零、做过的项目中复杂的功能
- 数据拖拽 根据 dataEase 上类似于低代码平台迁移到 web 页面中。根据左侧不同指标类型配置拖拽至右侧设置对应的样式。有些指标是一些 html元素,也需要同步将该 html元素 实体展现在右侧并且可输入或者选中或者点击。指标类型是后台返回的,通过 Vue.extend 挂载到页面上以实现响应式。
- 大量的树节点处理 将一个页面的地区信息单独抽出来的做成独立的项目。通过节点的选中在地图上标注。常规的 el-tree 组件是使用递归子组件的方式实现,在面对大量的节点时,每个节点都是一个响应式实例且有许多样式计算,10000条子节点就需要计算10000次,就会造成卡顿,根本原因就是创建的 Vue 实例过多,如果需要优化,就要减少 Vue实例的创建。
思路: 递归组件转化成扁平数组来实现。主要分为两部分:
- 更改 DOM 结构成平级结构,点击节点以及节点的视觉样式通过操作总的 List 来实现。
- 使用虚拟长列表来控制 Vue 子组件实例创建的数量
- 虚拟列表的实现,实际上就是在首屏加载的时候,只加载
可视区域内需要的列表项,当滚动发生时,动态通过计算获得可视区域内的列表项,并将非可视区域内存在的列表项删除
计算 可视区域 里所能容纳的数据数量(containSize)
this.containSize =
Math.floor(this.$refs.scrollContainer.offsetHeight / this.oneHeight) +
2;
计算当前可视区域起始数据索引(startIndex) @scroll
async setDataStartIndex() {
this.currentScrollTop = this.$refs.scrollContainer.scrollTop;
let currentIndex = ~~(
this.$refs.scrollContainer.scrollTop / this.oneHeight
);
if (this.startIndex === currentIndex) {
return;
}
this.startIndex = currentIndex;
if (
this.startIndex + this.containSize > this.list.length - 1 &&
!this.loading
) {
console.log("滚动到底部");
let newList = await this.getMockData(20);
this.list = [...this.list, ...newList];
}
},
计算当前可视区域结束数据索引(endIndex)
// 结束索引
endIndex() {
let endIndex = this.startIndex + this.containSize * 2; // 多一屏预加载
if (!endIndex === this.list.length - 1) {
endIndex = this.list.length - 1;
}
return endIndex;
}
计算当前可视区域的数据,并渲染到页面中
// 定义一个待显示的数组 computed
showDataList() {
let startIndex = 0;
if (this.startIndex <= this.containSize) {
startIndex = 0;
} else {
startIndex = this.startIndex - this.containSize;
}
return this.list.slice(startIndex, this.endIndex);
}
计算 startIndex 对应的数据在整个列表中的偏移位置startOffset并设置到列表上
改造之后的 tree Dom结构,父节点和子节点是平级的,在操作子节点时去操作内存中的 listData 数据来改变关联节点的状态。listData中的每一项的style、checked、path等信息来描述节点的样式位置和状态,操作一个节点时通过listData更改相关节点的状态样式等信息。 然后就是一些通过 some、filter、every、find 处理父节点全选、半选的判断。
- 页签组件 后台管理系统的 页签 组件。组件的思路是简单的,可以通过在组件内监听路由或者是通过路由守卫,将访问过的路由保存到 Vuex 中,同时存入 localStorage 避免刷新导致 vuex 数据清空的问题。因为当时需求是不限制打开的页签个数,如果使用横向滚动条的话不美观,在没有使用第三方插件的情况下采取了左右各一个箭头用来控制展示区域的滚动距离的形式。以及点击页签需要将展示区域自动滚动至该页签的可视范围内。需要合理的判断容器宽度和数据总宽度,以及偏移量的一些计算。
一、移动端适配说一下
项目中我使用 amfe-flexible 和 postcss-pxtorem 结合的方式。如果是响应式有需要也用到媒体查询。
amfe-flexible: 是配置可伸缩布局方案,主要是将 1rem 设置为 viewWidth / 10
postcss-pxtorem: 是 postcss 的插件,配置在根目录的 postcss.config.js 中,用于将像素单位转换为 rem 单位
npm install amfe-flexible --save
npm i postcss-pxtorem@5.1.1 --save //这个要装5.1.1版本的
// main.js
import 'amfe-flexible';
// postcss.config.js
// 配置postcss-pxtorem,可在vue.config.js、.postcssrc.js、postcss.config.js其中之一配置,权重从左到右降低,没有则新建文件,只需要设置其中一个即可
module.exports = {
"plugins": {
'postcss-pxtorem': {
rootValue: 37.5,
propList: ['*']
}
}
}
注意点:
1. rootValue 根据设计稿宽度除以 10 进行设置,这边假设设计稿为 375,即 rootValue 设为37.5;
2. propList 是设置需要转换的属性,这边 * 为所有都进行转换
二、登录注册怎么做的
- 请求登录接口拿到 token 后将 token 存于 vuex 和 localStorage 中并在路由守卫进行权限验证
- 利用 axios 进行请求拦截,并把 token 放置于头部 headers 中,每次请求返回都会经过拦截器,如果登录过期,需要清空 localStorage 和 vuex 的 token 且退回到登录页。
import axios from 'axios'
import store from './store'
import router from './router'
//http 全局拦截
//token 要放在我们请求的header上面带回去给反馈
export default function sexAxios() {
axios.interceptors.request.use(
config => {
if(store.state.token){
config.headers.token = store.state.token
}
return config
}
)
//每次请求有返回的都是先经过拦截器的
axios.interceptors.response.use(
response => {
if(response.status == 200){
const data = response.data
if(data.code == -1){
//登陆过期 需要重新登陆 清空vuex和localstorage 的token
store.commit('settoken','')
localStorage.removeItem('token')
//跳转到login页面
router.replace({path:'/login'})
}
return data
}
return response
}
)
}
三、栈和队列
- 栈
栈是一种后进先出的数据结构,类似于摞盘子,只能先从最顶部插入或删除。插入一个元素时,将元素压入栈的顶部 (array.unshift)。删除一个元素时,从栈的顶部弹出一个元素。栈可以使用数组的 pop() 或链表实现。
- 队列
队列是一种先进先出的数据结构。类似于现实生活中的排队,先到先得。向队列添加元素时,该元素被添加到队列的末尾 (array.push)。删除一个元素时,从队列的前端删除该元素。(相当于当前事物处理完成脱离队列)队列可以使用数组的 shift() 或链表实现。
- 栈和队列的应用
1. 撤销和恢复
撤销操作将最近的更改弹出栈顶,而恢复的操作将已撤消的更改压入栈顶
2. 数据传输
四、Set 和 Map 的区别
1、Map 是键值对,Set 是值得集合,当然键和值可以是任何得值 2、Map 可以通过 get 方法获取值,而 set 不能因为它只有值 3、都能通过迭代器进行 for...of 遍历 4、Set 的值是唯一的可以做数组去重,而 Map 由于没有格式限制,可以做数据存储
五、什么是 TCP 以及 三次握手 和 四次挥手
- 什么是 TCP
传输控制协议(Transmission Control Protocol)。是为了在不可靠的互联网络上提供可靠的端到端字节流而专门设计的一个传输协议。
- TCP 的状态
- SYN: 建立连接
- FIN: 关闭连接
- ACK: 响应
- PSH: 有数据传输
- RST: 连接重置
- Sequence Number(顺序号码)
- Acknowledge Number(确认号码)
- 三次握手
所谓三次握手,就是指建立一个 TCP 连接时,需要客户端和服务端总共发送 3 个包以确认连接的建立,也就是说为了确认双方的接收与发送能力是否正常。
- 第一次握手: 主机 A 发送位码为 SYN=1,随机产生Seq Number=1234567 的数据包到服务器,主机 B 由 SYN=1 知道,A 要求建立联机
- 第二次握手: 主机B收到请求后要确认联机信息,向 A 发送 ACK Number=(主机 A 的 Seq Number+1),SYN=1,ACK=1,随机产生Seq Number=7654321 的包
- 第三次握手: 主机 A 收到后检查 ACK Number 是否正确,即第一次发送的 Seq Number+1,以及位码 ACK 是否为1,若正确,主机 A 会再发送 Ack Number=(主机 B 的 Seq Number+1),ACK=1,主机 B 收到后确认 Seq Number 值与 ACK=1 则连接建立成功
- 为什么要传回SYN
接收端传回发送端所发送的 SYN 是为了告诉发送端,我接收到的信息确实就是你发送的信号
- 改成两次握手可以不
三次握手可以理解为为了客户端和服务器互相确认对方的发送和接收能力。如果是两次握手,可以确定服务器的发送和接收能力,但只能确定客户端的发送能力,无法确认其接收能力。另外,如果是两次握手的话,可能会因为网络阻塞等原因会发送多个请求报文,延时到达的请求又会与服务器建立连接,浪费服务器资源。
- 四次挥手
所谓四次挥手,即终止 TCP 连接,就是指断开 TCP 连接时,需要客户端和服务端总共发送 4 个包以确认连接断开
- 第一次挥手: 客户端发送一个FIN,用来关闭客户端到服务端的数据传送,客户端进入FIN_WAIT_1状态
- 第二次挥手: 服务端收到FIN后,发送一个ACK给客户端,确认序号为收到序号+1(与SYN相同,一个FIN占用一个序号),服务端进入CLOSE_WAIT状态
- 第三次挥手: 服务端发送一个FIN,用来关闭服务端到客户端的数据传送,服务端进入LAST_ACK状态
- 第四次挥手: 客户端收到 FIN 后,客户端 进入 TIME_WAIT 状态,接着发送一个 ACK 给Server,确认序号为收到序号+1,服务端进入 CLOSED 状态,完成四次挥手
- TCP 的 2MSL
MSL: Maximum Segment Lifetime 报文段最大生存时间,它是任何报文被丢弃前在网络内的最长时间。这个时间是有限的(MSL为最大报文段生存时间,LWIP为1分钟,windows为2分钟),因为 TCP 报文段以 IP 数据报在网络内传输,而IP数据报则有限制其生存时间的生存时间值 TTL(Time To Live) 字段。在 TCP 建立 socket 连接后,主动关闭 socket 连接的过程有一个状态为 Time_Wait(也就是 2MSL 等待机制, 需要停留 2MSL的时间,保证了最后一个 ACK 发送给被动关闭端,确保连接双方关闭完成,如果没有该机制,最后一个 ACK 丢失后会出现半连接的状态。报文段有生存时间,当连接关闭时,有可能收到迟到的报文段。这时,若立马就建立新的连接(同一端口),那么新的连接就会接收迟到的报文,误以为是发给自己的)
六、TCP 的拥塞控制(涉及算法)
流量控制是作用于接收者的,它是控制发送者的发送速度从而使接收者来得及接收,防止分组丢失的
拥塞控制是作用于网络的,它是防止过多的数据注入到网络中,避免出现网络负载过大的情况。
七、HTTP 缓存
HTTP 缓存机制是 Web 应用性能优化的重要手段,一次 HTTP 请求需要经过三次握手来和服务器建立连接,对一些大一点的数据更需要多次往返。HTTP 缓存主要针对如 CSS、JS 图片等更新频率不大的静态文件,主要优点如下:
1. 加快网页加载速度,提升用户体验
2. 减少服务器的负担,提升网站性能
3. 减少了冗余的数据传输,减少网络流量和带宽
HTTP 缓存主要通过服务端响应头告诉浏览器是否应该缓存资源,是否强制校验缓存、缓存多长时间(expires)等信息。HTTP缓存主要分为强制缓存和协商缓存。
- 强制缓存
强制缓存主要通过响应头中 Expires 或者 Cache-Control 两个字段来控制,用来表示资源的缓存时间。
- Cache-Control: 请求/响应头。缓存控制字段,可以说是控制 HTTP缓存 的最高指令,可以有多个值,意义也不尽相同。
no-store: 禁止缓存,每次请求都要向服务器重新请求数据
no-cache: 也缓存,但是在使用已缓存数据前,需要发送带验证器的请求到服务器。主要是结合协商缓存使用
max-age=x: 指定一个时间长度,在这个时间段内缓存是有效的,属于相对时间,单位是s。在 http1.1版本使用
private: 只能被单个用户缓存,不能被代理服务器缓存
public: 表明响应可以被任何对象(发送请求的客户端、代理服务器)缓存
2. Expires: http1.0 的属性,代表资源过期时间,属于绝对时间(时间戳),优先级比 max-age 低。由于客户端时间可被修改,和服务器时间会有差异,因此可能导致缓存混乱。当命中强缓存时,浏览器并不会将请求发送给服务器,此时在开发者工具中的状态码为 200,但 Size 列会显示(from cache)。
from memory cache: 从内存中获取资源,进程被关闭则清空缓存,一般是脚本、图片、字体等文件会存在内存当中
from disk cache: 从磁盘中获取资源,进程关闭也不会被清空,一般是css等样式文件会存在磁盘中
资源加载遵循三级缓存原理,即 内存 > 磁盘 > 网络请求。以加载一张图片为例:第一次访问,从服务器加载资源,此时刷新页面,则从内存中读取 `from memory cache`。关闭掉浏览器后,内存清空,再次打开该网页,在缓存未失效的情况下从磁盘读取 `from disk cache`
- 协商缓存
若没有命中强缓存,则浏览器会将请求发送至服务器。服务器根据http头信息中的 Last-Modify 和If-Modify-Since来判断是否命中协商缓存。具体步骤如下:
- 当第一次请求服务器,服务器响应头会返回一个
Last-Modify,Last-Modify是一个时间标识该资源的最后修改时间,例如 Last-Modify: Thu,31 Dec 2037 23:59:59 GMT - 浏览器第二次访问服务器,会在请求头中带一个If-Modified-Since,就是缓存之前请求时服务端返回的Last-Modify,服务器收到后,根据资源的If-Modify-Since和最后修改时间判断是否命中缓存。若命中缓存,此时状态码返回304且不会返回资源和 Last-Modify,表示文件没修改过,还是从本地缓存中去读取
八、跨域
跨域的主要原因是受同源策略的限制。
同源策略是指 协议 域名 端口 三者均相同,即便两个不同的域名指向同一个ip地址,也非同源。 同源策略限制的内容有:
Cookies、localStorage、indexDB等存储性内容
DOM节点
Ajax 请求发送后被拦截了
但是三个标签允许跨域加载资源
<img src=XXX>
<link href=XXX>
<script src=XXX>
跨域解决方案
- JSONP原理
当我们正常地请求一个JSON数据的时候,服务端返回的是一串 JSON类型的数据,而我们使用 JSONP模式来请求数据的时候服务端返回的是一段可执行的 JavaScript代码。因为jsonp 跨域的原理就是用的动态加载 script的 src ,所以我们只能把参数通过 url 的方式传递,所以 jsonp 的 type 类型只能是get
- 实现一个回调函数,其函数名当作参数值,要传递给跨域请求数据的服务器,函数形参为要获取目标数据(服务器返回数据)
- 创建一个
script标签,将跨域请求的地址赋予 src 属性,还要在这个地址中向服务器传递该函数名(可以通过问号传参: ?callback=函数名) - 服务器收到请求后,需要进行特殊的处理: 把传递进来的函数名和他需要给你的数据拼接成一个字符串,例如: 传递进去的函数名是 show,它准备好的数据是
show('你好') - 最后服务器把准备的数据通过 HTTP协议 返回给客户端,客户端再调用之前声明的回调函数,对返回的数据进行操作
// 自己封装 jspnp 请求
function jsonp(setting){
setting.data = setting.data || {}
setting.key = setting.key||'callback'
setting.callback = setting.callback||function(){}
setting.data[setting.key] = '__onGetData__'
window.__onGetData__ = function(data){
setting.callback (data);
}
var script = document.createElement('script')
var query = []
for(var key in setting.data){
query.push( key + '='+ encodeURIComponent(setting.data[key]) )
}
script.src = setting.url + '?' + query.join('&')
document.head.appendChild(script)
document.head.removeChild(script)
}
// 调用
jsonp({
url: 'http://photo.sina.cn/aj/index',
key: 'jsoncallback',
data: {
page: 1,
cate: 'recommend'
},
callback: function(ret){
console.log(ret)
}
})
- Cors
Cors 需要浏览器和后端同时支持.实现 Cors 通信后端是关键。只要后端实现了 Cors 就实现了跨域。服务端设置 Access-Control-Allow-Origin 就可以开启 CORS。 该属性表示哪些域名可以访问资源,如果设置通配符则表示所有网站都可以访问资源
const express = require('express')
const cors = require("cors")
//2、调用express()得到一个app
// 类似于 http.createServer()
const app = express()
app.get('/login', (req, res) => {
console.log('req =>', req)
res.header("Access-Control-Allow-Origin", "*")
})
- Websocket
Websocket 是 HTML5 的一个持久化的协议,它实现了浏览器与服务器的全双工通信,同时也是跨域的一种解决方案。WebSocket和HTTP都是应用层协议,都基于 TCP 协议。但是 WebSocket 是一种双向通信协议,在建立连接之后,WebSocket 的 server 与 client 都能主动向对方发送或接收数据
// socket.html
let socket = new WebSocket('ws://localhost:3000');
socket.onopen = function () { socket.send('我爱你');//向服务器发送数据 } socket.onmessage = function (e) { console.log(e.data);//接收服务器返回的数据 }
// server.js
let express = require('express');
let app = express();
let WebSocket = require('ws');//记得安装ws
let wss = new WebSocket.Server({port:3000});
wss.on('connection',function(ws) {
ws.on('message', function (data) {
console.log(data);
ws.send('我不爱你')
});
})
- Proxy代理
最常用的方式。通俗点说就是客户端浏览器发起一个请求会存在跨域问题,但是服务端向另一个服务端发起请求并无跨域,因为跨域问题归根结底源于同源策略,而同源策略只存在于浏览器。那么就可以通过Nginx配置一个代理服务器,反向代理访问跨域的接口,并且还可以修改 Cookie 中 domain 信息,方便当前域 Cookie 写入
跨域请求如何携带 Cookie
在前端请求的时候设置 request对象 的属性 withCredentials 为 true
九、HTTP状态码有哪些
状态码首位数字决定了不同的响应状态 1xx 表示请求已被接受,需要继续处理; 2xx 表示请求成功; 3xx 表示重定向; 4xx 表示客户端错误; 5xx 表示服务端错误。
常见的状态码
- 101: 服务器根据客户端的请求切换协议,主要用于 websocket 或 http2 升级
- 200: 请求已成功,请求所希望的数据将随响应一起返回
- 201: 请求成功并且服务器创建了新的资源
- 202: 服务器已接受响应请求,但尚未处理
- 301: 请求的网页已永久移动至新的位置
- 302: 临时重定向/临时主要。服务器目前从不同位置的网页响应请求,但请求者应继续使用原有位置来进行以后的请求
- 304: 本次获取到的内容是读取缓存中的数据,会每次去服务器校验
- 401: 请求需要进行身份验证。尚未认证,没有登录网站
- 404: 服务器没有找到相应资源
- 500: 服务器遇到错误,无法完成对请求的处理
- 503: 服务器无法使用
十、正向代理和反向代理
正向代理隐藏了真实的请求客户端,服务端不知道真实的客户端是谁,客户端请求的服务都被代理服务器代替来请求
反向代理隐藏了真实的服务端,当发送一个请求时,其背后可能有很多台服务器为我们服务,但具体是哪一台,我们不知道,也不需要知道,我们只需要知道反向代理服务器是谁就好了,反向代理服务器会帮我们把请求转发到真实的服务器那里去。反向代理一般用来实现负载平衡
十一、GET 和 POST 的区别
- 参数位置: GET 请求的参数是放在 url 中, POST请求放在请求体 Request Body 中。
- 参数长度: GET 请求在 url 中 传递的参数有长度限制(主要是因为浏览器对 url 长度有限制),POST则没有。
- 安全性: POST 比 GET 安全,因为数据在地址栏上不可见。但是从传输角度考虑,二者都是不安全的,因为 HTTP 是明文传输。
- 参数的数据类型: GET 只接受 ASCLL 字符,而 POST 没有限制
- 缓存: GET 请求会被浏览器主动缓存,而 POST 不会,除非主动设置
十二、什么是 HTTP
HTTP(HyperText Transfer Protocol),即超文本传输协议,是一种实现网络通信的规范。它定义了客户端和服务的之间交换报文的格式和方式,默认使用的是80端口,其底层使用 TCP 作为传输层协议,保证了数据传输的可靠性。
十三、HTTP 和 HTTPS的区别
HTTPS 是 HTTP 协议的安全版本。HTTPS 的出现主要是为了解决 HTTP 明文传输内容导致其不安全的特性。为保证数据加密传输,让 HTTP 运行安全的 SSL/TLS 协议上,即 HTTPS = HTTP + SSl/TLS。通过 SSL 证书来验证服务器身份,并为浏览器和服务器之间的通信进行加密。
二者的区别:
- 安全性: HTTP 协议的数据传输是明文的,是不安全的;HTTPS 使用了 SSL/TLS 协议进行加密处理,相对更加安全。
- 连接方式: 二者使用的连接方式不同,HTTP 是三次握手,HTTPS 是三次握手 + 数字证书。
- 默认端口: HTTP 默认端口是 80;HTTPS 默认端口是 443。
- 响应速度: 由于 HTTPS 需要进行加解密过程,因此速度不如 HTTP。
- 费用: HTTPS 需要使用 SSL 证书,功能越强大的证书其费用越高;HTTP 不需要。
十四、HTTP1.0 和 1.1 区别
- 连接: HTTP1.0 默认使用非持久连接,HTTP1.1 则默认使用持久连接。HTTP1.1 通过使用持久连接来使多个 HTTP 请求复用一个 TCP 连接,避免了 HTTP1.0 中使用非持久连接造成的每次请求都需要建立连接的时间延迟。
- 缓存: HTTP1.0 主要使用 header 中的
If-Modified-Since,Expires 来作为缓存判断的标准;HTTP1.1 则引入了更多的缓存控制策略,例如:Etag、If-Unmodified-Since、If-Match、If-None-Match等更多可供选择的缓存头来控制缓存策略。 - 资源请求: HTTP1.0 中,存在一些浪费带宽等现象,例如客户端只是需要某个对象的一部分,而服务器却将整个对象送过来了,并且不支持断点续传功能;HTTP 1.1 则在请求头引入了 range 头域,它允许只请求资源等某个部分,即返回码是 206,这样就方便了开发者自由的选择以便于充分利用带宽和连接。
- host: HTTP1.1 引入了 host,用来指定服务器的域名。
- 方法: HTTP1.1相较于 HTTP1.0 新增了许多方法,如:put、delete、options
十五、什么是 SSL 证书
SSL 证书是数字证书的一种,类似于驾驶证、护照和营业执照的电子副本,是遵守 SSL协议(Secure Socket Layer,网络安全协议。它是在传输[通信协议](TCP/IP)上实现的一种安全协议)的一种数字证书,由全球信任的证书的颁发机构 CA 验证服务器身份后颁发,将 SSL 证书安装在网站服务器上,会激活挂锁和 HTTPS 协议。SSL 证书解决了网民登录网站的信任问题,网民可以通过 SSL 证书轻松识别网站是否受到保护、是否安全的。
十六、HTTP 证书的作用是什么
- 身份认证: 安装 SSL 证书后的网站或系统会对通信双方身份进行安全认证,确保数据从正确的发送者安全传输到正确的接收者。未通过认证则无法发送或接收数据。
- 数据加密: 通过对传输数据进行加密来确保数据的完整性,确保数据在传输过程中不被改变,保护网站或系统的机密数据安全,保护用户的个人隐私信息不被泄露。
- 浏览器信任: 谷歌和火狐等浏览器对 HTTP 网站标记为
不安全警告,安装 SSL 证书可以使浏览器解除警告。
十七、h5首页为什么要做服务端渲染
浏览器渲染路线
- 请求一个 html。 -> 2. 服务端返回一个 html。 -> 3. 浏览器下载 html 里的 js/css 文件。 -> 4. 等待 js 文件下载完成。 -> 5. 等待 js 文件加载并初始化完成。 -> 6. js 代码可以运行,由 js 代码向后端请求数据(ajax/fetch)。 -> 7. 等待后端数据返回。-> 8. 客户端从无到完整地,把数据渲染为响应页面。
服务端渲染路线
- 请求一个 html。 -> 2. 服务端请求数据。 -> 3. 服务器初始渲染(服务端性能好,速度快) -> 4. 服务端返回已经有正确内容的页面。 -> 5. 客户端请求 js/css 文件。 -> 6. 等待 js 文件下载完成。 -> 7. 等待 js 记载并初始化完成。-> 8. 客户端把剩下一部分渲染完成
服务器渲染的优缺点
- 优点
- 前端耗时少。因为后端拼接完了 html, 浏览器只需要直接渲染出来。
- 有利于 SEO。因为后端有完整的 html 页面,所以爬虫更容易爬取获得信息。
- 无需占用客户端资源,即解析模版工作完全交由后端来做,客户端只要解析标准的 html 页面即可,这样对客户端的资源占用更少,尤其是移动端,也可以更省电。
- 后端生成静态化文件,即生成缓存片段,这样就可以减少数据库查询浪费的时间了,且对于数据变化不大的页面非常高效。
- 缺点
- 不利于前后端分离,开发效率低。使用服务端渲染,则无法进行分工合作,对于前端复杂度高的项目,不利于项目高效开发。如果是服务端渲染,则前端一般就是写一个静态 html 文件,再由后端修改为模板,有时候还需要双方共同修改。
- 占用服务端资源 客户端渲染的优缺点
- 优点
- 前后端分离。前端专注于 ui,后端专注于 api 开发,且前端有更多的选择性,而不需要遵循后端特定的模板。
- 体验更好。spa 的好处
- 缺点
- 首屏响应慢
- 不利于 SEO
选择? 如果是企业级网站,主要功能是展示而没有复杂的交互,并且需要良好的 SEO,则这时就可以需要使用服务端渲染;而类似后台管理页面,交互性比较强,不需要考虑 SEO,那么就可以使用客户端渲染。当然这只是常规情况下的建议。如果是强交互的网页做性能优化,想加快首屏渲染速度也可以将首屏做 服务端渲染。
十八、打包优化
移步另一文章(juejin.cn/post/722632…)
十九、Vue 中 beforeCreated 和 created 的区别
实例、组件通过new Vue() 创建出来之后会初始化事件和生命周期,然后就会执行beforeCreate钩子函数,这个时候,数据还没有挂载呢,props、data、methods等都无法使用,只是一个空壳,无法访问到数据和真实的dom,一般不做操作。
挂载数据,绑定事件等等,然后执行created函数,这个时候已经可以使用到数据,也可以更改数据,在这里更改数据不会触发updated函数,但是模板结构仍未生成。在这里可以在渲染前倒数第二次更改数据的机会,不会触发其他的钩子函数,一般可以在这里做初始数据的获取。
二十、Vue中用过哪些修饰器
- 事件修饰符
.stop
.prevent
.capture(给元素添加一个监听器,当元素发生冒泡时,先触发带有该修饰符的元素。若有多个该修饰符,则由外而内触发)
.self(当点击某个元素时会引发其父元素(父父元素、父父父元素…)的点击事件发生,使得点击某个元素时达不到想要的效果。.self修饰符可以很好的解决这一情况,.self修饰符只有在点击事件绑定的元素与当前被点击元素一致时才触发点击事件。)
.once
.passive(会告诉浏览器你不想阻止事件的默认行为.阻止默认事件,真正的目的是告诉浏览器,你可以不用去查询程序有没有阻止默认事件,也就是提前告诉浏览器程序不会阻止。例如,你有一个朋友要来你家,他如果提前告诉你,那么你可以提早准备午餐打扫卫生等,但是如果没有告诉,只有在等他进门时你才知道,那个时候你就来不及了。这也就是.passive提前告诉浏览器的原因,提早告诉,提高性能)
- 按键修饰符
.enter
.tab
.delete(捕获 删除 和 退格 键)
.esc
.space
.up
二十一、Vue 中的 Key 的作用是什么
主要用来在虚拟Dom的 diff 算法中,在新旧节点的对比时辨别 Vnode,使用 Key 时,Vue 会基于 Key 的变化重新排列元素顺序,尽可能的复用页面元素,只找出必须更新的 Dom,最终以减少 Dom 操作
- 没有 key 的操作会分为三步(使用同组件对比):
- 先获取新旧节点的长度并且取最小长度
- 遍历长度小的节点,对新旧节点依次patch(容易理解点就是对比节点类型和内容)
- 判断新旧长度,若旧的短,则新增节点,反之删除节点
- diff 算法的规则
-
首先,对比节点本身,判断是否为同一节点,如果不为相同节点,则删除该节点重新创建节点进行替换
-
如果为相同节点,进行 patchVnode,判断如何对该节点的子节点进行处理,先判断一方有子节点一方没有子节点的情况(如果新的children没有子节点,将旧的子节点移除)
-
如果都有子节点,则进行 updateChildren,判断如何对这些新老节点的子节点进行操作(diff核心)。
在对比其子节点数组时,vue对每个子节点数组使用了两个指针,分别指向头尾,然后不断向中间靠拢来进行对比,这样做的目的是尽量复用真实dom,尽量少的销毁和创建真实dom。如果发现相同,则进入和根节点一样的对比流程,如果发现不同,则移动真实dom到合适的位置。 这样一直递归的遍历下去,直到整棵树完成对比。
- 匹配时,找到相同的子节点,递归比较子节点。尽可能的复用重复出现的节点,把旧的当中没有在新的里出现的节点移除,把出现在新的节点中而旧的节点中没有的新增。
- key 是为 Vue 中 vnode 的唯一标记,通过这个 key,diff 操作可以更准确、更快速
- 将元素调换顺序,实际的 diff算法 是怎样的 Vue 不会移动 DOM 元素来匹配数据项的顺序,而是简单复用此处的每个元素。因此通过为每个列表项提供一个 key 值,来以便 Vue 跟踪元素的身份,从而高效的实现复用。这个时候 key 的作用是为了高效的更新渲染虚拟 DOM。 调换顺序时,如果使用 index 作为 key,因为不管数组的顺序怎么颠倒,index 都是 0, 1, 2...这样排列,导致 Vue 会复用错误的旧子节点,做很多额外的工作。
二十二、有没有做过组件的抽离和组件库的开发?具体做了什么工作?
- Echarts/G2 图表的二次封装。包括统一色盘、单位转换、样式统一、tooltip文本格式统一;
- 后台管理系统的 页签 组件。组件的思路是简单的,可以通过在组件内监听路由或者是通过路由守卫,将访问过的路由保存到 Vuex 中,同时存入 localStorage 避免刷新导致 vuex 数据清空的问题。因为当时需求是不限制打开的页签个数,如果使用横向滚动条的话不美观,在没有使用第三方插件的情况下采取了左右各一个箭头用来控制展示区域的滚动距离的形式。以及点击页签需要将展示区域自动滚动至该页签的可视范围内。需要合理的判断容器宽度和总宽度,以及偏移量的一些计算。
- 一套基于 elementUi 二次封装的组件库。通过封装 showCode组件 -> hightlight.js(可以使用自定义指令v-hightlight)配合
<pre>预格式化标签 和<code>标签实现示例代码的展示。还有一些部门、人员的等的业务弹出框、悬浮框,供操作人员选择。除此之外,还有一些业务布局,例如单表、左树右表、左树右列表、上下双表的布局。通过插槽的形式。还有图标库以及一些自定义指令例如复制和拖拽。公共样式的说明和工具类、ag-grid的使用说明和样式的统一
二十三、内部组件库,怎么本地开发和调试?怎么上线?本地调试有哪些方式?
-
npm link <pkg>
# 组件开发完成后,修改版本号
yarn build
yarn link
# or
# npm link
# 然后在测试的项目中
yarn link my-component-package
# or
# npm link my-component-package
-
使用
yalc进行本地调试(推荐)
yalc 是一个可以在本地模拟 npm package 发布环境的工具,官方文档。
yalc 主要本地化了一个 npm 的存储库,通过 yalc publish 可以把构建的产物发布到本地。通过 yalc add <pkg> 可以达到 npm install <pkg> 或 yarn add <pkg> 的效果。
二十四、浏览器从输入地址到页面展示的过程,发生了什么
- url解析: 首先会判断输入的是一个合法 url 还是关键词,并根据输入的内容进行相应的操作。
- 查找缓存: 浏览器会判断所请求的资源是否在浏览器缓存中,以及是否失效。如果没有失效就直接使用;如果没有缓存或失效了,就进行下一步。
- DNS解析: 此时需要获取 url 中域名对应的 IP 地址。DNS解析的过程就是寻找哪个服务器上有请求的资源。因为ip地址不容易记忆,一般会使用URL域名(如www.baidu.com)作为网址。DNS解析就是将域名翻译成IP地址的过程。
- 建立 TCP 连接: 根据 ip 地址,三次握手 与服务器建立 TCP 连接。
- 发起请求: 浏览器向服务器发起 HTTP 请求。
- 响应请求: 服务器响应 HTTP 请求,将响应的 HTML 文件返回给浏览器。
- 关闭 TCP 连接: 四次挥手 关闭 TCP 连接。
- 渲染页面: 浏览器解析 HTML 内容,并开始渲染。浏览器渲染过程如下:
- 构建 DOM 树: 词法分析然后解析成 DOM 树,DOM树是由DOM元素及属性节点组成,树的根是 document对象。
- 构建 CSS 规则树: 生成 CSS 规则树。
- 构建渲染树: 将 DOM树 和 CSS规则树 结合,构建出 渲染树
- 布局: 计算每个节点的位置。
- 绘制: 使用浏览器的 UI 接口进行绘制。
二十五、js 异步加载的方式?defer 和 async 的区别
一般情况下,当浏览器开始解析 HTML 原文件时遇到外部 <script> 标签时,会阻塞 HTML 的解析,只有在 <script> 完全下载和执行后才会继续解析 HTML,对于一些简单情况可以将 <script> 标签放在底部可以解决。如果面对一些复杂的业务情况,可以使用 defer 或者 async,二者都是 <script> 的异步执行
- defer 是在 HTML 解析完成之后才会执行,类似于将
<script>标签放置于页面底部,但是是异步加载,相较于直接放在底部可以节省时间。如果是多个,则按照加载的顺序依次执行。 - async HTML5中新增的属性,异步加载且是在加载完之后立即执行,如果 HTML 还没有加载完成则会阻塞解析。如果是多个,执行顺序和加载顺序无关。
二十六、重定向的状态码有哪些?它们的区别是什么?
- 301: 永久移动(一般是资源位置永久更改)
- 302: 发现(一般是普通的重定向需求:临时跳转)
- 303: 查看其他(基本不用)
- 307: 临时重定向(很少用,与302类似,只不过是针对POST方法的请求不允许更改方法)
- 308: 永久重定向(很少用,与301类似,只不过是针对POST方法的请求不允许更改方法)
301,302是http1.0的内容,303、307、308是http1.1的内容。 301和302本来在规范中是不允许重定向时改变请求方法的(将POST改为GET),但是许多浏览器却允许重定向时改变请求方法(这是一种不规范的实现)。 303的出现正是为了给上面的301,302这种行为作出个规范(将错就错吧),也就是允许重定向时改变请求方法。此外303响应禁止被缓存。 大多数的浏览器处理302响应时的方式恰恰就是上述规范要求客户端处理303响应时应当做的,所以303基本用的很少,一般用302。307和308的出现也是给上面的行为做个规范,不过是不允许重定向时改变请求方法。
二十七、js的原型链
概念:
构造函数: 是用来创建对象的函数,通过 new 关键字来声明。
原型对象: 每一个函数在创建的时候,系统都会给分配一个对象,这个对象就是原型对象(prototype)。
实例对象: 构造函数中通过 new 关键字返回的对象就是实例对象。
- 每个对象都有一个
__proto__属性,该属性指向自己的原型对象。 - 每个构造函数都有一个
prototype属性,该属性指向实例对象的原型对象,用来存放共有属性和方法的地址。 - 原型对象里的
constructor指向构造函数本身。主要用于记录该对象引用于哪个构造函数,它可以让原型对象重新指向原来的构造函数。
如下图:
JavaScript 常被描述为一种基于原型的语言 (prototype-based language) ——每个对象拥有一个原型对象,对象以其原型为模板、从原型继承方法和属性。原型对象也可能拥有原型,并从中继承方法和属性,一层一层、以此类推。这种关系常被称为原型链 (prototype chain) ,它解释了为何一个对象会拥有定义在其他对象中的属性和方法。这些属性和方法定义在 Object 的构造器函数 (constructor functions) 之上的prototype属性上,而非对象实例本身
每个对象都有自己的原型对象,而原型对象本身,也有自己的原型对象,从而形成了一条原型链条。
当试图访问一个对象的属性时,它不仅仅在该对象上搜寻,还会搜寻该对象的原型,以及该对象的原型的原型,依次层层向上搜索,直到找到一个名字匹配的属性或到达原型链的末尾。如果仍然没有找到则返回 null。
二十八、闭包以及实际开发中的应用。
- 概念
在 JS 忍者秘籍(P90)中对闭包的定义:闭包允许函数访问并操作函数外部的变量。红宝书上对于闭包的定义:闭包是指有权访问另外一个函数作用域中的变量的函数。 MDN 对闭包的定义为:闭包是指那些能够访问自由变量的函数。这里的自由变量是外部函数作用域中的变量。
- 形成闭包的原因
内部的函数存在外部作用域的引用就会导致闭包。 return f就是一个表现形式。
- 应用场景
- 函数作为参数
var a = '林一一'
function foo(){
var a = 'foo'
function fo(){
console.log(a)
}
return fo
}
function f(p){
var a = 'f'
p()
}
f(foo())
/* 输出
* foo
/
- setTimeout 传递参数
//原生的setTimeout传递的第一个函数不能带参数
setTimeout(function(param){
alert(param)
},1000)
function a(param) {
return function() {
console.log(param)
}
}
var b = a(true)
setTimeout(() => {
b()
}, 1000)
- 防抖节流
// 节流
function throttle(fn, timeout) {
let timer = null
return function (...arg) {
if(timer) return
timer = setTimeout(() => {
fn.apply(this, arg)
timer = null
}, timeout)
}
}
// 防抖
function debounce(fn, timeout){
let timer = null
return function(...arg){
clearTimeout(timer)
timer = setTimeout(() => {
fn.apply(this, arg)
}, timeout)
}
}
- 循环赋值
for(var i = 0; i<10; i++){
(function(j){
setTimeout(function(){
console.log(j)
}, 1000)
})(i)
}
因为存在闭包的原因上面能依次输出1~10,闭包形成了10个互不干扰的私有作用域。将外层的自执行函数去掉后就不存在外部作用域的引用了,输出的结果就是连续的 10。为什么会连续输出10,因为 JS 是单线程的遇到异步的代码不会先执行(会入栈),等到同步的代码执行完
i++到 10时,异步代码才开始执行此时的i=10输出的都是 10。
- 为多个组件独立属性 假如我现在要在页面中使用echarts画6个线状图,需要6个容器
需要为每个容器元素声明一个独立id,不然会混乱
constructor(){
this.state = {id: "EchartsLine"+Util.clourse()};
}
componentDidMount() {
this.myEChart =echarts.init(document.getElementById(this.state.id));//不同id
}
<div
id={this.state.id}
className='echarts-line'>
</div>
clourse(){
let clourse = (function(){
var a = 1;
return function(){
return a++;
}
})(this);
this.clourse = clourse;
}
//使用数字命名 不用害怕被篡改
- 使用闭包要注意什么
容易导致内存泄漏。闭包会携带包含其它的函数作用域,因此会比其他函数占用更多的内存。过度使用闭包会导致内存占用过多,所以要谨慎使用闭包。
- 内存泄漏
内存泄露是指当一块内存不再被应用程序使用的时候,由于某种原因,这块内存没有返还给操作系统或者内存池的现象.内存泄漏说白了就是本该被回收的内存因为一些异常没有被回收掉,而一直存在于内存中占用内存, 虽然说js有垃圾自动回收机制,但是如果我们的代码写法不当,会让变量一直处于“进入环境”的状态.内存泄漏可能会导致应用程序卡顿或者崩溃
垃圾回收机制: JavaScript的解释器可以检测到何时程序不再使用一个对象了,当他确定了一个对象是无用的时候,他就知道不再需要这个对象,可以把它所占用的内存释放掉了.
导致内存泄漏的情况 1、意外的全局变量:由于使用未声明的变量,而意外的创建了一个全局变量,而使这个变量一直留在内存中无法被回收
2、被遗忘的计时器或回调函数:设置了 setInterval 定时器,而忘记取消它,如果循环函数有对外部变量的引用的话,那么这个变量会被一直留在内存中,而无法被回收。
3、DOM中的addEventLisner 函数及派生的事件监听.脱离 DOM 的引用:获取一个 DOM 元素的引用,而后面这个元素被删除,由于一直保留了对这个元素的引用或监听,所以它也无法被回收。解决方案: removeEventListener
4、闭包:不合理的使用闭包,从而导致某些变量一直被留在内存当中。
二十九、flex-grow 和 flex-shrink 代表什么含义?
flex-grow: 属性定义项目放大比例,默认为0,即如果存在剩余空间,也不放大。 flex-shrink: 属性定义了项目的缩小比例,即如果空间不足,也不缩小。 flex-basic: 在配置上面两个属性前元素的基础大小,优先级比 width 高。
三十、CommandJS 与 ESModule 的区别
- CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。
- CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。
- CommonJS 模块的
require()是同步加载模块,ES6 模块的import命令是异步加载,有一个独立的模块依赖的解析阶段。
// CommonJS 导出对象
module.exports = { name: 'ff', age: 16 }
// CommonJS 引入
const obj = require('文件路径')
// ES Module 导出对象
export const obj = { name: 'ff', age: 16 }
// ES Module 引入
import { name } from 'xxx'
三十一、Tree-shaking 原理
- 概念
在 webpack 项目中,有一个入口文件,相当于一棵树的主干,入口文件有很多依赖的模块,相当于树枝。实际情况中,虽然依赖了某个模块,但其实只使用其中的某些功能。通过 tree-shaking,将没有使用的模块摇掉,这样来达到删除无用代码的目的。
- 原理
Tree-shaking 是一种基于 ESModule 规范的 Dead Code Elimination 技术,它会在运行过程中静态分析模块之间的导入导出,确定 ESM 模块中哪些导出值未曾使用,并将其删除,以此来实现打包产物的优化。简单来讲,就是在保持代码运行结果不变的前提下,去除无用的代码。比如在 Vue2 中,无论我们使用什么功能,它们最终都会出现在生产代码中。主要原因是 Vue实例在项目中是单例的,捆绑程序无法检测到该对象的哪些属性在代码中被使用到。而 Vue3 引入 Tree-shaking 特性,将 全局 API 进行分块。如果你不使用某些功能,它们将不会包含在你的基础包中。比如你要用 watch 就是 import { watch } from 'vue',其他的例如 computed 没用到就不会给你打包以此减少打包体积。
三十二、ESModule模块化 是怎么解决循环引用的问题的
参考(es6.ruanyifeng.com/#docs/modul…)
三十三、作用域与作用域链
作用域是在运行时代码中的某些特定部分中变量,函数和对象的可访问性。换句话说,作用域决定了代码区块中变量和其他资源的可见性。ES6 之前的 JavaScript 没有块级作用域,只有全局作用域和函数作用域。ES6 的到来,为我们提供了块级作用域。
作用域链指的是作用域与作用域之间形成的链条。当我们查找一个当前作用域没有定义的变量(自由变量)的时候,就会向上一级作用域查找,如果上一级也没有,就再一层一层向上寻找,直到找到全局作用域还是没找到,就宣布放弃。这种一层一层的关系,就是作用域链。
三十四、Vite 和 Webpack 对比
1、快速冷启动
Vite 只启动一台静态页面的服务器,不会打包全部项目文件代码,服务器根据客户端的请求加载不同的模块处理,实现按需加载.
而我们所熟知的 webpack 则是,一开始就将整个项目都打包一遍,再开启dev-server,如果项目规模庞大,打包时间必然很长.
默认的构建目标浏览器是能在script标签上支持原生ESM和原生ESM动态导入.
webpack构建的项目基于入口的js文件进行编译,比如entry里的index.js路径.vite是基于src目录底下的index.html进行编译的
<script type="module" src="/src/main.ts"></script>
- 热模块更新
对于热更新问题,Vite 采用立即编译当前修改文件的办法,同时结合 Vite 的缓存机制(http缓存 ->Vite内置缓存,加载更新后的文件内容,热更新速度巨快.
- 初始化项目速度快
通过传统的vue cli 的vue create命令构建的webpack项目需要下载很多的依赖,所以安装的时间也更长.启动速度也比vite慢很多。
三十五、Vue 的路由模式
hash的路由地址上有 #,history模式没有; 在做回车刷新的时候,hash模式会加载对应页面,history模式不会。
hash模式支持低版本浏览器,history不支持,因为是history是h5新增Api;
hash不会重新加载页面,单页面应用必备;
history是有历史记录的,h5 也新增了 history.pushState 和 replaceState 方法用于对历史记录进行修改的功能。history模式重写后 URL路径 并不包含原有路径文件的访问地址,比如进入/admin的一个子路由/admin/xxx,刷新后地址只有/admin,所以刷新会404。在生产环境需要配合服务器的转发规则重写,将admin后的路径进行重写。用以支持history模式路由的加载。
三十六、js事件循环
js 的事件循环就是浏览器渲染主线程的过程。
- 一开始时,主线程会进入一个无限循环;
- 每一次循环,都会检查一下消息队列是否有任务存在。如果有,则取出第一个任务执行,执行完再进入下一次循环,如果没有,则进入休眠状态。
- 其他线程可以随时往消息队列中添加任务。新任务会被添加到队尾。添加新任务时,如果主线程处于休眠状态,则会被唤醒。
在 js 中任务会分为同步任务和异步任务。
如果是同步任务,则会在主线程(也就是 js 引擎线程)上进行执行,形成一个执行栈。但是一旦遇到异步任务,则会将这些异步任务交给异步模块去处理,然后主线程继续执行后面的同步代码。 一旦执行栈中所有的同步任务执行完毕,就代表着当前的主线程(js 引擎线程)空闲了,系统就会读取任务队列,将可以运行的异步任务添加到执行栈中,开始执行。
js引擎遇到一个异步事件之后不会一直等待事件的返回结果,而是将事件挂起,继续执行执行栈中的其他任务。
当异步事件返回结果时,js将异步事件callback函数放入队列中,被放入队列中的异步事件不会立即回调,等到当前执行栈中的任务都执行完成,处于闲置状态的主线程按照队列顺序将处于首位事件的callback函数放入执行栈中,执行该函数的同步代码,如果遇到了异步事件,同样也会将其回调函数放入事件队列中......
在 js 中,任务队列中的任务又可以被分为 2 种类型:宏任务(macrotask)与微任务(microtask)
1、js宏任务有:<script> 整体代码、setTimeout、setInterval、setImmediate、Ajax、DOM事件
2、js微任务有:process.nextTick、MutationObserver、Promise.then catch finally
微任务优先级比宏任务优先级高。
三十七、谈一谈对 MVVM 的理解
MVVM 是 Model-View-ViewModel 的缩写。MVVM 是一种设计思想。 Model 层代表数据模型,也可以在 Model 中定义数据修改和操作的业务逻辑; View 代表 UI 组件,它负责将数据模型转化成 UI 展现出来,ViewModel 是一个同步 View 和 Model 的对象 在 MVVM 架构下,View 和 Model 之间并没有直接的联系,而是通过 ViewModel 进行交互, Model 和 ViewModel 之间的交互是双向的, 因此 View 数据的变化会同步到 Model 中,而 Model 数据的变化也会立即反应到 View 上。 对 ViewModel 通过双向数据绑定把 View 层和 Model 层连接了起来,而 View 和 Model 之间的 同步工作完全是自动的,无需人为干涉,因此开发者只需关注业务逻辑,不需要手动操作 DOM,不需要关注数据状态的同步问题,复杂的数据状态维护完全由 MVVM 来统一管理。