乐趣区

关于前端:你必须知道的webpack插件原理分析

本文在 github 做了收录 github.com/Michael-lzg…

demo 源码地址 github.com/Michael-lzg…

在 webpack 中,专一于解决 webpack 在编译过程中的某个特定的工作的功能模块,能够称为插件。它和 loader 有以下区别:

  1. loader 是一个转换器,将 A 文件进行编译成 B 文件,比方:将 A.less 转换为 A.css,单纯的文件转换过程。webpack 本身只反对 js 和 json 这两种格局的文件,对于其余文件须要通过 loader 将其转换为 commonJS 标准的文件后,webpack 能力解析到。
  2. plugin 是一个扩展器,它丰盛了 webpack 自身,针对是 loader 完结后,webpack 打包的整个过程,它并不间接操作文件,而是基于事件机制工作,会监听 webpack 打包过程中的某些节点,执行宽泛的工作。

plugin 的特色

webpack 插件有以下特色

  • 是一个独立的模块。
  • 模块对外裸露一个 js 函数。
  • 函数的原型 (prototype) 上定义了一个注入 compiler 对象的 apply 办法。
  • apply 函数中须要有通过 compiler 对象挂载的 webpack 事件钩子,钩子的回调中能拿到以后编译的 compilation 对象,如果是异步编译插件的话能够拿到回调 callback。
  • 实现自定义子编译流程并解决 complition 对象的外部数据。
  • 如果异步编译插件的话,数据处理实现后执行 callback 回调。
class HelloPlugin {
  // 在构造函数中获取用户给该插件传入的配置
  constructor(options) {}
  // Webpack 会调用 HelloPlugin 实例的 apply 办法给插件实例传入 compiler 对象
  apply(compiler) {
    // 在 emit 阶段插入钩子函数,用于特定机会解决额定的逻辑;compiler.hooks.emit.tap('HelloPlugin', (compilation) => {// 在性能流程实现后能够调用 webpack 提供的回调函数;})
    // 如果事件是异步的,会带两个参数,第二个参数为回调函数,compiler.plugin('emit', function (compilation, callback) {
      // 处理完毕后执行 callback 以告诉 Webpack
      // 如果不执行 callback,运行流程将会始终卡在这不往下执行
      callback()})
  }
}

module.exports = HelloPlugin
  1. webpack 读取配置的过程中会先执行 new HelloPlugin(options) 初始化一个 HelloPlugin 取得其实例。
  2. 初始化 compiler 对象后调用 HelloPlugin.apply(compiler) 给插件实例传入 compiler 对象。
  3. 插件实例在获取到 compiler 对象后,就能够通过 compiler.plugin (事件名称, 回调函数) 监听到 Webpack 播送进去的事件。并且能够通过 compiler 对象去操作 Webpack。

事件流机制

webpack 实质上是一种事件流的机制,它的工作流程就是将各个插件串联起来,而实现这所有的外围就是 Tapable

Webpack 的 Tapable 事件流机制保障了插件的有序性,将各个插件串联起来,Webpack 在运行过程中会播送事件,插件只须要监听它所关怀的事件,就能退出到这条 webapck 机制中,去扭转 webapck 的运作,使得整个零碎扩展性良好。

Tapable 也是一个小型的 library,是 Webpack 的一个外围工具。相似于 node 中的 events 库,外围原理就是一个 订阅公布模式。作用是提供相似的插件接口。办法如下:

//  播送事件
compiler.apply('event-name', params)
compilation.apply('event-name', params)

// 监听事件
compiler.plugin('event-name', function (params) {})
compilation.plugin('event-name', function (params) {})

咱们来看下 Tapable

function Tapable() {this._plugins = {}
}
// 公布 name 音讯
Tapable.prototype.applyPlugins = function applyPlugins(name) {if (!this._plugins[name]) return
  var args = Array.prototype.slice.call(arguments, 1)
  var plugins = this._plugins[name]
  for (var i = 0; i < plugins.length; i++) {plugins[i].apply(this, args)
  }
}
// fn 订阅 name 音讯
Tapable.prototype.plugin = function plugin(name, fn) {if (!this._plugins[name]) {this._plugins[name] = [fn]
  } else {this._plugins[name].push(fn)
  }
}
// 给定一个插件数组,对其中的每一个插件调用插件本身的 apply 办法注册插件
Tapable.prototype.apply = function apply() {for (var i = 0; i < arguments.length; i++) {arguments[i].apply(this)
  }
}

