Webpack经过Loader完成模块的转换工做,让“一切皆模块”成为可能。Plugin机制则让其更加灵活,能够在Webpack生命周期中调用钩子完成各类任务,包括修改输出资源、输出目录等等。html
今天咱们一块儿来学习如何编写Webpack插件[1]。前端
在编写插件以前,还须要了解一下Webpack的构建流程,以便在合适的时机插入合适的插件逻辑。Webpack的基本构建流程以下:node
run/watch
:若是运行在watch模式则执行watch方法,不然执行run方法
compilation
:建立Compilation对象回调compilation相关钩子
emit
:文件内容准备完成,准备生成文件,这是最后一次修改最终文件的机会
afterEmit
:文件已经写入磁盘完成
done
:完成编译
一个典型的Webpack插件代码以下:webpack
// 插件代码
class MyWebpackPlugin {
constructor(options) {
}
apply(compiler) {
// 在emit阶段插入钩子函数
compiler.hooks.emit.tap('MyWebpackPlugin', (compilation) => {});
}
}
module.exports = MyWebpackPlugin;
复制代码
接下来须要在webpack.config.js中引入这个插件。web
module.exports = {
plugins:[
// 传入插件实例
new MyWebpackPlugin({
param:'paramValue'
}),
]
};
复制代码
Webpack在启动时会实例化插件对象,在初始化compiler对象以后会调用插件实例的apply方法,传入compiler对象,插件实例在apply方法中会注册感兴趣的钩子,Webpack在执行过程当中会根据构建阶段回调相应的钩子。npm
在编写Webpack插件过程当中,最经常使用也是最主要的两个对象就是Webpack提供的Compiler和Compilation,Plugin经过访问Compiler和Compilation对象来完成工做。json
Webpack会根据执行流程来回调对应的钩子,下面咱们来看看都有哪些常见钩子,这些钩子支持的tap操做是什么。api
钩子 | 说明 | 参数 | 类型 |
---|---|---|---|
afterPlugins | 启动一次新的编译 | compiler | 同步 |
compile | 建立compilation对象以前 | compilationParams | 同步 |
compilation | compilation对象建立完成 | compilation | 同步 |
emit | 资源生成完成,输出以前 | compilation | 异步 |
afterEmit | 资源输出到目录完成 | compilation | 异步 |
done | 完成编译 | stats | 同步 |
Tapable是Webpack的一个核心工具,Webpack中许多对象扩展自Tapable类。Tapable类暴露了tap、tapAsync和tapPromise方法,能够根据钩子的同步/异步方式来选择一个函数注入逻辑。服务器
tap是一个同步钩子,同步钩子在使用时不能够包含异步调用,由于函数返回时异步逻辑有可能未执行完毕致使问题。app
下面一个在compile阶段插入同步钩子的示例。
compiler.hooks.compile.tap('MyWebpackPlugin', params => {
console.log('我是同步钩子')
});
复制代码
tapAsync是一个异步钩子,咱们能够经过callback告知Webpack异步逻辑执行完毕。
下面是一个在emit阶段的示例,在1秒后打印文件列表。
compiler.hooks.emit.tapAsync('MyWebpackPlugin', (compilation, callback) => {
setTimeout(()=>{
console.log('文件列表', Object.keys(compilation.assets).join(','));
callback();
}, 1000);
});
复制代码
tapPromise也是也是异步钩子,和tapAsync的区别在于tapPromise是经过返回Promise来告知Webpack异步逻辑执行完毕。
下面是一个将生成结果上传到CDN的示例。
compiler.hooks.afterEmit.tapPromise('MyWebpackPlugin', (compilation) => {
return new Promise((resolve, reject) => {
const filelist = Object.keys(compilation.assets);
uploadToCDN(filelist, (err) => {
if(err) {
reject(err);
return;
}
resolve();
});
});
});
复制代码
apply方法中插入钩子的通常形式以下:
compileer.hooks.阶段.tap函数('插件名称', (阶段回调参数) => {
});
复制代码
在emit阶段,咱们能够读取最终须要输出的资源、chunk、模块和对应的依赖,若是有须要还能够更改输出资源。
apply(compiler) {
compiler.hooks.emit.tapAsync('MyWebpackPlugin', (compilation, callback) => {
// compilation.chunks存放了代码块列表
compilation.chunks.forEach(chunk => {
// chunk包含多个模块,经过chunk.modulesIterable能够遍历模块列表
for(const module of chunk.modulesIterable) {
// module包含多个依赖,经过module.dependencies进行遍历
module.dependencies.forEach(dependency => {
console.log(dependency);
});
}
});
callback();
});
}
复制代码
经过操做compilation.assets对象,咱们能够添加、删除、更改最终输出的资源。
apply(compiler) {
compiler.hooks.emit.tapAsync('MyWebpackPlugin', (compilation) => {
// 修改或添加资源
compilation.assets['main.js'] = {
source() {
return 'modified content';
},
size() {
return this.source().length;
}
};
// 删除资源
delete compilation.assets['main.js'];
});
}
复制代码
assets对象须要定义source
和size
方法,source方法返回资源的内容
,支持字符串和Node.js的Buffer,size返回文件的大小字节数
。
接下来咱们开始编写自定义插件,全部插件使用的示例项目以下(须要安装webpack
和webpack-cli
):
|----src
|----main.js
|----plugins
|----my-webpack-plugin.js
|----package.json
|----webpack.config.js
复制代码
相关文件的内容以下:
// src/main.js
console.log('Hello World');
复制代码
// package.json
{
"scripts":{
"build":"webpack"
}
}
复制代码
const path = require('path');
const MyWebpackPlugin = require('my-webpack-plugin');
// webpack.config.js
module.exports = {
entry:'./src/main',
output:{
path: path.resolve(__dirname, 'build'),
filename:'[name].js',
},
plugins:[
new MyWebpackPlugin()
]
};
复制代码
经过在emit
阶段操做compilation.assets
实现。
class MyWebpackPlugin {
apply(compiler) {
compiler.hooks.emit.tapAsync('MyWebpackPlugin', (compilation, callback) => {
const manifest = {};
for (const name of Object.keys(compilation.assets)) {
manifest[name] = compilation.assets[name].size();
// 将生成文件的文件名和大小写入manifest对象
}
compilation.assets['manifest.json'] = {
source() {
return JSON.stringify(manifest);
},
size() {
return this.source().length;
}
};
callback();
});
}
}
module.exports = MyWebpackPlugin;
复制代码
构建完成后会在build目录添加manifest.json,内容以下:
{"main.js":956}
复制代码
在实际开发中,资源文件构建完成后通常会同步到CDN,最终前端界面使用的是CDN服务器上的静态资源。
下面咱们编写一个Webpack插件,文件构建完成后上传的七牛CDN。
咱们的插件依赖qiniu,所以须要额外安装qiniu模块
npm install qiniu --save-dev
复制代码
七牛的Node.js SDK文档地址以下:
https://developer.qiniu.com/kodo/sdk/1289/nodejs
复制代码
开始编写插件代码:
const qiniu = require('qiniu');
const path = require('path');
class MyWebpackPlugin {
// 七牛SDK mac对象
mac = null;
constructor(options) {
// 读取传入选项
this.options = options || {};
// 检查选项中的参数
this.checkQiniuConfig();
// 初始化七牛mac对象
this.mac = new qiniu.auth.digest.Mac(
this.options.qiniu.accessKey,
this.options.qiniu.secretKey
);
}
checkQiniuConfig() {
// 配置未传qiniu,读取环境变量中的配置
if (!this.options.qiniu) {
this.options.qiniu = {
accessKey: process.env.QINIU_ACCESS_KEY,
secretKey: process.env.QINIU_SECRET_KEY,
bucket: process.env.QINIU_BUCKET,
keyPrefix: process.env.QINIU_KEY_PREFIX || ''
};
}
const qiniu = this.options.qiniu;
if (!qiniu.accessKey || !qiniu.secretKey || !qiniu.bucket) {
throw new Error('invalid qiniu config');
}
}
apply(compiler) {
compiler.hooks.afterEmit.tapPromise('MyWebpackPlugin', (compilation) => {
return new Promise((resolve, reject) => {
// 总上传数量
const uploadCount = Object.keys(compilation.assets).length;
// 已上传数量
let currentUploadedCount = 0;
// 七牛SDK相关参数
const putPolicy = new qiniu.rs.PutPolicy({ scope: this.options.qiniu.bucket });
const uploadToken = putPolicy.uploadToken(this.mac);
const config = new qiniu.conf.Config();
config.zone = qiniu.zone.Zone_z1;
const formUploader = new qiniu.form_up.FormUploader()
const putExtra = new qiniu.form_up.PutExtra();
// 由于是批量上传,须要在最后将错误对象回调
let globalError = null;
// 遍历编译资源文件
for (const filename of Object.keys(compilation.assets)) {
// 开始上传
formUploader.putFile(
uploadToken,
this.options.qiniu.keyPrefix + filename,
path.resolve(compilation.outputOptions.path, filename),
putExtra,
(err) => {
console.log(`uploade ${filename} result: ${err ? `Error:${err.message}` : 'Success'}`)
currentUploadedCount++;
if (err) {
globalError = err;
}
if (currentUploadedCount === uploadCount) {
globalError ? reject(globalError) : resolve();
}
});
}
})
});
}
}
module.exports = MyWebpackPlugin;
复制代码
Webpack中须要传递给该插件传递相关配置:
module.exports = {
entry: './src/index',
target: 'node',
output: {
path: path.resolve(__dirname, 'build'),
filename: '[name].js',
publicPath: 'CDN域名'
},
plugins: [
new CleanWebpackPlugin(),
new QiniuWebpackPlugin({
qiniu: {
accessKey: '七牛AccessKey',
secretKey: '七牛SecretKey',
bucket: 'static',
keyPrefix: 'webpack-inaction/demo1/'
}
})
]
};
复制代码
编译完成后资源会自动上传到七牛CDN,这样前端只用交付index.html便可。
至此,Webpack相关经常使用知识和进阶知识都介绍完毕,须要各位读者在工做中去多加探索,Webpack配合Node.js生态,必定会涌现出更多优秀的新语言和新工具!
Webpack Plugin: https://webpack.js.org/api/plugins/