Appearance
工程化 & 项目优化(实践)
本章管什么:工程落地怎么讲——Vite vs Webpack(为什么快)、Axios 封装、项目级性能清单、懒加载 / 虚拟列表、Git、线上排查。先背「区别 + 手段」,被追问原理再进构建章。
本章不管什么
内容 去哪看 框架怎么选、Vue2 vs Vue3、Vite vs Webpack(面试选型口述) 架构 Webpack 依赖图、Loader/Plugin、Tree-shaking、HMR、Babel 机制 构建原理 重排 / 重绘 / 合成(浏览器渲染层) 浏览器 Vue3 框架内 API 优化( v-memo、shallowRef 等)Vue3 小程序分包 / 主包体积 UniApp 和 构建 的分工:这里偏选型与清单,那里偏打包器原理。
Webpack 和 Vite 的区别?Vite 为什么启动、打包更快?
面试选型口述也可对照 架构 · Vite vs Webpack。本节把 ESM 和「为什么快」讲细。
核心区别
| Webpack | Vite | |
|---|---|---|
| 开发启动 | 先打包再启动,项目越大越慢 | 不先全量打包,浏览器按需加载 |
| 热更新 | 改动常牵涉较大重编译 | 改哪个模块更新哪个,通常更快 |
| 生产打包 | 自身生态打包 | 默认用 Rollup 打包 |
| 定位 | 老牌打包器,插件生态大 | 现代开发服务器 + 打包方案 |
先搞懂 ESM( 常被追问)
ESM = ECMAScript Modules,就是浏览器原生支持的模块语法:
js
import { foo } from './foo.js'
export function bar() {}和以前打包思路的对比:
| 传统打包(Webpack 开发期常见做法) | 浏览器原生 ESM | |
|---|---|---|
| 怎么用模块 | 先把很多文件打成少数几个 bundle,浏览器再加载 | 浏览器直接认识 import,按文件去请求 |
| 启动成本 | 项目越大,要分析、打包的模块越多,启动越慢 | 不用先全量打包,用到哪个再请求哪个 |
| 模块格式 | 常混着 CommonJS(require),浏览器不能直接跑 | 标准 import / export,现代浏览器可直接跑 |
所以 Vite 「开发快」的前提,其实是:开发阶段把活分给浏览器原生 ESM,打包器不再每次启动都全量构建。
注意:生产环境一般仍会打包(网络请求数、兼容性、压缩优化),不是上线也裸奔几百个 ESM 文件。
Vite 为什么快
开发快:
- 源码走原生 ESM:Vite 当开发服务器,浏览器通过
import按需拉模块,不用等整个应用打完包再打开页面 - 依赖预构建(Premundle):
node_modules里很多包是 CommonJS / 文件零碎,浏览器吃力;Vite 用 esbuild 先把它们转成少量 ESM(esbuild 用 Go 写,比传统 JS 打包器快很多) - 热更新更轻:改某个源码文件,主要更新相关模块;依赖已被预构建并缓存,不用整包重来
生产打包: 交给 Rollup,做 Tree-shaking、拆包、压缩等;开发体验靠 ESM + esbuild,最终产物质量靠 Rollup。
🎤 口述精简版
先说 ESM:就是浏览器原生的 import / export,可以按需加载模块,不必先打成一个大包。Webpack 开发往往先打包再启动,项目一大就慢;Vite 开发让浏览器直接跑 ESM,源码按需请求,第三方依赖用 esbuild 预构建成 ESM,所以冷启动和热更新都快。生产打包还是交给 Rollup。可以记:开发快靠原生 ESM + esbuild,打包质量靠 Rollup。
简述 Axios 二次封装完整思路(请求拦截、响应拦截、token 处理、错误处理)
封装目标
统一 baseURL、超时、token、错误提示和业务码处理,业务里只关心「调接口、拿数据」。
完整思路
- 创建实例:
axios.create({ baseURL, timeout }) - 请求拦截
- 从本地取 token,写入
Authorization(或项目约定头) - 可加 loading、请求序号、防重复标记
- 从本地取 token,写入
- 响应拦截
- 先看 HTTP 状态;再看业务码(如
code === 0) - 成功:直接
return data,让调用方少拆一层 - 失败:统一 toast / 消息提示,再
Promise.reject
- 先看 HTTP 状态;再看业务码(如
- token 处理
- 请求头自动带 token
- 401 / token 失效:跳登录,或走无感刷新(见下一题)
- 错误处理
- 网络错误、超时、5xx:给可读文案
- 取消请求、重复请求:按需静默,避免乱弹窗
js
// 伪代码结构
instance.interceptors.request.use((config) => {
const token = getToken()
if (token) config.headers.Authorization = `Bearer ${token}`
return config
})
instance.interceptors.response.use(
(res) => {
const { code, data, message } = res.data
if (code === 0) return data
showError(message)
return Promise.reject(res.data)
},
(err) => {
if (err.response?.status === 401) toLogin()
showError(err.message)
return Promise.reject(err)
}
)🎤 口述精简版
二次封装就三块:create 出实例;请求拦截塞 token、统一配置;响应拦截拆业务码,成功抛 data,失败统一提示,401 去登录或刷新 token。业务页面只调用封装好的方法,不重复写头和错误处理。
token 过期如何自动刷新、无感刷新?
目标
access token 过期时,用 refresh token 换新 token,排队中的请求等刷新完自动重试,用户尽量无感。
常见流程
- 响应拦截发现 token 过期(如 401 或业务码过期)
- 如果当前没有在刷新:调用刷新接口,标记
isRefreshing = true - 刷新期间其它失败请求先推进队列,先不立刻失败
- 刷新成功:存新 token,把队列里请求带上新 token 重放
- 刷新失败:清空登录态,跳登录页
text
请求 A 401 → 开始 refresh
请求 B 401 → 进队列等待
refresh 成功 → A、B 用新 token 重试注意点
- 刷新接口本身不要再走「过期就刷新」的死循环
- 多请求并发过期时,只刷新一次,其余排队
- refresh token 也过期:只能重新登录
- 安全:refresh token 存储和轮换策略按后端约定
🎤 口述精简版
无感刷新就是 access 过期后,拦截器里用 refresh 换新票;同时只刷新一次,别的请求先排队,换成功再带着新 token 重试。refresh 也挂了就清登录态去登录页。
前端项目常见性能优化方案(加载、渲染、打包、运行优化)
| 方向 | 常见手段 |
|---|---|
| 加载优化 | 路由/组件懒加载、图片懒加载与压缩、CDN、HTTP 缓存、SSR/预渲染、骨架屏 |
| 渲染优化 | 减少重排重绘、合成动画(transform/opacity)、虚拟列表、避免无意义整树更新 |
| 打包优化 | 拆包(异步 chunk)、Tree-shaking、压缩、分析包体积、外部化大依赖 |
| 运行优化 | 防抖节流、Web Worker、及时销毁监听/定时器、合理缓存接口数据 |
可再补一条:先量再优化——用 Lighthouse、Performance、打包分析定位瓶颈,别盲目改。
🎤 口述精简版
性能优化四块说:加载靠懒加载和缓存;渲染靠少重排、虚拟列表;打包靠拆包压缩;运行靠防抖和别泄漏。先用工具找到慢在哪,再对症下药。
路由懒加载、组件懒加载的实现方式和作用?
作用
首屏少加载用不到的 JS,减小首包,加快首屏;按路由/交互再加载对应代码。
实现方式
路由懒加载(Vue Router):
js
{
path: '/about',
component: () => import('../views/About.vue')
}组件懒加载:
js
const Heavy = defineAsyncComponent(() => import('./Heavy.vue'))配合 webpack magic comment / Vite 动态 import,会打成独立 chunk。
注意
- 懒加载会多一次请求,极小页面不必硬拆
- 可配合 Loading / Error 组件提升体验
- 关键路径可预加载(
prefetch/ 鼠标悬停再拉)
🎤 口述精简版
懒加载就是别把所有页面打进首包。路由写成 () => import(...),组件用异步组件。作用是减小首屏体积,进到对应路由再下载。
大数据列表渲染卡顿如何优化?虚拟列表原理?
卡顿原因
一次挂载上千上万 DOM,创建和排版成本高,滚动时也容易卡。
优化手段
- 虚拟列表(首选)
- 分页 / 无限滚动一次少渲染
- 图片懒加载、骨架屏
- 稳定 key,避免整表重渲染
- 能用 canvas 的特殊场景(如复杂表格)另说
虚拟列表原理
只渲染「可视区域 + 上下缓冲」里的那几条,其余不建 DOM。
- 容器固定高度,可滚动
- 根据
scrollTop算当前该显示的起始下标 - 只渲染这一屏数据
- 用占位总高度(或上下 padding)撑出完整滚动条
- 滚动时更新起始下标,复用/替换节点
🎤 口述精简版
大列表卡是因为 DOM 太多。虚拟列表只渲染看得见的那一段,用总高度撑滚动条,根据滚动位置换数据。这样 Dom 数量几乎固定,滚动就顺了。
前端如何处理接口并发、接口重复请求问题?
接口并发
- Promise.all:多个都成功才继续;一个失败整体失败
- Promise.allSettled:都结束(成功失败都要结果)时用
- 限制并发:队列控制同时进行中的请求数(上传多文件常用)
- 有依赖的请求:串行
await,别硬并发
重复请求
同一接口短时间被点多次(搜索、连点提交):
- 取消上一次:Axios
AbortController/CancelToken,新请求来先 abort 旧的 - 请求锁 / loading:进行中禁止再次触发
- 防抖 / 节流:搜索用防抖,按钮用节流
- 幂等与去重:按 url+参数做 in-flight 缓存,相同请求复用同一个 Promise
🎤 口述精简版
并发用 all / allSettled,要限流就自己做请求队列。重复请求要么 abort 上一次,要么加锁和防抖,搜索防抖、提交加锁最常见。
Git 常用命令?团队分支开发规范?
常用命令
| 场景 | 命令 |
|---|---|
| 状态 / 差异 | git status、git diff |
| 提交 | git add、git commit |
| 同步 | git pull、git push、git fetch |
| 分支 | git branch、git checkout / git switch、git merge |
| 临时存档 | git stash |
| 日志 / 回退 | git log、git reset、git revert(线上更安全) |
| 变基(慎用) | git rebase |
常见分支规范
text
main / master 生产
develop 开发集成分支
feature/xxx 功能分支
hotfix/xxx 线上紧急修复
release/xxx 发版准备(部分团队使用)流程白话:从 develop(或 main)拉 feature → 开发提交 → 提 MR/PR → 评审合并 → 测试 → 上线。hotfix 从生产拉,修完合回生产和发展分支。
提交信息建议 Conventional Commits:feat / fix / docs / refactor 等。
🎤 口述精简版
日常就是 status、add、commit、pull、push,分支用 feature 开发、hotfix 救急。团队一般 main 保生产,develop 做集成,功能分支开发完合回去。线上问题优先 revert,少对共享分支强推。
前端如何排查线上 bug?常用调试手段?
排查思路
- 复现:哪个页面、哪个账号、哪个机型/浏览器,能否稳定复现
- 看报错:监控平台(Sentry 等)的堆栈、面包屑、用户轨迹
- 看数据:接口是否失败、字段是否变更、权限/灰度是否命中
- 看版本:是否新发版引入,对比发版记录与 sourcemap
- 缩小范围:本地切同环境配置复现;不行就加日志、走预发
常用手段
- Chrome DevTools:Network、Console、Performance、Application(存储/cookie)
- 源码映射 sourcemap(注意生产是否上传到监控,而非暴露给公众)
- 代理抓包、切换环境对比
- 特性开关 / 灰度回滚快速止损
v-console等移动端调试(按需)
🎤 口述精简版
线上问题先复现和看监控堆栈,再查接口和发版。DevTools 看网络和控制台,有 sourcemap 能对上源码。先止损(回滚/关开关),再修根因。
Vue 项目中常见 bug 及解决方案?
| 常见问题 | 原因 / 表现 | 解决思路 |
|---|---|---|
| 改了数据页面不更新 | Vue2 无法检测对象新属性、数组下标直接赋值等 | Vue.set / 替换新对象;Vue3 Proxy 好很多,仍注意别解构丢响应式 |
| 路由变了组件没刷新 | 同一组件复用,生命周期不重新走 | 监听 route,或给 router-view 加 :key |
| 子改父 props | 单向数据流被破坏 | emit 事件;或明确 v-model |
| 列表 key 乱用 | 用 index 当 key 导致错乱/动画异常 | 用稳定唯一 id |
| 异步组件白屏 | 加载失败或未处理 loading | defineAsyncComponent 配 loading/error;检查路径 |
| 内存泄漏 | 定时器、监听、Bus 未清理 | onBeforeUnmount / beforeDestroy 里清掉 |
| 跨组件通信混乱 | 层层 props 钻、事件乱飞 | provide/inject、Pinia/Vuex |
| scoped 改不了子组件样式 | 样式隔离 | :deep() 穿透,或改子组件暴露类名 |
🎤 口述精简版
Vue 常见坑:数据不更新(响应式限制)、路由复用不刷新、乱改 props、key 用 index、监听没销毁。解决分别是:按响应式方式改数据、监听路由或加 key、emit 通知父级、用稳定 id、在销毁钩子里清理。