介绍
webpack 是基于模块化的打包(构建)工具,它把一切视为模块。
它通过一个开发时态的入口模块为起点,分析出所有的依赖关系,然后经过一系列的过程(压缩、合并),最终生成运行时态的文件
Webpack的配置主要包括以下几个部分:
- entry。指定Webpack打包的入口文件,可以是单个或多个JavaScript文件。这个配置决定了Webpack从哪个模块开始生成依赖关系图。1234
- output。设置Webpack打包后输出的目录和文件名称,包括path、filename和publicPath等。235
- module。配置了不同的loaders来处理不同的模块,例如,对于CSS文件,可以使用css-loader和style-loader。2345
- resolve。设置Webpack如何解析模块依赖,包括别名、扩展名等。
- plugins。使用不同的插件可以增强Webpack的功能,例如,使用html-webpack-plugin可以将打包后的js文件自动引用到HTML文件中。
- devServer。提供了一个简单的web服务器和实时重载功能,可以通过devServer.contentBase、devServer.port、devServer.proxy等进行配置。
- optimization。可以使用optimization.splitChunks和optimization.runtimeChunk配置代码拆分和运行时代码提取等优化策略。
- externals。用于配置排除打包的模块,例如,可以将jQuery作为外置扩展,避免将其打包到应用程序中。
- devtool。配置source-map类型。
- context。webpack使用的根目录,string类型必须是绝对路径。
- target。指定Webpack编译的目标环境。
- performance。输出文件的性能检查配置。
- noParse。不用解析和处理的模块。
- stats。控制台输出日志控制。
特性
-
为前端工程化而生:webpack 致力于解决前端工程化,特别是浏览器端工程化中遇到的问题,让开发者集中注意力编写业务代码,而把工程化过程中的问题全部交给 webpack 来处理
-
简单易用:支持零配置,可以不用写任何一行额外的代码就使用 webpack
-
强大的生态:非常灵活、可以扩展,webpack 本身的功能并不多,但它提供了一些可以扩展其功能的机制,使得一些第三方库可以融于到 webpack 中
-
基于nodejs: 由于 webpack 在构建的过程中需要读取文件,因此它是运行在 node 环境中的
-
基于模块化:webpack 在构建过程中要分析依赖关系,方式是通过模块化导入语句进行分析的,它支持各种模块化标准,包括但不限于 CommonJS、ES6 Module
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const webpack = require('webpack');
const TerserPlugin = require('terser-webpack-plugin');
module.exports = {
entry: './src/index.js', // 打包的入口文件
// 指定打包后文件的输出位置和文件名
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'bundle.js',
},
// 指定 Webpack 模式,可以是 development、production 或 none。
mode: 'development',
// 配置 Loader,用于处理不同类型的文件
module: {
rules: [
{
test: /.js$/,
exclude: /node_modules/,
use: 'babel-loader',
},
{
test: /.css$/,
use: ['style-loader', 'css-loader'],
},
{
test: /.(png|svg|jpg|gif)$/,
use: ['file-loader'],
},
],
},
// 配置插件,用于执行各种任务,如打包优化、资源管理等
plugins: [
new HtmlWebpackPlugin({
template: './src/index.html',
}),
// 使用 DefinePlugin 插件定义环境变量
new webpack.DefinePlugin({
'process.env.NODE_ENV': JSON.stringify('production'),
}),
new WebpackManifestPlugin({
fileName: 'manifest.json', // 生成的 Manifest 文件名
publicPath: '/', // 公共路径
}),
],
// 配置开发服务器,用于本地开发和热更新
devServer: {
contentBase: './dist',
hot: true,
proxy: {
'/api': 'http://localhost:3000',
}, // 配置代理,用于将特定 URL 路径代理到另一个服务器
},
// 配置模块解析选项
resolve: {
// 自动补全文件扩展名,这样在导入模块时,可以省略这些扩展名
extensions: ['.js', '.jsx', '.json'],
// 创建模块别名,以便更方便地导入模块
alias: {
'@components': path.resolve(__dirname, 'src/components/'),
'@utils': path.resolve(__dirname, 'src/utils/'),
},
},
// 配置优化选项,如代码分割和压缩
optimization: {
splitChunks: {
chunks: 'all',
},
minimize: true,
minimizer: [
new TerserPlugin({
terserOptions: {
compress: {
drop_console: true,
},
},
}),
],
},
// 配置 SourceMap 选项,用于调试,默认没 SourceMap
devtool: 'source-map',
};
打包流程
-
初始化参数:从配置文件 和 Shell 语句中读取与合并参数,得出最终的参数;
-
开始编译:用上一步得到的参数
- 初始化 Compiler 对象
- 加载所有配置的 Plugin 插件
- 执行对象的
run
方法开始执行编译;
-
确定入口:根据配置项的
entry
,找出所有的入口文件; -
编译模块:从入口文件出发,调用所有配置的 Loader 对模块进行翻译,再找出该模块依赖的模块,递归此步骤直到所有依赖的文件也都处理过;
-
输出资源:根据入口和模块之间的依赖关系,组装成一个包含多个模块的 Chunk。再把每个 Chunk 转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会;
-
输出完成:在确定好输出内容后,根据
output
确定输出的路径和文件名,输出文件夹。
在以上过程中,Webpack 会在特定的时间点广播出特定的事件,插件在监听到感兴趣的事件后会执行特定的逻辑,并且插件可以调用 Webpack 提供的 API 改变 Webpack 的运行结果。
总的来说,Webpack 的模块打包原理就是通过递归解析模块之间的依赖关系,将所有的模块打包成为一个或多个文件,并通过一系列的插件和 loader 对代码进行处理和优化。
这样可以减少 HTTP 请求次数、提高页面加载速度,并大大提高了前端开发的效率和可维护性。
webpack 如何确定依赖引用顺序
-
入口点:Webpack 从配置的入口点
entry
开始,从入口文件开始解析。 -
递归解析:递归解析每个模块的依赖,找到所有被引用的模块。
-
构建依赖图:根据模块之间的依赖关系构建一个依赖图。
-
确定顺序:根据依赖图确定模块的引用顺序,确保被依赖的模块先于依赖它们的模块打包。
Module/Chunk/Bundle 是什么
-
Module:webpack 里一个概念性内容,每个文件都可以看为一个 module。 js、css、图片等都可以看作 module。
-
Chunk:代码块,webpack 处理代码时候的一个中间态,它表示有一组功能相关的模块的集合。一个 Chunk 可以由多个模块(module)组成
-
Bundle:是 Webpack 构建结果的输出,由一个或多个 Chunk 的合并优化后的结果,最终以文件形式输出,用于在浏览器中加载和执行。
文件指纹
文件指纹是打包后输出的文件名的后缀。
filename: "[name][hash:8][ext]"
-
hash
是跟整个项目的构建相关,只要项目里有文件更改,整个项目构建的hash
值都会更改,并且全部文件都共用相同的hash
值。(粒度整个项目) -
chunkhash
是根据不同的入口进行依赖文件解析,构建对应的chunk
(模块),生成对应的hash
值。只有被修改的chunk
(模块)在重新构建之后才会生成新的hash
值,不会影响其它的chunk
。(粒度entry
的每个入口文件) -
contenthash
是跟每个生成的文件有关,每个文件都有一个唯一的hash
值。当要构建的文件内容发生改变时,就会生成新的hash
值,且该文件的改变并不会影响和它同一个模块下的其它文件。(粒度每个文件的内容)
Loader和Plugin的区别
功能不同:
Loader本质是一个函数,它是一个转换器。webpack只能解析原生js文件,对于其他类型文件就需要loade进行转换。
Plugin它是一个插件,用于增强webpack功能。webpack在运行的生命周期中会广播出许多事件,Plugin 可以监听这些事件,在合适的时机通过 webpack 提供的 API 改变输出结果 。
用法不同:
Loader的配置是在module.rules下进行。类型为数组,每⼀项都是⼀个 Object ,⾥⾯描述了对于什么类型的⽂件( test ),使⽤什么加载( loader )和使⽤的参数( options ) 。
Plugin的配置在plugins下。类型为数组,每一项是一个 Plugin 的实例,参数都通过构造函数传入。
Loader:
- babel-loader:将ES6+的代码转换成ES5的代码。
- css-loader:解析CSS文件,并处理CSS中的依赖关系。
- style-loader:将CSS代码注入到HTML文档中。
- file-loader:解析文件路径,将文件赋值到输出目录,并返回文件路径。
- url-loader:类似于file-loader,但是可以将小于指定大小的文件转成base64编码的Data URL格式
- sass-loader:将Sass文件编译成CSS文件。
- less-loader:将Less文件编译成CSS文件。
- postcss-loader:自动添加CSS前缀,优化CSS代码等。
- vue-loader:将Vue单文件组件编译成JavaScript代码。
Plugin:
- HtmlWebpackPlugin:生成HTML文件,并自动将打包后的javaScript和CSS文件引入到HTML文件中。
- CleanWebpackPlugin:清除输出目录。
- ExtractTextWebpackPlugin:将CSS代码提取到单独的CSS文件中。
- DefinePlugin:定义全局变量。
- UglifyJsWebpackPlugin:压缩JavaScript代码。
- HotModuleReplacementPlugin:热模块替换,用于在开发环境下实现热更新。
- MiniCssExtractPlugin:与ExtractTextWebpackPlugin类似,将CSS代码提取到单独的CSS文件中。
- BundleAnalyzerPlugin:分析打包后的文件大小和依赖关系。
自定义Loader
Loader 本质上是一个函数,作用是将某个源码字符串转换成另一个源码字符串返回。接收源文件代码字符串为参数,经过处理转换,然后 return
目标代码字符串
module.exports = function(source) {
// 对源代码进行处理
const result = source.replace(/\b(foo)\b/g, 'bar');
// 返回更新后的代码
return result;
};
// 需要异步
module.exports = function(source) {
const callback = this.async();
someAsyncOperation(source, (err, transformedSource, sourceMap) => {
if (err) {
// 如果有错误发生,传递错误对象
callback(err);
return;
}
// 成功处理,传递处理后的结果和source map(如果有的话)
callback(null, transformedSource, sourceMap);
});
};
手写Plugin
在 Webpack 中,Plugin 是一个具有 apply
方法的对象。
apply
方法会被 Compiler 对象调用,并且在整个编译生命周期可以访问 Compiler 对象。所以步骤如下:
- 编写插件类:创建一个类,实现
apply
方法。Webpack 在启动编译过程时,会调用每个插件实例的apply
方法 - 注册钩子回调:在
apply
方法中,使用编译器 Compiler 对象注册你需要的钩子回调。 - 实现功能逻辑:在回调函数中实现具体的插件逻辑。
// 通过 tap 方法注册钩子,第一个参数是插件名称,第二个参数是回调函数
module.exports = class MyPlugin {
apply(compiler) {
// 注册事件,类似于window.onload = function() {}
compiler.hooks.done.tap('MyPlugin', (Compilation) => {
console.log('MyPlugin: Compilation finished!');
});
}
}
const MyPlugin = require('./MyPlugin');
module.exports = {
plugins: [
new MyPlugin(),
]
}
// 第一版
// function No1WebpackPlugin (options) {
// this.options = options
// }
// No1WebpackPlugin.prototype.apply = function (compiler) {
// compiler.plugin('done', () => {
// console.log(this.options.msg)
// })
// }
// 第二版
// class No1WebpackPlugin {
// constructor (options) {
// this.options = options
// }
// apply (compiler) {
// compiler.plugin('done', () => {
// console.log(this.options.msg)
// })
// }
// }
// 第三版
function No1WebpackPlugin (options) {
this.options = options
}
No1WebpackPlugin.prototype.apply = function (compiler) {
compiler.hooks.done.tap('No1', () => {
console.log(this.options.msg)
})
}
module.exports = No1WebpackPlugin;
Webpack热更新原理
Webpack
的热更新又称热替换(Hot Module Replacement
),缩写为 HMR
。 这个机制可以做到不用刷新浏览器而将新变更的模块替换掉旧的模块。
HMR的核心就是客户端从服务端拉取更新后的文件,准确的说是 chunk diff (chunk 需要更新的部分),实际上 WDS 与浏览器之间维护了一个 Websocket
,当本地资源发生变化时,WDS 会向浏览器推送更新,并带上构建时的 hash,让客户端与上一次资源进行对比。客户端对比出差异后会向 WDS 发起 Ajax
请求来获取更改内容(文件列表、hash),这样客户端就可以再借助这些信息继续向 WDS 发起 jsonp
请求获取该chunk的增量更新。
//webpack.config.dev.js
module.exports = {
devServer: {
hot: true //开启热更新
}
}
Tree Shaking
Tree Shaking 是一种用于移除 JavaScript 中未使用代码的优化技术。可以减小打包文件的体积,提高加载性能。
它依赖于 ES6 模块的静态结构特性(import
和 export
),使得构建工具能够在编译时确定哪些代码是未使用的,并将其移除。
工作原理
- 静态分析:编译时可以确定模块之间的依赖关系,哪些被使用了。
- 标记未使用:通过分析,标记所有未被引用的代码。
- 移除未使用:在最终生成的代码中移除那些未被标记为使用的代码。
实现步骤
- 使用 ES6 模块语法 (
import
和export
)。 - 在 Webpack 配置中启用生产模式和
usedExports
选项。
// webpack.config.js
const path = require('path');
module.exports = {
mode: 'production', // Tree Shaking 仅在生产模式下启用
entry: './src/main.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist'),
},
optimization: {
usedExports: true, // 启用 Tree Shaking
},
};
3. 确保模块是纯函数,没有副作用。
// 副作用:在模块加载时发起网络请求
export const data = fetch('https://api.example.com/data'); // module.js
// 即使 data 在 main.js 中未被使用,构建工具也不能安全地移除它
// 因为移除它会导致网络请求不再发生,从而改变程序的行为。
import { data } from './module'; // main.js
// fetchData 是一个纯函数
// 如果未被使用,构建工具可以安全地移除它,而不会影响程序的其他部分。
export function fetchData() {
return fetch('https://api.example.com/data');
}
import { fetchData } from './module';
代码分割
使用 SplitChunksPlugin,这是 Webpack 的内置插件,用于将公共的依赖模块提取到单独的 chunk 中,减少代码重复、提高加载速度。
在 webpack.config.js 文件中,你可以在配置 optimization.splitChunks
选项来指定如何提取公共模块
基本配置
js
代码解读
复制代码
module.exports = {
// 其他配置...
optimization: {
splitChunks: {
chunks: 'all', // 对所有模块进行优化
}
}
};
高级配置: 通过cacheGroups
自定义分割策略
module.exports = {
// 其他配置...
optimization: {
splitChunks: {
chunks: 'all', // 对所有模块进行优化
minSize: 20000, // 生成chunk的最小大小(以字节为单位)
minChunks: 1, // 分割前必须共享模块的最小块数
maxAsyncRequests: 30, // 按需加载时的最大并行请求数
maxInitialRequests: 30, // 入口点的最大并行请求数
automaticNameDelimiter: '~', // 默认情况下,webpack将使用块的来源和名称生成名称(例如vendors~main.js)
cacheGroups: { // 缓存组可以继承或覆盖splitChunks.*的任何选项
vendors: {
test: /[/]node_modules[/]/, // 控制哪些模块被这个缓存组选中
priority: -10 // 一个模块可以属于多个缓存组。优化将优先考虑具有更高优先级的缓存组
},
default: {
minChunks: 2,
priority: -20,
reuseExistingChunk: true // 如果当前块包含已经从主束分离出的模块,则将重用它而不是生成新的块
}
}
}
}
};
-
提取第三方库为chunk:通过
vendors
缓存组。- 可以将
node_modules
的模块提取到单独的文件中,这对于提取大型的第三方库(如React, Vue等)特别有用。
- 可以将
-
提取公共模块为chunk:通过
default
缓存组。- Webpack 会自动提取,被多个入口共享的模块到一个或多个公共块中。
Source Map
配置 devtool: 'source-map'
后,
在编译过程中,会生成一个 .map
文件,一般用于代码调试和错误监控。
-
包含了源代码、编译后的代码、以及它们之间的映射关系。
-
编译后的文件通常会在文件末尾添加一个注释,指向 SourceMap文件的位置。
// # sourceMappingURL=example.js.map
-
当在浏览器开发者工具调试时,浏览器会读取这行注释并加载对应的 SourceMap 文件
报错时,点击跳转。即使运行的是编译后的代码,也能够追溯到原始源代码的具体位置,而不是处理经过转换或压缩后的代码,从而提高了调试效率。
- 开发环境:
cheap-eval-source-map
,生产这种source map
速度最快,并且由于开发环境下没有代码压缩,所以不会影响断点调试 - 生产环境:
hidden-source-map
,由于进行了代码压缩,所以并不会占用多大的体积
Babel 原理
Babel 是 JS 的转换器,主要用来将新 JS 语法转为向后兼容版本
原理很简单,就三部分:解析/转换/生成。
- 解析:通过词法分析把代码变 token、语法分析把 token 解析成抽象语法树(AST)
- 转换:接收到 AST 并遍历,对树上节点增删改实现兼容性转换
- 生成:被转换的新 AST 被生成为新 JS 代码字符串
优化构建速度
alias 用于创建 import
或 require
的别名,用来简化模块引用,项目中基本都需要进行配置。
const path = require('path')
...
// 路径处理方法
function resolve(dir){
return path.join(__dirname, dir);
}
const config = {
...
resolve:{
// 配置别名
alias: {
'~': resolve('src'),
'@': resolve('src'),
'components': resolve('src/components'),
}
}
};
// 使用 src 别名 ~
import '~/fonts/iconfont.css'
// 使用 src 别名 @
import '@/fonts/iconfont.css'
// 使用 components 别名
import footer from "components/footer";
extensions尽可能减少后缀名尝试的可能性,webpack 就会按照 extensions 配置的数组从左到右的顺序去尝试解析模块
const config = {
//...
resolve: {
extensions: ['.js', '.json', '.wasm'],
},
};
// 如果想保留默认配置,可以用 `...` 扩展运算符代表默认配置
const config = {
//...
resolve: {
extensions: ['.ts', '...'],
},
};
modules 指定webpack 搜索的范围,快速找到相关模块
const path = require('path');
module.exports = {
resolve: {
modules: [path.resolve(__dirname, '../node_modules')]
}
}
externals,模块是外部引入的,不打包到 bundle 中
const config = {
//...
externals: {
jquery: 'jQuery',
},
};
缩小范围
在配置 loader 的时候,我们需要更精确的去指定 loader 的作用目录或者需要排除的目录,通过使用 include
和 exclude
两个配置项,可以实现这个功能,常见的例如:
include
:符合条件的模块进行解析exclude
:排除符合条件的模块,不解析exclude
优先级更高
例如在配置 babel 的时候
const path = require('path');
// 路径处理方法
function resolve(dir){
return path.join(__dirname, dir);
}
const config = {
//...
module: {
noParse: /jquery|lodash/,
rules: [
{
test: /\.js$/i,
include: path.resolve(__dirname,'../src'),
exclude: /node_modules/,
use: [
'babel-loader',
]
},
// ...
]
}
};
noParse
- 不需要解析依赖的第三方大型类库等,可以通过这个字段进行配置,以提高构建速度
- 使用 noParse 进行忽略的模块文件中不会解析
import
、require
等语法
const config = {
//...
module: {
noParse: /jquery|lodash/,
rules:[...]
}
};
IgnorePlugin
防止在 import
或 require
调用时,生成以下正则表达式匹配的模块:
requestRegExp
匹配(test)资源请求路径的正则表达式。contextRegExp
匹配(test)资源上下文(目录)的正则表达式。
// 引入 webpack
const webpack = require('webpack')
const config = {
...
plugins:[ // 配置插件
...
new webpack.IgnorePlugin({
resourceRegExp: /^\.\/locale$/,
contextRegExp: /moment$/,
}),
]
};
thread-loader 开启多进程打包
happypack(已弃用)
const path = require('path');
// 路径处理方法
function resolve(dir){
return path.join(__dirname, dir);
}
const config = {
//...
module: {
noParse: /jquery|lodash/,
rules: [
{
test: /\.js$/i,
include: resolve('src'),
exclude: /node_modules/,
use: [
{
loader: 'thread-loader', // 开启多进程打包
options: {
worker: 3,
}
},
'babel-loader',
]
},
// ...
]
}
};
缓存
babel-loader 开启缓存
const config = {
module: {
noParse: /jquery|lodash/,
rules: [
{
test: /\.js$/i,
include: resolve('src'),
exclude: /node_modules/,
use: [
// ...
{
loader: 'babel-loader',
options: {
cacheDirectory: true // 启用缓存
}
},
]
},
// ...
]
}
}
cache-loader
const config = {
module: {
// ...
rules: [
{
test: /\.(s[ac]|c)ss$/i, //匹配所有的 sass/scss/css 文件
use: [
// 'style-loader',
MiniCssExtractPlugin.loader,
'cache-loader', // 获取前面 loader 转换的结果
'css-loader',
'postcss-loader',
'sass-loader',
]
},
// ...
]
}
}
cache 持久化缓存
const config = {
cache: {
type: 'filesystem',
},
};
构建结果优化
构建结果分析
// 引入插件
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
const config = {
// ...
plugins:[
// ...
// 配置插件
new BundleAnalyzerPlugin({
// analyzerMode: 'disabled', // 不启动展示打包报告的http服务器
// generateStatsFile: true, // 是否生成stats.json文件
})
],
};
"scripts": {
// ...
"analyzer": "cross-env NODE_ENV=prod webpack --progress --mode production"
},
压缩css
// ...
// 压缩css
const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin')
// ...
const config = {
// ...
optimization: {
minimize: true,
minimizer: [
// 添加 css 压缩配置
new OptimizeCssAssetsPlugin({}),
]
},
// ...
}
压缩js
const TerserPlugin = require('terser-webpack-plugin');
const config = {
// ...
optimization: {
minimize: true, // 开启最小化
minimizer: [
// ...
new TerserPlugin({})
]
},
// ...
}
splitChunks 代码分割
module.exports = {
//...
optimization: {
splitChunks: {
chunks: 'async', // 有效值为 `all`,`async` 和 `initial`
minSize: 20000, // 生成 chunk 的最小体积(≈ 20kb)
minRemainingSize: 0, // 确保拆分后剩余的最小 chunk 体积超过限制来避免大小为零的模块
minChunks: 1, // 拆分前必须共享模块的最小 chunks 数。
maxAsyncRequests: 30, // 最大的按需(异步)加载次数
maxInitialRequests: 30, // 打包后的入口文件加载时,还能同时加载js文件的数量(包括入口文件)
enforceSizeThreshold: 50000,
cacheGroups: { // 配置提取模块的方案
defaultVendors: {
test: /[\/]node_modules[\/]/,
priority: -10,
reuseExistingChunk: true,
},
default: {
minChunks: 2,
priority: -20,
reuseExistingChunk: true,
},
},
},
},
};
const config = {
//...
optimization: {
splitChunks: {
cacheGroups: { // 配置提取模块的方案
default: false,
styles: {
name: 'styles',
test: /\.(s?css|less|sass)$/,
chunks: 'all',
enforce: true,
priority: 10,
},
common: {
name: 'chunk-common',
chunks: 'all',
minChunks: 2,
maxInitialRequests: 5,
minSize: 0,
priority: 1,
enforce: true,
reuseExistingChunk: true,
},
vendors: {
name: 'chunk-vendors',
test: /[\\/]node_modules[\\/]/,
chunks: 'all',
priority: 2,
enforce: true,
reuseExistingChunk: true,
},
// ... 根据不同项目再细化拆分内容
},
},
},
}