# webpack-code-plugins
**Repository Path**: yan_guoping/webpack-code-plugins
## Basic Information
- **Project Name**: webpack-code-plugins
- **Description**: No description available
- **Primary Language**: Unknown
- **License**: Not specified
- **Default Branch**: master
- **Homepage**: None
- **GVP Project**: No
## Statistics
- **Stars**: 0
- **Forks**: 0
- **Created**: 2024-03-22
- **Last Updated**: 2024-03-25
## Categories & Tags
**Categories**: Uncategorized
**Tags**: None
## README
# webpack加载器和插件开发
### Plugin概述
* ##### 概念
> 插件是 webpack 生态的关键部分, 它为社区用户提供了一种强有力的方式来直接触及 webpack 的编译过程(compilation process)。 插件能够 hook 到每一个编译(compilation)中发出的关键事件中。 在编译的每个阶段中,插件都拥有对 compiler 对象的完全访问能力, 并且在合适的时机,还可以访问当前的 compilation 对象。
> webpack内置的大部分features, 都是依靠插件系统实现的。这使得webpack的扩展非常灵活
插件是webpack内部运行机制中最重要的一个组成部分,它提供了一种方式让程序直接触及到编译过程中的各个阶段,这种方式就是通过我们的hook函数,也就是我们俗称的钩子函数,在每一个编译阶段,我们都可以挂载钩子函数,对我们的 compiler 对象以及compilation 对象有一个完全的访问能力,通过这种能力,我们就可以实现webpack的一些基础功能,甚至,我们webpack内置的大部分features, 都是依靠插件系统实现的。这使得webpack的扩展非常灵活
* ##### 使用方法
* 安装
```sh
$ yarn add xxxx
```
* 使用
```js
const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
module.exports = {
entry: "./src/index.js",
output: {
path: path.resolve(__dirname, "dist"),
filename: "bundle.js",
},
mode: "development",
module: {
rules: [{ test: /\.txt$/, use: "raw-loader" }],
},
// plugins: [new HtmlWebpackPlugin()],
plugins: [new HtmlWebpackPlugin({ template: "./src/index.html" })],
};
```
* 执行时机
### 说来话长…………
* ##### webpack 插件组成:
1. 一个JavaScript命名函数或JavaScript类
2. 在插件函数的prototype上定义一个apply方法。如果是JavaScript类,需要在该类中定义一个apply方法
3. 指定一个绑定到webpack自身的事件钩子。
4. 处理webpack内部实例的特定数据。
5. 功能完成后调用webpack提供的回调。(可选)
hello-plugin
```js
//es5方式
// function HelloPlugin() {
// //构造函数constructor
// }
// HelloPlugin.prototype.apply = function (compiler) {
// compiler.hooks.done.tap("HelloPlugin", function (stats) {
// console.log("======[compiler.hooks.done.tap]", stats.hasErrors());
// });
// };
//es6方式
class HelloPlugin {
apply(compiler) {
compiler.hooks.done.tap("HelloPlugin", function (stats) {
console.log("======[compiler.hooks.done.tap]", stats.hasErrors());
});
}
}
module.exports = HelloPlugin;
```
> 一些说明:
>
> 1. apply被调用的时机
> 参见:https://github.com/webpack/webpack/blob/main/lib/webpack.js
* ##### webpack的编译流程
Compiler->Compilation->Resolver->Module Factory->js or json->loaders (if js is no,exec)->parser->template
1. Compiler:
webpack在开始编译时,做的第一件事情就是创建`Compiler`对象,并调用`run`方法。它是webpack的最顶层的对象,负责webpack的启动,停止,以及资源生成。其内部定义了一整套的hooks方法,来控制上述的流程
2. Compilation:
具体编译在这,在`Compiler`的`run`方法中,创建compilation对象。webpack编译的核心对象,在这个对象内执行的build方法,并最终生成了bundle文件
3. Resolver
我们在`webpack.config.js`文件定义入口文件,会传递给`Resolver`,`Resolver`会解析入口的相对路径,并转换成绝对路径,附带额外的信息,传递`module Factory`
4. Module Factory
`Module Factory`接收`Resolver`成功解析传递过来的`request`,并从中获取source code,创建Module实例。
5. Parser
`Parse`接收`module`解析的source code,并将其解析成抽象语法树(AST), 遍历AST,找到require或者import语法,创建一个依赖图,然后将依赖追加到`module`上
6. Template
被解析的`module`对象会传递给`template`,最终由`template`打包,生成bundle文件
首先,webpack会创建一个compiler对象,它这个对象是整个webpack的总指挥,它负责webpack的启动,停止,以及资源生成。
在`Compiler`的`run`方法中,会创建compilation对象,调用compilation编译,compilation是实际负责编译的。在编译的过程中,首先会webpack.config.js配置的入口文件传递给Resolver,`Resolver`会解析入口的相对路径,并转换成绝对路径,附带额外的信息,传递`module Factory`,module factory拿到这个全路径,并判断这个文件是js还是其他文件,如果是js,直接读取其内部的源代码,再传递给parser,如果不是,就要调用对应的loader再得到源代码,把源代码传递给parser后,会解析成AST语法树,遍历AST,找到require或者import语法,创建一个依赖图,然后将依赖追加到`module`上,module对象就有了所有源代码的依赖关系,`module`对象会传递给`template`,template会根据这些依赖关系打包,生成bundle文件
### 钩子方法和`Tapable`库
* ##### 钩子方法:
可以理解为前端框架中的生命周期函数,手动注册回调函数后,在对应的生命周期事件触发时,回调函数会被调用。webpack为我们提供了很多的钩子方法,可以让我们在webpack编译的各个阶段,有能力添加自己的功能。
下面对象分别对应了webpack编译处理的各个阶段
* Compiler
* Compilation
* Resolvers
* ContextModuleFactory
* NormalModuleFactory
* JavascriptParser
每个阶段都有若干个钩子函数
webpack的钩子实现是依赖`Tapable`库
* ##### [Tapable](https://github.com/webpack/tapable#tapable):
webpack内部所依赖的Hook库,其提供了多种Hook类,可以满足各种应用场景
* 安装
```sh
npm install --save tapable
```
* 使用
1. 声明Hook
```js
const { SyncHook } = require("tapable");
const hook = new SyncHook(['arg1', 'arg2', 'arg3']);
```
2. 注册Hook:之后就可以在该hook上绑定回调,从而监听事件触发
```js
hook.tap('pluginName', () => console.log('========== hook event triggered.'))
```
3. 触发hook事件
```js
hook.call('参数1', '参数2', '参数3')
```
* Hook类型
* syncHook
回调函数内部的处理为同步的,当注册之后,会根据注册的回调的顺序,逐个调用
```js
const { SyncHook } = require("tapable");
const hook = new SyncHook(["arg1", "arg2", "arg3"]);
hook.tap("plugin-1", (arg1, arg2, arg3) => {
console.log("plugin-1", arg1, arg2, arg3);
});
hook.tap("plugin-2", (arg1, arg2, arg3) => {
console.log("plugin-2", arg1, arg2, arg3);
});
hook.call(1, 2, 3);
console.log("所有hook都处理这个事件了");
```
* syncWaterfallHook 瀑布
调用方式与syncHook类似,也是回调内部是同步处理,根据注册的顺序调用回调函数,可以将前一个回调的结果,通过返回值的方式,传递给下一个hook
```js
//SyncWaterfallHook
const { SyncWaterfallHook } = require("tapable");
//创建hook
const hook = new SyncWaterfallHook(["arg1", "arg2", "arg3"]);
//注册钩子的回调
hook.tap("plugin-1", (arg1, arg2) => {
console.log("plugin-1", arg1, arg2);
return arg1 + arg2;
});
hook.tap("plugin-2", (arg1, arg2, arg3) => {
console.log("plugin-2", arg1, arg2, arg3); //3 2 3 如果上一个钩子有返回值,arg1是上一个钩子的返回值
});
//触发钩子事件
hook.call(1, 2, 3);
console.log("所有hook都处理这个事件了");
```
* syncBailHook 离开
与syncHook,回调内是同步处理,回调的执行,也是安装注册的顺序逐个调用,前一个回调函数一旦返回了任何值(undefined除外), 后面所有的回调都会被终止
```js
//SyncWaterfallHook
const { SyncBailHook } = require("tapable");
//创建hook
const hook = new SyncBailHook(["arg1", "arg2", "arg3"]);
//注册钩子的回调
hook.tap("plugin-1", (arg1, arg2) => {
console.log("plugin-1", arg1, arg2);
return arg1 + arg2;
});
hook.tap("plugin-2", (arg1, arg2, arg3) => {
console.log("plugin-2", arg1, arg2, arg3); //3 2 3 如果上一个钩子有返回值,arg1是上一个钩子的返回值
});
//触发钩子事件
hook.call(1, 2, 3);
console.log("只有plugin-1的hook处理这个事件了");
```
* asyncParallelHook 并行 (哪个先,就哪个先执行)
回调函数内部处理包含异步处理,各个回调函数的异步处理,是并行触发的。
```js
//AsyncParallelHook tapAsync
// const { AsyncParallelHook } = require("tapable");
// //创建hook
// const hook = new AsyncParallelHook(["arg1", "arg2", "arg3"]);
// //注册钩子的回调 hook.tapAsync hook.tapPromise
// hook.tapAsync("plugin-1", (arg1, arg2, arg3, callback) => {
// console.log("plugin-1", arg1, arg2, arg3);
// setTimeout(() => {
// console.log("plugin-1结束了");
// //处理结束,一定要调用这个回调函数
// callback();
// }, 1000);
// });
// hook.tapAsync("plugin-2", (arg1, arg2, arg3, callback) => {
// console.log("plugin-2", arg1, arg2, arg3);
// setTimeout(() => {
// console.log("plugin-2结束了");
// //处理结束,一定要调用这个回调函数
// callback();
// }, 500);
// });
// //触发钩子事件 callAsync
// hook.callAsync(1, 2, 3, (err) => {
// //异步触发钩子时,回调被调用,才意味着所有的异步处理都完成了
// console.log(err);
// console.log("所有的结束了");
// });
// console.log("嘿嘿");
//AsyncParallelHook tapPromise
const { AsyncParallelHook } = require("tapable");
//创建hook
const hook = new AsyncParallelHook(["arg1", "arg2", "arg3"]);
//注册钩子的回调 hook.tapAsync hook.tapPromise
hook.tapPromise("plugin-1", (arg1, arg2, arg3) => {
console.log("plugin-1", arg1, arg2, arg3);
return new Promise((resolve) => {
setTimeout(() => {
console.log("plugin-1结束了");
//处理结束,一定要调用这个回调函数
resolve();
}, 1000);
});
});
hook.tapPromise("plugin-2", (arg1, arg2, arg3) => {
console.log("plugin-2", arg1, arg2, arg3);
return new Promise((resolve) => {
setTimeout(() => {
console.log("plugin-2结束了");
//处理结束,一定要调用这个回调函数
resolve();
}, 500);
});
});
hook.tapPromise("plugin-3", async (arg1, arg2, arg3) => {
console.log("plugin-3", arg1, arg2, arg3);
await sleep(3000);
console.log("plugin-3结束了");
});
//触发钩子事件 callAsync
hook.callAsync(1, 2, 3, (err) => {
//异步触发钩子时,回调被调用,才意味着所有的异步处理都完成了
console.log(err);
console.log("所有的结束了");
});
function sleep(ms) {
return new Promise((resolve) => {
setTimeout(() => {
resolve();
}, ms);
});
}
console.log("嘿嘿");
```
* asyncSeriesHook 串行
回调函数内部处理包含异步处理,各个回调函数的异步处理的方式,是前一个异步处理完成之后,开始调用下一个回调。
```js
//AsyncSeriesHook tapPromise
const { AsyncSeriesHook } = require("tapable");
//创建hook
const hook = new AsyncSeriesHook(["arg1", "arg2", "arg3"]);
//注册钩子的回调 hook.tapAsync hook.tapPromise
hook.tapPromise("plugin-1", (arg1, arg2, arg3) => {
console.log("plugin-1", arg1, arg2, arg3);
return new Promise((resolve) => {
setTimeout(() => {
console.log("plugin-1结束了");
//处理结束,一定要调用这个回调函数
resolve();
}, 1000);
});
});
hook.tapPromise("plugin-2", (arg1, arg2, arg3) => {
console.log("plugin-2", arg1, arg2, arg3);
return new Promise((resolve) => {
setTimeout(() => {
console.log("plugin-2结束了");
//处理结束,一定要调用这个回调函数
resolve();
}, 500);
});
});
hook.tapPromise("plugin-3", async (arg1, arg2, arg3) => {
console.log("plugin-3", arg1, arg2, arg3);
await sleep(3000);
console.log("plugin-3结束了");
});
//触发钩子事件 callAsync
hook.callAsync(1, 2, 3, (err) => {
//异步触发钩子时,回调被调用,才意味着所有的异步处理都完成了
console.log(err);
console.log("所有的结束了");
});
function sleep(ms) {
return new Promise((resolve) => {
setTimeout(() => {
resolve();
}, ms);
});
}
console.log("嘿嘿");
```
* asyncSeriesWaterfallHook 串行可传值
处理流程与asyncSeries类似,但是前一个回调的结果,可以传递给后一个回调函数。(在resolve或callback来实现,但是要记住callback(null,回调的结果),callback第一个参数代表的是错误)
```js
//arg1 arg2 arg3 如果上一个钩子有返回值,arg1是上一个钩子的返回值
```
* webpack中的Hook
* [compiler](https://webpack.js.org/api/compiler-hooks/)
* thisCompilation([触发时机](https://github.com/webpack/webpack/blob/4b4ca3bb53f36a5b8fc6bc1bd976ed7af161bd80/lib/Compiler.js#L1117))
compilation对象被初始化的时候调用。在compilation事件触发前。
* compilation
compilation对象创建后,执行plugin
* emit([触发时机](https://github.com/webpack/webpack/blob/4b4ca3bb53f36a5b8fc6bc1bd976ed7af161bd80/lib/Compiler.js#L891))
在资源被放置到output路径之前调用。(通常对编译后的文件进行处理时,监听该事件)
* done
编译结束时会被执行。
* [compilation](https://webpack.js.org/api/compilation-hooks/)
* optimize([触发时机](https://github.com/webpack/webpack/blob/4b4ca3bb53f36a5b8fc6bc1bd976ed7af161bd80/lib/Compilation.js#L2947))
在优化的开始阶段被触发
* optimizeAssets
将`compilation.assets`中的资源进行优化。
### Plugin源码解读
* ##### [clean-webpack-plugin](https://www.npmjs.com/package/clean-webpack-plugin)
1. 概述
> A webpack plugin to remove/clean your build folder(s).
2. 使用
1. 安装
```sh
$ yarn add -D clean-webpack-plugin
```
2. 使用
```js
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
module.exports = {
plugins: [
new CleanWebpackPlugin(),
],
};
```
3. 下载源码
[GitHub](https://github.com/johnagan/clean-webpack-plugin)
4. 源码解读
apply
-> compiler.hooks.emit.tap -> this.handleInitial() -> this.removeFiles
-> compiler.hooks.done.tap -> this.handleDone() -> this.removeFiles
> handleInitial()只执行一次(通过this.initialClean控制的)
> handleDone() 回比较前一次编译与当前编译的资源,并找出前一次有但是当前这次没有的,作为删除对象
5. 造轮子
```sh
yarn add del
```
##### [copy-webpack-plugin](https://www.npmjs.com/package/copy-webpack-plugin)
1. 概述
> Copies individual files or entire directories, which already exist, to the build directory.
1. 使用
1. 安装
```sh
$ yarn add -D copy-webpack-plugin
```
2. 使用
```js
const CopyPlugin = require("copy-webpack-plugin");
module.exports = {
plugins: [
new CopyPlugin({
patterns: [{ from: "./assets/1.txt", to: "assets" }],
}),
],
};
```
2. 下载源码
[GitHub](https://github.com/webpack-contrib/copy-webpack-plugin)
3. 源码解读
```txt
apply
-> compiler.hooks.thisCompilation.tap
-> compilation.hooks.processAssets.tapAsync
-> 遍历patters数组
-> runPatterns() //根据from的类型(文件,文件夹),生成一个数组,该数组中包含了所有要拷贝的文件的路径
-> 待拷贝文件与生成文件是否重复 ? updateAsset : emitAsset
```
2. 造轮子
实现思路:
1、apply中,绑定hooks(资源生成后,可以拿到被生成的资源,查看是否与拷贝的资源重复,compiler.hooks.thisCompilation—>compilation.hooks.processAssets)
2、遍历patterns
3、from是文件夹还是文件,如果是文件我们就调用copyFile—>compilation.emitAsset,如果是文件夹,我们就调用copyDir—>递归的找到文件夹下的所有文件(globby)—>对每一个文件逐个的调用copyFile
```sh
yarn add globby
```
my-copy-plugin.js
```js
const path = require("path");
class MyCopyWebpackPlugin {
constructor(options) {
this.compiler = null;
this.patterns = options.patterns;
}
// 接收相对路径,返回绝对路径
resolvePath(relativePath) {
return path.resolve(this.compiler.options.context, relativePath);
}
isDir(absPath) {
//webpack自身维护了一个方法,this.compiler.inputFileSystem.stat,判断文件的类别,异步函数
const {
inputFileSystem: { stat },
} = this.compiler;
return new Promise((resolve, reject) => {
stat(absPath, (err, stats) => {
if (err) {
reject(err);
return;
}
if (stats.isDirectory()) {
resolve(true);
} else if (stats.isFile()) {
resolve(false);
} else {
reject("unknown error");
}
});
});
}
//拷贝文件
copyFile(compilation, absFrom, to) {
console.log(absFrom, to, "copyFile");
//readFile异步函数
const {
inputFileSystem: { readFile },
} = this.compiler;
//通过this.compiler.webpack.sources可以得到RawSource
const { RawSource } = this.compiler.webpack.sources;
return new Promise((resolve, reject) => {
//如果要拷贝的文件在已生成的文件中存在,直接return,避免把生成的文件覆盖掉了
const existedAsset = compilation.getAsset(to);
console.log(existedAsset, "existedAsset");
if (existedAsset) {
// compilation.updateAsset(...);
return;
}
readFile(absFrom, (err, data) => {
if (err) {
reject(err);
return;
}
const source = new RawSource(data);
//如果我们要需要通过this.emitAssets来生成文件,必须将我们的asset转成RawSource
compilation.emitAsset(to, source);
resolve();
});
});
}
//拷贝文件夹
async copyDir(compilation, absFrom, to) {
// let globby; //是ES module需要动态导入,动态导入需要使用await **/*
// path.join(pathFrom,'**/*') 代表的是pathFrom下的所有文件夹的所有文件
try {
const { globby } = await import("globby");
let glob = path.join(absFrom, "**/*");
//需要对系统做兼容 darwin win32
if (process.platform === "win32") {
glob = glob.split(path.sep).join("/");
}
const globEntries = await globby(glob, {
objectMode: true, //返回值是一个对象形式
});
console.log(globEntries, "globEntries");
await Promise.all(
globEntries.map(async (globEntry) => {
//后者相对于前者的绝对路径
let pathTo = path.relative(absFrom, globEntry.path);
pathTo = path.join(to, pathTo);
//需要对系统做兼容 darwin win32
// if (process.platform === "win32") {
// pathTo = pathTo.split(path.sep).join("/");
// }
await this.copyFile(compilation, globEntry.path, pathTo);
})
);
} catch (error) {
console.log(error);
return;
}
}
apply(compiler) {
this.compiler = compiler;
//上下文 这是webpack.config.js所在的绝对路径
// console.log(compiler.options.context, "上下文");
compiler.hooks.thisCompilation.tap("MyCopyWebpackPlugin", (compilation) => {
compilation.hooks.processAssets.tapAsync(
{ name: "MyCopyWebpackPlugin" },
async (assets, cb) => {
this.patterns.forEach(async (pattern) => {
console.log(pattern, "pattern");
// from是相对路径,需要转成绝对路径,和上下文一拼接就是绝对路径了
const { from, to } = pattern;
const absFrom = this.resolvePath(from);
try {
const isDir = await this.isDir(absFrom);
const pathTo = path.join(to, path.basename(absFrom));
if (isDir) {
//文件夹
this.copyDir(compilation, absFrom, pathTo);
} else {
//文件 待拷贝文件与生成文件是否重复 ? updateAsset : emitAsset
await this.copyFile(compilation, absFrom, pathTo);
}
} catch (error) {
console.log(error);
}
});
//一定要调用这个回调
cb();
}
);
});
}
}
module.exports = MyCopyWebpackPlugin;
```
webpack.config.js
```js
const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
// const CopyPlugin = require("copy-webpack-plugin");
// const { CleanWebpackPlugin } = require("clean-webpack-plugin");
const HelloPlugin = require("./plugins/hello-plugin");
const MyCleanPlugin = require("./plugins/my-clean-plugin");
const MyCopyWebpackPlugin = require("./plugins/my-copy-plugin");
module.exports = {
entry: "./src/index.js",
output: {
path: path.resolve(__dirname, "dist"),
filename: "bundle.[contenthash].js",
clean: true,
},
// mode: "development",
module: {
rules: [{ test: /\.txt$/, use: "raw-loader" }],
},
// plugins: [new HtmlWebpackPlugin()],
plugins: [
new HtmlWebpackPlugin({ template: "./src/index.html" }),
// new HelloPlugin(),
// new CleanWebpackPlugin(),
// new MyCleanPlugin(),
new MyCopyWebpackPlugin({
patterns: [
{ from: "./assets/1.txt", to: "assets" },
{ from: "./assets/2", to: "assets" },
{ from: "./assets/bundle.js", to: "" },
],
}),
// new CopyPlugin({
// patterns: [{ from: "./assets/1.txt", to: "assets" }],
// }),
],
};
```
遇到了两个问题,一个是Windows和mac的兼容问题,解决
```js
//需要对系统做兼容 darwin win32
if (process.platform === "win32") {
glob = glob.split(path.sep).join("/");
}
```
还有一个问题就是webpack.config.js的mode设置成了development就copy失效,注释就好了,后面发现只是在mode:'development'不能使用,改成mode:'production'既然可以
zip-assets-plugin
是在编译的时候,将所有的文件打包成一个zip文件,可使在网络环境不顺畅的时候减少请求次数和下载流量,提高页面的加载速度
需求分析:
插件支持参数,指定打包时的文件名
打包所有的生成资源文件为zip,解压之后,其内部的文件结构,要与output路径的文件结构一致。
处理流程:
1、得到所有webpack生成的资源,在compiler.hooks.emit
2、将资源文件进行打包成zip
3、将zip的buffer内容,交给webpack进行管理,最终由webpack,通过emitAssets,将zip文件写到output的路径中
资源已经生成,但是还没有写到output(webpack)—>拿到生成资源—>执行上面的1、2、3步—>写资源到output路径(webpack)
jszip
```sh
yarn add jszip -D
```
zip-assets-plugin
```js
const JSZIP = require("jszip");
class ZipAssetsPlugin {
constructor(options) {
this.filename = options.filename;
}
apply(compiler) {
console.log("ZipAssetsPlugin", this.filename);
compiler.hooks.emit.tapAsync("ZipAssetsPlugin", async (compilation, cb) => {
const assets = compilation.assets;
const zip = new JSZIP();
for (const filename in assets) {
// 得到webpack打包后的资源
const source = assets[filename].source();
console.log(source, "source");
zip.file(filename, source);
}
const zipBuffer = await zip.generateAsync({
type: "nodebuffer",
});
//通过compiler.webpack.sources可以得到RawSource
const { RawSource } = compiler.webpack.sources;
// 将zip生成的buffer交由webpack处理,webpack会在emitAssets的时候,将zipBuffer写到output
compilation.assets[`${this.filename}.zip`] = new RawSource(zipBuffer);
console.log(zipBuffer, "zipBuffer");
//忘记掉下面的这个回调函数,webpack的处理会被中断
cb();
});
}
}
module.exports = ZipAssetsPlugin;
```
