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

最近在前端論壇閒逛,看到了一些講parcel、webpack的文章,就忽然很好奇,天天都在用的打包工具,他們打包的原理到底是什麼。只有知道了這一點,才能夠在衆多的打包工具裏,找到最適合的那個它。在瞭解打包原理以前,先花一些篇章說明了一下爲何要使用打包工具。html

0.模塊系統

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

模塊系統主要解決模塊的定義、依賴和導出。 原始的<script>標籤加載方式有一些常見的弊端:例如全局做用域下容易形成變量衝突;文件只能按照<script>的書寫順序進行加載;開發人員必須主觀解決模塊和代碼庫的依賴關係等。node

所以衍生出不少模塊化方案:webpack

1.CommonJs:優勢:服務器端模塊便於重用。缺點:同步的模塊加載方式不適合在瀏覽器環境中,同步意味着阻塞加載,瀏覽器資源是異步加載的。git

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

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

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解決了包與包之間潛在的循環依賴難題,同時,按需合併靜態文件,以免瀏覽器在網絡取數階段的併發瓶頸。除了打包,還能夠進一步實現壓縮(減小網絡傳輸)和編譯(ES六、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

相關文章
相關標籤/搜索