你必須知道的webpack插件原理分析

本文在github作了收錄 github.com/Michael-lzg…css

demo源碼地址 github.com/Michael-lzg…vue

在 webpack 中,專一於處理 webpack 在編譯過程當中的某個特定的任務的功能模塊,能夠稱爲插件。它和 loader 有如下區別:node

  1. loader 是一個轉換器,將 A 文件進行編譯成 B 文件,好比:將 A.less 轉換爲 A.css,單純的文件轉換過程。webpack 自身只支持 js 和 json 這兩種格式的文件,對於其餘文件須要經過 loader 將其轉換爲 commonJS 規範的文件後,webpack 才能解析到。
  2. plugin 是一個擴展器,它豐富了 webpack 自己,針對是 loader 結束後,webpack 打包的整個過程,它並不直接操做文件,而是基於事件機制工做,會監聽 webpack 打包過程當中的某些節點,執行普遍的任務。

plugin 的特徵

webpack 插件有如下特徵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
  1. webpack 讀取配置的過程當中會先執行 new HelloPlugin(options) 初始化一個 HelloPlugin 得到其實例。
  2. 初始化 compiler 對象後調用 HelloPlugin.apply(compiler) 給插件實例傳入 compiler 對象。
  3. 插件實例在獲取到 compiler 對象後,就能夠經過 compiler.plugin (事件名稱, 回調函數) 監聽到 Webpack 廣播出來的事件。 而且能夠經過 compiler 對象去操做 Webpack。

事件流機制

webpack 本質上是一種事件流的機制,它的工做流程就是將各個插件串聯起來,而實現這一切的核心就是 Tapablegit

Webpack 的 Tapable 事件流機制保證了插件的有序性,將各個插件串聯起來, Webpack 在運行過程當中會廣播事件,插件只須要監聽它所關心的事件,就能加入到這條 webapck 機制中,去改變 webapck 的運做,使得整個系統擴展性良好。github

Tapable 也是一個小型的 library,是 Webpack 的一個核心工具。相似於 node 中的 events 庫,核心原理就是一個訂閱發佈模式。做用是提供相似的插件接口。方法以下:web

//  廣播事件
compiler.apply('event-name', params)
compilation.apply('event-name', params)

// 監聽事件
compiler.plugin('event-name', function (params) {})
compilation.plugin('event-name', function (params) {})

咱們來看下 Tapablevue-cli

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 源碼中能夠看到,他們是:npm

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 還統一暴露了三個方法給插件,用於注入不一樣類型的自定義構建行爲:json

  • tap:能夠註冊同步鉤子和異步鉤子。
  • tapAsync:回調方式註冊異步鉤子。
  • tapPromise:Promise 方式註冊異步鉤子。

webpack 裏的幾個很是重要的對象,Compiler, CompilationJavascriptParser 都繼承了 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 就能看到效果了

image

Compiler 對象 (負責編譯)

Compiler 對象包含了當前運行 Webpack 的配置,包括 entryoutputloaders 等配置,這個對象在啓動 Webpack 時被實例化,並且是全局惟一的。Plugin 能夠經過該對象獲取到 Webpack 的配置信息進行處理。

compiler 上暴露的一些經常使用的鉤子:

image

下面來舉個例子

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 就能看到效果了

image

有一些編譯插件中的步驟是異步的,這樣就須要額外傳入一個 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 上暴露的一些經常使用的鉤子:

image

CompilerCompilation 的區別

  • Compiler 表明了整個 Webpack 從啓動到關閉的生命週期
  • Compilation 只是表明了一次新的編譯,只要文件有改動,compilation 就會被從新建立。

手寫插件 1:文件清單

在每次 webpack 打包以後,自動產生一個一個 markdown 文件清單,記錄打包以後的文件夾 dist 裏全部的文件的一些信息。

思路:

  1. 經過 compiler.hooks.emit.tapAsync() 來觸發生成資源到 output 目錄以前的鉤子
  2. 經過 compilation.assets 獲取文件數量
  3. 定義 markdown 文件的內容,將文件信息寫入 markdown 文件內
  4. 給 dist 文件夾裏添加一個資源名稱爲 fileListName 的變量
  5. 寫入資源的內容和文件大小
  6. 執行回調,讓 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 將更容易閱讀

思路:

  1. 經過 compiler.hooks.emit.tap() 來觸發生成文件後的鉤子
  2. 經過 compilation.assets 拿到生產後的文件,而後去遍歷各個文件
  3. 經過 .source() 獲取構建產物的文本,而後用正則去 replace 調註釋的代碼
  4. 更新構建產物對象
  5. 執行回調,讓 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開發經常使用技巧(乾貨滿滿哦!)

相關文章
相關標籤/搜索