webpack打包原理 ? 看完這篇你就懂了 !

前言

[實踐系列] 主要是讓我們通過實踐去加深對一些原理的理解。

[實踐系列]前端路由

[實踐系列]Babel 原理

[實踐系列]實踐這一次,徹底搞懂瀏覽器緩存機制

[實踐系列]你能手寫一個 Promise 嗎?Yes I promise。

有興趣的同學可以關注 [實踐系列] 。 求 star 求 follow~

本文通過實現一個簡單 webpack,來理解它的打包原理,看完不懂直接盤我 !!!
webpack.jpg

什麼是 webpack ?

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

webpack 就像一條生產線,要經過一系列處理流程後才能將源文件轉換成輸出結果。 這條生產線上的每個處理流程的職責都是單一的,多個流程之間有存在依賴關係,只有完成當前處理後才能交給下一個流程去處理。 插件就像是一個插入到生產線中的一個功能,在特定的時機對生產線上的資源做處理。
webpack 通過 Tapable 來組織這條複雜的生產線。 webpack 在運行過程中會廣播事件,插件只需要監聽它所關心的事件,就能加入到這條生產線中,去改變生產線的運作。 webpack 的事件流機制保證了插件的有序性,使得整個系統擴展性很好。 -- 深入淺出 webpack 吳浩麟

webpack.png

webpack 核心概念

Entry

入口起點(entry point)指示 webpack 應該使用哪個模塊,來作爲構建其內部依賴圖的開始。

進入入口起點後,webpack 會找出有哪些模塊和庫是入口起點(直接和間接)依賴的。

每個依賴項隨即被處理,最後輸出到稱之爲 bundles 的文件中。

Output

output 屬性告訴 webpack 在哪裏輸出它所創建的 bundles,以及如何命名這些文件,默認值爲 ./dist。

基本上,整個應用程序結構,都會被編譯到你指定的輸出路徑的文件夾中。

Module

模塊,在 Webpack 裏一切皆模塊,一個模塊對應着一個文件。Webpack 會從配置的 Entry 開始遞歸找出所有依賴的模塊。

Chunk

代碼塊,一個 Chunk 由多個模塊組合而成,用於代碼合併與分割。

Loader

loader 讓 webpack 能夠去處理那些非 JavaScript 文件(webpack 自身只理解 JavaScript)。

loader 可以將所有類型的文件轉換爲 webpack 能夠處理的有效模塊,然後你就可以利用 webpack 的打包能力,對它們進行處理。

本質上,webpack loader 將所有類型的文件,轉換爲應用程序的依賴圖(和最終的 bundle)可以直接引用的模塊。

Plugin

loader 被用於轉換某些類型的模塊,而插件則可以用於執行範圍更廣的任務。

插件的範圍包括,從打包優化和壓縮,一直到重新定義環境中的變量。插件接口功能極其強大,可以用來處理各種各樣的任務。

webpack 構建流程

Webpack 的運行流程是一個串行的過程,從啓動到結束會依次執行以下流程 :

  1. 初始化參數:從配置文件和 Shell 語句中讀取與合併參數,得出最終的參數。
  2. 開始編譯:用上一步得到的參數初始化 Compiler 對象,加載所有配置的插件,執行對象的 run 方法開始執行編譯。
  3. 確定入口:根據配置中的 entry 找出所有的入口文件。
  4. 編譯模塊:從入口文件出發,調用所有配置的 Loader 對模塊進行翻譯,再找出該模塊依賴的模塊,再遞歸本步驟直到所有入口依賴的文件都經過了本步驟的處理。
  5. 完成模塊編譯:在經過第 4 步使用 Loader 翻譯完所有模塊後,得到了每個模塊被翻譯後的最終內容以及它們之間的依賴關係。
  6. 輸出資源:根據入口和模塊之間的依賴關係,組裝成一個個包含多個模塊的 Chunk,再把每個 Chunk 轉換成一個單獨的文件加入到輸出列表,這步是可以修改輸出內容的最後機會。
  7. 輸出完成:在確定好輸出內容後,根據配置確定輸出的路徑和文件名,把文件內容寫入到文件系統。

