上一篇文章咱們實現了本身的 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
完了!