再也不只知其一;不知其二,此次真的讓你理解透徹webpack打包原理,小白讀不懂?不存在的!

文章由來

在上一篇文章中分享了關於運用webpack搭建vue項目的經驗和總結,但還僅僅停留在只是會用webpack搭建腳手架的階段,對webpack原理仍是不怎麼清楚,再加上各大論壇對webpack原理解析的精品文章較少,要麼是一些標題黨,通篇教你如何配置webpack,如何優化;要麼就是通篇copy源碼+簡單註解;固然也有大牛寫的文章,文章雖好,但晦澀難懂,誰讓小弟不才呢。css

種種緣由,決定狠下心研究下webpack的實現原理(真的是難啊)。但我相信,通讀此篇,就算是菜雞,也能對webpack的原理理解透徹。html

好了,閒話很少說,先看看webpack官網對本身的定義,從定義中尋找突破口!let's go~vue

本質上,webpack 是一個現代 JavaScript 應用程序的靜態模塊打包器(module bundler)。當 webpack 處理應用程序時,它會遞歸地構建一個依賴關係圖(dependency graph),其中包含應用程序須要的每一個模塊,而後將全部這些模塊打包成一個或多個 bundlenode

關鍵字 遞歸依賴生成一個或多個bundle

什麼是遞歸的構建,什麼又是依賴呢?以下圖webpack

模塊A.js引用模塊B.js,模塊B.js引用模塊C.js,此時A、B、C就構成了依賴關係,那爲何要遞歸的構建呢?請問,webpack配置文件是怎麼配置的?git

不論是單entry仍是多entry,配置文件的entry僅僅只有一個或多個入口文件。github

拿上個例子來講,將A.js設置爲entry,此時webpack打包時,就必須把A.js中全部require的模塊打包在一塊兒(B.js),但此時B.js也有依賴(C.js),這時候就必須遞歸的進行解析了(若是依賴中還有依賴,那接着遞歸)。先把C.js與B.js進行打包合併,而後把合併後的代碼與A.js合併,打包生成最終的bundle。web

是否是有點頭緒了?上面的例子僅僅是最爲簡單的分析了webpack是如何從entry解析構建依賴模塊。下面讓咱們從項目中分析下webpack的打包後的代碼。npm

項目結構目錄以下json

├── node_modules
├── src
│   ├── index.js
│   ├── ticket.js
│   ├── price.js
├── webpack.config.js
複製代碼

webpack配置項。

const path = require('path')
module.exports = {
  entry: './src/index.js',
  output: {
    filename: 'min-bundle.js',
    path: path.join(__dirname, 'dist')
  },
}
複製代碼

模塊代碼

分析webpack打包的bundle文件

三個模塊文件,index.js爲webpack的入口文件,它依賴了ticket.jsticket.js依賴了price.js。咱們但願webpack打包生成的min-bundle.js運行後,可以log出 「迪斯尼門票甩賣,門票價格爲299人民幣/人」,執行打包,果不其然。那麼問題來了,webpack怎麼作到的呢?

這就得看生成的min-bundle.js了,爲了更容易理解,將無關代碼儘量刪除後,主要代碼以下:

(臥槽,這都是啥?說好了不貼源碼呢?不要急,我們慢慢分析)

發現了嗎?生成的bundle.js其實就是一個自調用函數,參數是一個對象,爲當前項目中的入口文件和其依賴模塊,即./src/index.js,./src/ticket.js,./src/price.js是一個函數,就是對應每一個模塊內的代碼,使用eval來執行內部代碼。自調用函數中,函數體內有一個__webpack_require__函數。下面開始逐步分析:

第一次執行

自調用函數中直接return __webpack_require__('./src/index.js'),因而開始執行__webpack_require__函數,__webpack_require__函數的參數moduleId,在第一次執行時就是項目的入口文件,即./src/index.js。進入函數體內看看?發現有個 module 對象

const module = {
    i: moduleId,
    exports: {}
}
該module對象的主要做用是,爲每個模塊提供一個單獨的module對象,module對象內還有一個exports對象
這就使得每一個模塊均可以使用module.exports和exports來對外暴露
複製代碼

