看了入口文件之后,从compiler.run开始就一直是在调用不同的钩子函数,钩子函数执行到afterDone
之后就构建完成了,这...
看起来毫无厘头,因此在网上查了资料,才发现原来,webpack的核心库用了Tapable
,tapable
是在webpack
打包过程中,控制打包在什么阶段调用Plugin的库。
学习tabable对我们学习会写插件很有用!!!
目标:学习
tapable
,写一个简单的webpack插件(将打包好的dist目录存到本地一份,备份dist目录,防止打包后之前打包的文件被重写掉)。
Tapable简单介绍:文档链接,使用和相关例子也可以在这上面找。或者看大佬的这篇文章戳我。
Hook类型
基本的钩子:除下面三个的钩子之外的所有
waterfall:调用每个tap传进来的函数,不同的是它会从每一个函数传一个返回的值给下一个函数。return的值是下一个接受的值
Bail:允许更早的退出,当某个tap进去的函数返回任何值,bail会停止其他函数的执行。return之后不会向下执行
Loop:todo如果某个tap事件有返回值,则会循环之前执行的事件
三种注册方式:
tap:生产同步钩子
tapAsync:生产带callback回调的异步钩子
tapPromise:生产带promise回调的异步钩子
三种调用方式:
call:
callAsync
promise
串行和并行:
并行:Parallel。举例:AsyncParallelHook(异步并行)
串行:Series。举例:AsyncSeriesHook(异步串行)
接下来看一下webpack的打包流程图:
执行流程图:
官网对这些钩子的介绍官网对钩子不同钩子的介绍
举例一些简单的compiler钩子:
钩子 | 说明 | 同步or异步 | |
---|---|---|---|
beforeRun | 在开始执行一次构建之前调用,compiler.run 方法开始执行后立刻进行调用。 | AsyncSeriesHook | 异步串行 |
shouldEmit | 在输出 asset 之前调用。返回一个布尔值,告知是否输出。 | SyncBailHook | 同步 |
emit | 输出 asset 到 output 目录之前执行。这个钩子 不会 被复制到子编译器 | AsyncSeriesHook | 异步串行 |
afterEmit | 输出 asset 到 output 目录之后执行。这个钩子 不会 被复制到子编译器。 | AsyncSeriesHook | 异步串行 |
从上面3个emit钩子大概可以看出来,这三个钩子是在打包资源时触发的。因此猜想,我们想在打包中做一些事情就是在这些钩子的回调函数中进行各种操作。
证明猜想第一步:
在webpack.js中,创建compiler时new了一个WebpackOptionsApply
,并且调用了其process
方法,参数为处理后的配置项的config(即为options),第二个参数为compiler。
mytest\node_modules\webpack\lib\webpack.js
const createCompiler = rawOptions => {
const options = getNormalizedWebpackOptions(rawOptions);
applyWebpackOptionsBaseDefaults(options);
const compiler = new Compiler(options.context, options);
new NodeEnvironmentPlugin({
infrastructureLogging: options.infrastructureLogging
}).apply(compiler);
if (Array.isArray(options.plugins)) {
for (const plugin of options.plugins) {
if (typeof plugin === "function") {
plugin.call(compiler, compiler);
} else {
plugin.apply(compiler);
}
}
}
applyWebpackOptionsDefaults(options);
compiler.hooks.environment.call();
compiler.hooks.afterEnvironment.call();
new WebpackOptionsApply().process(options, compiler);
compiler.hooks.initialize.call();
return compiler;
};
第二步:
找到WebpackOptionsApply.js,找一个配置项中必填的entry选项,搜索entry,可以看到引入了一个EntryOptionPlugin。因此可以说明,我们所有的配置项都是通过插件的方式注册进去进行处理的。
mytest\node_modules\webpack\lib\WebpackOptionsApply.js
const EntryOptionPlugin = require("./EntryOptionPlugin");
//注册devtool的相关内容
if (options.devtool) {
if (options.devtool.includes("source-map")) {
const hidden = options.devtool.includes("hidden");
const inline = options.devtool.includes("inline");
const evalWrapped = options.devtool.includes("eval");
const cheap = options.devtool.includes("cheap");
const moduleMaps = options.devtool.includes("module");
const noSources = options.devtool.includes("nosources");
const Plugin = evalWrapped
? require("./EvalSourceMapDevToolPlugin")
: require("./SourceMapDevToolPlugin");
new Plugin({
filename: inline ? null : options.output.sourceMapFilename,
moduleFilenameTemplate: options.output.devtoolModuleFilenameTemplate,
fallbackModuleFilenameTemplate:
options.output.devtoolFallbackModuleFilenameTemplate,
append: hidden ? false : undefined,
module: moduleMaps ? true : cheap ? false : true,
columns: cheap ? false : true,
noSources: noSources,
namespace: options.output.devtoolNamespace
}).apply(compiler);
} else if (options.devtool.includes("eval")) {
const EvalDevToolModulePlugin = require("./EvalDevToolModulePlugin");
new EvalDevToolModulePlugin({
moduleFilenameTemplate: options.output.devtoolModuleFilenameTemplate,
namespace: options.output.devtoolNamespace
}).apply(compiler);
}
}
写一个自己的webpack插件
准备工作:
- 新建一个目录webpack-plugin-test
- 执行
npm init -y
,npm install 安装webpack和webpack-cli - package.json scripts中配置
"build": "webpack --mode production"
- 准备入口文件和plugin文件,webpack.config.js。新建main.js(入口文件中可以什么都不写空的即可),plugin.js
//webpack.config
const MyPlugin = require("./plugin");
var path = require("path");
module.exports = {
entry: "./main.js",//入口文件
output: {//出口
path: path.join(__dirname, "./dist"),
filename: "bundle.js",
},
plugins: [
//自定义插件
new MyPlugin({
path: path.resolve(__dirname, "../../../"),
}),
],
};
我在上面new Plugin时获取的是我的D:\workspace,通过path.resolve(__dirname, "../../../")
获取到本地的workspace目录,预期结果就会是在当前项目目录下会打包出来一个dist目录,在我的D:\workspace也有一个项目的dist目录,目录内包含内容相同。
//plugin.js
//引入node-fs模块
const fs = require("fs");
const path = require("path");
const filePath = path.resolve("./dist");
const pluginName = "MyPlugin";
class MyPlugin {
//获取配置项-因为plugin是new出来的,可以直接在构造函数中获取
constructor(options) {
this.options = options;
this.writePath = path.join(options.path, "dist");
}
apply(compiler) {
//在assets被输出后执行
compiler.hooks.afterEmit.tap(pluginName, (compilation) => {
// console.log(config);
//读取dist目录,并且遍历下面的文件&目录,如果是文件就创建一个文件将内容输出到指定的目录下新创建的文件内,如果是目录就创建目录
fs.readdir(filePath, (err, files) => {
if (err) {
return new Error("cannot resolve this path" + filePath);
} else {
//创建当前目录同名目录
fs.mkdir(this.writePath, {}, (err, directory) => {
files.forEach((file) => {
var filedir = path.join(filePath, file);
//判断文件类型
fs.stat(filedir, (err, stats) => {
if (!err) {
//文件类型
var isFile = stats.isFile();
//目录类型
let isFolder = stats.isDirectory();
if (isFile) {
fs.readFile(filedir, (err, fileData) => {
if (err) throw err;
// console.log(`${writePath}/${file}`);
fs.writeFile(
`${this.writePath}/${file}`,
fileData,
(err) => {
console.log("The file has been saved!");
}
);
});
}
if (isFolder) {
fs.mkdir(`${this.writePath}/${file}`, (err, directory) => {
console.log("The directory has been created!");
});
}
}
});
});
});
}
});
});
}
}
module.exports = MyPlugin;
执行npm run build
进行打包。
test代码git链接。