自定义webpack plugin 和 loader 为项目赋能

96 阅读3分钟

1. 自定义webpack plugin

首先要自定义一个webpack plugin一定要了解tapable, 因为webpack的主要流程是基于tapable这个库来设计的;首先要了解tapable的各种钩子的作用,你才能更容易看懂webpack的设计机制.

简要介绍tapable

1.以sync开头的,是同步的Hook2.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;
    });
};