接下來開始執行下面這行代碼:modules[moduleId].call(module.exports, module, module.exports, __webpack_require__)。一臉懵逼沒關係,跟上節奏,下面一步步解釋每行代碼的做用

  • modules是什麼?就是自調用函數中傳入的參數,也就是上圖中紅框內的對象

  • 那麼moduleId呢?在第一次執行階段爲'./src/index.js'

  • 因此代碼就變成了:modules['./src/index.js'],什麼意思? 這一步,就是執行modules對象內,鍵爲'./src/index.js'的函數,怎麼執行呢?this指向指給誰呢? ...call(module.exports, module, module.exports, __webpack_require__)中,有四個參數

    arg1:`module.exports`,明確this指向(由於每一個模塊都會有各自的module對象)
    arg2:`module`對象,使得模塊內能夠經過module.exports來對外暴露
    arg3:`module.exports`,使得模塊內能夠經過exports來對外暴露
    arg4:`__webpack_require__`函數,爲何?既然要執行modules對象內全部的鍵對應的函數
           那函數內使用`__webpack_require__()`來進一步添加依賴,這個函數從哪來呢?
           就是從這傳進來的,也就是用來遞歸的調用`__webpack_require__`
    複製代碼
  • 開始執行 modules對象內鍵爲'./src/index.js'的函數

    function(module, exports, __webpack_require__) {
       eval(`const ticket = __webpack_require__("./src/ticket.js");console.log('迪斯尼門票甩賣,' + ticket);`)
    }
    複製代碼
  • 發現該函數調用了__webpack_require__("./src/ticket.js"),那豈不是又要走一遍上面的流程? 沒錯,由於index.js依賴了ticket.js

第二次執行

此時__webpack_require__(moduleId)的實參就變成'./src/ticket.js',仍然重複上面的步驟,當執行modules['./src/ticket.js'].call()時,就要執行 modules 對象中鍵爲'./src/ticket.js'的函數了

function(module, exports, __webpack_require__) {
    eval(`const price = __webpack_require__("./src/price.js");module.exports = '門票價格爲' + price.content;`)
}
複製代碼

發現該函數又依賴了price.js,沒招啊,接着遞歸唄

第三次執行

此時__webpack_require__(moduleId)的實參就變成'./src/price.js',仍然重複上面的步驟,執行modules 對象中鍵爲'./src/price.js'的函數

function(module, exports, __webpack_require__) {
    eval(`module.exports = {content: '299人民幣/人'};`)
}
複製代碼

price.js中沒有依賴項,因而直接返回{content: '299人民幣/人'}

完事了?固然沒有

price.js是執行完了,ticket.js還等着呢,因而開始賦值

未遞歸執行price.js時
eval(` const price = __webpack_require__("./src/price.js"); module.exports = '門票價格爲' + price.content; `)
遞歸執行price.js後
eval(` const price = {content: '299人民幣/人'} module.exports = '門票價格爲' + '299人民幣/人' `)
複製代碼

別急啊老弟,ticket.js是執行完了,index.js還等着呢

未遞歸執行ticket.js時
eval(` const ticket = __webpack_require__("./src/ticket.js"); console.log('迪斯尼門票甩賣,' + ticket); `)
遞歸執行ticket.js後
eval(` const ticket = '門票價格爲299人民幣/人' console.log('迪斯尼門票甩賣,' + '門票價格爲299人民幣/人'); `)
複製代碼

此時,全部依賴模塊解析完成,回到最初自調用函數的代碼, return __webpack_require__("./src/index.js")

此時的__webpack_require__("./src/index.js")已經有告終果,即

'迪斯尼門票甩賣,門票價格爲299人民幣/人'

直接return,大功告成!可喜可賀!

階段總結:webpack將每一個js文件的名稱做爲,該js文件的代碼做爲,一一存入到對象中做爲參數。而後本身實現了一個__webpack_require__函數,經過該函數,遞歸導入依賴關係。

到這裏,分析webpack打包後的bundle.js就告一段落了。上面說了,webpack是把每一個js文件的名稱做爲,該js文件的代碼做爲,一一存入到對象中做爲參數的。那麼問題來了,它內部是怎麼操做的?若是配置了loaderplugin,又是如何處理模塊內的js代碼呢?

下面讓咱們實操一下,實現一個屬於本身的迷你webpack,深入的體會webpack的打包原理,loader原理和插件原理。

