4. plugin专

399 阅读7分钟

文件列表插件

/*
最终结果
## 文件名    资源大小
- bundle.js    32157
- bundle.js.map    30826
- index.html    317
*/

class FileListPlugin{
    constructor({filename}) {
        this.filename = filename;
    }
    apply(compiler) {
        compiler.hooks.emit.tap("FileListPlugin", (compilation, cb) => {
            // 所有的资源都挂在compilation.assets属性上, 可以在属性上再加一个文件 这样就会被打包生成出来
            let assets = compilation.assets;
            let content = `## 文件名    资源大小\r\n`;
            Object.entries(assets).forEach(element => {
                let [filename, statObj] = element;
                content += `- ${filename}    ${statObj.size()}\r\n`
            })
            assets[this.filename] = {
                source(){
                    return content;
                },
                size() {
                    return content.length;
                }
            }
        })
    }
}
module.exports = FileListPlugin;

内联webpack插件

// 将外链标签变成内联
const HtmlWebpackPlugin = require("html-webpack-plugin");
class InlineSourcePlugin{
    constructor({match}) {
        this.reg = match;  // 正则
    }
    apply(compiler) {
        // 要通过htmlwebpackplugin实现这个功能
        compiler.hooks.compilation.tap("InlineSourcePlugin", (compilation) => {
            HtmlWebpackPlugin.getHooks(compilation).alterAssetTagGroups.tapAsync("alterPlugin", (data,cb) => {
                data = this.processTags(data, compilation); // 因为资源内容都放在compilation.assets上
                cb(null,data);
            })
        })
    }
    processTags(data, compilation) { // 处理引入标签的数据
        let headTags = [];
        let bodyTags = [];
        data.headTags.forEach(headTag => {
            headTags.push(this.processTag(headTag, compilation))
        })
        data.bodyTags.forEach(bodyTag => {
            bodyTags.push(this.processTag(bodyTag, compilation))
        })
        return {...data, headTags, bodyTags};
    }

    processTag(tag, compilation) { // 处理某一个标签
        let newTag, url;
        if(tag.tagName === "link" && this.reg.test(tag.attributes.href)){
            newTag = {
                tagName: "style",
                attributes:{type:"text/css"}
            };
            url = tag.attributes.href;
        }
        if(tag.tagName === "script" && this.reg.test(tag.attributes.src)){
            newTag = {
                tagName: "script",
                attributes:{type:"application/javascript"}
            }
            url = tag.attributes.src;
        }
        if(url) {
            newTag.innerHTML = compilation.assets[url].source(); // 文件的内容 放到innerHTML属性上
            delete compilation.assets[url]; // 删除掉原资源 否则还是会被单独打包生成文件
            return newTag;
        }
        return tag;
    }
}
module.exports = InlineSourcePlugin;



tapable

  • tapable是插件的核心,也是webpack工作流的核心
  • webpack实现插件机制的大体方式是
    • 创建
    • 注册
    • 调用
  • tapable分类
    • 同步
    • SyncHook
    • SyncBailHook
    • SyncWaterfallHook
    • SyncLoopHook
    • 异步
    • AsyncParallelHook
    • AsyncParallelBailHook
    • AsyncSeriesHook 异步串行
    • AsyncSeriesBailHook
    • AsyncSeriesWaterfallHook 异步串行钩子,会传递返回的参数
  • bail
    • 执行每一个事件函数,遇到第一个结果 result !== undefined 则返回 不再继续执行
    • 有返回结果就结束
  • waterfall
    • 如果前一个事件函数的结果result !== undefined,则result会作为后一个事件函数的第一个参数
  • loop
    • 不停的循环执行事件函数,直到所有函数结果result == undefined
    • 返回值不为undefined 就重新从头开始执行
  • tapable同步钩子用法
    • 实例化钩子函数
    • tap注册
    • call触发
const { SyncHook } = require("tapable");
// SyncHook
// 参数是一个数组,参数的长度有用,代表取真实的call的参数的个数
// 数组中字符串的名字随意
const syncHook = new SyncHook(["name", "age"]);
// 注册事件函数
syncHook.tap("1", (name, age) => {
    console.log(1, age, name);
});
syncHook.tap("2", (name, age) => {
    console.log(2, age, name);
});
syncHook.tap("3", (name, age) => {
    console.log(3, age, name);
});
// 触发事件函数
syncHook.call("zhuzhu", "28");



const { SyncBailHook } = require("tapable");
// SyncBailHook与SyncHook都是顺序执行,唯一的不同是SyncBailHook如果有返回值就不再继续执行
// 参数是一个数组,参数的长度有用,代表取真实的call的参数的个数
// 数组中字符串的名字随意
const syncBailHook = new SyncBailHook(["name", "age"]);
// 注册事件函数
syncBailHook.tap("1", (name, age) => {
    console.log(1, age, name);
});
syncBailHook.tap("2", (name, age) => {
    console.log(2, age, name);
    return 2;
    // 下面的事件函数不再执行
});
syncBailHook.tap("3", (name, age) => {
    console.log(3, age, name);
});
// 触发事件函数
syncBailHook.call("zhuzhu", "28")




