Bootstrap

Webpack 钩子介绍、手写 Webpack Plugin

目录

1. Plugin 用作和工作原理

1.1 Plugin 的作用

1.2 Plugin 的工作原理

2. Webpack 底层逻辑和钩子介绍

2.1 Webpack 内部执行流程

2.2 Webpack 内部钩子

2.2.1 钩子是什么

2.2.2 Tapable —— 为 Webpack 提供 Plugin 钩子 数据类型接口 定义

2.2.3 Compiler Hooks(继承了 Tapable )

2.2.4 Compilation Hooks(继承了 Tapable )

2.2.5 JavascriptParser Hooks(继承了 Tapable )

3. 手写 Webpack Plugin

3.1 Plugin 基本结构

3.2 在 html 项目中,使用自定义插件

3.3 在 Vue 项目中,使用自定义插件

3.4 开发 Webpack 文件清单插件

4. 常用的 Webpack Plugin

5. 参考文章


1. Plugin 用作和工作原理

1.1 Plugin 的作用

关于 Plugin 的作用,Webpack 官方是这样介绍的: 

Plugins expose the full potential of the webpack engine to third-party developers. Using staged build callbacks, developers can introduce their own behaviors into the webpack build process. 

我们可以通过插件,扩展 Webpack,加入自定义的构建行为,使 Webpack 可以执行更广泛的任务,拥有更强的构建能力。

简单来说,就是扩展 Webpack 功能

1.2 Plugin 的工作原理

关于 Plugin 的工作原理,「深入浅出 Webpack」是这样介绍的: 

webpack 就像一条生产线,要经过一系列处理流程后才能将源文件转换成输出结果。 这条生产线上的每个处理流程的职责都是单一的,多个流程之间有存在依赖关系,只有完成当前处理后才能交给下一个流程去处理。

插件就像是一个插入到生产线中的一个功能,在特定的时机对生产线上的资源做处理。webpack 通过 Tapable 来组织这条复杂的生产线。 webpack 在运行过程中会广播事件,插件只需要监听它所关心的事件,就能加入到这条生产线中,去改变生产线的运作。

webpack 的事件流机制保证了插件的有序性,使得整个系统扩展性很好。
——「深入浅出 Webpack」

简单来说:

  • Webpack 通过内部的 事件流机制  ,保证了插件的有序性
  • Webpack 底层利用了 发布订阅模式,在运行过程中会广播事件
  • Webpack 插件只需要监听它所关心的事件,在特定的时机对资源做处理

站在代码逻辑的角度:

  • Webpack 在编译过代码程中,会触发一系列 Tapable 钩子事件
  • 插件需要找到相应的钩子,在上面添加自己的任务(注册事件)
  • 当 Webpack 构建时,插件注册的事件,会随着钩子的触发而执行

2. Webpack 底层逻辑和钩子介绍

开发 Plugin,需要用到一些 Webpack 底层的逻辑

2.1 Webpack 内部执行流程

一次完整的 Webpack 打包,大致是这样的过程:

  • 将 cli 命令行参数与 Webpack 配置文件 合并、解析得到参数对象 options,用于激活 webpack 加载项和插件
  • 把参数对象 options 传给 Webpack, 会创建生成 Compiler 对象,并初始化基础插件
  • 执行 Compiler 的 run 方法开始编译。每次执行 run 编译都会生成一个 Compilation 对象,也就是说,Compilation 对象负责一次编译过程
  • 触发 Compiler 的 make 方法分析入口文件,调用 compilation 的 buildModule 方法创建主模块对象
  • 生成入口文件 AST(抽象语法树),通过 AST 分析和递归加载依赖模块
  • 所有模块分析完成后,执行 compilation 的 seal 方法对每个 chunk 进行整理、优化、封装
  • 最后执行 Compiler 的 emitAssets 方法,把生成的文件输出到 output 的目录中

2.2 Webpack 内部钩子

2.2.1 钩子是什么

钩子的本质就是:事件

为了方便 开发者 直接介入和控制编译过程,Webpack 把编译过程中,触发的各类关键事件,封装成事件接口暴露了出来,这些接口被很形象地称做:hooks(钩子)

开发插件,离不开这些钩子

2.2.2 Tapable —— 为 Webpack 提供 Plugin 钩子 数据类型接口 定义

Tapable 暴露了三个方法给 Plugin,用于注入 不同类型的 自定义构建行为:

  • tap:可以注册同步钩子和异步钩子
  • tapAsync:回调方式注册异步钩子
  • tapPromise:Promise方式注册异步钩子

举个栗子~~~~~ 

 

Webpack 里的几个非常重要的对象,Compiler, Compilation 和 JavascriptParser 都继承了 Tapable 类,它们身上挂着丰富的钩子

Tapable 是 Webpack 的核心功能库

在 Tapable 源码中可以看到,Webpack 中目前有十种 hooks

// https://github.com/webpack/tapable/blob/master/lib/index.js