項目準備工做

新建兩個項目,一個項目是min-pack的主程序,你能夠理解爲webpack,發佈到npm後,供開發者經過 npm install min-pack 後使用;另外一個項目是開發者本身的項目,也就是說,你要用min-pack,得先有本身的程序代碼啊,否則你讓min-pack打包誰?

min-pack的實現

準備工做

  1. 首先新建一個目錄,命名爲min-pack

  2. 在根目錄下新建lib目錄,目錄內新建Compiler.js,該js用來實現解析打包,稍後會詳細解讀

  3. 在根目錄下,新建template目錄,目錄內新建output.ejs,使用ejs模板來生成打包代碼

  4. 在該項目下新建bin目錄,將打包工具主程序放入其中

    #!/usr/bin/env node
    const path = require('path')
    
    // minpack.config.js 爲開發者本身的項目下的配置文件,webpack4默認是0配置的
    // 咱們這裏就不作那麼複雜了,直接指定配置文件爲 minpack.config.js
    // 也就是說,你要用個人 min-pack,你項目的根目錄下就必須有 minpack.config.js 配置文件
    // 注意: path.resolve 能夠來解析開發者工做目錄下的 minpack.config.js
    const config = require(path.resolve('minpack.config.js'))
    
    // 引入打包的主程序代碼 Compiler.js
    const Compiler = require('../lib/Compiler')
    // 將配置文件傳入Compiler中,並執行start方法,開始打包
    new Compiler(config).start()
    複製代碼

    注意:主程序的頂部應當有:#!/usr/bin/env node標識,指定程序執行環境爲node

  5. 在該項目中的package.json中配置bin腳本

    "bin": {
         "min-pack": "./bin/min-pack.js"
     }
     // 這樣配置完後,在開發者本身的項目中,就可使用 `min-pack` 來進行打包了。
    複製代碼
  6. 經過npm link連接到全局包中,供本地測試使用。測試完成後再發布到npm上,供第三方開發者使用

完成了上述操做,你就能夠在另外一個項目中,也就是開發者要打包的項目裏運行 min-pack 了。但如今的Compiler.js尚未實現,因此還作不到解析構建,下面讓咱們來實現下打包功能。

Compiler類

Compiler.js要作什麼?

上面說了,你要用個人 min-pack,你的項目根目錄下必須有 minpack.config.js 配置文件

  1. Compiler.js 接受傳入的 minpack.config.js,獲取到配置文件中 entry 對應的值,也就是入口文件,如 ./src/index.js

  2. 使用 node 模塊中的 fs.readFileSync 讀取該模塊文件,得到模塊文件的源代碼

  3. 將該模塊源代碼轉換爲 AST 語法樹。what?什麼是AST 語法樹?

    • 其實UglifyJS或babel轉換代碼,實際的背後就是在對JavaScript的抽象語法樹進行操做。
    • 能夠先簡單的理解,AST語法樹,就是爲了讓咱們更高效,更簡潔的對JavaScript代碼進行操做。由於在下面第 4 步中會將模塊源代碼中require替換成 __webpack_require__,怎麼替換?難道你讓我寫正則?或是操做字符串?那就太Low了吧
  4. 將源代碼中的 require,所有替換成 __webpack_require__ (爲何?)

    • 由於瀏覽器環境並不識別require語法。你可能就要問了,我項目中全部的依賴都是使用 import A from 'xx'來導入模塊,使用 export const xx = 1exports default {...} 來導出模塊的,沒使用 require啊。那麼請問,你是否是使用 babel 來處理js的,babel 內部會把你的 import 轉換爲 require,把 exportexport default 轉換爲 exports。以下圖

    • 再回憶下最開始咱們分析 webpack 打包出的 min-bundle.js 時,能夠發現,該js內部把咱們項目中的入口文件的及其全部依賴內部的require() 所有替換成了 __webpack_require__,而後本身實現了 __webpack_require__,該函數內部定義了 module 對象,對象內部有 exports: {},因此,你可使用exports或module.exports來導出模塊了,使用 __webpack_require__ 來導入模塊。

  5. 將模塊文件的 require() 中的參數,也就是模塊文件的依賴模塊路徑,存入數組中,暫且將該數組命名爲 dependencies

  6. 將模塊文件的相對路徑,也就是 ./src/xxx.js 做爲,處理後的源代碼做爲,存儲到一個對象中,暫且把該對象定義爲 modules

    • 爲何要存入對象中?又得回憶下上面分析的 min-bundle.js 了。它內部是一個自調用函數,該函數的參數就是剛剛定義的 modules 對象,函數體內經過 __webpack_require__ 遞歸的調用 modules 對象中的每個對應的,也就是該對應的源代碼。
  7. 第一個模塊文件解析完畢,若是該模塊有依賴文件,就要開始解析它的依賴模塊了,怎麼解析呢?第 5 步驟中,將依賴模塊路徑存入到了 dependencies 數組中,ok,遍歷這個數組,遞歸的開始上面第 2 步,直到最後一個模塊沒有依賴模塊,完成遞歸。

  8. 此時 modules ,就是以模塊路徑爲鍵該模塊源代碼爲值的對象,以下圖

  9. 如今 modules 也有了,怎麼生成打包代碼呢?別忘了,咱們有一份模板 output.ejs,看看該模板內部:

    熟不熟悉?這不就相似於咱們最開始分析的webpack打包生成的 min-bundle.js嗎?咱們要作的,就是在 Compiler.js 內部,將入口文件路徑以及剛剛生成的 modules 對象,使用ejs模板語法,進行嵌套

  10. 嵌套完成後,讀取配置文件中的output路徑,經過 fs.writeFileSync,將output.ejs中的內容寫入到開發者項目中指定的目錄內

  11. 完成打包!

