Webpack入门到精通 四(Plugin原理)

863 阅读5分钟

前言

前面我们学习了webpack的核心功能如何实现源代码的转换以及打包。

  1. 基于@babel/parser把字符串转换成AST抽象语法树。
  2. AST进行修改,既对源代码的修改。
  3. 使用babelAPI再把修改后的AST转换成新的字符串(期望的代码)。
  4. 以及了解了css-loaderstyle-loader的基本实现。

下面我们学习一下plugin的使用场景以及工作原理。

前置问题

  1. 一个插件的基本代码结构
  2. webpack的构建流程
  3. Tapable自身有什么作用,以及如何把webpack的各个插件串联起来的?
  4. compiler以及compilation对象的作用以及主要的API

插件的基本结构

这里写的是一个简版的md文件转换成html文件的一个案例

使用插件

const {resolve} = require('path')
const MdToHtmlPlugin = require('./plugins/md-to-html-plugin')

module.exports = {
  mode: 'development',
  entry: resolve(__dirname, 'src/app.js'),
  output: {
    path: resolve(__dirname, 'dist'),
    filename: 'app.js'
  },
  plugins: [
    new MdToHtmlPlugin({
      template: resolve(__dirname, 'test.md'),
      filename: 'test.html'
    })
  ]
}

定义插件

const fs = require('fs')
const path = require('path')

function mdToHtml(mdStr) {
  let html = ''
  // 过滤掉空的
  let mdArrTemp = mdStr.split('\n').filter(item => item !== "")
  //ol 1. 
  //ul - 
  //h2 ## 
  let olReg = /\d\.\s/,
      ulReg = /\-\s/,
      h2Reg = /\#{2}\s/

  mdArrTemp.forEach((item, index) => {
    // 没有前一个元素, 或者前一个元素不是当前遍历的元素的时候
    let prev = mdArrTemp[index - 1];
    let next = mdArrTemp[index + 1];

    if (h2Reg.test(item)) {
      html += item.replace(h2Reg, "<h2>");
      html += "</h2>\n";
    } else if (ulReg.test(item)) {
      if (!prev || !ulReg.test(prev)) {
        html += "<ul>\n";
      }
      html += item.replace(ulReg, "  <li>");
      html += "</li>\n";
      if (!next || !ulReg.test(next)) {
        html += "</ul>\n";
      }
    } else if (olReg.test(item)) {
      if (!prev || !olReg.test(prev)) {
        html += "<ol>\n";
      }
      html += item.replace(olReg, "  <li>");
      html += "</li>\n";
      if (!next || !olReg.test(next)) {
        html += "<ol>\n";
      }
    }
  })
  return html
}

class MdToHtmlPlugin {
  // 在构造函数里面可以传递实例的参数
  constructor({template, filename}) {
    if(!template) {
      throw new Error('"template" must be configured')
    }
    this.template = template
    this.filename = filename
  }
  // webpack 调用HelloPlugin 实例的apply方法给插件传入compiler对象。
  apply(compiler) {
    compiler.hooks.emit.tap("md-to-html-plugin", (compilation) => {
      let _assets = compilation.assets
      // 读取md原始文件。
      let mdContent = fs.readFileSync(this.template, "utf-8");
      let htmlTemplateContent = fs.readFileSync(path.resolve(__dirname, './template.html')).toString()
      let html = mdToHtml(mdContent)
      // 读取template模板原始文件。
      // 将md文件的格式替换成html。
      // 将替换生成的html插入到html template文件里面。
      let genHtml = htmlTemplateContent.replace("<!--md-->", html);
      fs.writeFileSync(path.resolve(__dirname, '../../dist/'+this.filename), genHtml, 'utf-8')
    });
  }
}

module.exports = MdToHtmlPlugin;

