skip to content
蕉太狼的博客
目录

Webpack 核心原理与实践

Webpack 是目前生态最完整的前端打包工具,核心工作是以入口文件为起点,递归分析所有依赖,将各种资源(JS、CSS、图片、字体等)打包成浏览器可运行的静态文件。

核心概念

Entry — 入口

Webpack 从 entry 出发,递归解析所有依赖,构建依赖图(Dependency Graph)

// 单入口
module.exports = {
entry: './src/index.js',
}
// 多入口(多页应用)
module.exports = {
entry: {
app: './src/app.js',
admin: './src/admin.js',
},
}

Output — 输出

webpack.config.js
const path = require('path')
module.exports = {
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].[contenthash:8].js',
clean: true,
},
}

Mode — 模式

module.exports = {
mode: 'production', // development | production | none
}
mode效果
development开启 source map、HMR,不压缩代码,构建速度快
production自动开启 tree shaking、代码压缩、Scope Hoisting
none不做任何默认优化

Loader — 文件转换

Webpack 原生只能处理 JS 和 JSON,其他类型文件需要通过 loader 转换。

webpack.config.js
module.exports = {
module: {
rules: [
{
test: /\.tsx?$/,
use: 'ts-loader',
exclude: /node_modules/,
},
{
test: /\.css$/,
use: ['style-loader', 'css-loader'], // 从右到左执行
},
{
test: /\.(png|svg|jpg|gif)$/,
type: 'asset/resource', // webpack 5 内置,替代 file-loader
},
],
},
}

Plugin — 扩展构建流程

Plugin 作用于整个构建生命周期,可以做 loader 做不到的事:生成 HTML、提取 CSS 文件、注入环境变量等。

webpack.config.js
const HtmlWebpackPlugin = require('html-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const { DefinePlugin } = require('webpack')
module.exports = {
plugins: [
new HtmlWebpackPlugin({ template: './public/index.html' }),
new MiniCssExtractPlugin({ filename: '[name].[contenthash:8].css' }),
new DefinePlugin({
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV),
}),
],
}

完整配置示例

webpack.config.js
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const isProd = process.env.NODE_ENV === 'production'
module.exports = {
mode: isProd ? 'production' : 'development',
entry: './src/index.ts',
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].[contenthash:8].js',
clean: true,
},
resolve: {
extensions: ['.ts', '.tsx', '.js'],
alias: { '@': path.resolve(__dirname, 'src') },
},
module: {
rules: [
{ test: /\.tsx?$/, use: 'ts-loader', exclude: /node_modules/ },
{
test: /\.css$/,
use: [
isProd ? MiniCssExtractPlugin.loader : 'style-loader',
'css-loader',
'postcss-loader',
],
},
{ test: /\.(png|svg|jpg|gif|woff2?)$/, type: 'asset/resource' },
],
},
plugins: [
new HtmlWebpackPlugin({ template: './public/index.html' }),
...(isProd ? [new MiniCssExtractPlugin({ filename: '[name].[contenthash:8].css' })] : []),
],
devServer: {
port: 3000,
hot: true,
historyApiFallback: true,
proxy: { '/api': { target: 'http://localhost:4000', changeOrigin: true } },
},
devtool: isProd ? false : 'eval-cheap-module-source-map',
}

构建优化

缓存

webpack.config.js
module.exports = {
cache: {
type: 'filesystem', // 构建缓存写入磁盘,二次构建速度大幅提升
},
module: {
rules: [
{
test: /\.js$/,
use: {
loader: 'babel-loader',
options: { cacheDirectory: true },
},
},
],
},
}

缩小文件搜索范围

module.exports = {
resolve: {
modules: [path.resolve(__dirname, 'src'), 'node_modules'],
extensions: ['.ts', '.js'], // 后缀越少查找越快
},
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/, // 不转译 node_modules,速度提升显著
use: 'babel-loader',
},
],
},
}

多线程并行

const TerserPlugin = require('terser-webpack-plugin')
module.exports = {
module: {
rules: [
{
test: /\.js$/,
use: ['thread-loader', 'babel-loader'], // thread-loader 放最前,后续 loader 在 worker 中运行
},
],
},
optimization: {
minimizer: [new TerserPlugin({ parallel: true })],
},
}