總結下,基本思路就是

  • 遞歸的查找依賴, 並解析 AST 語法樹, 修改全部依賴的 require 爲__webpack_require__
  • 利用 fs 模塊讀取全部的修改後的依賴代碼
  • 將每個模塊依賴的相對路徑做爲鍵, 該模塊代碼做爲值, 存放到對象中, 用於生成最後的 bundle 文件

Compiler類的實現

const path = require('path')
const fs = require('fs')
const ejs = require('ejs')
// 解析AST語法樹
const parser = require('@babel/parser')
// 維護整個AST 樹狀態,負責替換,刪除和添加節點
const traverse = require('@babel/traverse').default
// 將AST轉換爲代碼
const generator = require('@babel/generator').default

class Compiler {
  constructor(config) {
    this.config = config
    this.entry = config.entry
    // root: 執行 min-pack 指令的目錄的絕對路徑
    this.root = process.cwd()
    this.modules = {}
  }
  
  /** * 打包依賴分析 * @param {Object} modulePath 當前模塊的絕對路徑 */
  depAnalyse(modulePath, relativePath) {

    let self = this

    // 1. 讀取模塊文件的代碼
    let source = fs.readFileSync(modulePath, 'utf-8')

    // 2. 聲明依賴數組, 存儲當前模塊的全部依賴
    let dependencies = []
    
    // 3. 將當前模塊代碼轉爲AST語法
    let ast = parser.parse(source)

    // 4. 修改 AST 語法樹
    traverse(ast, {
      CallExpression(p) {
        
        if(p.node.callee.name === 'require') {

          p.node.callee.name = '__webpack_require__'
          
          // 提取並處理require()中傳入的文件路徑
          p.node.arguments[0].value = './' + path.join('src', p.node.arguments[0].value))
          
          // 處理路徑中的反斜槓 \
          p.node.arguments[0].value = p.node.arguments[0].value.replace(/\\+/g, '/')
          
          // 將處理好的當前模塊路徑存入dependencies數組中,用於遞歸調用 depAnalyse 函數
          dependencies.push(p.node.arguments[0].value)
        }
      }
    })

    // 5. 將處理好的 AST 語法樹轉爲程序代碼
    let resultSourceCode = generator(ast).code

    // 6. 獲取 執行打包指令目錄的絕對路徑 與 當前模塊的絕對路徑的 相對路徑
    let modulePathRelative = this.replaceSlash('./' + path.relative(this.root, modulePath))
    
    // 7. 將 6 中獲取到的相對路徑爲鍵, 當前模塊AST處理後的代碼爲值, 存儲至 this.modules
    this.modules[modulePathRelative] = resultSourceCode