Tapable 为 webpack 提供了对立的插件接口(钩子)类型定义,它是 webpack 的外围性能库。webpack 中目前有十种 hooks,在 Tapable 源码中能够看到,他们是:

exports.SyncHook = require('./SyncHook')
exports.SyncBailHook = require('./SyncBailHook')
exports.SyncWaterfallHook = require('./SyncWaterfallHook')
exports.SyncLoopHook = require('./SyncLoopHook')
exports.AsyncParallelHook = require('./AsyncParallelHook')
exports.AsyncParallelBailHook = require('./AsyncParallelBailHook')
exports.AsyncSeriesHook = require('./AsyncSeriesHook')
exports.AsyncSeriesBailHook = require('./AsyncSeriesBailHook')
exports.AsyncSeriesLoopHook = require('./AsyncSeriesLoopHook')
exports.AsyncSeriesWaterfallHook = require('./AsyncSeriesWaterfallHook')

Tapable 还对立裸露了三个办法给插件,用于注入不同类型的自定义构建行为:

  • tap:能够注册同步钩子和异步钩子。
  • tapAsync:回调形式注册异步钩子。
  • tapPromise:Promise 形式注册异步钩子。

webpack 里的几个十分重要的对象,Compiler, CompilationJavascriptParser 都继承了 Tapable 类,它们身上挂着丰盛的钩子。

编写一个插件

一个 webpack 插件由以下组成:

  • 一个 JavaScript 命名函数。
  • 在插件函数的 prototype 上定义一个 apply 办法。
  • 指定一个绑定到 webpack 本身的事件钩子。
  • 解决 webpack 外部实例的特定数据。
  • 性能实现后调用 webpack 提供的回调。

上面实现一个最简略的插件

class WebpackPlugin1 {constructor(options) {this.options = options}
  apply(compiler) {compiler.hooks.done.tap('MYWebpackPlugin', () => {console.log(this.options)
    })
  }
}

module.exports = WebpackPlugin1

而后在 webpack 的配置中注册应用就行,只须要在 webpack.config.js 里引入并实例化就能够了:

const WebpackPlugin1 = require('./src/plugin/plugin1')

module.exports = {
  entry: {index: path.join(__dirname, '/src/main.js'),
  },
  output: {path: path.join(__dirname, '/dist'),
    filename: 'index.js',
  },
  plugins: [new WebpackPlugin1({ msg: 'hello world'})],
}

此时咱们执行一下 npm run build 就能看到成果了

Compiler 对象(负责编译)

Compiler 对象蕴含了以后运行 Webpack 的配置,包含 entryoutputloaders 等配置,这个对象在启动 Webpack 时被实例化,而且是全局惟一的。Plugin 能够通过该对象获取到 Webpack 的配置信息进行解决。

compiler 上裸露的一些罕用的钩子:

上面来举个例子

class WebpackPlugin2 {constructor(options) {this.options = options}
  apply(compiler) {compiler.hooks.run.tap('run', () => {console.log('开始编译...')
    })

    compiler.hooks.compile.tap('compile', () => {console.log('compile')
    })

    compiler.hooks.done.tap('compilation', () => {console.log('compilation')
    })
  }
}

module.exports = WebpackPlugin2

此时咱们执行一下 npm run build 就能看到成果了

有一些编译插件中的步骤是异步的,这样就须要额定传入一个 callback 回调函数,并且在插件运行完结时执行这个回调函数

class WebpackPlugin2 {constructor(options) {this.options = options}
  apply(compiler) {compiler.hooks.beforeCompile.tapAsync('compilation', (compilation, cb) => {setTimeout(() => {console.log('编译中...')
        cb()}, 1000)
    })
  }
}

module.exports = WebpackPlugin2

Compilation 对象

Compilation 对象代表了一次资源版本构建。当运行 webpack 开发环境中间件时,每当检测到一个文件变动,就会创立一个新的 compilation,从而生成一组新的编译资源。一个 Compilation 对象体现了以后的模块资源、编译生成资源、变动的文件、以及被跟踪依赖的状态信息,简略来讲就是把本次打包编译的内容存到内存里。Compilation 对象也提供了插件须要自定义性能的回调,以供插件做自定义解决时抉择应用拓展。

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

