diff --git a/blog/webpack/img/html-webpack-plugin-flow.png b/blog/webpack/img/html-webpack-plugin-flow.png new file mode 100644 index 0000000000000000000000000000000000000000..8420d7c67d4c9fb59aca1370dd8c91de541e2bdc Binary files /dev/null and b/blog/webpack/img/html-webpack-plugin-flow.png differ diff --git a/blog/webpack/img/plugin.jpg b/blog/webpack/img/plugin.jpg new file mode 100644 index 0000000000000000000000000000000000000000..262e1a43649eacaa4c6f63ccf967cf285414a144 Binary files /dev/null and b/blog/webpack/img/plugin.jpg differ diff --git a/blog/webpack/webpack-loader.md b/blog/webpack/webpack-loader.md index 6cf3d0e80b435b76d1ac0ddb4c8c3a43a26db16b..a4feb1d864997aa6342e86a273c6e883839523cf 100644 --- a/blog/webpack/webpack-loader.md +++ b/blog/webpack/webpack-loader.md @@ -1,5 +1,7 @@ # Webpack loader +文档:[https://webpack.docschina.org/api/loaders](https://webpack.docschina.org/api/loaders) + loader 将不同类型的文件转换为 webpack 可识别的模块 ## loader 使用方式 @@ -624,4 +626,3 @@ module.exports = { }; ``` -https://www.bilibili.com/video/BV14T4y1z7sw?p=77&spm_id_from=pageDriver \ No newline at end of file diff --git a/blog/webpack/webpack-plugin.md b/blog/webpack/webpack-plugin.md new file mode 100644 index 0000000000000000000000000000000000000000..f2d01d1248415b7672101b4f6bbfb6255accd1e3 --- /dev/null +++ b/blog/webpack/webpack-plugin.md @@ -0,0 +1,579 @@ +# webpack-plugin + +插件作用: 扩展 webpack + +文档:[https://webpack.docschina.org/api/plugins](https://webpack.docschina.org/api/plugins) + +### 钩子 + +钩子的本质就是:事件 + +### Tapable + +Tapable 为 webpack 提供了统一的插件接口(钩子)类型定义 + +统一暴露了三个方法给插件,用于注入不同类型的自定义构建行为: + +- tap:可以注册同步钩子和异步钩子。 +- tapAsync:回调方式注册异步钩子。 +- tapPromise:Promise 方式注册异步钩子。 + + +### compiler + +compiler 对象中保存着完整的 Webpack 环境配置,每次启动 webpack 构建时它都是一个独一无二,仅仅会创建一次的对象。 + +主要属性: + +- compiler.options 可以访问本次启动 webpack 时候所有的配置文件,包括但不限于 loaders 、 entry 、 output 、 plugin 等等完整配置信息。 +- compiler.inputFileSystem 和 compiler.outputFileSystem 可以进行文件操作,相当于 Nodejs 中 fs。 +- compiler.hooks 可以注册 tapable 的不同种类 Hook,从而可以在 compiler 生命周期中植入不同的逻辑。 + +### Compilation + +compilation 对象代表一次资源的构建,compilation 实例能够访问所有的模块和它们的依赖。 + +主要属性: + +- compilation.modules 可以访问所有模块,打包的每一个文件都是一个模块。 +- compilation.chunks chunk 即是多个 modules 组成而来的一个代码块。入口文件引入的资源组成一个 chunk,通过代码分割的模块又是另外的 chunk。 +- compilation.assets 可以访问本次打包生成所有文件的结果。 +- compilation.hooks 可以注册 tapable 的不同种类 Hook,用于在 compilation 编译模块阶段进行逻辑添加以及修改。 + +### 生命周期简图 + +![](img/plugin.jpg) + +## 第一个插件 + +执行流程: + +1. webpack 加载 webpack.config.js 中的所有配置,此时就会`new TestPlugin()`,执行插件的`constructor` +2. webpack 创建 compiler 对象 +3. 遍历所有 plugins 中的插件,调用插件的 apply 方法 +4. 执行剩下的编译流程,触发各个 hooks 时间 + +```js +// plugins/test-plugin.js +class TestPlugin { + constructor() { + console.log("TestPlugin constructor"); + } + + apply(compiler) { + console.log("TestPlugin apply"); + } +} + +module.exports = TestPlugin; +``` + +使用插件 + +```js +// webpack.config.js +const path = require("path"); +const TestPlugin = require("./plugins/test-plugin.js"); + +module.exports = { + entry: "./src/index.js", + + output: { + path: path.resolve(__dirname, "dist"), + filename: "bundle.js", + clean: true, + }, + + module: {}, + + plugins: [new TestPlugin()], + + mode: "development", +}; +``` + +compiler 钩子: [https://webpack.docschina.org/api/compiler-hooks/](https://webpack.docschina.org/api/compiler-hooks/) + +注册钩子 + +```js +// plugins/test-plugin.js +class TestPlugin { + constructor() { + console.log("TestPlugin constructor"); + } + + apply(compiler) { + console.log("TestPlugin apply"); + + // 从文档可知, environment hook 是 SyncHook, + // 也就是同步钩子, 只能用tap注册 + compiler.hooks.environment.tap("TestPlugin", (compilationParams) => { + console.log("compiler.environment()"); + }); + + // 从文档可知, emit 是 AsyncSeriesHook, + // 也就是异步串行钩子,特点就是异步任务顺序执行 + compiler.hooks.emit.tap("TestPlugin", (compilation) => { + console.log("compiler.emit() 111"); + }); + + compiler.hooks.emit.tapAsync("TestPlugin", (compilation, callback) => { + setTimeout(() => { + console.log("compiler.emit() 222"); + callback(); + }, 2000); + }); + + compiler.hooks.emit.tapPromise("TestPlugin", (compilation) => { + return new Promise((resolve) => { + setTimeout(() => { + console.log("compiler.emit() 333"); + resolve(); + }, 1000); + }); + }); + + // 从文档可知, make 是 AsyncParallelHook, + // 也就是异步并行钩子, 特点就是异步任务同时执行 + // 可以使用 tap、tapAsync、tapPromise 注册。 + // 如果使用tap注册的话,进行异步操作是不会等待异步操作执行完成的。 + compiler.hooks.make.tap("TestPlugin", (compilation) => { + setTimeout(() => { + console.log("compiler.make() 111"); + }, 2000); + }); + + // 使用tapAsync、tapPromise注册,进行异步操作会等异步操作做完再继续往下执行 + compiler.hooks.make.tapAsync("TestPlugin", (compilation, callback) => { + setTimeout(() => { + console.log("compiler.make() 222"); + // 必须调用 + callback(); + }, 1000); + }); + + compiler.hooks.make.tapPromise("TestPlugin", (compilation) => { + // 必须返回promise + return new Promise((resolve) => { + setTimeout(() => { + console.log("compiler.make() 333"); + resolve(); + }, 1000); + }); + }); + } +} + +module.exports = TestPlugin; +``` + +## Node.js 调试 debugger + +package.json + +```json +{ + "scripts": { + "debug": "node --inspect-brk ./node_modules/webpack-cli/bin/cli.js" + } +} +``` + +参数说明 + +``` +--inspect-brk 启动调试,首行打断点 +``` + +运行指令 + +```bash +npm run debug +``` + +打开 Chrome 浏览器任意页面,打开调试面板,找到 Node 的绿色图标 + +可以在代码任意位置打断点 + +```js +debugger; +``` + +## BannerWebpackPlugin + +给打包输出文件添加注释 + +```js +// plugins/banner-webpack-plugin.js +class BannerWebpackPlugin { + constructor(options = {}) { + this.options = options; + } + + apply(compiler) { + // 需要处理文件 + const extensions = ["js", "css"]; + + // 前缀注释 + const prefix = `/* +* Author: ${this.options.author} +*/\n`; + + // emit是异步串行钩子 + compiler.hooks.emit.tapAsync( + "BannerWebpackPlugin", + (compilation, callback) => { + // compilation.assets包含所有即将输出的资源 + // 通过过滤只保留需要处理的文件 + const assetPaths = Object.keys(compilation.assets).filter( + (assetPath) => { + const splitted = assetPath.split("."); + return extensions.includes(splitted[splitted.length - 1]); + } + ); + + // 遍历需要处理的资源,添加注释 + assetPaths.forEach((assetPath) => { + // 获取文件内容 + const source = compilation.assets[assetPath].source(); + + // 添加注释 + const content = prefix + source; + + // 覆盖资源 + compilation.assets[assetPath] = { + // 资源内容 + source() { + return content; + }, + // 资源大小 + size() { + return content.length; + }, + }; + }); + + callback(); + } + ); + } +} + +module.exports = BannerWebpackPlugin; +``` + +使用 + +```js +// webpack.config.js +const path = require("path"); +const BannerWebpackPlugin = require("./plugins/banner-webpack-plugin.js"); + +module.exports = { + entry: "./src/index.js", + + output: { + path: path.resolve(__dirname, "dist"), + filename: "bundle.js", + clean: true, + }, + + module: {}, + + plugins: [ + new BannerWebpackPlugin({ + author: "老王", + }), + ], + + // mode: "development", + mode: "production", +}; +``` + +## CleanWebpackPlugin + +在 webpack 打包输出前将上次打包内容清空。 + +```js +// plugins/clean-webpack-plugin.js +class CleanWebpackPlugin { + apply(compiler) { + // 获取操作文件的对象 + const fs = compiler.outputFileSystem; + // emit是异步串行钩子 + compiler.hooks.emit.tapAsync( + "CleanWebpackPlugin", + (compilation, callback) => { + // 获取输出文件目录 + const outputPath = compiler.options.output.path; + // 删除目录所有文件 + const err = this.removeFiles(fs, outputPath); + // 执行成功err为undefined,执行失败err就是错误原因 + callback(err); + } + ); + } + + removeFiles(fs, path) { + try { + // 读取当前目录下所有文件 + const files = fs.readdirSync(path); + + // 遍历文件,删除 + files.forEach((file) => { + // 获取文件完整路径 + const filePath = `${path}/${file}`; + // 分析文件 + const fileStat = fs.statSync(filePath); + // 判断是否是文件夹 + if (fileStat.isDirectory()) { + // 是文件夹需要递归遍历删除下面所有文件 + this.removeFiles(fs, filePath); + } else { + // 不是文件夹就是文件,直接删除 + fs.unlinkSync(filePath); + } + }); + + // 最后删除当前目录 + fs.rmdirSync(path); + } catch (e) { + // 将产生的错误返回出去 + return e; + } + } +} + +module.exports = CleanWebpackPlugin; +``` + +使用 + +```js +// webpack.config.js +const path = require("path"); + +const CleanWebpackPlugin = require("./plugins/clean-webpack-plugin.js"); + +module.exports = { + entry: "./src/index.js", + + output: { + path: path.resolve(__dirname, "dist"), + filename: "bundle.js", + clean: true, + }, + + module: {}, + + plugins: [new CleanWebpackPlugin()], + + // mode: "development", + mode: "production", +}; +``` + +## AnalyzeWebpackPlugin + +分析 webpack 打包资源大小,并输出分析文件。 + +```js +// plugins/analyze-webpack-plugin.js + +class AnalyzeWebpackPlugin { + apply(compiler) { + // emit是异步串行钩子 + compiler.hooks.emit.tap("AnalyzeWebpackPlugin", (compilation) => { + // Object.entries将对象变成二维数组。二维数组中第一项值是key,第二项值是value + + let list = ["# 分析打包资源大小", "", "| 名称 | 大小 |", "| --- | --- |"]; + + for (let [filename, file] of Object.entries(compilation.assets)) { + list.push(`| ${filename} | ${Math.ceil(file.size() / 1024)}KB |`); + } + + let source = list.join("\n"); + + // 生成一个md文件 + compilation.assets["analyze.md"] = { + source() { + return source; + }, + size() { + return source.length; + }, + }; + }); + } +} + +module.exports = AnalyzeWebpackPlugin; +``` + +使用 + +```js +// webpack.config.js +const path = require("path"); + +const AnalyzeWebpackPlugin = require("./plugins/analyze-webpack-plugin.js"); + +module.exports = { + entry: "./src/index.js", + + output: { + path: path.resolve(__dirname, "dist"), + filename: "bundle.js", + clean: true, + }, + + module: {}, + + plugins: [new AnalyzeWebpackPlugin()], + + // mode: "development", + mode: "production", +}; +``` + +## InlineChunkWebpackPlugin + +webpack 打包生成的 runtime 文件太小了,额外发送请求性能不好,所以需要将其内联到 js 中,从而减少请求数量。 + +html-webpack-plugin 执行流程 + +![](img/html-webpack-plugin-flow.png) + +``` +pnpm i safe-require -D +``` + +```js +// plugins/inline-chunk-webpack-plugin.js +const HtmlWebpackPlugin = require("safe-require")("html-webpack-plugin"); + +class InlineChunkWebpackPlugin { + constructor(tests) { + this.tests = tests; + } + + apply(compiler) { + compiler.hooks.compilation.tap( + "InlineChunkWebpackPlugin", + (compilation) => { + // 获取html-webpack-plugin插件实例 + const hooks = HtmlWebpackPlugin.getHooks(compilation); + + // 注册一个钩子,在html-webpack-plugin插件生成html文件时调用 + hooks.alterAssetTagGroups.tap("InlineChunkWebpackPlugin", (assets) => { + assets.headTags = this.getInlineTag( + assets.headTags, + compilation.assets + ); + assets.bodyTags = this.getInlineTag( + assets.bodyTags, + compilation.assets + ); + }); + + // 删除runtime文件 + hooks.afterEmit.tap("InlineChunkHtmlPlugin", () => { + Object.keys(compilation.assets).forEach((assetName) => { + if (this.tests.some((test) => assetName.match(test))) { + delete compilation.assets[assetName]; + } + }); + }); + } + ); + } + + // 将html-webpack-plugin生成的html文件中的link和script标签提取出来 + getInlineTag(tags, assets) { + /** + * + * 输入 + * [ + { + tagName: 'script', + voidTag: false, + meta: { plugin: 'html-webpack-plugin' }, + attributes: { defer: true, type: undefined, src: 'runtime-main.js' } + } + ] + + 输出: + [ + { + tagName: 'script', + innerHTML: 'runtime的文件内容', + closeTag: true, + } + ] + */ + return tags.map((tag) => { + if (tag.tagName !== "script") return tag; + + // 文件路径 + const scriptName = tag.attributes.src; + + if (!this.tests.some((test) => scriptName.match(test))) return tag; + + return { + tagName: "script", + innerHTML: assets[scriptName].source(), + closeTag: true, + }; + }); + } +} + +module.exports = InlineChunkWebpackPlugin; + +``` + +使用 +```js +// webpack.config.js +const path = require("path"); +const HtmlWebpackPlugin = require("html-webpack-plugin"); + +const InlineChunkWebpackPlugin = require("./plugins/inline-chunk-webpack-plugin.js"); + +module.exports = { + entry: "./src/index.js", + + output: { + path: path.resolve(__dirname, "dist"), + filename: "[name].js", + clean: true, + }, + + module: { + + }, + + optimization: { + splitChunks: { + chunks: "all", + }, + runtimeChunk: { + name: (entrypoint) => `runtime-${entrypoint.name}`, + }, + }, + + plugins: [ + new HtmlWebpackPlugin({ + template: "./src/index.html", + }), + new InlineChunkWebpackPlugin([/runtime(.*)\.js$/g]), + ], + + // mode: "development", + mode: "production", +}; + +``` +https://www.bilibili.com/video/BV14T4y1z7sw?p=84&spm_id_from=pageDriver diff --git a/doc/webpack.md b/doc/webpack.md index 8d5a53e3544dbaf529d0d008986e63ca26d75bc8..3bfa4db4490a82665e0265b4aa8fc5d22443b6bf 100644 --- a/doc/webpack.md +++ b/doc/webpack.md @@ -8,6 +8,8 @@ - [webpack-loader](blog/webpack/webpack-loader.md) +- [webpack-plugin](blog/webpack/webpack-plugin.md) + 中文文档: - [https://webpack.docschina.org](https://webpack.docschina.org)