插件向第三方开发者提供了 webpack 引擎中完整的能力。使用阶段式的构建回调,开发者可以引入它们自己的行为到 webpack 构建流程中。创建插件比创建 loader 更加高级,因为你将需要理解一些 webpack 底层的内部特性来做相应的钩子
对象 | 钩子 |
---|---|
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 |
webpack 插件由以下组成:
在插件开发中最重要的两个资源就是compiler
和compilation
对象。理解它们的角色是扩展webpack引擎重要的第一步。
compiler 对象代表了完整的 webpack 环境配置。这个对象在启动 webpack 时被一次性建立,并配置好所有可操作的设置,包括 options,loader 和 plugin。当在 webpack 环境中应用一个插件时,插件将收到此 compiler 对象的引用。可以使用它来访问 webpack 的主环境。
compilation 对象代表了一次资源版本构建。当运行 webpack 开发环境中间件时,每当检测到一个文件变化,就会创建一个新的 compilation,从而生成一组新的编译资源。一个 compilation 对象表现了当前的模块资源、编译生成资源、变化的文件、以及被跟踪依赖的状态信息。compilation 对象也提供了很多关键时机的回调,以供插件做自定义处理时选择使用。
webpack/lib/webpack.js:35
if (options.plugins && Array.isArray(options.plugins)) {
for (const plugin of options.plugins) {
plugin.apply(compiler);
}
}
class DonePlugin{
constructor(options) {
this.options=options;
}
apply(compiler) {
compiler.hooks.done.tap('DonePlugin', ()=> {
console.log('Hello ',this.options.name);
});
}
}
module.exports=DonePlugin;
const DonePlugin=require('./plugins/DonePlugin');
module.exports={
entry: './src/index.js',
output: {
path: path.resolve('build'),
filename:'bundle.js'
},
plugins: [
new DonePlugin({name:'zfpx'})
]
}
webpack/lib/Compiler.js:251
this.emitRecords(err => {
if (err) return finalCallback(err);
this.hooks.done.callAsync(stats, err => {});
});
class CompilationPlugin{
constructor(options) {
this.options=options;
}
apply(compiler) {
compiler.hooks.compilation.tap('CompilationPlugin',function (compilation) {
compilation.hooks.optimize.tap('optimize',function () {
console.log('资源正在被优化');
});
});
}
}
module.exports=CompilationPlugin;
webpack/lib/Compiler.js:496
newCompilation(params) {
const compilation = this.createCompilation();
this.hooks.compilation.call(compilation, params);
return compilation;
}
webpack/lib/Compilation.js:1183
seal(callback) {
this.hooks.seal.call();
this.hooks.optimize.call();
}
关于 compiler, compilation 的可用回调,和其它重要的对象的更多信息,请查看 插件 文档。
class CompilationAsyncPlugin{
constructor(options) {
this.options=options;
}
apply(compiler) {
compiler.hooks.emit.tapAsync('EmitPlugin',function (compilation,callback) {
setTimeout(function () {
console.log('异步任务完成');
callback();
},500);
});
}
}
module.exports=CompilationAsyncPlugin;
emit
事件在即将写入文件前触发
webpack/lib/Compiler.js:364this.hooks.emit.callAsync(compilation, err => {
if (err) return callback(err);
outputPath = compilation.getPath(this.outputPath);
this.outputFileSystem.mkdirp(outputPath, emitFiles);
});
const { RawSource } = require("webpack-sources");
const JSZip = require("jszip");
const path = require("path");
const fs = require("fs");
class ZipPlugin {
constructor(options) {
this.options = options;
}
apply(compiler) {
let that = this;
compiler.hooks.emit.tapAsync("FileListPlugin", (compilation, callback) => {
var zip = new JSZip();
for (let filename in compilation.assets) {
const source = compilation.assets[filename].source();
zip.file(filename, source);
}
zip.generateAsync({ type: "nodebuffer" }).then(content => {
const outputPath = path.join(
compilation.options.output.path,
this.options.filename
);
compilation.assets[that.options.filename] = new RawSource(content);
callback();
});
});
}
}
module.exports = ZipPlugin;
class PrefetchPlugin {
constructor() {
this.name = "PrefetchPlugin";
}
apply(compiler) {
compiler.hooks.compilation.tap(this.name, compilation => {
compilation.hooks.htmlWebpackPluginAlterAssetTags.tapAsync(
this.name,
(htmlPluginData, callback) => {
const chunkNames = htmlPluginData.plugin.options.chunks || "all";
const excludeChunkNames =
htmlPluginData.plugin.options.excludeChunks || [];
let chunks={};
let allPrefetchChunkIds = compilation.chunks
.filter(chunk => {
chunks[chunk.id] = chunk;
if (chunkNames === "all") {
return excludeChunkNames.indexOf(chunk.id) === -1&&(!!chunk.getChildIdsByOrders().prefetch);
}
return (
chunkNames.indexOf(chunk.id) !== -1 &&excludeChunkNames.indexOf(chunk.id) === -1&&(!!chunk.getChildIdsByOrders().prefetch)
);
}).map(chunk => chunk.getChildIdsByOrders().prefetch);
let linkTags = [];
allPrefetchChunkIds.forEach(prefetchChunkIds=>{
prefetchChunkIds.forEach(chunkId=>{
chunks[chunkId].files&&chunks[chunkId].files.forEach(file=>{
linkTags.push({
tagName: "link",
closeTag: true,
attributes: { rel: "prefetch",as:"script",href: file}
});
});
});
});
htmlPluginData.head.push(...linkTags);
callback(null, htmlPluginData);
}
);
});
}
}
module.exports = PrefetchPlugin;
生成分析文件
webpack --profile --json > stats.json
能否检测代码中的import自动处理这个步骤?
external
和script
的问题,需要怎么实现,该从哪方面开始考虑依赖
当检测到有import
该library
时,将其设置为不打包类似exteral
,并在指定模版中加入script,那么如何检测import?这里就用Parser
external依赖
需要了解external是如何实现的,webpack的external是通过插件ExternalsPlugin
实现的,ExternalsPlugin通过tap
NormalModuleFactory
在每次创建Module的时候判断是否是ExternalModule
Parser
获取需要指定类型moduleType,一般使用javascript/auto
即可plugins: [
new HtmlWebpackPlugin({
template: './src/index.html',
filename:'index.html'
}),
new AutoExternalPlugin({
jquery: {
expose: '$',
url: 'https://cdn.bootcss.com/jquery/3.1.0/jquery.js'
}
})
]
const ExternalModule = require('webpack/lib/ExternalModule');
class AutoExternalPlugin {
constructor(options) {
this.options = options;
this.externalModules = {};
}
apply(compiler) {
//1.在解析语法树的过程中查找那些需要外部引入的模块名称
compiler.hooks.normalModuleFactory.tap('AutoExternalPlugin', normalModuleFactory => {
normalModuleFactory.hooks.parser
.for('javascript/auto')
.tap('AutoExternalPlugin', parser => {
parser.hooks.import.tap('AutoExternalPlugin', (statement, source) => {
if (this.options[source])
this.externalModules[source] = true;
});
});
//2.在生产模块的过程中发现如果是外部模块则返回外部模块
normalModuleFactory.hooks.factory.tap('AutoExternalPlugin', factory => (data, callback) => {
const dependency = data.dependencies[0];
let value = dependency.request;
if (this.externalModules[value]) {
let varName = this.options[value].expose;
callback(null, new ExternalModule(varName, 'window'));
} else {
factory(data, callback);
}
});
});
compiler.hooks.compilation.tap('AutoExternalPlugin', compilation => {
//3.向body底部插入全局变量的脚本
compilation.hooks.htmlWebpackPluginAlterAssetTags.tapAsync('normalModuleFactory', (htmlPluginData, callback) => {
Object.values(this.externalModules).forEach(src => {
htmlPluginData.body.unshift({
tagName: 'script',
closeTag: true,
attributes: { type: 'text/javascript', src }
});
});
});
});
}
}
module.exports = AutoExternalPlugin;
const qiniu = require('qiniu');
const path = require('path');
const fs = require('fs');
class UploadPlugin {
constructor(options = {}) {
let { bucket = '', domain = "", accessKey = '', secretKey = '' } = options;
let mac = new qiniu.auth.digest.Mac(accessKey, secretKey);
let putPolicy = new qiniu.rs.PutPolicy({ scope: bucket });
this.uploadToken = putPolicy.uploadToken(mac);
let config = new qiniu.conf.Config();
this.formUploader = new qiniu.form_up.FormUploader(config);
this.putExtra = new qiniu.form_up.PutExtra();
}
apply(compiler) {
compiler.hooks.afterEmit.tapPromise('UploadPlugin', compilation => {
let assets = compilation.assets;
let promises = Object.entries(assets).map(([key, value]) => this.upload(key, value.source()));
return Promise.all(promises);
});
}
upload(key, value) {
return new Promise((resolve, reject) => {
this.formUploader.put(this.uploadToken, key, value, this.putExtra, (err, body, info) => {
err ? reject(err) : resolve(body);
});
});
}
}
module.exports = UploadPlugin;
new UploadPlugin({
bucket: 'cnpmjs',
domain: "img.zhufenpeixun.cn",
accessKey: 'fi5imW04AkxJItuFbbRy1ffH1HIoo17HbWOXw5fV',
secretKey: 'ru__Na4qIor4-V7U4AOJyp2KBUYEw1NWduiJ4Pby'
})