在以上過程中,Webpack 會在特定的時間點廣播出特定的事件,插件在監聽到感興趣的事件後會執行特定的邏輯,並且插件可以調用 Webpack 提供的 API 改變 Webpack 的運行結果。

實踐加深理解,擼一個簡易 webpack

1. 定義 Compiler 類

class Compiler {
  constructor(options) {
    // webpack 配置
    const { entry, output } = options
    // 入口
    this.entry = entry
    // 出口
    this.output = output
    // 模塊
    this.modules = []
  }
  // 構建啓動
  run() {}
  // 重寫 require函數,輸出bundle
  generate() {}
}

2. 解析入口文件,獲取 AST

我們這裏使用@babel/parser,這是 babel7 的工具,來幫助我們分析內部的語法,包括 es6,返回一個 AST 抽象語法樹。

// webpack.config.js

const path = require('path')
module.exports = {
  entry: './src/index.js',
  output: {
    path: path.resolve(__dirname, './dist'),
    filename: 'main.js'
  }
}
//
const fs = require('fs')
const parser = require('@babel/parser')
const options = require('./webpack.config')

const Parser = {
  getAst: path => {
    // 讀取入口文件
    const content = fs.readFileSync(path, 'utf-8')
    // 將文件內容轉爲AST抽象語法樹
    return parser.parse(content, {
      sourceType: 'module'
    })
  }
}

class Compiler {
  constructor(options) {
    // webpack 配置
    const { entry, output } = options
    // 入口
    this.entry = entry
    // 出口
    this.output = output
    // 模塊
    this.modules = []
  }
  // 構建啓動
  run() {
    const ast = Parser.getAst(this.entry)
  }
  // 重寫 require函數,輸出bundle
  generate() {}
}

new Compiler(options).run()

3. 找出所有依賴模塊

Babel 提供了@babel/traverse(遍歷)方法維護這 AST 樹的整體狀態,我們這裏使用它來幫我們找出依賴模塊。

const fs = require('fs')
const path = require('path')
const options = require('./webpack.config')
const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default

const Parser = {
  getAst: path => {
    // 讀取入口文件
    const content = fs.readFileSync(path, 'utf-8')
    // 將文件內容轉爲AST抽象語法樹
    return parser.parse(content, {
      sourceType: 'module'
    })
  },
  getDependecies: (ast, filename) => {
    const dependecies = {}
    // 遍歷所有的 import 模塊,存入dependecies
    traverse(ast, {
      // 類型爲 ImportDeclaration 的 AST 節點 (即爲import 語句)
      ImportDeclaration({ node }) {
        const dirname = path.dirname(filename)
        // 保存依賴模塊路徑,之後生成依賴關係圖需要用到
        const filepath = './' + path.join(dirname, node.source.value)
        dependecies[node.source.value] = filepath
      }
    })
    return dependecies
  }
}

class Compiler {
  constructor(options) {
    // webpack 配置
    const { entry, output } = options
    // 入口
    this.entry = entry
    // 出口
    this.output = output
    // 模塊
    this.modules = []
  }
  // 構建啓動
  run() {
    const { getAst, getDependecies } = Parser
    const ast = getAst(this.entry)
    const dependecies = getDependecies(ast, this.entry)
  }
  // 重寫 require函數,輸出bundle
  generate() {}
}

new Compiler(options).run()

4. AST 轉換爲 code

將 AST 語法樹轉換爲瀏覽器可執行代碼,我們這裏使用@babel/core 和 @babel/preset-env。

const fs = require('fs')
const path = require('path')
const options = require('./webpack.config')
const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default
const { transformFromAst } = require('@babel/core')

