webpack4 plugin 的原理和实现部分plugin
1.Plugin
- plugin 本质上就是一个类,有一个 apply 方法,接受一个 compiler 的对象, 我们会在 compiler 对象上钩子挂载一些监听函数,当 compiler 对象上这些钩子触发的时候,就会调用这些函数
- 插件向第三方开发者提供了 webpack 引擎中完整的能力,使用阶段式的构建回调,开发者可以引入它们自己的行为到 webpack 构建的流程中,创建插件比创建 loader 更加高级,因为你将需要 理解一些 webpack 底层的内部特性来做相应的钩子。
1.1 为什么需要一个插件
- webpack 基础配置无法满足需求
- 插件几乎能够任意更改 webpack 编译结果
- webpack 内部也是通过大量的内部插件实现的
1.2 可以加载插件的常用对象
- 对象 钩子
- compiler run,compile,compilation,make,emit,done
- Compilation buildModule,normalModuleLoader,succeedModule,finishModules,seal,optimize,after-seal
- Module Factory beforeResolver,afterResolver,module,parser
- Module
- Parser program,statement,call,expression
- Template hash,bootstrap,localVars,render
2.创建插件
webpack 插件由以下组成
- 一个 JavaScript 命名函数。
- 指定一个绑定到 webpack 内部实例的特定数据。
- 处理 webpack 内部实例的特定数据。
- 功能完成后调用 webpack 提供的回调。
3. compiler 和 Compilation
4.plugin 实现和原理
4.1 DonePlugin 监听 webpack 编译完毕的插件 [done 都干完了]
// 监听编译完毕事件
class DonePlugin {
constructor(options) {
this.options = options;
}
apply(compiler) {
// 每当编译[done写完了]完成后,都会call done这个事件
compiler.hooks.done.tap('DonePlugin', () => {
console.log(this.options.message || arguments);
});
}
}
module.exports = DonePlugin;
4.2 OptimizePlugin 监听 webpack 优化的时候的事件的插件
// 优化时监听插件,想知道如何或者compilation对象
class OptimizePlugin {
constructor(options) {
this.options = options;
}
apply(compiler) {
// 每当compiler对象创建出来一个compilation编译对象,就会触发回调,回调的参数就是compilation
compiler.hooks.compilation.tap('OptimizePlugin', compilation => {
// 监听compilation对象的钩子
compilation.hooks.optimize.tap('OptimizePlugin', () => {
console.log('webpack编译对象正在优化中...');
});
});
}
}
module.exports = OptimizePlugin;
4.3 EmitPlugin 监听 webpack 执行异步钩子的插件
// 优化插件,想知道如何或者compilation对象
class EmitPlugin {
constructor(options) {
this.options = options;
}
apply(compiler) {
// AsyncSeriesHook 异步的串行钩子
compiler.hooks.emit.tapAsync('EmitPlugin', (compilation, callback) => {
console.log('开始执行EmitPlugin');
setTimeout(() => {
callback(); // 等待三秒
console.log('结束执行EmitPlugin');
}, 3000);
});
}
}
// promise 版本
class EmitPromisePlugin {
constructor(options) {
this.options = options;
}
apply(compiler) {
// AsyncSeriesHook 异步的串行钩子
compiler.hooks.emit.tapPromise('EmitPromisePlugin', compilation => {
return new Promise((resolve, reject) => {
console.log('开始执行EmitPlugin promise version');
setTimeout(() => {
resolve(); // 等待三秒
console.log('结束执行EmitPlugin promise version');
}, 3000);
});
});
}
}
module.exports = { EmitPlugin, EmitPromisePlugin };
4.4 ZipPlugin 打包后的文件压缩成压缩包的插件
// 压缩文件的包
const JSZip = require('jszip');
// 引入webpack源码包
const { RawSource } = require('webpack-sources');
/**
* 写插件的一般思路:[压缩文件成为压缩包的插件]
* 1.找到写插件的代码,要执行的钩子
* 2.要知道钩子函数的参数和数据结构,进行加工
*
* compilation.assets : {
'main.js': CachedSource {
_source: ConcatSource { children: [Array] },
_cachedSource: undefined,
_cachedSize: 3887,
_cachedMaps: {},
node: [Function],
listMap: [Function]
},
'index.html': { source: [Function: source], size: [Function: size] }
}
*/
class ZipPlugin {
constructor(options) {
this.options = options;
}
apply(compiler) {
// 当资源准备就绪,准备向输出的目录里写入的时候会触发这个钩子
compiler.hooks.emit.tapAsync('ZipPlugin', (compilation, callback) => {
const zip = new JSZip();
// 遍历文件名,拿到对象上的所有属性
for (const filename in compilation.assets) {
const content = compilation.assets[filename];
// content.source()拿到资源
zip.file(filename, content.source());
}
// 把所有的文件压成压缩包,得到压缩包的二进制内容 content
zip.generateAsync({ type: 'nodebuffer' }).then(content => {
// 往compilation.assets上挂新的属性和值,里面一定要由source,size两个对象
// compilation.assets[this.options.name] = {
// source: () => content,
// size: () => content.length
// };
// 直接用RawSource类直接new
compilation.assets[this.options.name] = new RawSource(content);
// 然后调用执行下去,把compilation继续往下传递
callback(null, compilation);
});
});
}
}
module.exports = ZipPlugin;
4.5 preFetch-plugin 预获取
// /* webpackPrefetch: true */ 预加载
import(
/* webpackPrefetch: true */ /* webpackChunkName: 'name' */ './hello'
).then(result => {
console.log(result.default);
});
// plugins/PrefetchPlugin.js
class PrefetchPlugin {
constructor(options) {
this.options = options;
}
apply(compiler) {
compiler.hooks.compilation.tap('PrefetchPlugin', compilation => {
// 监听html-webpack-plugin插件产生的钩子,并直接修改html标签
compilation.hooks.htmlWebpackPluginAlterAssetTags.tapAsync(
'PrefetchPlugin',
(htmlData, callback) => {
// head: [], // 往head里面插入两个对象,就是两个link标签
// body: [ { tagName: 'script', closeTag: true, attributes: [Object] } ],
// 找到懒加载,预获取的
// 1.过滤哪些代码块是prefetch的
const chunks = compilation.chunks; // 所有的代码块都放compilation的chunks属性
const chunkObj = {}; // 把chunks数组变成对象
chunks.forEach(chunk => {
chunkObj[chunk.id] = chunk;
});
// 2.循环这个数组,拿到prefetch的代码块
chunks.forEach(chunk => {
const prefetchChunkIds = chunk.getChildIdsByOrders().prefetch;
// 3.存在的要预获取
if (prefetchChunkIds) {
// 4.进行预处理
prefetchChunkIds.forEach(prefetchChunkId => {
// '--------------------------------------------',
// todo 5.拿到上面的文件
const files = chunkObj[prefetchChunkId].files;
// 6.每个文件对应一个link标签
files.forEach(file => {
// 7.然后放在html数据中的head里面
htmlData.head.push({
tagName: 'link',
closeTag: true,
attributes: {
as: 'script',
rel: 'prefetch',
href: file // 'hello.js'
}
});
});
});
}
});
callback(null, htmlData);
}
);
});
}
}
module.exports = PrefetchPlugin;
/**
* 1.获取compilation [手工插入link标签] 写html-webpack-plugin插件的插件
* 2.监听钩子 compilation.hooks.htmlWebpackPluginAlterAssetTags = new AsyncSeriesWaterfallHook(['plugin'])
*/