exports.SyncHook = require("./SyncHook");
exports.SyncBailHook = require("./SyncBailHook");
exports.SyncWaterfallHook = require("./SyncWaterfallHook");
exports.SyncLoopHook = require("./SyncLoopHook");
exports.AsyncParallelHook = require("./AsyncParallelHook");
exports.AsyncParallelBailHook = require("./AsyncParallelBailHook");
exports.AsyncSeriesHook = require("./AsyncSeriesHook");
exports.AsyncSeriesBailHook = require("./AsyncSeriesBailHook");
exports.AsyncSeriesLoopHook = require("./AsyncSeriesLoopHook");
exports.AsyncSeriesWaterfallHook = require("./AsyncSeriesWaterfallHook");

2.2.3 Compiler Hooks(继承了 Tapable )

  • Compiler 编译器模块,是创建 编译实例 的主引擎
  • 大多数面向用户的插件,都在 Compiler 上注册

Compiler 上暴露的一些常用的钩子:

钩子类型什么时候调用
runAsyncSeriesHook在编译器 开始读取记录前 执行
compileSyncHook在一个新的 compilation 创建之前执行
compilationSyncHook在一次 compilation 创建后执行插件
makeAsyncParallelHook完成一次编译前执行
emitAsyncSeriesHook在生成文件到 output 目录之前执行,回调参数:compilation
afterEmitAsyncSeriesHook在生成文件到 output 目录之后执行
assetEmittedAsyncSeriesHook生成文件的时候执行,提供访问产出文件信息的入口,回调参数:file,info
doneAsyncSeriesHook一次编译完成后执行,回调参数:stats

 

2.2.4 Compilation Hooks(继承了 Tapable )

  • Compilation 是 Compiler 用来创建一次新的编译过程的模块
  • 一个 Compilation 实例可以访问所有模块和它们的依赖
  • 在一次编译阶段,模块被加载、封装、优化、分块、散列和还原

Compilation 上暴露的一些常用的钩子:

钩子类型什么时候调用
buildModuleSyncHook在模块开始编译之前触发,可以用于修改模块
succeedModuleSyncHook当一个模块被成功编译,会执行这个钩子
finishModulesAsyncSeriesHook当所有模块都编译成功后被调用
sealSyncHook当一次 compilation 停止接收新模块时触发
optimizeDependenciesSyncBailHook在依赖优化的开始执行
optimizeSyncHook在优化阶段的开始执行
optimizeModulesSyncBailHook在模块优化阶段开始时执行,插件可以在这个钩子里执行对模块的优化,回调参数:modules
optimizeChunksSyncBailHook在代码块优化阶段开始时执行,插件可以在这个钩子里执行对代码块的优化,回调参数:chunks
optimizeChunkAssetsAsyncSeriesHook优化任何代码块资源,这些资源存放在 compilation.assets 上。一个 chunk 有一个 files 属性,它指向由一个 chunk 创建的所有文件。任何额外的 chunk 资源都存放在 compilation.additionalChunkAssets 上。回调参数:chunks
optimizeAssetsAsyncSeriesHook优化所有存放在 compilation.assets 的所有资源。回调参数:assets

2.2.5 JavascriptParser Hooks(继承了 Tapable )

  • Parser 解析器实例在 Compiler 编译器中产生,用于解析 Webpack 正在处理的每个模块
  • 可以用它提供的 Tapable 钩子,自定义解析过程

JavascriptParser 上暴露的一些常用的钩子:

钩子类型什么时候调用
evaluateSyncBailHook在计算表达式的时候调用
statementSyncBailHook为代码片段中每个 已解析的语句 调用的通用钩子
importSyncBailHook为代码片段中每个 import 语句调用,回调参数:statement,source
exportSyncBailHook为代码片段中每个 export 语句调用,回调参数:statement
callSyncBailHook解析一个 call 方法的时候调用,回调参数:expression
programSyncBailHook解析一个 表达式 的时候调用,回调参数:expression

 

3. 手写 Webpack Plugin

3.1 Plugin 基本结构

① 本质上是 Node 模块,该导出了一个 Javascript 方法 或 JavaScript 类

② 它的原型上,需要定义一个叫做 apply 的方法

③ 通过 compiler 获取 Webpack 内部的钩子,获取 Webpack 打包过程中的各个阶段;钩子分为同步和异步的钩子,异步钩子在功能完成后,必须执行对应的回调

④ 通过 compilation,操作 Webpack 内部实例特定数据

⑤ 功能完成后,调用 Webpack 提供的回调

 

3.2 在 html 项目中,使用自定义插件

新建项目,通过 npm init -y 创建 package.json,并在 script 中配置打包命令 "build": "webpack"

安装 webpack、webpack-cli

新建 MyPlugin.js 文件,声明一个 class,并导出(这就是自定义手写的 Webpack 插件)

通过 compiler 获取 webpack 内部的钩子 done,并在钩子中注入一条 控制台打印 的语句

根据上文 2.2.3 钩子介绍,done 会在一次编译完成后执行

所以这个插件会在每次打包结束时,向控制台输出 “♪(^∇^*) Lyrelion 打包已完成!”

// 自定义 MyPlugin 插件,该插件在打包完成后,在控制台输出 "打包已完成"

