如何从 0 搭建一个 Webpack 项目

面试题

Q:如果不使用脚手架,你会如何从 0 搭建一个可用于开发和生产的 Webpack 项目?

一、面试时先给结论

我会先明确项目需要处理哪些资源、目标浏览器和部署环境,再按下面的链路搭建:

初始化 npm 项目 → 安装 webpack 与 webpack-cli
→ 配置 entry/output/mode → 用 Loader 处理源码和样式
→ 用 Plugin 生成 HTML → 配置开发服务器与 Source Map
→ 增加生产优化、缓存和构建验证

最小配置只需要入口和出口,但真正可用的工程还要解决 JavaScript 兼容、CSS 和图片处理、HTML 注入、开发调试、生产压缩与长期缓存。搭建的重点不是把配置项堆齐,而是让每一项配置对应一个明确需求。

二、先理解本质

浏览器不能直接承担项目的全部工程化工作:源码可能包含浏览器不支持的新语法,CSS、图片等资源之间存在依赖,生产环境还需要拆包、压缩和缓存。

Webpack 从入口开始,把 import 形成的依赖关系构建为模块图,再将模块组织成 Chunk,最终生成浏览器可以加载的 Asset:

源文件(输入)
  → Resolver 找到模块
  → Loader 转换模块内容
  → Webpack 建立 ModuleGraph / ChunkGraph
  → Plugin 介入构建生命周期
  → JS、CSS、图片、HTML(输出)

这里要区分两个边界:Loader 主要负责“一个模块内容如何转换”,Plugin 负责“整个构建流程如何扩展”。

三、最小可运行项目

以下示例基于 Webpack 5,目标是支持 JavaScript、CSS、图片、自动生成 HTML 和本地开发服务器。

1. 初始化并安装依赖

mkdir webpack-demo
cd webpack-demo
npm init -y
npm install -D webpack webpack-cli webpack-dev-server \
  html-webpack-plugin style-loader css-loader \
  babel-loader @babel/core @babel/preset-env

各依赖的职责:

依赖职责
webpack构建核心
webpack-cli从命令行读取配置并启动构建
webpack-dev-server开发服务器、自动刷新和 HMR 基础能力
html-webpack-plugin生成 HTML 并注入产物
babel-loader + Babel把 JS 交给 Babel 转换
css-loader解析 CSS 中的 @importurl()
style-loader开发时把 CSS 注入页面

2. 创建目录

webpack-demo/
├── public/
│   └── index.html
├── src/
│   ├── index.js
│   └── style.css
├── package.json
└── webpack.config.js

public/index.html

<!doctype html>
<html lang="zh-CN">
  <head><meta charset="UTF-8"><title>Webpack Demo</title></head>
  <body><div id="app"></div></body>
</html>

src/style.css

body { font-family: sans-serif; }

src/index.js

import './style.css';

document.querySelector('#app').textContent = 'Hello Webpack';

import './style.css' 是触发 CSS Loader 链的关键;DOM 代码只是用来验证构建结果。

3. 编写 Webpack 配置

webpack.config.js

const path = require('node:path');
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = (_, argv) => {
  const isProduction = argv.mode === 'production';

  return {
    mode: isProduction ? 'production' : 'development',
    entry: './src/index.js',
    output: {
      path: path.resolve(__dirname, 'dist'),
      filename: isProduction ? 'js/[name].[contenthash:8].js' : 'js/[name].js',
      assetModuleFilename: 'assets/[name].[contenthash:8][ext]',
      clean: true,
    },
    module: {
      rules: [
        {
          test: /\.js$/,
          exclude: /node_modules/,
          use: {
            loader: 'babel-loader',
            options: { presets: ['@babel/preset-env'] },
          },
        },
        {
          test: /\.css$/i,
          use: ['style-loader', 'css-loader'],
        },
        {
          test: /\.(png|jpe?g|gif|svg)$/i,
          type: 'asset',
        },
      ],
    },
    plugins: [
      new HtmlWebpackPlugin({ template: './public/index.html' }),
    ],
    devtool: isProduction ? 'source-map' : 'eval-cheap-module-source-map',
    devServer: {
      static: path.resolve(__dirname, 'dist'),
      hot: true,
      port: 3000,
      open: true,
    },
    optimization: {
      splitChunks: { chunks: 'all' },
      runtimeChunk: 'single',
    },
    cache: { type: 'filesystem' },
  };
};

