前端技术面试核心问题(持续更新)

发布于:2023-01-04 ⋅ 阅读:(516) ⋅ 点赞:(0)

参考资料 前端面试宝典
在这里插入图片描述

首屏渲染优化

代码层面

  • 组件库的按需引入
  • 路由的异步加载
  • 使用服务端渲染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的原理

# jwt鉴权机制

md5消息摘要算法保护用户的信息

md5消息摘要算法的基本原理

  • md5是消息摘要(digest)算法;
  • 无论【信息】多长或多短,一律生成一个不可逆的32位字符串;
  • 只要【信息】变化一点点,生成的消息摘要都会大相径庭;
  • 消息摘要算法用于校验数据的完整性(即原始数据是否被人篡改过);
  • 其它知名的消息摘要算法还有SHA256,原理相同,生成的消息摘要更长;

以保护用户的密码为例

  • 用户注册时填入的密码以消息摘要的形式存储在数据库中;
  • 用户登录时填写的登录密码,在服务端生成消息摘要,再与数据库中的密码消息摘要做比对;
  • 服务端在校验密码时生成消息摘要的过程,只要程序员不偷偷记录和转移数据,则理论上用户信息是“安全”的;

“这不需要测试,肯定是好的,不用担心”

在这里插入图片描述