前言
在现代前端开发中,Webpack 已成为不可或缺的工具之一。它通过模块化打包的方式,将我们分散的代码、资源整合成高效的输出。然而,Webpack 的强大不仅体现在它的打包能力上,更多是通过灵活的 Loader 和 Plugin 机制,赋予开发者无限扩展的可能性。
在这篇文章中,我们将深入了解 Webpack 的 Loader 和 Plugin。我们将从它们的基本概念和工作原理入手,分析它们在构建流程中的作用。随后,我会手把手展示如何实现一个自定义的 HtmlWebpackPlugin,帮助你更好地理解 Webpack 插件的开发方式。
Webpack是什么?
概念:
官方文档定义: Webpack是一个用于现代 JavaScript 应用程序的 静态模块打包工具,基于 Node.js 开发出的前端工具。
我们来拆分一下这段话的重点:
- 打包: 将多个分散的代码文件和资源整合成一个或多个文件的过程
- 静态资源: 比如常见的 Css 文件、JS 、Images、字体文件、模板文件 。这一类属于都静态资源。
所以我们再来总结文档中定义的这段话:
Webpack 是一个用于 JS 应用程序中众多静态资源文件整合在一起的工具。
为什么要整合静态资源?
在网页中,如果静态资源过多,会产生一些性能以及依赖管理的问题:
- 网页加载速度变慢
- 处理复杂的依赖关系
所以,在我们开发大型的应用程序,一个合适的打包工具是必不可少,它可以帮助我们解决许多性能上、资源上、依赖上的许多问题。
Webpack基本使用
配置 Webpack 必不可缺的俩个概念:entry
和 output
,接下来我们来展示这俩个概念分别如何作用。
entry
指定一个入口文件:
Webpack会从指定的这个入口文件开始,递归解析应用程序中的依赖模块,最后打包成一个或者多个 bundle.js
文件
entry
通常在 webpack.config.js
中配置,代码如下:
// webpack.config.js
module.exports = {
entry: './src/index.js'
};
这里指定的入口文件为 src 目录下的 index.js 文件,不过 entry 默认值一般都为 src/index.js ,你可以自定义配置它的入口文件。
output
指定一个输出文件:
Wenpack 会指定一个打包后输出的文件,它定义打包后的文件输出在哪,以及输出的文件名称是什么。
output
也在 webpack.config.js
中配置,代码如下:
module.exports = {
output: {
filename: 'bundle.js',
path: __dirname + '/dist'
}
};
这里指定的输出文件在 当前路径下的 dist 目录下,输出的文件名称是 bundle.js,你也可以自定义配置它的输出文件。
Webpack的核心
Wevpack俩个核心概念:Loader
和 Plugin
是其强大灵活性的基础,它们分别用于处理文件转换和扩展 Webpack 的功能。
Loader
概念:
Webpack处理模块文件的转换器:例如将 TS 转换成 JS 文件,less 转换成 CSS 文件。
转换原理:
Webpack 默认只识别 JavaScript 和 JSON 文件,其他类型文件无法被打包,如:TS Scss CSS 文件等,Loader
通过转换这些文件,将它们转换成 Webpack 可以处理的模块,然后再去打包。
主要作用:
- 模块解析前的预处理
- 将一种文件类型转换成另一种文件类型
使用:
loader
在 Webpack 配置文件中的 module.rules
进行配置。
通过 test 属性指定要处理的文件类型,通过 use 属性指定要使用的 loader 转换器。
代码示例:
module.exports = {
module: {
rules: [
{
test: /\.js$/,
use: 'babel-loader', // 将ES6+转换成ES5
},
{
test: /\.css$/,
use: ['style-loader', 'css-loader'],
// style-loader 将 css 文件插入到 HTML 中的 <style> 标签中
// css-loader 用于解析 CSS 文件中的 @import 和 url() 语句
},
{
test: /\.scss$/,
use: ['style-loader', 'css-loader', 'sass-loader'],
// sass-loader 将 scss 文件转换成 css 文件
},
{
test: /\.(png|jpg|gif)$/,
use: 'file-loader', // 处理图像文件
},
],
},
};
Plugin
概念:
Plugin
是一个 Webpack 功能扩展器,在 Webpack 打包周期中执行一些复杂的操作。
工作原理:
Webpack 在构建项目时会触发一系列钩子,Plugin 可以挂载到这些钩子上,在特定期间执行特定的行为。
主要作用:
- HtmlWebpackPlugin: 自动生成 HTML 文件,并将打包后的 JS 文件自动引入到 HTML 中。
- CleanWebpackPlugin: 在每次构建前清理输出目录(如 dist/)。
- MiniCssExtractPlugin: 将 CSS 提取到独立的文件中,实现更好的缓存和页面加载速度。
以上是一些常用插件的功能,用于更好的构建项目,优化项目。
使用:
Plugin
在 Webpack 配置文件中的 plugins 数组进行配置。
使用 new 操作符来生成构造函数。
代码示例:
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const webpack = require('webpack');
module.exports = {
plugins: [
new HtmlWebpackPlugin({
template: './src/index.html',
}), // 在template指定路径下生成该html文件,并注入打包后的资源
new CleanWebpackPlugin(), // 在每次构建之前清理输出目录
new webpack.DefinePlugin({
'process.env.NODE_ENV': JSON.stringify('production'),
}), // 定义环境变量
],
};
实现一个 htmlWebpackPlugin
通过上面的概念以及代码示例,我们基本已经了解了 Webpack 部分功能以及核心概念,那么我们将会实践,自己实现一个插件:htmlWebpackPlugin
。
配置要求:
首先是我们需要使用到的依赖:
"devDependencies": {
"html-minifier": "^4.0.0",
"webpack": "^5.93.0",
"webpack-cli": "^5.1.4"
}
执行构建的命令:
"scripts": {
"build": "webpack"
}
webpack.config.js
配置
const path = require('path');
// 提前创建好一个 htmlWebpackPlugin.js 文件
const HtmlWebpackPlugin = require('./htmlWebpackPlugin')
module.exports = {
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist')
},
plugins: [
new HtmlWebpackPlugin({
// ... 这里是一些参数配置
})
]
};
参数要求:
- title: 用来指定 HTML 标题。
- filename: 用来指定生成的 HTML 文件名称。
- template: 用来指定 HTML 模板文件,根据该模板,插入打包后的 js 或 css 文件。
- inject: 指定资源的插入位置, 或者 中。
- meta: 指定在 HTML 中生成 标签。
- minify: 配置生成的 HTML 文件中压缩和优化选项。
现在所有的参数以及罗列出来,我们可以去配置 htmlWebpackPlugin
的参数了。
代码示例:
const path = require('path');
// 提前创建好一个 htmlWebpackPlugin.js 文件
const HtmlWebpackPlugin = require('./htmlWebpackPlugin')
module.exports = {
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist')
},
plugins: [
new HtmlWebpackPlugin({
title: 'My Webpack App',
filename: 'index.html',
template: 'src/template.html',
inject: 'head',
meta: {
viewport: 'width=device-width, initial-scale=1, shrink-to-fit=no',
author: 'Shr',
},
minify: {
removeComments: true,
collapseWhitespace: true,
removeAttributeQuotes: true,
removeEmptyAttributes: true,
removeOptionalTags: true,
},
})
]
};
代码实现:
在 HtmlWebpackPlugin
的配置中,可以通过 minify
属性来设置 HTML 压缩选项,这些选项会传递给 html-minifier
,以控制生成 HTML 文件的压缩方式,所以我们现在对 minify
的处理需要引入 html-minifier
来处理。
const path = require('path')
const fs = require('fs')
const htmlMinifier = require('html-minifier').minify
大多数的 Webpack 插件都是定义一个类来实现的,我们在调用 htmlWebpackPlugin
时也是采用了 new 操作符来创建一个实例对象,所以我们这里来定义一个类:
class HtmlWebpackPlugin {
// 这里接受options选项作为参数
constructor(options) {
this.options = options; // 参数包含标题/文件名
}
}
module.exports = HtmlWebpackPlugin
实现 apply
方法:apply 方法用于注册 Webpack 提供的钩子,在特定期间执行特定操作。
// 注册插件
apply(compiler) {
// 指定一个挂载到 webpack 自身的事件钩子。
compiler.hooks.emit.tapAsync(
// 钩子名
'SimpleHtmlWebpackPlugin',
// 钩子函数
(compilation, callback) => {
// 调用generateHtml方法生成html内容
// 传入compilation对象作为参数
const html = this.generateHtml(compilation);
// 生成html文件输出路径
compilation.assets[this.options.filename || 'index.html'] = {
source: () => html, // 返回html内容
size: () => html.length // 返回html长度
};
// 使用tapAsync方法绑定插件,必须调用callback指定回调函数
// 表示处理完成
callback();
});
}
实现 generateHtml :生成 html 内容
generateHtml(compilation) {
let html
// template:用于文件系统读取模板文件/templateContent:直接从参数值中提供HTML模板内容
// 这里用文件中template模板
if (this.options.templateContent) {
html = this.options.templateContent;
} else if (this.options.template) {
const templatePath = path.resolve(this.options.template);
html = fs.readFileSync(templatePath, 'utf-8');
} else {
html = this.defaultTemplate();
}
// title
if (this.options.title) {
html = html.replace('<title>', `<title>${this.options.title}`)
}
const assets = Object.keys(compilation.assets); // 输出所有文件名称
const scripts = assets.map(asset => `<script src="${asset}"></script>`).join('\n');
// inject
if (this.options.inject === 'body') {
html = html.replace('</body>', `</body>\n${scripts}`)
} else if (this.options.inject === 'head') {
html = html.replace('</head>', `${scripts}\n</head>`)
}
// meta
if (this.options.meta) {
const metaTag = Object.entries(this.options.meta)
.map(([name, content]) => `<meta name="${name}" content="${content}">`).join('\n')
html = html.replace('<head>', `<head>\n${metaTag}`)
}
// minify
if (this.options.minify) {
html = htmlMinifier(html, this.options.minify)
}
return html
}
完整代码:
const path = require('path')
const fs = require('fs')
const htmlMinifier = require('html-minifier').minify
// js类
class HtmlWebpackPlugin {
// 这里接受options选项作为参数
constructor(options) {
this.options = options; // 参数包含标题/文件名
}
// 注册插件
// 在插件函数的 prototype 上定义一个 `apply` 方法,以 compiler 为参数。
apply(compiler) {
// 指定一个挂载到 webpack 自身的事件钩子。
compiler.hooks.emit.tapAsync(
// 钩子名
'SimpleHtmlWebpackPlugin',
// 钩子函数
(compilation, callback) => {
// 调用generateHtml方法生成html内容
// 传入compilation对象作为参数
const html = this.generateHtml(compilation);
// 生成html文件输出路径
compilation.assets[this.options.filename || 'index.html'] = {
source: () => html, // 返回html内容
size: () => html.length // 返回html长度
};
// 使用tapAsync方法绑定插件,必须调用callback指定回调函数
// 表示处理完成
callback();
});
}
generateHtml(compilation) {
let html
// template:用于文件系统读取模板文件/templateContent:直接从参数值中提供HTML模板内容
// 这里用文件中template模板
if (this.options.templateContent) {
html = this.options.templateContent;
} else if (this.options.template) {
const templatePath = path.resolve(this.options.template);
html = fs.readFileSync(templatePath, 'utf-8');
} else {
html = this.defaultTemplate();
}
// title
if (this.options.title) {
html = html.replace('<title>', `<title>${this.options.title}`)
}
const assets = Object.keys(compilation.assets); // 输出所有文件名称
const scripts = assets.map(asset => `<script src="${asset}"></script>`).join('\n');
// inject
if (this.options.inject === 'body') {
html = html.replace('</body>', `</body>\n${scripts}`)
} else if (this.options.inject === 'head') {
html = html.replace('</head>', `${scripts}\n</head>`)
}
// meta
if (this.options.meta) {
const metaTag = Object.entries(this.options.meta)
.map(([name, content]) => `<meta name="${name}" content="${content}">`).join('\n')
html = html.replace('<head>', `<head>\n${metaTag}`)
}
// minify
if (this.options.minify) {
html = htmlMinifier(html, this.options.minify)
}
return html
}
}
module.exports = HtmlWebpackPlugin;
现在我们的 htmlWebpackPlugin 就已经实现了,有大多数参数项还没有实现,这只是一个基本的例子。