前言
webpack 只认识 js 文件,像html 、css 、图片等都不认识的
Loader
帮助 webpack 将不同类型的文件 转化为 webpack 能识别的模块
loader的 分类 及 执行顺序
loader 有以下 4 种类型
类别 | 解释 | 类别 | 解释 |
---|---|---|---|
pre | 前置 loader | inline | 内联 loader |
nomal | 普通 loader | post | 后置 loader |
执行顺序分别是:
- pre > normal > inline > post
- 相同优先级的 loader 执行顺序( 如2个 normal 或 2个 inline)遵守
从右向左,从下到上
的原则
🌰:以下 3 个 loader( 默认没有配置 )都属于normal-loader
,执行顺序就是 loader3
-> loader2
-> loader1
js
module: {
rules: [
{
test: /\.js$/,
loader: 'loader1'
},
{
test: /\.js$/,
loader: 'loader2'
},
{
test: /\.js$/,
loader: 'loader3'
},
]
}
但有时候我们希望有些loader先执行,有些loader后执行,这时需要用到 enforce
属性把其定义成前置loader
或者 后置loader
。如果没有指定类型,那默认还是 normal-loader
🌰:此时的执行顺序就是 loader1
-> loader2
-> loader3
js
module: {
rules: [
{
test: /\.js$/,
loader: 'loader1',
enforce: 'pre'
},
{
test: /\.js$/,
loader: 'loader2'
},
{
test: /\.js$/,
loader: 'loader3',
enforce: 'post'
},
]
}
inline-loader
此时你发现没有使用 inline-loader ,其他 loader ( pre、nomal、post ) 都可以 配置使用
,inline-loader 顾名思义,需要内联使用
, 在每个 import
语句中显式指定 loader
🌰: 下面是 inline-loader 的使用方法,!
是loader之间的分隔符
, params
是传递给inline-loader2
的参数,我要用内联的方式显式指定两个loader 来处理./styles.css文件
js
import Styles from 'inline-loader1!inline-loader2?params!./styles.css
inline-loader 通过添加不同的前缀
,跳过其他类型的loader
这里其他类型指的是,你在配置文件webpack.config.js
里module.rules
中针对某类文件(例如.css)已经配置过的loader
!
:跳过 nomal-loader
js
import Styles from '!inline-loader1!inline-loader2?params!./styles.css
-!
:跳过 pre-loader 、nomal-loader
js
import Styles from '-!inline-loader1!inline-loader2?params!./styles.css
!!
:跳过 pre-loader 、nomal-loader 、post-loader
js
import Styles from '!!inline-loader1!inline-loader2?params!./styles.css
总结: 推荐配置方式,不推荐内联方式,因为不好复用,了解一下即可
创建一个最简单的loader
- 创建一个空项目,记得安装必须的依赖
yarn add webpack webpack-cli html-webpack-plugin -D
入口文件 main.js
写一行简单的代码
js
const message = 'Hello webpack-loader-plugin';
- 根目录下创建一个
loaders/simple-loader.js
的文件
js
module.exports = function(content) {
console.log(content);
return content;
}
loader 一共有 3 个参数,content
、 map
、meta
- content:文件内容
- map:与source-map相关
- meta:来自其他 loader(上一个)传递过来的参数
- 在
webpack.config.js
配置文件中引入 simple-loader.js
js
module: {
rules: [
{
test: /\.js$/,
loader: './loaders/simple-loader.js'
}
]
},
- 执行
npx webpack
打包命令,可以看到输出了main.js
中的内容
阶段总结
可见 loader 就是一个函数 ,当执行打包命令时,会执行 webpack.config.js 中所有 loader ,每个 loader 把自己监听的文件,当作参数传递进 loader 函数内 ,因为项目中只有一个 main.js
的 js 文件,故被当作 content
参数传递进了 simple-loader
函数内部,被打印了出来
4 种定义 loader 的方式
同步 loader
在 /loaders
目录下创建一个 sync-loader.js
的同步 loader 文件,同步 loader 有 两 种书写方式,第一种默认,第二种通过 this.callback()
可以把打包时的错误信息 、和 其他参数 一并传递出去
js
// 方式 1
module.exports = function(content) {
return content
}
// 方式 2
module.exports = function(content, map, meta) {
/**
* 参数1: err 代表错误信息
* 参数2: 文件内容
* 参数3: source-map相关
* 参数4: 其他 loader 传递过来的参数
*/
console.log('同步loader内部');
return this.callback(null, content, map, meta)
}
异步 loader
在 /loaders
目录下创建一个 async-loader.js
的异步 loader 文件,注意到 this.async()
这个方法,同时使用 setTimeout
模拟一个异步操作
js
module.exports = function(content, map, meta) {
const callback = this.async();
// 模拟异步操作
setTimeout(() => {
console.log('异步loader内部');
callback(null, content, map, meta)
}, 1000);
}
修改配置文件后,我们打包一下,看会输出什么
js
rules: [
// {
// test: /\.js$/,
// loader: './loaders/simple-loader.js'
// },
{
test: /\.js$/,
use: ['./loaders/sync-loader', './loaders/async-loader']
},
]
1 秒中之后控制台先输出了 async-loader
异步 loader 里的打印,又输出了sync-loader
同步 loader 里的打印
raw loader
在 /loaders
目录下创建一个 raw-loader.js
的 raw loader 文件,区别在于,content 将被转换为 Buffer数据流
, 同时在导出模块是添加 module.exports.raw = true
js
module.exports = function(content) {
// content 为 Buffer 数据流
console.log(content);
return content;
}
module.exports.raw = true;
修改配置文件,让我们打包看一下打印结果,输出了Buffer类型的数据
js
module: {
rules: [
// {
// test: /\.js$/,
// loader: './loaders/simple-loader.js'
// },
{
test: /\.js$/,
// use: ['./loaders/sync-loader', './loaders/async-loader']
use: ['./loaders/raw-loader']
},
]
}
当我们处理 图片 、字体图标 等文件时,可以使用 raw loader
pitch loader
在 /loaders
目录下创建一个 pitch-loader.js
的 pitch loader 文件
js
module.exports = function(content) {
console.log('pitch-loader1');
return content;
}
module.exports.pitch = function() {
console.log('pitch-fn-1');
}
我们看到 pitch loader 多了一个 pitch
方法,它是如何运行的那,我们知道当配置多个 loader 时,遵循 从右向左,从下到上
的规则 ,例如:['style-loader', 'css-loader']
当我们的 loader 配置了 pitch 方法后,将按照下图的方法顺序运行
测试一下
在 /loaders
目录下再 创建一个 pitch-loader2.js
js
module.exports = function(content) {
console.log('pitch-loader2');
return content;
}
module.exports.pitch = function() {
console.log('pitch-fn-2');
}
修改配置文件,让我们打包看一下输出结果
js
rules: [
{
test: /\.js$/,
use: ['./loaders/pitch-loader', './loaders/pitch-loader2']
},
]
结果如我们预期的一样,总结一下:先从左向右执行 pitch方法,再从右往左执行正常方法 ,❗️一旦任何一个 pitch 方法中执行 return
语句,整个链条就会截止到此方法,然后跳到 上一个 pitch
方法的正常方法
实战 clean-console-loader
下面我们创建一个真正具备功能意义上的loader,它有以下能力:
- 我们希望将 js 中的
console.log
语句全部干掉 - 动态添加作者信息
在 /loaders
目录下创建一个 clean-console-loader.js
,使用正则把 console.log
替空,通过 this.getOptions
和自定义的 schema 验证规则,获取 wepack.config / loader /options 中传入的属性
js
const schema = require('./schema.json');
module.exports = function(content) {
// 获取 配置文件中 options 中的选项
// schema 是对 options 的验证规则
// schema 要复合 json-schema 的规则
const opts = this.getOptions(schema);
const prefix = `
/**
* Auth: ${opts.author}
*/
`;
return prefix + content.replace(/console\.log\(.*\);?/g, '')
}
创建 /loaders/schema.json
,对 options 的验证规则
js
{
"type": "object",
"properties": {
"author": {
"type": "string"
}
},
"additionalProperties": false
}
修改 main.js 文件、配置文件
js
const message = 'Hello webpack-loader-plugin';
// 添加打印语句
console.log(1)
console.log(2)
console.log(3)
js
rules: [
{
test: /\.js$/,
loader: './loaders/clean-console-loader',
options: {
author: '田川_'
}
},
]
执行 npx webpack
后 dist/js/main.js
文件内没有任何 console.log
,同时动态添加了作者信息
Plugin
webpack 就像一条生产线,要经过一系列的处理,才能把源文件转化为输出结果
webpack 在执行时会创建 compiler
、 compilation
对象,此二对象身上有很多 hooks
钩子函数,这些生命周期(钩子)
组成了webpack的运行流程,
插件要做的就是,找到相应的钩子🪝,往上面挂上自己的任务,也就是注册事件 ,当 webpack 执行时就会自动触发
我们注册的事件
总之,插件就是,通过扩展wepack的能力,使webpack变得更强
创建第一个插件
创建 plugins/first-plugin.js
js
class FirstPlugin {
constructor() {
console.log('插件的构造函数打印')
}
apply(compiler) {
console.log('apply')
}
}
module.exports = FirstPlugin;
在 webpack.config.js
中使用插件
js
const FirstPlugin = require('./plugins/first-plugin.js');
...
{
plugins: [new FirstPlugin()]
}
执行 npx webpack
我们看到打印顺序
注册 compile 事件
以下生命周期钩子函数,是由 compiler
暴露, 可以通过如下方式访问
js
compiler.hooks.someHook.tap('MyPlugin', (params) => {
/* ... */
});
官方文档 中 compiler
有如此多 hooks,第一个钩子是 enviroment
由文档可知,environment
是同步钩子,所以要 tab
调用
调用方法 | 介绍 |
---|---|
tap | 同步 / 异步都可以 |
tapAsync | 异步 |
tapPromise | 异步 |
以下是一个 异步串行 的例子🌰
js
class FirstPlugin {
constructor() {
console.log('插件的构造函数打印')
}
apply(compiler) {
console.log('apply 方法执行');
// 由文档可知,environment 是同步钩子,所以要 tab 调用
compiler.hooks.environment.tap('FirstPlugin', () => {
console.log('environment钩子没有参数,每个🪝具体参数,见官方文档')
});
// 由文档可知,emit 是异步串行钩子
compiler.hooks.emit.tap('FirstPlugin', (compilation) => {
console.log('first-plugin emit 111')
});
compiler.hooks.emit.tapAsync('FirstPlugin', (compilation, callback) => {
setTimeout(() => {
console.log('first-plugin emit 222');
callback();
}, 2000);
});
compiler.hooks.emit.tapPromise('FirstPlugin', (compilation) => {
return new Promise((resolve) => {
setTimeout(() => {
console.log('first-plugin emit 333');
resolve();
}, 1000)
})
});
}
}
module.exports = FirstPlugin;
以下是一个 异步并行 的例子🌰
js
// 异步并行
compiler.hooks.make.tapAsync('FirstPlugin', (compilation, callback) => {
setTimeout(() => {
console.log('first-plugin make 111');
callback();
}, 3000);
});
compiler.hooks.make.tapAsync('FirstPlugin', (compilation, callback) => {
setTimeout(() => {
console.log('first-plugin make 222');
callback();
}, 1000);
});
compiler.hooks.make.tapAsync('FirstPlugin', (compilation, callback) => {
setTimeout(() => {
console.log('first-plugin make 333');
callback();
}, 2000);
});
注册 compilation 事件
由上文流程图可知, compilation
的执行阶段在 compile.afterCompile()
之前,也就是 compiler.make()
阶段才会生效,所以我们可以在 make
事件里 注册 compilation
的事件,以下是一个 seal
的例子
js
compiler.hooks.make.tapAsync('FirstPlugin', (compilation, callback) => {
compilation.hooks.seal.tap('FirstPlugin', () => {
console.log(' --- compilation.seal --- ')
})
setTimeout(() => {
console.log('first-plugin make 111');
callback();
}, 3000);
});
实战 sign-plugin
开发思路
- 需要打包输出前添加注释签名,需要使用
compiler.hooks.emit
钩子🪝,它是打包 📦 输出前触发,emit
是我们最后的机会
- 如何获取打包输出的资源,
compilation.assets
,可以获取所有即将输出的资源文件
创建 plugins/sign-plugin.js
js
class SignPlugin {
constructor(options = []) {
this.options = options
}
apply(compiler) {
compiler.hooks.emit.tap("SignPlugin", (compilation) => {
const extensions = ["css", "js"];
// 1. 筛选目标资源的扩展类型: compilation.assets
// 2. 过滤只保留 js和css资源
const assets = Object.keys(compilation.assets).filter((assetPath) => {
// 过滤代码略
const splitted = assetPath.split(".");
// 获取最后一个元素作为扩展名
const extension = splitted[splitted.length - 1];
// 判断是否为资源
return extensions.includes(extension);
});
const prefix = `/**
Author: ${this.options.author}
*/`
// 3. 遍历所有资源添加上注释
// console.log(assets);
assets.forEach((asset) => {
const source = compilation.assets[asset].source();
const content = prefix + source;
// ... some missing code ...
compilation.assets[asset] = {
// ... some missing code ...
source() {
return content;
},
// ... some missing code ...
size() {
return content.length;
},
};
});
});
}
}
module.exports = SignPlugin;
在 webpack.config.js
配置中使用插件
js
const SignPlugin = require('./plugins/sign-plugin.js');
plugins: [
new SignPlugin({
author: '田大大'
})
],
mode: 'production'
执行 npx webpack