Skip to content
webpack 概述
模块化与静态资源管理
项目规模扩大后,JavaScript 的模块化问题会直接暴露出来。早期的做法是依靠全局变量和手动排序的 <script> 标签加载脚本。一旦脚本数量增加到几十个,依赖顺序就完全依赖人工维护,极容易出错。后来出现的 CommonJS 与 ES Module 规范在 Node.js 侧可以自然地通过 require 加载模块,但浏览器并不能直接消费这些语法。
除了 JavaScript 自身的依赖关系,现代页面还会引入 CSS、图片、字体等静态资源。这些资源同样需要管理:预处理、压缩、文件名哈希、按需加载。如果只用 npm 脚本拼凑命令行工具,可以搭出一个构建流程,但跨项目复用和团队协作时,配置很快就会膨胀成难以维护的胶水层。
Grunt 和 Gulp 这类任务运行器把胶水层规范化了。它们定义任务(task),通过文件流串联处理步骤:读源文件、插件转换、写目标文件。任务运行器对模块依赖结构几乎没有认知,只按文件路径执行转换。开发者必须手动保证文件顺序,无法自动识别模块间的引用关系。例如,一个 JavaScript 文件里用 import 引入了另一个模块,任务运行器不会自动把它们打包到一起——除非显式配置合并规则。
webpack 是什么
webpack 把自己定位成 静态模块打包器(static module bundler)。它从一个入口模块出发,通过 import / require 语句递归地找出所有直接或间接依赖的模块,构建一个内部依赖图,然后将图中的所有模块打包成一个或多个 bundle 文件。处理过程中,webpack 不只负责 JavaScript。任何文件——CSS、图片、字体、JSON,甚至模板文件——只要配置了对应的 loader,都会被 webpack 视为模块并纳入依赖图。最终输出的 bundle 可以直接作为静态资源部署。
可以这样理解:任务运行器在文件系统上操作文件流,而 webpack 是在构建一个模块关系图谱,再把这个图谱压缩到几个输出文件里。
构建流程概览:从 Entry 到 Bundle
webpack 的工作从 entry 配置开始。entry 指定依赖图的起点,默认是 ./src/index.js。从入口文件出发,webpack 解析其中的 import 和 require,一层层往下找,直到没有新的模块加入为止。这个过程中解析出的所有模块构成了完整的依赖图。
依赖图构建完成后,webpack 把关联的模块合并到 chunk 中。chunk 是构建过程的中间产物,一个 chunk 包含多个模块的代码。在树摇(tree shaking)和代码分割等优化步骤之后,每个 chunk 再经过最终处理生成一个或多个 bundle 文件,这些文件就是实际输出的静态资源。output 配置决定了这些 bundle 的存放位置和命名规则。
一个最小的配置如下:
javascript
// webpack.config.js
const path = require('path');
module.exports = {
entry: './src/app.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist')
}
};执行 npx webpack 后,webpack 读取 ./src/app.js 及其依赖树,生成 dist/bundle.js。如果没有额外的配置,生成的 bundle 会包含一个简单的模块运行时和所有业务模块的代码。
在更复杂的项目中,一个 entry 可能产生多个 chunk。例如使用了动态 import() 或配置代码分割时,webpack 会将公共依赖拆分到独立的 chunk 中,最终输出多个 bundle 文件。从这个角度看,chunk 是模块组的内部表示,bundle 是最终写入磁盘的文件。
Loader:将资源转换为模块
webpack 本身只能处理 JavaScript 和 JSON 模块。对于 CSS、图片等资源,需要靠 loader 做转换。loader 本质上是一个函数:接收源代码(或其他资源内容)作为输入,返回处理后的新内容。webpack 将返回值当作模块处理,从而纳入依赖图。
在 module.rules 中配置 loader 的匹配规则。例如处理 .css 文件:
javascript
module.exports = {
module: {
rules: [
{
test: /\.css$/,
use: ['style-loader', 'css-loader']
}
]
}
};css-loader 负责解析 CSS 中的 @import 和 url(),将它们转换为 CommonJS 模块;style-loader 的作用是将 CSS 模块的内容通过 <style> 标签插入到 DOM 中,使样式生效。use 数组中的 loader 从右向左依次执行:先经过 css-loader,输出再传给 style-loader。这样一个 import './style.css' 语句,最终会在页面运行时向文档里注入样式内容。
其他常见的 loader 还有 babel-loader(将 ES6+ 代码编译为 ES5)、file-loader(处理文件导入并返回最终路径)等。
Plugin:介入构建生命周期
Loader 的能力限于模块内容的转换。要在构建的不同阶段做更复杂的操作——如生成 HTML 文件、注入环境变量、优化输出等——就需要 plugin 介入。
plugin 是一个带有 apply 方法的对象,webpack 在编译过程中通过 Tapable 钩子暴露各个时点的事件。plugin 在这些事件上注册回调,就能访问当前编译状态并对输出进行修改。
一个常用的 plugin 是 HtmlWebpackPlugin:
javascript
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
plugins: [
new HtmlWebpackPlugin({ template: './src/index.html' })
]
};这个插件会在构建完成时自动生成一个 HTML 文件,并使用 <script> 标签将 webpack 输出的 bundle 注入其中。如果不使用该插件,就需手动在 HTML 里维护带哈希的文件名,这在开发阶段很不方便。
从职责上看,loader 负责转换具体资源的内容,plugin 则面向整个构建流程,可以监听生命周期钩子,做编译上下文中任何需要的事情。webpack 内部的许多功能也是以 plugin 形式实现的。
应用场景
webpack 最主要的应用场景是单页应用(SPA)。一个或几个入口脚本,加上各种组件、样式和静态资源,经过 webpack 处理后输出一个或多个 bundle,部署到 CDN 或静态服务器即可。
除了 SPA,webpack 也能处理服务端代码。通过配置 target: 'node',webpack 可以打包 Node.js 应用,并自动忽略内置模块。库作者也可通过 output.libraryTarget 导出 UMD、CommonJS 等格式的包。这种灵活性使得 webpack 成为前端工程化工具链中的核心。
在生态方面,create-react-app 和 Vue CLI 底层都使用 webpack,为其提供开发和构建能力,但开发者往往不需要直接接触 webpack 配置。需要定制时,才转向 webpack.config.js 进行修改。
本篇小结
本篇梳理了 webpack 的定位:它是静态模块打包器,解决前端模块化依赖管理和静态资源处理的问题。核心流程是从 entry 出发构建依赖图,生成 chunk 并输出 bundle,可通过 loader 和 plugin 灵活扩展。
参考链接
- [1] https://webpack.js.org/concepts
- [5] [官方] 概念:Loader 是应用于模块源代码的转换器,写成函数形式,接收源代码作为参数,返回转换后的新代码。来源
- [7] https://webpack.js.org
- [8] https://learn.lianglianglee.com/%E4%B8%93%E6%A0%8F/%E5%89%8D%E7%AB%AF%E5%B7%A5%E7%A8%8B%E5%8C%96%E7%B2%BE%E8%AE%B2-%E5%AE%8C/10%20%20%E6%B5%81%E7%A8%8B%E5%88%86%E8%A7%A3%EF%BC%9AWebpack%20%E7%9A%84%E5%AE%8C%E6%95%B4%E6%9E%84%E5%BB%BA%E6%B5%81%E7%A8%8B.md
- [9] https://webpack.js.org/guides/production
- [10] https://developer.cloud.tencent.com/article/2018884?policyId=1003
- [11] https://developer.aliyun.com/article/1136982
