2023前端面试系列—面试汇总篇
HTML
1. HTML5 新特性
- 新的语义化元素:main、footer、header、nav、section
- 新的表单控件:date、time、email、url、search
- 新的 API:
- 音视频:audio 和 video 元素
- 绘图图形:canvas 元素
- 本地存储:localStorage,sessionStorage
- 多线程操作:Web Worker
引申的内容:
- 语义化:是什么?有什么用?
- 有利于 SEO,代码更加规范和可读
- SEO 是搜索引擎优化:提升网站在搜索引擎排名
- 可以通过 meta 标签设置 description / keyword 等值,一般值由产品需求提供
- 可以通过服务端渲染技术 SSR (react - next / vue - nuxt)
- canvas
- 概念:画布,用来绘制图形图表
- 项目中主要用来完成数据可视化功能:比如数据大屏展示/适配/实时更新等
- localStorage / sessionStorage
- 区别
- localStorage 永久存储,通常用来存储用户的登录唯一标识 token
- sessionStorage 临时会话存储(关闭浏览器会自动清空内容)
- 扩展其他存储方案
- cookie
- 储存体积小(4KB)
- 发送请求,cookie 会自动携带
- webStorage
- 储存体积更大(2mb)
- 必须手动携带(封装 axios 中请求拦截器在请求头中携带 token 参数)
- cookie
- WebWorker
- 多线程操作,可以用于项目性能优化:比如将计算量大的任务交给 web worker 处理(大文件上传)
2. 常见的行内元素和块级元素有哪些?它们的区别?
- 行内元素 span i a
- 块级元素 div p ul/li header/footer form
- 行内块元素 img input
区别:
- 行内和行内块元素一行多个,块级元素独占一行
- 行内元素不能设置 width/height,行内块和块级元素可以设置
3. 谈谈 iframe
-
概念:能在当前页面嵌套其他页面(能在当前应用嵌套其他子应用)
-
问题
- 同源情况下, 可以通过 iframe 标签直接获取子页面的数据(比如,iframe.documentWindow 获取到 window 对象)
- 不同源情况下,postMessage 方法 和 message 事件的方式进行通信
- postMessage 方法,用来发送数据
- message 事件,用来接受数据
- 引申
公司有多个项目,需要汇总到一个项目中一起使用,简单实现可以用 iframe 标签,也可以使用前端微应用框架:qiankun / micro 来实现
CSS
1. CSS3 新特性
- 新增了选择器
- :last-child 匹配父元素的最后一个子元素
- :nth-child(n) 匹配父元素的第 n 个子元素
- 边框特性
- border-radius 圆角
- 颜色与不透明度
- opacity: 0.5;
- color: rgba(0, 0, 0, 0.5)
- 阴影
- text-shadow 文字阴影
- box-shadow 盒子阴影
- transform 变形
- transform: rotate(9deg) 旋转
- transform: scale(0.5) 缩放
- transform: translate(100px, 100px) 位移
- 过渡与动画
-
transition 过渡
-
animation 动画
Vue 中 transition 组件,用来给显示隐藏切换的元素一个动画/过渡效果
https://gitee.com/xxpromise/class220829/blob/master/03.vue3-vite/src-16-vue-router/App.vue
- 媒体查询
- @media 用来做响应式布局
至少说 4 个点
2. 盒模型
-
概念:页面渲染时,DOM 元素所采用的布局模型。 可通过 box-sizing 进行设置。
-
分类:
- content-box (W3C 标准盒模型)
当给元素设置 width 和 height 时,只会改变 width + height。 - border-box (IE 盒模型)
当给元素设置 width 和 height 时,会改变 width + height + padding。 - 其他未实现了
3. BFC
- 概念
- BFC,又称为块级格式化上下文,指的是:一个独立的渲染区域.
- 触发条件(开启 BFC)
- 设置 overflow,即 hidden,auto,scroll
- 设置浮动,不包括 none
- 设置定位,absolute 或者 fixed 等方式
- 具体规则
- BFC 是一个块级元素,块级元素在垂直方向上依次排列。
- BFC 是一个独立的容器,内部元素不会影响容器外部的元素。
- 属于同一个 BFC 的两个盒子,外边距 margin 会发生重叠,并且取最大外边距。
- 计算 BFC 高度时,浮动子元素也要参与计算。
- 应用
- 阻止 margin 重叠
- 清除浮动,防止高度塌陷
- 阻止标准流元素被浮动元素覆盖
4. 选择器权重&优先级
!important > 行内样式 > #id > .class > tag > * > 继承 > 默认
- CSS 选择器浏览器是 从右往左 依次解析
5. CSS 预处理器(Sass/Less/Stylus)
-
概念:CSS 预处理器定义了一种新的语言,可以更方便的维护和管理 CSS 代码
-
比如我最常用的 sass
- 可以使用
$
来定义变量 - 可以样式嵌套
- 可以通过
mixin
来定义混合,复用代码
- Vue 项目中使用
- 通过安装
sass sass-loader
依赖就可以直接使用 sass 了 - 开发中我会定义全局变量文件,那么这个文件每个 sass 使用都要引入,太麻烦了。所以我通常在项目中会进行脚手架配置,scss - additionalData 让其给每个 sass 文件自动追加引入全局变量文件的代码,从而让其全局生效
- 扩展
- 现阶段样式变量 css 也支持了
- 通过
:root{ --xxx-xx: xxx }
定义 - 通过
var(--xxx-xx)
使用
- 通过
6. flex 布局
- 概念
Flex 是 Flexible Box 的缩写,意为"弹性布局",用来为盒状模型提供最大的灵活性。
采用 Flex 布局的元素,称为 Flex 容器(flex container),简称"容器"。它的所有子元素自动成为容器成员,称为 Flex 项目(flex item),简称"项目"。
容器默认存在两根轴:主轴和交叉轴(也叫做侧轴)。默认水平方向的为主轴,垂直方向为侧轴。
- 容器的属性(2-3 个)
- flex-direction 定义主轴的方向
- flex-wrap 定义是否换行
- flex-flow 是 flex-direction 属性和 flex-wrap 属性的简写形式
- justify-content 定义项目在主轴上的对齐方式
- align-items 定义项目在侧轴上的对齐方式
- align-content 定义项目在交叉轴上如何对齐
- 项目的属性(2-3 个)
- order 定义项目的排列顺序。数值越小,排列越靠前,默认为 0。
- flex-grow 定义项目的放大比例,默认为 0,即如果存在剩余空间,也不放大。
- flex-shrink 定义了项目的缩小比例,默认为 1,即如果空间不足,该项目将缩小。
- flex-basis 定义了在分配多余空间之前,项目占据的主轴空间。它的默认值为 auto,即项目的本来大小。
- flex 是 flex-grow, flex-shrink 和 flex-basis 的简写,默认值为 0 1 auto。
- align-self 允许单个项目有与其他项目不一样的对齐方式,可覆盖 align-items 属性。
- 扩展:flex: 1 啥意思
- flex-grow: 1 如果存在剩余空间, 该项目会放大。
- flex-shrink: 1 如果剩余空间不足,该项目会缩小。
- flex-basis: 0% 设置为 0% 之后,即不占据主轴空间,但是因为有 flex-grow 和 flex-shrink 的设置,该项目会自动放大或缩小。
7. 实现两栏布局
1 | .container { |
8. 隐藏页面元素方式
- display: none 不占位。不会响应 DOM 事件。
- opacity: 0 占位,但不可见。会响应 DOM 事件。
- visibility: hidden 占位,但不可见。不会响应 DOM 事件。
- position: absolute; left: -10000px 移动到屏幕外
- z-index: -1 将别的定位元素遮盖掉当前元素
9. 让元素水平垂直居中方式
- 利用绝对定位, 子元素未知宽高
1 | .father { |
- 利用绝对定位, 子元素未知宽高
1 | .father { |
- 利用绝对定位, 子元素必须明确宽高
1 | .father { |
- 利用 flex
1 | .father { |
JavaScript
1. 说说 JS 的数据类型
- 基本数据类型
- number
- string
- boolean
- null
- undefined
- symbol
- 用来给变量唯一的值
- 用来给对象唯一的属性名(对象属性名的类型只能是:string,symbol)
- bigint
- 当 number 类型计算超过最大数(Number.MAX_SALE_NUMBER)或小于最小值,计算就会出现问题,这个时候可以用 bigint 解决(实际开发很少见)
- 引用数据类型
- object
- function
- array
2. 如何判断 JS 数据类型
- typeof
大部分类型都能检查出来,但是对于 null\object\array 无法区分
- A instanceof B
简单理解:检查 A 是否是 B 的实例
真正理解:检查 A 的某一个__proto__
是否和 B.prototype 指向同一个对象
主要用来检测引用数据类型
- Object.prototype.toString.call(xxx).slice(8, -1)
完美解决方案,可以检测所有数据类型,开发中使用 toString 方法去封装工具函数去检测类型
- Array.isArray
检测是否是数组类型
- A === B
检测 A 和 B 的值和类型都要相等
3. 说说常见的数组方法
- 更新数组的方法
- push 给数组最后添加一个元素
- pop 删除数组最后一个元素
- unshift 给数组最前面添加一个元素
- shift 删除数组最前面一个元素
- splice 删除/新增指定下标元素
- sort 排序
- reverse 反转
- 遍历元素的方法
- forEach 遍历
- map 返回一个新数组,新数组长度和原数组一致,但内部的值往往会发生变化。(长度不变,值变)
- 在 React 中更新数据中某个值,往往使用 map 方法。
- filter 返回一个新数组,新数组长度往往比原数组更少,但内部的值和原数组一致。(长度变,值不变)
- 在 React 中删除数据中某个值,往往使用 filter 方法。
- reduce 常用于统计、累加和求和等功能。
- 购物车模块,计算总价。
- find 查找某个元素,找到返回这个元素,找不到返回 undefined。
- findIndex 查找某个元素的下标,找到返回这个元素下标,找不到返回 -1。
- every 所有返回 true 整体才返回 true,只要有一个返回 false,整体就返回 false。
- some 只要有一个返回 true,整体就返回 true,只有全部返回 false,整体才返回 false。
- 其它方法
- slice 截取数组中某些元素
- concat 拼接数组
- join 将数组内部元素以某种方式拼接成字符串
- includes 判断是否包含某个元素,包含返回 true,不包含返回 false
- indexOf 判断是否包含某个元素,包含返回其下标,不包含返回-1
至少能说个 6-7,并且相对复杂的方法
4. 常见的 DOM 操作有哪些
- 新增 DOM 元素
- document.createElement() 创建 DOM 元素
- xxxDom.appendChild() 将某个 dom 元素插入到 xxxDom 内
- 删除 DOM 元素
- xxxDom.removeChild() 将 xxxDom 下面某个子元素删除
- xxxDom.remove()将 xxxDom 自身删除
- 修改 DOM 元素
- xxxDom.innerText / xxxDom.textContent 设置元素的文本内容
- xxxDom.innerHTML 设置元素的 html 内容
- 获取/查询 DOM 元素
- document.getElementById() 根据 id 选择器获取某个 DOM 元素
- document.querySelector()根据任意选择器获取找到的第一个 DOM 元素
- document.querySelectorAll() 根据任意选择器获取找到的所有 DOM 元素集合
5. 说说你对闭包的理解
- 概念
通过 chrome 开发者调试工具得知: 闭包本质是内部函数中的一个容器(非 js 对象), 这个容器中包含了引用的局部变量
- 产生原因
- 函数嵌套
- 内部函数引用外部函数的局部变量
- 调用外部函数时就会产生闭包
- 闭包的生命周期
-
产生:当内部函数创建时
-
死亡:当内部函数没有变量引用,成为垃圾对象,就会自动被 GC(垃圾回收机制)自动回收
- 闭包作用:
- 延长局部变量生命周期(活的久点)
- 让函数外部可以间接操作函数内部的局部变量数据
- 闭包在项目中的应用
- React 中复用函数的手段,高阶函数形式
- 实际开发中很少使用闭包,我一般研究源码时发现闭包的场景,vue2 响应式原理中数据劫持阶段,内部通过闭包形式保存 dep 对象,这个 dep 对象在 vue2 响应式原理非常重要
6. 谈谈 this 指向
普通情况下,this 指向看函数的调用方式:
-
直接调用(默认绑定),this 指向 window, 严格模式(‘use strict’)下指向 undefined
-
对象调用函数(隐式绑定),this 指向调用的对象
-
call/apply/bind 调用函数(显示绑定),this 指向传入第一个参数
- call/apply/bind 区别和联系:
- call/apply 都会立即执行函数,bind 返回一个新函数
- call/apply 执行函数时函数的 this 指向第一个参数,bind 方法返回的新函数的 this 指向第一个参数,原函数不变
- call/bind 方法传参是一致的, 可以 n 个参数, apply 只能两个参数,第二个参数是数组
- new 调用函数,this 指向生成的实例对象
特殊情况:
-
箭头函数:this 指向包裹它离它最近的函数的 this(指向外部函数的 this)
-
回调函数:
- 定时器回调函数:window,严格模式下 undefined
- DOM 事件回调函数:指向绑定的事件的 DOM 元素
- Vue 生命周期函数 / methods 中的函数 / 微信小程序生命周期函数:指向组件实例对象
7. 说说原型与原型链
- 原型
- 我们说的原型,指的是两个原型属性:
__proto__
和 prototype - prototype 叫做显示原型属性
__proto__
叫做隐式原型属性- 每个函数都有一个显式原型属性,它的值是一个对象,我们叫做原型对象。
- 这个原型对象上默认会有一个 constructor 方法,指向函数本身,有一个
__proto__
属性,指向 Object 的原型对象 - 每个实例都有一个隐式原型属性,它的值指向其对应构造函数的原型对象。
特殊情况:
Function.prototype === Function.__proto__
他们指向同一个对象Object.prototype.__proto__ === null
这里是原型链的尽头
- 原型链
- 概念:从对象的
__proto__
开始, 连接的所有对象, 这个结构叫做原型链,也可称为“隐式原型链” - 作用:用来查找对象的属性
- 规则:在查找对象属性或调用对象方法时,会先在对象自身上查找, 找不到就会沿着原型链查找,找到就返回属性的值,最终来到
Object.prototype.__proto__
,找不到返回 undefined - 应用:利用原型链可以实现继承
- Vue 中全局事件总线 $bus
- 项目中
$api / $http
汇总所有接口函数
8. 说说 JS 的垃圾回收机制
在 JS 中对象的释放(回收)是靠浏览器中的垃圾回收器来回收处理的
- 垃圾回调器
- 浏览器中有个专门的线程, 它每隔很短的时间就会运行一次
- 主要工作:判断一个对象是否是垃圾对象,如果是,清除其内存数据,并标记内存是空闲状态
- 垃圾回收策略
- 机制 1:标记清除法
- 简述:标记阶段即为所有活动对象做上标记,清除阶段则把没有标记(也就是非活动对象)销毁。
- 缺点:
- 内存碎片化,空闲内存块是不连续的,容易出现很多空闲内存块,还可能会出现分配所需内存过大的对象时找不到合适的块
- 分配速度慢,因为即便是使用 First-fit 策略,其操作仍是一个 O(n) 的操作,最坏情况是每次都要遍历到最后,同时因为碎片化,大对象的分配效率会更慢。
- 解决:可以使用 标记整理(Mark-Compact)算法,标记结束后,标记整理算法会将活着的对象(即不需要清理的对象)向内存的一端移动,最后清理掉边界的内存
至少说上面内容
- 机制 2:引用计数法
- 简述:它把对象是否不再需要简化定义为对象有没有其他对象引用到它。如果没有引用指向该对象(引用计数为 0),对象将被垃圾回收机制回收。
- 缺点:
- 需要一个计数器,所占内存空间大,因为我们也不知道被引用数量的上限。
- 解决不了循环引用导致的无法回收问题。
- JS 的 V8 引擎垃圾回收机制使用的是基于标记清除算法,不过对其做了一些优化。
- 针对新生区采用并行回收。
- 针对老生区采用增量标记与惰性回收。
9. 说一下 JS 的事件循环机制
-
概念:异步代码执行机制
-
流程:
- js 主线程会依次执行所有代码,遇到同步代码依次执行,遇到异步代码会交给相应管理模块(分线程)去处理:
- 比如遇到定时器,会交给定时器管理模块管理,它会计时,到点时会自动将回调函数添加到回调队列中(宏任务队列)
- 比如遇到 DOM 事件,会交给 DOM 管理模块管理,它会绑定事件,当触发事件时会自动将回调函数添加到回调队列中(宏任务队列)
- 比如遇到 ajax 请求,会交给 ajax 管理模块管理,它会发送请求,等响应回来时会自动将回调函数添加到回调队列中(宏任务队列)
- js 主线程并不会等,会依次执行后面的代码,直到所有代码执行完毕,js 主线程就会开启事件轮询
- js 主线程会遍历回调队列中回调函数,依次取出,同步执行
- 回调队列分为两种队列:宏任务队列和微任务队列
- 宏任务队列:定时器回调函数、ajax 回调函数、DOM 事件回调函数
- 微任务队列:Promise.then/catch/finally、mutationObserver(nextTick 方法的原理)
- 优先执行微任务队列中的回调函数,直到全部执行完毕,
- 在执行宏任务队列中第一个回调函数,执行完又会执行,微任务队列中的回调函数,直到全部执行完毕,
- 在执行下一个宏任务队列中回调函数,执行完又会执行,微任务队列中的回调函数,直到全部执行完毕,以此类推
10. Web Worker
- 概念
Web Worker 是 H5 新特性,允许我们开辟分线程,运行 js 代码。
- 使用
const worker = new Worker('./xxx.js')
创建分线程执行 js 脚本- 主线程通过
worker.onmessage
事件接受分线程的消息 - 主线程通过
worker.postMessage
方法向分线程发送消息 - 分线程通过
self.onmessage
事件接受主线程的消息 - 分线程通过
self.postMessage
方法向主线程发送消息
- 应用
主要用于 js 中大量计算工作,比如大文件上传中计算文件的 hash,使用了 web worker
11. 说出 ES6 常用新语法
- 简单的语法
- const 与 let
- 解构赋值
- 形参默认值
- 扩展运算符: …
- 模板字符串
- 对象的属性与方法简写
- 模块化语法
- 比较复杂的语法
- 箭头函数
- 编码简洁
- 不能通过 new 来创建实例对象,没有 constructor 方法
- 内部没有 arguments, 可以通过 rest 参数来代替
- 没有自己的 this, 使用外部作用域中的 this, 不能通过 bind 来绑定 this
- 说到 this 指向,平时我专门总结过 this 指向
- …
- 还有 class 与 extends
- 主要用来做继承的,是构造函数+原型的语法糖
- 平时在项目主要还是通过原型的方式去继承:比如$bus
- promise / generator / async & await(引申 js 事件循环机制)
- promise…(细说 promise)
- promise 并不是解决回调地狱的最佳方案,后面推出了 generator
- generator 惰性函数(懒),暂停 yield 执行 next,但是使用太复杂了,后面又推出了 async 函数
- async、await、promise 一起使用,是项目中解决回调地狱的最佳方案
- Proxy(引申 vue2 和 vue3 响应式原理)
- 可以对对象进行代理,当读取/设置对象上的属性时,会执行相应的 get/Set
- 这个内容主要在 Vue 响应式原理中使用了,Vue3 响应式通过 Proxy 实现的,原有和新增属性都是响应式。
- Map / Set / WeakMap / WeakSet (引申 vue3 响应式原理)
- 他们都是新的存储数据的结构
- Map 类似于对象,存储 key-value 的结构,特点:1. 有序 2. key 是任意类型
- Set 类似于数组,特点:1. 值是唯一的
- WeakMap 类似于 Map,存储 key-value 的结构,特点:1. 有序 2. key 必须是对象类型 3. 一旦 key 没有外部引用,会自动删除
- WeakSet 类似于 Set,特点:1. 值是唯一的 2. 值必须是对象类型 3. 一旦值没有外部引用,会自动删除
12. 说说 ES6 的 promise
- 概念
异步代码解决方案,用于解决回调地狱问题。
- promise 对象内部有 3 种状态
- pending 初始化状态
- resolved / fulfilled 成功状态
- rejected 失败状态
状态只能变化一次,只能有以下两种变化:
- pending --> resolved
- pending --> rejected
状态发生变化后不能在改了。
- 如何改变 promise 的状态
- 调用 resolve(), 改成成功状态
- 调用 reject(), 改为失败状态
- throw new Error(), 改为失败状态
- promise 实例对象上的方法
- then 接受两个回调(一般只接受一个),第一个是成功回调,第二个是失败回调
- catch 接受一个回调,是失败回调
- finally 接受一个回调,不管成功/失败都会触发
- Promise 构造函数上的方法
- Promise.resolve() 返回一个成功的 promise 对象
- 也可能返回失败的 promise 对象,比如 Promise.resolve(Promise.reject())
- Promise.reject() 返回一个失败的 promise 对象
- Promise.all([promise1, promise2, …]) 只有所有 promise 成功才成功,只要有一个 promise 失败就会失败
- Promise.allSettled([promise1, promise2, …]) 只要所有 promise 状态发生变化就成功,结果值包含所有 promise 的结果值(不管成功/失败)
- Promise.race([promise1, promise2, …]) 只要有一个 promise 成功/失败,就成功/失败。
- 应用
- 在项目中一般是使用 axios 发送请求时会使用,返回值是一个 promise 对象,结合 async await 来处理
- 如果同时要发送多个请求的话,可以使用 Promise.all() 方法来处理
13. 谈谈模块化语法
- Commonjs 模块化语法
- 主要用于 NodeJS 端
- 语法:
- 引入:require
- 暴露:exports / module.exports
- ES6 模块化语法
-
主要用于浏览器端
-
语法:
-
引入:import
-
暴露:export
-
总结
- 如果模块采用默认暴露:import xxx from ‘xxx’
- 如果模块采用分别/统一暴露:
- 如果需要引入模块部分内容:import { xxx } from ‘xxx’
- 如果需要引入模块全部内容:
import * as xxx from 'xxx'
-
扩展
- 无论什么暴露方式,暴露的一定是一个对象
- 默认暴露:对象上添加一个 default 属性,值为暴露的内容
- 分别暴露/统一暴露:直接在对象上暴露内容
import { xxx } from './xxx'
引入暴露内容的某个属性(部分)import xxx from './xxx'
引入暴露内容的 default 属性(只要 default)import * as xxx from './xxx'
引入暴露内容的所有内容(全部)
- 无论什么暴露方式,暴露的一定是一个对象
14. 谈谈箭头函数
- 编码简洁
- 不能通过 new 来创建实例对象,没有 constructor 方法
- 内部没有 arguments, 可以通过 rest 参数来代替
- 没有自己的 this, 使用外部作用域中的 this, 不能通过 bind 来绑定 this
- 说到 this 指向,平时我专门总结过 this 指向
- …
15. 浅度克隆和深度克隆
- 浅度克隆
- object.assign()
- 扩展运算符: { …obj }
- Array.prototype.slice()
- Array.prototype.concat()
- 深度克隆
- JSON.parse(JSON.stringify())
- 自定义深度克隆
- lodash 中 cloneDeep
- 区别
- 浅度克隆: 对当前对象进行克隆,基本类型克隆生成新值,引用类型克隆的是地址值(所以当修改对象内部引用类型数据时,原对象也会发生变化)
- 深度克隆: 会完全复制整个对象,包括这个对象所包含的内部对象(所以不管如何修改对象数据,原对象都不会发生变化)。
- 深度克隆应用
权限管理功能中使用深度克隆克隆了异步路由表
16. 节流和防抖
TypeScript
1. JavaScript 和 TypeScript 的区别
- JavaScript 没有重载概念,TypeScript 有可以重载和接口类型合并
- TypeScript 扩展更多的类型: 接口 interface、泛型、enum、unknown、any、void、never 等;
- TypeScript 中引入了模块的概念,可以把声明、数据、函数和类封装在模块中;
2. interface 和 type 的区别
- interface 只能定义引用数据类型,不能定义基本数据类型。而 type 都可以;
- interface 定义对象类型不够灵活,type 都可以;
- type 声明的类型不能重名,interface 声明的重命名会声明合并;
- 两者继承的方法不同;
- interface 使用 extends 继承
- type 使用 & 关键字
总结: 一般开发中对象类型用 interface 定义,数组类型用 type
3. 泛型
- 概念
在接口、类、函数的时候,不预先定义类型,而是在使用时定义类型
- 泛型默认值
<T = string>
- 泛型约束
<T extends U>
-
泛型工具类型:Pick(挑选)、Omit(排除)、Partial(可选)、Required(必填)
-
泛型应用:
- axios:
request<any, Xxx>()
- react:
useState<Xxx>()
- vue3:
ref<Xxx>()
reactive<Xxx>()
Server
1. 谈谈 http 协议
- 概念
HTTP 是超文本传输协议(Hypertext Transfer Protocol),是浏览器与服务器通信的协议。
- 报文
协议通信的内容我们称为报文。
浏览器发送给服务器叫做请求报文,服务器返回给浏览器的叫做响应报文。
报文由报文首行、报文头部、空行、报文体组成。
- 请求报文
请求报文:请求首行、请求头、空行、请求体组成。
请求首行主要有:请求方式、请求地址、get 请求的查询字符串参数、协议和版本号
请求头主要有:
Connection: keep-alive
保持长链接, 能共享一个 TCP 链接通道来发送请求,通信效率更高Content-Type
请求体参数类型- 我们使用 axios 发送请求,一般是:application/json 格式
- 我们使用原生 form 表单发送请求,一般是:application/x-www-form-urlencoded 格式
User-Agent
用户代理, 可以识别用户的客户端环境Referer
请求来源地址, 防盗链Cookie / token
用户唯一标识
请求体主要有:POST 请求的参数
- 响应报文
响应报文:响应首行、响应头、空行、响应体组成。
响应首行主要有:协议和版本号、响应状态码
响应状态码分为 5 种类型:1xx、2xx、3xx、4xx、5xx(1 开头、2 开头、3 开头、4 开头、5 开头)
- 1xx 请求正在处理
- 2xx 请求处理成功
- 200 请求成功
- 3xx 请求需要进一步处理(请求重定向)
- 301 永久重定向
- 302 临时重定向
- 304 重定向到浏览器缓存中(协商缓存)
- 4xx 客户端错误
- 401 认证/授权失败
- 403 禁止访问
- 404 找不到资源
- 407 token 过期/失效
- 5xx 服务器错误
- 500 服务器内部错误
响应头:
- 强制缓存字段 Cache-Control / Expires
- 协商缓存字段 Etag / Last-Modified
- CORS 解决跨域字段:Access-Control-Allow-Origin
响应体:响应的具体数据
2. HTTP 1.1 和 HTTP 2.0 的区别
- 二进制格式(Binary Format)
- HTTP/1.1 通信内容可以是文本或二进制
- HTTP/2 通信内容都是二进制
- 多路复用(MultiPlexing)
- HTTP/1.1 长链接(connection: keep-alive)是串行的,后面的请求等待前面请求的返回才能发送
- HTTP/2 多路复用,一个连接里,客户端和服务器都可以同时发送多个请求或响应,而且不用按照顺序一一发送
- 头部压缩
- HTTP 1.1 通信的请求头内容太多了,多次通信字段重复
- HTTP/2 实现了头部压缩( gzip 或 compress ),体积更小,同时只会发送不同的字段,和相同字段的索引号,体积更小
- 服务器推送(server push)
- HTTP 1.1 协议不能服务器向浏览器发送消息,需要借助 ajax 轮询或 WebSocket 实现
- HTTP/2 允许服务器向浏览器主动发送消息
3. HTTP 和 HTTPS 协议的区别
- HTTP 协议是明文传输的,HTTPS 通过 SSL 加密传输的;
- 使用不同的连接方式,端口也不同,HTTP 协议默认端口是 80,HTTPS 协议默认端口是 443;
4. TCP 的三次握手和四次挥手
三次握手的目的:是为了确认双方的接收能力和发送能力是否正常。
第一次握手:由浏览器发送给服务器,服务器确定浏览器的发送能力
第二次握手:由服务器发送给浏览器,浏览器确定的服务器接受和发送能力
第三次握手:由浏览器发送给服务器,服务器确定的浏览器的接受能力
四次挥手的原因是因为 TCP 的连接是全双工的,所以需要双方分别释放到对方的连接,单独一方的连接释放,只代表不能再向对方发送数据,连接处于的是半释放的状态。(确保双方都接受完毕和发送完毕数据)
第一次挥手:由浏览器发送给服务器,告诉服务器数据发送完了
第二次挥手:由服务器发送给浏览器,告诉浏览器数据接受完了
第三次挥手:由服务器发送给浏览器,告诉浏览器数据发送完了
第四次挥手:由浏览器发送给服务器,告诉服务器数据接受完了,此时浏览器会自动断开链接,服务器也会
5. 当在浏览器输入一个地址,按下回车发生了什么
- 解析 URL
解析 URL 是否合法,合法进行下一步,不合法就直接出错。
- 缓存判断
判断该请求是否命中缓存,命中直接读取缓存,没有命中进行下一步
- DNS 解析
将域名地址解析为 ip 地址
流程:
- 首先读取 DNS 缓存:浏览器 DNS 缓存 - 计算机 DNS 缓存 - 路由器 DNS 缓存 - 网络运营商
- 然后 DNS 递归查询:根域名服务器 - 顶级域名服务器 - 权威域名服务器
- TCP 三次握手
为了确认双方的接收能力和发送能力是否正常。
第一次握手:由浏览器发送给服务器,服务器确定浏览器的发送能力
第二次握手:由服务器发送给浏览器,浏览器确定的服务器接受和发送能力
第三次握手:由浏览器发送给服务器,服务器确定的浏览器的接受能力
- 发送请求
将请求以请求报文形式发送给服务器。
请求报文由请求首行、请求头、空行、请求体组成。
请求首行主要有:请求方式、请求地址、get 请求的查询字符串参数、协议和版本号
请求头主要有:
Connection: keep-alive
保持长链接, 能共享一个 TCP 链接通道来发送请求,通信效率更高Content-Type
请求体参数类型- 我们使用 axios 发送请求,一般是:application/json 格式
- 我们使用原生 form 表单发送请求,一般是:application/x-www-form-urlencoded 格式
User-Agent
用户代理, 可以识别用户的客户端环境Referer
请求来源地址, 防盗链Cookie / token
用户唯一标识
请求体主要有:POST 请求的参数
报文内容简单说说也行,可以不用这么复杂
- 返回数据
服务器以响应报文形式返回响应
响应报文:响应首行、响应头、空行、响应体组成。
响应首行主要有:协议和版本号、响应状态码
响应状态码分为 5 种类型:1xx、2xx、3xx、4xx、5xx(1 开头、2 开头、3 开头、4 开头、5 开头)
- 1xx 请求正在处理
- 2xx 请求处理成功
- 200 请求成功
- 3xx 请求需要进一步处理(请求重定向)
- 301 永久重定向
- 302 临时重定向
- 304 重定向到浏览器缓存中(协商缓存)
- 4xx 客户端错误
- 401 认证/授权失败
- 403 禁止访问
- 404 找不到资源
- 407 token 过期/失效
- 5xx 服务器错误
- 500 服务器内部错误
响应头:
- 强制缓存字段 Cache-Control / Expires
- 协商缓存字段 Etag / Last-Modified
- CORS 解决跨域字段:Access-Control-Allow-Origin
响应体:响应的具体数据
报文内容简单说说也行,可以不用这么复杂
- 页面渲染
- 将 DOM 结构解析成 DOM 树
- 将 CSS 样式解析成 CSSOM 树
- 将 DOM 树和 CSSOM 树合成渲染(render)树
- 根据渲染(render)树首先进行布局,然后进行渲染
- 整个过程会触发多次渲染,也就是说构建了部分 DOM 树和 CSSOM 树就会开始合成渲染了
- 如果遇到 script 标签会同步执行里面的 js 代码,会阻塞渲染
- 如果 script 标签 js 操作了 DOM 或 CSS,重新生成 DOM 树和 CSSOM 树,合成渲染树,布局(重排),渲染(重绘)
- TCP 四次挥手
四次挥手的原因是因为 TCP 的连接是全双工的,所以需要双方分别释放到对方的连接,单独一方的连接释放,只代表不能再向对方发送数据,连接处于的是半释放的状态。(确保双方都接受完毕和发送完毕数据)
第一次挥手:由浏览器发送给服务器,告诉服务器数据发送完了
第二次挥手:由服务器发送给浏览器,告诉浏览器数据接受完了
第三次挥手:由服务器发送给浏览器,告诉浏览器数据发送完了
第四次挥手:由浏览器发送给服务器,告诉服务器数据接受完了,此时浏览器会自动断开链接,服务器也会
6. 强制缓存和协商缓存
强制缓存字段:Cache-Control(优先级更高) 、Expires
协商缓存字段:位于响应头的:Etag / Last-Modified 和位于请求头的 If-None-Match / If-Modified-Since
整体流程:
-
第一次请求该资源,没有缓存,服务器返回新资源,同时在响应头携带:强制缓存字段 Cache-Control 和 协商缓存字段:Etag / Last-Modified,此时响应状态码 200
-
第二次以后请求该资源,首先判断该资源强制缓存是否过期,没有过期,直接读取缓存,不会发送请求了,响应状态码 200
-
如果强制缓存过期了,就会发送请求给服务器,同时在请求头自动携带协商缓存字段: 此时 Etag 会改名为 If-None-Match, Last-Modified 会改名为 If-Modified-Since
-
由服务器检查和服务器的 Etag 和 Last-Modified 是否匹配
-
如果匹配没变,就返回响应状态码 304,浏览器接受到会自动去缓存中找该资源使用
-
如果变了,就会返回新的资源,同时在响应头携带新的:强制缓存字段 Cache-Control 和 协商缓存字段:Etag / Last-Modified,此时响应状态码 200
7. 谈谈 WebSocket
WebSocket 是一个全双工通讯协议。
特点:服务器可以向客户端主动推动消息,客户端也可以主动向服务器推送消息。
使用:
new WebSocket(url) 传入 url 就可以连接上 WebSocket 服务器了
通过 message:接受服务器发送的消息
通过 send 方法:可以给服务器发送消息
除此以外还有其他方法:close 关闭连接
其他事件:open:打开事件 error 错误事件 close 关闭事件
功能:WebSocket 连接时可能因为不好的网络环境会断开链接,此时可以使用心跳检测机制来进行断开重连
心跳检测:
web socket 在连接过程中可能会因为网络问题断开连接,我们需要重连,这里用到心跳检测机制
具体来说:
- 检测是否断开链接:
当我们连接上 ws 服务时,需要开启定时器,隔一段时间(10s - 30s)向 ws 服务器发送一个特殊信号(ping),同时开启定时器(2s)检测服务器是否有回应信号(pong)
如果在规定时间内有回应,继续此操作,如果没有回应,说明断开连接了,我们要开始重连 ws
- 重连机制
每隔一段时间发起 ws 的重连,同时重连次数增加,一旦重新连接成功,就归零重连次数,方便下次重连,如果重连次数超过最大次数,就要提示错误,不在尝试了
8. 谈谈 ajax
可以发送异步请求,进行局部页面更新
流程:
- new XMLHttpRequest() 来创建 xhr 对象
- 通过 xhr 调用 open 方法,设置请求方式和请求地址(请求地址可以添加查询字符串参数)
- 通过 xhr.send 方法发送请求(可以携带请求体参数)
- 通过 xhr.onreadystatechange 或 onload 事件监听响应回来的结果,往往需要判断 xhr 的响应状态码是否 2 开头,来判断请求成功还是失败
9. 跨域
-
什么是跨域?
违背同源策略产生跨域。 -
什么是同源策略?
同源指的是:协议、域名、端口号三者必须完全一致。 -
什么样的请求存在跨域问题?
只有浏览器端的 ajax 请求存在跨域。
服务器与服务器之间没有跨域,浏览器的 script、img、form 等标签都没有跨域。
- 解决跨域的方案:
- jsonp: 利用 script 标签可以跨域特性进行跨域。
可以说说具体做法
-
cors: 服务器设置特定响应头即可实现。access-control-allow-Origin
-
NodeJS 代理服务器: 这是我开发中主要使用解决跨域的方案
比如 vue2 项目,可以在 vue.config.js 中进行配置,配置 devServer 的 proxy,设置代理请求前缀和目标服务器地址等来完成
- Nginx 代理服务器:项目上线时公司采用 nginx 反向代理解决跨域
10. Nginx
- 概念
Nginx 是开源、高性能、高可靠的 Web 服务器和反向代理服务器。
- 基本使用
- 反向代理解决跨域
1 | server { |
- 解决 history 模式前端路由 404 问题
try_files $uri $uri/ /index.html;
Builder
1. 谈谈 Git 常见指令
2. 谈谈 Git Flow / Git 工作流程
- 概念
是一个 git 标准化操作流程
- Git 分支
- master 是长期分支,一般用于管理对外发布版本,每个 commit 对一个 tag,也就是一个发布版本
- develop 是长期分支,一般用于作为日常开发汇总,即开发版的代码
开发一个新的 feature 直接新在 develop 新开一个临时的 feature 分支,开发完成向 develop 提 Pull Request。 - feature 是短期分支,一般用于一个新功能的开发
- hotfix 是短期分支 ,一般用于正式发布以后,出现 bug,需要创建一个分支,进行 bug 修补。
- release 是短期分支,一般用于发布正式版本之前(即合并到 master 分支之前),需要有的预发布的版本进行测试。release 分支在经历测试之后,测试确认验收,将会被合并的 develop 和 master。
- 提交 commit 规范
-
feat:开发新功能(feature)
-
fix:修复 bug
-
style: 调整格式(不影响代码运行的变动)
-
build:修改构建相关内容,如 npm、maven 内容。
-
imp:优化已有功能(improve)
-
refactor:重构功能
-
test:添加测试
-
docs:撰写文档(documentation)
-
ci:修改持续集成相关内容(Continues Intergration)
记住一部分就好
- 流程概述
- 开发时,以 develop 分支为模板新建一个新的分支:feature/{任务 id}-{任务名称}
- 克隆仓库,拉取 feature 开发分支,切换到 feature 开发分支上进行开发
- 开发完成,提交合并请求(Pull Request, 也叫 PR),合并 feature 和 develop 分支代码
- 由组长审核通过,经过流水线(eslint 检查、测试等),完成合并
- 当所有特性开发&基本测试完成,会以 develop 分支为模板新建一个新的分支:release/{版本号}
- release 是预发布分支,会进行进一步测试
- 没问题后,会将 release 分支合并到 master 分支,同时生成一个 tag 备份代码,上线代码从 master 分支取出使用
- 将来上线代码出现问题,会以 master 分支为模板创建新的分支:hotfix/{任务 id}-{bug 名称}
- 解决 bug 后,会合并到 master 分支上,生成新的 tag
3. 谈谈 Webpack
- 概念
静态模块打包工具,可以将静态模块编译、打包和输出成一个或多个文件(bundles)。
- 5 个核心概念
- 入口(entry): 指示 webpack 从哪个文件开始打包。
- 输出(output): 指示 webpack 编译、打包后的文件输出到哪里去。
- 加载器(loader): webpack 只能识别 js、json 文件,其他类型的文件需要通过 loader 转化成有效模块才能识别。
- 比如:处理样式文件可以使用 css-loader\style-loader\less-loader\sass-loader
- 处理 vue 文件可以使用 vue-loader
- 插件(plugin): 相对 loader,plugin 可以做范围更广的工作,比如:打包优化,资源管理,注入环境变量。
- 比如:处理 html 资源需要使用 html-webpack-plugin
- eslint 语法风格检查需要使用 eslint-webpack-plugin
- 模式(mode): 可以选择 development, production 或 none 之中的一个。不同模式会加载不同的配置。
4. webpack 优化
5. vite 和 webpack 的区别
- 底层语言不同
Vite 是基于 esbuild 采用 go 语言编写,go 语言的操作是纳秒级别
Webpack 是基于 Nodejs,以毫秒计数
所以 vite 比 webpack 更快。
- 启动方式
webpack 启动慢:webpack 首先分析各个模块之间的依赖,然后将所有内容进行打包,模块越多打包速度越慢,所以启动慢。
vite 启动快:vite 采用了一种懒加载的方式,它在启动的时候不需要打包,而是需要某个模块时,再对模块内容进行编译,所以启动很快
- 首屏渲染
webpack 渲染快:webpack 启动时已经将所有内容进行打包了,渲染时直接获取资源渲染即可
vite 渲染慢:Vite 渲染时才会打包编译文件,然后再渲染,打包越慢,渲染速度越慢(但是 vite 有缓存,所以第二次渲染速度没问题)
- 生态
webpack 诞生很久了,生态基本完善
vite 生态不够全,对代码分割不够友好
React
1. React 和 Vue 的区别
- 数据流
- Vue 默认支持双向数据绑定
- React 提倡单向数据流
- 虚拟 DOM
- Vue 宣称可以更快地计算出 Virtual DOM 的差异,这是由于它在渲染过程中,会跟踪每一个组件的依赖关系,不需要重新渲染整个组件树。
- 对于 React 而言,每当应用的状态被改变时,全部子组件都会重新渲染。当然,这可以通过 PureComponent/shouldComponentUpdate 这个生命周期方法来进行控制,但 Vue 将此视为默认的优化。
- 组件化
- Vue 提供单文件组件方式,类似于 HTML 语法。
- React 通过 JSX 语法来定义组件。
- 更新用户界面方式
- Vue 的数据是响应式的,更新数据同时用户界面也会更新
- React 要触发用户界面更新需要使用 this.setState 方法或者 useState 提供的函数
- 构建工具
- Vue 有 vite-cli / vue-cli
- React 有 create-react-app
- 跨平台
- Vue 有 Weex
- React 有 React Native
- 数据可变性
- Vue 使用的是可变数据
- React 强调不可变数据
2. 谈谈 redux
3. 对 React Hook 的理解
4. React 组件间通信
5. React 生命周期
6. 谈谈 React Router
7. 什么是虚拟 DOM?
8. React 中虚拟 DOM Diff 算法
Vue
1. 生命周期函数
- 基础
- 初始化阶段
- beforeCreate
- created
- 挂载阶段
- beforeMount
- mounted
- 更新阶段
- beforeUpdate
- updated
- 卸载阶段
- Vue2: beforeDestroy / destroyed
- Vue3: beforeUnmount / unmounted
- 重要的生命周期
其中重要生命周期:mounted 和 beforeDestroy(beforeUnmount)
- mounted 用来发送请求、设置定时器、绑定事件等任务
- beforeDestroy 用来解绑定时器,解绑事件等收尾工作,防止内存泄漏
- 其他生命周期函数
除此之外,还有一些别的生命周期函数:
-
被 keep-alive 缓存的组件会自动拥有两个生命周期函数
- activated
- deactivated
-
用来捕获后代组件产生的错误
- errorCaptured
- Vue3 提供了两种开发模式:选项式和组合式,其中组合式 API 生命周期用法有一些变化
- setup:相当于之前的 beforeCreate/created
- 剩下的生命周期函数需要引入使用,比如:
- onMounted
- onBeforeUnmount
- 每个生命周期函数都能使用多次
2. 组件间通信
- props 父 -> 子
可以展开说说 props 具体内容
- 自定义事件 子 -> 父
- Vue2 中给组件绑定的事件默认都是自定义事件,加上
.native
才是 DOM 事件 - Vue3 中给组件绑定的事件默认是 DOM 事件(当然实际要满足条件才会绑定:1. 事件名需要是 DOM 事件名称 2. 子组件必须有根标签 3. 子组件内部不能 defineEmits 声明接受),不满足条件就是自定义事件
- v-model 父 <-> 子
vue2 和 vue3 用法也不一样:
- vue2 中给组件绑定 value 属性和 input 自定义事件
- vue3 中默认给组件绑定 modelValue 属性和 update:modelValue 事件
- 也可以通过 v-model:xxx 的方式修改属性名和事件名
- v-bind:xxx.sync 父 <-> 子
- 只有 vue2 才能使用,vue3 不能使用了
- 绑定 xxx 属性和 update:xxx 自定义事件
- 插槽 父 <-> 子
- 通信的内容主要是标签数据(之前通信方案都是普通数据)
- 分类:默认插槽、具名插槽和作用域插槽
- 一般我设置组件时,会优先最重要内容用默认插槽(因为简单),其他内容考虑具名插槽,如果需要子向父通信用作用域插槽(table)
- vuex / pinia 兄弟、祖孙
- 一般 vue2 项目用 vuex,vue3 项目用 pinia
- 还有其他通信方案
- 全局事件总线(兄弟):vue2 中能使用,vue3 不能使用了, 因为删除了
$on/$off
等自定义事件方法,想要使用必须用第三方库,比如 mitt 实现 $parent/$children/$refs
(父 <-> 子),其中 vue3 删除了$children
$attrs/$listeners
(父 -> 子), 其中 vue3 删除了$listeners
, 内容放入了$attrs
中- provide/inject 祖孙
3. 谈谈 Vuex
一个集中式状态管理方案,通常用于管理多个组件共享的状态数据。
开发时需要定义主模块和其他分模块
主模块主要定义 modules 属性用来汇总其他分模块
其他分模块主要定义 state、getters、actions、mutations、namespaced。
- state 模块管理的状态数据
- getters 只读计算属性数据
- actions 一般用来与服务器进行交互的函数,比如:发送请求
- mutations 直接更新数据的函数
- namespaced 的值为 true,开启命名空间,这样每个 vuex 模块的内容就被隔离,不会互相影响。
组件读取/更新 vuex 数据有两种方式,一种通过$store
,一种 map 函数形式
一般如果数据只用一两次,我会用$store,比较简单。数据比较频繁使用,用 map 函数形式
4. 谈谈 Pinia
一个集中式状态管理方案,通常用于管理多个组件共享的状态数据。
相对于 Vuex 来说,pinia 优点:
- 没有 mutations (流程更简洁)
- Typescript 支持更友好
- pinia 模块定义即可使用,不用汇总
开发时需要定义主模块和其他分模块:主模块定义好后,就定义分模块即可,不用汇总
分模块主要由以下内容:
- state 模块管理的状态数据
- getters 只读计算属性数据
- actions 与服务器进行交互(发送请求),同时更新数据
组件引入分模块暴露的 useXxxStore 函数,得到 Store 对象,即可操作数据和方法了
- 一般如果我需要直接更新一个数据:就直接操作数据即可
- 如果我需要直接更新多个数据:store.$patch 方法更新数据
- 如果我需要更新数据并且发送请求:就需要定义 action 函数,通过 store 调用 action 函数来更新
5. Vue2 和 Vue3 的区别
- 生命周期不一样
卸载阶段
- vue2 beforeDestroy destroyed
- vue3 beforeUnmount unmounted
- 组件间通信方案用法不一样
- vue3 删除了
$on/$off/$once
API,所以默认不能全局事件总线,如果想要使用全局事件总线,需要使用第三方库 mitt - vue3 删除了 v-bind:xxx.sync 修饰符,父子组件双向通信只能使用 v-model
- v-model 对组件用法不一样
- vue2 v-model 绑定的是 value 属性和 input 自定义事件
- vue3 v-model 绑定的是 modelValue 属性和 update:modelValue 自定义事件
- vue3 删除
$listeners
,整合到$attrs
中 - vue3 删除
$children
,获取子组件实例对象必须使用 ref
- 指令不一样
- v-for 和 v-if 优先级不同
- vue2 是 v-for 优先级更高
- vue3 是 v-if 优先级更高
- vue3 新增了一个指令:v-memo 用来缓存 DOM 元素
- vue3 删除了 v-bind:xxx.sync 修饰符
- vue3 v-model 用法不一样
- 开发模式不一样
- vue2 只有选项式开发模式
- vue3 除了有选项式开发模式以外,新增了组合式开发模式
- setup
- ref / reactive / watch / computed
- onMounted / onBeforeUnmount
- 响应式原理不一样
- vue2 通过 Object.defineProperty 实现的响应式
- vue3 通过 Proxy 实现的响应式
6. 谈谈 Vue 的指令
- 常用指令
- v-if / v-else-if / v-else 条件渲染(控制元素的显示和隐藏)
- v-show 条件渲染(控制元素的显示和隐藏)
- v-if 和 v-show 区别:
- v-if 隐藏时,销毁元素(卸载组件)
- v-show 隐藏时,通过 display:none 来隐藏的
- 结论:频繁切换用 v-show,不频繁用 v-if
- v-if 和 v-show 区别:
- v-for 遍历展示(列表渲染)
- key 属性作用:在 diff 算法中,尽可能复用相同 key 的元素,更新性能更好
- key 属性取值:一般用 id,用 index 可能导致更新性能不好
- v-for 和 v-if 优先级:
- vue2 是 v-for 更高
- vue3 是 v-if 更高
- v-on 绑定事件 @
- 事件修饰符 .prevent .stop .once .self
- 按键修饰符 .enter .13
- v-model 双向数据绑定
- 双向数据绑定原理
- 给普通 input 元素(text)绑定,绑定的是 value 属性和 input 事件
- 给单选(radio)或多选(checkout)绑定,绑定的是 checked 属性和 change 事件
- 给下拉列表(select)绑定,绑定的是 value 属性和 change 事件
- 给组件绑定
- vue2 中,组件绑定和普通元素绑定效果一样
- vue3 中,默认绑定 modelValue 属性和 update:modelValue 事件
- 双向数据绑定原理
- v-bind 单向数据绑定(强制绑定数据) :
- v-slot 插槽 #
- 默认插槽、具名插槽和作用域插槽
- v-memo(新增的指令) 用于缓存部分 DOM 元素
- 不常用指令
- v-text 设置元素 textContent
- v-html 设置元素 innerHTML
- v-once 元素只解析渲染一次,后续再也不变了
- v-pre 跳过解析,直接渲染最原始的内容
- v-cloak 防止解析时渲染表达式(用于隐藏尚未完成编译的 DOM 模板。)
7. ref 和 reactive 的区别
- ref 可以用来定义基本类型和引用类型数据,reactive 只能用来定义引用类型数据;
- ref 定义的数据如果是引用数据类型,实际是通过 reactive 来定义的。
- ref 定义的数据需要通过 .value 来读取或更新,reactive 可以直接操作数据;
8. 谈谈 Vue Router
- 概念
用来实现 Vue 的单页面应用(single page web application,SPA)。
单页面应用特点:
- 整个应用只有一个完整页面,所有更新只是这个页面的局部渲染
- 点击页面链接不会刷新整个页面,只会更新浏览历史记录和页面局部更新
- 路由两种模式
- hash
- 特点
- hash 路径带 #,# 后面的值(路由路径)不会提交到 server 端;
- hash 可以改变 url ,准确来说改变的是 # 后面的值,页面不会刷新;
- 兼容性更好, IE6+。
- 原理
- hash 通过 window.location.hash 的方式,实现路由跳转的功能。
- hash 通过 window.onhashchange 的方式,来监听 hash 的改变,借此实现无刷新跳转的功能。
- 特点
- history
- 特点
- history 路径不带 #,更美观。
- history 可以改变 url ,改变的是整个 url,页面不会刷新;
- 兼容性稍差, IE10+;
- 页面刷新时,history 可能会出现 404 问题。
- 原理
- history 通过 window.history.pushState / replaceState 等方式,实现路由跳转的功能。
- history 通过 window.onpopstate 的方式,来监听 url 的改变,借此实现无刷新跳转的功能。
- 特点
- 基本内容
- 提供两个组件
- router-link 用来路由跳转(声明式导航)
- router-view 用来加载渲染路由组件
- 提供两个属性
- $router 用来路由跳转(编程式导航)
- $route 用来获取路由参数和路由路径
- 路由跳转两种方式
- 声明式导航 router-link
- 编程式导航 this.$router.push/replace/go
- 路由传参
- query
- params
- meta
- 路由导航守卫
- 全局路由导航守卫
- beforeEach
- afterEach
- 路由独享守卫
- 组件独享守卫
最重要的是全局路由导航守卫的 beforeEach
主要用来:权限管理功能,控制用户的访问权限
- 路由懒加载
通过 import 动态加载组件,实现组件懒加载
内部做了两件事:
- 代码分割:将路由组件单独打包成一个 js 文件
- 按需加载:需要使用这个路由组件时,才会加载对应的 js 文件
9. nextTick 方法的原理
9.1. Vue2
- 定义一个数组(callbacks)用来存储回调函数
- 定义一个用来遍历数组,执行回调函数的方法(flushCallbacks)
- 定义一个用来将执行回调函数的方法添加到异步队列去的方法(timerFunc)
- 这个方法内部会通过 4 种方式来操作:Promise、MutationObserver、setImmediate、setTimeout
- 会从上到下依次判断来选择,一旦选择前面的方案,后面就不看了
-
到此准备工作完成了,接下来组件会调用 nextTick 方法
-
调用 nextTick 方法时,会将回调函数添加到数组(callbacks)中,在通过 timerFunc 将执行回调函数的方法 flushCallbacks 添加到异步队列等待将来执行
-
等主线程执行完所有同步代码,就会执行异步代码,此时会就执行 flushCallbacks 函数
-
执行 flushCallbacks 函数就会遍历所有回调函数依次执行
总结:
所以 nextTick 方法的真正理解就是将回调函数添加到异步队列中,等待将来执行。
它之所以可以等 DOM 元素渲染完成才触发回调函数,是因为我们先更新响应式数据,此时内部会将更新用户界面的方法通过 nextTick 添加到异步队列去,在调用 nextTick 方法,会将其他代码也添加到异步队列去,队列先进先出,所以先更新用户界面,在执行其他代码,此时就能操作更新后的 DOM 元素了
9.2. Vue3
调用 nextTick 方法,如果不传入参数,就会直接返回一个成功的 promise 对象。后续代码会添加到异步队列等待将来执行。
问题:为什么 Vue3 的 nextTick 这么简单?
- 因为 Vue2 要考虑低版本浏览器的兼容性处理,所以用了 4 种方式来将回调函数添加到异步队列。
- 而 Vue3 放弃了低版本浏览器的兼容,所以只需要考虑最佳方案:Promise 即可。
10. 双向数据绑定原理
v-model 主要用于双向数据绑定(收集表单数据),它给元素绑定时,不同元素做法不一样:
- 如果是文本类型元素( 和
- 如果是单选或多选元素( 和 ):绑定 checked 属性和 change 事件;
- 如果是下拉列表元素(
- 如果不是上述这些元素(比如组件),会按照文本类型元素处理。
- Vue3 中,给组件绑定的是 modelValue 属性和 update:modelValue 事件
11. 响应式原理
11.1. Vue2
- 数据代理
-
总结:将 data/props/methods 等属性代理到 this 上,可以通过 this 直接访问数据,从而让访问数据更加方便
-
详情:遍历 data 所有属性,对每一个属性通过 Object.defineProperty 方法,将 data 属性定义在 vm 上,同时定义了读取数据的 get 和设置属性值的 set 方法。此时我们就能通过 this.xxx 的方式访问 data 中的数据了。 get/set 实际访问/操作的都是原 data 数据
- 数据劫持
-
总结:将原 data 数据中所有属性进行重新定义,定义成响应式的属性
-
详情:遍历 data 所有属性,每一个属性都会创建一个 dep 对象, 然后通过 Object.defineProperty 方法进行重新定义,同时定义读取数据的 get 和设置属性值的 set 方法, 此时 dep 对象会以闭包的形式保存在 get 和 set 方法中。
- 未来当你读取属性数据就会触发 get 方法,会返回属性的值,同时会调用 dep.depend() 方法,它用来建立响应式联系,响应式联系就是,dep 保存 watcher,watcher 保存 dep
- 未来当你设置属性的值就会触发 set 方法,内部会同步更新值,同时会调用 dep.notify() 方法,它用来遍历 dep 保存的所有 watcher,调用 watcher 的方法将更新用户界面的方法添加到异步队列,等待异步更新。(更新数据是同步的,更新用户界面是异步的。)
- 页面解析渲染
-
总结:将模板页面内部模板语法进行解析生成虚拟 DOM 树,这个过程会建立响应式联系,遍历虚拟 DOM 树渲染成真实 DOM 元素,插入页面根标签生效,完成初始化渲染
-
详情:首先 new Watcher,然后内部会调用方法去进行页面初始化渲染,初始化渲染过程中就会构建虚拟 DOM 树,此时会读取表达式的值,会触发数据代理的 get,又触发数据劫持的 get,此时会通过 dep.depend() 建立响应式联系,所谓的响应式联系就是 dep 保存 watcher,watcher 保存 dep(dep 保存 watcher 的目的为了将来能通过 dep 找到 watcher 从而更新用户界面,watcher 保存 dep 为了防止重复保存),构建完虚拟 DOM 树,就会将虚拟 DOM 树解析成真实 DOM 元素,插入页面生效
- 更新触发响应式
- 将来当你更新 data 数据时,就会触发数据代理阶段给属性设置的 set 方法,方法内部实际更新的是原 data 数据,又会触发数据劫持阶段给属性设置的 set 方法,此时会同步更新数据的值,同时触发 dep.notify() 方法遍历 dep 保存的 watcher 将更新用户界面的方法添加到异步队列,等待异步更新(通过 nextTick 方法做得:我在官网查询 nextTick 方法的时候,发现他能等 DOM 元素渲染好在触发指定回调,我就去研究它怎么做到的,发现原来它真正原理,并没有等 DOM 元素渲染好,仅仅是将回调函数添加到异步队列而已(这里可以在展开说为什么,也可以等他来问))。
11.1. Vue3
- 概述
主要由 4 个方法构成 reactive、track、trigger、effect
- reactive 用来将数据定义成响应式
- track 用来进行依赖收集:建立响应式数据和 effect 实例之间的联系
- trigger 用来触发依赖更新:找到响应式数据对应的 effect 实例,去更新用户界面
- effect 用来存储更新用户界面的方法
-
当你定义 reactive 数据时,内部通过 Proxy 方法对数据进行代理,当你读取属性的时候会触发 get,设置属性的值的时候会触发 set,get 方法中会返回属性的值,同时通过 track 进行依赖收集,set 方法中会更新属性的值,同时会通过 trigger 触发依赖更新
-
默认 effect 方法一上来就会执行一遍,此时会生成 effect 实例,将更新用户界面的方法存储在 effect 实例, 并调用更新用户界面的方法来完成页面初始化渲染
-
初始化渲染时会读取表达式的值,触发 Proxy 设置的 get,此时会通过 track 进行依赖收集:依赖收集会创建一个 weakMap 容器,存储的 key 为响应式对象,值为 Map 容器,Map 容器 存储的 key 为响应式对象中某个属性,值为 Set 容器,Set 容器会存储对应的 effect 实例。到此依赖收集完成,初始化渲染后续也会完成
1 | { // WeakMap容器 key是响应式数据 value是一个Map容器 |
- 将来更新响应式数据时,数据的值会同步更新,同时会触发 Proxy 设置的 set,此时会通过 trigger 触发依赖更新: 依赖更新会通过 weakMap 容器找到响应式数据对应的 Map 容器,通过 Map 容器找到对应属性的 Set 容器,遍历 Set 容器中 effect 实例调用更新用户界面的方法,从而更新用户界面达到响应式
12. Vue2 中虚拟 DOM Diff 算法
- Diff 算法简述
初始化渲染阶段会生成一个虚拟 DOM 树(旧树),更新数据时会生成一个新的虚拟 DOM 树,新旧虚拟 DOM 树进行比较,找到不同的内容,从而更新,这就是虚拟 DOM Diff 算法做的事情
- 虚拟 DOM 树简述
虚拟 DOM 树就是一个对象,这个对象通过特定属性代表真实 DOM 元素的信息,比如:tag 代表元素标签,data 代表元素属性,children 代表子元素
- Diff 算法详细流程
Diff 算法一共会经历三个函数:patch、patchVnode、updateChildren
其中最重要的是 updateChildren 方法:
它是对相同层级的所有子节点进行 diff(比较),整体流程是从两端到中间遍历,一个一个元素进行比较,从而更新。
具体来说:
-
定义 4 个指针,分别指向旧的虚拟 DOM 树开头,也叫做旧前,指向旧的虚拟 DOM 树结尾,叫做旧后,新的虚拟 DOM 树开头,叫做新前,新的虚拟 DOM 树结尾,叫做新后
-
整个遍历过程(详细版本):
- 判断旧前元素是否存在,不存在就更新下标(旧前++),存在就下一步
- 判断旧后元素是否存在,不存在就更新下标(旧后–)存在就下一步
- 判断旧前和新前元素是否一致(sameVnode:看 key 和 tag),如果一致就会进一步比较(patchVnode),(新前旧前)下标++,不一致就下一步
- 判断旧后和新后元素是否一致(sameVnode:看 key 和 tag),如果一致就会进一步比较(patchVnode),(新后旧后)下标–,不一致就下一步
- 判断旧前和新后元素是否一致(sameVnode:看 key 和 tag),如果一致就会进一步比较(patchVnode),更新下标(新后–旧前++),移动 DOM 元素位置(将旧前插入到旧后的后面),不一致就下一步
- 判断旧后和新前元素是否一致(sameVnode:看 key 和 tag),如果一致就会进一步比较(patchVnode),更新下标(旧后–新前++),移动 DOM 元素位置(将旧后插入到旧前的前面),不一致就下一步
- 看 key,提取旧树剩下的子元素的 key 属性,判断新前节点的 key 在不在旧树 key 中,存在就移动 DOM 元素位置,不存在创建新前元素插入页面,更新下标(新前++)
-
整个遍历过程(简化版本):
- 旧前新前
- 旧后新后
- 旧前新后
- 旧后新前
- 看 key
- 其中 旧前新后 旧后新前 看 key 更新 DOM 元素后,还需要移动 DOM 元素位置
到此更新基本完成。(后面还有两个流程,也可以说)
13. keep-alive 原理
Mobile
1. 移动端适配方案
- rem
原理:
- rem 单位是相对长度单位,是相对 html 标签字体大小的。
- 我们通过动态更新 html 标签字体大小,就能控制 rem 的大小,从而实现适配
具体做法
- 准备 js
1 | const docEl = document.documentElement; |
- 配置插件:postcss-pxtorem,能够自动将 px 转 rem 单位
- viewport
原理:
- vw/vh 也是相对长度单位,相对于整个浏览器视口的宽度和高度
- 1vw 等于屏幕宽度的 1%
具体做法:
- 设置 meta 标签
1 | <meta |
- 配置插件 postcss-px-to-viewport,能够自动将 px 转 vw 单位
2. 1 物理像素边框方案
1 | .border-b { |
3. 小程序的生命周期函数
- 应用生命周期
- onLaunch: 用于登录
- onShow
- onHide
- 页面生命周期
- onLoad
- onShow
- onReady
- onHide
- onUnload:做收尾工作,清除定时器等任务,防止内存泄漏
如果需要发送请求,需要分析需求,看当前页面的数据是否要实时更新:
- 需要实时更新:onShow
- 不需要实时更新,一次就好:onLoad
- 组件生命周期
-
组件自身生命周期函数
- created
-
组件所在页面的生命周期函数
- show
- hide
4. 小程序的分包
- 分包目的
- 小程序体积能更大能写更多功能
- 优化小程序首次启动的下载时间
- 分类
主包和分包
分包又分为:普通分包和独立分包
- 普通分包和独立分包区别
-
普通分包可以使用自己和主包的内容
-
独立分包只能使用自己的内容(除了插件等以外)
- 分包大小限制
-
整个小程序所有分包大小不超过 20M
-
单个分包/主包大小不能超过 2M
- 分包具体配置
需要将分包的页面和其他资源放入一个单独的文件夹中,再在 app.json 中配置 subpackages
独立分包需要配置中加上 independent
- 分包预下载
有时候我们加载分包速度比较慢,可以提前加载分包
需要在 app.json 配置 preloadRule 就可以
5. 微信小程序组件间通信
- 页面与组件之间的通信
- properties 父 -> 子
- 自定义事件 子 -> 父
- 通过 bind:xxx 绑定事件
- 通过 this.triggerEvent 触发事件
- 全局通信方案
-
globalData
- 在 app.js 中定义 globalData
- 其他页面/组件通过 getApp() 得到 app 实例对象,进而操作数据
-
storage
- wx.getStorageSync() 获取
- wx.setStorageSync() 设置
通常情况下不建议直接用 storage 通信,性能较差。storage 主要用于持久化存储数据,如果要频繁通信,可以在 app.globalData 中存储一份使用
- 事件总线
- pubsub-js
- 页面之间通信
-
query
- 跳转路由携带 query 参数
- 下个页面通过 onLoad 生命周期函数的参数接受使用
-
getCurrentPages()
- 可以获取路由历史记录,从而得到上个页面的实例对象进而操作数据
6. 微信小程序登录流程
- 需求:用户无需登录,可以访问部分页面。需要用户手动点击授权登录进行登录
-
点击授权登录,通过 wx.login 得到 code
-
将 code 发送请求给服务器得到用户的 token
-
将 token 存储起来(globalData 和 storage)
- storage 为了持久化存储
- globalData 读写速度更快,性能更好
-
在封装请求函数中在请求头携带 token 参数
-
发送请求获取用户数据,将用户数据存储起来(globalData 和 storage)
-
跳转回之前页面
-
默认会返回默认头像和用户名,可以通过微信提供最新填写能力,引导用户设置用户头像和用户名
- 需求:用户必须登录才能使用小程序,一上来就需要进行登录
- 用户无需点击,在 app.js 中 onLaunch 生命周期函数就进行登录操作
- wx.login - 发送请求携带 code - 得到 token 并存储 - 发送请求携带 …
7. 微信小程序支付流程
- 用户可以从两个地方来到订单支付页面
-
商品详情 - 点击立即购买
-
购物车 - 点击提交订单
-
商品详情点击立即购买就打算要买这个商品,将商品 id 以 query 参数传递给订单支付页面,在跳转过去
-
购物车点击提交订单直接跳转订单支付页面
- 订单支付页面需要展示商品信息和收件人信息
-
商品信息,可以通过判断是否有 query 参数来识别是哪种渠道进入的
- 如果是有商品 id,说明是商品详情进入,通过商品 id 发送请求获取商品数据展示
- 如果没有商品 id,说明是购物车进入的,直接发送请求获取购物车已选择商品列表展示
-
收件人信息,直接发送请求获取默认收件人展示即可。同时提供方式跳转页面可以对收件人信息 CRUD
- 当用户确认并填写好信息后,点击结算开始支付
- 首先将收件人、商品详情等信息发送请求给服务器获取 orderId
- 通过 orderId 发送请求获取微信支付需要的参数:(timeStamp 时间戳、paySign 签名、signType 签名算法 nonceStr 随机字符串等)
- 将微信支付需要的参数通过 wx.requestPayment(微信支付接口)来发起支付
- 支付成功进行提示,跳转到支付成功页面
8. 小程序上线流程
- 将写好的代码上传到微信
微信开发者工具 - 右上角 - 上传
-
来到微信小程序管理平台,填写小程序的信息和条目等内容
-
来到 管理 - 版本管理 - 提交审核
-
此时微信小程序官方会审核你提交的小程序代码,审核通过小程序就上线了
9. 小程序代码上线后修改代码,如何让用户看到最新效果
在 app.js 中 onLaunch 中通过 UpdateManager 实例来监听小程序新版本下载完成事件,调用实例的 applyUpdate 方法就能应用新版本并重启了
1 | function checkUpdateVersion() { |
10. 原生小程序和 uni-app 的区别
- uni-app 开发一套代码,能运行多个平台。原生小程序只能在微信小程序中运行
- uni-app 是使用 Vue 框架语法进行开发。原生小程序使用 WXML 语法进行开发
- uni-app 生命周期支持 Vue 的生命周期,原生小程序只能使用 onLoad、onShow 等
- uni-app 全局对象是 uni,能兼容多个平台,原生小程序全局对象是 wx
11. uni-app 的生命周期
生命周期和微信小程序类似,但是支持 Vue 的生命周期
12. uni-app 怎么打包成 apk(打包 App 流程)
- 来到 manifest.json 文件配置 app 的图标、启动页、appid、使用权限等
- 使用 HbuilderX - 发行 - 云打包开始打包
- 打包需要填写应用名称和证书等内容,点击打包即可
- 云打包后会提供下载地址,下载打包好的 app
- 也可以本地打包
13. 开发 uni-app 项目,使用过哪些插件库
-
UI 组件库:uView-ui
-
其他内容可看插件市场:https://ext.dcloud.net.cn/
Project
1. 项目开发过哪些功能模块
6-7 个模块以上
2. 项目难点
2.1. axios 二次封装
- 基本功能
说法一:
- 携带统一的请求前缀
通过 baseURL 指定,它的值通常从.env 环境变量文件中获取
- 携带公共请求参数:token
在设置请求拦截器中携带公共参数
- 成功返回成功的数据,失败返回失败的原因
在响应拦截器成功回调中,还需要进一步判断返回值中的 code,来判断是否功能成功
成功返回成功的数据(外面使用更简单),失败做出对应的错误提示(不同的 code 错误提示不一样的)
- 响应拦截器失败回调错误提示
通过判断响应状态吗和响应 message 信息来做出相应的错误提示
例如:404 资源找不到等
说法二:
首先通过 axios.create 来生成 axios 实例,这个时候可以定义 baseUrl 来指定公共请求前缀,它的值通常从.env 环境变量文件中获取
接下来可以定义 axios 的请求拦截器和响应拦截器,它们分别是发送请求前和得到响应后触发的回调函数
在请求拦截器中我们添加公共请求参数 token
在响应拦截器成功回调中,还需要进一步判断返回值中的 code,来判断是否功能成功
成功返回成功的数据(外面使用更简单),失败做出对应的错误提示(不同的 code 错误提示不一样的)
在响应拦截器失败回调中进行更加详细的错误提示,通过判断响应状态吗和响应 message 信息来做出相应的错误提示
例如:404 资源找不到等
- 取消重复请求
- 给每个请求生成一个 key(相同请求生成的 key 相同,不同请求升成的 key 不相同)
key 由 url、method、params 和 data 拼接组成
- 给每个请求生成一个取消请求的方法 cancel
给每个请求 config 添加 cancelToken,值 new axios.CancelToken 产生,此时在回调函数中可以得到取消当前请求的 cancel 方法。
-
使用 map 容器将 key 和 cancel 存储起来(使用对象也可以的)
-
发送请求的请求拦截器中,先判断当前请求的 key 是否存在 map 容器中,存在就调用 cancel 方法取消请求,同时删除这个数据,不存在就将当前请求的 key 的 cancel 存储起来
-
完成请求的响应拦截器中,将当前请求的 key 和 cancel 给删除掉
- 取消上一个页面的请求
- 在存储 key 和 cancel 时,还存储当前请求对应页面的路由路径(可以通过 window.location.pathname 获取)
- 在路由前置守卫中,遍历 map 容器,取出每一个请求,判断请求对应页面的路径和当前 to.path 是否相同,不相同说明是之前页面的请求,调用 cancel 方法取消掉即可
2.2 echarts 大屏适配/大数据渲染问题/实时数据通信
1. echarts 基本理解
echarts 用来数据可视化,将数据以图形图表的方式展示
它默认有两种渲染方式:canvas 和 svg 形式,一般用 canvas 渲染
它主要是通过配置去渲染图表,它的配置有:
- title 标题
- xAxis x 轴
- yAxis y 轴
- series 类型:指定图表类型、图表数据、图表样式
- legend 图例:切换显示图表
- grid 布局
- tooltip 提示框
- dataZoom 数据缩放:只查看一定范围内的数据
- geo 地理坐标系,可以绘制地图
2. echarts 大屏适配
有 rem、viewport、scale 等方案,其中 rem 和 viewport 方案对图表中的字体大小等调整不友好,需要手动处理,比较麻烦,所以项目中我采用了 scale 的方案
scale 方案通过比较屏幕和设计稿宽高比来对页面进行缩放,缩放过程保持页面整体宽高比不变。
- 计算设计稿的宽高比和当前页面的宽高比
- 判断当前页面的宽高比是否大于设计稿的宽高比,如果大于说明当前屏幕宽度更宽,高度更小,此时需要拿取高度来进行缩放
1 | // 缩放的值等于屏幕高度/设计稿高度 |
- 判断当前页面的宽高比是否小于设计稿的宽高比,如果小于说明当前屏幕宽度更窄,高度更大,此时需要拿取宽度来进行缩放
1 | // 缩放的值等于屏幕宽度/设计稿宽度 |
- onMounted 绑定窗口 resize 事件,当窗口大小调整时,重新设置 scale 即可,为了防止内存泄漏,onBeforeUnmount 时解绑事件
1 | const scale = ref(1); |
3. echarts 数据实时通信
echarts 数据实时通信和 echarts 没啥关系,主要使用 web socket 技术进行实时通信的
WebSocket 是一个全双工通讯协议。
特点:服务器可以向客户端主动推动消息,客户端也可以主动向服务器推送消息。
使用:
new WebSocket(url) 传入 url 就可以连接上 WebSocket 服务器了
通过 message:接受服务器发送的消息
通过 send 方法:可以给服务器发送消息
除此以外还有其他方法:close 关闭连接
其他事件:open:打开事件 error 错误事件 close 关闭事件
功能:WebSocket 连接时可能因为不好的网络环境会断开链接,此时可以使用心跳检测机制来进行断开重连
心跳检测:
web socket 在连接过程中可能会因为网络问题断开连接,我们需要重连,这里用到心跳检测机制
具体来说:
- 检测是否断开链接:
当我们连接上 ws 服务时,需要开启定时器,隔一段时间(10s - 30s)向 ws 服务器发送一个特殊信号(ping),同时开启定时器(2s)检测服务器是否有回应信号(pong)
如果在规定时间内有回应,继续此操作,如果没有回应,说明断开连接了,我们要开始重连 ws
- 重连机制
每隔一段时间发起 ws 的重连,同时重连次数增加,一旦重新连接成功,就归零重连次数,方便下次重连,如果重连次数超过最大次数,就要提示错误,不在尝试了
4. echarts 大数据渲染
有三种方案:
- 降采样策略
不会渲染全部数据,按照某种算法只加载部分数据,但是保留整体图形相似的形状。
具体配置:大部分图表都有一个配置项 sampling,配置它就可以达到了
- 添加 dataZoom 组件
设置渲染数据范围:startValue 和 endValue,这么做的目的就是一次性渲染一部分数据,速度更快
- appendData
能支持大数据量(百万以上)的渲染场景,可以对数据进行分片加载数据和增量渲染。
但是只能部分图表才能使用,具体用法一般会参考文档使用。
2.3. 后台实现权限管理功能
- 搭建三个页面来管理权限功能:用户管理、角色管理、菜单管理
- 菜单管理:需要填写路由名称、路由组件地址、菜单图标、按钮权限值等内容
- 角色管理:管理角色对应访问菜单的权限
- 用户管理:管理用来登录的账户,同时会分配角色(一旦分配该用户就有角色对应的访问权限了)
- 登录
当用户进行登录时,就能得到 token,我们将 token 存储起来(pinia/vuex 和 localStorage)
使用:在 axios 请求拦截器中携带 token 参数
- 路由全局前置导航守卫
登录成功,会进行路由跳转。或者用户第一次访问,都会触发路由全局前置导航守卫
此时会判断是否有 token,没有 token 说明没有登录,只能访问登录页面
有 token 说明登录了,需要进一步判断是否有用户数据,如果有,直接访问页面
如果没有用户数据,需要发送请求获取用户数据
- 获取用户数据
获取用户数据可以得到用户基本信息、用户的菜单权限列表和用户按钮权限列表
用户的菜单权限列表:就是服务器返回的用户拥有访问权限的路由表
用户按钮权限列表:就是用户拥有页面上某个按钮的权限表
- 路由权限控制
一开始路由表分成两部分:静态路由表和任意路由表
静态路由表:登录、404 等页面
任意路由表:匹配任意地址,跳转 404
一上来只加载静态路由表,所以可以访问登录页面
当用户数据加载完成时,我们会遍历用户的菜单权限列表,将内部 component 通过路由懒加载形式替换成路由组件,然后在遍历通过 router.addRoute 方法将配置追加到路由中,此时我们就能访问这些路由了(最后会追加任意路由表)
- 按钮权限控制
通过指令方式实现的:自定义全局指令 v-permission
内部会判断某个按钮传入的权限值在不在用户按钮权限列表中,如果在啥也不处理,不在就删除当前元素
让用户无法访问,从而实现按钮权限控制
2.4 其他
也能说项目优化和组件封装
3. 项目优化
- 样式方面
- 定义公共样式,混合,全局变量复用
- js 方面
- v-if 和 v-show,频繁用 v-show,不频繁用 v-if
- 页面比较长的表达式,或者使用了多次表达式,用 computed 缓存
- vue 中遇到大量数据可以使用 Object.freeze 冻结数据(这样能让内部数据不被数据劫持,不会生产 dep 对象等,节省内存提升性能)
- vue 中遇到不涉及用户界面更新的数据不定义成响应式(这个数据也不会被数据劫持,比如定时器的返回值)
- 使用 keep-alive 缓存路由组件,切换性能更好,但是要注意数据是否需要及时更新(activated 生命周期)
- v-for 遍历 key 属性尽量唯一且稳定(这里可以聊一下 diff 算法),且在 vue2 中避免同时使用 v-if,vue3 中可以。
- echarts 大量数据渲染优化(…),普通大数据渲染:分页 或 虚拟长列表优化(商品数据展示)
- 图片懒加载
- 路由懒加载
- 第三方插件的按需引入(element-ui、echarts 按需加载配置)
- 高频触发的事件使用防抖、节流(购物车商品数量操作)
- 大量的事件使用事件委托优化(三级分类导航)
- 封装函数、封装组件复用(EchartsCard、Card 等)
- 打包工具层面的优化
vue2 项目:通过配置 webpack splitChunk 代码分割,从而将 element-ui、echarts、vue 等库进行单独打包,就能并行加载速度更快
- 网络层面的优化
- 开启 gzip 压缩
- 浏览器缓存(强缓存和协商缓存)
- CDN 的使用(要结合 webpack 配置一起使用)
- 升级 http 协议为 2.0
4. 项目组件封装
1. 大文件上传
需求: 项目中有超过几百 mb 的文件需要上传,原来 el-upload 是整体上传比较慢,同时网络一旦不稳定,需要重新上传,不太好,所以重新封装 el-upload 组件,实现大文件上传
- 切片上传
- 通过 el-upload 组件定义 http-request 方法来自定义上传,在回调函数中可以得到上传文件
- 将上传文件按照预先定义好单个切片大小(比如 10mb),通过文件的 slice 方法,将大文件截取成一个个小文件,添加到数组中,实现切片
- 设置文件名称和代表顺序的下标,通过 formData 方式并行上传到服务器
- 再通过 Promise.all 方法等所有切片上传好,在发送一个请求通知服务器所有切片上传完成,请求合并文件,服务器返回大文件在线访问地址
- 进度条
-
el-upload 组件 file-list 文件列表中每个文件都有一个 status 和 percentage 属性
- status 代表上传的状态:uploading 代表上传中,此时会显示上传进度条,success 代表上传成功,显示成功的图标
- percentage 显示上传的进度
-
上传切片之前初始化 file 的 status 为 uploading,此时会显示上传进度条,percentage 为 0,同时定义一个用来存储已经上传文件大小的属性(loaded)
-
在上传切片请求方法中添加 onProgress 回调,它会在上传文件时触发,此时就能得到已经上传的文件大小,将
已经上传的文件大小累加 / 文件总大小 * 100
就能得到上传的进度 percentage -
判断
percentage=100
时,将 status 调成 success,上传成功
- 创建 hash
要想实现断点续传,需要服务器记录已经上传的切片内容,并且要保证如果多个用户上传文件名一样的话,文件不能冲突
所以我们根据文件内容生成 hash 值(相同文件生成 hash 值一样,不同文件生成 hash 值不一样),将 hash 值作为切片名称,这样就不会受文件名影响了
因为上传文件非常大,计算 hash 值需要比较长的时间,这里我使用 web worker 进行优化,将根据文件内容计算 hash 值的任务交给分线程完成,这样主线程还可以干其他事情
- 断点续传
- 在上传切片之前,需要先发送请求给服务器获取当前 hash 对应文件已经上传的切片
- 服务器会返回值,值有一个数组,内部包含已经上传的切片名称
- 我们上传切片之前,对所有切片进行过滤,判断当前切片是否已经上传过,上传过就过滤,没有上传过就保留,这样就能实现断点续传了(此时 loaded 值初始化就不能为 0 了,而是
已经上传的切片数量 * 10mb
)
- 秒传
- 在上传切片之前,需要先发送请求给服务器获取当前 hash 对应文件已经上传的切片
- 如果文件全部上传过,会返回一个标识和文件在线访问地址 url,我们判断标识如果满足条件,就直接使用 url 就好
- 不满足条件继续走断点续传逻辑
2. 配置化表单/动态化表单
需求: 后台管理项目有大量的表单需要开发,传统一个个开发效率太低,重复度高,所以封装了一个 Schema Form 组件来配置化表单
- 给组件传递 schema、rules、model 等属性
-
schema:是一个数组内部是对象,包含:
- component 代表要渲染的组件
- label 左侧文字
- props 渲染组件的 props 数据
- model 要收集的数据名称,也是表单校验规则名称
- key 遍历的 key 属性
- span 栅格布局占份数
- 下拉列表 selectOptions、单选 radioOptions 等
-
rules:表单校验规则
-
model:表单的数据
- 组件内部会根据 schema 的值来动态渲染表单(可以说说具体做法)