工程化题的分水岭在于能否说清"这个配置解决了什么真实问题"。这份清单把每个考点压成结论、机制和踩坑后果三段。
工程化没有标准答案,只有取舍。回答时按四步走:
| 步骤 | 说法 | 示例 |
|---|---|---|
| 定位问题 | 先说它在解决什么痛点 | 浏览器不认识 TS/JSX |
| 给出机制 | 说清工具在哪个阶段做了什么 | 编译期转译,不是运行时 |
| 说明代价 | 主动提成本和副作用 | 构建变慢、sourcemap 变大 |
| 给出判据 | 什么场景选哪个 | 大型老项目 vs 新项目 |
只报配置名不报版本约束、只讲优点不讲代价,是这一块最常见的失分方式。
一句话:浏览器只认有限的 HTML/CSS/JS,构建工具负责把开发态的代码翻译成可部署、可缓存、体积合理的产物。
| 职责 | 具体问题 |
|---|---|
| 模块化 | 浏览器原生 ESM 之外还有 CJS、CSS @import、图片引用 |
| 语法兼容 | TS、JSX、新语法要按 browserslist 降级 |
| 资源处理 | CSS 预处理、图片压缩、字体、静态资源指纹 |
| 产物优化 | 压缩、tree shaking、代码分割、chunk 拆分、hash |
| 开发体验 | HMR、sourcemap、代理、环境变量注入 |
webpack 与 vite 的模型差异
| 维度 | webpack | vite |
|---|---|---|
| 开发态 | 先打包出 bundle 再起服务 | 原生 ESM 按需编译,依赖用 esbuild 预构建 |
| 启动速度 | 随项目规模线性增长 | 近乎常数,只编译当前请求的模块 |
| 热更新 | 重新构建受影响 chunk | 精确到模块,边界清晰时只失效该模块 |
| 生产构建 | 自身完成 | 交给 Rollup |
| 代价 | 配置复杂、冷启动慢 | 首屏请求数多、CJS 依赖需预构建、生态插件不如 webpack 全 |
追问:vite 生产为什么不用原生 ESM——请求数太多、没有 tree shaking 与 chunk 拆分,HTTP/2 也扛不住深层依赖瀑布。
一句话:靠 ESM 的静态结构,在打包时删掉没有被引用的导出。
三个前提
import/export 在顶层,模块路径是字面量,能被静态分析。CJS 的 require 可写在条件里,做不到。package.json 的 sideEffects 字段让打包器知道哪些文件不能删。usedExports,最终由 Terser/esbuild 删除死代码;开发模式为了调试一般不删。sideEffects 写错的后果
false:打包器认为所有文件都无副作用,import './polyfill'、import './global.css'、注册自定义元素的模块会被整段删掉,运行时报 xxx is not defined。"sideEffects": ["*.css", "./src/polyfill.ts"],其余文件按无副作用处理。module 指向 ESM、main 指向 CJS,打包器优先 module 才能摇;只提供 CJS 时只能靠 usedExports 做有限删除。追问:为什么 import { a } from './utils' 有时整包都进不来——utils 里有顶层副作用,或 babel 把 ESM 转成了 CJS,导致静态分析失效。
一句话:CJS 是运行时同步加载、值拷贝;ESM 是编译时静态解析、实时绑定。
| 维度 | CommonJS | ESM |
|---|---|---|
| 加载时机 | 运行时,require 执行到才加载 | 编译时解析依赖图,再执行 |
| 静态分析 | 难,路径可动态拼接 | 易,支持 tree shaking |
| 导出本质 | module.exports 对象的属性,值拷贝 | 实时绑定(live binding),只读 |
顶层 this | module.exports | undefined |
| 循环依赖 | 返回未完成的 exports,可能拿到 undefined | 提升声明可用,但访问未初始化变量会命中 TDZ |
| 异步能力 | 无 | 支持 import() 与 top-level await |
循环依赖的差异要能举例
// a.js
exports.done = false;
const b = require('./b'); // b 此时拿到的是 { done: false }
exports.done = true;
// b.js
const a = require('./a'); // 拿到 a 的半成品 exports
console.log(a.done); // false
ESM 下 b 看到的是 live binding,值会随 a 的执行更新,但若在初始化前读取仍会报 "Cannot access before initialization"。
追问:为什么打包后 this 变了——Babel 把 ESM 转 CJS 时会给顶层 this 补 void 0,行为是对齐规范的。
一句话:版本号决定"能升到哪",lock 文件决定"这次装到哪",peerDependencies 决定"和谁共用一份"。
^1.2.3:允许 >=1.2.3 <2.0.0,锁 major。~1.2.3:允许 >=1.2.3 <1.3.0,锁 minor。1.2.3:依赖方最省心,库作者最常被抱怨。lock 文件的作用:记录完整依赖树、解析地址与 integrity 校验值,保证不同机器、不同时间装出同一棵树;CI 用 npm ci 严格按 lock 安装,npm install 则可能更新 lock。
peerDependencies 解决什么
插件(如 eslint-plugin-react、react-dom)声明"我需要宿主项目的 React 17–19"。包管理器据此提示版本不匹配,并在 pnpm/npm 7+ 中避免装第二份 React——两份 React 会导致 hooks 报 "Invalid hook call"、Context 读不到。
幽灵依赖
代码里 import _ from 'lodash',但 package.json 里没写,只是恰好被别的包依赖并被 npm/yarn 扁平化提升到顶层 node_modules。换包管理器、上游删依赖、版本变化都会突然报错。pnpm 用"只暴露声明过的依赖"的 node_modules 结构从根上消除,代价是要处理一些老库的软链接兼容问题。
一句话:错误、性能、行为三条线采集,采样上报,再用 sourcemap 把压缩后的堆栈还原。
错误捕获
| 类型 | 方式 | 注意 |
|---|---|---|
| 运行时错误 | window.onerror 或 addEventListener('error') | 能拿到 message/source/line/col/error |
| Promise 未捕获 | unhandledrejection | 拿到 reason,建议序列化兜底 |
| 资源加载失败 | addEventListener('error', fn, true) | 资源错误不冒泡,必须捕获阶段 |
| 框架内 | Vue errorHandler、React ErrorBoundary | ErrorBoundary 只捕渲染期,不捕事件与异步 |
| 接口异常 | 请求库拦截器 | 区分业务码与 HTTP 码 |
资源错误必须用捕获阶段:error 事件在资源元素上触发且不冒泡,冒泡阶段监听不到;回调里 event.target 是元素本身,需要判断 tagName 再取 src/href。
跨域脚本报 "Script error." 时,需要 <script crossorigin="anonymous"> 且 CDN 返回 Access-Control-Allow-Origin。
性能上报:PerformanceObserver 订阅 paint、largest-contentful-paint、layout-shift、longtask,导航数据取 performance.getEntriesByType('navigation');上报用 navigator.sendBeacon,避免阻塞卸载。
sourcemap 还原:构建产出 .map,上传到监控平台而不公开发布,平台用 source-map 库按 stack 里的行列号反查原始文件、函数名与源码片段;hidden-source-map 只生成不写 sourceMappingURL。
追问:监控 SDK 自己报错怎么办——加开关与 try/catch 包裹,限制单页上报条数,避免死循环。
一句话:资源用内容 hash 长缓存,入口 HTML 不缓存,回滚就是切回上一份入口。
app.[contenthash].js + Cache-Control: public, max-age=31536000, immutable,内容变则文件名变,天然失效。Cache-Control: no-cache(可缓存但每次校验)或极短 max-age,保证拿到最新的资源引用。追问:为什么不能只刷新 CDN 缓存——用户浏览器里可能还存着旧 HTML,只能靠短缓存加版本对比。
一句话:ESLint 管"对不对",Prettier 管"好不好看",git hooks 把检查卡在提交前。
| 工具 | 职责 | 边界 |
|---|---|---|
| ESLint | 潜在 bug、未使用变量、hooks 依赖、导入顺序 | 不负责换行缩进的美观 |
| Prettier | 统一格式化,不做语义判断 | 不理解业务规则 |
eslint-config-prettier | 关掉与 Prettier 冲突的格式规则 | 必须放在 extends 末尾 |
pre-commit,配合 lint-staged 只处理暂存文件,避免全量检查拖慢提交。commit-msg 用 commitlint 校验 Conventional Commits:type(scope): subject,便于生成 changelog 与语义化版本。--no-verify 绕过本地钩子。.editorconfig 与 .vscode/settings.json,减少无意义 diff。