webpack 打包過程

本文參考webpack創始人 Tobias Koppers 的視頻 Webpack founder Tobias Koppers demos bundling live by hand,梳理webpack打包過程。javascript

手動打包文件

文件目錄

咱們準備一個極簡單的項目來進行打包,目錄結構和內容以下:html

+-- src
| +-- big.js
| +-- helloWorld.js
| +-- index.js
| +-- lazy.js
複製代碼

index.jsjava

import helloWorld from './helloWorld'
const node = document.createElement("div")
node.innerHTML = helloWorld + 'loading...'
import(/* webpackChunkName: "async" */ './lazy').then(({ default: lazy }) => {
node.innerHTML = helloWorld + lazy
})
document.body.appendChild(node)
複製代碼

helloWorld.jsnode

import big from './big'
const helloWorld = big('hello world!')
export default helloWorld
複製代碼

big.jswebpack

export default (val) => {
return val && val.toUpperCase()
}
複製代碼

lazy.jsgit

import big from './big'
const lazy = big("lazy loaded!")
export default lazy
複製代碼

咱們先來看下webpack打包以後的結果,省略了一些代碼,可是大致能夠看到,全部分散的文件最終變成一個當即執行函數,參數是文件(模塊)隊列數組。es6

/******/ (function(modules) {// webpackBootstrap
   /******/ // ...
 /******/ })({
/************************************************************************/
/***/ "./src/big.js":
/***/ (function(module, __webpack_exports__, __webpack_require__) {}),
/***/ "./src/index.js":
/***/ (function(module, __webpack_exports__, __webpack_require__) {
/***/ })

/******/ });

複製代碼

咱們的目標是經過人工打包的方式生成這樣一個當即執行函數,經過必定的串聯邏輯,將全部的模塊整合到一塊兒。github

模塊劃分

能夠看到項目模塊之間有這樣的引用關係,入口文件引入了helloWorldlazyhelloWorldlazy分別又引入了bigweb

* src/index.js (ESM)
    # ./helloWorld
    # (async) ./lazy
    -  src/helloWorld.js
    -  (async) src/lazy.js
* src/helloWorld.js (ESM)
    # ./big
    -  src/big.js
* src/big.js
* src/lazy.js (ESM)
    # ./big
    -  src/big.js
複製代碼

打包以後將生成兩個文件,一個主文件main.js,一個是動態引入的async.js。其中,main是async的父文件,main中有的模塊,asycn能夠不引入。main文件裏面已經包含了src/big.js,這裏進行優化,打包後的async.js不須要包含src/big.js 以下圖所示:json

main.js

- src/index.js
- src/helloWorld.js
- src/big.js
複製代碼

async.js (parent:main)

- src/lazy.js
- src/big.js ( in parent)---delete
複製代碼

如今劃分一下模塊,能夠看到入口文件--index.js,咱們將它import的文件直接串聯當成第一個模塊。這裏只有引入一個模塊helloWorld(lazy是打包進去async.js暫不考慮)。由此能夠劃分紅三個模塊,咱們手動爲每一個模塊賦予一個id(中括號中的數字)。

* [0]src/index.js (ESM) + 1modules
   # ./helloWorld
   # (async) ./lazy
   -  src/helloWorld.js
   -  (async) src/lazy.js
   -  src/big.js
* [1]src/big.js
* [2]src/lazy.js (ESM)
   # ./big
   -  src/big.js
複製代碼

import和export

咱們知道webpack把分散的代碼經過importexport串成一個當即執行函數(IIFE),參數是模塊對象數組。 其中模塊對象是一個這樣的結構:

{
 [moduleId]: function() {
     // 模塊代碼
 }
}
複製代碼

如今來處理一下每一個文件的importexport

對於每個模塊,要保證有獨立的做用域,用一個funtion去包裹。而且傳入兩個參數,用來實現importexport的功能。 index.js + 1 modules(hellowWorld.js)

