插件向第三方开发者提供了 webpack 引擎中完整的能力。使用阶段式的构建回调,开发者可以引入它们自己的行为到 webpack 构建流程中。创建插件比创建 loader 更加高级,因为你将需要理解一些 webpack 底层的内部特性来做相应的钩子
| 对象 | 钩子 | 
|---|---|
| Compiler | run,compile,compilation,make,emit,done | 
| Compilation | buildModule,normalModuleLoader,succeedModule,finishModules,seal,optimize,after-seal | 
| Module Factory | beforeResolver,afterResolver,module,parser | 
| Module | |
| Parser | program,statement,call,expression | 
| Template | hash,bootstrap,localVars,render | 
class DonePlugin {
    constructor(options) {
        this.options = options;
    }
    apply(compiler) {
    }
}
module.exports = DonePlugin;
在插件开发中最重要的两个资源就是compiler和compilation对象。理解它们的角色是扩展 webpack 引擎重要的第一步。
compiler 对象代表了完整的 webpack 环境配置。这个对象在启动 webpack 时被一次性建立,并配置好所有可操作的设置,包括 options,loader 和 plugin。当在 webpack 环境中应用一个插件时,插件将收到此 compiler 对象的引用。可以使用它来访问 webpack 的主环境。
compilation 对象代表了一次资源版本构建。当运行 webpack 开发环境中间件时,每当检测到一个文件变化,就会创建一个新的 compilation,从而生成一组新的编译资源。一个 compilation 对象表现了当前的模块资源、编译生成资源、变化的文件、以及被跟踪依赖的状态信息。compilation 对象也提供了很多关键时机的回调,以供插件做自定义处理时选择使用。
if (options.plugins && Array.isArray(options.plugins)) {
  for (const plugin of options.plugins) {
    plugin.apply(compiler);
  }
}
class DonePlugin {
  constructor(options) {
    this.options = options;
  }
  apply(compiler) {
    compiler.hooks.done.tap("DonePlugin", (stats) => {
      console.log("Hello ", this.options.name);
    });
  }
}
module.exports = DonePlugin;
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;
const DonePlugin = require("./plugins/DonePlugin");
module.exports = {
  entry: "./src/index.js",
  output: {
    path: path.resolve("build"),
    filename: "bundle.js",
  },
  plugins: [new DonePlugin({ name: "zhufeng" })],
};
if (this.hooks.shouldEmit.call(compilation) === false) {
                const stats = new Stats(compilation);
                stats.startTime = startTime;
                stats.endTime = Date.now();
+                this.hooks.done.callAsync(stats, err => {
+                    if (err) return finalCallback(err);
+                    return finalCallback(null, stats);
+                });
                return;
            }
plugins\asset-plugin.js
class AssetPlugin{
    constructor(options){
        this.options = options;
    }
    apply(compiler){
        //compiler只有一个,每当监听到文件的变化,就会创建一个新的compilation
        //每当compiler开启一次新的编译,就会创建一个新的compilation,触发一次compilation事件 
        compiler.hooks.compilation.tap('AssetPlugin',(compilation)=>{
            // main=>main.js  vendor=>vendor.js
            compilation.hooks.chunkAsset.tap('AssetPlugin',(chunk,filename)=>{
                console.log(chunk,filename);
            });
        });
    }
}
module.exports = AssetPlugin;
newCompilation(params) {
        const compilation = this.createCompilation();
        this.hooks.compilation.call(compilation, params);
        return compilation;
    }
