引子
Webpack 是一个强大的模块打包工具,它可以处理各种类型的资源,并将它们转换为浏览器可以理解的代码。在这个过程中,Webpack 使用了一种叫做 "插件" 的机制,它允许你自定义如何处理特定类型的资源。
在这篇文章中,我们将深入探讨如何创建和使用自定义的 webpack 插件。我们将通过一系列的例子,展示如何使用插件来实现各种常见的任务,如移除注释、添加版权声明、转换 Markdown 到 HTML 等等。这些例子将帮助你理解插件的工作原理,并教你如何创建自己的插件来满足特定的需求。
Lets Go!
几个最常用的钩子
entryOption: 在 webpack 选项中的 entry 配置项被处理过后,立即执行。这个钩子可以用来修改、添加或删除 entry 配置。
compilation: 每次新的编译创建时,都会触发此钩子。编译对象包含了当前的模块资源、编译生成资源、变化的文件等。这个钩子可以用来在编译过程中添加自定义的功能。
emit: 在生成资源并输出到目录之前。这个钩子可以用来修改输出资源,或者在输出资源到目录之前执行一些操作。
afterEmit: 在生成资源并输出到目录之后。这个钩子可以用来执行一些清理或者其他的收尾工作。
done: 编译完成时。这个钩子可以用来报告编译结果,或者执行一些编译完成后的操作。
failed: 编译失败时。这个钩子可以用来处理编译错误,例如记录错误日志。
插件实例
@构建开始之前
ConsoleLogOnBuildWebpackPlugin
这个插件在每次构建开始时在控制台打印一条消息。
class ConsoleLogOnBuildWebpackPlugin {
apply(compiler) {
compiler.hooks.run.tap('ConsoleLogOnBuildWebpackPlugin', compilation => {
console.log('The webpack build process is starting!!!');
});
}
}
@单个模块编译时
ReplaceTextPlugin
这个插件会替换源代码中的特定文本,你可以通过参数指定要替换的文本和替换内容。
class ReplaceTextPlugin {
constructor(options) {
this.options = options;
}
apply(compiler) {
compiler.hooks.compilation.tap('ReplaceTextPlugin', (compilation) => {
compilation.hooks.optimizeChunkAssets.tap('ReplaceTextPlugin', (chunks) => {
chunks.forEach((chunk) => {
chunk.files.forEach((file) => {
const source = compilation.assets[file].source();
const newSource = source.replace(this.options.search, this.options.replace);
compilation.assets[file].source = () => newSource;
});
});
});
});
}
}
使用方法:
const ReplaceTextPlugin = require('./ReplaceTextPlugin');
module.exports = {
// ...
plugins: [
new ReplaceTextPlugin({
search: /Hello/g,
replace: 'Hi',
}),
],
};
@资源输出之前
FileListPlugin
这个插件在每次构建完成后,会在输出目录中生成一个 filelist.md 文件,其中列出了所有的输出文件。
const fs = require('fs');
const path = require('path');
class FileListPlugin {
apply(compiler) {
// emit 是异步 hook,使用 tapAsync 触发它
compiler.hooks.emit.tapAsync('FileListPlugin', (compilation, callback) => {
let filelist = 'In this build:\n\n';
// 遍历所有编译过的资源文件,
// 对于每个文件名称,都添加一行内容。
for (let filename in compilation.assets) {
filelist += '- ' + filename + '\n';
}
// 将这个列表作为一个新的文件资源,插入到 webpack 构建中:
compilation.assets['filelist.md'] = {
source: function() {
return filelist;
},
size: function() {
return filelist.length;
},
};
callback();
});
}
}
module.exports = FileListPlugin;
使用方法:
const FileListPlugin = require('./FileListPlugin');
module.exports = {
// ...
plugins: [
new FileListPlugin(),
],
};
CopyWebpackPlugin
这个插件用于将单个文件或整个目录复制到构建目录。
const fs = require('fs-extra');
const path = require('path');
class CopyWebpackPlugin {
constructor(options) {
this.options = options;
}
apply(compiler) {
compiler.hooks.emit.tapAsync('CopyWebpackPlugin', (compilation, callback) => {
this.options.forEach(option => {
fs.copySync(option.from, path.join(compiler.options.output.path, option.to));
});
callback();
});
}
}
module.exports = CopyWebpackPlugin;
使用方法:
const CopyWebpackPlugin = require('./CopyWebpackPlugin');
module.exports = {
// ...
plugins: [
new CopyWebpackPlugin([
{ from: 'src/assets', to: 'assets' },
]),
],
};
@资源输出以后
ZipWebpackPlugin
这个插件用于将输出文件打包为一个 zip 文件。
const fs = require('fs');
const path = require('path');
const archiver = require('archiver');
class ZipWebpackPlugin {
constructor(options) {
this.options = options;
}
apply(compiler) {
compiler.hooks.afterEmit.tapAsync('ZipWebpackPlugin', (compilation, callback) => {
const outputPath = compiler.options.output.path;
const output = fs.createWriteStream(path.join(outputPath, this.options.filename));
const archive = archiver('zip', {
zlib: { level: 9 }, // 设置压缩级别。
});
output.on('close', () => {
callback();
});
archive.pipe(output);
archive.directory(outputPath, false);
archive.finalize();
});
}
}
module.exports = ZipWebpackPlugin;
使用方法:
const ZipWebpackPlugin = require('./ZipWebpackPlugin');
module.exports = {
// ...
plugins: [
new ZipWebpackPlugin({
filename: 'output.zip',
}),
],
};
AssetVersioningPlugin
这个插件会为所有的输出资源添加版本号,以解决缓存问题。
class AssetVersioningPlugin {
apply(compiler) {
compiler.hooks.emit.tap('AssetVersioningPlugin', (compilation) => {
const version = new Date().getTime();
for (let filename in compilation.assets) {
compilation.assets[`${filename}?v=${version}`] = compilation.assets[filename];
delete compilation.assets[filename];
}
});
}
}
@打包完成后
LogOnFinishPlugin
这是一个简单的插件,它在每次构建完成后在控制台打印 "Build Finished!"。
class HelloWorldPlugin {
apply(compiler) {
compiler.hooks.done.tap(
'LogOnFinishPlugin',
(stats /* 在处理完所有模块后触发 */) => {
console.log('Build Finished!');
});
}
}
module.exports = HelloWorldPlugin;
FriendlyErrorsWebpackPlugin
这个插件会改善 webpack 的错误信息和日志,使其更友好。
class FriendlyErrorsWebpackPlugin {
apply(compiler) {
compiler.hooks.done.tap('FriendlyErrorsWebpackPlugin', (stats) => {
const errors = stats.compilation.errors;
if (errors && errors.length) {
console.log('\n\n');
console.log('Oops, something went wrong :(');
console.log('===========================');
errors.forEach((error) => {
console.log(error.message || error);
console.log('\n');
});
} else {
console.log('\n\n');
console.log('Build completed successfully!');
console.log('=============================');
}
});
}
}
PerformanceBudgetPlugin
这个插件会检查你的应用是否超出了预设的性能预算。
class PerformanceBudgetPlugin {
constructor(options) {
this.options = options;
}
apply(compiler) {
compiler.hooks.done.tap('PerformanceBudgetPlugin', (stats) => {
const assets = stats.toJson().assets;
assets.forEach((asset) => {
if (asset.size > this.options.maxSize) {
console.warn(`Asset ${asset.name} is over performance budget of ${this.options.maxSize} bytes.`);
}
});
});
}
}
小结
自定义 Webpack 插件是一个强大的工具,它可以让你在构建过程中执行自定义的操作,以满足特定的需求。以下是关于自定义 Webpack 插件的一些关键点:
-
生命周期钩子:Webpack 提供了一系列的生命周期钩子,你可以在插件中使用这些钩子来执行自定义的操作。这些钩子包括 entryOption、afterPlugins、compilation、emit、afterEmit、done、failed、invalid 和 watchRun 等。
-
插件结构:一个 Webpack 插件通常是一个具有 apply 方法的 JavaScript 对象。apply 方法会被 Webpack 编译器调用,并可以在其中访问到编译器实例。
-
访问编译资源:在插件中,你可以通过编译器实例访问到所有的编译资源,包括模块资源、编译生成资源、变化的文件等。
-
异步处理:Webpack 插件支持异步处理,你可以在插件中执行异步操作,例如读写文件、网络请求等。
-
参数化插件:你可以通过插件的构造函数接收参数,以创建参数化的插件。这使得插件可以更灵活地适应不同的需求。
-
错误处理:在插件中,你应该正确处理可能出现的错误,并通过编译器实例报告这些错误。
自定义 Webpack 插件是一个非常强大的工具,它可以让你深入到构建过程的每一个环节,执行自定义的操作,以满足你的特定需求。理解并掌握如何创建和使用自定义插件,将极大地提升你的 Webpack 使用效率!
镇场名画:拯救世界的任务就交给你了好吗~