一文让你弄懂Webpack的Loader、Plugin
一、什么是Loader
loader
用于对模块的"源代码"进行转换,在 import
或"加载"模块时预处理文件
webpack
做的事情,仅仅是分析出各种模块的依赖关系,然后形成资源列表,最终打包生成到指定的文件中。如下图所示:
在webpack
内部中,任何文件都是模块,不仅仅只是js
文件
默认情况下,在遇到import
或者require
加载模块的时候,webpack
只支持对js
和 json
文件打包
像css
、sass
、png
等这些类型的文件的时候,webpack
则无能为力,这时候就需要配置对应的loader
进行文件内容的解析
在加载模块的时候,执行顺序如下:
当 webpack
碰到不识别的模块的时候,webpack
会在配置的中查找该文件解析规则
关于配置loader
的方式有三种:
- 配置方式(推荐):在 webpack.config.js文件中指定 loader
- 内联方式:在每个 import 语句中显式指定 loader
- CLI 方式:在 shell 命令中指定它们
配置方式
关于loader
的配置,我们是写在module.rules
属性中,属性介绍如下:
rules
是一个数组的形式,因此我们可以配置很多个loader
- 每一个
loader
对应一个对象的形式,对象属性test
为匹配的规则,一般情况为正则表达式 - 属性
use
针对匹配到文件类型,调用对应的loader
进行处理
代码编写,如下形式:
module.exports = {
module: {
rules: [
{
test: /.css$/,
use: [
{ loader: 'style-loader' },
{
loader: 'css-loader',
options: {
modules: true
}
},
{ loader: 'sass-loader' }
]
}
]
}
};
二、Loader的特性
这里继续拿上述代码,来讲讲loader
的特性
从上述代码可以看到,在处理css
模块的时候,use
属性中配置了三个loader
分别处理css
文件
因为loader
支持链式调用,链中的每个loader
会处理之前已处理过的资源,最终变为js
代码。顺序为相反的顺序执行,即上述执行方式为sass-loader
、css-loader
、style-loader
除此之外,loader
的特性还有如下:
- loader 可以是同步的,也可以是异步的
- loader 运行在 Node.js 中,并且能够执行任何操作
- 除了常见的通过
package.json
的main
来将一个 npm 模块导出为 loader,还可以在 module.rules 中使用loader
字段直接引用一个模块 - 插件(plugin)可以为 loader 带来更多特性
- loader 能够产生额外的任意文件
可以通过 loader 的预处理函数,为 JavaScript 生态系统提供更多能力。用户现在可以更加灵活地引入细粒度逻辑,例如:压缩、打包、语言翻译和更多其他特性
三、常见的loader
在页面开发过程中,我们经常性加载除了js
文件以外的内容,这时候我们就需要配置响应的loader
进行加载
常见的loader
如下:
- style-loader: 将css添加到DOM的内联样式标签style里
- css-loader :允许将css文件通过require的方式引入,并返回css代码
- less-loader: 处理less
- sass-loader: 处理sass
- postcss-loader: 用postcss来处理CSS
- autoprefixer-loader: 处理CSS3属性前缀,已被弃用,建议直接使用postcss
- file-loader: 分发文件到output目录并返回相对路径
- url-loader: 和file-loader类似,但是当文件小于设定的limit时可以返回一个Data Url
- html-minify-loader: 压缩HTML
- babel-loader :用babel来转换ES6文件到ES
下面给出一些常见的loader
的使用:
css-loader
分析 css
模块之间的关系,并合成⼀个 css
npm install --save-dev css-loader
rules: [
...,
{
test: /.css$/,
use: {
loader: "css-loader",
options: {
// 启用/禁用 url() 处理
url: true,
// 启用/禁用 @import 处理
import: true,
// 启用/禁用 Sourcemap
sourceMap: false
}
}
}
]
如果只通过css-loader
加载文件,这时候页面代码设置的样式并没有生效
原因在于,css-loader
只是负责将.css
文件进行一个解析,而并不会将解析后的css
插入到页面中
如果我们希望再完成插入style
的操作,那么我们还需要另外一个loader
,就是style-loader
style-loader
把 css-loader
生成的内容,用 style
标签挂载到页面的 head
中
npm install --save-dev style-loader
rules: [
...,
{
test: /.css$/,
use: ["style-loader", "css-loader"]
}
]
同一个任务的 loader
可以同时挂载多个,处理顺序为:从右到左,从下往上
less-loader
开发中,我们也常常会使用less
、sass
、stylus
预处理器编写css
样式,使开发效率提高,这里需要使用less-loader
npm install less-loader -D
rules: [
...,
{
test: /.css$/,
use: ["style-loader", "css-loader","less-loader"]
}
]
raw-loader
在 webpack
中通过 import
方式导入文件内容,该loader
并不是内置的,所以首先要安装
npm install --save-dev raw-loader
然后在 webpack.config.js 中进行配置
module.exports = {
...,
module: {
rules: [
{
test: /.(txt|md)$/,
use: 'raw-loader'
}
]
}
}
file-loader
把识别出的资源模块,移动到指定的输出⽬目录,并且返回这个资源在输出目录的地址(字符串)
npm install --save-dev file-loader
rules: [
...,
{
test: /.(png|jpe?g|gif)$/,
use: {
loader: "file-loader",
options: {
// placeholder 占位符 [name] 源资源模块的名称
// [ext] 源资源模块的后缀
name: "[name]_[hash].[ext]",
//打包后的存放位置
outputPath: "./images",
// 打包后文件的 url
publicPath: './images',
}
}
}
]
url-loader
可以处理 file-loader
所有的事情,但是遇到图片格式的模块,可以选择性的把图片转成 base64
格式的字符串,并打包到 js
中,对小体积的图片比较合适,大图片不合适。
npm install --save-dev url-loader
rules: [
...,
{
test: /.(png|jpe?g|gif)$/,
use: {
loader: "url-loader",
options: {
// placeholder 占位符 [name] 源资源模块的名称
// [ext] 源资源模块的后缀
name: "[name]_[hash].[ext]",
//打包后的存放位置
outputPath: "./images"
// 打包后文件的 url
publicPath: './images',
// 小于 100 字节转成 base64 格式
limit: 100
}
}
}
]
四、什么是Plugin
Plugin
(Plug-in)是一种计算机应用程序,它和主应用程序互相交互,以提供特定的功能
是一种遵循一定规范的应用程序接口编写出来的程序,只能运行在程序规定的系统下,因为其需要调用原纯净系统提供的函数库或者数据
webpack
中的plugin
也是如此,plugin
赋予其各种灵活的功能,例如打包优化、资源管理、环境变量注入等,它们会运行在 webpack
的不同阶段(钩子 / 生命周期),贯穿了webpack
整个编译周期
目的在于解决loader
无法实现的其他事
配置方式
这里讲述文件的配置方式,一般情况,通过配置文件导出对象中plugins
属性传入new
实例对象。如下所示:
const HtmlWebpackPlugin = require('html-webpack-plugin'); // 通过 npm 安装
const webpack = require('webpack'); // 访问内置的插件
module.exports = {
...
plugins: [
new webpack.ProgressPlugin(),
new HtmlWebpackPlugin({ template: './src/index.html' }),
],
};
五、Plugin的特性
其本质是一个具有apply
方法javascript
对象
apply
方法会被 webpack compiler
调用,并且在整个编译生命周期都可以访问 compiler
对象
const pluginName = 'ConsoleLogOnBuildWebpackPlugin';
class ConsoleLogOnBuildWebpackPlugin {
apply(compiler) {
compiler.hooks.run.tap(pluginName, (compilation) => {
console.log('webpack 构建过程开始!');
});
}
}
module.exports = ConsoleLogOnBuildWebpackPlugin;
compiler hook
的 tap
方法的第一个参数,应是驼峰式命名的插件名称
关于整个编译生命周期钩子,有如下:
- entry-option :初始化 option
- run
- compile: 真正开始的编译,在创建 compilation 对象之前
- compilation :生成好了 compilation 对象
- make 从 entry 开始递归分析依赖,准备对每个模块进行 build
- after-compile: 编译 build 过程结束
- emit :在将内存中 assets 内容写到磁盘文件夹之前
- after-emit :在将内存中 assets 内容写到磁盘文件夹之后
- done: 完成所有的编译过程
- failed: 编译失败的时候
六、常见的Plugin
常见的plugin
有如图所示:
下面介绍几个常用的插件用法:
HtmlWebpackPlugin
在打包结束后,⾃动生成⼀个 html
⽂文件,并把打包生成的js
模块引⼊到该 html
中
npm install --save-dev html-webpack-plugin
// webpack.config.js
const HtmlWebpackPlugin = require("html-webpack-plugin");
module.exports = {
...
plugins: [
new HtmlWebpackPlugin({
title: "My App",
filename: "app.html",
template: "./src/html/index.html"
})
]
};
<!--./src/html/index.html-->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title><%=htmlWebpackPlugin.options.title%></title>
</head>
<body>
<h1>html-webpack-plugin</h1>
</body>
</html>
在 html
模板中,可以通过 <%=htmlWebpackPlugin.options.XXX%>
的方式获取配置的值
更多的配置可以自寻查找
clean-webpack-plugin
删除(清理)构建目录
npm install --save-dev clean-webpack-plugin
const {CleanWebpackPlugin} = require('clean-webpack-plugin');
module.exports = {
...
plugins: [
...,
new CleanWebpackPlugin(),
...
]
}
mini-css-extract-plugin
提取 CSS
到一个单独的文件中
npm install --save-dev mini-css-extract-plugin
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = {
...,
module: {
rules: [
{
test: /.s[ac]ss$/,
use: [
{
loader: MiniCssExtractPlugin.loader
},
'css-loader',
'sass-loader'
]
}
]
},
plugins: [
...,
new MiniCssExtractPlugin({
filename: '[name].css'
}),
...
]
}
DefinePlugin
允许在编译时创建配置的全局对象,是一个webpack
内置的插件,不需要安装
const { DefinePlugun } = require('webpack')
module.exports = {
...
plugins:[
new DefinePlugin({
BASE_URL:'"./"'
})
]
}
这时候编译template
模块的时候,就能通过下述形式获取全局对象
<link rel="icon" href="<%= BASE_URL%>favicon.ico>"
copy-webpack-plugin
复制文件或目录到执行区域,如vue
的打包过程中,如果我们将一些文件放到public
的目录下,那么这个目录会被复制到dist
文件夹中
npm install copy-webpack-plugin -D
new CopyWebpackPlugin({
parrerns:[
{
from:"public",
globOptions:{
ignore:[
'**/index.html'
]
}
}
]
})
复制的规则在patterns
属性中设置:
- from:设置从哪一个源中开始复制
- to:复制到的位置,可以省略,会默认复制到打包的目录下
- globOptions:设置一些额外的选项,其中可以编写需要忽略的文件
七、Loader和Plugin的区别
上面我们有提到Loader
与Plugin
对应的概念,先来回顾下
- loader 是文件加载器,能够加载资源文件,并对这些文件进行一些处理,诸如编译、压缩等,最终一起打包到指定的文件中
- plugin 赋予了 webpack 各种灵活的功能,例如打包优化、资源管理、环境变量注入等,目的是解决 loader 无法实现的其他事
从整个运行时机上来看,如下图所示:
可以看到,两者在运行时机上的区别:
- loader 运行在打包文件之前
- plugins 在整个编译周期都起作用
在Webpack
运行的生命周期中会广播出许多事件,Plugin
可以监听这些事件,在合适的时机通过Webpack
提供的 API
改变输出结果
对于loader
,实质是一个转换器,将A文件进行编译形成B文件,操作的是文件,比如将A.scss
或A.less
转变为B.css
,单纯的文件转换过程
八、编写loader
在编写 loader
前,我们首先需要了解 loader
的本质
其本质为函数,函数中的 this
作为上下文会被 webpack
填充,因此我们不能将 loader
设为一个箭头函数
函数接受一个参数,为 webpack
传递给 loader
的文件源内容
函数中 this
是由 webpack
提供的对象,能够获取当前 loader
所需要的各种信息
函数中有异步操作或同步操作,异步操作通过 this.callback
返回,返回值要求为 string
或者 Buffer
代码如下所示:
// 导出一个函数,source为webpack传递给loader的文件源内容
module.exports = function(source) {
const content = doSomeThing2JsString(source);
// 如果 loader 配置了 options 对象,那么this.query将指向 options
const options = this.query;
// 可以用作解析其他模块路径的上下文
console.log('this.context');
/*
* this.callback 参数:
* error:Error | null,当 loader 出错时向外抛出一个 error
* content:String | Buffer,经过 loader 编译后需要导出的内容
* sourceMap:为方便调试生成的编译后内容的 source map
* ast:本次编译生成的 AST 静态语法树,之后执行的 loader 可以直接使用这个 AST,进而省去重复生成 AST 的过程
*/
this.callback(null, content); // 异步
return content; // 同步
}
一般在编写loader
的过程中,保持功能单一,避免做多种功能
如less
文件转换成 css
文件也不是一步到位,而是 less-loader
、css-loader
、style-loader
几个 loader
的链式调用才能完成转换
九、编写plugin
由于webpack
基于发布订阅模式,在运行的生命周期中会广播出许多事件,插件通过监听这些事件,就可以在特定的阶段执行自己的插件任务
在之前也了解过,webpack
编译会创建两个核心对象:
- compiler:包含了 webpack 环境的所有的配置信息,包括 options,loader 和 plugin,和 webpack 整个生命周期相关的钩子
- compilation:作为 plugin 内置事件回调函数的参数,包含了当前的模块资源、编译生成资源、变化的文件以及被跟踪依赖的状态信息。当检测到一个文件变化,一次新的 Compilation 将被创建
如果自己要实现plugin
,也需要遵循一定的规范:
- 插件必须是一个函数或者是一个包含
apply
方法的对象,这样才能访问compiler
实例 - 传给每个插件的
compiler
和compilation
对象都是同一个引用,因此不建议修改 - 异步的事件需要在插件处理完任务时调用回调函数通知
Webpack
进入下一个流程,不然会卡住
实现plugin
的模板如下:
class MyPlugin {
// Webpack 会调用 MyPlugin 实例的 apply 方法给插件实例传入 compiler 对象
apply (compiler) {
// 找到合适的事件钩子,实现自己的插件功能
compiler.hooks.emit.tap('MyPlugin', compilation => {
// compilation: 当前打包构建流程的上下文
console.log(compilation);
// do something...
})
}
}
在 emit
事件发生时,代表源文件的转换和组装已经完成,可以读取到最终将输出的资源、代码块、模块及其依赖,并且可以修改输出资源的内容
转载自:https://juejin.cn/post/7079765458416566303