上一篇文章咱们实现了本身的 loader
,这篇来实现 plugin
html
与 loader
相比,plugin
功能更强大,更灵活node
插件向第三方开发者提供了webpack
引擎中完整的能力。使用阶段式的构建回调,开发者能够引入它们本身的行为到webpack
构建流程中。
loader
和 plugin
的区别loader
: 顾名思义,某种类型资源文件的加载器,做用于某种类型的文件上。webpack
自己也是不能直接打包这些非 js
文件的,须要一个转化器即 loader
。 loader
自己是单一,简单的,不能将多个功能放在一个loader里。plugin
: plugin
比 loaders
更加先进一点,你能够扩展 webpack
的功能来知足本身的须要。当 loader
不能知足的时候,就须要 plugin
了。plugin
的基本结构想必你们对 html-webpack-plugin
见得很是多,一般咱们都是这么使用的webpack
plugins: [ new webpack.DefinePlugin({ 'process.env': require('../config/dev.env') }), new webpack.HotModuleReplacementPlugin(), new webpack.NamedModulesPlugin(), // HMR shows correct file names in console on update. new webpack.NoEmitOnErrorsPlugin(), new HtmlWebpackPlugin({ filename: 'index.html', template: 'index.html', inject: true }) ]
发现 webpack
plugin
实际上是一个构造函数(class
或 function
)。为了可以得到 compiler
,须要 plugin
对外暴露一个 apply
接口,这个 apply
函数在构造函数的 prototype
上。web
webpack
插件由如下组成:npm
- 一个
JavaScript
命名函数。- 在插件函数的
prototype
上定义一个apply
方法。- 指定一个绑定到
webpack
自身的事件钩子。- 处理
webpack
内部实例的特定数据。- 功能完成后调用
webpack
提供的回调。
在插件开发中最重要的两个资源就是 compiler
和 compilation
对象。理解它们的角色是扩展 webpack
引擎重要的第一步。segmentfault
compiler
对象表明了完整的 webpack
环境配置。这个对象在启动 webpack
时被一次性创建,并配置好全部可操做的设置,包括 options
,loader
和 plugin
。当在 webpack
环境中应用一个插件时,插件将收到此 compiler
对象的引用。可使用它来访问 webpack
的主环境。compilation
对象表明了一次资源版本构建。当运行 webpack
开发环境中间件时,每当检测到一个文件变化,就会建立一个新的 compilation
,从而生成一组新的编译资源。一个 compilation
对象表现了当前的模块资源、编译生成资源、变化的文件、以及被跟踪依赖的状态信息。compilation
对象也提供了不少关键时机的回调,以供插件作自定义处理时选择使用。知道了 plugin
的基本构造,咱们就能够着手来写一个 plugin
了,仍是和开发 loader
时的目录同样,在src
中新建一个 plugins
文件夹,里面新建一个 DemoPlugin.js
,里面内容为api
// src/plugins/DemoPlugin.js class DemoPlugin { constructor(options) { this.options = options } apply(compiler) { // console.log(compiler) console.log('applying', this.options) } }
入口文件 app.js
app
// src/app.js console.log('hello world')
webpack
配置async
// webpack.config.js const DemoPlugin = require('./src/plugins/DemoPlugin') module.exports = { mode: 'development', entry: __dirname + "/src/app.js", output: { path: __dirname + "/dist", filename: "[name].js" }, ... plugins: [ new DemoPlugin({ name: 'Jay' }) ] }
执行 ./node_modules/.bin/webpack
走一波,能够看到输出结果函数
说明咱们的插件已经成功运行了,你们也可自行将 compiler
打印出来看看。咱们再看涉及到 compiler
和 compilation
的例子
// src/plugins/DemoPlugin.js class DemoPlugin { constructor(options) { this.options = options } apply(compiler) { // Tap into compilation hook which gives compilation as argument to the callback function compiler.hooks.compilation.tap("DemoPlugin", compilation => { // Now we can tap into various hooks available through compilation compilation.hooks.optimize.tap("DemoPlugin", () => { console.log('Assets are being optimized.') }) }) } }
关于 compiler
, compilation
的可用钩子函数,请查看插件文档。
接下来咱们来本身写一个 BannerPlugin 的插件,这个插件是 webpack
官方提供的一款插件,能够在打包后的每一个文件上面加上说明信息,像是这样子的
固然官方提供的功能更丰富一些,打包时还能够加上文件更多诸如 hash
, chunkhash
, 文件名以及路径等信息。
这里咱们只实如今打包时加个说明,插件就命名为 MyBannerPlugin
吧。在 plugins
文件下新建 MyBannerPlugin.js
,怎么写待会儿再说,咱们先在 webpack.config.js
中加上该插件
const path = require('path') const MyBannerPlugin = require('./src/plugins/MyBannerPlugin') module.exports = { mode: 'development', devtool: 'eval-source-map', entry: __dirname + "/src/app.js", output: { path: __dirname + "/dist", filename: "[name].js" }, plugins: [ new DemoPlugin({ name: 'Jay' }), new MyBannerPlugin('版权全部,翻版必究') // 或这么调调用 // new MyBannerPlugin({ // banner: '版权全部,翻版必究' // }) ] }
但愿支持两种调用方式,直接传字符串或者对象的形式,那就开始写吧
// src/plugins/MyBannerPlugin.js class MyBannerPlugin { constructor(options) { if (arguments.length > 1) throw new Error("MyBannerPlugin only takes one argument (pass an options object or string)") if (typeof options === 'string') { options = { banner: options } } this.options = options || {} this.banner = options.banner } } module.exports = MyBannerPlugin
这样,咱们已经拿到传过来的配置,可是咱们的需求是在打包后的文件头部加上的说明信息是带有注释的,固然,也能够给使用者一个选项是否用注释包裹
// src/plugins/MyBannerPlugin.js const wrapComment = str => { if (!str.includes('\n')) return `/*! ${str} */` return `/*!\n * ${str.split('\n').join('\n * ')}\n */` } class MyBannerPlugin { constructor(options) { ... if (typeof options === 'string') { options = { banner: options, raw: false // 默认是注释形式 } } this.options = options || {} this.banner = this.options.raw ? options.banner : wrapComment(options.banner) } } module.exports = MyBannerPlugin
接下来就写 apply
部分了。因为要对文件写入东西,咱们须要引入一个 npm
包。
npm install --save-dev webpack-sources
const { ConcatSource } = require('webapck-sources') ... apply (compiler) { const banner = this.banner // console.log('banner: ', banner) compiler.hooks.compilation.tap("MyBannerPlugin", compilation => { compilation.hooks.optimizeChunkAssets.tap("MyBannerPlugin", chunks => { for (const chunk of chunks) { for (const file of chunk.files) { compilation.updateAsset( file, old => new ConcatSource(banner, '\n', old) ) } } }) }) } ...
跑一下
./node_modules/.bin/webpack
能够看到结果了
打包出来的文件也有说明信息
完整代码以下
const { ConcatSource } = require('webpack-sources') const wrapComment = (str) => { if (!str.includes('\n')) return `/*! ${str} */` return `/*!\n * ${str.split('\n').join('\n * ')}\n */` } class MyBannerPlugin { constructor (options) { if (arguments.length > 1) throw new Error("MyBannerPlugin only takes one argument (pass an options object or string)") if (typeof options === 'string') { options = { banner: options, raw: false // 默认是注释形式 } } this.options = options || {} this.banner = this.options.raw ? options.banner : wrapComment(options.banner) } apply (compiler) { const banner = this.banner console.log('banner: ', banner) compiler.hooks.compilation.tap("MyBannerPlugin", compilation => { compilation.hooks.optimizeChunkAssets.tap("MyBannerPlugin", chunks => { for (const chunk of chunks) { for (const file of chunk.files) { compilation.updateAsset( file, old => new ConcatSource(banner, '\n', old) ) } } }) }) } } module.exports = MyBannerPlugin
再看一个官网给的统计打包后文件列表的例子,在 plugins
中新建 FileListPlugin.js
,直接贴代码
// src/plugins/FileListPlugin.js class FileListPlugin { apply(compiler) { // emit is asynchronous hook, tapping into it using tapAsync, you can use tapPromise/tap(synchronous) as well compiler.hooks.emit.tapAsync('FileListPlugin', (compilation, callback) => { // Create a header string for the generated file: var filelist = 'In this build:\n\n' // Loop through all compiled assets, // adding a new line item for each filename. for (var filename in compilation.assets) { filelist += '- ' + filename + '\n' } // Insert this list into the webpack build as a new file asset: compilation.assets['filelist.md'] = { source: function() { return filelist }, size: function() { return filelist.length } } callback() }) } } module.exports = FileListPlugin;
// webpack.config.js ... const FileListPlugin = require('./src/plugins/FileListPlugin') ... plugins: [ new DemoPlugin({ name: 'Jay' }), new MyBannerPlugin({ banner: '版权全部,翻版必究' }), new FileListPlugin() ] ...
打包后会发现,dist
里面生成了一个 filelist.md
的文件,里面内容为
In this build: - main.js
完了!