3

开发webpack插件需要知道的几个必要条件:

  • 获取编译器 compiler 对象,通过这个对象能过获取包括config配置,资源文件,编译信息,钩子函数等信息
  • 编译阶段的生命周期函数,找到适合的钩子函数处理对应逻辑
  • 返回结果支持同步和异步两种方式

获取compiler实例

第一步获取 compiler 实例对象:

// helloPlugin.js
module.exports = class RemoveLogs {
    constructor(options){
        this.options = options
    }
    apply(compiler) {
        console.log(`Hello ${this.options.name}`)
    }
};

引入这个脚本,在控制台执行就能看到编译结果了:

// webpack.config.js
var HelloWorldPlugin = require('./helloPlugin.js');

module.exports = {
  // 配置插件
  plugins: [new HelloWorldPlugin({ name:"chenwl" })]
};

生命周期钩子函数

通过官方文档 compiler-hooks 可以查看到compiler 提供的钩子函数,也可以直接到 /node_modules/webpack/lib/Compiler.js 查看

同步和异步方式

钩子函数可以同步也可以异步的方式处理:

module.exports = class SyncPlugin {
    apply(compiler){
        // tap 同步
        compiler.hooks.emit.tap("tap", (compilation) => {
          console.log("***** tap *****")
        })
        // tapAsync 参数cb未调用之前进程会暂停
        compiler.hooks.emit.tapAsync("tapAsync", (compilation,cb) => {
          start(0);
          function start(index){
              console.log(index);
              if(index<=3){
                  setTimeout(() => {
                      start(++index);
                  }, 1000);
              }else{
                  cb()
              }
          }
        })
        // tapPromise 通过promise的方式调用
        compiler.hooks.emit.tapPromise("tapPromise", (compilation)=>{
            return new Promise((resolve,reject)=>{
                console.log("start tap-promise");
                setTimeout(()=>{
                    resolve()
                },2000)
            })
        })
    }
}

logRemoverPlugin

文件编译完成后,去掉console

// logRemoverPlugin.js

const fs = require("fs");
module.exports = class RemoveLogs {
  apply(compiler) {
    compiler.hooks.done.tap("RemoveLogs", stats => {
      const { path, filename } = stats.compilation.options.output;
      try {
        // 这里可以做匹配到 filename 才做处理
        let filePath = path + "/" + filename;
        fs.readFile(filePath, "utf8", (err, data) => {
          const rgx = /console.log\(['|"](.*?)['|"]\)/;
          const newdata = data.replace(rgx, "");
          if (err) console.log(err);
          fs.writeFile(filePath, newdata, function(err) {
            if (err) {
              return console.log(err)
            }
            console.log("Logs Removed");
          });
        });
      } catch (error) {
        console.log(error)
      }
    });
  }
};

AnalyzePlugin

分析打包后的资源文件信息,并生成文件:

文件名文件大小
index.html1266
文件总数 1 个

// AnalyzePlugin.js
const { compilation } = require("webpack")

module.exports = class Analyze {
    constructor(config){
        // 获取打包文件名
        this.filename = config.filename;
    }
    apply(compiler){
        compiler.hooks.emit.tap("analyze-plugin",(compilation)=>{
            const assets = compilation.assets;
            const entries = Object.entries(assets);
            const content = `| 文件名  |  文件大小 |
| ------------ | ------------ |
`
            entries.forEach(([filename,fileObj])=>{
                content+=`|${filename}|${fileObj.size()}|
`
            });
            content += `

> 文件总数 ${entries.length} 个`
            // console.log(this.filename)
            compilation.assets[this.filename] = {
                source(){
                    return content
                },
                size(){
                    return content.length
                }
            }
        })
    }
}

inlinePlugin

将资源文件插入到html中

  • 获取 head 标签组和 body 标签组
  • link标签转成style标签,获取link属性链接的样式内容,插入到style标签内部
  • script标签获取src属性链接的脚本内容,插入到script标签内部
const HtmlWebpackPlugin = require("html-webpack-plugin");

module.exports = class InlinePlugin {
  constructor(config) {
    this.match = config.match; // 匹配需要转换的文件
    this.compilation = null; // 保存 compilation
  }
  processTag(tag) {
    if (!this.compilation) return;

    // 获取文件链接
    const url = tag.attributes.href || tag.attributes.src;
    // 获取文件内容
    const source = this.compilation.assets[url].source()
    
    if (!this.match || !this.match.test(url)) return tag;
    
    if (tag.tagName === "link") {
        tag = {
          tagName: "style",
          innerHTML: source
        }
    }
    if (tag.tagName === "script") {
        tag = {
            tagName: "script",
            innerHTML: source
        }
    }

    delete this.compilation.assets[url];

    return tag
  }
  processTags(data) {
    let headTags = data.headTags
    let bodyTags = data.bodyTags

    headTags = headTags.map((tag) => {
      return this.processTag(tag)
    })
    bodyTags = bodyTags.map((tag) => {
      return this.processTag(tag)
    });

    return {
      headTags,
      bodyTags,
    }
  }
  apply(compiler) {
    compiler.hooks.compilation.tap("MyPlugin", (compilation) => {
      HtmlWebpackPlugin.getHooks(compilation).alterAssetTagGroups.tapAsync(
        "MyPlugin",
        (data, cb) => {
          // 保存 compilation
          this.compilation = compilation;
          cb(null, this.processTags(data))
        }
      )
    })
  }
}

编写将文件上传到七牛云的plugin

UploadQiNiuPlugin

首先要安装qiniu云的依赖包

npm install qiniu
const path = require("path")
const qiniu = require("qiniu")

module.exports = class UploadQiNiuPlugin {
  constructor(options) {
    let { bucket = "", accessKey = "", secretKey = "" } = options
    let mac = new qiniu.auth.digest.Mac(accessKey, secretKey)
    let putPolicy = new qiniu.rs.PutPolicy({ scope: bucket })

    this.outputPath = ""
    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()
  }
  upload(filename) {
    return new Promise((resolve, reject) => {
      let realPath = path.join(this.outputPath, filename)
      // 上传文件
      this.formUploader.putFile(
        this.uploadToken,
        filename,
        realPath,
        this.putExtra,
        (err, body) => {
          err ? reject(err) : resolve(body)
        }
      )
    })
  }
  apply(compiler) {
    compiler.hooks.afterEmit.tapPromise("upload-plugin", (compilation) => {
      this.outputPath = compiler.outputPath
      let assets = compilation.assets
      let promises = []
      Object.keys(assets).forEach((filename) => {
        promises.push(this.upload(filename))
      })
      return Promise.all(promises)
    })
  }
}

QiniuManager

上传之前,可能要先删掉七牛云旧的资源文件,这里也写个工具:

class QiniuManager {
  constructor({ bucket, accessKey, secretKey }) {
    let mac = new qiniu.auth.digest.Mac(accessKey, secretKey)
    let config = new qiniu.conf.Config()
    this.bucketManager = new qiniu.rs.BucketManager(mac, config)
  }
  deleteFiles(filenames) {
    let deleteFile = (filename) => {
      return new Promise((resolve, reject) => {
        this.bucketManager.delete(bucket, filename, (err) =>
          err ? reject(err) : resolve(filename)
        )
      })
    }
    let deletePromises = filenames.map((f) => deleteFile(f))
    return Promise.all(deletePromises)
  }
}

chenwl
117 声望5 粉丝

平坦的路面上曲折前行