chunk.files.push(file);
+this.hooks.chunkAsset.call(chunk, file);
关于 compiler, compilation 的可用回调,和其它重要的对象的更多信息,请查看 插件 文档。
plugins\zip-plugin.js
const { RawSource } = require("webpack-sources");
const JSZip = require("jszip");
const path = require("path");
class ZipPlugin {
  constructor(options) {
    this.options = options;
  }
  apply(compiler) {
    let that = this;
    compiler.hooks.emit.tapAsync("ZipPlugin", (compilation, callback) => {
      var 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[that.options.filename] = new RawSource(content);
        callback();
      });
    });
  }
}
module.exports = ZipPlugin;
webpack.config.js
  plugins: [
+    new zipPlugin({
+      filename: 'assets.zip'
+    })
]
externalscript能否检测代码中的 import 自动处理这个步骤?
{
  externals:{
    //key jquery是要require或import 的模块名,值 jQuery是一个全局变量名
  'jquery':'$'
}, 
  module:{}
}
external和script的问题,需要怎么实现,该从哪方面开始考虑依赖 当检测到有import该library时,将其设置为不打包类似exteral,并在指定模版中加入 script,那么如何检测 import?这里就用Parserexternal依赖
需要了解 external 是如何实现的,webpack 的 external 是通过插件ExternalsPlugin实现的,ExternalsPlugin 通过tap NormalModuleFactory 在每次创建 Module 的时候判断是否是ExternalModuleParser获取需要指定类型 moduleType,一般使用javascript/auto即可plugins: [
  new HtmlWebpackPlugin({
    template: "./src/index.html",
    filename: "index.html",
  }),
  new AutoExternalPlugin({
    jquery: {
      expose: "$",
      url: "https://cdn.bootcss.com/jquery/3.1.0/jquery.js",
    },
  }),
];
AsyncSeriesBailHook factorize
let { AsyncSeriesBailHook } = require("tapable");
let factorize = new AsyncSeriesBailHook(["resolveData"]);
factorize.tapAsync("tap1", (resolveData, callback) => {
    if (resolveData === "jquery") {
        callback(null, { externalModule: "jquery" });
    } else {
        callback();
    }
});
factorize.tapAsync("tap2", (resolveData, callback) => {
    callback(null, { normalModule: resolveData });
});
//由tap1返回
factorize.callAsync("jquery", (err, module) => {
    console.log(module);
});
//由tap2返回
factorize.callAsync("jquery2", (err, module) => {
    console.log(module);
});
plugins\auto-external-plugin.js
/**
 * https://webpack.js.org/api/compiler-hooks/#normalmodulefactory
 * 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('AutoExternalPlugin', (normalModuleFactory) => {
      normalModuleFactory.hooks.parser
        .for('javascript/auto')//parser babel esprima  acorn 可以把源代码转成抽象语法树,然后进行遍历,
        //遍历到不同类型的节点会触发不同的钩子,执行钩子对应的事件函数
        .tap('AutoExternalPlugin', parser => {
          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)){//jquery
              this.importedModules.add(value);
            }
          })
        })
       //改造创建模块的过程
       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)=>{
        console.log('htmlData',htmlData);
        let {assetTags} = htmlData;
        //找了我实际引入了哪些模块 jquery lodash
        let importedExternalModules = Object.keys(this.options).filter(item=>this.importedModules.has(item));
        importedExternalModules.forEach(key=>{
          assetTags.scripts.unshift({
            tagName:'script',
            voidTag:false,
            attributes:{
              src:this.options[key].url,
              defer:false
            }
          });
        });
        callback(null,htmlData);
      });
    });
  }
}
module.exports = AutoExternalPlugin;
/**
 * Node {
  type: 'ImportDeclaration',
  start: 0,
  end: 23,
  loc: SourceLocation {
    start: Position { line: 1, column: 0 },
    end: Position { line: 1, column: 23 }
  },
  range: [ 0, 23 ],
  specifiers: [
    Node {
      type: 'ImportDefaultSpecifier',
      start: 7,
      end: 8,
      loc: [SourceLocation],
      range: [Array],
      local: [Node]
    }
  ],
  source: Node {
    type: 'Literal',
    start: 14,
    end: 22,
    loc: SourceLocation { start: [Position], end: [Position] },
    range: [ 14, 22 ],
    value: 'jquery',
    raw: "'jquery'"
  }
}
jquery
 */
class HashPlugin{
    constructor(options){
        this.options = options;
    }
    apply(compiler){
        compiler.hooks.compilation.tap('HashPlugin',(compilation,params)=>{
            //如果你想改变hash值,可以在hash生成这后修改
            compilation.hooks.afterHash.tap('HashPlugin',()=>{
                let fullhash = 'fullhash';//时间戳
                console.log('本次编译的compilation.hash',compilation.hash);
                compilation.hash= fullhash;//output.filename [fullhash]
                for(let chunk of compilation.chunks){
                    console.log('chunk.hash',chunk.hash);
                    chunk.renderedHash = 'chunkHash';//可以改变chunkhash
                    console.log('chunk.contentHash',chunk.contentHash);
                    chunk.contentHash= { javascript: 'javascriptContentHash','css/mini-extract':'cssContentHash' }
                }
            });
        });
    }
}
module.exports = HashPlugin;
/**
 * 三种hash
 * 1. hash compilation.hash 
 * 2. chunkHash 每个chunk都会有一个hash
 * 3. contentHash 内容hash 每个文件会可能有一个hash值
 */