这份配置中的因果关系是:

  1. entry 告诉 Webpack 从哪里建立依赖图;
  2. module.rules 按资源类型选择转换方式,Loader 从右向左执行;
  3. HtmlWebpackPlugin 在构建阶段生成 HTML,并自动注入带哈希的脚本;
  4. devServer 服务于开发反馈,不会被打进生产包;
  5. contenthashsplitChunks 和独立 runtime 改善生产环境的长期缓存;
  6. 文件系统缓存减少后续构建中的重复工作。

4. 增加命令

package.json 中加入:

{
  "scripts": {
    "dev": "webpack serve --mode development",
    "build": "webpack --mode production"
  }
}

然后执行:

npm run dev
npm run build

前者启动开发服务器,后者生成 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 抽离、部署缓存策略和业务级拆包。

五、常见边界与陷阱

问题原因与处理方式
Loader 顺序写反use 默认从右向左执行,CSS 应先经 css-loader,再经 style-loader
Babel 配了仍不兼容Babel 的语法转换和 API polyfill 是两件事,还要明确目标浏览器与 polyfill 策略
所有文件都交给 Babel会扩大构建范围,应排除 node_modules 或使用 include 精确匹配
生产环境仍只用 style-loaderCSS 进入 JS,可能影响缓存和首屏;生产中通常提取独立 CSS
只使用 [hash]构建中无关模块也可能更换文件名;长期缓存优先按产物内容使用 [contenthash]
拆包越细越好过多请求和运行时代价可能抵消收益,应结合 HTTP、缓存命中和真实指标权衡
devServer 当生产服务器它面向开发体验;生产产物应由 CDN、对象存储或正式 Web 服务器提供
Source Map 直接公开可能暴露源码;可上传到错误监控平台并限制线上访问
环境变量中放密钥前端构建后的值可被用户读取,真正的秘密不能进入浏览器包

六、源码视角下的关键对象

不同 Webpack 5 小版本的内部细节可能变化,但稳定的理解模型是:

  • Compiler:一次 Webpack 运行的总调度器,持有配置并暴露生命周期钩子;
  • Compilation:某一次具体构建,包含模块、Chunk、Asset、错误和警告;
  • Resolver:把请求转换为实际模块路径;
  • ModuleGraph:记录模块之间的依赖关系;
  • ChunkGraph:记录模块如何组织进 Chunk;
  • Tapable hooks:Plugin 通过 apply(compiler) 注册钩子,参与构建阶段。

普通构建通常创建一个 Compilation;Watch 模式会复用 Compiler,在文件变化后产生新的 Compilation,并尽可能复用缓存。

七、面试官递进追问

1. Webpack 最少需要配置什么?

Webpack 有默认入口和出口,因此甚至可以零配置运行;显式工程中通常至少声明 modeentryoutput。面试官想确认你是否理解“最小配置”和“生产可用”不是一回事。

2. 为什么 CSS 需要两个 Loader?顺序为什么不能反?

css-loader 先把 CSS 及其依赖转换成模块,style-loader 再把结果注入 DOM。Loader 从右向左执行。重点是说出每一步输入和输出,而不只是背顺序。

3. Loader 和 Plugin 的边界是什么?

Loader 针对模块内容做转换,Plugin 通过生命周期钩子影响更广的构建过程。面试官在判断你能否为扩展需求选择正确机制。

4. 为什么开发和生产配置不能完全一样?

开发环境优先启动速度、增量构建和调试体验;生产环境优先体积、加载性能和长期缓存。重点是讲清目标不同导致的配置取舍。

5. [contenthash] 为什么有利于缓存?

文件内容不变时哈希通常不变,浏览器可继续复用缓存;配合独立 runtime 可以减少业务变更对 vendor 文件名的影响。面试官会进一步判断你是否理解构建配置必须与 HTTP 缓存策略配合。

6. Babel 为什么不能自动解决所有兼容问题?

Babel 主要转换语法,PromiseArray.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-loadercss-loader 的配置顺序是 ['style-loader', 'css-loader']?请按“每一步的输入和输出”来回答。