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

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

技术博客 admin 493 浏览

前言

本文将介绍如何实现一个 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 提供的回调。

具体插件长这样:

js
复制代码
// 一个 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 的基本使用。

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

图1

tapapble 基本使用

js
复制代码
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想象成是工厂中的一次生产批次,它关注的是如何从原料(源代码)生产出成品(输出文件)的具体步骤。

compiler.hooks 和 compilation.hooks关系

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

mini-html-webpack-plugin

js
复制代码
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() } }

代码及参考文档

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

源文:自定义插件【html-webpack-plugin】

如有侵权请联系站点删除!

技术合作服务热线,欢迎来电咨询!