不聊webpack配置,來說說它的原理

saku發表於2018-07-01

最近在前端論壇閒逛,看到了一些講parcel、webpack的文章,就突然很好奇,每天都在用的打包工具,他們打包的原理究竟是什麼。只有知道了這一點,才可以在眾多的打包工具裡,找到最適合的那個它。在瞭解打包原理之前,先花一些篇章說明了一下為什麼要使用打包工具。

0.模組系統

前端產品的交付是基於瀏覽器,這些資源是通過增量載入的方式執行到瀏覽器端,如何在開發環境組織好這些碎片化的程式碼和資源,並且保證他們在瀏覽器端快速、優雅的載入和更新,就需要一個模組化系統。這個理想中的模組化系統是前端工程師多年來一直探索的難題。

模組系統主要解決模組的定義、依賴和匯出。 原始的<script>標籤載入方式有一些常見的弊端:例如全域性作用域下容易造成變數衝突;檔案只能按照<script>的書寫順序進行載入;開發人員必須主觀解決模組和程式碼庫的依賴關係等。

因此衍生出很多模組化方案:

1.CommonJs:優點:伺服器端模組便於重用。缺點:同步的模組載入方式不適合在瀏覽器環境中,同步意味著阻塞載入,瀏覽器資源是非同步載入的。

2.AMD:依賴前置。優點:適合在瀏覽器環境非同步載入;缺點:閱讀和書寫比較困難。

3.CMD:依賴就近,延遲執行。優點:很容易在node中執行;缺點:依賴spm打包,模組的載入邏輯偏重。

4.ES6模組::儘量的靜態化,使得編譯時就能確定模組的依賴關係,以及輸入輸出的變數。CommonJS 和 AMD 模組,都只能在執行時確定這些東西。優點:容易進行靜態分析;缺點:原生瀏覽器未實現該標準。

說到模組的載入和傳輸,若是每個檔案都單獨請求,會導致請求次數過多,導致啟動速度過慢。若是全部打包在一塊只請求一次,會導致流量浪費,初始化過程慢。因此最佳方案是分塊傳輸,按需進行懶載入,在實際用到某些模組的時候再增量更新。要實現模組的按需載入,就需要一個對整個程式碼庫中的模組進行靜態分析、編譯打包的過程。Webpack 就是在這樣的需求中應運而生。

注:要注意一個概念,一切皆模組。樣式、圖片、字型、HTML 模板等等眾多的資源,都可以視作模組。

1.模組打包器:webpack

Webpack 是一個模組打包器。它將根據模組的依賴關係進行靜態分析,然後將這些模組按照指定的規則生成對應的靜態資源。 那麼問題來了,webpack真的能做到上述提到的靜態分析、編譯打包麼?我們首先來看一下webpack能做什麼:

1.程式碼拆分 Webpack 有兩種組織模組依賴的方式,同步和非同步。非同步依賴作為分割點,形成一個新的塊。在優化了依賴樹後,每一個非同步區塊都作為一個檔案被打包。

2.Loader Webpack 本身只能處理原生的 JavaScript 模組,但是 loader 轉換器可以將各種型別的資源轉換成 JavaScript 模組。這樣,任何資源都可以成為 Webpack 可以處理的模組。

3.智慧解析 Webpack 有一個智慧解析器,幾乎可以處理任何第三方庫,無論它們的模組形式是 CommonJS、 AMD 還是普通的 JS 檔案。

4.外掛系統 Webpack 還有一個功能豐富的外掛系統。大多數內容功能都是基於這個外掛系統執行的,還可以開發和使用開源的 Webpack 外掛,來滿足各式各樣的需求。

5.快速執行 Webpack 使用非同步 I/O 和多級快取提高執行效率,這使得 Webpack 能夠以令人難以置信的速度快速增量編譯。

以上是webpack五個主要特點,但是看完還是覺得有些霧裡看山,webpack到底是如何把一些分散的小模組,整合成大模組?又是如何處理好各模組的依賴關係?下面就以parcel核心開發者@ronami的開源專案minipack為例,說明以上問題。