代码分割(Code Splitting)

webpack.config.js
module.exports = {
optimization: {
splitChunks: {
chunks: 'all',
cacheGroups: {
vendors: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
priority: 10,
},
common: {
minChunks: 2, // 被至少 2 个 chunk 引用才提取
name: 'common',
priority: 5,
},
},
},
runtimeChunk: 'single', // 将 hash 映射关系单独打包,防止业务变化污染 vendors hash
},
}

Tree Shaking

Tree Shaking 依赖 ES Module 的静态结构,生产模式下自动开启。

分析构建产物

Terminal window
npm install --save-dev webpack-bundle-analyzer
webpack.config.js
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer')
module.exports = {
plugins: [
new BundleAnalyzerPlugin(), // 启动本地服务,可视化分析各模块体积
],
}

Loader 原理与自定义

Loader 本质是一个函数,接收源代码字符串,返回转换后的代码。

loaders/remove-log.js
module.exports = function (source) {
const options = this.getOptions() // 获取用户传入的 options
return source.replace(/console\.log\(.*?\);?/g, '')
}

异步 loader:

loaders/async-loader.js
module.exports = function (source) {
const callback = this.async() // 声明为异步,返回 callback
someAsyncOperation(source).then(result => {
callback(null, result) // 第一个参数是 error,第二个是转换结果
})
}

注册使用:

webpack.config.js
module.exports = {
resolveLoader: {
modules: ['node_modules', path.resolve(__dirname, 'loaders')],
},
module: {
rules: [
{ test: /\.js$/, use: 'remove-log' },
],
},
}

Plugin 原理与自定义

Tapable 钩子系统

Webpack 的插件机制基于 Tapable,Compiler 和 Compilation 对象上挂载了大量生命周期钩子。

钩子触发时机
compiler.hooks.compile开始编译
compiler.hooks.thisCompilation创建 compilation 对象
compiler.hooks.emit输出文件前,可修改输出内容
compiler.hooks.done构建完成
compilation.hooks.buildModule模块开始构建
compilation.hooks.optimizeChunks优化 chunk

实现一个自定义 Plugin

Plugin 是一个类,必须实现 apply(compiler) 方法。

示例:构建完成后输出文件清单

plugins/FileListPlugin.js
class FileListPlugin {
constructor(options = {}) {
this.filename = options.filename || 'file-list.txt'
}
apply(compiler) {
compiler.hooks.emit.tapAsync('FileListPlugin', (compilation, callback) => {
const fileList = Object.keys(compilation.assets)
.map(name => `- ${name} (${compilation.assets[name].size()} bytes)`)
.join('\n')
const content = `构建产物清单\n${'='.repeat(20)}\n${fileList}`
// 向输出目录注入一个新文件
compilation.assets[this.filename] = {
source: () => content,
size: () => content.length,
}
callback()
})
}
}
module.exports = FileListPlugin

示例:自动注入版本号

plugins/InjectVersionPlugin.js
const { DefinePlugin } = require('webpack')
class InjectVersionPlugin {
apply(compiler) {
const { version } = require('./package.json')
new DefinePlugin({ '__APP_VERSION__': JSON.stringify(version) }).apply(compiler)
compiler.hooks.done.tap('InjectVersionPlugin', () => {
console.log(`✅ 构建完成,版本号:${version}`)
})
}
}
module.exports = InjectVersionPlugin

使用:

webpack.config.js
const FileListPlugin = require('./plugins/FileListPlugin')
module.exports = {
plugins: [new FileListPlugin({ filename: 'assets.txt' })],
}

Webpack 构建流程概览

初始化参数
创建 Compiler,注册所有插件(plugin.apply(compiler))
compiler.run() 开始编译
从 entry 出发,调用对应 loader 转换模块
递归解析依赖,构建依赖图
根据依赖图将模块分配到 chunk
对 chunk 进行优化(tree shaking、splitChunks 等)
将 chunk 转换为最终输出文件(emit)
写入磁盘