    dependencies.forEach(dep => {
      return this.depAnalyse(path.resolve(this.root, dep), dep)
    })

  }

  /** * 將生成的 this.modules 與獲取模板字符串進行拼接 */
  emitFile() {
    const templatePath = path.join(__dirname, '../template/output.ejs')
    // 讀取模板文件
    let template = fs.readFileSync(templatePath, 'utf-8')
    // 進行模板渲染
    let result = ejs.render(template, {
      entry: this.entry,
      modules: this.modules
    })

    // 讀取執行打包的配置文件中的output, 將生成好的 result 寫入配置output指定文件中
    let outputPath = path.join(this.config.output.path, this.config.output.filename)

    fs.writeFileSync(outputPath, result)

  }

  start() {
    // 1. 依賴分析
    this.depAnalyse(path.resolve(this.root, this.entry), this.entry
    // 2. 生成最終的打包後的代碼
    this.emitFile()
  }
  
}

module.exports = Compiler
複製代碼

上面就是Compiler的代碼實現,完成了該步驟,意味着你的項目代碼就能夠經過 min-pack 進行打包了,趕忙動手嘗試一下吧~

固然,這個僅僅是超級無敵迷你的webpack版,讀到這,你可能忽略了 loaderplugin的存在,也可能有一些疑問,如何在本身寫的 min-pack中加入相似於 webpack中的loaderplugin功能呢?

什麼是loader

webpack 可使用 loader 來預處理文件。這容許你打包除 JavaScript 以外的任何靜態資源。你可使用 Node.js 來很簡單地編寫本身的 loader

簡單地說,一個loader就是一個js文件,對外暴露一個函數,該函數用來處理模塊代碼,如

上圖中,就是一個既簡單的loader,該loader接受一個參數,這個參數就是 模塊的源代碼,函數體內對源代碼一系列進行操做

那麼如何將loader與咱們本身寫好的 Compiler.js 結合呢?

min-pack中添加loader的功能

  • 既然你須要loader來處理你的代碼,就必須有這個loader,並且你的 minpack.config.js 也必須配置相應的 rules,注意哦,rules 中 use 的值,多是字符串,多是對象,也多是數組。
    module: {
        rules: [
          {
            test: /\.js$/,
            use: [
              './loaders/loader1.js',
              './loaders/loader2.js',
              './loaders/loader3.js'
            ]
          }
        ]
      }
    複製代碼
  • Compiler.js 中,讀取配置文件中的 rules
  • 由於 Compiler.jsdepAnalyse 函數內部,讀取到模塊文件的源代碼,此時將模塊代碼做爲參數,倒序迭代調用全部loader函數(loader的加載順序從右到左,因此調用時也必須倒敘的調用)
  • 最後返回處理後的代碼,進行 AST 語法樹解析,替換 require (以前的步驟).....
  • 因此,一旦loader匹配到正確的文件類型,就要調用該loader函數,一個文件有n個loader匹配到,該文件就會被處理n次,完成後,返回處理後的代碼,這也就是爲何webpack打包在 loader 這一層上耗時最多的緣由,只有匹配到,就調用loader函數處理

啊,好累啊,有點寫不動了...

什麼是plugin

plugin可謂是 webpack 生態系統的重要組成部分之一,它同時對外提供了插件接口,可讓開發者直接觸及到編譯過程當中

官方定義:插件可以 鉤入(hook) 到在每一個編譯(compilation)中觸發的全部關鍵事件

簡單理解,插件就是在webpack編譯過程的生命週期鉤子中,進行編碼開發,實現對應功能。也就是你的插件是須要在編譯過程當中的哪個週期中執行,就調用對應的鉤子函數,在該鉤子內部,實現功能

附上webpack編譯時compiler的生命週期鉤子

疑問: webpack不是打包器嗎?爲何要有生命週期呢?它又是如何實現生命週期的?

經過上面 Compiler.jsloader 的實現,不難看出,webpack 的編譯流程就好像一條流水線,每個編譯 階段的就像是一個流水線工人對其進行加工,A加工完交給B,B加工完交給C...每一個工人的職責都是單一的,直到加工完成。

