1. 自定义webpack plugin
首先要自定义一个webpack plugin一定要了解tapable, 因为webpack的主要流程是基于tapable这个库来设计的;首先要了解tapable的各种钩子的作用,你才能更容易看懂webpack的设计机制.
简要介绍tapable
1.以sync开头的,是同步的Hook;
2.以async开头的,两个事件处理回调,不会等待上一次处理回调结束后再执行下一次回调
3.Paralle关键字并行,会同时执行次事件处理回调结束,才执行下一次事件处理回调;
4.Series串行,会等待上一是异步的Hook
const {
SyncHook, // 普通钩子 全部都会正常触发
SyncBailHook, // 当有返回值时,就不会执行后续的事件触发了
SyncWaterfallHook,// 当返回值不为undefined时,会将这次返回的结果作为下次事件的第一个参数
SyncLoopHook, // 当返回值为true,就会反复执行该事件,当返回值为undefined或者不返回内容,就退出事件;
AsyncParallelHook,
AsyncParallelBailHook,
AsyncSeriesHook,
AsyncSeriesBailHook,
AsyncSeriesWaterfallHook
} = require("tapable");
// 使用案例
class Car {
constructor() {
this.hooks = {
//Hook构造函数都采用一个可选参数,即参数名称列表
accelerate: new SyncHook(["newSpeed"]),
brake: new SyncHook(),
// 串行hook
asyncHook: new AsyncSeriesHook(["name", "price"])
// 并行hook
calculateRoutes: new AsyncParallelHook(["source", "target", "routesList"])
};
}
emit(){
this.hooks.brake.call()
}
}
const myCar = new Car();
// 同步事件 使用tap进行监听
myCar.hooks.brake.tap("WarningLampPlugin", () => warningLamp.on());
myCar.hooks.brake.call()
// 串行异步事件 tapAsync监听
myCar.hooks.asyncHook.tapAsync("event1", (name, price, callback) => {
setTimeout(()=>{
console.log(name, price)
callback() // 调用cb表示执行完成
}, 2000)
});
myCar.hooks.asyncHook.callAsync('BMW', '30W', () => {
// 当tapAsync执行callback时, 此函数就会触发
console.log("本次事件执行完成")
})
// 串行异步事件 tapPromise监听
myCar.hooks.asyncHook.tapPromise("event2", (name, price) => {
return new Promise((reslove) => {
setTimeout(()=>{
console.log(name, price)
resolve()
}, 2000)
})
});
myCar.hooks.asyncHook.promise('BMW', '30W').then(() => {
console.log("本次事件执行完成")
})
编写webpack plugin
搞明白tapable的用法后,我们只需要在官方文档查看webpack各个生命周期的钩子,即可随意我们操作
案例:编写一个自动上传打包后的文件至服务器 并备份
我们先确认使用方式
module.exports={
module:{
plugins:[
// 上传远程服务器
new GoldfingerPlugin({
host: 'your remote host',
username: 'your remote username',
password: 'your remote password',
remotePath: 'your remote path',
}),
]
}
}
具体实现
// 我们借助node-ssh这个库实现与服务器ssh连接
const { NodeSSH } = require('node-ssh')
const chalk = require('chalk')
class AutoUpload {
constructor(options) {
this.ssh = new NodeSSH()
this.options = options
}
apply(compiler) {
// 我们要在打包输出文件后,所以选择afterEmit钩子
compiler.hooks.afterEmit.tapAsync("autoUpload", async (compilation, callback) => {
if(this.options.remotePath) {
// 获取当前时间作为备份文件夹名称
let time = this.getNowFormatDate()
// 从compilation获取输出目录
const outputPath = compilation.outputOptions.path
// 连接
await this.connectServer()
// 以下为linux命令行操作
const serverDir = this.options.remotePath
let result = await this.ssh.execCommand(`find ${serverDir} -maxdepth 1 -printf "%f\n"`)
let fileNameList = result.stdout.split('\n').slice(1)
if(!fileNameList.length || this.options.power === 'root' || (fileNameList.includes('css')&&fileNameList.includes('fonts')&&fileNameList.includes('config')
&&fileNameList.includes('resources')&&fileNameList.includes('worker')&&fileNameList.includes('index.html'))){
if(fileNameList.length) {
await this.ssh.execCommand(`mkdir -p ${serverDir}/_bk/${time}`)
await this.ssh.execCommand(`rsync -av --exclude='_bk' ${serverDir}/* ${serverDir}/_bk/${time}/`)
await this.ssh.execCommand(`rm -rf ${serverDir}/* !(_bk)`)
}
// 传文件
await this.uploadFiles(outputPath, serverDir)
// 关闭连接
this.ssh.dispose()
}else{
console.log(chalk.red('上传失败, 设置的上传目录并非空目录或者目录内没有匹配到前端部署痕迹!'))
}
}
callback()
})
}
async connectServer() {
await this.ssh.connect({
host:this.options.host,
username:this.options.username,
password:this.options.password
})
}
async uploadFiles(localPath, remotePath) {
const status = await this.ssh.putDirectory(localPath, remotePath, {
recursive:true, // 递归目录上传
concurrency:10
})
if(status){
console.log(chalk.green('传输服务器成功!'))
}else{
console.log(chalk.red('传输至服务器失败!'))
}
}
getNowFormatDate() {
var date = new Date();
var seperator1 = "";
var seperator2 = "";
var month = date.getMonth() + 1;
var strDate = date.getDate();
var strHours = date.getHours();
var strMin = date.getMinutes();
var strSec = date.getSeconds();
if (month >= 1 && month <= 9) {
month = "0" + month;
}
if (strDate >= 0 && strDate <= 9) {
strDate = "0" + strDate;
}
if (strHours >= 0 && strHours <= 9) {
strHours = "0" + strHours;
}
if (strMin >= 0 && strMin <= 9) {
strMin = "0" + strMin;
}
if (strSec >= 0 && strSec <= 9) {
strSec = "0" + strSec;
}
var currentdate = date.getFullYear() + seperator1 + month + seperator1 + strDate
+ "" + strHours + seperator2 + strMin + seperator2 + strSec;
return currentdate;
}
}
module.exports = AutoUpload
2. 自定义webpack loader
loader编写相对较为简单, 这里有官网文档, 让我们直接上手吧
编写一个pxtorem的loader
我们先确认使用方式
module.exports={
module:{
rules:[
{
test:/\.(css|scss)$/,
loader:'pxtorem',
options:{
// 1rem=npx 默认为 10
basePx:10,
// 只会转换大于min的px 默认为0
// 因为很小的px(比如border的1px)转换为rem后在很小的设备上结果会小于1px,有的设备就会不显示
min:1,
// 转换后的rem值保留的小数点后位数 默认为3
floatWidth:3
}
}
]
}
}
具体实现
var _ = require('lodash');
module.exports = function (source) {
if (_.isUndefined(this.cacheable)) return source;
this.cacheable();
var query = this.getOptions(),
basePx = !_.isUndefined(query.basePx) ? query.basePx : 10,
min = !_.isUndefined(query.min) ? query.min : 0,
exclude = !_.isUndefined(query.exclude) ? query.exclude : null,
floatWidth = !_.isUndefined(query.floatWidth) ? query.floatWidth : 3,
matchPXExp = /([0-9,.]+px)([;| |}|'|"|);|/])/g;
return source.replace(matchPXExp, function (match, m1, m2) {
var pxValue = parseFloat(m1.slice(0, m1.length - 2));
if (pxValue <= min) return match;
if (exclude&& pxValue == exclude) return match;
var remValue = pxValue / basePx;
if (pxValue % basePx != 0)
remValue = (pxValue / basePx).toFixed(floatWidth);
return remValue + 'rem' + m2;
});
};