工程化面试考察的是你能不能把项目从“能跑”带到“可维护、可构建、可发布、可协作”。

构建工具

Vite

知识点讲解

Vite 开发环境基于原生 ESM,按需转换模块,所以冷启动快。生产构建默认使用 Rollup,负责打包、代码分割和资源优化。

面试常问

问:Vite 为什么比 Webpack 开发启动快?

答:Webpack 开发时通常要先构建依赖图并打包;Vite 利用浏览器原生 ESM,源码按需编译,依赖预构建后缓存,启动成本更低。

Webpack、Rollup 与 Rspack

知识点讲解

Webpack 生态成熟,适合复杂业务和历史项目。Rollup 更擅长库打包,输出更干净。Rspack 用 Rust 实现,目标是兼容 Webpack 生态并提升性能。

面试常问

问:Loader 和 Plugin 区别?

答:Loader 处理单个模块的转换,比如 TS、CSS、图片;Plugin 参与构建生命周期,可以做资源优化、HTML 注入、环境变量、分析报告等更广的事情。

编译与包管理

Babel、SWC 与 ESBuild

知识点讲解

Babel 生态成熟、插件丰富,适合语法转换和定制。SWC 和 ESBuild 更快,常用于转译和压缩,但某些 Babel 插件能力不完全等价。

面试常问

问:Babel 做 polyfill 吗?

答:Babel 本身主要做语法转换。新的 API 需要 polyfill,比如 Promise、Array.from。通常配合 core-js@babel/preset-env 的 useBuiltIns。

npm、pnpm 与 yarn

知识点讲解

包管理器负责依赖安装、锁定版本、脚本执行。pnpm 通过内容寻址存储和硬链接节省磁盘,并用严格 node_modules 结构减少幽灵依赖。

面试常问

问:什么是幽灵依赖?

答:项目代码使用了没有直接声明的依赖,只是因为它被其他依赖提升到了 node_modules 根目录。换环境或升级依赖后可能突然失效。

质量与发布

ESLint、Prettier 与 Husky

知识点讲解

ESLint 关注代码质量和潜在错误,Prettier 关注格式统一。Husky 常配合 lint-staged 在提交前检查变更文件,防止低级问题进入仓库。

面试常问

问:ESLint 和 Prettier 冲突怎么办?

答:职责分离。用 Prettier 处理格式,用 ESLint 处理代码规则,通过相关配置关闭 ESLint 中与格式冲突的规则。

CI/CD

知识点讲解

CI 负责自动检查、测试、构建;CD 负责自动部署。常见流程包括安装依赖、缓存、lint、test、build、产物上传、部署、回滚。

面试常问

问:前端上线流程怎么设计?

答:提交代码后触发 CI,执行 lint/test/build,产物上传到对象存储或服务器,CDN 刷新缓存,灰度发布,监控错误和性能指标,异常时回滚。

Monorepo

基本概念

知识点讲解

Monorepo 是把多个项目或包放在同一个仓库里统一管理。优点是依赖统一、代码复用方便、跨包修改原子提交;挑战是构建缓存、权限、版本发布和仓库规模。

面试常问

问:Monorepo 适合什么场景?

答:多个包之间频繁协作、组件库和业务项目共存、多应用共享工具链时适合。小项目或团队协作边界很清晰时不一定需要。

TurboRepo 与 Nx

知识点讲解

TurboRepo 强调任务编排和缓存,配置较轻。Nx 能力更完整,包含项目图、生成器、插件和更强约束,适合大型组织。

面试常问

问:构建缓存有什么价值?

答:如果输入文件、依赖和命令没变,就可以复用上次构建结果,减少 CI 时间和本地等待时间。

底层追问与代码示例

Tree Shaking 为什么依赖 ESM

知识点讲解

Tree Shaking 的基础是静态分析。ESM 的 import/export 是静态结构,构建工具可以在编译阶段知道哪些导出被使用。CommonJS 的 require 可以出现在条件语句和动态路径里,静态分析更困难。

1
2
3
4
// ESM,容易静态分析
import { add } from './math.js'

console.log(add(1, 2))
1
2
3
// CommonJS,路径和导出都可能动态变化
const name = process.env.MODULE_NAME
const mod = require(`./${name}`)

面试常问

问:为什么有些代码明明没用却没被摇掉?

答:可能是模块有副作用、包没有正确声明 sideEffects、使用了动态导入方式、代码被 Babel 转成 CommonJS,或者导出对象被整体引用。

1
2
3
4
5
6
{
"sideEffects": [
"*.css",
"src/polyfill.ts"
]
}

手写一个简化版 Loader

知识点讲解

Webpack Loader 本质是把一种资源转换成 JS 模块或另一种资源。它接收源码,返回转换后的源码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// markdown-loader.js
module.exports = function markdownLoader(source) {
const escaped = source
.replace(/`/g, '\\`')
.replace(/\$\{/g, '\\${')

return `
const html = \`${escaped}\`
.replace(/^# (.*)$/gm, '<h1>$1</h1>')
.replace(/^## (.*)$/gm, '<h2>$1</h2>')

export default html
`
}

真实 loader 要考虑 source map、异步处理、缓存、错误上报和 loader 顺序。多个 loader 执行顺序通常是从右到左、从下到上。

Plugin 的生命周期

知识点讲解

Plugin 通过 Tapable 订阅编译生命周期。它不只处理单个文件,而是能介入整个构建过程。

1
2
3
4
5
6
7
8
9
10
class BuildTimePlugin {
apply(compiler) {
compiler.hooks.done.tap('BuildTimePlugin', stats => {
const time = stats.endTime - stats.startTime
console.log(`build finished in ${time}ms`)
})
}
}

module.exports = BuildTimePlugin

面试常问

问:Loader 和 Plugin 怎么选?

答:处理某类模块内容用 Loader;需要影响构建流程、生成额外资源、分析依赖图、修改输出产物,用 Plugin。

Vite 依赖预构建

知识点讲解

Vite 会用 esbuild 对依赖做预构建,主要解决两个问题:把 CommonJS/UMD 转成 ESM;把依赖内部大量小模块合并,减少浏览器请求数量。

1
2
3
4
5
6
7
// vite.config.js
export default {
optimizeDeps: {
include: ['lodash-es'],
exclude: ['my-local-linked-package']
}
}

专业回答要补充:源码通常按需转换,依赖通常预构建并缓存,生产构建仍然交给 Rollup。