参考资料 前端面试宝典
首屏渲染优化
代码层面
- 组件库的按需引入
- 路由的异步加载
- 使用服务端渲染SSR,直接由服务端返回渲染好的HTML
- 事件防抖节流,减少事件处理次数
- 减少DOM层级
- 减少DOM渲染次数(批量渲染documengFragment,差量渲染Vue/React)
通信层面
- 缓存网路数据在localStorage里或vuex/redux里,减少请求次数
- 小图片直接使用base64以减少网络请求次数
- 使用HTTP协议的强缓,服务端给更新频率低的数据一个较长的缓存时间(响应头Cache-Control:maxAge(3600247))
- 从CDN或静态资源服务器去获取图片、js、css、图标字体等静态资源
打包层面
- 对图片进行物理压缩,如在线压缩工具TinyPNG
- 打包时对HTML,CSS,JS进行压缩处理
- 开启Gzip压缩;
- 合理分包,将第三方包、轮子包、业务代码包分开,这样在前端版本升级时,往往还需要更新业务代码包,而第三方包和轮子包由于内容未变导致hash未变,因此直接被服务端判断为304,从而直接使用本地缓存;
- 打包时开启Tree-Shaking,摇掉无用代码,减少包体积;
- 从HTML中抽离CSS文件,以便单独下载与缓存;
小程序的微信授权流程(用微信账号登录自家服务器)
- 在小程序端wx.login()得到登录码code
- 小程序请求自家的登录接口,携带登录码code
- 自家服务器请求微信服务器,使用appid+appSecret+code换回session_key与openid
- 自家服务器将session_key与openid重新换算为自家的登录信息(如token)
- 小程序在后续请求自家服务器的过程中都携带该token,自家服务器就知道小程序端已经登录过了
参考链接
Set与Map
- ES6的新增数据结构
- Set用于基础数据的去重,一些核心API如下:
// 对数组去重
cosnt mySet = new Set([3,1,4,1,5,9,2,6,5,3,5,8])
// 转化回数组
const norepeatArr = [...mySet]
mySet.add(value) //添加元素
mySet.has(value) //判断某元素是否存在
mySet.size //元素数量
- Map用于键值对的便捷操作与查询,底层就是Object
const contacts = new Map()
//设置或覆盖key-value
contacts.set(‘Jessie’, {phone: “213-555-1234”, address: “123 N 1st Ave”})
contacts.has(‘Jessie’) //查询key是否存在
contacts.get(‘Hilary’) //查询key对应的value
contacts.delete(‘Raymond’) //删除key-value
contracts.size //查询键值对的数量
Promise与asycn,await的区别
- await是promise.then的语法糖
//ajaxApi(url,parmas)的返回值是promise对象
//阻塞等待promise对象resolve数据出来
const data = await ajaxApi(url,parmas)
- await关键字所在的函数必须声明为异步函数
async function fn(){//内含await}
async ()=>{//内含await}
const fn = async ()=>{//内含await}
- promise.catch由await所在的代码块进行try-catch
async fn(){
try{
// ajaxApi(url,parmas)的返回值是promise对象
// 如果promise毁约(reject),则错误信息由try-catch的catch获得
const data = await ajaxApi(url,parmas)
}catch(err){
//此处的err即promise所reject出的错误信息
hanleErr(err)
}
}
HOC与RenderProps
答题要点
- 两者都是React复用逻辑的主要方式,另外个主要方式是大名鼎鼎的天王【自定义Hook】
- HOC的逻辑是子组件进去,父组件出来,父组件在直接渲染子组件之余,为子组件注入了新的props、新的生命周期、新的DOM事件监听、新的副作用…这一大堆东西即你想复用的数据与逻辑
- RenderProps的逻辑是组件进去,怀了孕(Baby)的组件出来,Baby组件自带响应式数据与变化逻辑,即你想复用的数据与逻辑
- 案例:以【实时显式鼠标移动位置】这一复用逻辑为例:
HOC案例
/* 子组件App进去 增强型的父组件XApp出来 */
const withMouse = (App) => {
class XApp extends Component {
constructor(props) {
super(props);
// 为子组件注入响应式数据x,y
this.state = {
x: 0,
y: 0,
};
}
/* 鼠标移动动态修改状态x,y的值 */
onMouseMove = (e) => {
this.setState(() => ({
x: e.pageX,
y: e.pageY,
}));
};
render() {
// 在此以外做的一切事情都是增强
// return <App {...this.props}></App>
return (
// 给子组件的顶层DOM添加鼠标移动监听器
<div onMouseMove={this.onMouseMove}>
{/* 为子组件注入响应式数据x,y */}
{/* <Com name={props.name} x={this.state.x} y={this.state.y} /> */}
<App {...this.props} {...this.state} />
</div>
);
}
}
// 普通的App进来 增强型App出去
// 增强1:在顶层DOM身上绑定了鼠标移动事件监听器
// 增强2:注入了响应式数据x,y即鼠标实时移动到的位置
return XApp;
};
RenderProps案例
import React, { Component } from 'react'
/* 对鼠标移动的state与事件的封装提取 */
class Mouse extends Component {
constructor(props) {
super(props);
// 老鼠baby自带响应式数据
this.state = {
x: 0,
y: 0
}
}
/* 在DOM事件中改变响应式数据 */
onMouseMove = (e) => {
this.setState({
x: e.pageX,
y: e.pageY
})
}
render() {
return (
// 老鼠baby自带DOM事件监听——用以动态改变响应式数据
<div onMouseMove={this.onMouseMove}>
{/* 拿出我妈给我的renderFn(x,y)执行得到DOM */}
{this.props.renderFn(this.state.x, this.state.y)}
</div>
)
}
}
class App extends Component {
render() {
return (
<div>
{this.props.name}
<br />
{/* 将一只老鼠子组件放在此处 即相当于把【实时显式鼠标移动位置】放在这 */}
{/* <div style={{ height: "1000px", backgroundColor: "#eee" }}>鼠标实时位置:{x},{y}</div> */}
<Mouse
// 告诉我的baby拿到响应式数据x,y后如何渲染JSX
renderFn={
(x, y) => <div style={{ height: "1000px", backgroundColor: "#eee" }}>鼠标实时位置:{x},{y}</div>
}
/>
</div>
)
}
}
export default App
前端权限控制
- 登录成功后,服务端返回该用户的角色/权限;
- 可以将该角色/权限等级数据存储在全局(例如全局状态管理);
- 路由层面A计划-动态生成路由表:根据该用户的等级动态添加它有权访问的路由(一旦用户访问自己无权访问的路由时命中404);
- 路由层面B计划-路由守卫:使用路由守卫,当用户访问越权的路由时一脚踹到登录页;
- 界面层面A计划-条件渲染:根据用户的权限条件渲染它能访问的Link,组件、具体元素等;
- 界面层面B计划-封装:
- Vue中可以自定义指令如
v-auth="3"
当用户的等级不足3时将该按钮disable掉或隐藏掉; - React中可以使用HOC实现例如
WithCustomAuth(MyButton,3)
在返回JSX的时候使用条件渲染
function WithCustomAuth(Com,level){
function Parent(props){
reurn ({
authFromRedux >= level
? <Com {...props} />
: <a href="/login">登录</a>
})
}
return Parent
}
Websocket与HTTP有啥区别
- Websocket基于TCP协议,工作在传输层,长连接双工通信;
- HTTP是应用层协议,单向短链接,请求-响应-断开;
- Websocket已为大多数浏览器所支持,场景如:客服、秒杀、实盘、直播…
案例
服务端
var app = require('express')();
var server = require('http').Server(app);
var WebSocket = require('ws');
// 服务端监socket监听在8080 等待客户端连接
var wss = new WebSocket.Server({ port: 8080 });
// 准备客户端存储对象
const clients = {}
/* 有客户端请求接入 入参ws即为接入的客户端socket对象 */
wss.on('connection', function connection(ws) {
console.log('server: receive connection from client');
// 将该客户端存下来 时间戳:socket
clients[Date.now()] = ws
console.log("clients",Object.keys(clients));
// 接收到该客户端发来的消息
ws.on('message', function incoming(message) {
console.log('message',message);
ws.send('got it:'+message);
// 转发给它想要发送的其它客户端
const [to,msg] = message.toString().split(":")
clients[to*1].send(message.toString())
});
// 该客户端主动断开连接
ws.on("close",function(code,reason){
console.log("客户端已断开连接");
})
// 与该客户端的连接发送错误
ws.on("error",function(err){
//处理错误
})
// 立刻给刚刚接入的客户端发送消息
ws.send('welcome!');
});
/* 在http的3000端口返回页面给客户端 */
app.get('/', function (req, res) {
res.sendfile(__dirname + '/index.html');
});
app.listen(3000);
客户端
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<input type="text" placeholder="please enter..." id="ipMsg">
<button id="btn">发送消息</button>
<button id="btnClose">断开连接</button>
<script>
// 发起远程连接请求websocket长连接
var ws = new WebSocket('ws://localhost:8080');
// 连接成功时打印一下
ws.onopen = function () {
console.log('ws onopen');
// ws.send('from client: hello');
};
// 收到服务端的消息时打印一下
ws.onmessage = function (e) {
console.log('ws onmessage');
console.log('from server: ' + e.data);
};
// 点击按钮给服务端发送消息
btn.onclick = function(e){
ws.send(ipMsg.value);
}
// 点击按钮断开与服务端的连接
btnClose.onclick = function(e){
ws.close()
console.log("连接已断开");
}
</script>
</body>
</html>
react中怎么在父组件里使用子组件的函数
- 父组件给子组件一个callback,子组件在使用该callback时入参一个自己的函数,让父组件去调用
PS:纯脑洞问题 没有普适性
function Parent(props){
const callMe = (sonFn)=>sonFn()
return (
<>
<Son callback={callMe}>
</>
)
}
function Son({callback}){
const sonFn = (sonFn)=>alert('老爸调用到我了')
return (
<>
<button onClick={callback(sonFn)}>让父组件调用我的函数</button>
</>
)
}
Map与Object的区别
- 本质上都是key-value管理数据
- Map是ES6新增的数据结构,主要就是在Object的基础上封装除了几个便捷API,以做到和其它语言接轨(很多其它语言中都有Map这种数据结构)
- 这些便捷API包括
map.set(key,value)
,let value = map.get(key)
,map.delete(key)
,map.size
等 - 一个简单的MyMap封装
class MyMap {
constructor(){
this.dataObj = {}
this.size = 0
}
set(key,value){
if(!this.dataObj.hasOwnProperty(key)){
this.size ++
}
this.dataObj[key] = value
}
get(key){
return this.dataObj[key]
}
delete(key){
delete this.dataObj[key]
this.size --
}
}
const mm = new MyMap()
mm.set("name","OOP小菜鸡")
console.log(mm.get("name"));
console.log(mm);
按需加载 VS 异步加载 VS TreeShakig VS Webpack分包
- 按需加载主要指代码层面不引入不需要的东西,典型场景是组件库中只引入自己需要的组件
import {Button} from "antd"
- 异步组件,什么时候访问,就什么时候加载,否则不加载
//同步引入 编译时引入 无论用户访问不访问都引入
//import Home from "@/views/Home.vue"
//异步加载 运行时加载 用户访问该页面时才临时去加载
// 注释部分代表Webpack打包时将该组件打包为about.js这样一个异步chunk
const About = ()=>import(/* webpackChunkName: "about" */,"@/views/About.vue")
- TreeShaking:Webpack打包时自动将引而未用的内容排除在外,启动方式
mode:production
- Wepack分包的核心目的是为了最大化地使用包的缓存,例如将常用的第三方包打在vendor.hash.js中,自己的轮子代码打在common.hash.js中,将自身的业务逻辑打在main.hash.js中,当项目需要升级时,如果只改动了业务代码而没有触及轮子与第三方包,则vendor.hash.js与common.hash.js这两个chunk的hash是不变的,HTTP的缓存机制自动生效,用户端只需要更新main.hash.js这一个包即可;
递归组件
- 自己作为自己的子组件,例如:无穷子菜单,博文的无穷回复;
- 核心逻辑,以无穷回复为例:
- 每篇博文有它的回复数据,假设叫replies
- 组件CommentItem正常渲染出每个回复item的用户名、回复时间
- 每个回复的item依然有它的子回复replies
- 将replies中的每一项,继续映射为一个新的CommentItem
function CommentItem({item}){
return <>
<h3>item.username</h3>
<span>item.date</span>
{/*渲染item的子回复replies*/}
{
item.replies.map(
it=><CommentItem item={it}/>
)
}
</>
}
[参考链接] (https://juejin.cn/post/7087904975400992798)
JWT-token的原理
md5消息摘要算法保护用户的信息
md5消息摘要算法的基本原理
- md5是消息摘要(digest)算法;
- 无论【信息】多长或多短,一律生成一个不可逆的32位字符串;
- 只要【信息】变化一点点,生成的消息摘要都会大相径庭;
- 消息摘要算法用于校验数据的完整性(即原始数据是否被人篡改过);
- 其它知名的消息摘要算法还有SHA256,原理相同,生成的消息摘要更长;
以保护用户的密码为例
- 用户注册时填入的密码以消息摘要的形式存储在数据库中;
- 用户登录时填写的登录密码,在服务端生成消息摘要,再与数据库中的密码消息摘要做比对;
- 服务端在校验密码时生成消息摘要的过程,只要程序员不偷偷记录和转移数据,则理论上用户信息是“安全”的;
“这不需要测试,肯定是好的,不用担心”