Webpack 基础
大约 9 分钟约 2841 字
Webpack 基础
简介
Webpack 是经典的前端模块打包工具,它的核心能力是:把 JavaScript、TypeScript、CSS、图片、字体等资源统一纳入模块依赖图,再根据配置输出浏览器可用的静态产物。虽然 Vite 已经成为很多新项目的默认选择,但 Webpack 仍然在大量企业项目、老项目迁移、复杂构建链路和微前端场景中非常常见。
Webpack 的工作流程可以概括为四个阶段:
- 初始化:读取配置文件,合并默认参数,创建 Compiler 对象
- 编译:从 entry 出发,递归解析所有依赖,构建模块依赖图
- 输出:根据配置的 loader 转换每个模块,根据 plugin 扩展构建流程,最终生成输出文件
- 完成:写入文件系统,触发钩子通知
特点
实现
最小配置:入口、输出、Loader、Plugin
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
module.exports = (env, argv) => {
const isDev = argv.mode === 'development'
return {
entry: './src/main.ts',
output: {
path: path.resolve(__dirname, 'dist'),
filename: isDev ? '[name].js' : 'js/[name].[contenthash:8].js',
clean: true,
publicPath: '/'
},
devtool: isDev ? 'eval-cheap-module-source-map' : 'source-map',
module: {
rules: [
{
test: /\.ts$/,
use: 'ts-loader',
exclude: /node_modules/
},
{
test: /\.css$/,
use: [isDev ? 'style-loader' : MiniCssExtractPlugin.loader, 'css-loader', 'postcss-loader']
},
{
test: /\.(png|jpe?g|gif|svg)$/,
type: 'asset',
parser: {
dataUrlCondition: { maxSize: 8 * 1024 }
},
generator: {
filename: 'images/[name].[hash:8][ext]'
}
}
]
},
resolve: {
extensions: ['.ts', '.js', '.json'],
alias: {
'@': path.resolve(__dirname, 'src')
}
},
plugins: [
new HtmlWebpackPlugin({
template: './public/index.html'
}),
...(!isDev ? [new MiniCssExtractPlugin({ filename: 'css/[name].[contenthash:8].css' })] : [])
]
}
}{
"scripts": {
"dev": "webpack serve --mode development",
"build": "webpack --mode production"
}
}Webpack 的五个最核心概念:
- entry:从哪里开始构建依赖图
- output:最终产物输出到哪里
- loader:如何处理非 JS 资源
- plugin:如何扩展构建流程
- mode:开发 / 生产模式开关Webpack 构建流程深入
// Webpack 构建过程中的核心钩子
// Compiler(全局)和 Compilation(单次编译)两个对象上的钩子
module.exports = {
plugins: [
{
apply(compiler) {
// 初始化阶段
compiler.hooks.initialize.tap('MyPlugin', () => {
console.log('Webpack 初始化')
})
// 编译开始前
compiler.hooks.beforeCompile.tapAsync('MyPlugin', (params, callback) => {
console.log('即将开始编译')
callback()
})
// 编译完成
compiler.hooks.done.tap('MyPlugin', (stats) => {
console.log('编译完成')
console.log('耗时:', stats.endTime - stats.startTime, 'ms')
})
}
}
]
}
// Compilation 钩子(在每次编译中触发)
compiler.hooks.compilation.tap('MyPlugin', (compilation) => {
// 优化树阶段
compilation.hooks.optimizeTree.tapAsync('MyPlugin', (chunks, modules, callback) => {
// 可以在这里修改 chunk 结构
callback()
})
// 生成资源阶段
compilation.hooks.assetEmitted.tap('MyPlugin', (filename, info) => {
console.log('生成文件:', filename, '大小:', info.content.length, 'bytes')
})
})Loader 详解
// Loader 是一个函数,接收源码,返回转换后的代码
// Loader 的执行顺序:从右到左,从下到上
// 手写一个简单的 loader
// my-loader.js
module.exports = function(source) {
// source 是文件的原始内容
// this 是 loaderContext,包含文件路径、options 等
// 同步返回转换结果
return source.replace(/console\.log\(.*?\);?/g, '')
}
// 异步 loader
module.exports = function(source) {
const callback = this.async()
someAsyncProcess(source, (result) => {
callback(null, result)
})
}
// 在 webpack 配置中使用
module: {
rules: [
{
test: /\.js$/,
use: [
'babel-loader', // 3. 最后执行(转换为 ES5)
'eslint-loader', // 2. 代码检查
'./my-loader.js', // 1. 最先执行
]
}
]
}
// 常用 Loader 详解
// babel-loader:转换 JavaScript/TypeScript
{
test: /\.(js|ts|jsx|tsx)$/,
use: {
loader: 'babel-loader',
options: {
cacheDirectory: true, // 开启缓存加速
presets: [
['@babel/preset-env', { targets: '> 1%, not dead' }],
'@babel/preset-typescript',
],
plugins: [
'@babel/plugin-proposal-optional-chaining',
],
},
},
},
// css-loader + style-loader + postcss-loader
{
test: /\.css$/,
use: [
isDev ? 'style-loader' : MiniCssExtractPlugin.loader, // 注入 <style> 或提取文件
{
loader: 'css-loader',
options: {
modules: { auto: true }, // CSS Modules
importLoaders: 1, // 前面有 1 个 loader(postcss-loader)
},
},
'postcss-loader', // 自动添加 vendor prefix
],
},
// sass-loader:处理 SCSS/Sass
{
test: /\.scss$/,
use: [
isDev ? 'style-loader' : MiniCssExtractPlugin.loader,
'css-loader',
'postcss-loader',
'sass-loader', // 编译 SCSS -> CSS
],
},
// vue-loader:处理 .vue 单文件组件
{
test: /\.vue$/,
loader: 'vue-loader',
},
// asset/resource:处理图片和字体
{
test: /\.(png|jpe?g|gif|webp|svg)$/,
type: 'asset/resource', // 输出文件
generator: {
filename: 'images/[name].[hash:8][ext]',
},
},
{
test: /\.(woff2?|eot|ttf|otf)$/,
type: 'asset/resource',
generator: {
filename: 'fonts/[name].[hash:8][ext]',
},
},
// asset/inline:内联为 base64
{
test: /\.svg$/,
type: 'asset/inline',
},开发服务器与代理
devServer: {
port: 3000,
hot: true,
historyApiFallback: true,
proxy: {
'/api': {
target: 'http://localhost:5000',
changeOrigin: true
}
}
}开发代理常见作用:
- 解决本地联调跨域问题
- 模拟前后端同源访问
- 避免在前端代码里写死完整后端地址代码分割与动态导入
// 路由级懒加载
const routes = [
{
path: '/users',
component: () => import(/* webpackChunkName: "users" */ './pages/Users.vue')
},
{
path: '/orders',
component: () => import(/* webpackChunkName: "orders" */ './pages/Orders.vue')
}
]// 条件加载大型库
async function loadChart() {
if (!window.__chartLoaded) {
const echarts = await import(/* webpackChunkName: "echarts" */ 'echarts')
window.__chartLoaded = echarts
}
return window.__chartLoaded
}optimization: {
splitChunks: {
chunks: 'all',
cacheGroups: {
vendors: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
priority: -10
},
common: {
minChunks: 2,
name: 'common',
priority: -20,
reuseExistingChunk: true
}
}
}
}高级分包策略
optimization: {
splitChunks: {
// 自动分割的阈值
chunks: 'all',
minSize: 20000, // 最小 20KB 才分割
maxSize: 244000, // 最大 244KB,超出会二次分割
minChunks: 1, // 最少被引用 1 次
maxAsyncRequests: 30, // 最大并行请求数
maxInitialRequests: 30, // 入口最大并行请求数
automaticNameDelimiter: '~',
cacheGroups: {
// Vue 全家桶
vue: {
test: /[\\/]node_modules[\\/](vue|vue-router|pinia)[\\/]/,
name: 'vue-vendor',
chunks: 'all',
priority: 20,
},
// UI 组件库
ui: {
test: /[\\/]node_modules[\\/](element-plus|ant-design-vue)[\\/]/,
name: 'ui-vendor',
chunks: 'all',
priority: 15,
},
// 工具库
utils: {
test: /[\\/]node_modules[\\/](lodash|axios|dayjs)[\\/]/,
name: 'utils-vendor',
chunks: 'all',
priority: 10,
},
// 其他第三方库
vendors: {
test: /[\\/]node_modules[\\/]/,
name: 'vendor',
chunks: 'all',
priority: 0,
reuseExistingChunk: true,
},
// 公共代码
common: {
minChunks: 2,
name: 'common',
chunks: 'all',
priority: -10,
reuseExistingChunk: true,
},
},
},
}常用 Plugin 与产物优化
const CompressionPlugin = require('compression-webpack-plugin')
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer')
plugins: [
new HtmlWebpackPlugin({ template: './public/index.html' }),
new MiniCssExtractPlugin({ filename: 'css/[name].[contenthash:8].css' }),
new CompressionPlugin(),
...(process.env.ANALYZE ? [new BundleAnalyzerPlugin()] : [])
]// DefinePlugin 注入环境变量
const webpack = require('webpack')
plugins: [
new webpack.DefinePlugin({
__APP_ENV__: JSON.stringify(process.env.NODE_ENV)
})
]完整生产级配置
const path = require('path')
const webpack = require('webpack')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const CompressionPlugin = require('compression-webpack-plugin')
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer')
const TerserPlugin = require('terser-webpack-plugin')
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin')
module.exports = (env, argv) => {
const isDev = argv.mode === 'development'
const isAnalyze = env.ANALYZE
return {
entry: {
main: './src/main.ts',
},
output: {
path: path.resolve(__dirname, 'dist'),
filename: isDev ? 'js/[name].js' : 'js/[name].[contenthash:8].js',
chunkFilename: isDev ? 'js/[name].chunk.js' : 'js/[name].[contenthash:8].chunk.js',
clean: true,
publicPath: '/',
assetModuleFilename: 'assets/[name].[hash:8][ext]',
},
// Source Map 配置
devtool: isDev ? 'eval-cheap-module-source-map' : 'hidden-source-map',
// 开发服务器
devServer: {
port: 3000,
hot: true,
open: true,
historyApiFallback: true,
compress: true,
client: {
overlay: { errors: true, warnings: false },
},
proxy: {
'/api': {
target: 'http://localhost:5000',
changeOrigin: true,
pathRewrite: { '^/api': '' },
},
},
static: {
directory: path.join(__dirname, 'public'),
},
},
// 模块处理
module: {
rules: [
{
test: /\.ts$/,
use: {
loader: 'babel-loader',
options: {
cacheDirectory: true,
},
},
exclude: /node_modules/,
},
{
test: /\.css$/,
use: [
isDev ? 'style-loader' : MiniCssExtractPlugin.loader,
{ loader: 'css-loader', options: { importLoaders: 1 } },
'postcss-loader',
],
},
{
test: /\.(png|jpe?g|gif|webp|svg)$/,
type: 'asset',
parser: { dataUrlCondition: { maxSize: 8 * 1024 } },
generator: { filename: 'images/[name].[hash:8][ext]' },
},
{
test: /\.(woff2?|eot|ttf|otf)$/,
type: 'asset/resource',
generator: { filename: 'fonts/[name].[hash:8][ext]' },
},
],
},
// 模块解析
resolve: {
extensions: ['.ts', '.js', '.json'],
alias: { '@': path.resolve(__dirname, 'src') },
},
// 优化
optimization: {
minimize: !isDev,
minimizer: [
new TerserPlugin({
terserOptions: {
compress: {
drop_console: !isDev,
drop_debugger: true,
},
},
extractComments: false,
}),
new CssMinimizerPlugin(),
],
splitChunks: {
chunks: 'all',
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendor',
priority: 10,
},
},
},
runtimeChunk: 'single',
},
// 插件
plugins: [
new HtmlWebpackPlugin({
template: './public/index.html',
minify: !isDev ? { collapseWhitespace: true, removeComments: true } : false,
}),
!isDev && new MiniCssExtractPlugin({
filename: 'css/[name].[contenthash:8].css',
chunkFilename: 'css/[name].[contenthash:8].chunk.css',
}),
new webpack.DefinePlugin({
__APP_ENV__: JSON.stringify(process.env.NODE_ENV || 'development'),
}),
new webpack.ProgressPlugin(),
!isDev && new CompressionPlugin({
algorithm: 'gzip',
test: /\.(js|css|html|svg)$/,
threshold: 10240,
minRatio: 0.8,
}),
isAnalyze && new BundleAnalyzerPlugin({ analyzerMode: 'static' }),
].filter(Boolean),
}
}Webpack 与 Vite 的边界
Webpack 更适合:
- 历史项目维护
- 构建链路复杂的企业项目
- 对 loader / plugin 深度定制的场景
- Module Federation 微前端场景
Vite 更适合:
- 新项目起步
- 开发体验优先
- 中小型前端项目
- 快速迭代场景性能优化
// Webpack 构建速度优化
// 1. 缓存
// babel-loader 缓存
{ loader: 'babel-loader', options: { cacheDirectory: true } }
// 2. 多线程
const TerserPlugin = require('terser-webpack-plugin')
// terser-webpack-plugin 默认支持多线程
new TerserPlugin({ parallel: true })
// 3. 排除不需要编译的目录
{
test: /\.ts$/,
exclude: /node_modules/,
}
// 4. alias 减少解析时间
resolve: {
alias: {
'@': path.resolve(__dirname, 'src'),
'vue$': 'vue/dist/vue.runtime.esm.js',
},
},
// 5. 指定 modules 范围
resolve: {
modules: [path.resolve(__dirname, 'node_modules')],
},
// 6. DLL Plugin(预编译不常变化的依赖)
// webpack.dll.config.js
const DllPlugin = require('webpack/lib/DllPlugin')
module.exports = {
entry: {
vendor: ['vue', 'vue-router', 'axios', 'element-plus'],
},
output: {
filename: '[name].dll.js',
library: '[name]_library',
path: path.resolve(__dirname, 'dll'),
},
plugins: [
new DllPlugin({
name: '[name]_library',
path: path.resolve(__dirname, 'dll', '[name]-manifest.json'),
}),
],
}优点
缺点
总结
Webpack 最值得学的,不是记住一堆配置字段,而是理解它是如何构建依赖图、转换资源、切割代码和输出产物的。掌握这些核心原理后,无论你是在维护老项目、接手企业项目,还是做微前端和复杂工程化改造,都会更从容。
关键知识点
- Loader 负责"怎么处理资源",Plugin 负责"怎么扩展构建流程"。
- Code Splitting 要围绕真实加载路径设计,而不是为了拆而拆。
- Webpack 的强项在复杂场景控制力,不在开发体验速度。
- 构建问题常常不仅是工具问题,还和项目结构、依赖和缓存策略相关。
项目落地视角
- 企业老项目、组件库、复杂后台项目中仍大量使用 Webpack。
- 多环境构建、CDN 发布、产物 hash 管理都是高频落地点。
- 微前端 Module Federation 场景里 Webpack 仍然很强势。
- 需要分析首屏、缓存命中和 chunk 粒度时,Webpack 的构建图很重要。
常见误区
- 看见 Vite 流行就认为 Webpack 不值得学。
- 插件越多越高级,最后构建链路越来越脆弱。
- 只会复制配置,不理解 loader / plugin 的真实作用。
- 遇到构建慢只会换工具,不先检查依赖图和 chunk 设计。
进阶路线
- 深入理解 Module Federation、持久缓存、构建分析和产物优化。
- 学习 Babel、PostCSS、Sass、TS Loader 在 Webpack 中的协同。
- 为大项目建立构建性能监控和包体积基线。
- 逐步掌握从 Webpack 到 Vite / Rspack 的迁移思路。
适用场景
- 存量前端项目维护。
- 微前端工程。
- 复杂企业级构建体系。
- 需要细粒度控制构建流程的项目。
落地建议
- 新项目优先考虑 Vite,但团队必须懂 Webpack 原理。
- 构建配置按开发 / 生产拆分,避免一个配置文件无限膨胀。
- 对大依赖做单独拆包分析,不要把所有问题都交给默认 splitChunks。
- 建立构建耗时、包体积和缓存命中率的持续观测习惯。
排错清单
- 构建失败:先看 loader / plugin 版本和模块解析路径。
- 页面白屏:先看 HtmlWebpackPlugin 输出和资源路径。
- 包体积过大:先分析 bundle,再决定拆包方案。
- 本地和线上不一致:先检查 publicPath、环境变量和静态资源部署路径。
