likes
comments
collection
share

自定义插件【html-webpack-plugin】

作者站长头像
站长
· 阅读数 12

前言

本文将介绍如何实现一个 webpack 插件,以实现一个 mini 版的 html-webpack-plugin 为例。

什么是webpack 插件?

Webpack 插件是一种可扩展的机制,允许你对 Webpack 构建过程中的不同阶段进行操作和调整。这些插件可以在 Webpack 的生命周期的不同点被触发,从而让你能够实现各种自动化任务,比如优化输出文件、清理目录、注入变量、生成额外的文件(如 HTML 文件)、热替换模块等等。

常见有哪些插件?

  1. 优化和压缩:例如 UglifyJsPluginTerserWebpackPlugin 可以用来压缩 JavaScript 文件,MiniCssExtractPlugin 可以用来提取 CSS 到单独的文件。
  2. 资源管理:例如 CleanWebpackPlugin 可以在构建前清除旧的输出文件,CopyWebpackPlugin 可以复制静态资源到输出目录。
  3. 服务端支持:例如 webpack-dev-server 提供了一个本地服务器,支持自动刷新和热模块替换。
  4. HTML 文件生成:例如 HtmlWebpackPlugin 可以根据模板生成 HTML 文件,并自动注入打包后的 JS 和 CSS 文件。
  5. 代码拆分:例如 SplitChunksPlugin 可以帮助你优化和拆分代码块。
  6. 环境变量注入:例如 DefinePlugin 可以在编译时定义全局常量。
  7. 缓存和离线支持:例如 WorkboxWebpackPlugin 可以帮助你设置 Service Worker,以便提供离线支持。

简单总结下,插件就是在特定的时机触发,允许你执行各种文件操作的一种机制。

html-webpack-plugin

既然要实现 mini 版的 html-webpack-plugin ,我们来看下它有哪些核心的功能点。

  1. HTML 文件生成

    • 自动生成 HTML 文件,通常用于项目入口文件。
    • 可以根据模板文件(如 index.ejs 或 index.html)来生成 HTML 文件,允许你保持 HTML 结构和样式的一致性。
  2. 资源注入

    • 自动在生成的 HTML 文件中注入编译后的 JavaScript 和 CSS 文件。
    • 支持在 <head> 或 <body> 标签内注入 <script> 和 <link> 标签。
    • 支持 chunk hashes 和 content hashes,确保浏览器强制重新下载更新后的资源。
  3. 标题和元数据

    • 可以设置 HTML 文件的 <title> 和其他元数据,如 <meta> 标签。
  4. 自定义模板

    • 使用 EJS 引擎作为默认的模板引擎,可以嵌入 JavaScript 表达式来动态生成内容。
    • 支持自定义模板变量和函数。

虽然它还有很多功能点,我们这次的模版是实现上面四个功能点。

创建插件

创建插件有固定的模版写法,官方的介绍如下:

webpack 插件由以下组成:

  • 一个 JavaScript 命名函数或 JavaScript 类。
  • 在插件函数的 prototype 上定义一个 apply 方法。
  • 指定一个绑定到 webpack 自身的事件钩子
  • 处理 webpack 内部实例的特定数据。
  • 功能完成后调用 webpack 提供的回调。

具体插件长这样:

// 一个 JavaScript 类
class MyExampleWebpackPlugin {
  // 在插件函数的 prototype 上定义一个 `apply` 方法,以 compiler 为参数。
  apply(compiler) {
    // 指定一个挂载到 webpack 自身的事件钩子。
    compiler.hooks.emit.tapAsync(
      'MyExampleWebpackPlugin',
      (compilation, callback) => {
        console.log('这是一个示例插件!');
        console.log(
          '这里表示了资源的单次构建的 `compilation` 对象:',
          compilation
        );

        // 用 webpack 提供的插件 API 处理构建过程
        compilation.addModule(/* ... */);

        callback();
      }
    );
  }
}

这里我们需要知道几个知识点

  • 插件都拥有一个 apply 方法,webpack 内部会调用
  • compiler.hoos 后面详细说明
  • tapAsync 使用,此方法为异步注册回调,当然也有同步

compiler.hooks

compiler.hooks 使用tapable库来实现。tapable是一个用于定义和调用钩子的抽象层,它是Webpack的核心机制之一,用于插件系统。tapable提供了一种机制,让不同的插件可以注册回调函数到特定的事件上,这样当事件触发时,这些回调函数就可以按照一定的顺序被调用。

基本使用

注意点:我们根据 webpack 暴露的 hooks 添加需要的监听事件做一些文件的操作,至于触发监听事件是 webpack 内部做的,不需要我们手动触发。如要了解过程可参考下面的 tapapble 的基本使用。

// 将一个名为 'MyPlugin' 的插件的回调函数添加到 'initialize' 钩子上
// 这个回调函数会在Webpack的 initialize 过程中被调用
compiler.hooks.initialize.tap('MyPlugin', (context, entry) => {
  /* ... */
});

