给webpack-core实现plugin功能

127 阅读4分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第12天,点击查看活动详情

前言

身为一个前端小菜鸟,总是有一个飞高飞远的梦想,因此,每点小成长,我都想要让它变得更有意义,为了自己,也为了更多值得的人

开开心心学技术大法~~

开心

来了来了,他真的来了~

正文

之前写了webpack-core的代码,阐述了webapck的核心思想,后来又补充了webpack-loader的实现思想,今天把webpack最后一档子事儿,plugin也给搞下。

webpack-plugin核心概念

首先要明确wbepack-plugin是怎样实现的

他核心的有两个概念,一个是compiler,一个是compilation

其中compiler是webapck的核心,集成了webpack整个机器运行的变量、属性、方法。

compilation是在webpack特定阶段产出的一个包含当前阶段构建信息的一个对象。多数plugins都要依靠该对象进行操作。

他们都有各自的hooks方法,compiler管理webpack的整个进程,compilation管理某个时期的webpack进程

compilercompilation的hooks都是基于tapable的事件机制来实现的。

tapable是一个node包,将一般方法封装成具有不同功能的高阶函数。

具体有以下内容

const {
	SyncHook, // 同步钩子
	SyncBailHook, // 同步捞错钩子
	SyncWaterfallHook, // 同步瀑布流钩子
	SyncLoopHook, // 同步循环钩子
	AsyncParallelHook, // 异步并行钩子
	AsyncParallelBailHook, // 异步并行捞错钩子
	AsyncSeriesHook, // 异步串行钩子
	AsyncSeriesBailHook, // 异步串行捞错钩子
	AsyncSeriesWaterfallHook // 异步串行瀑布流钩子
 } = require("tapable");

tapable基于一个订阅分发模式来实现,通过tap订阅,通过call来分发。

webpack-plugin实现思路

我们知道webpack的plugin都是一个个的class,我们在使用时需要new出来。

class内部还得有apply方法,这个方法是webpack在初始化的时候就会被调用。我们的插件要做的事情也应该在apply中写,通过tapable来注册,然后在合适的hooks中被执行。

具体拆分来看,大体如下

  • 每个plugin都是一个class
  • 每个plugin都包含一个apply方法
  • 在webpack打包之前plugin们就应该被初始化
  • 初始化的时候,会通过apply将事件tap
  • 然后在指定时刻拿到compilation对象对资源进行操作

webpack-plugin在webpack-core中的具体实现

我们实现一个很简单的功能,比如说将文件文件输出路径通过插件进行改变

定义插件内容

// outputPlugin.js
/**
 * 实现一个更改输出目录的插件
 * @param {*} content 
 * @returns 
 */

module.exports = class ChangeOutputPath{
  constructor(options) {
    // 接收options传入的参数
    this.options = options
  }
  apply(compiler) {
     // emitAsset是我们定义在compiler上的一个hooks,通过tap进行注册
    compiler.hooks.emitAsset.tap('dist/newtest.js', (compilation) => {
     // changeOutputPath是我们定义的compilation的一个hooks
     // 这里的../dist/test2.js是写死的路径,也可以通过options传入
     compilation.changeOutputPath('../dist/test2.js')
    })
  }
}

初始化插件

const OutputPlugin = require('./outputPlugin.js');

const webpackConfig = {
  plugins: [new OutputPlugin()]
}
// 实现webpack-plugin内部的compiler
const compiler = {
  outputPath: '../dist/built.js',
  hooks: {
    emitAsset: new SyncHook(['compilation'])
  }
}

// 因为webpack-plugin用到了tabable,所以需要先注册,然后在某个webapck的生命周期才会执行。因此要先触发注册事件
const initPlugin = () => {
  if (webpackConfig.plugins) {
    webpackConfig.plugins.map(plugin => {
      // webpack-plugin需要在class内实现一个apply方法用于接收compiler,
      // 之后通过compiler和之后在生命周期调用指定compiler hooks的时候传入的compilation来做一系列操作
      plugin.apply(compiler)
    })
  }
}

// 初始化插件
initPlugin();

在最终输出文件前的build阶段callemitAsset

function build(graph) {
  // 拿到定义的target.ejs模板文件的utf-8的内容
  // 因为webpack会将很多小文件打包成一个大文件,而上面我们也会将所有模块的import语法转换成require语法
  // 然后转化后的require语法浏览器依然是不支持的,因此就需要我们手动实现一个requre方法,相应的
  // module.epoxrts也要实现。具体的实现方法在target.ejs文件中
  const template = fs.readFileSync(path.resolve(__dirname, './target.ejs'), {
    encoding: 'utf-8'
  })
  // 通过ejs的库将模板通过动态数据来渲染成我们需要的动态的打包后的目标文件
  const targetCode = ejs.render(template, { data: graph })

  // 将bundle之后的文件输出到dist/built.js文件
  // fs.writeFileSync(path.resolve(__dirname, '../dist/built.js'), targetCode)

  // 实现webpack-plugin的compilation对象
  const compilation = {
    changeOutputPath(path) {
      compiler.outputPath = path;
    }
  }
  // 对标webpack-plugin的emitAsset方法,因为是更改输出资源的,所以要在输出最终资源前调用
  compiler.hooks.emitAsset.call(compilation)

  // 假设有一个compiler保存着webpack中的所有信息,那当然也有outputPath,当然这个outputPath也会因为用户外部输入而覆盖掉
  fs.writeFileSync(path.resolve(__dirname, compiler.outputPath), targetCode)
}

ok,大功告成。

这个时候我们再build的时候,就会将输出目录变为../dist/test2.js

完整代码

结语

往期好文推荐「我不推荐下,大家可能就错过了史上最牛逼vscode插件集合啦!!!(嘎嘎~)😄」