webpack.config.js
const path = require('path');
const DonePlugin = require('./plugins/DonePlugin');
const AssetPlugin = require('./plugins/AssetPlugin');
const ZipPlugin = require('./plugins/ZipPlugin');
const HashPlugin = require('./plugins/HashPlugin');
const AutoExternalPlugin = require('./plugins/AutoExternalPlugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
    module: {
        rules: [
            {
                test: /\.css$/,
                use: [
+                   MiniCssExtractPlugin.loader,
                    'css-loader'
                ]
            }
        ]
    },
    plugins: [
+       new HashPlugin(),
    ]
}
let AsyncQueue = require('webpack/lib/util/AsyncQueue');
let AsyncQueue = require('./AsyncQueue');
function processor(item, callback) {
    setTimeout(() => {
        console.log('process',item);
        callback(null, item);
    }, 3000);
}
const getKey = (item) => {
    return item.key;
}
let queue  = new AsyncQueue({
    name:'createModule',parallelism:3,processor,getKey
});
const start = Date.now();
let item1 = {key:'module1'};
queue.add(item1,(err,result)=>{
    console.log(err,result);
    console.log(Date.now() - start);
});
queue.add(item1,(err,result)=>{
    console.log(err,result);
    console.log(Date.now() - start);
});
queue.add({key:'module2'},(err,result)=>{
    console.log(err,result);
    console.log(Date.now() - start);
});
queue.add({key:'module3'},(err,result)=>{
    console.log(err,result);
    console.log(Date.now() - start);
});
queue.add({key:'module4'},(err,result)=>{
    console.log(err,result);
    console.log(Date.now() - start);
});
use.js
const QUEUED_STATE = 0;//已经 入队,待执行
const PROCESSING_STATE = 1;//处理中
const DONE_STATE = 2;//处理完成
class ArrayQueue {
    constructor() {
        this._list = [];
    }
    enqueue(item) {
        this._list.push(item);//[1,2,3]
    }
    dequeue() {
        return this._list.shift();//移除并返回数组中的第一个元素
    }
}
class AsyncQueueEntry {
    constructor(item, callback) {
        this.item = item;//任务的描述
        this.state = QUEUED_STATE;//这个条目当前的状态
        this.callback = callback;//任务完成的回调
    }
}
class AsyncQueue {
    constructor({ name, parallelism, processor, getKey }) {
        this._name = name;//队列的名字
        this._parallelism = parallelism;//并发执行的任务数
        this._processor = processor;//针对队列中的每个条目执行什么操作
        this._getKey = getKey;//函数,返回一个key用来唯一标识每个元素
        this._entries = new Map();
        this._queued = new ArrayQueue();//将要执行的任务数组队列 
        this._activeTasks = 0;//当前正在执行的数,默认值1
        this._willEnsureProcessing = false;//是否将要开始处理
    }
    add = (item, callback) => {
        const key = this._getKey(item);//获取这个条目对应的key
        const entry = this._entries.get(key);//获取 这个key对应的老的条目
        if (entry !== undefined) {
            if (entry.state === DONE_STATE) {
                process.nextTick(() => callback(entry.error, entry.result));
            } else if (entry.callbacks === undefined) {
                entry.callbacks = [callback];
            } else {
                entry.callbacks.push(callback);
            }
            return;
        }
        const newEntry = new AsyncQueueEntry(item, callback);//创建一个新的条目
        this._entries.set(key, newEntry);//放到_entries
        this._queued.enqueue(newEntry);//把这个新条目放放队列
        if (this._willEnsureProcessing === false) {
            this._willEnsureProcessing = true;
            setImmediate(this._ensureProcessing);
        }
    }
    _ensureProcessing = () => {
        //如果当前的激活的或者 说正在执行任务数行小于并发数
        while (this._activeTasks < this._parallelism) {
            const entry = this._queued.dequeue();//出队 先入先出
            if (entry === undefined) break;
            this._activeTasks++;//先让正在执行的任务数++
            entry.state = PROCESSING_STATE;//条目的状态设置为执行中
            this._startProcessing(entry);
        }
        this._willEnsureProcessing = false;
    }
    _startProcessing = (entry) => {
        this._processor(entry.item, (e, r) => {
            this._handleResult(entry, e, r);
        });
    }
    _handleResult = (entry, error, result) => {
        const callback = entry.callback;
        const callbacks = entry.callbacks;
        entry.state = DONE_STATE;//把条目的状态设置为已经完成
        entry.callback = undefined;//把callback
        entry.callbacks = undefined;
        entry.result = result;//把结果赋给entry
        entry.error = error;//把错误对象赋给entry
        callback(error, result);
        if (callbacks !== undefined) {
            for (const callback of callbacks) {
                callback(error, result);
            }
        }
        this._activeTasks--;
        if (this._willEnsureProcessing === false) {
            this._willEnsureProcessing = true;
            setImmediate(this._ensureProcessing);
        }
    }
}
module.exports = AsyncQueue;