话说,前端练习时长也快两年了,但是关于 webpack 的东西好像也没怎么研究过 😅
🚩一是没有这方面的需求:回想一下,关于 webpack 的配置相关工作,也就只有自己配置过一次 loader「使用 svg-sprite-loader、svgo-loader 优化 svg symbols」,还是摸着石头过河;
🚩二是大部分的配置工作脚手架都已经做好了,这很可能导致一个问题,就是别人问你 webpack 相关的知识的时候,阿巴阿巴阿巴... 🤕️
确实,大多数情况下,前端开发人员可能不需要深入了解 webpack,但了解 webpack 的基本概念和用法对于前端开发仍然是很有益的。话不多说,开搞!🤓️
1. webpack
先让我们 👀 一下 webpack 官网 的解释:
本质上,webpack 是一个用于现代 JavaScript 应用程序的 静态模块打包工具。当 webpack 处理应用程序时,它会在内部从一个或多个入口点构建一个 依赖图(dependency graph),然后将你项目中所需的每一个模块组合成一个或多个 bundles,它们均为静态资源,用于展示你的内容。
怎么感觉所有框架或工具的官网定义都不是那么的通俗易懂?🤔
通俗点讲,就是当我们开发应用时,无论你用什么框架也好,都会在项目内部有一个入口文件,比如 Vue 和 React 项目的 `main.ts` 文件,其他模块的代码一般会分散在多个文件中,这些文件可能包含不同的功能、库或模块。为了能在浏览器中运行这些代码,我们需要将它们打包成一个或多个文件,比如我们平时打包出来的 `dist` 或 `build` 目录,这就是 webpack 的作用 🤷♂️
webpack 的主要功能包括:
-
模块打包:webpack 将应用程序的各个模块作为输入,通过解析模块之间的依赖关系,将它们打包成一个或多个静态资源文件。➡️ `pnpm run build`
-
资源转换:webpack 支持加载各种类型的文件,并且可以通过加载器(Loaders)对它们进行转换。比如,可以使用 Babel-loader 将 ES6/ES7 的 JavaScript 代码转换为浏览器可识别的 ES5 代码。➡️ loader 加载器
-
插件系统:webpack 提供了丰富的插件系统,开发者可以使用插件来扩展和定制打包过程。比如,可以使用 UglifyJS 插件来压缩 JavaScript 代码,或者使用 HtmlWebpackPlugin 插件生成 HTML 文件。➡️ plugin 插件
此外,webpack 还提供了许多优化功能,如代码压缩、代码拆分、懒加载等,以优化应用性能 🐂🍺
2. loader
loader,顾名思义,加载器。
webpack 只能理解 JavaScript 和 JSON 文件,这是 webpack 开箱即用自带的能力。loader 让 webpack 拥有能够去处理其他类型的文件都能力,并将它们转换为有效模块,以供应用程序使用,以及被添加到依赖图中。比如刚才提到的,可以使用 Babel-loader 将 ES6/ES7 的 JavaScript 代码转换为浏览器可识别的 ES5 代码。
🚩一句话概括:loader 就是协助 webpack 打包处理特定的文件模块。
在更高层面,在 webpack 的配置中,loader 有两个属性:
test
属性,识别出哪些文件会被转换。use
属性,定义出在进行转换时,应该使用哪个 loader。
// webpack.config.js
const path = require('path');
module.exports = {
output: {
filename: 'my-first-webpack.bundle.js',
},
module: {
rules: [{ test: /\.txt$/, use: 'raw-loader' }],
},
};
以上配置中,对一个单独的 module 对象定义了 rules
属性,里面包含两个必须属性:test
和 use
。这告诉 webpack 编译器(compiler) 如下信息:
“嘿,webpack 编译器,当你碰到「在 require()
/import
语句中被解析为 '.txt' 的路径」时,在你对它打包之前,先 use(使用) raw-loader
转换一下。” 🐒
是不是还挺简单的?去看文档!🙄️
下面简单看一下 webpack 常见的 loader 🤔
🚩 2.1 babel-loader
作用:将高级 JS 语法转化成低级语法 → 才能运行在 IE
webpack 只能打包处理部分高级 JS 语法,对于无法处理的需借助 babel-loader 打包,比如:
class Person {
// 通过 static 关键字,为 Person 类定义了一个静态属性 info
// webpack 无法打包处理“静态属性”这个高级语法
static info = 'person info'
}
// 安装 babel-loader 相关的包
npm i babel-loader @babel/core @babel/plugin-proposal-class-properties
// 在 webpack.config.js 的 module -> rules 数组中,添加 loader 规则:
module: [
rules: [
{
test: /.js$/, // 匹配的文件类型
exclude: /node_modules/, // 排除项
use: { // 对应要调用的loader
loader: "babel-loader",
options: { // 参数项
// 声明一个babel插件,此插件用来转化class中的高级语法
plugins:['@babel/plugin-proposal-class-properties']
}
}
}
]
]
🚩 2.2 ts-loader
作用:把 TS 转变成 JS,并提示类型错误
// 安装
// npm install ts-loader typescript --save-dev
// 在 webpack.config.js 的 module -> rules 数组中,添加 loader 规则:
module: {
rules: [
// `.ts/.cts/.mts/.tsx` extension files will be handled by `ts-loader`
{ test: /\.([cm]?ts|tsx)$/, loader: "ts-loader" }
]
}
🚩 2.3 less/sass-loader、postcss-loader、css-loader、style-loader
- less/sass-loader: 将 less/sass 转化成 css
- postcss-loader: 优化 css (如:加前缀) → 最好放 css-loader 之前
- css-loader: 将 css 转化成 JS 字符串
- style-loader: 将 JS 字符串转化成 style 标签
// 安装 css 相关的 loader 的包
npm install style-loader css-loader less/sass-loader less/sass -D
// 在 webpack.config.js 的 module -> rules 数组中,添加 loader 规则:
module: {
rules: [
{
test : /.css$/,
use : ['style-loader', 'css-loader', 'postcss-loader', 'less/sass-loader']
} // 多个 loader 的调用顺序是:从后往前调用
]
}
🤔 多个 loader 的调用顺序是从后往前的?其实不然,官方文档 有这样一段描述:
loader 总是 从右到左被调用。有些情况下,loader 只关心 request 后面的 元数据(metadata),并且忽略前一个 loader 的结果。在实际(从右到左)执行 loader 之前,会先 从左到右 调用 loader 上的
pitch
方法。
🚩 2.4 url-loader、file-loader
- file-loader:一个简单的文件加载器,它会将源文件复制到输出目录,并返回文件的最终路径。它通常用于处理像图片、字体等文件类型,可以将这些文件复制到输出目录,并根据需要生成正确的 URL 地址供应用程序使用。
例如,在 webpack 配置中使用 file-loader
处理图片文件:
// 安装相关的 loader 的包
npm i file-loader
// 在 webpack.config.js 的 module -> rules 数组中,添加 loader 规则:
module: {
rules: [
{
test: /\.(png|jpg|gif)$/,
use: [
{
loader: 'file-loader',
options: {
name: '[name].[hash].[ext]', // 输出文件名的格式
outputPath: 'images/' // 输出文件的目录
}
}
]
}
]
}
这个配置会将匹配到的图片文件复制到输出目录中的 images/
目录,并生成一个对应的文件名。
- url-loader:基于
file-loader
的封装,并增加了一些额外的功能。它可以根据文件大小将文件转换为 Data URL 或将其保留为文件,并返回相应的 URL 地址。这样做的好处是,对于小文件,可以将其转换为 Data URL,避免额外的网络请求,而对于大文件,则可以保留为文件。
// 安装相关的 loader 的包
npm i url-loader
// 在 webpack.config.js 的 module -> rules 数组中,添加 loader 规则:
module: {
rules: [
{
test : /.jpg|png|gif$/,
use : { // 带参数项的 loader 可以通过对象的方式进行配置
loader: "url-loader",
options: {
limit: 10240, // limit 指定图片的大小,单位是字节(byte)
name: '[name].[hash].[ext]', // 输出文件名的格式
outputPath: 'images/' // 输出文件的目录
}
} // 只有 <= limit大小的图片,才会被转为 base64格式的图片
} // 配了 url-loader 在配置里面就不要再给图片配 file-loader 了
] // 因为 url-loader 默认会使用 file-loader 来处理图片的路径关系的
}
🚩 2.5 svg-sprite-loader、svgo-loader
- svg-sprite-loader:官方解释是:一个用于创建 svg 雪碧图的 Webpack 加载器。这个加载器现在已经被 JetBrains 公司收录和维护了。通俗的讲:svg-sprite-loader 会把你引入的 svg 塞到一个个 symbol 中,合成一个大的 svg,最后将这个大的 svg 放入 body 中。symbol 的 id 如果不特别指定,就是你的文件名。
- svgo-loader:是基于 SVG Optimizer 的一个加载器,而 SVG Optimizer 是一个基于node.js 的工具,用于优化 SVG 矢量图形文件,它可以删除和修改SVG元素,折叠内容,移动属性等。
// 安装相关的 loader 的包
npm i svg-sprite-loader svgo-loader --dev
// 在 webpack.config.js 的 module -> rules 数组中,添加 loader 规则:
module: {
rules: [
{
test : /\.svg$/,
use : [
{ loader: 'svg-sprite-loader', options: {} },
{ loader: 'svgo-loader', options: {
plugins: [{
name: 'removeAttrs', // 必须指定name!
params: {attrs: 'fill'}
}]
}
]
}
]
}
ps:对这个 loader 感兴趣的话可以参考 使用 svg-sprite-loader、svgo-loader 优化 svg symbols
3. plugin
plugin,顾名思义,插件。
通过安装和配置第三方插件,可以扩展 webpack 的能力,从而让 webpack 用起来更方便。
loader 用于转换某些类型的模块,而插件则可以用于执行范围更广的任务。包括:打包优化,资源管理,注入环境变量。
🚩一句话概括:plugin 是用于扩展和定制 webpack 功能的工具。没用过浏览器插件吗?🤷♂️
想要使用一个插件,你只需要 require()
它,然后把它添加到 plugins
数组中。多数插件可以通过选项(option)自定义。你也可以在一个配置文件中因为不同目的而多次使用同一个插件,这时需要通过使用 new
操作符来创建一个插件实例。
// webpack.config.js
const HtmlWebpackPlugin = require('html-webpack-plugin');
const webpack = require('webpack'); // 用于访问内置插件
module.exports = {
module: {
rules: [{ test: /\.txt$/, use: 'raw-loader' }],
},
plugins: [new HtmlWebpackPlugin({ template: './src/index.html' })],
};
在上面的示例中,html-webpack-plugin
为应用程序生成一个 HTML 文件,并自动将生成的所有 bundle 注入到此文件中。
是不是还挺简单的?去看文档!🙄️
下面简单看一下 webpack 常见的 plugin 🤔
一些常用的 Webpack 插件:
-
HtmlWebpackPlugin
:用于生成 HTML 文件,并将打包生成的资源文件自动注入到 HTML 文件中。 -
MiniCssExtractPlugin
:用于将 CSS 代码提取为独立的文件,而不是内联到 JavaScript 文件中。 -
CleanWebpackPlugin
:用于清理输出目录中的旧文件,以便在每次构建之前保持输出目录的干净。 -
OptimizeCSSAssetsPlugin
:用于优化和压缩 CSS 代码。 -
DefinePlugin
:用于定义全局常量,可以在应用程序的代码中直接使用。
可以根据官网给出的步骤配置插件:
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
module.exports = {
entry: './src/index.js',
output: {
path: 'dist',
filename: 'bundle.js'
},
module: { rules: [ /* 添加 Loader 的规则 */ ] },
plugins: [
new HtmlWebpackPlugin({
template: './src/index.html'
}),
new MiniCssExtractPlugin({
filename: 'styles.css'
}),
new CleanWebpackPlugin()
]
};
4. loader vs plugin
如果看完上面的解释,还是不知道 webpack 的 loader 和 plugin 的区别的话,那我们举个 🌰
假如你是一名厨师,你有一些食材(模块文件)需要处理,并且需要一些工具来做完这道菜。
🚩 Loader 就像你的各种厨房工具。例如,切菜刀、搅拌器、炉灶等,这些工具帮助你对食材进行加工和转换,以便制作出美味的菜肴。在 webpack 中 loader 的作用也是一样的,它们负责将不同类型的文件进行处理和转换,比如:将 ES6 代码转换为 ES5 代码,将 CSS 文件转换为浏览器可识别的样式等。
🚩 Plugin 则像你的特殊调料和烹饪技巧。假设你想给菜肴增添特殊的风味或实现特定的效果。你可能会使用辣椒酱增加辣味,柠檬汁增添酸味,或者使用烘烤技巧让菜表面金黄酥脆。在 webpack 中 plugin 的作用也是一样的,它们可以在构建过程中监听事件,并执行一些特殊的操作。例如,你可以使用 HtmlWebpackPlugin 生成一个带有引入资源的 HTML 文件,使用 UglifyJSPlugin 压缩和混淆 JavaScript 代码,或者使用 ExtractTextPlugin 将 CSS 提取为独立的文件。
总结来说,loader 是用于处理和转换文件的工具,类似于厨房中的各种工具,而 plugin 则是用于扩展和定制构建过程的工具,类似于特殊的调料和烹饪技巧。它们共同协作,使得 Webpack 能够处理各种文件类型、进行模块化开发,并通过插件机制进行灵活的定制和优化。🎉
5. 自己写一个 plugin
Webpack 插件就是一个 JavaScript 对象,通过扩展或修改 webpack 的功能来实现特定的构建需求。它可以在 webpack 的构建过程中干预并做出相应的处理。基本的 webpack 插件结构如下:
class MyPlugin {
constructor(options) {
// 在构造函数中接收插件的配置选项
this.options = options;
}
apply(compiler) {
// 在 apply 方法中定义插件的逻辑
// compiler 对象代表了完整的 webpack 环境配置
// 可以通过 compiler 对象来访问 webpack 的各种钩子函数
// 注册钩子函数,以在 webpack 构建过程中执行特定操作
compiler.hooks.someHook.tap('MyPlugin', () => {
// 在这里执行你的插件逻辑
});
}
}
👆这是一个最基本的 webpack 插件结构示例,webpack 插件的结构包括一个 apply
方法和一些钩子函数。apply
方法在插件被应用时被调用,接受一个 compiler
参数,该参数代表了完整的 webpack 环境配置。通过 compiler
对象,插件可以访问 webpack 的各种钩子函数并注册自己的逻辑。
钩子函数是 webpack 在构建过程中的特定时间点触发的函数。插件可以根据需求选择合适的钩子函数,并在这些函数中执行自定义的逻辑。例如,在构建开始前可以使用 beforeRun
或 run
钩子,在构建完成后可以使用 done
钩子。
下面是一些常用的 webpack 钩子函数:
beforeRun
:在 webpack 构建启动之前执行。run
:在开始构建之前执行。beforeCompile
:在编译之前执行。compile
:在开始编译之前执行。compilation
:在每次新的编译创建之前执行。emit
:在生成资源并输出到输出目录之前执行。afterEmit
:在资源输出到输出目录之后执行。done
:在构建完成时执行。- 不止这些钩子吧?去看文档!🙄
插件可以使用这些钩子函数来执行各种任务,如修改、添加、删除资源,生成额外的文件,提取公共代码,优化输出等等。
OK,举个 🌰
假如我们比较关心项目在构建完成后产出的文件的路径和大小,go!
const fs = require('fs');
class MyPlugin {
constructor(options) {
this.options = options;
}
apply(compiler) { // 构建时只会执行一次
compiler.hooks.done.tap('MyPlugin', (stats) => {
const outputPath = stats.compilation.outputOptions.path;
const outputFileName = stats.compilation.outputOptions.filename;
const filePath = `${outputPath}/${outputFileName}`;
const fileSize = fs.statSync(filePath).size;
console.log(`Built file: ${filePath}`);
console.log(`File size: ${fileSize} bytes`);
});
}
}
在上述示例中,我们在 webpack 构建完成后的 done
钩子中获取构建输出文件的路径和大小,并将其输出到控制台。
🚩 为什么说 apply 只会执行一次?🤔
我在实践手写这个插件的时候,为了调试,在 apply 内部写了一个日志 log,我发现日志仅在第一次编译的时候可以执行,在热更新的时候,日志并没有打印,这是怎么回事?
还是刚才那个例子,apply 方法相当于你炒菜的点火动作,具体到了什么时机以及我们需要具体做什么,是需要提前注册具体的逻辑的。这里为了方便记忆,你也可以类比成 dom 的 addEventListener,具体的注册动作只发生一次,但是监听到事件之后是每次都会执行的!
最后,要使用此插件,还需要在 webpack 配置文件中引入并实例化它
const MyPlugin = require('./path/to/MyPlugin');
module.exports = {
// ...其他配置项
plugins: [
new MyPlugin({
// 插件的配置选项
})
]
};
end 🎉