本文在github做了收录 github.com/Michael-lzg…
demo源码地址 github.com/Michael-lzg…
在 webpack 中,专一于解决 webpack 在编译过程中的某个特定的工作的功能模块,能够称为插件。它和 loader
有以下区别:
loader
是一个转换器,将 A 文件进行编译成 B 文件,比方:将A.less
转换为A.css
,单纯的文件转换过程。webpack 本身只反对 js 和 json 这两种格局的文件,对于其余文件须要通过loader
将其转换为 commonJS 标准的文件后,webpack 能力解析到。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
- webpack 读取配置的过程中会先执行
new HelloPlugin(options)
初始化一个HelloPlugin
取得其实例。 - 初始化
compiler
对象后调用HelloPlugin.apply(compiler)
给插件实例传入 compiler 对象。 - 插件实例在获取到
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
, Compilation
和 JavascriptParser
都继承了 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 的配置,包含 entry
、output
、loaders
等配置,这个对象在启动 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
上裸露的一些罕用的钩子:
Compiler
和 Compilation
的区别
Compiler
代表了整个 Webpack 从启动到敞开的生命周期Compilation
只是代表了一次新的编译,只有文件有改变,compilation
就会被从新创立。
手写插件 1:文件清单
在每次 webpack 打包之后,主动产生一个一个 markdown 文件清单,记录打包之后的文件夹 dist 里所有的文件的一些信息。
思路:
- 通过
compiler.hooks.emit.tapAsync()
来触发生成资源到 output 目录之前的钩子 - 通过
compilation.assets
获取文件数量 - 定义 markdown 文件的内容,将文件信息写入 markdown 文件内
- 给 dist 文件夹里增加一个资源名称为 fileListName 的变量
- 写入资源的内容和文件大小
- 执行回调,让 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
将更容易浏览
思路:
- 通过
compiler.hooks.emit.tap()
来触发生成文件后的钩子 - 通过
compilation.assets
拿到生产后的文件,而后去遍历各个文件 - 通过
.source()
获取构建产物的文本,而后用正则去 replace 调正文的代码 - 更新构建产物对象
- 执行回调,让 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开发罕用技巧(干货满满哦!)