const Parser = {
  getAst: path => {
    // 讀取入口文件
    const content = fs.readFileSync(path, 'utf-8')
    // 將文件內容轉爲AST抽象語法樹
    return parser.parse(content, {
      sourceType: 'module'
    })
  },
  getDependecies: (ast, filename) => {
    const dependecies = {}
    // 遍歷所有的 import 模塊,存入dependecies
    traverse(ast, {
      // 類型爲 ImportDeclaration 的 AST 節點 (即爲import 語句)
      ImportDeclaration({ node }) {
        const dirname = path.dirname(filename)
        // 保存依賴模塊路徑,之後生成依賴關係圖需要用到
        const filepath = './' + path.join(dirname, node.source.value)
        dependecies[node.source.value] = filepath
      }
    })
    return dependecies
  },
  getCode: ast => {
    // AST轉換爲code
    const { code } = transformFromAst(ast, null, {
      presets: ['@babel/preset-env']
    })
    return code
  }
}

class Compiler {
  constructor(options) {
    // webpack 配置
    const { entry, output } = options
    // 入口
    this.entry = entry
    // 出口
    this.output = output
    // 模塊
    this.modules = []
  }
  // 構建啓動
  run() {
    const { getAst, getDependecies, getCode } = Parser
    const ast = getAst(this.entry)
    const dependecies = getDependecies(ast, this.entry)
    const code = getCode(ast)
  }
  // 重寫 require函數,輸出bundle
  generate() {}
}

new Compiler(options).run()

5. 遞歸解析所有依賴項,生成依賴關係圖

const fs = require('fs')
const path = require('path')
const options = require('./webpack.config')
const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default
const { transformFromAst } = require('@babel/core')

const Parser = {
  getAst: path => {
    // 讀取入口文件
    const content = fs.readFileSync(path, 'utf-8')
    // 將文件內容轉爲AST抽象語法樹
    return parser.parse(content, {
      sourceType: 'module'
    })
  },
  getDependecies: (ast, filename) => {
    const dependecies = {}
    // 遍歷所有的 import 模塊,存入dependecies
    traverse(ast, {
      // 類型爲 ImportDeclaration 的 AST 節點 (即爲import 語句)
      ImportDeclaration({ node }) {
        const dirname = path.dirname(filename)
        // 保存依賴模塊路徑,之後生成依賴關係圖需要用到
        const filepath = './' + path.join(dirname, node.source.value)
        dependecies[node.source.value] = filepath
      }
    })
    return dependecies
  },
  getCode: ast => {
    // AST轉換爲code
    const { code } = transformFromAst(ast, null, {
      presets: ['@babel/preset-env']
    })
    return code
  }
}

class Compiler {
  constructor(options) {
    // webpack 配置
    const { entry, output } = options
    // 入口
    this.entry = entry
    // 出口
    this.output = output
    // 模塊
    this.modules = []
  }
  // 構建啓動
  run() {
    // 解析入口文件
    const info = this.build(this.entry)
    this.modules.push(info)
    this.modules.forEach(({ dependecies }) => {
      // 判斷有依賴對象,遞歸解析所有依賴項
      if (dependecies) {
        for (const dependency in dependecies) {
          this.modules.push(this.build(dependecies[dependency]))
        }
      }
    })
    // 生成依賴關係圖
    const dependencyGraph = this.modules.reduce(
      (graph, item) => ({
        ...graph,
        // 使用文件路徑作爲每個模塊的唯一標識符,保存對應模塊的依賴對象和文件內容
        [item.filename]: {
          dependecies: item.dependecies,
          code: item.code
        }
      }),
      {}
    )
  }
  build(filename) {
    const { getAst, getDependecies, getCode } = Parser
    const ast = getAst(filename)
    const dependecies = getDependecies(ast, filename)
    const code = getCode(ast)
    return {
      // 文件路徑,可以作爲每個模塊的唯一標識符
      filename,
      // 依賴對象,保存着依賴模塊路徑
      dependecies,
      // 文件內容
      code
    }
  }
  // 重寫 require函數,輸出bundle
  generate() {}
}

new Compiler(options).run()

6. 重寫 require 函數,輸出 bundle

const fs = require('fs')
const path = require('path')
const options = require('./webpack.config')
const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default
const { transformFromAst } = require('@babel/core')