// 一个命名的 Javascript 方法 或 JavaScript 类
class MyPlugin {
    // 原型上需要定义 apply 的方法
    apply(compiler) {
        // 通过 compiler 获取 webpack 内部的钩子,获取 Webpack 打包过程中的各个阶段
        compiler.hooks.done.tap("My Plugin", (compilation, cb) => {
            // 通过 compilation,操作 Webpack 内部实例特定数据
            console.log("♪(^∇^*) Lyrelion 打包已完成!");
            // 分为同步和异步的钩子,异步钩子在功能完成后,必须执行对应的回调
            // cb();
        });
    }
}

module.exports = MyPlugin;

 

新建 webpack.config.js,引入自定义插件

const path = require('path');
const MyPlugin = require('./plugins/MyPlugin');

module.exports = {
    mode: 'development',
    entry: {
        index: './src/index.js',
        a: './src/a.js'
    },
    output: {
        filename: '[name].js',
        path: path.resolve(__dirname, 'dist')
    },
    plugins: [
        new MyPlugin(),
    ]
}

执行打包,控制台输入 yarn build,打包完成后,控制台输出效果: 

 

3.3 在 Vue 项目中,使用自定义插件

在 vue.config.js 引入该插件

const MyPlugin = require('./MyPlugin.js')

在 configureWebpack 的 plugins 列表中,注册该插件

module.exports = {

  configureWebpack: {

    plugins: [new MyPlugin()]

  }

};

执行项目打包命令,会在控制台输出 “♪(^∇^*) Lyrelion 打包已完成!”

3.4 开发 Webpack 文件清单插件

需求:

  • 每次 Webpack 打包后,自动产生一个打包文件清单
  • 清单上要记录 文件名、文件数量 等信息

基本思路:

  • 本操作需要在文件生成到 dist 目录之前进行,所以要注册 Compiler 上的 emit 钩子
  • emit 是一个异步串行钩子,用 tapAsync 来注册
  • 在 emit 回调函数里,可以拿到 compilation 对象,所有待生成的文件都在它的 assets 属性上;通过 compilation.assets 获取文件信息,并整理输出
  • 然后往 compilation.assets 添加这个新的文件

编写插件:

/**
 * 自定义 FileListPlugin 插件
 * 每次 Webpack 打包后,自动产生一个打包文件清单
 * 清单上要记录 文件名、文件数量 等信息
 */
class FileListPlugin {
    constructor(options) {
        // 获取插件配置项
        this.filename = options && options.filename ? options.filename : 'FILELIST.md';
    }

    apply(compiler) {
        // 注册 compiler 上的 emit 钩子
        compiler.hooks.emit.tapAsync('FileListPlugin', (compilation, cb) => {

            // 通过 compilation.assets 获取文件数量
            let len = Object.keys(compilation.assets).length;

            // 添加统计信息
            let content = `# ${len} file${len > 1 ? 's' : ''} emitted by webpack\n\n`;

            // 通过 compilation.assets 获取文件名列表
            for (let filename in compilation.assets) {
                content += `- ${filename}\n`;
            }

            // 往 compilation.assets 中添加清单文件
            compilation.assets[this.filename] = {
                // 写入新文件的内容
                source: function () {
                    return content;
                },
                // 新文件大小(给 webapck 输出展示用)
                size: function () {
                    return content.length;
                }
            }

            // 执行回调,让 webpack 继续执行
            cb();
        })
    }
}

module.exports = FileListPlugin;

 

配置 webpack.config.js:

const path = require('path');
const MyPlugin = require('./plugins/MyPlugin');
const FileListPlugin = require('./plugins/FileListPlugin');

module.exports = {
    mode: 'development',
    entry: {
        index: './src/index.js',
        a: './src/a.js'
    },
    output: {
        filename: '[name].js',
        path: path.resolve(__dirname, 'dist')
    },
    plugins: [
        new MyPlugin(),
        new FileListPlugin({
            filename: '_filelist.md'
        })
    ]
}

最终效果:

 

4. 常用的 Webpack Plugin

插件名称作用
html-webpack-plugin生成 html 文件,引入公共的 js 和 css 资源
webpack-bundle-analyzer对打包后的文件进行分析,生成资源分析图
terser-webpack-plugin代码压缩,移除 console.log 打印等
HappyPack Plugin开启多线程打包,提升打包速度
Dllplugin动态链接库,将项目中依赖的三方模块抽离出来,单独打包
DllReferencePlugin配合 Dllplugin,通过 manifest.json 映射到相关的依赖上去
clean-webpack-plugin清理上一次项目生成的文件
vue-skeleton-webpack-pluginvue 项目实现骨架屏

5. 参考文章

揭秘webpack plugin | ChampYin's BlogPlugin(插件) 是 webpack 生态的的一个关键部分。它为社区提供了一种强大的方法来扩展 webpack 和开发 webpack 的编译过程。本文将尝试探索 webpack plugin,揭秘它的工作原理,以及如何开发一个 plugin。https://champyin.com/2020/01/12/%E6%8F%AD%E7%A7%98webpack-plugin/

;