自定义插件【html-webpack-plugin】

图1

tapapble 基本使用

import { SyncHook, AsyncParallelHook } from 'tapable' 


class Car {
	constructor() {
        // hooks 属性初始化, 该工作在 webapck内部做
        // 这里支持的 hooks 就是对应【图1】中的 hooks
		this.hooks = {
            // 这是一个同步钩子 (SyncHook),接受一个参数 "newSpeed"。
            // 这意味着任何订阅此钩子的代码将在汽车加速时被调用,并且可以访问新的速度值。
			accelerate: new SyncHook(["newSpeed"]),
			brake: new SyncHook(),
			calculateRoutes: new AsyncParallelHook(["source", "target", "routesList"])
		};
	}
}
const car = new Car()
// 如何使用这些钩子添加监听函数:
// 同步
car.hooks.accelerate.tap('speedLogger', newSpeed => {
    console.log(`New speed: ${newSpeed}`);
})

// 异步
car.hooks.calculateRoutes.tapAsync('routeOptimizer', (source, target, routesList)=> {
    console.log(`Optimized route from ${source} to ${target} routesList ${routesList}`);
})


/**
 * 触发钩子
 */
// 同步
car.hooks.accelerate.call('100km')
// 异步
car.hooks.calculateRoutes.promise('source-1','target-1', [{path:1},{path:2}])

compilation.hooks

首先我们来了解下 compilation 和 compiler 概念

compiler可以看作是Webpack的环境实例,它是Webpack的主要引擎,负责整个构建过程的管理。当你运行Webpack时,你实际上是在启动一个compiler实例。你可以把compiler想象成一个工厂,它负责组织和调度所有的资源和工作流程。

compilation负责具体的构建逻辑,如模块的解析、加载、转换、优化和打包。它跟踪构建过程中产生的模块、chunk、asset以及错误和警告。你可以把compilation想象成是工厂中的一次生产批次,它关注的是如何从原料(源代码)生产出成品(输出文件)的具体步骤。

自定义插件【html-webpack-plugin】

compiler.hooks 和 compilation.hooks关系

  • 主线是执行 compiler hooks
  • 如果在主线的 hooks 上添加回调回执行 compiler.hooks 对应回调
  • 可以在在 compilerhooks 回调中给 compilation.hooks 添加了回调
  • 在具体的时机比如 compilation.hooks.buildModule 会在在模块构建开始之前触发,可用于修改模块。

自定义插件【html-webpack-plugin】

mini-html-webpack-plugin

 class HtmlWebpackPlugin {
    apply(compiler) {
        // initialize 阶段添加自定义插件
        compiler.hooks.initialize.tap('HtmlWebpackPlugin', () => {
            // 入口文件配置的 { entry : {app: "./src/main.js"}}
            const entryName = Object.keys(compiler.options.entry);
            const outputFileName = this.options.filename.replace(/\[name\]/g, entryName)
            compiler.hooks.thisCompilation.tap('HtmlWebpackPlugin', (compilation) => {
                // 资产处理阶段添加回调
                compilation.hooks.processAssets.tapAsync(
                    {
                      name: 'HtmlWebpackPlugin',
                      stage:
                      compiler.webpack.Compilation.PROCESS_ASSETS_STAGE_OPTIMIZE_INLINE
                    },
                    (_, callback) => {
                        this.generateHTML(compiler, compilation, outputFileName, callback);
                    }
                )
            })
        })
    }
     generateHTML(compiler, compilation, outputFileName, callback) {
        const template = fs.readFileSync(this.template, 'utf-8')
        let code = ejs.render(template, this.templateParameters);
        const assets = compilation.assets;
        // 插入 head 标签的位置
        const insertIndex = code.lastIndexOf('</head>');
        // 插入的内容
        let content = ''
        Object.keys(assets).forEach(asset => {
            if (asset.endsWith('.js')) {
                content += `<script defer="defer" src="${asset}"></script>`
            } else if (asset.endsWith('.css') || asset.endsWith('.ico')) {
                content += `<link href="${asset}" rel="stylesheet">`
            }
        })
        code = code.slice(0, insertIndex) + content + code.slice(insertIndex)
        // 使用 html-minifier 对 HTML 内容进行压缩
        const minifiedHtml = htmlMinifier.minify(code, {
            removeComments: true, // 移除 HTML 注释
            collapseWhitespace: true, // 压缩 HTML,移除空格和换行
            minifyCSS: true, // 压缩内联 CSS
            minifyJS: true, // 压缩内联 JavaScript
        });
        const outputPath = path.resolve(compiler.options.output.path,outputFileName)
        // 创建打包后的 index.html
        fs.writeFileSync(outputPath, minifiedHtml)
        // 执行下一个插件
        callback()
    }
}

代码及参考文档

项目代码 英文文档 中文文档

转载自:https://juejin.cn/post/7379058697618227200
评论
请登录