2.打包工具核心原理——以minipack為例

打包工具就是負責把一些分散的小模組,按照一定的規則整合成一個大模組的工具。與此同時,打包工具也會處理好模組之間的依賴關係,將專案執行在平臺上。minipack專案最想說明的問題,也是打包工具最核心的部分,就是如何處理好模組間的依賴關係

首先,打包工具會從一個入口檔案開始,分析裡面的依賴,並進一步地分析依賴中的依賴。 我們新建三個檔案,並建立依賴:

/* name.js */
export const name = 'World'

/* message.js */
import { name } from './name.js'
export default `Hello ${name}!`

/* entry.js */
import message from './message.js'
console.log(message)
複製程式碼

首先引入必要的工具

/* minipack.js */
const fs = require('fs')
const path = require('path')
const babylon = require('babylon')
const traverse = require('babel-traverse').default
const { transformFromAst } = require('babel-core')
複製程式碼

接著我們將建立一個函式,引數是檔案的路徑,作用是讀取檔案內容並提取它的依賴關係。

function createAsset(filename) {
  // 以字串形式讀取檔案的內容. 
  const content = fs.readFileSync(filename, 'utf-8');
// 現在我們試圖找出這個檔案依賴於哪個檔案。雖然我們可以通過檢視其內容來獲取import字串. 然而,這是一個非常笨重的方法,我們將使用JavaScript解析器來代替。
  
// JavaScript解析器是可以讀取和理解JavaScript程式碼的工具,它們生成一個更抽象的模型,稱為`ast (抽象語法樹)(https://astexplorer.net)`。
  const ast = babylon.parse(content, {
    sourceType: 'module',
  });

// 定義陣列,這個陣列將儲存這個模組依賴的模組的相對路徑.
  const dependencies = [];

//  我們遍歷`ast`來試著理解這個模組依賴哪些模組,要做到這一點,我們需要檢查`ast`中的每個 `import` 宣告。
// `Ecmascript`模組相當簡單,因為它們是靜態的. 這意味著你不能`import`一個變數,或者有條件地`import`另一個模組。每次我們看到`import`宣告時,我們都可以將其數值視為`依賴性`。
  traverse(ast, {
    ImportDeclaration: ({node}) => 
        // 我們將依賴關係存入陣列
        dependencies.push(node.source.value);
    },
  });
  

//   我們還通過遞增簡單計數器為此模組分配唯一識別符號. 
  const id = ID++;

//  我們使用`Ecmascript`模組和其他JavaScript,可能不支援所有瀏覽器。
//  為了確保我們的程式在所有瀏覽器中執行,
//  我們將使用[babel](https://babeljs.io)來進行轉換。
//  我們可以用`babel-preset-env``將我們的程式碼轉換為瀏覽器可以執行的東西. 
  const {code} = transformFromAst(ast, null, {
    presets: ['env'],
  });

  // 返回有關此模組的所有資訊.
  return {
    id,
    filename,
    dependencies,
    code,
  };
}

複製程式碼

現在我們可以提取單個模組的依賴關係,那麼,我們將提取它的每一個依賴關係的依賴關係,並迴圈下去,直到我們瞭解應用程式中的每個模組以及他們是如何相互依賴的。