(function(__require__, exports) {
 let X = __require__(1)
 const helloWorld = X.default('hello world!')
 const node = document.createElement("div")
 node.innerHTML = helloWorld + 'loading...'
 // 先看普通的import
 // import(/* webpackChunkName: "async" */ './lazy').then(({ default: lazy }) => {
 // node.innerHTML = helloWorld + lazy
 // })
 document.body.appendChild(node)
})
複製代碼

big.js

(function(__require__, exports) {
  exports.default = (val) => {
    return val && val.toUpperCase()
  }
})
複製代碼

模擬import

import的功能就是: 1.執行目標模塊的代碼; 2.導出目標模塊的export內容給外部使用。 以下__require__函數的實現,

function __require__(id) {
   // 設置一個緩存,有的話直接返回
   if(cache[id]) return cache[id].exports
   
   var module = {
       exports: {}
   };
   // 一、執行當前模塊的內容,這個modules[id]就是咱們剛纔對每一個模塊封裝的那個方法
   modules[id](__require__, module.exports, module)
   cache[id] = module
   // 二、導出當前模塊的export內容給外部使用
   return module.exports
}
複製代碼

runtime.js

!(function(modules){
    function __require__ (id) {
        var module = {
            exports: []
        }
        modules[id](__require__, module.exports, module);
        return module.exports
    }
    __require__(0)
    })(
    {
        0: (function(__require__, exports) {
                let X = __require__(1)
                const helloWorld = X.default('hello world!')
                const node = document.createElement("div")
                node.innerHTML = helloWorld + 'loading...'
                // import(/* webpackChunkName: "async" */ './lazy').then(({ default: lazy }) => {
                // node.innerHTML = helloWorld + lazy
                // }
                document.body.appendChild(node)
              }),
        1: (function(__require__, exports) {
                exports.default = (val) => {
                return val && val.toUpperCase()
                }
            })
    }
)
複製代碼

在index.html上引入這個文件,打開就能看到結果了。至此,咱們完成了最基本的手動打包流程。

懶加載模塊

如今還剩下對lazy.js的打包,它是做爲一個單獨的文件,按需引入的。咱們但願使用的時候是這樣的,加載完模塊,而後進行require: index.js (bundled)

// import(/* webpackChunkName: "async" */ './lazy').then(({ default: lazy }) => {
// node.innerHTML = helloWorld + lazy
// })
__require__.loadChunk(0)
  .then(__require__.bind(null, 3))
      .then(function(Y){
          node.innerHTML = helloWorld + Y.default
      })
複製代碼

請求一個文件地址,獲得文件中的數據,這個過程用相似jsonp的方式來實現。

首先是下載文件,這個過程是異步的,要用一個promise來封裝。下載完成,還須要解析出數據才能執行下一步。因此,promise的回調函數resolve下載完成先放在一個全局變量chunkResolves當中,等解析出數據以後再調用它。

runtime.js

// 每一個模塊下載(promise)完成對應的resolve
let chunkResolves = {};

__require__.loadChunk = function(chunkId) {
    return new Promise(resolve => {
        chunkResolves[chunkId] = resolve
        let script = document.createElement('script')
        script.src = 'src/' + {0: 'async'}[chunkId]+ '.js'
        document.head.appendChild(script)
    })
}
複製代碼

根據jsonp的原理,下載下來的模塊對象須要用一個callback(這裏是requireJsonp)包裹,變成一個可執行的腳本,下載完成以後在本地執行這個callback才能解析出模塊對象。因此手動對異步的模塊進行一個封裝: async.js

window.requireJsonp( 0, {
    3: (function(__require__, exports) {
        let X = __require__(1)
        const lazy = X.default("lazy loaded!")
        exports.default = lazy
    })
})
複製代碼

而且咱們應提早聲明好window.requireJsonp這個回調函數。咱們把下載獲得的動態模塊對象添加到當即執行函數參數的個模塊對象,就回到了普通的模塊打包的狀況,這時候解析完成,執行promiseresolve,算是整個異步加載的過程結束。 runtime.js