如今我有一個礦泉水加工廠,讓咱們看看一瓶水是怎麼生產出來的:

  • 首先,儲存原水,沒有水,你讓我加工啥?相似於編寫webpack.config.js,沒有配置文件,你讓我打包啥?(固然webpack4牛逼,默認不配置)
  • 多層過濾器,相似於 loader,每一瓶水都要通過過濾器過濾
  • 紫外線殺菌,相似於 JS css 代碼壓縮(uglifyjs, mini-css-extract-plugin)。咦,這不就是插件嗎?
  • 裝瓶並粘貼廣告,相似於 html-webpack-plugin插件,將 bundle.js 自動引入生產的html中。咦,這不也是插件嗎?
  • 加工完成!

如今有個問題,加工礦泉水的機器,是怎麼知道何時殺菌,何時裝瓶,何時貼廣告呢? 同理 webpack

其實,webpack內部,經過 Tapable 這個小型 library ,有了它就能夠經過事件流的形式,將各個生成線串聯起來,其核心原理採用了發佈訂閱者的模式。Tapable提供了一系列同步和異步鉤子,webpack 使用這些鉤子,定義本身的生命週期。webpack 在運行過程當中,在不一樣階段,發佈相應的事件,插件內部只須要訂閱你須要使用的事件,webpack編譯到了該階段時,會去執行你插件中訂閱事件的回調函數。

一臉懵逼?不要緊,讓咱們接着回到上一個例子中

  • 在儲存原水時,安排一個工人張三,一旦開始加工,廣播:「開始加工啦!!!」(發佈)
  • 在開始過濾水階段,安排一個工人李四,一旦開始過濾,廣播:「開始過濾啦!!!」(發佈)
  • 過濾完成後,安排一個工人王五,一旦過濾完成,廣播:「過濾結束啦!!!」(發佈)
  • 在加工完成時,安排一個工人趙六,一旦加工完成,廣播:「加工完成啦!!!」(發佈)
  • 那麼問題來了,我有兩個步驟,殺菌和裝瓶並粘貼廣告,這兩個到底該放到何時呢?我能在加工前殺菌嗎?張三就說了,你走開,我還沒開始呢!那我能在過濾前殺菌嗎?李四又不讓了。
  • 因此,在殺菌這一步,我就必須在內部告訴機器,你要給我在王五廣播完「過濾結束後,開始殺菌」,這叫什麼?這就是在訂閱王五的廣播,一旦到了王五廣播的時候,我就要殺菌!
  • 同理,裝瓶並粘貼廣告這一步也一樣,它也要訂閱王五的廣播,在王五廣播後,裝瓶。
    • 不對吧?我沒殺菌就裝瓶?黑商!
    • 其實,殺菌機器和裝瓶機器,是誰按順序放置的?固然是製造商,因此,這就須要人爲的操控了
    • 一樣,webpack 中的插件,也會按照順序執行,個人代碼先通過A插件處理,處理完後把處理後的代碼交給B插件。那插件順序誰寫的?固然是你咯,因此,在使用插件時,必須知道每一個插件是作什麼的,而後按順序調用插件。

是否是對插件的運行機制有所瞭解了?別急,讓咱們在本身實現的 min-pack 中利用 Tapable 這個庫,實現一個插件。

