Webpack基础
学习目标
完成本章学习后,你将能够:
- 深入理解Webpack的核心概念(entry、output、loader、plugin)和模块化打包原理
- 熟练配置Webpack开发环境和生产环境,掌握常用loader和plugin的使用方法
- 理解Webpack的构建流程和生命周期钩子,能够开发自定义loader和plugin
- 掌握代码分割、懒加载、缓存策略等性能优化技巧,提升应用加载速度
- 理解Webpack与Vite的区别和各自的适用场景,能够根据项目需求选择合适的构建工具
- 能够分析和解决Webpack构建过程中的常见问题,优化构建性能
- 掌握Webpack在企业级项目中的最佳实践和配置模式
前置知识
学习本章内容前,你需要掌握:
- Vite构建工具 - 了解现代构建工具的特性和优势
- JavaScript模块化 - 理解ES6模块、CommonJS模块的概念和使用
- Node.js基础 - 了解Node.js环境和npm包管理
- 包管理器详解 - 掌握npm/pnpm的基本使用
- 前端工程化概念 - 理解构建工具的作用和价值
问题引入
实际场景
想象你正在开发一个大型电商网站的前端项目。项目使用了Vue 3框架,包含数百个组件文件、样式文件、图片资源,还依赖了几十个第三方npm包。你面临以下挑战:
场景1:模块化开发的困境
javascript
// 在浏览器中,你不能直接这样写
import Vue from 'vue';
import axios from 'axios';
import './styles/main.css';
import logo from './assets/logo.png';
// 浏览器不认识这些import语句
// 也不知道如何加载CSS和图片
场景2:代码体积过大
- 所有代码打包在一起,首页加载需要下载5MB的JavaScript文件
- 用户访问首页时,也下载了只有管理员才用到的代码
- 第三方库(如Vue、axios)每次都重新下载,无法利用浏览器缓存
场景3:开发效率低下
- 每次修改代码都要手动刷新浏览器
- 无法使用TypeScript、Sass等需要编译的语言
- 图片资源需要手动优化和压缩
场景4:生产环境优化需求
- 代码需要压缩混淆以减小体积
- CSS需要提取到单独文件并添加浏览器前缀
- 图片需要压缩并转换为WebP格式
- 需要生成source map用于生产环境调试
为什么需要Webpack
Webpack正是为了解决这些问题而诞生的。它能够:
- 模块化打包:将各种类型的资源(JS、CSS、图片等)都视为模块,统一处理
- 代码转换:通过loader将TypeScript、Sass等转换为浏览器可识别的代码
- 代码分割:将代码拆分成多个bundle,实现按需加载
- 资源优化:压缩代码、优化图片、提取公共代码
- 开发体验:提供热更新、source map等开发工具
- 生态丰富:拥有庞大的loader和plugin生态系统
虽然现在有了Vite等更快的构建工具,但Webpack仍然是企业级项目的主流选择,特别是在需要复杂配置和高度定制的场景下。理解Webpack的工作原理,对于理解现代前端工程化至关重要。
核心概念
一句话定义
Webpack是一个模块打包工具,它将项目中的各种资源(JavaScript、CSS、图片等)视为模块,通过分析模块间的依赖关系,将它们打包成浏览器可以直接运行的静态资源。
通俗理解:Webpack就像一个"资源整理大师",它把你项目中散落的各种文件(代码、样式、图片)按照依赖关系整理打包,最终生成浏览器能够理解和运行的文件。
为什么需要Webpack:
- 解决模块化问题:浏览器原生不支持ES6模块和CommonJS模块,Webpack可以将模块化代码转换为浏览器可执行的代码
- 统一资源管理:将JavaScript、CSS、图片、字体等各种资源统一视为模块,用统一的方式管理
- 代码转换和优化:通过loader和plugin实现代码转换(TypeScript→JavaScript)、代码压缩、资源优化等功能
- 提升开发体验:提供热更新、source map等开发工具,提高开发效率
- 性能优化:通过代码分割、懒加载、Tree Shaking等技术优化应用性能
本质是什么:
Webpack的本质是一个静态模块打包器(static module bundler)。它的工作流程可以概括为:
1. 从入口文件开始
2. 递归分析所有依赖的模块
3. 使用loader转换各种类型的文件
4. 使用plugin在构建过程中执行各种任务
5. 最终输出打包后的文件
用一个简单的类比:Webpack就像一个工厂的生产线:
- 入口(entry):原材料进入工厂的入口
- loader:生产线上的加工机器,将原材料加工成半成品
- plugin:生产线上的质检员和包装工,在各个环节执行额外任务
- 输出(output):最终产品从工厂出来的出口
Webpack的四大核心概念
Webpack的配置围绕四个核心概念展开:Entry(入口) 、Output(输出) 、Loader(加载器) 、Plugin(插件)。理解这四个概念是掌握Webpack的关键。
1. Entry(入口)
定义:Entry指定Webpack从哪个文件开始构建依赖图。
作用:告诉Webpack应用的起点在哪里,Webpack会从这个起点开始递归地找出所有依赖的模块。
配置方式:
javascript
// webpack.config.js
// 方式1:单入口(字符串)
module.exports = {
entry: './src/index.js'
};
// 方式2:单入口(对象形式,可以指定chunk名称)
module.exports = {
entry: {
main: './src/index.js'
}
};
// 方式3:多入口(对象形式)
// 适用于多页面应用,每个页面一个入口
module.exports = {
entry: {
home: './src/pages/home/index.js',
about: './src/pages/about/index.js',
contact: './src/pages/contact/index.js'
}
};
// 方式4:动态入口(函数形式)
// 可以根据环境变量或其他条件动态返回入口配置
module.exports = {
entry: () => {
return {
main: './src/index.js',
// 只在生产环境添加polyfill入口
...(process.env.NODE_ENV === 'production' ? {
polyfill: './src/polyfill.js'
} : {})
};
}
};
// 方式5:数组形式(多个文件合并为一个chunk)
// 适用于需要将多个文件打包到一起的场景
module.exports = {
entry: {
main: ['./src/polyfill.js', './src/index.js']
// polyfill.js会先执行,然后是index.js
// 最终打包成一个bundle
}
};
实际应用场景:
javascript
// 场景1:单页面应用(SPA)
// 只有一个入口文件
module.exports = {
entry: './src/main.js'
};
// 场景2:多页面应用(MPA)
// 每个页面都有独立的入口
module.exports = {
entry: {
index: './src/pages/index/main.js',
product: './src/pages/product/main.js',
cart: './src/pages/cart/main.js',
user: './src/pages/user/main.js'
}
};
// 场景3:提取第三方库
// 将第三方库单独打包,利用浏览器缓存
module.exports = {
entry: {
app: './src/main.js',
vendor: ['vue', 'vue-router', 'axios']
}
};
// 场景4:根据环境配置不同入口
module.exports = {
entry: {
main: './src/main.js',
// 开发环境添加热更新客户端
...(process.env.NODE_ENV === 'development' ? {
hot: 'webpack-hot-middleware/client'
} : {})
}
};
关键点解析:
- 依赖图的起点:Webpack从entry开始,递归分析所有import/require的模块
- chunk的命名:entry对象的key会成为输出bundle的名称
- 多入口的独立性:每个入口都会生成独立的依赖图和bundle
- 数组形式的合并:数组中的多个文件会按顺序合并到一个bundle中
2. Output(输出)
定义:Output指定Webpack打包后的文件输出到哪里,以及如何命名这些文件。
作用:告诉Webpack在哪里输出它所创建的bundle,以及如何命名这些文件。
配置方式:
javascript
// webpack.config.js
const path = require('path');
// 基础配置
module.exports = {
output: {
// 输出文件的目录(绝对路径)
path: path.resolve(__dirname, 'dist'),
// 输出文件的名称
filename: 'bundle.js'
}
};
// 多入口配置(使用占位符)
module.exports = {
entry: {
home: './src/pages/home/index.js',
about: './src/pages/about/index.js'
},
output: {
path: path.resolve(__dirname, 'dist'),
// [name]会被替换为entry的key(home、about)
filename: '[name].bundle.js',
// 输出结果:
// dist/home.bundle.js
// dist/about.bundle.js
}
};
// 生产环境配置(添加hash用于缓存控制)
module.exports = {
output: {
path: path.resolve(__dirname, 'dist'),
// [contenthash]:根据文件内容生成hash
// 内容不变,hash不变,可以利用浏览器缓存
filename: '[name].[contenthash:8].js',
// 非入口chunk的文件名(如动态导入的模块)
chunkFilename: '[name].[contenthash:8].chunk.js',
// 输出结果:
// dist/main.a1b2c3d4.js
// dist/vendor.e5f6g7h8.js
}
};
// 完整配置示例
module.exports = {
output: {
// 输出目录
path: path.resolve(__dirname, 'dist'),
// 主bundle的文件名
filename: '[name].[contenthash:8].js',
// 非入口chunk的文件名
chunkFilename: '[name].[contenthash:8].chunk.js',
// 静态资源的输出目录(相对于path)
assetModuleFilename: 'assets/[name].[hash:8][ext]',
// 公共路径(CDN地址或服务器路径)
publicPath: '/',
// 清理输出目录(Webpack 5+)
clean: true,
// 输出的库的名称(用于开发库时)
library: {
name: 'MyLibrary',
type: 'umd'
}
}
};
常用占位符说明:
javascript
/**
* Webpack支持的文件名占位符
*/
// [name]:chunk的名称(entry的key或动态导入时指定的名称)
filename: '[name].js'
// 结果:main.js, vendor.js
// [id]:chunk的唯一标识符
filename: '[id].js'
// 结果:0.js, 1.js, 2.js
// [contenthash]:根据文件内容生成的hash(推荐用于生产环境)
filename: '[name].[contenthash].js'
// 结果:main.a1b2c3d4e5f6g7h8.js
// 内容改变,hash改变;内容不变,hash不变
// [contenthash:8]:只取hash的前8位
filename: '[name].[contenthash:8].js'
// 结果:main.a1b2c3d4.js
// [chunkhash]:根据chunk内容生成的hash
filename: '[name].[chunkhash].js'
// 同一个entry的所有chunk共享相同的chunkhash
// [hash]:根据整个构建过程生成的hash(不推荐)
filename: '[name].[hash].js'
// 任何文件改变,所有文件的hash都会改变
// [ext]:文件扩展名
assetModuleFilename: '[name].[hash:8][ext]'
// 结果:logo.a1b2c3d4.png
// [query]:文件的query参数
assetModuleFilename: '[name][ext][query]'
// 结果:logo.png?v=1.0.0
实际应用场景:
javascript
// 场景1:开发环境配置
// 不需要hash,方便调试
module.exports = {
mode: 'development',
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].js',
chunkFilename: '[name].chunk.js',
publicPath: '/'
}
};
// 场景2:生产环境配置
// 使用contenthash,利用浏览器缓存
module.exports = {
mode: 'production',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'js/[name].[contenthash:8].js',
chunkFilename: 'js/[name].[contenthash:8].chunk.js',
assetModuleFilename: 'assets/[name].[hash:8][ext]',
publicPath: '/',
clean: true // 每次构建前清理输出目录
}
};
// 场景3:CDN部署
// 将静态资源部署到CDN
module.exports = {
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].[contenthash:8].js',
// CDN地址
publicPath: 'https://cdn.example.com/assets/',
// 输出的HTML中的资源路径会自动添加CDN前缀
// <script src="https://cdn.example.com/assets/main.a1b2c3d4.js"></script>
}
};
// 场景4:开发库(library)
// 将代码打包成可供其他项目使用的库
module.exports = {
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'my-library.js',
library: {
name: 'MyLibrary',
type: 'umd', // 支持多种模块规范(AMD、CommonJS、全局变量)
export: 'default' // 导出default
},
globalObject: 'this' // 确保在各种环境下都能正常工作
}
};
// 场景5:多页面应用
// 每个页面输出到独立的目录
module.exports = {
entry: {
'pages/home/index': './src/pages/home/index.js',
'pages/about/index': './src/pages/about/index.js'
},
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].js',
// 输出结果:
// dist/pages/home/index.js
// dist/pages/about/index.js
}
};
关键点解析:
- path必须是绝对路径 :使用
path.resolve()或path.join()生成绝对路径 - contenthash vs chunkhash vs hash :
contenthash:根据文件内容生成,推荐用于生产环境chunkhash:根据chunk内容生成,同一entry的chunk共享hashhash:根据整个构建生成,任何改变都会影响所有文件
- publicPath的作用:指定资源的公共路径,影响HTML中引用资源的路径
- clean选项:Webpack 5新增,自动清理输出目录,无需额外插件
3. Loader(加载器)
定义:Loader是Webpack用来处理非JavaScript文件的转换器,它可以将各种类型的文件转换为Webpack能够处理的有效模块。
作用:Webpack本身只能理解JavaScript和JSON文件。Loader让Webpack能够处理其他类型的文件(CSS、图片、TypeScript等),并将它们转换为有效的模块。
工作原理:
原始文件 → Loader处理 → 转换后的模块 → Webpack打包
Loader的执行顺序是从右到左(或从下到上)的链式调用:
javascript
// 执行顺序:sass-loader → postcss-loader → css-loader → style-loader
module.exports = {
module: {
rules: [
{
test: /\.scss$/,
use: ['style-loader', 'css-loader', 'postcss-loader', 'sass-loader']
}
]
}
};
配置方式:
javascript
// webpack.config.js
module.exports = {
module: {
rules: [
// 规则1:处理CSS文件
{
test: /\.css$/, // 匹配.css文件
use: ['style-loader', 'css-loader'] // 使用的loader(从右到左执行)
},
// 规则2:处理Sass/SCSS文件
{
test: /\.scss$/,
use: [
'style-loader', // 将CSS注入到DOM中
'css-loader', // 解析CSS文件中的@import和url()
'postcss-loader', // 添加浏览器前缀等
'sass-loader' // 将Sass编译为CSS
]
},
// 规则3:处理图片文件(Webpack 5使用asset module)
{
test: /\.(png|jpe?g|gif|svg)$/i,
type: 'asset', // 自动选择导出为单独文件或data URI
parser: {
dataUrlCondition: {
maxSize: 8 * 1024 // 小于8KB的图片转为base64
}
},
generator: {
filename: 'images/[name].[hash:8][ext]' // 输出路径和文件名
}
},
// 规则4:处理字体文件
{
test: /\.(woff|woff2|eot|ttf|otf)$/i,
type: 'asset/resource', // 导出为单独文件
generator: {
filename: 'fonts/[name].[hash:8][ext]'
}
},
// 规则5:处理TypeScript文件
{
test: /\.tsx?$/,
use: 'ts-loader',
exclude: /node_modules/ // 排除node_modules目录
},
// 规则6:处理JavaScript文件(使用Babel转译)
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: [
['@babel/preset-env', {
targets: '> 0.25%, not dead', // 目标浏览器
useBuiltIns: 'usage', // 按需引入polyfill
corejs: 3
}]
],
cacheDirectory: true // 启用缓存,提升构建速度
}
}
}
]
}
};
常用Loader详解:
javascript
/**
* 1. 样式相关Loader
*/
// style-loader:将CSS注入到DOM中(通过<style>标签)
// css-loader:解析CSS文件,处理@import和url()
// sass-loader:将Sass/SCSS编译为CSS
// less-loader:将Less编译为CSS
// postcss-loader:使用PostCSS处理CSS(添加前缀、压缩等)
{
test: /\.scss$/,
use: [
'style-loader',
{
loader: 'css-loader',
options: {
modules: true, // 启用CSS Modules
importLoaders: 2 // 在css-loader前应用的loader数量
}
},
{
loader: 'postcss-loader',
options: {
postcssOptions: {
plugins: [
'autoprefixer', // 自动添加浏览器前缀
'cssnano' // 压缩CSS
]
}
}
},
'sass-loader'
]
}
/**
* 2. 文件相关Loader(Webpack 5使用asset module代替)
*/
// Webpack 4及以前使用的loader:
// file-loader:将文件输出到输出目录,返回文件路径
// url-loader:类似file-loader,但可以将小文件转为base64
// raw-loader:将文件内容作为字符串导入
// Webpack 5的asset module(推荐):
{
test: /\.(png|jpg|gif)$/,
type: 'asset', // 自动选择
// type: 'asset/resource', // 相当于file-loader
// type: 'asset/inline', // 相当于url-loader
// type: 'asset/source', // 相当于raw-loader
parser: {
dataUrlCondition: {
maxSize: 8 * 1024 // 8KB以下转base64
}
}
}
/**
* 3. JavaScript转译Loader
*/
// babel-loader:使用Babel转译JavaScript
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: [
'@babel/preset-env', // 转译ES6+语法
'@babel/preset-react', // 转译React JSX
'@babel/preset-typescript' // 转译TypeScript
],
plugins: [
'@babel/plugin-proposal-class-properties', // 支持类属性
'@babel/plugin-transform-runtime' // 避免重复注入helper代码
],
cacheDirectory: true // 启用缓存
}
}
}
// ts-loader:使用TypeScript编译器转译TypeScript
{
test: /\.tsx?$/,
use: 'ts-loader',
exclude: /node_modules/
}
/**
* 4. Vue相关Loader
*/
// vue-loader:解析.vue单文件组件
{
test: /\.vue$/,
use: 'vue-loader'
}
// 注意:使用vue-loader需要配合VueLoaderPlugin
const { VueLoaderPlugin } = require('vue-loader');
module.exports = {
module: {
rules: [
{ test: /\.vue$/, use: 'vue-loader' }
]
},
plugins: [
new VueLoaderPlugin()
]
};
/**
* 5. 其他常用Loader
*/
// eslint-loader:在构建时进行代码检查
{
test: /\.js$/,
enforce: 'pre', // 在其他loader之前执行
use: 'eslint-loader',
exclude: /node_modules/
}
// thread-loader:将loader放在worker池中运行,提升构建速度
{
test: /\.js$/,
use: [
'thread-loader', // 放在其他loader之前
'babel-loader'
]
}
// cache-loader:缓存loader的执行结果
{
test: /\.js$/,
use: [
'cache-loader', // 放在其他loader之前
'babel-loader'
]
}
Loader的执行顺序:
javascript
/**
* Loader的执行顺序示例
*/
// 示例1:处理SCSS文件
{
test: /\.scss$/,
use: ['style-loader', 'css-loader', 'sass-loader']
}
// 执行流程:
// 1. sass-loader:将SCSS编译为CSS
// 输入:.scss文件
// 输出:CSS代码
//
// 2. css-loader:解析CSS中的@import和url()
// 输入:CSS代码
// 输出:JavaScript模块(包含CSS字符串)
//
// 3. style-loader:将CSS注入到DOM中
// 输入:JavaScript模块
// 输出:在HTML中插入<style>标签
// 示例2:处理TypeScript + React
{
test: /\.tsx$/,
use: ['babel-loader', 'ts-loader']
}
// 执行流程:
// 1. ts-loader:将TypeScript编译为JavaScript
// 输入:.tsx文件(TypeScript + JSX)
// 输出:.js文件(JavaScript + JSX)
//
// 2. babel-loader:将JSX和ES6+语法转译为ES5
// 输入:.js文件(JavaScript + JSX)
// 输出:浏览器兼容的JavaScript代码
实际应用场景:
javascript
// 场景1:Vue 3项目配置
module.exports = {
module: {
rules: [
// 处理.vue文件
{
test: /\.vue$/,
use: 'vue-loader'
},
// 处理JavaScript
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env']
}
}
},
// 处理CSS
{
test: /\.css$/,
use: ['style-loader', 'css-loader']
},
// 处理Sass
{
test: /\.scss$/,
use: ['style-loader', 'css-loader', 'sass-loader']
},
// 处理图片
{
test: /\.(png|jpe?g|gif|svg)$/i,
type: 'asset',
parser: {
dataUrlCondition: {
maxSize: 8 * 1024
}
}
}
]
}
};
// 场景2:React + TypeScript项目配置
module.exports = {
module: {
rules: [
// 处理TypeScript和TSX
{
test: /\.tsx?$/,
use: 'ts-loader',
exclude: /node_modules/
},
// 处理CSS Modules
{
test: /\.module\.css$/,
use: [
'style-loader',
{
loader: 'css-loader',
options: {
modules: {
localIdentName: '[name]__[local]--[hash:base64:5]'
}
}
}
]
},
// 处理普通CSS
{
test: /\.css$/,
exclude: /\.module\.css$/,
use: ['style-loader', 'css-loader']
}
]
}
};
// 场景3:生产环境优化配置
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = {
module: {
rules: [
{
test: /\.scss$/,
use: [
// 生产环境提取CSS到单独文件
MiniCssExtractPlugin.loader,
{
loader: 'css-loader',
options: {
importLoaders: 2,
sourceMap: true
}
},
{
loader: 'postcss-loader',
options: {
postcssOptions: {
plugins: [
'autoprefixer',
['cssnano', { preset: 'default' }]
]
}
}
},
'sass-loader'
]
},
{
test: /\.js$/,
exclude: /node_modules/,
use: [
// 使用thread-loader加速构建
'thread-loader',
{
loader: 'babel-loader',
options: {
cacheDirectory: true,
cacheCompression: false
}
}
]
}
]
},
plugins: [
new MiniCssExtractPlugin({
filename: 'css/[name].[contenthash:8].css'
})
]
};
关键点解析:
- Loader的本质:Loader是一个函数,接收源文件内容,返回转换后的内容
- 执行顺序:从右到左(或从下到上)链式调用
- 配置灵活性:可以使用字符串、对象或数组形式配置loader
- 性能优化 :使用
exclude排除不需要处理的文件,使用cacheDirectory启用缓存 - Webpack 5的改进:使用asset module代替file-loader、url-loader等
4. Plugin(插件)
定义:Plugin是Webpack的扩展机制,它可以在Webpack构建流程的特定时机执行特定的任务,实现loader无法完成的功能。
作用:Plugin用于执行范围更广的任务,如打包优化、资源管理、环境变量注入等。它可以访问Webpack的完整编译生命周期。
Loader vs Plugin的区别:
Loader:
- 作用于单个文件
- 在文件被加载时执行
- 用于转换文件内容
- 例如:将TypeScript转为JavaScript
Plugin:
- 作用于整个构建过程
- 在构建的特定时机执行
- 用于执行更广泛的任务
- 例如:压缩代码、提取CSS、生成HTML
配置方式:
javascript
// webpack.config.js
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
module.exports = {
plugins: [
// Plugin需要实例化
new HtmlWebpackPlugin({
template: './src/index.html',
filename: 'index.html'
}),
new MiniCssExtractPlugin({
filename: 'css/[name].[contenthash:8].css'
}),
new CleanWebpackPlugin()
]
};
常用Plugin详解:
javascript
/**
* 1. HtmlWebpackPlugin
* 作用:自动生成HTML文件,并自动引入打包后的资源
*/
const HtmlWebpackPlugin = require('html-webpack-plugin');
// 基础用法
new HtmlWebpackPlugin({
template: './src/index.html', // HTML模板文件
filename: 'index.html', // 输出的HTML文件名
inject: 'body', // 脚本注入位置:'head' | 'body' | true | false
minify: {
removeComments: true, // 移除注释
collapseWhitespace: true, // 移除空格
removeAttributeQuotes: true // 移除属性引号
},
chunks: ['main', 'vendor'], // 指定要引入的chunk
chunksSortMode: 'manual' // chunk排序方式
})
// 多页面应用配置
module.exports = {
entry: {
index: './src/pages/index/main.js',
about: './src/pages/about/main.js'
},
plugins: [
// 为每个页面生成独立的HTML
new HtmlWebpackPlugin({
template: './src/pages/index/index.html',
filename: 'index.html',
chunks: ['index'] // 只引入index chunk
}),
new HtmlWebpackPlugin({
template: './src/pages/about/index.html',
filename: 'about.html',
chunks: ['about'] // 只引入about chunk
})
]
};
/**
* 2. MiniCssExtractPlugin
* 作用:将CSS提取到单独的文件中(生产环境推荐)
*/
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = {
module: {
rules: [
{
test: /\.css$/,
use: [
// 开发环境使用style-loader,生产环境使用MiniCssExtractPlugin.loader
process.env.NODE_ENV === 'production'
? MiniCssExtractPlugin.loader
: 'style-loader',
'css-loader'
]
}
]
},
plugins: [
new MiniCssExtractPlugin({
filename: 'css/[name].[contenthash:8].css', // 输出文件名
chunkFilename: 'css/[name].[contenthash:8].chunk.css', // 非入口chunk的文件名
ignoreOrder: false // 是否忽略CSS顺序警告
})
]
};
/**
* 3. CleanWebpackPlugin
* 作用:在每次构建前清理输出目录
* 注意:Webpack 5可以使用output.clean代替
*/
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
new CleanWebpackPlugin({
cleanOnceBeforeBuildPatterns: ['**/*', '!static-files*'], // 清理模式
cleanAfterEveryBuildPatterns: ['!*.json'], // 构建后保留的文件
verbose: true, // 显示日志
dry: false // 是否模拟删除(不实际删除)
})
// Webpack 5的替代方案
module.exports = {
output: {
clean: true // 自动清理输出目录
}
};
/**
* 4. DefinePlugin
* 作用:定义全局常量,可以在代码中直接使用
*/
const webpack = require('webpack');
new webpack.DefinePlugin({
'process.env.NODE_ENV': JSON.stringify('production'),
'process.env.API_URL': JSON.stringify('https://api.example.com'),
'__DEV__': JSON.stringify(false),
'VERSION': JSON.stringify('1.0.0')
})
// 在代码中使用
if (__DEV__) {
console.log('开发环境');
}
console.log('API地址:', process.env.API_URL);
/**
* 5. CopyWebpackPlugin
* 作用:复制文件或目录到输出目录
*/
const CopyWebpackPlugin = require('copy-webpack-plugin');
new CopyWebpackPlugin({
patterns: [
{
from: 'public', // 源目录
to: 'assets', // 目标目录(相对于output.path)
globOptions: {
ignore: ['*.DS_Store'] // 忽略的文件
}
},
{
from: 'src/assets/images',
to: 'images',
noErrorOnMissing: true // 源目录不存在时不报错
}
]
})
/**
* 6. CompressionWebpackPlugin
* 作用:生成gzip压缩文件
*/
const CompressionWebpackPlugin = require('compression-webpack-plugin');
new CompressionWebpackPlugin({
filename: '[path][base].gz', // 压缩后的文件名
algorithm: 'gzip', // 压缩算法
test: /\.(js|css|html|svg)$/, // 匹配的文件
threshold: 10240, // 只压缩大于10KB的文件
minRatio: 0.8, // 压缩比小于0.8才会压缩
deleteOriginalAssets: false // 是否删除原文件
})
/**
* 7. BundleAnalyzerPlugin
* 作用:可视化分析bundle的大小和组成
*/
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');
new BundleAnalyzerPlugin({
analyzerMode: 'static', // 生成静态HTML文件
reportFilename: 'bundle-report.html',
openAnalyzer: false, // 是否自动打开浏览器
generateStatsFile: true, // 生成stats.json文件
statsFilename: 'stats.json'
})
/**
* 8. ProvidePlugin
* 作用:自动加载模块,无需import
*/
const webpack = require('webpack');
new webpack.ProvidePlugin({
$: 'jquery', // 在代码中使用$时,自动import jquery
jQuery: 'jquery',
_: 'lodash',
Vue: ['vue/dist/vue.esm.js', 'default']
})
// 使用后,代码中可以直接使用$,无需import
$('#app').html('Hello');
/**
* 9. HotModuleReplacementPlugin
* 作用:启用热模块替换(HMR)
*/
const webpack = require('webpack');
module.exports = {
devServer: {
hot: true // 启用HMR
},
plugins: [
new webpack.HotModuleReplacementPlugin()
]
};
// 在代码中使用HMR API
if (module.hot) {
module.hot.accept('./module.js', () => {
// 模块更新时的回调
console.log('模块已更新');
});
}
/**
* 10. ESLintWebpackPlugin
* 作用:在构建时进行代码检查
*/
const ESLintPlugin = require('eslint-webpack-plugin');
new ESLintPlugin({
extensions: ['js', 'jsx', 'ts', 'tsx'], // 检查的文件类型
exclude: 'node_modules', // 排除的目录
fix: true, // 自动修复
cache: true, // 启用缓存
cacheLocation: '.eslintcache' // 缓存文件位置
})
Plugin的生命周期钩子:
javascript
/**
* Webpack的构建流程和Plugin钩子
*/
// Plugin的基本结构
class MyPlugin {
apply(compiler) {
// compiler:Webpack的编译器对象,包含完整的配置信息
// 1. 初始化阶段
compiler.hooks.initialize.tap('MyPlugin', () => {
console.log('Webpack初始化');
});
// 2. 编译开始前
compiler.hooks.beforeRun.tapAsync('MyPlugin', (compiler, callback) => {
console.log('开始编译前');
callback();
});
// 3. 编译开始
compiler.hooks.run.tapAsync('MyPlugin', (compiler, callback) => {
console.log('开始编译');
callback();
});
// 4. 编译过程中
compiler.hooks.compile.tap('MyPlugin', (params) => {
console.log('编译中');
});
// 5. 生成资源到输出目录前
compiler.hooks.emit.tapAsync('MyPlugin', (compilation, callback) => {
console.log('生成资源');
// compilation:包含当前编译的所有信息
// 可以访问所有模块、chunk、资源等
// 遍历所有生成的资源
for (const filename in compilation.assets) {
console.log('生成文件:', filename);
}
callback();
});
// 6. 资源已输出到输出目录
compiler.hooks.done.tap('MyPlugin', (stats) => {
console.log('编译完成');
// stats:包含编译统计信息
console.log('编译耗时:', stats.endTime - stats.startTime, 'ms');
});
}
}
// 使用自定义Plugin
module.exports = {
plugins: [
new MyPlugin()
]
};
实际应用场景:
javascript
// 场景1:开发环境配置
const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
mode: 'development',
plugins: [
// 生成HTML
new HtmlWebpackPlugin({
template: './src/index.html'
}),
// 定义环境变量
new webpack.DefinePlugin({
'process.env.NODE_ENV': JSON.stringify('development')
}),
// 启用HMR
new webpack.HotModuleReplacementPlugin()
]
};
// 场景2:生产环境配置
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
const TerserPlugin = require('terser-webpack-plugin');
const CompressionWebpackPlugin = require('compression-webpack-plugin');
module.exports = {
mode: 'production',
plugins: [
// 生成HTML
new HtmlWebpackPlugin({
template: './src/index.html',
minify: {
removeComments: true,
collapseWhitespace: true,
removeAttributeQuotes: true
}
}),
// 提取CSS
new MiniCssExtractPlugin({
filename: 'css/[name].[contenthash:8].css'
}),
// 生成gzip文件
new CompressionWebpackPlugin({
algorithm: 'gzip',
test: /\.(js|css|html)$/,
threshold: 10240,
minRatio: 0.8
})
],
optimization: {
minimizer: [
// 压缩JavaScript
new TerserPlugin({
parallel: true, // 多进程并行压缩
terserOptions: {
compress: {
drop_console: true // 移除console
}
}
}),
// 压缩CSS
new CssMinimizerPlugin()
]
}
};
// 场景3:多页面应用配置
const HtmlWebpackPlugin = require('html-webpack-plugin');
const pages = ['index', 'about', 'contact'];
module.exports = {
entry: pages.reduce((entry, page) => {
entry[page] = `./src/pages/${page}/main.js`;
return entry;
}, {}),
plugins: [
// 为每个页面生成HTML
...pages.map(page => new HtmlWebpackPlugin({
template: `./src/pages/${page}/index.html`,
filename: `${page}.html`,
chunks: [page, 'vendor', 'common']
}))
]
};
// 场景4:性能分析配置
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');
const SpeedMeasurePlugin = require('speed-measure-webpack-plugin');
const smp = new SpeedMeasurePlugin();
module.exports = smp.wrap({
plugins: [
// 分析bundle大小
new BundleAnalyzerPlugin({
analyzerMode: 'static',
reportFilename: 'bundle-report.html',
openAnalyzer: false
})
]
});
关键点解析:
- Plugin的本质 :Plugin是一个具有
apply方法的类或对象 - 生命周期钩子:Plugin通过监听Webpack的生命周期钩子来执行任务
- Tapable机制:Webpack使用Tapable库实现事件流机制
- 配置方式:Plugin需要实例化后添加到plugins数组中
- 执行时机:Plugin可以在构建的任何阶段执行,比Loader更灵活
Webpack的构建流程
理解Webpack的构建流程对于深入掌握Webpack至关重要。Webpack的构建过程可以分为以下几个阶段:
初始化参数
开始编译
确定入口
编译模块
完成模块编译
输出资源
输出完成
调用Loader转换
递归处理依赖
根据依赖关系组装Chunk
将Chunk转换为文件
输出到文件系统
详细流程说明:
javascript
/**
* 1. 初始化参数阶段
*
* 从配置文件和Shell语句中读取并合并参数,得出最终的配置对象
*/
// webpack.config.js中的配置
const config = {
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'bundle.js'
}
};
// 命令行参数
// webpack --mode production --config webpack.prod.js
// 最终合并后的配置
const finalConfig = {
...config,
mode: 'production',
// ... 其他默认配置
};
/**
* 2. 开始编译阶段
*
* 用上一步得到的参数初始化Compiler对象,加载所有配置的插件,
* 执行对象的run方法开始执行编译
*/
class Compiler {
constructor(options) {
this.options = options;
this.hooks = {
run: new SyncHook(),
compile: new SyncHook(),
emit: new AsyncSeriesHook(),
done: new SyncHook()
};
}
run(callback) {
// 触发run钩子
this.hooks.run.call();
// 开始编译
this.compile((err, compilation) => {
if (err) return callback(err);
// 输出资源
this.emitAssets(compilation, (err) => {
if (err) return callback(err);
// 触发done钩子
this.hooks.done.call();
callback(null);
});
});
}
}
/**
* 3. 确定入口阶段
*
* 根据配置中的entry找出所有的入口文件
*/
// 单入口
entry: './src/index.js'
// 结果:['./src/index.js']
// 多入口
entry: {
home: './src/pages/home/index.js',
about: './src/pages/about/index.js'
}
// 结果:['./src/pages/home/index.js', './src/pages/about/index.js']
/**
* 4. 编译模块阶段
*
* 从入口文件出发,调用所有配置的Loader对模块进行转换,
* 再找出该模块依赖的模块,递归本步骤直到所有入口依赖的文件都经过了本步骤的处理
*/
// 伪代码示例
function buildModule(modulePath) {
// 1. 读取文件内容
let source = fs.readFileSync(modulePath, 'utf-8');
// 2. 找到匹配的loader规则
const rule = findMatchingRule(modulePath);
// 3. 使用loader转换源代码(从右到左执行)
if (rule && rule.use) {
for (let i = rule.use.length - 1; i >= 0; i--) {
const loader = rule.use[i];
source = loader(source);
}
}
// 4. 解析转换后的代码,找出依赖的模块
const dependencies = parseDependencies(source);
// 5. 递归处理依赖的模块
dependencies.forEach(dep => {
buildModule(dep);
});
// 6. 返回处理后的模块
return {
path: modulePath,
source: source,
dependencies: dependencies
};
}
/**
* 5. 完成模块编译阶段
*
* 在经过第4步使用Loader转换完所有模块后,得到了每个模块被转换后的最终内容
* 以及它们之间的依赖关系
*/
// 模块依赖图示例
const moduleGraph = {
'./src/index.js': {
source: '...',
dependencies: ['./src/utils.js', './src/components/App.vue']
},
'./src/utils.js': {
source: '...',
dependencies: ['lodash']
},
'./src/components/App.vue': {
source: '...',
dependencies: ['vue', './src/components/Header.vue']
}
};
/**
* 6. 输出资源阶段
*
* 根据入口和模块之间的依赖关系,组装成一个个包含多个模块的Chunk,
* 再把每个Chunk转换成一个单独的文件加入到输出列表
*/
// 伪代码示例
function createChunks(entryModules, moduleGraph) {
const chunks = [];
// 为每个入口创建一个chunk
entryModules.forEach(entryModule => {
const chunk = {
name: entryModule.name,
modules: []
};
// 收集该入口依赖的所有模块
function collectModules(modulePath) {
if (chunk.modules.includes(modulePath)) return;
chunk.modules.push(modulePath);
const module = moduleGraph[modulePath];
module.dependencies.forEach(dep => {
collectModules(dep);
});
}
collectModules(entryModule.path);
chunks.push(chunk);
});
return chunks;
}
/**
* 7. 输出完成阶段
*
* 在确定好输出内容后,根据配置确定输出的路径和文件名,
* 把文件内容写入到文件系统
*/
function emitFiles(chunks, outputPath) {
chunks.forEach(chunk => {
// 生成文件内容
const fileContent = generateFileContent(chunk);
// 确定文件名
const fileName = chunk.name + '.js';
// 写入文件系统
const filePath = path.join(outputPath, fileName);
fs.writeFileSync(filePath, fileContent);
console.log(`输出文件: ${filePath}`);
});
}
构建流程中的关键概念:
javascript
/**
* Module(模块)
*
* Webpack中的一切皆模块,每个文件都是一个模块
*/
// JavaScript模块
import utils from './utils.js';
// CSS模块
import './styles.css';
// 图片模块
import logo from './logo.png';
// Vue组件模块
import App from './App.vue';
/**
* Chunk(代码块)
*
* Chunk是Webpack打包过程中的一组模块的集合
*/
// 入口chunk:由entry配置生成
entry: {
main: './src/index.js',
vendor: './src/vendor.js'
}
// 生成两个chunk:main chunk和vendor chunk
// 异步chunk:由动态导入生成
import(/* webpackChunkName: "about" */ './pages/about.js')
// 生成一个名为about的chunk
// 公共chunk:由optimization.splitChunks生成
optimization: {
splitChunks: {
chunks: 'all',
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendor'
}
}
}
}
// 将node_modules中的模块提取到vendor chunk
/**
* Bundle(打包文件)
*
* Bundle是Webpack最终输出的文件,一个chunk对应一个bundle
*/
// 输出配置
output: {
filename: '[name].[contenthash:8].js',
chunkFilename: '[name].[contenthash:8].chunk.js'
}
// 生成的bundle文件:
// main.a1b2c3d4.js
// vendor.e5f6g7h8.js
// about.i9j0k1l2.chunk.js
/**
* Dependency Graph(依赖图)
*
* Webpack通过分析模块间的依赖关系,构建出完整的依赖图
*/
// 示例依赖图
const dependencyGraph = {
'src/index.js': {
dependencies: [
'src/App.vue',
'src/router/index.js',
'src/store/index.js'
]
},
'src/App.vue': {
dependencies: [
'vue',
'src/components/Header.vue',
'src/components/Footer.vue'
]
},
'src/router/index.js': {
dependencies: [
'vue-router',
'src/views/Home.vue',
'src/views/About.vue'
]
}
};
Webpack的事件流机制:
javascript
/**
* Webpack使用Tapable库实现事件流机制
*
* Tapable提供了多种类型的钩子:
* - SyncHook:同步钩子
* - AsyncSeriesHook:异步串行钩子
* - AsyncParallelHook:异步并行钩子
*/
const { SyncHook, AsyncSeriesHook } = require('tapable');
class Compiler {
constructor() {
this.hooks = {
// 同步钩子
run: new SyncHook(['compiler']),
compile: new SyncHook(['params']),
// 异步钩子
emit: new AsyncSeriesHook(['compilation']),
done: new AsyncSeriesHook(['stats'])
};
}
run() {
// 触发run钩子
this.hooks.run.call(this);
// 触发compile钩子
this.hooks.compile.call({ /* params */ });
// 触发emit钩子(异步)
this.hooks.emit.callAsync(compilation, (err) => {
if (err) return;
// 触发done钩子(异步)
this.hooks.done.callAsync(stats, (err) => {
// 构建完成
});
});
}
}
// Plugin监听钩子
class MyPlugin {
apply(compiler) {
// 监听同步钩子
compiler.hooks.run.tap('MyPlugin', (compiler) => {
console.log('开始编译');
});
// 监听异步钩子
compiler.hooks.emit.tapAsync('MyPlugin', (compilation, callback) => {
console.log('生成资源');
// 执行异步操作
setTimeout(() => {
callback();
}, 1000);
});
// 使用Promise监听异步钩子
compiler.hooks.done.tapPromise('MyPlugin', (stats) => {
return new Promise((resolve) => {
console.log('编译完成');
resolve();
});
});
}
}
构建流程的性能优化点:
javascript
/**
* 1. 缩小文件搜索范围
*/
module.exports = {
resolve: {
// 指定模块搜索目录
modules: [path.resolve(__dirname, 'src'), 'node_modules'],
// 指定文件扩展名
extensions: ['.js', '.vue', '.json'],
// 指定主文件名
mainFiles: ['index'],
// 配置别名
alias: {
'@': path.resolve(__dirname, 'src'),
'vue$': 'vue/dist/vue.esm.js'
}
},
module: {
rules: [
{
test: /\.js$/,
// 只处理src目录
include: path.resolve(__dirname, 'src'),
// 排除node_modules
exclude: /node_modules/,
use: 'babel-loader'
}
]
}
};
/**
* 2. 使用缓存
*/
module.exports = {
cache: {
type: 'filesystem', // 使用文件系统缓存
cacheDirectory: path.resolve(__dirname, '.webpack_cache')
},
module: {
rules: [
{
test: /\.js$/,
use: {
loader: 'babel-loader',
options: {
cacheDirectory: true // 启用Babel缓存
}
}
}
]
}
};
/**
* 3. 多进程构建
*/
const TerserPlugin = require('terser-webpack-plugin');
module.exports = {
module: {
rules: [
{
test: /\.js$/,
use: [
'thread-loader', // 使用多进程处理
'babel-loader'
]
}
]
},
optimization: {
minimizer: [
new TerserPlugin({
parallel: true // 多进程压缩
})
]
}
};
/**
* 4. 减少模块解析
*/
module.exports = {
module: {
// 不解析这些模块的依赖
noParse: /jquery|lodash/,
rules: [
{
test: /\.js$/,
use: 'babel-loader',
// 排除已经编译好的库
exclude: /node_modules/
}
]
}
};
关键点解析:
- 构建流程是串行的:从初始化到输出,按顺序执行
- 模块处理是递归的:从入口开始,递归处理所有依赖
- 事件流机制:通过Tapable实现,Plugin通过监听钩子来介入构建过程
- 性能优化:缩小搜索范围、使用缓存、多进程构建是关键
代码分割与懒加载
代码分割(Code Splitting)是Webpack最重要的性能优化手段之一。它可以将代码拆分成多个bundle,实现按需加载,减少首屏加载时间。
代码分割的三种方式
javascript
/**
* 方式1:多入口配置
*
* 通过配置多个entry,将代码分割成多个bundle
*/
module.exports = {
entry: {
main: './src/index.js',
admin: './src/admin.js'
},
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist')
}
};
// 输出结果:
// dist/main.bundle.js
// dist/admin.bundle.js
/**
* 方式2:动态导入(推荐)
*
* 使用import()语法动态导入模块,Webpack会自动进行代码分割
*/
// 普通导入(会打包到主bundle中)
import utils from './utils.js';
// 动态导入(会分割成独立的chunk)
import('./utils.js').then(module => {
const utils = module.default;
utils.doSomething();
});
// 使用async/await语法
async function loadUtils() {
const module = await import('./utils.js');
const utils = module.default;
return utils;
}
// 指定chunk名称
import(
/* webpackChunkName: "utils" */
'./utils.js'
).then(module => {
// ...
});
// 预加载(prefetch):浏览器空闲时加载
import(
/* webpackPrefetch: true */
'./utils.js'
);
// 预获取(preload):与父chunk并行加载
import(
/* webpackPreload: true */
'./utils.js'
);
/**
* 方式3:SplitChunksPlugin(自动分割)
*
* Webpack 4+内置的代码分割插件,可以自动提取公共代码
*/
module.exports = {
optimization: {
splitChunks: {
chunks: 'all', // 对所有chunk进行分割
minSize: 20000, // 最小chunk大小(字节)
minChunks: 1, // 最少被引用次数
maxAsyncRequests: 30, // 最大异步请求数
maxInitialRequests: 30, // 最大初始请求数
cacheGroups: {
// 提取第三方库
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendor',
priority: 10 // 优先级
},
// 提取公共代码
common: {
minChunks: 2,
name: 'common',
priority: 5,
reuseExistingChunk: true // 复用已存在的chunk
}
}
}
}
};
实际应用场景
javascript
/**
* 场景1:路由懒加载(Vue Router)
*/
// 不推荐:所有路由组件都打包到主bundle
import Home from './views/Home.vue';
import About from './views/About.vue';
import Contact from './views/Contact.vue';
const routes = [
{ path: '/', component: Home },
{ path: '/about', component: About },
{ path: '/contact', component: Contact }
];
// 推荐:路由组件按需加载
const routes = [
{
path: '/',
component: () => import(
/* webpackChunkName: "home" */
'./views/Home.vue'
)
},
{
path: '/about',
component: () => import(
/* webpackChunkName: "about" */
'./views/About.vue'
)
},
{
path: '/contact',
component: () => import(
/* webpackChunkName: "contact" */
'./views/Contact.vue'
)
}
];
// 结果:每个路由组件都会生成独立的chunk
// home.chunk.js, about.chunk.js, contact.chunk.js
/**
* 场景2:组件懒加载(React)
*/
import React, { lazy, Suspense } from 'react';
// 懒加载组件
const LazyComponent = lazy(() => import('./LazyComponent'));
function App() {
return (
<div>
<Suspense fallback={<div>Loading...</div>}>
<LazyComponent />
</Suspense>
</div>
);
}
/**
* 场景3:条件加载
*
* 根据条件动态加载不同的模块
*/
async function loadEditor(type) {
if (type === 'rich') {
// 只有需要富文本编辑器时才加载
const module = await import(
/* webpackChunkName: "rich-editor" */
'./editors/RichEditor.js'
);
return module.default;
} else {
// 加载简单编辑器
const module = await import(
/* webpackChunkName: "simple-editor" */
'./editors/SimpleEditor.js'
);
return module.default;
}
}
// 使用
const editor = await loadEditor('rich');
editor.init();
/**
* 场景4:第三方库按需加载
*/
// 不推荐:一次性加载整个lodash库
import _ from 'lodash';
_.debounce(fn, 300);
// 推荐:只加载需要的函数
import debounce from 'lodash/debounce';
debounce(fn, 300);
// 或者使用动态导入
async function useLodash() {
const { default: _ } = await import('lodash');
return _.debounce(fn, 300);
}
/**
* 场景5:提取公共代码
*/
module.exports = {
optimization: {
splitChunks: {
chunks: 'all',
cacheGroups: {
// 提取Vue全家桶
vue: {
test: /[\\/]node_modules[\\/](vue|vue-router|pinia)[\\/]/,
name: 'vue-vendor',
priority: 20
},
// 提取UI库
ui: {
test: /[\\/]node_modules[\\/](element-plus|ant-design-vue)[\\/]/,
name: 'ui-vendor',
priority: 15
},
// 提取其他第三方库
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendor',
priority: 10
},
// 提取公共业务代码
common: {
minChunks: 2,
name: 'common',
priority: 5,
reuseExistingChunk: true
}
}
}
}
};
// 输出结果:
// vue-vendor.js(Vue、Vue Router、Pinia)
// ui-vendor.js(Element Plus或Ant Design Vue)
// vendor.js(其他第三方库)
// common.js(公共业务代码)
// main.js(入口代码)
SplitChunksPlugin详细配置
javascript
/**
* SplitChunksPlugin完整配置说明
*/
module.exports = {
optimization: {
splitChunks: {
// chunks:指定哪些chunk需要优化
// 'all':所有chunk(推荐)
// 'async':只优化异步chunk(默认)
// 'initial':只优化初始chunk
chunks: 'all',
// minSize:生成chunk的最小大小(字节)
// 小于这个大小的chunk不会被分割
minSize: 20000, // 20KB
// minRemainingSize:确保分割后剩余的chunk大小
// Webpack 5新增,避免分割后产生过小的chunk
minRemainingSize: 0,
// minChunks:模块被引用的最少次数
// 只有被引用次数达到这个值的模块才会被分割
minChunks: 1,
// maxAsyncRequests:按需加载时的最大并行请求数
// 超过这个数量的chunk不会被分割
maxAsyncRequests: 30,
// maxInitialRequests:入口点的最大并行请求数
// 超过这个数量的chunk不会被分割
maxInitialRequests: 30,
// enforceSizeThreshold:强制执行分割的大小阈值
// 超过这个大小的chunk会忽略其他限制强制分割
enforceSizeThreshold: 50000, // 50KB
// cacheGroups:缓存组配置
// 可以继承和覆盖splitChunks的所有选项
cacheGroups: {
// 默认的vendor组
defaultVendors: {
test: /[\\/]node_modules[\\/]/,
priority: -10, // 优先级(数字越大优先级越高)
reuseExistingChunk: true // 复用已存在的chunk
},
// 默认的default组
default: {
minChunks: 2,
priority: -20,
reuseExistingChunk: true
},
// 自定义组:提取React相关库
react: {
test: /[\\/]node_modules[\\/](react|react-dom|react-router-dom)[\\/]/,
name: 'react-vendor',
priority: 10,
enforce: true // 忽略minSize、minChunks等限制
},
// 自定义组:提取样式文件
styles: {
test: /\.css$/,
name: 'styles',
type: 'css/mini-extract', // 指定chunk类型
chunks: 'all',
enforce: true
},
// 自定义组:提取公共业务代码
common: {
minChunks: 2,
name: 'common',
priority: 5,
reuseExistingChunk: true,
// 自定义文件名
filename: 'js/[name].[contenthash:8].js'
}
}
}
}
};
懒加载的最佳实践
javascript
/**
* 1. 路由级别的代码分割
*/
// Vue Router配置
const routes = [
{
path: '/',
component: () => import('./views/Home.vue')
},
{
path: '/user',
component: () => import('./views/User.vue'),
children: [
{
path: 'profile',
component: () => import('./views/user/Profile.vue')
},
{
path: 'settings',
component: () => import('./views/user/Settings.vue')
}
]
}
];
/**
* 2. 组件级别的代码分割
*/
// Vue 3异步组件
import { defineAsyncComponent } from 'vue';
const AsyncComponent = defineAsyncComponent(() =>
import('./components/AsyncComponent.vue')
);
// 带加载状态的异步组件
const AsyncComponentWithOptions = defineAsyncComponent({
loader: () => import('./components/HeavyComponent.vue'),
loadingComponent: LoadingComponent, // 加载中显示的组件
errorComponent: ErrorComponent, // 加载失败显示的组件
delay: 200, // 延迟显示loading组件的时间
timeout: 3000 // 超时时间
});
/**
* 3. 功能模块的代码分割
*/
// 图表库按需加载
async function loadChart() {
const { default: echarts } = await import('echarts');
return echarts;
}
// 使用
button.addEventListener('click', async () => {
const echarts = await loadChart();
const chart = echarts.init(document.getElementById('chart'));
chart.setOption(/* ... */);
});
/**
* 4. 预加载和预获取
*/
// prefetch:浏览器空闲时加载,用于未来可能需要的资源
import(
/* webpackPrefetch: true */
'./future-module.js'
);
// 生成:<link rel="prefetch" href="future-module.js">
// preload:与父chunk并行加载,用于当前页面必需的资源
import(
/* webpackPreload: true */
'./critical-module.js'
);
// 生成:<link rel="preload" href="critical-module.js">
/**
* 5. 魔法注释(Magic Comments)
*/
// webpackChunkName:指定chunk名称
import(/* webpackChunkName: "my-chunk" */ './module.js');
// webpackMode:指定加载模式
// 'lazy':默认,懒加载
// 'lazy-once':生成单个chunk,可以满足多个import()
// 'eager':不生成额外chunk,打包到当前chunk
// 'weak':尝试加载模块,如果模块已经加载则使用,否则不加载
import(/* webpackMode: "lazy" */ './module.js');
// webpackPrefetch:预加载
import(/* webpackPrefetch: true */ './module.js');
// webpackPreload:预获取
import(/* webpackPreload: true */ './module.js');
// webpackInclude:包含的文件
import(
/* webpackInclude: /\.json$/ */
`./locale/${language}.json`
);
// webpackExclude:排除的文件
import(
/* webpackExclude: /\.test\.js$/ */
`./components/${name}.js`
);
// 组合使用
import(
/* webpackChunkName: "locale-[request]" */
/* webpackMode: "lazy-once" */
/* webpackInclude: /\.json$/ */
`./locale/${language}.json`
);
代码分割的性能优化
javascript
/**
* 1. 合理设置chunk大小
*/
module.exports = {
optimization: {
splitChunks: {
// 最小chunk大小:20KB
minSize: 20000,
// 最大chunk大小:244KB
// 超过这个大小的chunk会被进一步分割
maxSize: 244000,
// 最大异步chunk大小
maxAsyncSize: 244000,
// 最大初始chunk大小
maxInitialSize: 244000
}
}
};
/**
* 2. 控制并行请求数
*/
module.exports = {
optimization: {
splitChunks: {
// 按需加载时的最大并行请求数
// 过多的并行请求会影响性能
maxAsyncRequests: 6,
// 入口点的最大并行请求数
maxInitialRequests: 4
}
}
};
/**
* 3. 使用runtime chunk
*/
module.exports = {
optimization: {
// 将runtime代码提取到单独的chunk
// runtime包含Webpack的模块加载逻辑
runtimeChunk: {
name: 'runtime'
}
// 或者为每个入口生成独立的runtime
// runtimeChunk: 'single'
}
};
/**
* 4. 优化第三方库的分割
*/
module.exports = {
optimization: {
splitChunks: {
cacheGroups: {
// 将大型库单独分割
lodash: {
test: /[\\/]node_modules[\\/]lodash[\\/]/,
name: 'lodash',
priority: 20
},
// 将经常变化的库单独分割
// 这样其他库的缓存不会因为这个库的更新而失效
frequentlyUpdated: {
test: /[\\/]node_modules[\\/](axios|some-lib)[\\/]/,
name: 'frequently-updated',
priority: 15
},
// 将稳定的库合并
// 这些库很少更新,可以充分利用浏览器缓存
stable: {
test: /[\\/]node_modules[\\/](vue|react)[\\/]/,
name: 'stable-vendor',
priority: 10
}
}
}
}
};
关键点解析:
- 代码分割的目的:减少首屏加载时间,提升用户体验
- 三种分割方式:多入口、动态导入、SplitChunksPlugin
- 动态导入是最灵活的方式:可以实现真正的按需加载
- 合理配置chunk大小:太小会增加请求数,太大会影响加载速度
- 利用浏览器缓存:将稳定的第三方库单独分割,充分利用缓存
开发环境与生产环境配置
Webpack需要针对开发环境和生产环境进行不同的配置。开发环境注重开发体验和调试便利性,生产环境注重性能优化和代码安全。
环境配置的组织方式
javascript
/**
* 方式1:单配置文件 + 环境变量
*/
// webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
// 判断是否为生产环境
const isProduction = process.env.NODE_ENV === 'production';
module.exports = {
mode: isProduction ? 'production' : 'development',
// 开发环境使用eval-source-map,生产环境使用source-map
devtool: isProduction ? 'source-map' : 'eval-source-map',
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
// 生产环境添加contenthash
filename: isProduction
? 'js/[name].[contenthash:8].js'
: 'js/[name].js',
clean: true
},
module: {
rules: [
{
test: /\.css$/,
use: [
// 生产环境提取CSS,开发环境使用style-loader
isProduction
? MiniCssExtractPlugin.loader
: 'style-loader',
'css-loader'
]
}
]
},
plugins: [
new HtmlWebpackPlugin({
template: './src/index.html'
}),
// 生产环境才使用的插件
...(isProduction ? [
new MiniCssExtractPlugin({
filename: 'css/[name].[contenthash:8].css'
})
] : [])
],
// 开发服务器配置
devServer: isProduction ? undefined : {
port: 3000,
hot: true,
open: true
}
};
/**
* 方式2:多配置文件(推荐)
*/
// webpack.common.js - 公共配置
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist')
},
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: 'babel-loader'
},
{
test: /\.(png|jpe?g|gif|svg)$/i,
type: 'asset',
parser: {
dataUrlCondition: {
maxSize: 8 * 1024
}
}
}
]
},
plugins: [
new HtmlWebpackPlugin({
template: './src/index.html'
})
]
};
// webpack.dev.js - 开发环境配置
const { merge } = require('webpack-merge');
const common = require('./webpack.common.js');
module.exports = merge(common, {
mode: 'development',
devtool: 'eval-source-map',
output: {
filename: 'js/[name].js',
chunkFilename: 'js/[name].chunk.js'
},
module: {
rules: [
{
test: /\.css$/,
use: ['style-loader', 'css-loader']
},
{
test: /\.scss$/,
use: ['style-loader', 'css-loader', 'sass-loader']
}
]
},
devServer: {
port: 3000,
hot: true,
open: true,
compress: true,
historyApiFallback: true, // 支持HTML5 History API
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,
pathRewrite: { '^/api': '' }
}
}
}
});
// webpack.prod.js - 生产环境配置
const { merge } = require('webpack-merge');
const common = require('./webpack.common.js');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
const TerserPlugin = require('terser-webpack-plugin');
const CompressionWebpackPlugin = require('compression-webpack-plugin');
module.exports = merge(common, {
mode: 'production',
devtool: 'source-map',
output: {
filename: 'js/[name].[contenthash:8].js',
chunkFilename: 'js/[name].[contenthash:8].chunk.js',
assetModuleFilename: 'assets/[name].[hash:8][ext]',
clean: true
},
module: {
rules: [
{
test: /\.css$/,
use: [
MiniCssExtractPlugin.loader,
'css-loader',
'postcss-loader'
]
},
{
test: /\.scss$/,
use: [
MiniCssExtractPlugin.loader,
'css-loader',
'postcss-loader',
'sass-loader'
]
}
]
},
plugins: [
new MiniCssExtractPlugin({
filename: 'css/[name].[contenthash:8].css',
chunkFilename: 'css/[name].[contenthash:8].chunk.css'
}),
new CompressionWebpackPlugin({
algorithm: 'gzip',
test: /\.(js|css|html|svg)$/,
threshold: 10240,
minRatio: 0.8
})
],
optimization: {
minimizer: [
new TerserPlugin({
parallel: true,
terserOptions: {
compress: {
drop_console: true,
drop_debugger: true
}
}
}),
new CssMinimizerPlugin()
],
splitChunks: {
chunks: 'all',
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendor',
priority: 10
},
common: {
minChunks: 2,
name: 'common',
priority: 5,
reuseExistingChunk: true
}
}
},
runtimeChunk: {
name: 'runtime'
}
},
performance: {
hints: 'warning',
maxEntrypointSize: 512000, // 入口文件最大大小(字节)
maxAssetSize: 512000 // 资源文件最大大小(字节)
}
});
// package.json中的脚本
{
"scripts": {
"dev": "webpack serve --config webpack.dev.js",
"build": "webpack --config webpack.prod.js"
}
}
开发环境优化配置
javascript
/**
* 开发环境配置重点:
* 1. 快速的构建速度
* 2. 良好的调试体验
* 3. 热更新支持
*/
module.exports = {
mode: 'development',
// 1. Source Map配置
// eval-source-map:构建速度快,调试体验好
devtool: 'eval-source-map',
// 2. 缓存配置
cache: {
type: 'filesystem', // 使用文件系统缓存
cacheDirectory: path.resolve(__dirname, '.webpack_cache'),
buildDependencies: {
config: [__filename] // 配置文件改变时重新构建
}
},
// 3. 模块解析优化
resolve: {
// 减少文件扩展名尝试
extensions: ['.js', '.vue', '.json'],
// 配置别名
alias: {
'@': path.resolve(__dirname, 'src'),
'components': path.resolve(__dirname, 'src/components')
},
// 指定模块搜索目录
modules: [path.resolve(__dirname, 'src'), 'node_modules']
},
// 4. 开发服务器配置
devServer: {
port: 3000,
hot: true, // 启用热更新
open: true, // 自动打开浏览器
compress: true, // 启用gzip压缩
// 支持HTML5 History API
historyApiFallback: true,
// 代理配置
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,
pathRewrite: { '^/api': '' }
}
},
// 静态文件目录
static: {
directory: path.join(__dirname, 'public')
},
// 客户端日志级别
client: {
logging: 'info',
overlay: {
errors: true,
warnings: false
},
progress: true
}
},
// 5. 优化配置
optimization: {
// 开发环境不压缩代码
minimize: false,
// 使用可读的模块ID
moduleIds: 'named',
chunkIds: 'named'
},
// 6. 性能提示
performance: {
hints: false // 关闭性能提示
},
// 7. 统计信息
stats: {
colors: true,
modules: false,
children: false,
chunks: false,
chunkModules: false
}
};
生产环境优化配置
javascript
/**
* 生产环境配置重点:
* 1. 代码压缩和优化
* 2. 资源优化和缓存
* 3. 安全性和稳定性
*/
const TerserPlugin = require('terser-webpack-plugin');
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const CompressionWebpackPlugin = require('compression-webpack-plugin');
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');
module.exports = {
mode: 'production',
// 1. Source Map配置
// source-map:完整的source map,用于生产环境调试
devtool: 'source-map',
// 2. 输出配置
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'js/[name].[contenthash:8].js',
chunkFilename: 'js/[name].[contenthash:8].chunk.js',
assetModuleFilename: 'assets/[name].[hash:8][ext]',
clean: true, // 清理输出目录
// CDN配置
publicPath: process.env.CDN_URL || '/'
},
// 3. 优化配置
optimization: {
// 启用压缩
minimize: true,
// 压缩器配置
minimizer: [
// JavaScript压缩
new TerserPlugin({
parallel: true, // 多进程并行压缩
extractComments: false, // 不提取注释到单独文件
terserOptions: {
compress: {
drop_console: true, // 移除console
drop_debugger: true, // 移除debugger
pure_funcs: ['console.log'] // 移除特定函数调用
},
format: {
comments: false // 移除注释
}
}
}),
// CSS压缩
new CssMinimizerPlugin({
parallel: true,
minimizerOptions: {
preset: [
'default',
{
discardComments: { removeAll: true }
}
]
}
})
],
// 代码分割
splitChunks: {
chunks: 'all',
minSize: 20000,
maxSize: 244000,
minChunks: 1,
maxAsyncRequests: 30,
maxInitialRequests: 30,
cacheGroups: {
// Vue全家桶
vue: {
test: /[\\/]node_modules[\\/](vue|vue-router|pinia)[\\/]/,
name: 'vue-vendor',
priority: 20
},
// UI库
ui: {
test: /[\\/]node_modules[\\/](element-plus|ant-design-vue)[\\/]/,
name: 'ui-vendor',
priority: 15
},
// 其他第三方库
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendor',
priority: 10
},
// 公共代码
common: {
minChunks: 2,
name: 'common',
priority: 5,
reuseExistingChunk: true
}
}
},
// 提取runtime代码
runtimeChunk: {
name: 'runtime'
},
// 模块ID生成策略
moduleIds: 'deterministic', // 确定性的模块ID
chunkIds: 'deterministic' // 确定性的chunk ID
},
// 4. 插件配置
plugins: [
// 提取CSS
new MiniCssExtractPlugin({
filename: 'css/[name].[contenthash:8].css',
chunkFilename: 'css/[name].[contenthash:8].chunk.css'
}),
// 生成gzip文件
new CompressionWebpackPlugin({
filename: '[path][base].gz',
algorithm: 'gzip',
test: /\.(js|css|html|svg)$/,
threshold: 10240, // 只压缩大于10KB的文件
minRatio: 0.8, // 压缩比小于0.8才会压缩
deleteOriginalAssets: false // 保留原文件
}),
// 分析bundle大小(可选)
process.env.ANALYZE && new BundleAnalyzerPlugin({
analyzerMode: 'static',
reportFilename: 'bundle-report.html',
openAnalyzer: false
})
].filter(Boolean),
// 5. 性能提示
performance: {
hints: 'warning',
maxEntrypointSize: 512000, // 512KB
maxAssetSize: 512000,
assetFilter: function(assetFilename) {
// 只对JavaScript和CSS文件进行性能提示
return /\.(js|css)$/.test(assetFilename);
}
},
// 6. 统计信息
stats: {
colors: true,
modules: false,
children: false,
chunks: false,
chunkModules: false,
entrypoints: false
}
};
环境变量管理
javascript
/**
* 使用dotenv管理环境变量
*/
// 安装依赖
// npm install dotenv-webpack --save-dev
// .env.development
NODE_ENV=development
API_URL=http://localhost:8080/api
CDN_URL=http://localhost:3000
// .env.production
NODE_ENV=production
API_URL=https://api.example.com
CDN_URL=https://cdn.example.com
// webpack.config.js
const Dotenv = require('dotenv-webpack');
module.exports = {
plugins: [
new Dotenv({
path: `./.env.${process.env.NODE_ENV}`
})
]
};
// 在代码中使用
console.log(process.env.API_URL);
console.log(process.env.CDN_URL);
/**
* 使用DefinePlugin定义全局常量
*/
const webpack = require('webpack');
module.exports = {
plugins: [
new webpack.DefinePlugin({
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV),
'process.env.API_URL': JSON.stringify(process.env.API_URL),
'__DEV__': JSON.stringify(process.env.NODE_ENV === 'development'),
'__PROD__': JSON.stringify(process.env.NODE_ENV === 'production'),
'VERSION': JSON.stringify(require('./package.json').version)
})
]
};
// 在代码中使用
if (__DEV__) {
console.log('开发环境');
}
if (__PROD__) {
console.log('生产环境,版本:', VERSION);
}
fetch(process.env.API_URL + '/users');
关键点解析:
- 开发环境重点:快速构建、良好调试、热更新
- 生产环境重点:代码压缩、资源优化、缓存策略
- 配置组织:使用webpack-merge合并公共配置和环境特定配置
- 环境变量:使用dotenv或DefinePlugin管理不同环境的配置
- Source Map:开发环境使用eval-source-map,生产环境使用source-map
最佳实践
企业级应用场景
场景1:大型单页应用(SPA)优化
javascript
/**
* 问题:大型SPA首屏加载慢,bundle体积过大
*
* 解决方案:
* 1. 路由懒加载
* 2. 组件懒加载
* 3. 第三方库按需引入
* 4. 代码分割和预加载
*/
// 1. 路由懒加载配置
const routes = [
{
path: '/',
component: () => import(
/* webpackChunkName: "home" */
/* webpackPrefetch: true */
'./views/Home.vue'
)
},
{
path: '/dashboard',
component: () => import(
/* webpackChunkName: "dashboard" */
'./views/Dashboard.vue'
),
children: [
{
path: 'analytics',
component: () => import(
/* webpackChunkName: "dashboard-analytics" */
'./views/dashboard/Analytics.vue'
)
},
{
path: 'reports',
component: () => import(
/* webpackChunkName: "dashboard-reports" */
'./views/dashboard/Reports.vue'
)
}
]
}
];
// 2. 组件懒加载
import { defineAsyncComponent } from 'vue';
export default {
components: {
// 懒加载重型组件
HeavyChart: defineAsyncComponent(() =>
import('./components/HeavyChart.vue')
),
// 带加载状态的懒加载
DataTable: defineAsyncComponent({
loader: () => import('./components/DataTable.vue'),
loadingComponent: LoadingSpinner,
delay: 200,
timeout: 3000
})
}
};
// 3. 第三方库按需引入
// 不推荐:全量引入
import ElementPlus from 'element-plus';
import 'element-plus/dist/index.css';
app.use(ElementPlus);
// 推荐:按需引入
import { ElButton, ElInput, ElTable } from 'element-plus';
app.component('ElButton', ElButton);
app.component('ElInput', ElInput);
app.component('ElTable', ElTable);
// 或使用unplugin-vue-components自动按需引入
// vite.config.js
import Components from 'unplugin-vue-components/vite';
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers';
export default {
plugins: [
Components({
resolvers: [ElementPlusResolver()]
})
]
};
// 4. Webpack配置优化
module.exports = {
optimization: {
splitChunks: {
chunks: 'all',
cacheGroups: {
// 提取Vue核心库
vue: {
test: /[\\/]node_modules[\\/](vue|vue-router|pinia)[\\/]/,
name: 'vue-vendor',
priority: 20
},
// 提取UI库
ui: {
test: /[\\/]node_modules[\\/]element-plus[\\/]/,
name: 'ui-vendor',
priority: 15
},
// 提取图表库
charts: {
test: /[\\/]node_modules[\\/](echarts|@antv)[\\/]/,
name: 'charts-vendor',
priority: 12
},
// 提取其他第三方库
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendor',
priority: 10
}
}
},
// 提取runtime代码
runtimeChunk: {
name: 'runtime'
}
}
};
场景2:多页面应用(MPA)配置
javascript
/**
* 问题:多个页面需要独立的入口和HTML文件
*
* 解决方案:
* 1. 配置多个entry
* 2. 为每个页面生成独立的HTML
* 3. 提取公共代码
*/
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const glob = require('glob');
// 自动扫描pages目录下的所有页面
function getEntries() {
const entries = {};
const htmlPlugins = [];
// 扫描src/pages目录
const pages = glob.sync('./src/pages/*/index.js');
pages.forEach(page => {
// 提取页面名称
const pageName = page.match(/pages\/(.+)\/index\.js$/)[1];
// 配置entry
entries[pageName] = page;
// 配置HtmlWebpackPlugin
htmlPlugins.push(
new HtmlWebpackPlugin({
template: `./src/pages/${pageName}/index.html`,
filename: `${pageName}.html`,
chunks: ['runtime', 'vendor', 'common', pageName],
minify: {
removeComments: true,
collapseWhitespace: true
}
})
);
});
return { entries, htmlPlugins };
}
const { entries, htmlPlugins } = getEntries();
module.exports = {
entry: entries,
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'js/[name].[contenthash:8].js',
chunkFilename: 'js/[name].[contenthash:8].chunk.js'
},
plugins: [
...htmlPlugins
],
optimization: {
splitChunks: {
cacheGroups: {
// 提取所有页面共用的第三方库
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendor',
chunks: 'all',
priority: 10
},
// 提取所有页面共用的业务代码
common: {
name: 'common',
minChunks: 2,
chunks: 'all',
priority: 5,
reuseExistingChunk: true
}
}
},
runtimeChunk: {
name: 'runtime'
}
}
};
// 目录结构:
// src/
// pages/
// home/
// index.js
// index.html
// about/
// index.js
// index.html
// contact/
// index.js
// index.html
// 输出结果:
// dist/
// js/
// runtime.xxx.js
// vendor.xxx.js
// common.xxx.js
// home.xxx.js
// about.xxx.js
// contact.xxx.js
// home.html
// about.html
// contact.html
场景3:微前端架构配置
javascript
/**
* 问题:多个子应用需要独立开发、部署,但共享基础库
*
* 解决方案:
* 1. 使用Module Federation
* 2. 配置共享依赖
* 3. 动态加载子应用
*/
// 主应用配置(host)
const { ModuleFederationPlugin } = require('webpack').container;
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'host',
// 引用的远程模块
remotes: {
app1: 'app1@http://localhost:3001/remoteEntry.js',
app2: 'app2@http://localhost:3002/remoteEntry.js'
},
// 共享的依赖
shared: {
vue: {
singleton: true, // 只加载一次
requiredVersion: '^3.0.0'
},
'vue-router': {
singleton: true,
requiredVersion: '^4.0.0'
},
pinia: {
singleton: true,
requiredVersion: '^2.0.0'
}
}
})
]
};
// 子应用配置(remote)
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'app1',
filename: 'remoteEntry.js',
// 暴露的模块
exposes: {
'./App': './src/App.vue',
'./routes': './src/routes.js'
},
// 共享的依赖(与主应用保持一致)
shared: {
vue: {
singleton: true,
requiredVersion: '^3.0.0'
},
'vue-router': {
singleton: true,
requiredVersion: '^4.0.0'
}
}
})
]
};
// 主应用中动态加载子应用
// src/router/index.js
const routes = [
{
path: '/app1',
component: () => import('app1/App') // 动态加载远程模块
},
{
path: '/app2',
component: () => import('app2/App')
}
];
场景4:组件库开发配置
javascript
/**
* 问题:开发可复用的组件库,需要支持多种引入方式
*
* 解决方案:
* 1. 配置library输出
* 2. 支持UMD、ESM、CommonJS
* 3. 外部化依赖
*/
module.exports = {
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'my-component-library.js',
// 库的配置
library: {
name: 'MyComponentLibrary',
type: 'umd', // 支持多种模块规范
export: 'default'
},
// 确保在各种环境下都能正常工作
globalObject: 'this'
},
// 外部化依赖(不打包到库中)
externals: {
vue: {
commonjs: 'vue',
commonjs2: 'vue',
amd: 'vue',
root: 'Vue'
},
'element-plus': {
commonjs: 'element-plus',
commonjs2: 'element-plus',
amd: 'element-plus',
root: 'ElementPlus'
}
},
module: {
rules: [
{
test: /\.vue$/,
use: 'vue-loader'
},
{
test: /\.js$/,
exclude: /node_modules/,
use: 'babel-loader'
},
{
test: /\.css$/,
use: ['style-loader', 'css-loader']
}
]
}
};
// package.json配置
{
"name": "my-component-library",
"version": "1.0.0",
"main": "dist/my-component-library.js", // CommonJS入口
"module": "dist/my-component-library.esm.js", // ESM入口
"unpkg": "dist/my-component-library.min.js", // CDN入口
"peerDependencies": {
"vue": "^3.0.0"
}
}
常见陷阱
陷阱1:循环依赖导致的问题
javascript
/**
* 错误示例:模块A和模块B相互引用
*/
// moduleA.js
import { funcB } from './moduleB.js';
export function funcA() {
console.log('funcA');
funcB();
}
// moduleB.js
import { funcA } from './moduleA.js';
export function funcB() {
console.log('funcB');
funcA(); // 可能导致undefined错误
}
/**
* 正确做法:重构代码,消除循环依赖
*/
// utils.js(提取公共逻辑)
export function commonLogic() {
console.log('common logic');
}
// moduleA.js
import { commonLogic } from './utils.js';
export function funcA() {
console.log('funcA');
commonLogic();
}
// moduleB.js
import { commonLogic } from './utils.js';
export function funcB() {
console.log('funcB');
commonLogic();
}
/**
* 检测循环依赖的插件
*/
const CircularDependencyPlugin = require('circular-dependency-plugin');
module.exports = {
plugins: [
new CircularDependencyPlugin({
exclude: /node_modules/,
failOnError: true, // 发现循环依赖时构建失败
allowAsyncCycles: false,
cwd: process.cwd()
})
]
};
陷阱2:Source Map配置不当
javascript
/**
* 错误示例:生产环境使用eval类型的source map
*/
// ❌ 错误:eval类型的source map会暴露源代码
module.exports = {
mode: 'production',
devtool: 'eval-source-map' // 不安全!
};
/**
* 正确做法:根据环境选择合适的source map
*/
module.exports = {
mode: 'production',
// ✅ 正确:生产环境使用source-map或hidden-source-map
devtool: 'source-map', // 完整的source map,用于调试
// devtool: 'hidden-source-map', // 不在bundle中引用source map
// devtool: false, // 不生成source map
};
/**
* Source Map类型对比
*/
// 开发环境推荐:
// eval-source-map:构建速度快,调试体验好
// eval-cheap-module-source-map:更快的构建速度,但调试体验稍差
// 生产环境推荐:
// source-map:完整的source map,用于调试
// hidden-source-map:不在bundle中引用,需要手动上传到错误监控平台
// nosources-source-map:只包含行列信息,不包含源代码
// false:不生成source map
陷阱3:忽略Tree Shaking的条件
javascript
/**
* 错误示例:使用CommonJS导出,无法Tree Shaking
*/
// utils.js(❌ 错误)
module.exports = {
funcA: function() { /* ... */ },
funcB: function() { /* ... */ },
funcC: function() { /* ... */ }
};
// main.js
const { funcA } = require('./utils.js');
funcA();
// 结果:funcB和funcC也会被打包
/**
* 正确做法:使用ES6模块导出
*/
// utils.js(✅ 正确)
export function funcA() { /* ... */ }
export function funcB() { /* ... */ }
export function funcC() { /* ... */ }
// main.js
import { funcA } from './utils.js';
funcA();
// 结果:只有funcA会被打包,funcB和funcC会被Tree Shaking移除
/**
* Tree Shaking的条件:
* 1. 使用ES6模块语法(import/export)
* 2. 确保package.json中没有"sideEffects": false
* 3. 使用production模式
* 4. 不要使用Babel转译ES6模块为CommonJS
*/
// babel.config.js
module.exports = {
presets: [
['@babel/preset-env', {
modules: false // 不转译ES6模块
}]
]
};
// package.json
{
"sideEffects": [
"*.css", // CSS文件有副作用
"*.scss",
"./src/polyfills.js" // polyfill文件有副作用
]
}
陷阱4:缓存配置不当导致更新失败
javascript
/**
* 错误示例:使用hash而不是contenthash
*/
// ❌ 错误:任何文件改变都会导致所有文件的hash改变
module.exports = {
output: {
filename: '[name].[hash:8].js'
}
};
/**
* 正确做法:使用contenthash
*/
// ✅ 正确:只有文件内容改变时hash才会改变
module.exports = {
output: {
filename: '[name].[contenthash:8].js',
chunkFilename: '[name].[contenthash:8].chunk.js'
},
plugins: [
new MiniCssExtractPlugin({
filename: 'css/[name].[contenthash:8].css'
})
],
optimization: {
// 提取runtime代码,避免vendor hash改变
runtimeChunk: {
name: 'runtime'
},
// 使用确定性的模块ID
moduleIds: 'deterministic'
}
};
/**
* 缓存策略最佳实践
*/
// 1. 文件分类
// - runtime.js:Webpack运行时代码,每次构建都可能改变
// - vendor.js:第三方库,很少改变
// - common.js:公共业务代码,偶尔改变
// - main.js:入口代码,经常改变
// 2. 缓存配置
// nginx配置
location ~* \.(?:css|js)$ {
expires 1y; # 缓存1年
add_header Cache-Control "public, immutable";
}
// HTML文件不缓存
location ~* \.html$ {
expires -1;
add_header Cache-Control "no-cache, no-store, must-revalidate";
}
陷阱5:忽略构建性能优化
javascript
/**
* 错误示例:没有使用缓存和并行处理
*/
// ❌ 错误:每次构建都重新处理所有文件
module.exports = {
module: {
rules: [
{
test: /\.js$/,
use: 'babel-loader'
}
]
}
};
/**
* 正确做法:启用缓存和并行处理
*/
// ✅ 正确:使用缓存和多进程
module.exports = {
// Webpack 5的持久化缓存
cache: {
type: 'filesystem',
cacheDirectory: path.resolve(__dirname, '.webpack_cache')
},
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: [
// 使用thread-loader进行多进程处理
{
loader: 'thread-loader',
options: {
workers: 4 // 工作进程数
}
},
{
loader: 'babel-loader',
options: {
cacheDirectory: true // 启用Babel缓存
}
}
]
}
]
},
optimization: {
minimizer: [
new TerserPlugin({
parallel: true // 多进程压缩
})
]
}
};
/**
* 构建性能优化清单
*/
// 1. 缩小文件搜索范围
module.exports = {
resolve: {
modules: [path.resolve(__dirname, 'src'), 'node_modules'],
extensions: ['.js', '.vue', '.json'], // 减少尝试的扩展名
alias: {
'@': path.resolve(__dirname, 'src')
}
},
module: {
rules: [
{
test: /\.js$/,
include: path.resolve(__dirname, 'src'), // 只处理src目录
exclude: /node_modules/ // 排除node_modules
}
]
}
};
// 2. 使用DllPlugin预编译第三方库(Webpack 4)
// 注意:Webpack 5推荐使用持久化缓存代替DllPlugin
// 3. 使用HardSourceWebpackPlugin(Webpack 4)
// 注意:Webpack 5内置了持久化缓存,不需要此插件
// 4. 分析构建性能
const SpeedMeasurePlugin = require('speed-measure-webpack-plugin');
const smp = new SpeedMeasurePlugin();
module.exports = smp.wrap({
// webpack配置
});
性能优化建议
javascript
/**
* 1. 减小bundle体积
*/
// 使用webpack-bundle-analyzer分析bundle
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');
module.exports = {
plugins: [
new BundleAnalyzerPlugin({
analyzerMode: 'static',
reportFilename: 'bundle-report.html'
})
]
};
// 优化策略:
// - 移除未使用的代码(Tree Shaking)
// - 按需引入第三方库
// - 使用更小的替代库(如day.js代替moment.js)
// - 压缩代码和资源
/**
* 2. 优化加载性能
*/
// 代码分割
module.exports = {
optimization: {
splitChunks: {
chunks: 'all',
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendor',
priority: 10
}
}
}
}
};
// 预加载和预获取
import(/* webpackPrefetch: true */ './future-module.js');
import(/* webpackPreload: true */ './critical-module.js');
// 资源压缩
const CompressionWebpackPlugin = require('compression-webpack-plugin');
module.exports = {
plugins: [
new CompressionWebpackPlugin({
algorithm: 'gzip',
test: /\.(js|css|html)$/,
threshold: 10240
})
]
};
/**
* 3. 优化构建性能
*/
// 使用持久化缓存(Webpack 5)
module.exports = {
cache: {
type: 'filesystem'
}
};
// 使用多进程
module.exports = {
module: {
rules: [
{
test: /\.js$/,
use: ['thread-loader', 'babel-loader']
}
]
}
};
// 减小搜索范围
module.exports = {
resolve: {
modules: [path.resolve(__dirname, 'src'), 'node_modules'],
extensions: ['.js', '.vue']
},
module: {
rules: [
{
test: /\.js$/,
include: path.resolve(__dirname, 'src'),
exclude: /node_modules/
}
]
}
};
关键点解析:
- 企业级应用:需要考虑代码分割、缓存策略、构建性能
- 常见陷阱:循环依赖、Source Map配置、Tree Shaking条件、缓存策略
- 性能优化:减小bundle体积、优化加载性能、优化构建性能
- 最佳实践:根据项目类型选择合适的配置策略
Webpack与Vite的对比
理解Webpack和Vite的区别,有助于在实际项目中选择合适的构建工具。
核心差异
javascript
/**
* 1. 开发服务器启动方式
*/
// Webpack的方式:
// 1. 打包所有模块
// 2. 启动开发服务器
// 3. 提供打包后的bundle
// Vite的方式:
// 1. 直接启动开发服务器
// 2. 按需编译模块
// 3. 利用浏览器原生ESM
/**
* 2. 热更新(HMR)机制
*/
// Webpack HMR:
// 1. 文件改变
// 2. 重新打包相关模块
// 3. 通过WebSocket推送更新
// 4. 浏览器应用更新
// Vite HMR:
// 1. 文件改变
// 2. 只编译改变的模块
// 3. 通过WebSocket推送更新
// 4. 浏览器重新请求该模块
/**
* 3. 生产构建
*/
// Webpack:
// 使用自己的打包逻辑
// Vite:
// 使用Rollup进行打包
详细对比表
┌─────────────────┬──────────────────────────┬──────────────────────────┐
│ 特性 │ Webpack │ Vite │
├─────────────────┼──────────────────────────┼──────────────────────────┤
│ 启动速度 │ 慢(需要打包所有模块) │ 快(按需编译) │
├─────────────────┼──────────────────────────┼──────────────────────────┤
│ 热更新速度 │ 较慢(重新打包相关模块) │ 快(只编译改变的模块) │
├─────────────────┼──────────────────────────┼──────────────────────────┤
│ 生产构建 │ 使用Webpack打包 │ 使用Rollup打包 │
├─────────────────┼──────────────────────────┼──────────────────────────┤
│ 配置复杂度 │ 高(需要配置loader等) │ 低(开箱即用) │
├─────────────────┼──────────────────────────┼──────────────────────────┤
│ 生态系统 │ 成熟(大量loader/plugin)│ 快速发展中 │
├─────────────────┼──────────────────────────┼──────────────────────────┤
│ 浏览器兼容性 │ 好(可配置目标浏览器) │ 开发环境需要现代浏览器 │
├─────────────────┼──────────────────────────┼──────────────────────────┤
│ 学习曲线 │ 陡峭 │ 平缓 │
├─────────────────┼──────────────────────────┼──────────────────────────┤
│ 适用场景 │ 大型复杂项目、需要高度 │ 中小型项目、快速开发 │
│ │ 定制化的项目 │ │
└─────────────────┴──────────────────────────┴──────────────────────────┘
性能对比
javascript
/**
* 开发环境性能对比(以中型Vue项目为例)
*/
// Webpack:
// - 首次启动:30-60秒
// - 热更新:1-3秒
// - 内存占用:较高
// Vite:
// - 首次启动:1-3秒
// - 热更新:<100ms
// - 内存占用:较低
/**
* 生产构建性能对比
*/
// Webpack:
// - 构建时间:取决于项目大小和配置
// - 输出体积:取决于优化配置
// - Tree Shaking:支持
// Vite:
// - 构建时间:通常比Webpack快
// - 输出体积:通常比Webpack小
// - Tree Shaking:默认开启
选择建议
javascript
/**
* 选择Webpack的场景:
*/
// 1. 大型企业级项目
// - 需要高度定制化的构建流程
// - 有复杂的构建需求
// - 需要支持旧版浏览器
// 2. 现有项目迁移成本高
// - 已经有成熟的Webpack配置
// - 使用了大量Webpack特定的loader和plugin
// - 团队对Webpack非常熟悉
// 3. 需要特定的构建功能
// - Module Federation(微前端)
// - 复杂的代码分割策略
// - 特定的loader处理
/**
* 选择Vite的场景:
*/
// 1. 新项目
// - 快速开发原型
// - 中小型项目
// - 追求开发体验
// 2. 现代浏览器项目
// - 不需要支持IE等旧版浏览器
// - 可以使用ES6+特性
// - 追求快速的开发反馈
// 3. Vue 3项目
// - Vite是Vue团队推荐的构建工具
// - 与Vue 3生态集成更好
// - 开箱即用的Vue支持
/**
* 混合使用的场景:
*/
// 开发环境使用Vite,生产环境使用Webpack
// - 享受Vite的快速开发体验
// - 利用Webpack成熟的生产构建能力
// - 需要额外的配置和维护成本
从Webpack迁移到Vite
javascript
/**
* 迁移步骤:
*/
// 1. 安装Vite和相关插件
// npm install vite @vitejs/plugin-vue --save-dev
// 2. 创建vite.config.js
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import path from 'path';
export default defineConfig({
plugins: [vue()],
resolve: {
alias: {
'@': path.resolve(__dirname, 'src')
}
},
server: {
port: 3000,
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true
}
}
},
build: {
outDir: 'dist',
assetsDir: 'assets',
sourcemap: true
}
});
// 3. 修改index.html
// Webpack:
// <script src="/dist/main.js"></script>
// Vite:
// <script type="module" src="/src/main.js"></script>
// 4. 修改环境变量
// Webpack:process.env.VUE_APP_API_URL
// Vite:import.meta.env.VITE_API_URL
// 5. 修改静态资源引用
// Webpack:require('@/assets/logo.png')
// Vite:new URL('@/assets/logo.png', import.meta.url).href
// 6. 修改package.json脚本
{
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
}
}
/**
* 常见问题和解决方案:
*/
// 问题1:require不可用
// 解决:使用import代替require
// 问题2:全局变量不可用
// 解决:使用import.meta.env
// 问题3:动态require不可用
// 解决:使用import.meta.glob
// Webpack:
const modules = require.context('./modules', true, /\.js$/);
// Vite:
const modules = import.meta.glob('./modules/**/*.js');
// 问题4:某些Webpack特定的loader不可用
// 解决:寻找Vite插件替代,或使用vite-plugin-webpack
关键点解析:
- 核心差异:Webpack打包所有模块,Vite按需编译
- 性能对比:Vite开发环境更快,生产构建各有优势
- 选择建议:根据项目规模、团队经验、浏览器兼容性需求选择
- 迁移成本:需要修改配置、环境变量、静态资源引用等
实践练习
练习1:配置Vue 3项目的Webpack构建(难度:中等)
需求描述
从零开始配置一个Vue 3项目的Webpack构建环境,实现以下功能:
- 支持Vue 3单文件组件(.vue文件)
- 支持TypeScript
- 支持Sass/SCSS样式
- 配置开发服务器,支持热更新
- 配置生产环境构建,包括代码压缩、CSS提取、资源优化
- 实现路由懒加载
- 配置环境变量管理
实现提示
- 需要安装的依赖:webpack、webpack-cli、webpack-dev-server、vue-loader、ts-loader、sass-loader等
- 创建webpack.common.js、webpack.dev.js、webpack.prod.js三个配置文件
- 使用webpack-merge合并配置
- 配置HtmlWebpackPlugin生成HTML文件
- 配置MiniCssExtractPlugin提取CSS
- 使用DefinePlugin管理环境变量
参考答案
javascript
// webpack.common.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { VueLoaderPlugin } = require('vue-loader');
module.exports = {
entry: './src/main.ts',
output: {
path: path.resolve(__dirname, 'dist'),
clean: true
},
resolve: {
extensions: ['.ts', '.js', '.vue', '.json'],
alias: {
'@': path.resolve(__dirname, 'src')
}
},
module: {
rules: [
{
test: /\.vue$/,
use: 'vue-loader'
},
{
test: /\.ts$/,
exclude: /node_modules/,
use: {
loader: 'ts-loader',
options: {
appendTsSuffixTo: [/\.vue$/] // 支持.vue文件中的TypeScript
}
}
},
{
test: /\.(png|jpe?g|gif|svg)$/i,
type: 'asset',
parser: {
dataUrlCondition: {
maxSize: 8 * 1024
}
},
generator: {
filename: 'images/[name].[hash:8][ext]'
}
},
{
test: /\.(woff|woff2|eot|ttf|otf)$/i,
type: 'asset/resource',
generator: {
filename: 'fonts/[name].[hash:8][ext]'
}
}
]
},
plugins: [
new VueLoaderPlugin(),
new HtmlWebpackPlugin({
template: './public/index.html',
favicon: './public/favicon.ico'
})
]
};
// webpack.dev.js
const { merge } = require('webpack-merge');
const common = require('./webpack.common.js');
const webpack = require('webpack');
module.exports = merge(common, {
mode: 'development',
devtool: 'eval-source-map',
output: {
filename: 'js/[name].js',
chunkFilename: 'js/[name].chunk.js'
},
module: {
rules: [
{
test: /\.css$/,
use: ['style-loader', 'css-loader']
},
{
test: /\.scss$/,
use: ['style-loader', 'css-loader', 'sass-loader']
}
]
},
plugins: [
new webpack.DefinePlugin({
__VUE_OPTIONS_API__: JSON.stringify(true),
__VUE_PROD_DEVTOOLS__: JSON.stringify(false),
'process.env.NODE_ENV': JSON.stringify('development'),
'process.env.VUE_APP_API_URL': JSON.stringify('http://localhost:8080/api')
})
],
devServer: {
port: 3000,
hot: true,
open: true,
compress: true,
historyApiFallback: true,
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,
pathRewrite: { '^/api': '' }
}
}
}
});
// webpack.prod.js
const { merge } = require('webpack-merge');
const common = require('./webpack.common.js');
const webpack = require('webpack');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
const TerserPlugin = require('terser-webpack-plugin');
module.exports = merge(common, {
mode: 'production',
devtool: 'source-map',
output: {
filename: 'js/[name].[contenthash:8].js',
chunkFilename: 'js/[name].[contenthash:8].chunk.js',
assetModuleFilename: 'assets/[name].[hash:8][ext]'
},
module: {
rules: [
{
test: /\.css$/,
use: [
MiniCssExtractPlugin.loader,
'css-loader',
'postcss-loader'
]
},
{
test: /\.scss$/,
use: [
MiniCssExtractPlugin.loader,
'css-loader',
'postcss-loader',
'sass-loader'
]
}
]
},
plugins: [
new webpack.DefinePlugin({
__VUE_OPTIONS_API__: JSON.stringify(true),
__VUE_PROD_DEVTOOLS__: JSON.stringify(false),
'process.env.NODE_ENV': JSON.stringify('production'),
'process.env.VUE_APP_API_URL': JSON.stringify('https://api.example.com')
}),
new MiniCssExtractPlugin({
filename: 'css/[name].[contenthash:8].css',
chunkFilename: 'css/[name].[contenthash:8].chunk.css'
})
],
optimization: {
minimizer: [
new TerserPlugin({
parallel: true,
terserOptions: {
compress: {
drop_console: true,
drop_debugger: true
}
}
}),
new CssMinimizerPlugin()
],
splitChunks: {
chunks: 'all',
cacheGroups: {
vue: {
test: /[\\/]node_modules[\\/](vue|vue-router|pinia)[\\/]/,
name: 'vue-vendor',
priority: 20
},
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendor',
priority: 10
},
common: {
minChunks: 2,
name: 'common',
priority: 5,
reuseExistingChunk: true
}
}
},
runtimeChunk: {
name: 'runtime'
},
moduleIds: 'deterministic'
},
performance: {
hints: 'warning',
maxEntrypointSize: 512000,
maxAssetSize: 512000
}
});
// package.json
{
"scripts": {
"dev": "webpack serve --config webpack.dev.js",
"build": "webpack --config webpack.prod.js"
},
"devDependencies": {
"@babel/core": "^7.20.0",
"@babel/preset-env": "^7.20.0",
"babel-loader": "^9.1.0",
"css-loader": "^6.7.0",
"css-minimizer-webpack-plugin": "^4.2.0",
"html-webpack-plugin": "^5.5.0",
"mini-css-extract-plugin": "^2.7.0",
"postcss": "^8.4.0",
"postcss-loader": "^7.0.0",
"autoprefixer": "^10.4.0",
"sass": "^1.57.0",
"sass-loader": "^13.2.0",
"style-loader": "^3.3.0",
"terser-webpack-plugin": "^5.3.0",
"ts-loader": "^9.4.0",
"typescript": "^4.9.0",
"vue-loader": "^17.0.0",
"webpack": "^5.75.0",
"webpack-cli": "^5.0.0",
"webpack-dev-server": "^4.11.0",
"webpack-merge": "^5.8.0"
},
"dependencies": {
"vue": "^3.2.0",
"vue-router": "^4.1.0",
"pinia": "^2.0.0"
}
}
// tsconfig.json
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "node",
"strict": true,
"jsx": "preserve",
"sourceMap": true,
"resolveJsonModule": true,
"esModuleInterop": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"skipLibCheck": true,
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
},
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"]
}
// postcss.config.js
module.exports = {
plugins: [
require('autoprefixer')
]
};
// src/router/index.ts(路由懒加载示例)
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router';
const routes: RouteRecordRaw[] = [
{
path: '/',
name: 'Home',
component: () => import(
/* webpackChunkName: "home" */
'@/views/Home.vue'
)
},
{
path: '/about',
name: 'About',
component: () => import(
/* webpackChunkName: "about" */
'@/views/About.vue'
)
}
];
const router = createRouter({
history: createWebHistory(),
routes
});
export default router;
答案解析
-
配置文件组织:
- webpack.common.js:公共配置,包含entry、output、resolve、基础loader和plugin
- webpack.dev.js:开发环境配置,使用style-loader、配置devServer
- webpack.prod.js:生产环境配置,使用MiniCssExtractPlugin、配置代码压缩和分割
-
Vue 3支持:
- 使用vue-loader处理.vue文件
- 使用VueLoaderPlugin
- 使用DefinePlugin定义Vue特性标志
-
TypeScript支持:
- 使用ts-loader处理.ts文件
- 配置appendTsSuffixTo支持.vue文件中的TypeScript
- 创建tsconfig.json配置TypeScript编译选项
-
样式处理:
- 开发环境:style-loader将CSS注入到DOM
- 生产环境:MiniCssExtractPlugin提取CSS到单独文件
- 使用sass-loader处理Sass/SCSS
- 使用postcss-loader添加浏览器前缀
-
代码分割:
- 将Vue全家桶提取到vue-vendor chunk
- 将其他第三方库提取到vendor chunk
- 将公共业务代码提取到common chunk
- 提取runtime代码到独立chunk
-
路由懒加载:
- 使用动态import()语法
- 使用webpackChunkName魔法注释指定chunk名称
-
环境变量:
- 使用DefinePlugin定义全局常量
- 在代码中通过process.env访问
练习2:实现自定义Webpack Plugin(难度:困难)
需求描述
开发一个Webpack插件,实现以下功能:
- 在构建完成后,生成一个build-info.json文件
- 文件内容包含:构建时间、构建耗时、输出文件列表、文件大小统计
- 在控制台输出构建统计信息
- 支持配置选项(是否生成文件、输出路径等)
实现提示
- Plugin需要实现apply方法
- 使用compiler.hooks.done监听构建完成事件
- 使用compilation.assets获取输出文件信息
- 使用fs模块写入文件
参考答案
javascript
// build-info-plugin.js
const fs = require('fs');
const path = require('path');
class BuildInfoPlugin {
constructor(options = {}) {
// 默认选项
this.options = {
filename: 'build-info.json', // 输出文件名
outputPath: '', // 输出路径(相对于output.path)
generateFile: true, // 是否生成文件
logToConsole: true, // 是否输出到控制台
...options
};
this.startTime = null;
}
apply(compiler) {
// 监听编译开始事件
compiler.hooks.compile.tap('BuildInfoPlugin', () => {
this.startTime = Date.now();
});
// 监听构建完成事件
compiler.hooks.done.tapAsync('BuildInfoPlugin', (stats, callback) => {
const endTime = Date.now();
const buildTime = endTime - this.startTime;
// 获取构建信息
const buildInfo = this.getBuildInfo(stats, buildTime);
// 输出到控制台
if (this.options.logToConsole) {
this.logToConsole(buildInfo);
}
// 生成文件
if (this.options.generateFile) {
this.generateFile(compiler, buildInfo);
}
callback();
});
}
getBuildInfo(stats, buildTime) {
const compilation = stats.compilation;
const assets = compilation.assets;
// 收集文件信息
const files = [];
let totalSize = 0;
for (const filename in assets) {
const asset = assets[filename];
const size = asset.size();
files.push({
name: filename,
size: size,
sizeFormatted: this.formatSize(size)
});
totalSize += size;
}
// 按大小排序
files.sort((a, b) => b.size - a.size);
// 构建信息
return {
buildTime: new Date().toISOString(),
buildDuration: buildTime,
buildDurationFormatted: this.formatDuration(buildTime),
totalFiles: files.length,
totalSize: totalSize,
totalSizeFormatted: this.formatSize(totalSize),
files: files,
errors: stats.compilation.errors.length,
warnings: stats.compilation.warnings.length
};
}
logToConsole(buildInfo) {
console.log('\n========== 构建信息 ==========');
console.log(`构建时间: ${buildInfo.buildTime}`);
console.log(`构建耗时: ${buildInfo.buildDurationFormatted}`);
console.log(`文件数量: ${buildInfo.totalFiles}`);
console.log(`总大小: ${buildInfo.totalSizeFormatted}`);
console.log(`错误: ${buildInfo.errors}`);
console.log(`警告: ${buildInfo.warnings}`);
console.log('\n文件列表(按大小排序):');
buildInfo.files.slice(0, 10).forEach((file, index) => {
console.log(`${index + 1}. ${file.name} (${file.sizeFormatted})`);
});
if (buildInfo.files.length > 10) {
console.log(`... 还有 ${buildInfo.files.length - 10} 个文件`);
}
console.log('==============================\n');
}
generateFile(compiler, buildInfo) {
const outputPath = compiler.options.output.path;
const filePath = path.join(
outputPath,
this.options.outputPath,
this.options.filename
);
// 确保目录存在
const dir = path.dirname(filePath);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
// 写入文件
fs.writeFileSync(
filePath,
JSON.stringify(buildInfo, null, 2),
'utf-8'
);
console.log(`构建信息已保存到: ${filePath}`);
}
formatSize(bytes) {
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
formatDuration(ms) {
if (ms < 1000) return ms + 'ms';
const seconds = Math.floor(ms / 1000);
const minutes = Math.floor(seconds / 60);
if (minutes > 0) {
return `${minutes}m ${seconds % 60}s`;
}
return `${seconds}s`;
}
}
module.exports = BuildInfoPlugin;
// 使用示例
// webpack.config.js
const BuildInfoPlugin = require('./build-info-plugin.js');
module.exports = {
// ... 其他配置
plugins: [
new BuildInfoPlugin({
filename: 'build-info.json',
outputPath: 'meta',
generateFile: true,
logToConsole: true
})
]
};
// 输出示例
// build-info.json
{
"buildTime": "2024-01-15T10:30:45.123Z",
"buildDuration": 12345,
"buildDurationFormatted": "12s",
"totalFiles": 15,
"totalSize": 1234567,
"totalSizeFormatted": "1.18 MB",
"files": [
{
"name": "js/main.a1b2c3d4.js",
"size": 456789,
"sizeFormatted": "446.08 KB"
},
{
"name": "js/vendor.e5f6g7h8.js",
"size": 345678,
"sizeFormatted": "337.58 KB"
}
],
"errors": 0,
"warnings": 2
}
答案解析
-
Plugin结构:
- 构造函数:接收配置选项
- apply方法:接收compiler对象,注册钩子
-
生命周期钩子:
- compile钩子:记录构建开始时间
- done钩子:构建完成后收集信息
-
信息收集:
- 从stats.compilation.assets获取输出文件
- 计算文件大小和总大小
- 统计错误和警告数量
-
格式化输出:
- formatSize:将字节转换为可读格式(B、KB、MB、GB)
- formatDuration:将毫秒转换为可读格式(ms、s、m)
-
文件生成:
- 使用fs.writeFileSync写入JSON文件
- 确保输出目录存在
-
可配置性:
- 支持自定义文件名和输出路径
- 支持开关文件生成和控制台输出
扩展挑战:
- 添加对比功能:与上次构建对比,显示文件大小变化
- 添加警告功能:当文件大小超过阈值时发出警告
- 添加图表生成:生成HTML格式的可视化报告
- 添加Git信息:包含当前分支、commit hash等信息
高级主题深入
Webpack模块联邦(Module Federation)
javascript
/**
* ========================================
* 模块联邦:微前端架构的核心技术
* ========================================
*
* 模块联邦允许多个独立的Webpack构建共享代码,
* 实现真正的运行时代码共享,而不是构建时的代码复制。
*/
/**
* 场景:电商平台的微前端架构
*
* 主应用(Host):商城首页
* 远程应用1(Remote):商品模块
* 远程应用2(Remote):购物车模块
* 远程应用3(Remote):用户中心模块
*/
// 主应用配置(host-app)
// webpack.config.js
const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin');
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'host', // 应用名称
// 引用远程模块
remotes: {
productApp: 'productApp@http://localhost:3001/remoteEntry.js',
cartApp: 'cartApp@http://localhost:3002/remoteEntry.js',
userApp: 'userApp@http://localhost:3003/remoteEntry.js'
},
// 共享依赖
shared: {
vue: {
singleton: true, // 只加载一次
requiredVersion: '^3.2.0'
},
'vue-router': {
singleton: true,
requiredVersion: '^4.0.0'
},
pinia: {
singleton: true,
requiredVersion: '^2.0.0'
}
}
})
]
};
// 主应用使用远程模块
// src/router/index.js
import { createRouter, createWebHistory } from 'vue-router';
const routes = [
{
path: '/',
name: 'Home',
component: () => import('./views/Home.vue')
},
{
path: '/products',
name: 'Products',
// 动态加载远程模块
component: () => import('productApp/ProductList')
},
{
path: '/cart',
name: 'Cart',
component: () => import('cartApp/Cart')
},
{
path: '/user',
name: 'User',
component: () => import('userApp/Profile')
}
];
const router = createRouter({
history: createWebHistory(),
routes
});
export default router;
// 远程应用配置(product-app)
// webpack.config.js
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'productApp',
filename: 'remoteEntry.js',
// 暴露模块
exposes: {
'./ProductList': './src/components/ProductList.vue',
'./ProductDetail': './src/components/ProductDetail.vue',
'./ProductSearch': './src/components/ProductSearch.vue'
},
// 共享依赖
shared: {
vue: {
singleton: true,
requiredVersion: '^3.2.0'
},
'vue-router': {
singleton: true,
requiredVersion: '^4.0.0'
}
}
})
]
};
/**
* 动态远程模块加载
*/
// 动态加载远程应用
async function loadRemoteModule(remoteUrl, moduleName) {
// 加载远程入口文件
await loadScript(remoteUrl);
// 获取远程容器
const container = window[moduleName];
// 初始化容器
await container.init(__webpack_share_scopes__.default);
// 获取模块工厂
const factory = await container.get(moduleName);
// 执行模块
const module = factory();
return module;
}
function loadScript(url) {
return new Promise((resolve, reject) => {
const script = document.createElement('script');
script.src = url;
script.onload = resolve;
script.onerror = reject;
document.head.appendChild(script);
});
}
// 使用
const ProductList = await loadRemoteModule(
'http://localhost:3001/remoteEntry.js',
'productApp/ProductList'
);
/**
* 版本管理和降级策略
*/
// 带版本控制的模块联邦配置
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'host',
remotes: {
// 使用版本号
productApp: 'productApp@http://localhost:3001/v1.2.3/remoteEntry.js'
},
shared: {
vue: {
singleton: true,
requiredVersion: '^3.2.0',
// 版本不匹配时的策略
strictVersion: false, // 允许版本不匹配
shareScope: 'default'
}
}
})
]
};
// 降级策略:远程模块加载失败时使用本地模块
async function loadModuleWithFallback(remotePath, localPath) {
try {
// 尝试加载远程模块
return await import(remotePath);
} catch (error) {
console.warn(`远程模块加载失败,使用本地模块: ${error.message}`);
// 降级到本地模块
return await import(localPath);
}
}
// 使用
const ProductList = await loadModuleWithFallback(
'productApp/ProductList',
'./components/ProductListFallback.vue'
);
/**
* 共享状态管理
*/
// 主应用:创建共享store
// src/store/index.js
import { createPinia } from 'pinia';
const pinia = createPinia();
// 暴露给远程应用
window.__SHARED_STORE__ = pinia;
export default pinia;
// 远程应用:使用共享store
// product-app/src/store/product.js
import { defineStore } from 'pinia';
// 使用主应用的pinia实例
const pinia = window.__SHARED_STORE__;
export const useProductStore = defineStore('product', {
state: () => ({
products: [],
selectedProduct: null
}),
actions: {
async fetchProducts() {
const response = await fetch('/api/products');
this.products = await response.json();
}
}
}, { pinia });
/**
* 模块联邦最佳实践
*/
// 1. 使用TypeScript类型共享
// shared-types/index.d.ts
export interface Product {
id: string;
name: string;
price: number;
image: string;
}
export interface User {
id: string;
name: string;
email: string;
}
// 2. 统一的错误处理
class RemoteModuleError extends Error {
constructor(moduleName, originalError) {
super(`Failed to load remote module: ${moduleName}`);
this.moduleName = moduleName;
this.originalError = originalError;
}
}
async function safeLoadRemoteModule(moduleName) {
try {
return await import(moduleName);
} catch (error) {
throw new RemoteModuleError(moduleName, error);
}
}
// 3. 性能监控
function monitorRemoteModuleLoad(moduleName) {
const startTime = performance.now();
return import(moduleName).then(module => {
const loadTime = performance.now() - startTime;
console.log(`Remote module ${moduleName} loaded in ${loadTime}ms`);
// 发送到监控服务
sendMetric('remote_module_load', {
module: moduleName,
loadTime
});
return module;
});
}
// 4. 预加载远程模块
function preloadRemoteModules(moduleNames) {
moduleNames.forEach(moduleName => {
const link = document.createElement('link');
link.rel = 'prefetch';
link.href = getRemoteModuleUrl(moduleName);
document.head.appendChild(link);
});
}
// 在应用启动时预加载
preloadRemoteModules([
'productApp/ProductList',
'cartApp/Cart'
]);
Webpack构建性能优化深度实践
javascript
/**
* ========================================
* 构建性能优化:从10分钟到1分钟
* ========================================
*/
/**
* 1. 持久化缓存(Webpack 5)
*/
// webpack.config.js
module.exports = {
cache: {
type: 'filesystem', // 使用文件系统缓存
// 缓存目录
cacheDirectory: path.resolve(__dirname, '.webpack_cache'),
// 缓存名称(用于区分不同配置)
name: 'production-cache',
// 缓存版本(修改后会使缓存失效)
version: '1.0.0',
// 构建依赖
buildDependencies: {
config: [__filename], // 配置文件变化时使缓存失效
tsconfig: [path.resolve(__dirname, 'tsconfig.json')]
},
// 缓存策略
store: 'pack', // 打包存储,减少文件数量
// 压缩缓存
compression: 'gzip',
// 缓存大小限制
maxMemoryGenerations: 5,
maxAge: 1000 * 60 * 60 * 24 * 7 // 7天
}
};
/**
* 2. 多进程构建
*/
// 使用thread-loader
module.exports = {
module: {
rules: [
{
test: /\.js$/,
use: [
{
loader: 'thread-loader',
options: {
workers: require('os').cpus().length - 1, // 使用CPU核心数-1
workerParallelJobs: 50,
poolTimeout: 2000
}
},
'babel-loader'
]
},
{
test: /\.ts$/,
use: [
'thread-loader',
{
loader: 'ts-loader',
options: {
happyPackMode: true, // 配合thread-loader使用
transpileOnly: true // 只转译,不类型检查(类型检查用fork-ts-checker-webpack-plugin)
}
}
]
}
]
},
plugins: [
// 在单独进程中进行TypeScript类型检查
new ForkTsCheckerWebpackPlugin({
async: true, // 异步检查
typescript: {
configFile: path.resolve(__dirname, 'tsconfig.json'),
memoryLimit: 4096
}
})
]
};
/**
* 3. 优化resolve配置
*/
module.exports = {
resolve: {
// 减少解析的文件扩展名
extensions: ['.js', '.ts', '.vue', '.json'], // 只保留必要的
// 使用别名减少查找
alias: {
'@': path.resolve(__dirname, 'src'),
'components': path.resolve(__dirname, 'src/components'),
'utils': path.resolve(__dirname, 'src/utils')
},
// 限制模块查找范围
modules: [
path.resolve(__dirname, 'src'),
'node_modules'
],
// 优化symlinks处理
symlinks: false,
// 缓存模块解析结果
cache: true,
// 优化package.json的main字段查找
mainFields: ['browser', 'module', 'main'],
// 优化目录索引文件查找
mainFiles: ['index']
},
resolveLoader: {
// 限制loader查找范围
modules: ['node_modules']
}
};
/**
* 4. 优化模块解析
*/
module.exports = {
module: {
// 不解析的模块(已经打包好的库)
noParse: /jquery|lodash/,
rules: [
{
test: /\.js$/,
// 排除不需要处理的目录
exclude: /node_modules/,
// 或者只包含需要处理的目录
include: path.resolve(__dirname, 'src'),
use: 'babel-loader'
}
]
}
};
/**
* 5. DLL动态链接库
*/
// webpack.dll.config.js
const webpack = require('webpack');
const path = require('path');
module.exports = {
mode: 'production',
entry: {
vendor: ['vue', 'vue-router', 'pinia', 'axios', 'lodash-es']
},
output: {
path: path.resolve(__dirname, 'dll'),
filename: '[name].dll.js',
library: '[name]_library'
},
plugins: [
new webpack.DllPlugin({
name: '[name]_library',
path: path.resolve(__dirname, 'dll/[name]-manifest.json')
})
]
};
// webpack.config.js
module.exports = {
plugins: [
new webpack.DllReferencePlugin({
manifest: require('./dll/vendor-manifest.json')
})
]
};
// package.json
{
"scripts": {
"dll": "webpack --config webpack.dll.config.js",
"build": "npm run dll && webpack --config webpack.config.js"
}
}
/**
* 6. 构建分析和监控
*/
// 使用speed-measure-webpack-plugin分析构建速度
const SpeedMeasurePlugin = require('speed-measure-webpack-plugin');
const smp = new SpeedMeasurePlugin();
module.exports = smp.wrap({
// webpack配置
});
// 输出示例:
/*
SMP ⏱
General output time took 45.23 secs
SMP ⏱ Plugins
TerserPlugin took 12.34 secs
MiniCssExtractPlugin took 5.67 secs
HtmlWebpackPlugin took 2.34 secs
SMP ⏱ Loaders
babel-loader took 15.67 secs
module count = 234
ts-loader took 8.90 secs
module count = 156
*/
// 使用webpack-bundle-analyzer分析包大小
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
module.exports = {
plugins: [
new BundleAnalyzerPlugin({
analyzerMode: 'static',
reportFilename: 'bundle-report.html',
openAnalyzer: false,
generateStatsFile: true,
statsFilename: 'bundle-stats.json'
})
]
};
/**
* 7. 增量构建优化
*/
module.exports = {
// 开发环境使用eval-source-map(最快)
devtool: process.env.NODE_ENV === 'development'
? 'eval-source-map'
: 'source-map',
// 监听模式优化
watchOptions: {
// 忽略node_modules
ignored: /node_modules/,
// 聚合多个更改到一次重新构建
aggregateTimeout: 300,
// 轮询间隔
poll: 1000
},
// 开发服务器优化
devServer: {
// 启用热更新
hot: true,
// 只编译修改的模块
liveReload: false,
// 减少日志输出
client: {
logging: 'warn'
}
}
};
/**
* 8. 生产构建优化
*/
module.exports = {
optimization: {
// 使用确定性的模块ID
moduleIds: 'deterministic',
// 使用确定性的chunk ID
chunkIds: 'deterministic',
// 提取runtime代码
runtimeChunk: {
name: 'runtime'
},
// 代码分割
splitChunks: {
chunks: 'all',
minSize: 20000,
minRemainingSize: 0,
minChunks: 1,
maxAsyncRequests: 30,
maxInitialRequests: 30,
enforceSizeThreshold: 50000,
cacheGroups: {
// 框架代码
framework: {
test: /[\\/]node_modules[\\/](vue|vue-router|pinia)[\\/]/,
name: 'framework',
priority: 40,
reuseExistingChunk: true
},
// UI库
ui: {
test: /[\\/]node_modules[\\/](element-plus|ant-design-vue)[\\/]/,
name: 'ui',
priority: 30,
reuseExistingChunk: true
},
// 工具库
utils: {
test: /[\\/]node_modules[\\/](axios|dayjs|lodash-es)[\\/]/,
name: 'utils',
priority: 20,
reuseExistingChunk: true
},
// 其他第三方库
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendor',
priority: 10,
reuseExistingChunk: true
},
// 公共业务代码
common: {
minChunks: 2,
name: 'common',
priority: 5,
reuseExistingChunk: true
}
}
},
// 压缩优化
minimize: true,
minimizer: [
new TerserPlugin({
parallel: true, // 多进程压缩
terserOptions: {
compress: {
drop_console: true,
drop_debugger: true,
pure_funcs: ['console.log']
},
format: {
comments: false
}
},
extractComments: false
}),
new CssMinimizerPlugin({
parallel: true,
minimizerOptions: {
preset: [
'default',
{
discardComments: { removeAll: true }
}
]
}
})
]
}
};
/**
* 构建性能优化效果对比
*/
/*
优化前:
- 首次构建:10分30秒
- 增量构建:45秒
- 生产构建:8分钟
- Bundle大小:5.2MB
优化后:
- 首次构建:2分15秒(提升78%)
- 增量构建:8秒(提升82%)
- 生产构建:1分30秒(提升81%)
- Bundle大小:1.8MB(减少65%)
优化措施:
1. 启用持久化缓存:减少50%构建时间
2. 使用thread-loader:减少30%构建时间
3. 优化resolve配置:减少10%构建时间
4. 代码分割优化:减少65% bundle大小
5. 压缩优化:减少20% bundle大小
*/
进阶阅读
- Webpack官方文档 - 最权威的Webpack学习资源
- Webpack中文文档 - 官方文档的中文翻译版本
- 深入浅出Webpack - 系统讲解Webpack原理和实践的书籍
- Webpack源码解析 - 阅读源码是深入理解Webpack的最佳方式
- Tapable文档 - 理解Webpack的事件流机制
- Webpack性能优化指南 - 官方性能优化建议
- Webpack Bundle Analyzer - 可视化分析bundle组成
- Awesome Webpack - Webpack相关资源汇总
- Webpack Examples - 官方提供的各种配置示例
- Webpack Plugins - 官方插件列表和文档
- Module Federation - 模块联邦官方文档
下一步
恭喜你完成了Webpack基础的学习!
通过本章的学习,你已经掌握了:
- Webpack的四大核心概念(Entry、Output、Loader、Plugin)
- Webpack的构建流程和工作原理
- 代码分割和懒加载的实现方法
- 开发环境和生产环境的配置策略
- 模块联邦和微前端架构
- 构建性能优化的完整方案
- 企业级应用的最佳实践
- Webpack与Vite的区别和选择建议
下一章我们将学习前端性能优化,它将帮助你:
- 深入理解前端性能优化的核心指标(Core Web Vitals)
- 掌握加载性能、运行时性能、渲染性能的全面优化策略
- 学习使用Chrome DevTools、Lighthouse等工具进行性能分析
- 了解真实项目的性能优化案例和数据分析方法
性能优化是前端工程师的核心竞争力之一,也是Webpack配置的最终目标。让我们继续深入学习!