!(function(modules){
    function __require__ (id) {
     // ...
    }
    // 每一個模塊下載(promise)完成對應的resolve
   let chunkResolves = {};
   
    window.requireJsonp = function(chunkId, newModules) {
        for (const id in newModules) {
            modules[id] = newModules[id]
            chunkResolves[chunkId]();
        }
    }
    __require__(0)
    })({
        //...模塊對象
    })
複製代碼

這樣,咱們就完成了人工打包一個項目的簡單流程。接下來看要怎麼用代碼來實現自動打包。

自動打包

咱們參考開源項目minipack,來看看要怎麼實現一個簡易的打包工具。 先不看詳細的細節,咱們主要的步驟就是:

// 解析模塊
function createAsset(filename) {}
// 生成依賴圖
function createGraph(entry){}
// 打包
function bundle(graph){}

const graph = createGraph('./src/index.js')
const result = bundle(graph)
複製代碼

所依賴的工具

  • abylon:js解析器,將文本代碼轉化成AST(語法樹)
  • babel-travse:遍歷AST尋找依賴關係
  • babel-core的transformFromAst:將AST代碼轉化成瀏覽器所能識別的代碼(ES5)
const fs = require('fs');
const path = require('path');
const babylon = require('babylon'); // 將文件轉化成AST
const traverse = require('babel-traverse').default; // 尋找依賴關係
const {transformFromAst} = require('babel-core'); // 將 AST 轉化成 ES5
複製代碼

主要就是把文本文件轉化成語法樹,拿到importexport知道模塊之間的依賴關係,再把語法樹轉換成ES5。

能夠了解一下語法樹,以下圖所示,能夠拿到每句代碼對應的信息。

解析單個文件

function createAsset(filename) {
  // 讀一個文件,獲得一個文件內容的字符串
  const content = fs.readFileSync(filename, 'utf-8');

  // 咱們經過 babylon 這個 javascript 解析器來理解 import 進來的字符串
  const ast = babylon.parse(content, {
    sourceType: 'module',
  });

  // 該模塊所依賴的模塊的相對路徑放在這個 dependencies 數組
  const dependencies = [];

  // import聲明
  traverse(ast, {
    // es6 的模塊是靜態的,不能導入一個變量或者有條件的導入另外一個模塊
    ImportDeclaration: ({node}) => {
      // 所依賴的模塊的路徑
      dependencies.push(node.source.value);
    },
  });

  // 遞增設置模塊ID
  const id = ID++;

// AST -> ES5
  const {code} = transformFromAst(ast, null, {
    presets: ['env'],
  });
  // 返回模塊的信息
  return {
    id,
    filename,
    dependencies,
    code,
  };
}
複製代碼

index文件解析後輸出以下內容

import helloWorld from './helloWorld'
const node = document.createElement("div")
node.innerHTML = helloWorld + 'loading...'
document.body.appendChild(node)
複製代碼
{ id: 0,
  filename: './src/index.js',
  dependencies: [ './helloWorld.js' ],
  code: '"use strict";\n\n
    var _helloWorld = require("./helloWorld.js");\n\n
    var _helloWorld2 = _interopRequireDefault(_helloWorld);\n\n
    function  _interopRequireDefault(obj) { 
        return obj && obj.__esModule ? obj : {
        default: obj }; }\n\n
    var node = document.createElement("div");\nn
    ode.innerHTML = _helloWorld2.default + \'loading...\';\n\n
    document.body.appendChild(node);' 
}
複製代碼

生成依賴圖