const { SyncWaterfallHook } = require("tapable");
// 事件函数中的name参数的值是往上找,离得最近的return的非undefined的值
const SyncWaterfallHook = new SyncWaterfallHook(["name", "age"]);
// 注册事件函数
SyncWaterfallHook.tap("1", (name, age) => {
    console.log(1, age, name);
    return 1
});
SyncWaterfallHook.tap("2", (name, age) => {
    console.log(2, age, name);
});
SyncWaterfallHook.tap("3", (name, age) => {
    console.log(3, age, name);
});
// 触发事件函数
SyncWaterfallHook.call("zhuzhu", "28")



// SyncWaterfallHook
// 不停的循环执行事件函数 知道函数返回的结果都是undefined为止
// 每次循环都是从头开始
  • tapable异步钩子用法
    • 实例化钩子函数
    • Async异步钩子注册方式更加多样化 有3种
      • tap注册 callAsync触发
      • tapAsync注册 callAsync触发
      • tapPromise注册 promise触发
  • 异步会等待 什么时候调用callback 事件函数才结束
const { AsyncParallelHook } = require("tapable");
// 事件函数中的name参数的值是往上找,离得最近的return的非undefined的值
const AsyncParallelHook = new AsyncParallelHook(["name", "age"]);
// 注册事件函数
AsyncParallelHook.tap("1", (name, age) => {
    console.log(1, age, name);
    return 1
});
AsyncParallelHook.tap("2", (name, age) => {
    console.log(2, age, name);
});
AsyncParallelHook.tap("3", (name, age) => {
    console.log(3, age, name);
});
// 触发事件函数
AsyncParallelHook.callAsync("zhuzhu", "28",(err)=>{
    console.log('err', err);
})


// tapAsync注册
// 注册的异步函数同时执行 全部执行完成后执行最终的回调函数

const { AsyncParallelHook } = require("tapable");
// 事件函数中的name参数的值是往上找,离得最近的return的非undefined的值
const hook = new AsyncParallelHook(["name", "age"]);
// 注册事件函数
hook.tapAsync("1", (name, age, callback) => {
    setTimeout(() => {
        console.log(1, name, age);
        callback()
    }, 1000)
});
hook.tapAsync("2", (name, age, callback) => {
    setTimeout(() => {
        console.log(2, name, age);
        callback()
    }, 2000)
});
hook.tapAsync("3", (name, age) => {
    setTimeout(() => {
        console.log(3, name, age);
        callback()
    }, 3000)
});
// 触发事件函数
hook.callAsync("zhuzhu", "28", (err) => {
    console.log('err',  err);
})




// promise方式
const { AsyncParallelHook } = require("tapable");
// 事件函数中的name参数的值是往上找,离得最近的return的非undefined的值
const hook = new AsyncParallelHook(["name", "age"]);
// 注册事件函数
hook.tapPromise("1", (name, age, ) => {
    return new Promise((resolve, reject) => {
        console.log(1, name, age);
        resolve();
    })
});
hook.tapPromise("2", (name, age, ) => {
    return new Promise((resolve, reject) => {
        console.log(2, name, age);
        resolve();
    })
});
hook.tapPromise("3", (name, age) => {
    return new Promise((resolve, reject) => {
        console.log(3, name, age);
        resolve();
    })
});
// 触发事件函数
hook.promise("zhuzhu", "28").then(result => {
    console.log(result);
})


// AsyncParallelBailHook 有返回值就停止
callback(null, "返回值");
类似于Promise.race 有一个有返回值就结束了 然后执行最终回调
resolve('返回值')


插件

  • 常见的插件对象

    • compiler
    • compilation
    • module factory
    • module
    • parser
    • template
  • 创建插件

    • 插件是一个类 里面有apply方法
  • compiler和compilation

    • compiler对象代表了完整的webpack环境配置,这个对象在启动webpack时被一次性建立,并配置好所有可操作的设置,包括options/loader/plugin。当在webpack环境中应用一个插件时,插件将收到此compiler对象的引用。可以使用它来访问webpack的主环境
    • compilation 对象表示了一次资源版本构建。当运行webpack开发环境中间件时,每当检测到一个文件变化,就会创建一个新的compilation,从而生成一组新的编译资源。一个compilation对象表现了当前的模块资源,编译生成资源,变化的文件,以及被跟踪依赖的状态信息。compilation对象也提供了很多关键时机的回调,以供插件做自定义处理时选择使用
  • compiler插件

// compiler插件
class DonePlugin{
    constructor(options) {
        this.options = options;
    }
    apply(compiler) {
        compiler.hooks.done.tapAsync('DonePlugin',(stats, callback) => {
            console.log("hello", this.options.name);
            callback();
        })
    }
}
module.exports = DonePlugin;
  • compilation插件
    • 使用compiler对象时,可以绑定提供了编译compilation引用的回调函数,然后拿到每次新的compilation对象。这些compilation对象提供了一些钩子函数,来购入到构建流程的很多步骤中
    • compilation里面放着编译的过程和编译的结果