Compiler 代表了整个 Webpack 从启动到敞开的生命周期,而 Compilation 只是代表了一次新的编译,只有文件有改变,compilation 就会被从新创立。

Compilation 上裸露的一些罕用的钩子:

CompilerCompilation 的区别

  • Compiler 代表了整个 Webpack 从启动到敞开的生命周期
  • Compilation 只是代表了一次新的编译,只有文件有改变,compilation 就会被从新创立。

手写插件 1:文件清单

在每次 webpack 打包之后,主动产生一个一个 markdown 文件清单,记录打包之后的文件夹 dist 里所有的文件的一些信息。

思路:

  1. 通过 compiler.hooks.emit.tapAsync() 来触发生成资源到 output 目录之前的钩子
  2. 通过 compilation.assets 获取文件数量
  3. 定义 markdown 文件的内容,将文件信息写入 markdown 文件内
  4. 给 dist 文件夹里增加一个资源名称为 fileListName 的变量
  5. 写入资源的内容和文件大小
  6. 执行回调,让 webpack 继续执行
class FileListPlugin {constructor(options) {
    // 获取插件配置项
    this.filename = options && options.filename ? options.filename : 'FILELIST.md'
  }

  apply(compiler) {
    // 注册 compiler 上的 emit 钩子
    compiler.hooks.emit.tapAsync('FileListPlugin', (compilation, cb) => {
      // 通过 compilation.assets 获取文件数量
      let len = Object.keys(compilation.assets).length

      // 增加统计信息
      let content = `# ${len} file${len > 1 ? 's' : ''} emitted by webpacknn`

      // 通过 compilation.assets 获取文件名列表
      for (let filename in compilation.assets) {content += `- ${filename}n`
      }

      // 往 compilation.assets 中增加清单文件
      compilation.assets[this.filename] = {
        // 写入新文件的内容
        source: function () {return content},
        // 新文件大小(给 webapck 输入展现用)size: function () {return content.length},
      }

      // 执行回调,让 webpack 继续执行
      cb()})
  }
}

module.exports = FileListPlugin

手写插件 2:去除正文

开发一个插件可能去除打包后代码的正文,这样咱们的 bundle.js 将更容易浏览

思路:

  1. 通过 compiler.hooks.emit.tap() 来触发生成文件后的钩子
  2. 通过 compilation.assets 拿到生产后的文件,而后去遍历各个文件
  3. 通过 .source() 获取构建产物的文本,而后用正则去 replace 调正文的代码
  4. 更新构建产物对象
  5. 执行回调,让 webpack 继续执行
class RemoveCommentPlugin {constructor(options) {this.options = options}
  apply(compiler) {
    // 去除正文正则
    const reg = /("([^"]*(.)?)*")|('([^']*(.)?)*')|(/{2,}.*?(r|n))|(/*(n|.)*?*/)|(/******/)/g

    compiler.hooks.emit.tap('RemoveComment', (compilation) => {
      // 遍历构建产物,.assets 中蕴含构建产物的文件名
      Object.keys(compilation.assets).forEach((item) => {// .source()是获取构建产物的文本
        let content = compilation.assets[item].source()
        content = content.replace(reg, function (word) {
          // 去除正文后的文本
          return /^/{2,}/.test(word) || /^/*!/.test(word) || /^/*{3,}//.test(word) ? '' : word
        })
        // 更新构建产物对象
        compilation.assets[item] = {source: () => content,
          size: () => content.length,}
      })
    })
  }
}

module.exports = RemoveCommentPlugin

举荐文章

webpack 的异步加载原理及分包策略
总结 18 个 webpack 插件,总会有你想要的!
搭建一个 vue-cli4+webpack 挪动端框架(开箱即用)
从零构建到优化一个相似 vue-cli 的脚手架
封装一个 toast 和 dialog 组件并公布到 npm
从零开始构建一个 webpack 我的项目
总结几个 webpack 打包优化的办法
总结 vue 常识体系之高级利用篇
总结 vue 常识体系之实用技巧
总结 vue 常识体系之根底入门篇
总结挪动端 H5 开发罕用技巧(干货满满哦!)

退出移动版