// 咱們須要知道單個模塊的依賴,而後從入口文件開始,提取依賴圖
function createGraph(entry) {
  // 從第一個文件開始,首先解析index文件
  const mainAsset = createAsset(entry);

  // 定義一個依賴隊列,一開始的時候只有入口文件
  const queue = [mainAsset];

  // 遍歷 queue,廣度優先
  for (const asset of queue) {
    asset.mapping = {};

    const dirname = path.dirname(asset.filename);

    // 遍歷依賴數組,解析每個依賴模塊
    asset.dependencies.forEach(relativePath => {
      const absolutePath = path.join(dirname, relativePath);

      // 解析
      const child = createAsset(absolutePath);

      // 子模塊`路徑-id`map
      asset.mapping[relativePath] = child.id;

      // 每個子模塊加入依賴圖隊列,進行遍歷
      queue.push(child);
    });
  }
複製代碼

輸出的依賴圖長這樣:

[ { id: 0,
    filename: './src/index.js',
    dependencies: [ './helloWorld.js' ],
    code:
     '"use strict";\n\nvar _helloWorld = require("./helloWorld.js");\n\nvar _helloWorld2 = _interopRequireDefault(_helloWorld);\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }\n\nvar node = document.createElement("div");\nnode.innerHTML = _helloWorld2.default + \'loading...\';\n// import(/* webpackChunkName: "async" */ \'./lazy\').then(({ default: lazy }) => {\n// node.innerHTML = helloWorld + lazy\n// })\ndocument.body.appendChild(node);',
    mapping: { './helloWorld.js': 1 }
   },
  { id: 1,
    filename: 'src\\helloWorld.js',
    dependencies: [ './big.js' ],
    code:
     '"use strict";\n\nObject.defineProperty(exports, "__esModule", {\n value: true\n});\n\nvar _big = require("./big.js");\n\nvar _big2 = _interopRequireDefault(_big);\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }\n\nvar helloWorld = (0, _big2.default)(\'hello world!\');\nexports.default = helloWorld;',
    mapping: { './big.js': 2 } 
  },
  { id: 2,
    filename: 'src\\big.js',
    dependencies: [],
    code:
     '"use strict";\n\nObject.defineProperty(exports, "__esModule", {\n value: true\n});\n\nexports.default = function (val) {\n return val && val.toUpperCase();\n};',
    mapping: {}
  } ]
複製代碼

構造自執行函數和參數modules

// 最終咱們要生成一個自執行函數,參數是模塊依賴圖
// (function() {})()

function bundle(graph) {
  let modules = '';
  graph.forEach(mod => {
    // 利用 createAsset 解析的時候,咱們是把 import 轉化成 commonJs 的 require

    // 模塊`id-路徑`的map,由於咱們轉化以後的代碼的require是使用相對路徑.寫一個map,拿到模塊id的時候能夠知道該模塊對應的路徑
    // { './relative/path': 1 }.

    modules += `${mod.id}: [ function (require, module, exports) { ${mod.code} }, ${JSON.stringify(mod.mapping)}, ],`;
  });
  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;
}
複製代碼

生成的模塊對象參數跟咱們第一部分手動打包的模塊對像是同樣的:

0: [
      function (require, module, exports) {
 "use strict";

        var _helloWorld = require("./helloWorld.js");
        
        var _helloWorld2 = _interopRequireDefault(_helloWorld);
        
        function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
        
        var node = document.createElement("div");
        node.innerHTML = _helloWorld2.default + 'loading...';
        // import(/* webpackChunkName: "async" */ './lazy').then(({ default:
        lazy }) => {
        // node.innerHTML = helloWorld + lazy
        // })
        document.body.appendChild(node);
              },
     {"./helloWorld.js":1},
    ],
1: [
      function (require, module, exports) {
 "use strict";

        Object.defineProperty(exports, "__esModule", {
          value: true
        });
        
        var _big = require("./big.js");
        
        var _big2 = _interopRequireDefault(_big);
        
        function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
        
        var helloWorld = (0, _big2.default)('hello world!');
        exports.default = helloWorld;
              },
     {"./big.js":2},
    ],
2: [
      function (require, module, exports) {
 "use strict";

        Object.defineProperty(exports, "__esModule", {
          value: true
        });
        
        exports.default = function (val) {
          return val && val.toUpperCase();
        };
              },
        {},
    ],
複製代碼

至此咱們完成了一個簡易的模塊打包器。

參考文獻:

Webpack founder Tobias Koppers demos bundling live by hand

github 項目 minipack

相關文章
相關標籤/搜索