Webpack的plugin和loader开发
官方中文文档 (opens new window),其实想要自己开发 webpack 的 loader 和 plugin,只需要查阅其文档,并有需要的查阅部分源码即可完成插件的开发。而在其正式打包成 bundle 前对文件进行处理的 plugin 甚至只需要在开始前的生命周期完成文件的处理即可。
# loader
Loader 的本质是将 node 不认识的文件资源、格式置换成 node 认识的模式,从而开发 loader 总是 从右到左被调用。有些情况下,loader 只关心 request 后面的 元数据(metadata),并且忽略前一个 loader 的结果。在实际(从右到左)执行 loader 之前,会先 从左到右 调用 loader 上的 pitch 方法。若在 pitch 中返回值会跳过余下的 loader,loader 会将 pitch 返回的值作为“文件内容”来处理,并返回给 webpack。
webpack -> compile -> 'loader'(资源)
# Loader 的种类
对单文件打包的方式的 loader 被称为行内(inline)loader,loader 被应用在 import/require 行内。inline loader 优先级高于 config 配置文件中的 loader。
所有一个接一个地进入的 loader,都有两个阶段,这两个阶段就很像 dom 的捕获冒泡:
- Pitching 阶段: loader 上的 pitch 方法,按照 后置(post)、行内(inline)、普通(normal)、前置(pre) 的顺序调用。
- Normal 阶段: loader 上的 常规方法,按照 前置(pre)、普通(normal)、行内(inline)、后置(post) 的顺序调用。模块源码的转换, 发生在这个阶段。
对于 rules 中的 loader,webpack 还定义了一个属性 enforce,可取值有 pre(为 pre loader)、post(为 post loader),如果没有值则为(normal loader)。所以 loader 在 webpack 中有 4 种:normal,inline,pre,post。
# 简易 Less-loader 和 Style-loader 联动
// 首先,webpack中做好配置
module: {
rules: [{
test: /\.less$/,
use: [
{ loader: 'style-loader' },
{ loader: 'less-loader', options: {
lessOptions: {
a: 1
}
} }
]
}]
},
2
3
4
5
6
7
8
9
10
11
12
13
14
Less Loader
const less = require("less");
// 异步loader - context是会将在webpack.config配置中匹配到的规则的文件内容整合在一起传入到自定义的less-loader中
// 走的是像gulp那样的pipeline
module.exports = function(context) {
const callback = this.async(); // 异步loader需要执行this。async通过callback进行回调
// this.getOptions() 可以拿到在loader中的options配置项,接受传入一个预设的值
// target node \ web
// console.log(this.getOptions(), this.target )
less.render(context, { sourceMap: {} }, function(err, output) {
const { css, map } = output;
// console.log(css, map)
callback(null, css, map);
});
};
module.exports.pitch = function(remainingRequest, precedingRequest, data) {
data.value = "cd";
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Style-loader : 拿到 lessloader 解析成的 css 插入到 html 中
// loader接下来拿到的resource, map 就会是先执行了 less-loader后传下来的东西
module.exports = function loader(resource, data) {
// console.log(resource, data)
let style = `
const style = document.createElement('style');
style.innerHTML = ${JSON.stringify(resource)};
document.head.appendChild(style);
`;
return style;
};
2
3
4
5
6
7
8
9
10
11
# Plugin
插件和 loader 其实可以干同样的事情,只是插件相对来说能力更强大。
插件里有非常重要的两个概念,需要区分清楚:
compiler 对象代表了完整的 webpack 环境配置。这个对象在启动 webpack 时被一次性建立,并配置好所有可操作的设置,包括 options,loader 和 plugin。当在 webpack 环境中应用一个插件时,插件将收到此 compiler 对象的引用。可以使用它来访问 webpack 的主环境。
compilation 对象代表了一次资源版本构建。当运行 webpack 开发环境中间件时,每当检测到一个文件变化,就会创建一个新的 compilation,从而生成一组新的编译资源。一个 compilation 对象表现了当前的模块资源、编译生成资源、变化的文件、以及被跟踪依赖的状态信息。compilation 对象也提供了很多关键时机的回调,以供插件做自定义处理时选择使用
下面是 webpack 执行时的流程生命周期
我们通过使用 compiler.hooks.xxx(生命周期).tap (tapAsync/tapPromise) 的时机来执行我们想要执行的 webpack 插件,通过查阅官方文档使用其对应 api 拿到想要的内容后开发完成我们需要的功能。
下面写两个 demo
- 将打包出来的文件转 zlib 压缩。需要的生命周期为 emit(输出 asset 到 output 目录之前执行。)这时候内容已经解析完毕。
const zlib = require("zlib");
const chalk = require("chalk");
function GzipPlugin() {}
GzipPlugin.option = {
level: 7,
};
GzipPlugin.prototype.apply = function(compiler) {
// compiler.hooks.run.tapPromise('MyPlugin', (source, target, routesList) => {
// return new Promise((resolve) => setTimeout(resolve, 1000)).then(() => {
// console.log('以异步的方式触发具有延迟操作的钩子。');
// });
// });
// compiler.hooks.run.tapPromise(
// 'MyPlugin',
// async (source, target, routesList) => {
// await new Promise((resolve) => setTimeout(resolve, 1000));
// console.log('以异步的方式触发具有延迟操作的钩子。');
// }
// );
compiler.hooks.emit.tap("MyPlugin", (compilation) => {
const assets = compilation.getAssets(); // 返回当前编译下所有资源的数组
// console.log(Array.from(compilation.fileDependencies), this.sourceDirectories)
for (const file of assets) {
if (/\.js$/.test(file.name)) {
const gzipFile = zlib.gzipSync(file.source._value, {
level: 7,
});
//添加一份需要被打包出来的文件 - 并按格式给到源头和文件的大小
compilation.assets[file.name + ".gz"] = {
source: function() {
return gzipFile;
},
size: function() {
return gzipFile.length;
},
};
}
}
});
};
module.exports = GzipPlugin;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
- 找到没有被用到的文件并输出其地址
const path = require("path");
const chalk = require("chalk");
// options为在webpack中添加的配置,一般写plugin时会为自己的plugin定义好options的写法和作用
// plugin的入口为apply ,会为其传入一个compiler
function UnUsePlugin(options) {
// 是否要根据源目录筛出未使用的文件
this.sourceDirectories = options.directories || [];
// 是否要将某些文件除外
this.exclude = options.exclude || [];
// 文件的根路径
this.root = options.root;
}
UnUsePlugin.prototype.apply = function(compiler) {
// 有异步的逻辑 要用tapAsync
compiler.hooks.emit.tapAsync("UnUsePlugin", (compilation, callback) => {
console.log(Array.from(compilation.fileDependencies)); //拿到所有被跟踪的依赖文件
// 他是一个文件类的伪数组,需要将其转换为真正的数组从而调用数组方法
const useModules = Array.from(compilation.fileDependencies)
.filter((file) =>
this.sourceDirectories.some((dir) => file.indexOf(dir) !== -1)
)
.reduce((obj, item) => Object.assign(obj, { [item]: true }), {});
Promise.all(
this.sourceDirectories.map((dir) => searchFiles(dir, this.exclude))
)
.then((files) =>
files.map((arr) => arr.filter((file) => !useModules[file]))
) // 筛出没有被引用的文件
.then(display.bind(this))
.then(callback);
});
};
const deglob = require("deglob");
// https://www.npmjs.com/package/deglob
// deglob帮我们从指定位置筛选出想要的文件 这里从directory目录下寻找**/* 的所有文件
/**
* 应返回是一个[[], [], []]格式的二维数组
*/
function searchFiles(directory, ignoreGlobPatterns = [], useGitIgnore = true) {
const config = { ignore: ignoreGlobPatterns, cwd: directory, useGitIgnore };
return new Promise((resolve, reject) => {
deglob("**/*", config, (err, files) => {
if (err) reject(err);
else resolve(files);
});
});
}
function display(filesDirectory) {
//将数组拍平
const allFiles = filesDirectory.reduce((arr, item) => item.concat(arr), []);
if (!allFiles.length) {
return [];
}
console.log("filesDirectory", filesDirectory);
process.stdout.write("\n");
process.stdout.write(chalk.green("\n*** Unused Plugin ***\n"));
process.stdout.write(
chalk.red(`${allFiles.length} unused source files found. \n`)
);
filesDirectory.forEach((files, index) => {
if (files.length === 0) return;
const directory = this.sourceDirectories[index];
const relative = this.root
? path.relative(this.root, directory)
: directory;
process.stdout.write(chalk.blue(`\n~~ ${relative}\n`));
files.forEach((file) =>
process.stdout.write(
chalk.yellow(` ~~${path.relative(directory, file)}\n`)
)
);
});
process.stdout.write(chalk.green("\n*** Unused Plugin ***\n"));
}
module.exports = UnUsePlugin;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85