# 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; ```