實現生命週期併發布事件

  1. 首先安裝 tapable,如何使用 tapable傳送門

  2. 而後在 Compiler 類中,定義生命週期

    class Compiler {
      constructor(config) {
        this.config = config
        this.entry = config.entry
        // root: 執行 min-pack 指令的目錄的絕對路徑
        this.root = process.cwd()
        this.hooks = {
          start: new SyncHook(), // min-pack開始編譯鉤子
          compile: new SyncHook(["relativePath"]), // 編譯中的鉤子 能夠知道當前編譯的模塊名
          afterCompile: new SyncHook(), // 所有編譯完成鉤子
          emit: new SyncHook(["filename"]), // 開始打包bundle.js鉤子
          afterEmit: new SyncHook(["outputPath"]), // 打包bundle.js結束鉤子
          done: new SyncHook() // min-pack編譯結束鉤子
        }
        this.modules = {}
      }
    }
    複製代碼

    上面,咱們定義了6個生命週期鉤子,那在何時發佈呢?

  3. 發佈生命週期鉤子

    start() {
        // 總體編譯開始鉤子(start)
        this.hooks.start.call()
        
        // 正在編譯鉤子(compile)
        this.hooks.compile.call() 
        
        // 主編譯函數 開始編譯
        this.depAnalyse(path.resolve(this.root, this.entry), this.entry)
        
        // 編譯結束鉤子(afterCompile)
        this.hooks.afterCompile.call()
        
        // 總體編譯完成鉤子(done)
        this.hooks.done.call()
      }
    複製代碼

    在 函數內,發佈 emit 和 afterEmit 鉤子,具體代碼在上面講解過,此處省略部分代碼

    emitFile() {
        // ......此處省略代碼
        
        // 開始打包bundle.js鉤子(emit)
        this.hooks.emit.call(this.config.output.filename)
        
        // fs 寫入文件(生成bundle.js)
        fs.writeFileSync(outputPath, result)
        
        // 打包bundle.js結束鉤子(afterEmit)
        this.hooks.afterEmit.call(outputPath)
      }
    複製代碼
  4. ok,咱們的生命週期有了,也在指定的階段發佈了相應的事件了,接下來幹嗎?寫插件啊!終於能寫一個屬於本身的插件了。

    • 可是因爲是咱們本身實現的迷你版的 webpack,因此並無 Compilation 對象,嗯?第一次據說,什麼是 Compilation?稍後解釋。
    • 因此,咱們的插件只能是編寫 helloWorld 級別的,那就將他暫時命名爲 HelloWorldPlugins

實現 HelloWorldPlugins 插件

怎麼寫一個webpack插件呢? 官方定義:

webpack 插件由如下組成:

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

補充下第3條,並不必定只能是一個,當你的插件中須要在不一樣階段作不一樣操做時,也能夠綁定多個事件鉤子,只不過不推薦罷了,最好一個插件單獨作一個功能。

看代碼~

module.exports = class HelloWorldPlugins {
  // apply方法
  apply(compiler) {
   // 指定一個(這個插件中爲多個)綁定到 webpack 自身的事件鉤子。
   // 訂閱 start 鉤子
    compiler.hooks.start.tap('HelloWorldPlugin', () => {
      console.log('webpack開始編譯')
    });
    
    // 訂閱 compile 鉤子
    compiler.hooks.compile.tap('HelloWorldPlugin', () => {
      console.log('編譯中')
    });
    
    // 訂閱 afterCompile 鉤子
    compiler.hooks.afterCompile.tap('HelloWorldPlugin', () => {
      console.log('webpack編譯結束')
    });
    
    // 訂閱 emit 鉤子
    compiler.hooks.emit.tap('HelloWorldPlugin', (filename) => {
      console.log('開始打包文件,文件名爲: ', filename)
    });
    
    // 訂閱 afterEmit 鉤子
    compiler.hooks.afterEmit.tap('HelloWorldPlugin', (path) => {
      console.log('文件打包結束,打包後文件路徑爲: ', path)
    });
    
    // 訂閱 done 鉤子
    compiler.hooks.done.tap('HelloWorldPlugin', () => {
      console.log('webpack打包結束')
    })
  }
}
複製代碼

運行後看看日誌:

到此,咱們的 HelloWorldPlugins 插件就寫完了,由於沒有 Compilation 對象,因此並不能作什麼炫酷的功能,旨在理解webpack插件的運行原理便可。其實要寫一個真正的webpack插件也很簡單

一個函數->調用apply方法->訂閱事件鉤子->寫你的程序代碼->調用 webpack 提供的回調

Compiler 和 Compilation

上面留個個疑問,什麼是Compilation,對於 CompilerCompilation 的區別,網上也有不少文章,其實很簡單

  • compiler 對象表示不變的webpack環境,是針對webpack的,包括了options,loaders,plugins等信息,能夠理解爲 webpack 的實例,也就是咱們本身寫的 Compiler
  • compilation 對象則是針對隨時可變的項目文件,即每一次編譯的過程,只要文件有改動,compilation 就會被從新建立。能夠經過 compilation.assets 來獲取全部須要輸出的資源文件,compilation 也能獲取到 compiler 對象。

總結

到此,webpack原理分析就告一段落了,能讀到這裏,我相信你對webpack的原理有了更深層次的理解,文章篇幅較多,若有不足之處,還請多多指正。github源碼地址webpack源碼剖析

相關文章
相關標籤/搜索