本文在 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 开发罕用技巧(干货满满哦!)