const Parser = {
  getAst: path => {
    // 讀取入口文件
    const content = fs.readFileSync(path, 'utf-8')
    // 將文件內容轉爲AST抽象語法樹
    return parser.parse(content, {
      sourceType: 'module'
    })
  },
  getDependecies: (ast, filename) => {
    const dependecies = {}
    // 遍歷所有的 import 模塊,存入dependecies
    traverse(ast, {
      // 類型爲 ImportDeclaration 的 AST 節點 (即爲import 語句)
      ImportDeclaration({ node }) {
        const dirname = path.dirname(filename)
        // 保存依賴模塊路徑,之後生成依賴關係圖需要用到
        const filepath = './' + path.join(dirname, node.source.value)
        dependecies[node.source.value] = filepath
      }
    })
    return dependecies
  },
  getCode: ast => {
    // AST轉換爲code
    const { code } = transformFromAst(ast, null, {
      presets: ['@babel/preset-env']
    })
    return code
  }
}

class Compiler {
  constructor(options) {
    // webpack 配置
    const { entry, output } = options
    // 入口
    this.entry = entry
    // 出口
    this.output = output
    // 模塊
    this.modules = []
  }
  // 構建啓動
  run() {
    // 解析入口文件
    const info = this.build(this.entry)
    this.modules.push(info)
    this.modules.forEach(({ dependecies }) => {
      // 判斷有依賴對象,遞歸解析所有依賴項
      if (dependecies) {
        for (const dependency in dependecies) {
          this.modules.push(this.build(dependecies[dependency]))
        }
      }
    })
    // 生成依賴關係圖
    const dependencyGraph = this.modules.reduce(
      (graph, item) => ({
        ...graph,
        // 使用文件路徑作爲每個模塊的唯一標識符,保存對應模塊的依賴對象和文件內容
        [item.filename]: {
          dependecies: item.dependecies,
          code: item.code
        }
      }),
      {}
    )
    this.generate(dependencyGraph)
  }
  build(filename) {
    const { getAst, getDependecies, getCode } = Parser
    const ast = getAst(filename)
    const dependecies = getDependecies(ast, filename)
    const code = getCode(ast)
    return {
      // 文件路徑,可以作爲每個模塊的唯一標識符
      filename,
      // 依賴對象,保存着依賴模塊路徑
      dependecies,
      // 文件內容
      code
    }
  }
  // 重寫 require函數 (瀏覽器不能識別commonjs語法),輸出bundle
  generate(code) {
    // 輸出文件路徑
    const filePath = path.join(this.output.path, this.output.filename)
    // 懵逼了嗎? 沒事,下一節我們捋一捋
    const bundle = `(function(graph){
      function require(module){
        function localRequire(relativePath){
          return require(graph[module].dependecies[relativePath])
        }
        var exports = {};
        (function(require,exports,code){
          eval(code)
        })(localRequire,exports,graph[module].code);
        return exports;
      }
      require('${this.entry}')
    })(${JSON.stringify(code)})`

    // 把文件內容寫入到文件系統
    fs.writeFileSync(filePath, bundle, 'utf-8')
  }
}

new Compiler(options).run()

7. 看完這節,徹底搞懂 bundle 實現

我們通過下面的例子來進行講解,先死亡凝視 30 秒

;(function(graph) {
  function require(moduleId) {
    function localRequire(relativePath) {
      return require(graph[moduleId].dependecies[relativePath])
    }
    var exports = {}
    ;(function(require, exports, code) {
      eval(code)
    })(localRequire, exports, graph[moduleId].code)
    return exports
  }
  require('./src/index.js')
})({
  './src/index.js': {
    dependecies: { './hello.js': './src/hello.js' },
    code: '"use strict";\n\nvar _hello = require("./hello.js");\n\ndocument.write((0, _hello.say)("webpack"));'
  },
  './src/hello.js': {
    dependecies: {},
    code:
      '"use strict";\n\nObject.defineProperty(exports, "__esModule", {\n  value: true\n});\nexports.say = say;\n\nfunction say(name) {\n  return "hello ".concat(name);\n}'
  }
})

step 1 : 從入口文件開始執行