class AssetPlugin {
    constructor(options) {
        this.options = options;
    }
    apply(compiler) {
        // compiler只有一个,每当监听到文件的变化,就会创建一个新的compilaton
        // 每当compiler开启一次新的编译,就会创建一个新的compilation,触发一次compilation事件
        compiler.hooks.compilation.tap('AssetPlugin', (compilation) => {
            compilation.hooks.chunkAsset.tap('AssetPlugin', (chunk, filename) => {
                console.log(chunk, filname);
            })
        })
    }
}
module.exports = AssetPlugin;
  • compiler一般用来监听编译的流程 如开始,编译结束等
  • compilation一般用来监听编译过程中的一些资源

zip-plugin插件

  • 把编译之后的文件放到一个文件夹
const path = require("path");
const JSZip = require("jszip");
const {RawSource} = require("webpack-sources");
class ZipPlugin{
    constructor(options) {
        this.options = options;
    }
    apply(compiler) {
        // 把打包后的文件全部打包在一起生成一个文件包,压缩包
        compiler.hooks.emit.tapAsync('ZipPlugin', (compilation, callback) => {
            let 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 => {
                 compilation.assets[this.options.filename] = new RawSource(content);
                 callback();
            })
        })
    }
}
module.exports = ZipPlugin;

自动外链插件

  • 使用外部类库(之前的做法)
      1. 手动指定externals:{jquey:"$"}
      1. 手动引入 script cdn地址
    • 使用 import $ from 'jquery' 会自动在window上找 window.jquery
  • 实现思路
    • 解决import自动处理externals和script,需要怎么实现?
    • 依赖当检测到有import该library时,将其设置为不打包 类似于externals,并在指定模版中加入script。那么如何检测import?用到parser
    • webpack的externals是通过externalsPlugin实现的,ExternalsPlugin通过tap NormalModuleFactory在每次创建Module的时候判断是否是ExternalModule
    • webpack4加入了模块类型之后,parser获取需要指定类型moduleType,一般使用javascript/auto即可
1. 自动实现externals
2. 自动向产出的html文件中插入cdn脚本
// 使用
plugins: [
  new AutoExternalPlugin({
    jquery: {
      expose: '$', // jquery模块要从window上的哪个全局变量取值
      url: 'https://cdn.bootcss.com/jquery/3.1.0/jquery.js', // 要向HTML中插入的脚本
    }
  })
]

// 实现
1. 通过ast语法树检测当前项目脚本中引入了那些模块 是不是引入了jquery
2. 如果发现引入了,则要自动插入cdn脚本

const {externalModule} = require("webpack");
const HtmlWebpackPlugin = require("html-webpack-plugin");
class AutoExternalPlugin{
  constructor(options) {
    this.options = options;
    this.externalModules = Object.keys(this.options); // ['jquery', 'lodash']
    this.importedModules = new Set(); // 存放着所有导入的外部依赖模块

  }
  apply(compiler){
    // 每种模块都有一个对应的模块工厂来创建这个模块,普通模块对应的工作就是普通模块工厂
    compiler.hooks.normalModuleFactory.tap('AutoExternalFactory', (normalModuleFactory) => {
      normalModuleFactory.hooks.parser
        .for('javascript/auto')
        // 把源代码转成抽象语法树,然后进行遍历
        // 遍历到不同类型的节点会触发不同的钩子 执行钩子对应的时间函数
        .tap('AutoExternalPlugin', parser => {
          // 拦截import
          parser.hooks.import.tap('AutoExternalPlugin', (statement, source) => {
            // console.log(statement, source);
            if(this.externalModules.includes(source)) { // jquery
              this.importedModules.add(source);
            }
          })
          // 拦截对require的方法调用
          parser.hooks.call.for("require").tap('AutoExternalPlugin', (expression) => {
            console.log(expression);
            let value = expression.arguments[0].value;
            if(this.externalModules.includes(value)) {
              this.importedModules.add(value);
            }
          })
        })
        // 改造创建模块的过程
        // factorize 是一个 AsyncSeriesBailHook
        normalModuleFactory.hooks.factorize.tapAsync('AutoExternalPlugin', (resolveData, callback) => {
          let request = resolveData.request; // jquery
          if(this.externalModules.includes(request)) {
            let expose = this.options[request].expose;
            // 创建一个外部模块并返回 jquery = window.jquery
            callback(null, new externalModule(expose, 'window', request))
          }else{
            callback(); // 如果是正常模块 直接调用callback向后执行
          }
        })
    })

    compiler.hooks.compilation.tap('AutoExternalPlugin', (compilation) => {
      HtmlWebpackPlugin.getHooks(compilation).alterAssetTags.tapAsync('AutoExternalPlugin', (htmlData, callback)=>{
      let {assetTags} = htmlData;
      let importedExternalModules = Object.keys(this.options).filter(item => this.importedModules.has(item));
      importedExternalModules.forEach(key => {
          assetTags.script.unshift({
              tagsName: 'script',
              voidTag: false,
              attributes:{
                  src: this.options[key].url,
                  defer: false
              }
          })
      })
      callback(null, htmlData);
      }
    })
  }
}
module.exports = AutoExternalPlugin;