如何从 0 搭建一个 Webpack 项目
面试题
Q:如果不使用脚手架,你会如何从 0 搭建一个可用于开发和生产的 Webpack 项目?
一、面试时先给结论
我会先明确项目需要处理哪些资源、目标浏览器和部署环境,再按下面的链路搭建:
最小配置只需要入口和出口,但真正可用的工程还要解决 JavaScript 兼容、CSS 和图片处理、HTML 注入、开发调试、生产压缩与长期缓存。搭建的重点不是把配置项堆齐,而是让每一项配置对应一个明确需求。
二、先理解本质
浏览器不能直接承担项目的全部工程化工作:源码可能包含浏览器不支持的新语法,CSS、图片等资源之间存在依赖,生产环境还需要拆包、压缩和缓存。
Webpack 从入口开始,把 import 形成的依赖关系构建为模块图,再将模块组织成 Chunk,最终生成浏览器可以加载的 Asset:
这里要区分两个边界:Loader 主要负责“一个模块内容如何转换”,Plugin 负责“整个构建流程如何扩展”。
三、最小可运行项目
以下示例基于 Webpack 5,目标是支持 JavaScript、CSS、图片、自动生成 HTML 和本地开发服务器。
1. 初始化并安装依赖
各依赖的职责:
2. 创建目录
public/index.html:
src/style.css:
src/index.js:
import './style.css' 是触发 CSS Loader 链的关键;DOM 代码只是用来验证构建结果。
3. 编写 Webpack 配置
webpack.config.js:
这份配置中的因果关系是:
entry告诉 Webpack 从哪里建立依赖图;module.rules按资源类型选择转换方式,Loader 从右向左执行;HtmlWebpackPlugin在构建阶段生成 HTML,并自动注入带哈希的脚本;devServer服务于开发反馈,不会被打进生产包;contenthash、splitChunks和独立 runtime 改善生产环境的长期缓存;- 文件系统缓存减少后续构建中的重复工作。
4. 增加命令
在 package.json 中加入:
然后执行:
前者启动开发服务器,后者生成 dist 生产产物。验收时除了页面能打开,还应检查构建错误、产物大小、浏览器兼容目标、Source Map 和缓存头是否符合部署要求。
四、从“能运行”走向“可上线”
上面的 CSS 通过 style-loader 写入 <style>,适合最小开发示例。生产项目通常还会继续做这些工作:
- 用
mini-css-extract-plugin抽出 CSS,并配合 CSS 压缩; - 通过 Browserslist 和 Babel 明确兼容范围;需要兼容缺失 API 时再按策略引入 polyfill;
- 按路由使用动态
import(),并结合splitChunks控制公共依赖; - 为环境变量、代理、接口地址建立明确的开发与生产配置;
- 用 Bundle Analyzer 和性能预算观察体积,不凭感觉拆包;
- 在 CI 中执行 lint、测试和生产构建,再部署
dist; - 服务器为带内容哈希的静态资源设置长期缓存,为 HTML 设置协商缓存或较短缓存。
注意:Webpack 的 mode: 'production' 会启用一组生产默认优化,但不会替你完成 CSS 抽离、部署缓存策略和业务级拆包。
五、常见边界与陷阱
六、源码视角下的关键对象
不同 Webpack 5 小版本的内部细节可能变化,但稳定的理解模型是:
Compiler:一次 Webpack 运行的总调度器,持有配置并暴露生命周期钩子;Compilation:某一次具体构建,包含模块、Chunk、Asset、错误和警告;- Resolver:把请求转换为实际模块路径;
ModuleGraph:记录模块之间的依赖关系;ChunkGraph:记录模块如何组织进 Chunk;- Tapable hooks:Plugin 通过
apply(compiler)注册钩子,参与构建阶段。
普通构建通常创建一个 Compilation;Watch 模式会复用 Compiler,在文件变化后产生新的 Compilation,并尽可能复用缓存。
七、面试官递进追问
1. Webpack 最少需要配置什么?
Webpack 有默认入口和出口,因此甚至可以零配置运行;显式工程中通常至少声明 mode、entry 和 output。面试官想确认你是否理解“最小配置”和“生产可用”不是一回事。
2. 为什么 CSS 需要两个 Loader?顺序为什么不能反?
css-loader 先把 CSS 及其依赖转换成模块,style-loader 再把结果注入 DOM。Loader 从右向左执行。重点是说出每一步输入和输出,而不只是背顺序。
3. Loader 和 Plugin 的边界是什么?
Loader 针对模块内容做转换,Plugin 通过生命周期钩子影响更广的构建过程。面试官在判断你能否为扩展需求选择正确机制。
4. 为什么开发和生产配置不能完全一样?
开发环境优先启动速度、增量构建和调试体验;生产环境优先体积、加载性能和长期缓存。重点是讲清目标不同导致的配置取舍。
5. [contenthash] 为什么有利于缓存?
文件内容不变时哈希通常不变,浏览器可继续复用缓存;配合独立 runtime 可以减少业务变更对 vendor 文件名的影响。面试官会进一步判断你是否理解构建配置必须与 HTTP 缓存策略配合。
6. Babel 为什么不能自动解决所有兼容问题?
Babel 主要转换语法,Promise、Array.prototype 新方法等运行时 API 还需要按目标环境选择 polyfill。面试官想识别“能配 Loader”与“理解兼容体系”的区别。
7. 动态 import() 和 splitChunks 有什么区别?
动态 import() 在源码中创建异步加载边界;splitChunks 根据策略重新组织和提取可共享模块。前者表达业务加载时机,后者偏向产物组织优化。
8. HMR 更新时为什么不必整页刷新?
开发服务器通知客户端有新编译结果,Webpack runtime 拉取更新清单和模块代码,再沿模块关系寻找可接受更新的边界;没有可接受边界时才可能回退到刷新。重点是不要把 HMR 简化成 WebSocket 自动刷新。
9. 项目构建突然变慢,你会怎么排查?
先用构建统计和分析工具定位耗时阶段,再检查 Loader 匹配范围、插件、Source Map、缓存命中和依赖变化,最后针对瓶颈优化。面试官在观察你是否基于数据排查,而不是机械加入并行插件。
10. 你会如何把这个最小项目演进成团队工程?
增加环境分层、代码规范、类型检查、测试、CI、体积预算、监控上传、部署和缓存策略,并把公共配置封装但保留可调试性。面试官希望听到构建工具与完整研发链路的连接。
八、常见错误回答
- 只背五个概念:Entry、Output、Loader、Plugin、Mode 都说到了,却没有说明依赖图如何变成产物。应按输入、转换、组织、优化、输出串起因果链。
- 把“能打包”当成“搭建完成”:没有开发调试、兼容目标、生产缓存和部署验证。应先给最小闭环,再主动说明生产化补充。
- 认为 Babel 等于兼容所有浏览器:混淆语法降级与 API polyfill。应明确 Browserslist、preset 和 polyfill 的分工。
- 罗列大量插件却说不出需求:这通常像背配置。回答每个配置时都应说明它解决的具体问题和代价。
- 声称 Webpack 一定把一个入口输出成一个文件:动态导入、运行时代码和拆包策略都可能产生多个 Chunk 与 Asset。
九、可迁移总结
核心关键词:依赖图、Loader、Plugin、Chunk、Asset、环境差异。
一句话本质:Webpack 从入口建立依赖图,通过转换和生命周期扩展,把开发源码组织为适合浏览器和部署环境的产物。
这套思路也可以迁移到 Vite、Rollup、Rspack:先问输入是什么、模块如何解析和转换、产物如何组织、开发与生产目标有何不同,再比较工具的实现与性能取舍。
不同时长的回答策略
- 1 分钟:说清初始化、入口出口、Loader、Plugin、开发服务器和生产优化这条主链路。
- 3 分钟:补充最小目录与配置,解释 CSS Loader 链、HTML 注入、Source Map 和内容哈希。
- 10 分钟:继续展开 Compiler/Compilation、模块图到 Chunk、HMR、拆包、缓存、兼容策略和 CI 部署。
十、自测追问
第一题:为什么 style-loader 和 css-loader 的配置顺序是 ['style-loader', 'css-loader']?请按“每一步的输入和输出”来回答。

