Skip to content

工程化 & 项目优化(实践) ​

本章管什么:工程落地怎么讲——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 和「为什么快」讲细。

核心区别 ​

WebpackVite
开发启动先打包再启动,项目越大越慢不先全量打包,浏览器按需加载
热更新改动常牵涉较大重编译改哪个模块更新哪个,通常更快
生产打包自身生态打包默认用 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 为什么快 ​

开发快:

  1. 源码走原生 ESM:Vite 当开发服务器,浏览器通过 import 按需拉模块,不用等整个应用打完包再打开页面
  2. 依赖预构建(Premundle):node_modules 里很多包是 CommonJS / 文件零碎,浏览器吃力;Vite 用 esbuild 先把它们转成少量 ESM(esbuild 用 Go 写,比传统 JS 打包器快很多)
  3. 热更新更轻:改某个源码文件,主要更新相关模块;依赖已被预构建并缓存,不用整包重来

生产打包: 交给 Rollup,做 Tree-shaking、拆包、压缩等;开发体验靠 ESM + esbuild,最终产物质量靠 Rollup。

🎤 口述精简版 ​

先说 ESM:就是浏览器原生的 import / export,可以按需加载模块,不必先打成一个大包。Webpack 开发往往先打包再启动,项目一大就慢;Vite 开发让浏览器直接跑 ESM,源码按需请求,第三方依赖用 esbuild 预构建成 ESM,所以冷启动和热更新都快。生产打包还是交给 Rollup。可以记:开发快靠原生 ESM + esbuild,打包质量靠 Rollup。

简述 Axios 二次封装完整思路(请求拦截、响应拦截、token 处理、错误处理) ​

封装目标 ​

统一 baseURL、超时、token、错误提示和业务码处理,业务里只关心「调接口、拿数据」。

完整思路 ​

  1. 创建实例:axios.create({ baseURL, timeout })
  2. 请求拦截
    • 从本地取 token,写入 Authorization(或项目约定头)
    • 可加 loading、请求序号、防重复标记
  3. 响应拦截
    • 先看 HTTP 状态;再看业务码(如 code === 0)
    • 成功:直接 return data,让调用方少拆一层
    • 失败:统一 toast / 消息提示,再 Promise.reject
  4. token 处理
    • 请求头自动带 token
    • 401 / token 失效:跳登录,或走无感刷新(见下一题)
  5. 错误处理
    • 网络错误、超时、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,排队中的请求等刷新完自动重试,用户尽量无感。

常见流程 ​

  1. 响应拦截发现 token 过期(如 401 或业务码过期)
  2. 如果当前没有在刷新:调用刷新接口,标记 isRefreshing = true
  3. 刷新期间其它失败请求先推进队列,先不立刻失败
  4. 刷新成功:存新 token,把队列里请求带上新 token 重放
  5. 刷新失败:清空登录态,跳登录页
text
请求 A 401 → 开始 refresh
请求 B 401 → 进队列等待
refresh 成功 → A、B 用新 token 重试

注意点 ​

  • 刷新接口本身不要再走「过期就刷新」的死循环
  • 多请求并发过期时,只刷新一次,其余排队
  • refresh token 也过期:只能重新登录
  • 安全:refresh token 存储和轮换策略按后端约定

🎤 口述精简版 ​

无感刷新就是 access 过期后,拦截器里用 refresh 换新票;同时只刷新一次,别的请求先排队,换成功再带着新 token 重试。refresh 也挂了就清登录态去登录页。

前端项目常见性能优化方案(加载、渲染、打包、运行优化) ​

浏览器「为什么会卡」→ 重排重绘;Vue 组件层手段 → Vue3 性能;这里按项目四块报清单。

方向常见手段
加载优化路由/组件懒加载、图片懒加载与压缩、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,创建和排版成本高,滚动时也容易卡。

优化手段 ​

  1. 虚拟列表(首选)
  2. 分页 / 无限滚动一次少渲染
  3. 图片懒加载、骨架屏
  4. 稳定 key,避免整表重渲染
  5. 能用 canvas 的特殊场景(如复杂表格)另说

虚拟列表原理 ​

只渲染「可视区域 + 上下缓冲」里的那几条,其余不建 DOM。

  1. 容器固定高度,可滚动
  2. 根据 scrollTop 算当前该显示的起始下标
  3. 只渲染这一屏数据
  4. 用占位总高度(或上下 padding)撑出完整滚动条
  5. 滚动时更新起始下标,复用/替换节点

🎤 口述精简版 ​

大列表卡是因为 DOM 太多。虚拟列表只渲染看得见的那一段,用总高度撑滚动条,根据滚动位置换数据。这样 Dom 数量几乎固定,滚动就顺了。

前端如何处理接口并发、接口重复请求问题? ​

接口并发 ​

  • Promise.all:多个都成功才继续;一个失败整体失败
  • Promise.allSettled:都结束(成功失败都要结果)时用
  • 限制并发:队列控制同时进行中的请求数(上传多文件常用)
  • 有依赖的请求:串行 await,别硬并发

重复请求 ​

同一接口短时间被点多次(搜索、连点提交):

  1. 取消上一次:Axios AbortController / CancelToken,新请求来先 abort 旧的
  2. 请求锁 / loading:进行中禁止再次触发
  3. 防抖 / 节流:搜索用防抖,按钮用节流
  4. 幂等与去重:按 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?常用调试手段? ​

排查思路 ​

  1. 复现:哪个页面、哪个账号、哪个机型/浏览器,能否稳定复现
  2. 看报错:监控平台(Sentry 等)的堆栈、面包屑、用户轨迹
  3. 看数据:接口是否失败、字段是否变更、权限/灰度是否命中
  4. 看版本:是否新发版引入,对比发版记录与 sourcemap
  5. 缩小范围:本地切同环境配置复现;不行就加日志、走预发

常用手段 ​

  • 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
异步组件白屏加载失败或未处理 loadingdefineAsyncComponent 配 loading/error;检查路径
内存泄漏定时器、监听、Bus 未清理onBeforeUnmount / beforeDestroy 里清掉
跨组件通信混乱层层 props 钻、事件乱飞provide/inject、Pinia/Vuex
scoped 改不了子组件样式样式隔离:deep() 穿透,或改子组件暴露类名

🎤 口述精简版 ​

Vue 常见坑:数据不更新(响应式限制)、路由复用不刷新、乱改 props、key 用 index、监听没销毁。解决分别是:按响应式方式改数据、监听路由或加 key、emit 通知父级、用稳定 id、在销毁钩子里清理。