前言
本文将介绍如何实现一个 webpack 插件,以实现一个 mini 版的 html-webpack-plugin 为例。
什么是webpack 插件?
Webpack 插件是一种可扩展的机制,允许你对 Webpack 构建过程中的不同阶段进行操作和调整。这些插件可以在 Webpack 的生命周期的不同点被触发,从而让你能够实现各种自动化任务,比如优化输出文件、清理目录、注入变量、生成额外的文件(如 HTML 文件)、热替换模块等等。
常见有哪些插件?
-
优化和压缩:例如
UglifyJsPlugin
或TerserWebpackPlugin
可以用来压缩 JavaScript 文件,MiniCssExtractPlugin
可以用来提取 CSS 到单独的文件。 -
资源管理:例如
CleanWebpackPlugin
可以在构建前清除旧的输出文件,CopyWebpackPlugin
可以复制静态资源到输出目录。 -
服务端支持:例如
webpack-dev-server
提供了一个本地服务器,支持自动刷新和热模块替换。 -
HTML 文件生成:例如
HtmlWebpackPlugin
可以根据模板生成 HTML 文件,并自动注入打包后的 JS 和 CSS 文件。 -
代码拆分:例如
SplitChunksPlugin
可以帮助你优化和拆分代码块。 -
环境变量注入:例如
DefinePlugin
可以在编译时定义全局常量。 -
缓存和离线支持:例如
WorkboxWebpackPlugin
可以帮助你设置 Service Worker,以便提供离线支持。
简单总结下,插件就是在特定的时机触发,允许你执行各种文件操作的一种机制。
html-webpack-plugin
既然要实现 mini 版的 html-webpack-plugin ,我们来看下它有哪些核心的功能点。
-
HTML 文件生成:
- 自动生成 HTML 文件,通常用于项目入口文件。
- 可以根据模板文件(如
index.ejs
或index.html
)来生成 HTML 文件,允许你保持 HTML 结构和样式的一致性。
-
资源注入:
- 自动在生成的 HTML 文件中注入编译后的 JavaScript 和 CSS 文件。
- 支持在
<head>
或<body>
标签内注入<script>
和<link>
标签。 - 支持 chunk hashes 和 content hashes,确保浏览器强制重新下载更新后的资源。
-
标题和元数据:
- 可以设置 HTML 文件的
<title>
和其他元数据,如<meta>
标签。
- 可以设置 HTML 文件的
-
自定义模板:
- 使用 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) => {
/* ... */
});
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()
}
}
代码及参考文档
如有侵权请联系站点删除!
技术合作服务热线,欢迎来电咨询!