webpack原理

webpack早就已經在前端領域大放異彩,會使用和優化webpack也已是中、高級工程師必備技能,在此基礎之上再對webpack的原理進行理解和掌握,一定會在將來的開發中事半功倍。如果對於webpack不熟悉能夠查看以前的文章進行學習和了解。javascript

因爲本人能力通常、水平有限,因此會在本篇文章編寫過程當中對一些內容進行又臭又長的贅述,就是爲了能讓一些基礎比較薄弱的同窗閱讀起來能夠更加省心點,接下來即將開始正題了,但願此文章能對你有些許幫助。css

構建項目

  1. 新建一個文件夾 webpack-theoryhtml

    是以後插件的名字,能夠理解爲webpack的別名,能夠直接 wepack-theory進行使用。前端

  2. 新建 bin 目錄,在此目錄下建立webpack-theory.js文件, 將打包工具主程序放入其中vue

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

    #!/usr/bin/env node
    // log的內容修改直接,能夠直接生效
    console.log('當經過npm link連接以後,經過webpack-theory指令能夠直接打出');
    複製代碼
  3. 在package.json中配置 bin 腳本,與scripts平級node

    {
     "bin": "./bin/webpack-theory.js"
    }
    複製代碼
  4. 經過 npm link 將本地的項目webpack-theory 連接到全局包中,連接以後即可以直接在本地使用,供本地測試使用,具體參考 npm linkreact

    1. 成功以後,能夠 cd /usr/local/lib/node_modules 查看全部安裝的包

進入目錄後,能夠看到webpack-theory,webpack-theory就是npm link時,在全局的node_modules中生成一個符號連接,指向模塊(webpack-theory)的本地目錄,當本地的文件(bin/webpack-theory)修改時會自動連接到全局,由於全局的node_modules只是本地的引用webpack

  1. 在本地執行 webpack-theory, 會直接將 bin/webpack-theory.js 的console.log內容輸出
>>> webpack-theory   
>>> 當經過npm link連接以後,經過webpack-theory指令能夠直接打出
複製代碼

分析bundle

在深刻接觸webpack 原理以前,須要知道其打包生成的文件結果是什麼樣,經過打包生成的文件能夠從總體瞭解webpack在對文件處理過程當中作了哪些事情,經過結果反推其原理。git

  • 自行建立一個簡單的weback項目,建立三個js文件,分別是index.js,parent.js 和 child.js,並將其經過webpack進行打包

    • index.js 內容
    const parent = require('./parent.js')
    
    console.log(parent)
    複製代碼
    • parent.js 內容
    const child = require('./child.js')
    
    module.exports = {
      msg: '我是parent的信息',
      child: child.msg
    }
    複製代碼
    • child.js 內容
    module.exports = {
      msg: '我是child的信息'
    }
    複製代碼
  • 經過 npx webpack 進行打包,將打包文件進行簡單的刪除和整理以後

(function (modules) { // 將全部的模塊組成一個modules對象傳遞進來, 鍵就是模塊的路徑,值就是模塊內部的代碼
  // 模塊緩存對象, 已經解析過的路徑都會放進來,能夠判斷當前須要解析的模塊是否已經解析過
  var installedModules = {};

  // 定義一個 webpack 本身的的 require polyfill
  function __webpack_require__(moduleId) {

    // 檢測 moduleId 是否已經存在緩存中了,如果已經存在則不須要在進行依賴解析
    if (installedModules[moduleId]) {
      return installedModules[moduleId].exports;
    }

    // 建立一個新的 module, 並將其push至緩存中,方便在後續遞歸遍歷解析依賴時,檢測是否已經解析過
    var module = installedModules[moduleId] = {
      i: moduleId, // moduleId 是自執行函數的參數 modules 對象的鍵,根本是模塊的路徑
      exports: {}
    };

    // 執行 modules[moduleId] 函數
    modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);

    // 將 exports 返回
    return module.exports;
  }

  // 將 webpack.config.js 配置中的 entry 做爲 moduleId 進行傳遞
  return __webpack_require__("./src/index.js");
})
/*** 將項目中的幾個模塊做爲自執行函數的參數傳遞 ***/
({
  // webpack.config.js 配置中 entry 的值,會將其做爲遞歸解析依賴的入口
  "./src/index.js": (function (module, exports, __webpack_require__) {
    eval("const parent = __webpack_require__(/*! ./parent.js */ \"./src/parent.js\")\n\nconsole.log(parent)\n\n//# sourceURL=webpack:///./src/index.js?");
  }),
  "./src/parent.js": (function (module, exports, __webpack_require__) {
    eval("const child = __webpack_require__(/*! ./child.js */ \"./src/child.js\")\n\nmodule.exports = {\n msg: '我是parent的信息',\n child: child.msg\n}\n\n\n\n//# sourceURL=webpack:///./src/parent.js?");
  }),
  "./src/child.js": (function (module, exports) {
    eval("\nmodule.exports = {\n msg: '我是child的信息'\n}\n\n//# sourceURL=webpack:///./src/child.js?");
  })
});
複製代碼