插件是如何工作的?

  1. 在读取webpack配置的过程中我们先使用new MdToHtmlPlugin(options) 初始化了一个 MdToHtmlPlugin实例
  2. 也在其他代码中初始化了compiler对象,调用了MdToHtmlPlugin.apply(compiler)方法,给插件传入compiler对象。
  3. 插件获取到compiler对象,就可以通过调用compiler.hooks.<hook name>.call。具体的事件在文档中有体现。

image.png

node events && tabable

node

events 模块只提供了一个对象: events.EventEmitter。EventEmitter 的核心就是事件触发与事件监听器功能的封装。

var EventEmitter = require('events').EventEmitter; 
var event = new EventEmitter(); 
event.on('some_event', function() { 
    console.log('some_event 事件触发'); 
}); 
setTimeout(function() { 
    event.emit('some_event'); 
}, 1000); 

tapable

github tapable

const { 
SyncHook,
SyncBailHook, 
SyncWaterfallHook,
SyncLoopHook,
AsyncParallelHook,
AsyncParallelBailHook,
AsyncSeriesHook, 
AsyncSeriesBailHook,
AsyncSeriesWaterfallHook
} = require("tapable");

image.png

实现一个简单的tapable

class Hook{
    constructor(args){
        this.taps = []
        this._args = args 
    }
    tap(name,fn){
        this.taps.push({name,fn})
    }
}
class SyncHook extends Hook{
    call(name,fn){
        try {
            this.taps.forEach(tap => tap.fn(name))
            fn(null,name)
        } catch (error) {
            fn(error)
        }

    }
}

// 使用

let $ = new SyncHook()
$.tap('xx', () => {console.log('xx')})
$.call('xx', () => {console.log('xx called')})

理解Sync类型的钩子

1、SyncHook

const {SyncHook} = require('tapable')

// 创建实例
const syncHook = new SyncHook(['name', 'age'])

//注册事件

syncHook.tap('1', (name, age) => {
  console.log('1' ,name , age)
})

syncHook.tap("2", (name, age) => {
  console.log("2", name, age);
});

syncHook.tap("3", (name, age) => {
  console.log("3", name, age);
});

// 触发事件 ,让回调函数执行

syncHook.call('天明', 10)

image.png

理解Async类型的钩子

1、AsyncSeriesHook

const { AsyncSeriesHook } = require("tapable");

// 创建实列
const asyncSeriesHook = new AsyncSeriesHook(["name", "age"]);

// 注册事件
asyncSeriesHook.tapAsync("1", (name, age, done) => {
  setTimeout(() => {
    console.log("1", name, age, new Date());
    done();
  }, 1000);
});

asyncSeriesHook.tapAsync("2", (name, age, done) => {
  setTimeout(() => {
    console.log("2", name, age, new Date());
    done();
  }, 2000);
});

asyncSeriesHook.tapAsync("3", (name, age, done) => {
  setTimeout(() => {
    console.log("3", name, age, new Date());
    done();
  }, 3000);
});

// 触发事件,让监听函数执行
asyncSeriesHook.callAsync("月儿", 10, () => {
  console.log("执行完成");
});

image.png

tapable和webpack是如何关联起来的

Compiler.js

const { AsyncSeriesHook ,SyncHook } = require("tapable");
//创建类
class Compiler {
  constructor(options) {
    // 从options上面取出plugin 并执行apply方法,把this传入
    this.hooks = {
      run: new SyncHook(["run"]), //同步钩子
      compile: new AsyncSeriesHook(["name", "age"]), //异步钩子
      done: new SyncHook(['done'])
    };
    options.plugins[0].apply(this);
  }
  run() {
    //执行异步钩子
    this.hooks.run.call();
    this.compile()
  }
  compile() {
    //执行同步钩子 并传参
    this.hooks.compile.callAsync("月儿", 10, (err) => {
      this.done();
    });
  }
  done() {
    this.hooks.done.call();
  }
}
module.exports = Compiler
  • MyPlugin.js
const Compiler = require("./Compiler");