function createGraph(entry) {
  // 首先解析整個檔案.
  const mainAsset = createAsset(entry);

//   我們將使用queue來解析每個asset的依賴關係. 
//   我們正在定義一個只有entry asset的陣列.
  const queue = [mainAsset];

// 我們使用一個`for ... of`迴圈遍歷 佇列. 
// 最初 這個佇列 只有一個asset,但是當我們迭代它時,我們會將額外的assert推入到queue中. 
// 這個迴圈將在queue為空時終止. 
  for (const asset of queue) {
    // 我們的每一個asset都有它所依賴模組的相對路徑列表. 
    // 我們將重複它們,用我們的`createAsset() `函式解析它們,並跟蹤此模組在此物件中的依賴關係.
    asset.mapping = {};

    // 這是這個模組所在的目錄. 
    const dirname = path.dirname(asset.filename);

    // 我們遍歷其相關路徑的列表
    asset.dependencies.forEach(relativePath => {
    // 我們可以通過將相對路徑與父資源目錄的路徑連線,將相對路徑轉變為絕對路徑.
      const absolutePath = path.join(dirname, relativePath);

    // 解析asset,讀取其內容並提取其依賴關係.
      const child = createAsset(absolutePath);

    //   瞭解`asset`依賴取決於`child`這一點對我們來說很重要. 
    //   通過給`asset.mapping`物件增加一個新的屬性(值為child.id)來表達這種一一對應的關係.
      asset.mapping[relativePath] = child.id;

      // 最後,我們將`child`這個資產推入佇列,這樣它的依賴關係也將被迭代和解析.
      queue.push(child);
    });
  }

  return queue;
}
複製程式碼

接下來我們定義一個函式,傳入上一步的graph,返回一個可以在瀏覽器上執行的包。

function bundle(graph) {
  let modules = '';

// 在我們到達該函式的主體之前,我們將構建一個作為該函式的引數的物件. 
// 請注意,我們構建的這個字串被兩個花括號 ({}) 包裹,因此對於每個模組,
// 我們新增一個這種格式的字串: `key: value,`.
  graph.forEach(mod => {
     //  圖表中的每個模組在這個物件中都有一個entry. 我們用模組的id`作為`key`,用陣列作為`value`
    // 第一個引數是用函式包裝的每個模組的程式碼. 這是因為模組應該被限定範圍: 在一個模組中定義變數不會影響其他模組或全域性範圍. 
    
    // 對於第二個引數,我們用`stringify`解析模組及其依賴之間的關係(也就是上文的asset.mapping). 解析後的物件看起來像這樣: `{'./relative/path': 1}`. 
    
    // 這是因為我們模組的被轉換後會通過相對路徑來呼叫`require()`. 當呼叫這個函式時,我們應該能夠知道依賴圖中的哪個模組對應於該模組的相對路徑. 
    modules += `${mod.id}: [
      function (require, module, exports) { ${mod.code} },
      ${JSON.stringify(mod.mapping)},
    ],`;
    / 最後,使用`commonjs`,當模組需要被匯出時,它可以通過改變exports物件來暴露模組的值. 
   // require函式最後會返回exports物件.
    const result = `
    (function(modules) {
      function require(id) { 
        const [fn, mapping] = modules[id];
        function localRequire(name) { 
          return require(mapping[name]); 
        }
        const module = { exports : {} };
        fn(localRequire, module, module.exports); 
        return module.exports;
      }
      require(0);
    })({${modules}})
    `;
  return result;
  });
複製程式碼

執行!

const graph = createGraph('./example/entry.js');
const result = bundle(graph);
//得到結果,開心!
console.log(result);

複製程式碼

更多資訊可訪問專案github地址

3.總結

webpack解決了包與包之間潛在的迴圈依賴難題,同時,按需合併靜態檔案,以避免瀏覽器在網路取數階段的併發瓶頸。除了打包,還可以進一步實現壓縮(減少網路傳輸)和編譯(ES6、JSX等語法向下相容)的功能。

基於對webpack.config.js檔案的配置,執行打包時的工作原理,可總結為:把頁面邏輯當作一個整體,通過一個給定的入口檔案,webpack從這個檔案開始,找到所有的依賴檔案,進行打包、編譯、壓縮,最後輸出一個瀏覽器可識別的JS檔案。

一個模組打包工具,第一步會從入口檔案開始,對其進行依賴分析,第二步對其所有依賴再次遞迴進行依賴分析,第三步構建出模組的依賴圖集,最後一步根據依賴圖集使用CommonJS規範構建出最終的程式碼。

4.參考網址

https://mp.weixin.qq.com/s/w-oXmHNSyu0Y_IlfmDwJKQ

https://github.com/chinanf-boy/minipack-explain/blob/master/src/minipack.js

https://zhaoda.net/webpack-handbook/configuration.html

相關文章