根據生成的bundle.js能夠梳理webpack的總體打包思路,就是利用一個自執行函數建立一個閉包,在這個獨立的做用域中,將模塊的路徑做爲modules的鍵、模塊的內容放在一個函數中做爲值做爲自執行函數的形參傳遞進來,經過自定義的函數 __webpack_require__進行遞歸解析。

簡單分析一下bundle的總體執行過程

  1. 第一步: 自執行函數第一次執行時,會直接運行內部的__webpack_require__函數,並將入口文件的路徑./src/index.js做爲形參moduleId傳遞
  2. 第二步: 在函數__webpack_require__執行過程當中
    1. 會首先判斷當前moduleId是否已經存在緩存installedModules中,如果存在則直接返回,不須要再繼續解析其依賴。如果不存在,則會構造一個對象並將其同時存到installedModules中和module中。第一次執行時installedModules爲空對象,moduleId爲./src/index.js
    2. 執行modules[moduleId]函數,即執行modules['./src/index.js'],會經過call改變其做用域並傳遞module, module.exports, __webpack_require__三個形參,執行的內容就是入口文件模塊./src/index.js中的js代碼。
      1. call傳遞的做用域置爲module.exports,因爲module.exports此時爲空對象,則index.js中的做用域就是指向它,這也是典型的使用閉包來解決做用域的問題。
      2. module, module.exports的做用就是用於模塊內拋出對象使用的,做用是一個的,能夠參考require.js進行這塊的理解
      3. __webpack_require__的做用就很巧妙了,此時入口index.js中使用的require('./parent.js')已經被替換成__webpack_require__("./src/parent.js\"),執行modules[moduleId]函數時便會在此調用__webpack_require__函數進行遞歸調用,會再次回到第二步,直到child.js執行完畢,整個bundle纔算執行結束。

分析完bundle以後,會發現對於webpack的打包結果,除了形參modules會跟着代碼的業務邏輯修改而變化以外,自執行函數中的代碼始終是固定不變的,所以想要編寫一個屬於本身的webpack時,重點關注和須要解決的就是modules這個對象是如何生成的。

建立bundle

分析完webpack打包完成以後的bundle文件,以結果爲導向反推實現過程便會簡單許多,如果讓咱們本身動手實現一個簡單版的webpack,便會有了些思路。

首先須要一個簡單的wbepack配置

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

簡單版本的webpack實現思路

  1. 獲取webpack配置文件
  2. 封裝一個用於解析配置並將其簡單打包的方法
    1. 利用抽象語法書解析模塊內容
    2. 遞歸解析模塊依賴
    3. 使用模版引擎輸出結果

有了思路,接下來就是循序漸進的實現

  • 獲取webpack配置文件,而須要作的事情就是將這個配置文件進行解析,根據配置文件進行打包生成bundle。首先就是讀取須要打包項目的配置文件
const config = require(path.resolve('webpack.config.js'))
複製代碼
  • 獲取配置文件以後,即是如何解析並實現webpack的功能,這些功能所有封裝在Compiler類中,用於解析配置文件的配置,並經過start進行啓動解析
const Compiler = require('../lib/Compiler')
new Compiler(config).start()
複製代碼
  • 重點就是如何實現這個方法,定義一個Compiler類,提供一個start方法開始webpack打包,經過depAnalyse即可以獲取到入口文件index的內容
const path = require('path')
const fs = require('fs')

class Compiler {
  constructor(config){
    this.config = config
    const { entry } = config // 配置文件
    this.entry = entry // 入口文件
    this.root = process.cwd() // 輸入 webpack-theory 的路徑
    this.modules = {} // 初始化一個控對象,存放全部的模塊
  }

  /** * 開始打包 * 打包最主要的就是依賴的分析 */
  start(){
    this.depAnalyse(path.resolve(this.root, this.entry))
  }

  /** * 依賴分析 * 須要根據入口entry進行開始分析 */
  depAnalyse(modulePath){
    // 獲取 index.js 的內容
    let source = this.getSource(modulePath)
  }

  // 讀取文件
  getSource(path){
    return fs.readFileSync(path, 'utf-8')
  }
}

module.exports = Compiler
複製代碼
  • 獲取到index.js的文件內容以後,並不能直接使用,須要經過將其解析成抽象語法樹進行處理,須要使用一個插件@babel/parser將模塊代碼解析成AST,而後插件@babel/traverse配合着使用,將AST的節點進行替換,替換完成以後,使用插件@babel/generator將AST轉換成模塊的原有代碼,改變的只是將require變成__webpack_require__,須要注意的是須要將路徑處理一下,由於此時的路徑是相對於src下面的。處理完index以後須要遞歸調用處理所有的模塊,並聲稱bundle中自執行函數的參數modules

此時index的模塊代碼通過處理以後,變成了須要的代碼

const parent = __webpack_require__("./src/parent.js");
console.log(parent);
複製代碼

在函數depAnalyse中添加以下處理

// 獲取 index.js 的內容
let source = this.getSource(modulePath) 
// -------

// 準備一個依賴數組,用於存儲當前模塊
let dependenceArr = []
// 將js代碼 解析成AST
let ast = parser.parse(source)

// 將AST中的 require 替換爲 __webpack_require__
traverse(ast, {
  // p 是抽象語法樹的節點
  CallExpression(p) {
    if (p.node.callee.name === 'require') {
      // 將代碼中的 require 替換爲 __webpack_require__
      p.node.callee.name = '__webpack_require__'
      const oldValue = p.node.arguments[0].value
      // 修改路徑,避免windows出現反斜槓 \
      p.node.arguments[0].value = ('./' + path.join('src', oldValue)).replace(/\\+/g, '/')

      // 每找到一個require調用,就將其中的路徑修改完畢後加入到依賴數組中
      dependenceArr.push(p.node.arguments[0].value)
    }
  }
})

// 構建modules對象
const sourceCode = generator(ast).code
const modulePathRelative = './' + (path.relative(this.root, modulePath)).replace(/\\+/g, '/')
this.modules[modulePathRelative] = sourceCode

// 遞歸調用加載全部依賴
dependenceArr.forEach(dep => this.depAnalyse(path.resolve(this.root, dep)))
複製代碼

至此已經完成了modules的處理,接下來須要處理的就是怎麼生成bundle.js,分析bundle時已經指出咱們須要關注的地方只有modules的拼接,至於自執行函數中的內容都是基本都是固定的,不須要額外的處理

  • 如何使用模版引擎打包模塊的代碼呢?
  1. 使用模版引擎ejs建立模版,模版的內容就是webpack打包生成的內容,只須要根據Compilermodules進行遍歷便可,還須要關注的是return __webpack_require__(__webpack_require__.s = "<%-entry%>"),這裏傳入的是配置文件的入口,也是自執行函數第一次執行時的參數

    1. 建立ejs的模板文件output.ejs,須要關注的只有兩個地方,其它地方使用默認的代碼
    // 第一次執行的參數就是配置的entry
    return __webpack_require__(__webpack_require__.s = "<%-entry%>");
    
    // 拼接函數須要的形參 modules
    {
      <% for (let k in modules) {%>
      "<%-k%>": (function (module, exports, __webpack_require__) {
    
        eval(`<%-modules[k]%>`);
      }),
      <%}%>
    }
    複製代碼
  2. Compiler增長一個emitFile方法,用於根據模板生成打包的bundle文件,在start函數中的depAnalyse以後進行調用

    /** * 根據寫好的模板 建立文件 */
    emitFile(){
      // 已經建立好的 ejs 模版
      const template = this.getSource(path.join(__dirname, '../template/output.ejs'))
      // 使用 ejs 進行編譯
      const result = ejs.render(template, {
        entry: this.entry,
        modules: this.modules
      })
    
      // 獲取輸出路徑和文件名
      const {
        path: filePath,
        filename
      } = this.output
      const outputPath = path.join(filePath, filename)
    
      // 打包生成bundle 並放在指定的目錄下
      fs.writeFile(outputPath, result, (err) => {
        console.log(err ? err : '打包生成bundle完成');
      })
    }
    複製代碼

到目前爲止,已經能夠進行簡單的模塊打包,能夠將index.js、parent.js和child.js進行簡單的打包,這裏僅僅是支持最簡單的webpack用法打包

loader

loader是webpack的重要核心功能之一,也是使用頻率很是高的,主要功能就是將代碼按照預期的結果進行加工處理,生成最終的代碼後輸出,所以掌握loade的基本機制是頗有必要的。loader的使用也是很是簡單的,其基本配置和用法這裏再也不贅述,接下來一塊兒看看如何在本身的webpack-theory中添加解析loader和如何編寫一個本身的loader。

自制loader

在爲webpack-theory添加處理loader的能力以前,先看看如何在webpack中實現一個本身的loader

  • webpack中loader,主要步驟以下

    • 讀取webpack.config.js配置文件的module.rules配置項,進行倒序迭代(rules的每項匹配規則按倒序匹配)
    • 根據正則匹配到對應的文件類型,同時再批量導入loader函數
    • 默認是倒序迭代調用全部的loader函數(loader從右到左,從下到上),也能夠本身來控制這個順序
    • 最後返回處理後的代碼
  • 當想要在webpack中增長處理cass文件能力的時候,會進行loader的配置

{
  test:/\.scss$/,
  use:['style-loader', 'css-loader', 'sass-loader']
}
複製代碼

sass-loader其實就是一個函數,根據test的匹配規則,將以.scss結束的文件內容讀取出來,而後將匹配到的文件內容做爲參數傳遞給一個函數,這個函數將sass文件的內容按照規則進行加工處理成瀏覽器可以識別的css並輸出,因此loader的本質就是一個函數,接受一個參數,這個參數就是匹配到的文件裏面的代碼。同理,css-loaderstyle-loader也是這樣的處理流程,只是內部作的事情不一樣。

function handlerScss(sourceCode){
  // 這裏就是將scss文件的內容,按照規則進行加工、處理,結果就是瀏覽器可以識別解析的css,並將其返回
  return newSourceCode
}
複製代碼
  • 接下來實現一個本身的簡單loader,將以前的parent.jschild.js中的信息使用loader處理爲msg
// 將js文件中的 信息 換成 msg
module.exports = function (source) {
  return source.replace(/信息/g, 'msg')
}
複製代碼

在webpack中配置loader

{
  test:/\.js/,
  use:['./loader/handlerLoader1.js']
}
複製代碼

使用npx webpack打包以後,能夠看到打包的代碼中已經將原有代碼中的信息更換爲msg

  • 如果想講handlerLoader1的loader中替換的內容經過配置自定義處理呢?就像是url-loader那樣傳遞一個配置選項options,而後在loader中進行接受並處理。能夠經過loader-utilsgetOptions提取loader中的options進行處理,老版本是經過thus.query來進行處理

修改loader文件handlerLoader1

const loaderUtils = require('loader-utils')
// 將js文件中的 信息 換成 經過options傳遞的name
module.exports = function (source) {
  const optionsName = loaderUtils.getOptions(this).name || ''
  return source.replace(/信息/g, optionsName)
}
複製代碼

修改webpack的loader

{
  test:/\.js/,
    use:{
      loader: './loader/loader1.js',
        options:{
          name:'新的信息'
        }
    }
}
複製代碼

使用npx webpack打包以後,即可以經過options配置進行替換

  • 如果handlerLoader1處理完的東西還須要交給下一個loader進行處理以後,這樣就會牽扯到多個同級loader的狀況,將handlerLoader1拷貝兩份,分別命名爲handlerLoader11handlerLoader12,內容可保持原有的,只是在原有的函數中分別打印其對應的loader的文件名稱,由於只是爲了看看loader的加載。

handlerLoader1的內容爲

// 將js文件中的 信息 換成 msg
module.exports = function (source) {
  console.log('我是 handlerLoader1'); // 其他兩loader 的log分別爲 handlerLoader2 handlerLoader3
  return source.replace(/信息/g, 'msg')
}
複製代碼

webpack配置loader

{
  test:/\.js/,
  use:[
    './loader/handlerLoader1.js',
    './loader/handlerLoader2.js',
    './loader/handlerLoader3.js'
  ]
}
複製代碼

執行webpack打包,輸出結果,能夠得出loader的默認順序是由右到左

>>> 我是 handlerLoader3
>>> 我是 handlerLoader2
>>> 我是 handlerLoader1
複製代碼
  • 若修改webpack的loader爲
{
  test:/\.js/,
  use:['./loader/loader1.js']
},{
  test:/\.js/,
  use:['./loader/loader2.js']
},{
  test:/\.js/,
  use:['./loader/loader3.js']
},
複製代碼

執行webpack打包,輸出結果,能夠得出loader的默認順序是由下到上的

>>> 我是 handlerLoader3
>>> 我是 handlerLoader2
>>> 我是 handlerLoader1
複製代碼

添加loader功能

經過自制一個loader以後,能夠總結獲得webpack支持loader的功能,主要就是4步

  1. 讀取配置文件webpack.config.jsmodule.rulesloader配置項,進行倒序迭代
  2. 根據正則匹配到對應的文件類型,同時再批量導入loader函數
  3. 倒序迭代調用全部loader函數
  4. 返回處理後的代碼

webpack-theory中增長處理loader的能力,無非就是在加載每一個模塊的時候,根據配置的rules的正則進行匹配須要的資源,知足條件以後就會加載並使用對應的loader進行處理並迭代調用

須要注意的是,在何時去執行loader呢,在每次獲取模塊依賴的時候,都須要進行loadertest匹配,如果匹配到就加載對應的loader進行處理。例如本文的案例代碼存在三個js文件,首先會加載index.js,在加載解析index的依賴以前就須要對其進行倒序便利所有的loader,如果匹配到對應的loader就會加載對應的loaderindex.js的內容進行處理,由於index引入了parent.js,接下來便會在遞歸調用depAnalyse方法解析parnet以前進行判斷和處理,child.js同理。

depAnalyse方法中每次解析以來以前添加以下代碼:

// 內部定義一個處理loader的函數
const _handleLoader = (usePath, _this) => {
  const loaderPath = path.join(this.root, usePath)
  const loader = require(loaderPath)
  source = loader.call(_this, source)
}

// 讀取 rules 規則, 進行倒序遍歷
const rules = this.rules
for (let i = rules.length - 1; i >= 0; i--) {
  const {
    test,
    use
  } = rules[i]

  // 匹配 modulePath 是否符合規則,如果符合規則就須要倒序遍歷獲取全部的loader
  // 獲取每一條規則,和當前的 modulePath 進行匹配
  if (test.test(modulePath)) {
    // use 多是 數組、對象、字符串
    console.log(use);
    
    if (Array.isArray(use)) {
      // array
      for (let j = use.length - 1; j >= 0; j--) {
        // const loaderPath = path.join(this.root, use[j])
        // const loader = require(loaderPath)
        // source = loader(source)
        _handleLoader(use[j])
      }
    } else if (typeof use === 'string') {
      // string
      _handleLoader(use)
    } else if (use instanceof Object) {
      // object
      _handleLoader(use.loader, {
        query: use.options
      })
    }
  }
}
複製代碼

loader基礎的相關編寫到此爲止,可是仍是須要多加練習的思考,這裏僅僅是演示了最簡單的,你們能夠參考官方文檔進行loader的enforce異步loader等知識點的深刻學習和查看babelsass-loader等社區優秀loader進行深刻的理解和練習。

plugin

插件是 webpack 生態系統的重要組成部分,爲社區用戶提供了一種強大方式來直接觸及 webpack 的編譯過程(compilation process)。插件可以 鉤入(hook) 到在每一個編譯(compilation)中觸發的全部關鍵事件。在編譯的每一步,插件都具有徹底訪問 compiler 對象的能力,若是狀況合適,還能夠訪問當前 compilation 對象。

自定義插件本質就是在webpack的編譯過程的提供的生命週期鉤子中,進行編碼開發實現一些功能,在適當的時間節點作該作的事情,例如clean-webpack-plugin插件,就是在編譯以前執行插件,將打包目錄清空。

自制plugin

  • 在實現自制插件以前,先了解一下webpack插件組成

    • 一個JavaScript命名函數

    • 在插件函數的prototype上定義一個apply方法

    • 指定一個綁定到webpack自身的事件鉤子

    • 處理webpack內部實例的特定數據

    • 功能完成後調用webpack提供的回調

  • webpack的生命週期鉤子 生命週期鉤子

Compiler 模塊是 webpack 的支柱引擎,它經過 CLINode API 傳遞的全部選項,建立出一個 compilation 實例。它擴展(extend)自 Tapable 類,以便註冊和調用插件。大多數面向用戶的插件首,會先在 Compiler上註冊。

hello word

根據官方文檔實現一個hello word插件,能夠簡單的瞭解到plugin

// 1. 一個JavaScript命名函數
// 2. 在插件函數的 prototype 上定義一個apply方法
class HelloWordPlugin {
  // 3. apply 中有一個 compiler 形參
  apply(compiler){
    console.log('插件執行了');
    // 4. 經過compiler對象能夠註冊對應的事件,所有的鉤子均可以使用
    // 註冊一個編譯完成的鉤子, 通常須要將插件名做爲事件名便可
    compiler.hooks.done.tap('HelloWordPlugin', (stats) => {
      console.log('整個webpack打包結束了');
    })

    compiler.hooks.emit.tap('HelloWordPlugin', (compilation) => {
      console.log('觸發emit方法');
    })
  }
}

module.exports = HelloWordPlugin
複製代碼

webpack.config.js引入並使用

const HelloWordPlugin = require('./plugins/HelloWordPlugin')

{
  // ... 
  plugins:[
    new HelloWordPlugin()
  ]
}
複製代碼

npx webpack打包,能夠查看插件的觸發

>>> 插件執行了
>>> 觸發emit方法
>>> 整個webpack打包結束了
複製代碼

HtmlWebpackPlugin

模仿實現HtmlWebpackPlugin插件的功能

html-webpack-plugin 能夠將制定的html模板複製一份輸出到dist目錄下,自動引入bundle.js

  • 實現步驟
    • 編寫一個自定義插件,註冊 afterEmit 鉤子
    • 根據建立對象時傳入的 template 屬性來讀取 html 模板
    • 使用工具分析HTML,推薦使用 cheerio,能夠直接使用jQuery API
    • 循環遍歷webpack打包的資源文件列表,若是有多個bundle就都打包進去
    • 輸出新生成的HTML字符串到dist目錄中
const path = require('path')
const fs = require('fs')
const cheerio = require('cheerio')

class HTMLPlugin {
  constructor(options){
    // 插件的參數,filename、template等
    this.options = options
  }

  apply(compiler){
    // 1. 註冊 afterEmit 鉤子
    // 若是使用done鉤子,則須要使用stats.compilation.assets獲取,並且會比 afterEmit 晚一些
    compiler.hooks.afterEmit.tap('HTMLPlugin', (compilation) => {
      // 2. 根據模板讀取html文件內容
      const result = fs.readFileSync(this.options.template, 'utf-8')
      
      // 3. 使用 cheerio 來分析 HTML
      let $ = cheerio.load(result)

      // 4. 建立 script 標籤後插入HTML中
      Object.keys(compilation.assets).forEach(item => {
        $(`<script src="/${item}"></script>`).appendTo('body')
      })

      // 5. 轉換成新的HTML並寫入到 dist 目錄中
      fs.writeFileSync(path.join(process.cwd(), 'dist', this.options.filename), $.html())
    })
  }
}

module.exports = HTMLPlugin
複製代碼
  • 注意 Compiler 和 Compilattion 的區別
    • compile: compile對象表示不變的webpack環境,是針對webpack的
    • compilation: compilation對象針對的是隨時可變的項目文件,只要文件有改動,compilation就會被從新建立

添加plugin功能

webpack-theory添加plugin功能,只需在Compiler構造時,建立對應的鉤子便可,webpack-theory只是負責定義鉤子,並在適當的時間節點去觸發,至於鉤子的事件註冊都是各個plugin本身內部去實現。

// tapable 的構造函數內部定義的鉤子
this.hooks = {
  afterPlugins: new SyncHook(),
  beforeRun: new SyncHook(),
  run: new SyncHook(),
  make: new SyncHook(),
  afterCompiler: new SyncHook(),
  shouldEmit: new SyncHook(),
  emit: new SyncHook(),
  afterEmit: new SyncHook(['compilation']),
  done: new SyncHook(),
}

// 觸發plugins中全部插件的apply方法, 並傳入Compiler對象
if(Array.isArray(this.config.plugins)){
  this.config.plugins.forEach(plugin => {
    plugin.apply(this)
  });
}
複製代碼

在合適的時機調用對應鉤子的call方法便可,如須要傳入參數,能夠在對應的鉤子中定義好須要傳入的參數,call時直接傳入

start中調用定義的鉤子

start() {
  this.hooks.compiler.call() // 開始編譯
  this.depAnalyse(path.resolve(this.root, this.entry))
  this.hooks.afterCompiler.call() //編譯完成了
  this.hooks.emit.call() // 開始發射文件
  this.emitFile()
  this.hooks.done.call() // 完成
}
複製代碼

補充

AST

就是將一行代碼解析成對象的格式,可使用在線工具生成ast語法樹 astexplorer 進行查看

  1. 安裝@babel/parser插件
npm i -S @babel/parser
複製代碼
  1. 使用
const parser = require('@babel/parser')
//source是須要生成ast語法樹的代碼片斷
const ast = parser.parse(source)
複製代碼
  1. 生成效果

源碼

const news = require('./news')
console.log(news.content)
複製代碼

生成的ast語法樹

Node {
  type: 'File',
  start: 0,
  end: 57,
  loc:
   SourceLocation {
     start: Position { line: 1, column: 0 },
     end: Position { line: 3, column: 25 } },
  program:
   Node {
     type: 'Program',
     start: 0,
     end: 57,
     loc: SourceLocation { start: [Position], end: [Position] },
     sourceType: 'script',
     interpreter: null,
     body: [ [Node], [Node] ],
     directives: [] },
  comments: [] }
複製代碼

tabable

在webpack內部實現事件流機制的核心就在於tapable,有了它就能夠經過事件流的形式,將各個插件串聯起來,tapable相似於node中的events庫,核心原理就是一個訂閱發佈模式

  • 基本用法

    • 定義鉤子
    • 使用者註冊事件
    • 在合適的階段調用鉤子,觸發事件
    const { SyncHook } = require('tapable')
    /** * 學習前端 * 學習過程 1.準備 2.學html 3.學css 4.學js 5.學框架 * 學習的每一個過程就相似於生命週期 */
    class Frontend{
    
      constructor(){
        // 1. 定義生命週期鉤子
        this.hooks = {
          beforeStudy: new SyncHook(),
          afterHtml: new SyncHook(),
          afterCss: new SyncHook(),
          afterJs: new SyncHook(),
          // 須要傳遞的參數 須要在 new SyncHook() 的時候定義好
          afterFrame: new SyncHook(['name']),
        }
      }
    
      study(){
        // 3. 在合適的時候 調用
        console.log('準備');
        this.hooks.beforeStudy.call()
        console.log('準備學html');
        this.hooks.afterHtml.call()
        console.log('準備學css');
        this.hooks.afterCss.call()
        console.log('準備學js');
        this.hooks.afterJs.call()
        console.log('準備學框架');
        this.hooks.afterFrame.call('vue、react')
      }
    }
    
    const f = new Frontend()
    
    // 2. 註冊事件
    f.hooks.afterHtml.tap('afterHtml', () => {
      console.log('學完html,完成了一部分前端知識');
    })
    
    f.hooks.afterJs.tap('afterJs', () => {
      console.log('學完js,完成了一部分前端知識');
    })
    
    f.hooks.afterFrame.tap('afterFrame', (name) => {
      console.log(`學完了${name}框架,天下無敵....`);
    })
    
    
    f.study()
    複製代碼

    演示代碼

    原理代碼 配合原理使用的代碼

相關文章
相關標籤/搜索