// 定義一個立即執行函數,傳入生成的依賴關係圖
;(function(graph) {
  // 重寫require函數
  function require(moduleId) {
    console.log(moduleId) // ./src/index.js
  }
  // 從入口文件開始執行
  require('./src/index.js')
})({
  './src/index.js': {
    dependecies: { './hello.js': './src/hello.js' },
    code: '"use strict";\n\nvar _hello = require("./hello.js");\n\ndocument.write((0, _hello.say)("webpack"));'
  },
  './src/hello.js': {
    dependecies: {},
    code:
      '"use strict";\n\nObject.defineProperty(exports, "__esModule", {\n  value: true\n});\nexports.say = say;\n\nfunction say(name) {\n  return "hello ".concat(name);\n}'
  }
})

step 2 : 使用 eval 執行代碼

// 定義一個立即執行函數,傳入生成的依賴關係圖
;(function(graph) {
  // 重寫require函數
  function require(moduleId) {
    ;(function(code) {
      console.log(code) // "use strict";\n\nvar _hello = require("./hello.js");\n\ndocument.write((0, _hello.say)("webpack"));
      eval(code) // Uncaught TypeError: Cannot read property 'code' of undefined
    })(graph[moduleId].code)
  }
  // 從入口文件開始執行
  require('./src/index.js')
})({
  './src/index.js': {
    dependecies: { './hello.js': './src/hello.js' },
    code: '"use strict";\n\nvar _hello = require("./hello.js");\n\ndocument.write((0, _hello.say)("webpack"));'
  },
  './src/hello.js': {
    dependecies: {},
    code:
      '"use strict";\n\nObject.defineProperty(exports, "__esModule", {\n  value: true\n});\nexports.say = say;\n\nfunction say(name) {\n  return "hello ".concat(name);\n}'
  }
})

可以看到,我們在執行"./src/index.js"文件代碼的時候報錯了,這是因爲 index.js 裏引用依賴 hello.js,而我們沒有對依賴進行處理,接下來我們對依賴引用進行處理。

step 3 : 依賴對象尋址映射,獲取 exports 對象

// 定義一個立即執行函數,傳入生成的依賴關係圖
;(function(graph) {
  // 重寫require函數
  function require(moduleId) {
    // 找到對應moduleId的依賴對象,調用require函數,eval執行,拿到exports對象
    function localRequire(relativePath) {
      return require(graph[moduleId].dependecies[relativePath]) // {__esModule: true, say: ƒ say(name)}
    }
    // 定義exports對象
    var exports = {}
    ;(function(require, exports, code) {
      // commonjs語法使用module.exports暴露實現,我們傳入的exports對象會捕獲依賴對象(hello.js)暴露的實現(exports.say = say)並寫入
      eval(code)
    })(localRequire, exports, graph[moduleId].code)
    // 暴露exports對象,即暴露依賴對象對應的實現
    return exports
  }
  // 從入口文件開始執行
  require('./src/index.js')
})({
  './src/index.js': {
    dependecies: { './hello.js': './src/hello.js' },
    code: '"use strict";\n\nvar _hello = require("./hello.js");\n\ndocument.write((0, _hello.say)("webpack"));'
  },
  './src/hello.js': {
    dependecies: {},
    code:
      '"use strict";\n\nObject.defineProperty(exports, "__esModule", {\n  value: true\n});\nexports.say = say;\n\nfunction say(name) {\n  return "hello ".concat(name);\n}'
  }
})

這下應該明白了吧 ~ 可以直接複製上面代碼到控制檯輸出哦~

完整代碼地址戳我 👈

總結

Webpack 是一個龐大的 Node.js 應用,如果你閱讀過它的源碼,你會發現實現一個完整的 Webpack 需要編寫非常多的代碼。 但你無需瞭解所有的細節,只需瞭解其整體架構和部分細節即可。

對 Webpack 的使用者來說,它是一個簡單強大的工具; 對 Webpack 的開發者來說,它是一個擴展性的高系統。

Webpack 之所以能成功,在於它把複雜的實現隱藏了起來,給用戶暴露出的只是一個簡單的工具,讓用戶能快速達成目的。 同時整體架構設計合理,擴展性高,開發擴展難度不高,通過社區補足了大量缺失的功能,讓 Webpack 幾乎能勝任任何場景。

參考

webpack 中文文檔

深入淺出 webpack

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章