手把手用代碼教你實現一個 webpack plugin

上一篇文章咱們實現了本身的 loader,這篇來實現 pluginhtml

什麼是 plugin

loader 相比,plugin 功能更強大,更靈活node

插件向第三方開發者提供了 webpack 引擎中完整的能力。使用階段式的構建回調,開發者能夠引入它們本身的行爲到 webpack 構建流程中。

loaderplugin 的區別

  • loader: 顧名思義,某種類型資源文件的加載器,做用於某種類型的文件上。webpack 自己也是不能直接打包這些非 js 文件的,須要一個轉化器即 loaderloader 自己是單一,簡單的,不能將多個功能放在一個loader裏。
  • plugin: pluginloaders 更加先進一點,你能夠擴展 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 實際上是一個構造函數(classfunction)。爲了可以得到 compiler,須要 plugin 對外暴露一個 apply 接口,這個 apply 函數在構造函數的 prototype 上。web

webpack 插件由如下組成:npm

  • 一個 JavaScript 命名函數。
  • 在插件函數的 prototype 上定義一個apply方法。
  • 指定一個綁定到 webpack 自身的事件鉤子
  • 處理 webpack 內部實例的特定數據。
  • 功能完成後調用 webpack 提供的回調。

Compiler 和 Compilation

在插件開發中最重要的兩個資源就是 compilercompilation 對象。理解它們的角色是擴展 webpack 引擎重要的第一步。segmentfault

  • compiler 對象表明了完整的 webpack 環境配置。這個對象在啓動 webpack 時被一次性創建,並配置好全部可操做的設置,包括 optionsloaderplugin。當在 webpack 環境中應用一個插件時,插件將收到此 compiler 對象的引用。可使用它來訪問 webpack 的主環境。
  • compilation 對象表明了一次資源版本構建。當運行 webpack 開發環境中間件時,每當檢測到一個文件變化,就會建立一個新的 compilation,從而生成一組新的編譯資源。一個 compilation 對象表現了當前的模塊資源、編譯生成資源、變化的文件、以及被跟蹤依賴的狀態信息。compilation 對象也提供了不少關鍵時機的回調,以供插件作自定義處理時選擇使用。

開發 plugin

知道了 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.jsapp

// 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 走一波,能夠看到輸出結果函數

image.png

說明咱們的插件已經成功運行了,你們也可自行將 compiler 打印出來看看。咱們再看涉及到 compilercompilation 的例子

// 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 官方提供的一款插件,能夠在打包後的每一個文件上面加上說明信息,像是這樣子的

image.png

固然官方提供的功能更豐富一些,打包時還能夠加上文件更多諸如 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

能夠看到結果了

image.png

打包出來的文件也有說明信息

image.png

完整代碼以下

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

完了!

相關文章
相關標籤/搜索