class MyPlugin {
  apply(compiler) {
    // 这边就是写好钩子,
    // 等待webpack 内部在一定的时机触发钩子的回调函数即可,加入自己的逻辑在里面
    //接受 compiler参数
    compiler.hooks.run.tap("MyPlugin", () => console.log("开始编译..."));
    // 触发异步钩子
    compiler.hooks.compile.tapAsync("MyPlugin", (name, age, done) => {
      setTimeout(() => {
        console.log(`编译中...收到参数name:${name}-age:${age}编译中...`);
        done();
      }, 3000);
    });
    //同步钩子
    compiler.hooks.done.tap("MyPlugin", () => console.log("结束编译..."));
  }
}

//这里类似于webpack.config.js的plugins配置
//向 plugins 属性传入 new 实例

const myPlugin = new MyPlugin();

const options = {
  plugins: [myPlugin],
};
let compiler = new Compiler(options);
compiler.run();

运行完上面这段代码,会得到以下的输出。

image.png 顺便吐槽一下原文中的代码,看了好几个版本的代码。 image.png

image.png

webpack构建流程

  1. 校验配置文件 :读取命令行传入或者webpack.config.js文件,初始化本次构建的配置参数
  2. 生成Compiler对象:执行配置文件中的插件实例化语句new MyWebpackPlugin(),为webpack事件流挂上自定义hooks
  3. 进入entryOption阶段:webpack开始读取配置的Entries,递归遍历所有的入口文件
  4. run/watch:如果运行在watch模式则执行watch方法,否则执行run方法
  5. compilation:创建Compilation对象回调compilation相关钩子,依次进入每一个入口文件(entry),使用loader对文件进行编译。通过compilation我可以可以读取到moduleresource(资源路径)、loaders(使用的loader)等信息。再将编译好的文件内容使用acorn解析生成AST静态语法树。然后递归、重复的执行这个过程, 所有模块和和依赖分析完成后,执行 compilationseal 方法对每个 chunk 进行整理、优化、封装__webpack_require__来模拟模块化操作.
  6. emit:所有文件的编译及转化都已经完成,包含了最终输出的资源,我们可以在传入事件回调的compilation.assets上拿到所需数据,其中包括即将输出的资源、代码块Chunk等等信息。
// 修改或添加资源
compilation.assets['new-file.js'] = {
  source() {
    return 'var a=1';
  },
  size() {
    return this.source().length;
  }
};
复制代码
  1. afterEmit:文件已经写入磁盘完成
  2. done:完成编译 还是的配一张图 image.png

compiler(负责编译)

Compiler 模块是 webpack 的主要引擎,它通过 CLI 或者 Node API 传递的所有选项创建出一个 compilation 实例。 它扩展(extends)自 Tapable 类,用来注册和调用插件。 大多数面向用户的插件会首先在 Compiler 上注册。

在为 webpack 开发插件时,你可能需要知道每个钩子函数是在哪里调用的。想要了解这些内容,请在 webpack 源码中搜索 hooks.<hook name>.call参考webpack中文文档plugins

compilation(负责创建bundles)

简单来说,Compilation的职责就是构建模块和Chunk,并利用插件优化构建过程。

常用API

compilation.hooks.optimizeChunkAssets.tapAsync(
  'MyPlugin',
  (chunks, callback) => {
    chunks.forEach((chunk) => {
      chunk.files.forEach((file) => {
        compilation.assets[file] = new ConcatSource(
          '/**Sweet Banner**/',
          '\n',
          compilation.assets[file]
        );
      });
    });

    callback();
  }
);

webpack 文档链接

参考文献

cnblogs 掘金

系列文章

Webpack入门到精通 一(AST、Babel、依赖)

Webpack入门到精通 二(核心原理)

Webpack入门到精通 三(Loader原理)

Webpack入门到精通 四(Plugin原理)

Webpack入门到精通 五(常用配置)