如何實現一個 Webpack 的 Bundler 打包機制 ?

前言

image

我想這兩年,應該是「Webpack」受衝擊最明顯的時間段。前有「Snowpack」基於瀏覽器原生ES Module 提出,後有「Vite」站在「Vue3」肩膀上的迅猛發展,真的是後浪推前浪,前浪....javascript

而且,「Vite」主推的實現技術不是一點點新,典型的一點使用「esbuild」來充當「TypeScript」的解釋器,這一點是和目前社區內絕大多數打包工具是不一樣的。java

在下一篇文章,我將會介紹什麼是「esbuild」,以及其帶來的價值。

可是,雖然說後浪確實很強,不過起碼近兩年來看「Webpack」所處的地位是仍然不可撼動的。因此,更好地瞭解「Webpack」相關的原理,能夠增強咱們的我的競爭力。node

那麼,回到今天的正題,咱們就來從零實現一個「Webpack」的 Bundler 打包機制。segmentfault

1 Bundler 打包背景

Bundler 打包背景,即它是什麼?Bundler 打包指的是咱們能夠將模塊化的代碼經過構建模塊依賴圖解析代碼執行代碼等一系列手段來將模塊化的代碼聚合成可執行的代碼瀏覽器

在日常的開發中,咱們常常使用的就是 ES Module 的形式進行模塊間的引用。那麼,爲了實現一個 Bundler 打包,咱們準備這樣一個例子:babel

目錄模塊化

|—— src
    |-- person.js
    |-- introduce.js
    |-- index.js    ## 入口
|—— bundler.js      ## bundler 打包機制

代碼函數

// person.js
export const person = 'my name is wjc'
// introduce.js
import { person } from "./person.js";

const introduce = `Hi, ${person}`;
export default introduce;
// index.js
import introduce from "./introduce.js";

console.log(introduce);

除開 bundler.js 打包機制實現文件,另外咱們建立了三個文件,它們分別進行了模塊間的引用,最終它們會被 Bundler 打包機制解析生成可執行的代碼。工具

接下來,咱們就來一步步地實現 Bundler 打包機制。學習

2 單模塊解析

Bundler 的打包實現第一步,咱們須要知道每一個模塊中的代碼,而後對模塊中的代碼進行依賴分析、代碼轉化,從而保證代碼的正常執行。

首先,從入口文件 index.js 開始,獲取其文件的內容(代碼):

const fs = require("fs")

const moduleParse = (file = "") => {
  const rawCode = fs.readFileSync(file, 'utf-8')
}

獲取到模塊的代碼後,咱們須要知道它依賴了哪些模塊?這個時候,咱們須要藉助兩個 babel 的工具:@babel/parser@babel/traverse。前者負責將代碼轉化爲「抽象語法樹 AST」,後者能夠根據模塊的引用構建依賴關係。

@babel/parser 將模塊的代碼解析成「抽象語法樹 AST」:

const rawCode = fs.readFileSync(file, 'utf-8')
const ast = babelParser(rawCode, {
  sourceType: "module"
})

@babel/traverse 根據模塊的引用標識 ImportDeclaration 來構建依賴:

const dependencies = {};
traverse(ast, {
  ImportDeclaration({ node }) {
    const dirname = path.dirname(file);
    const absoulteFile = `./${path
      .join(dirname, node.source.value)
      .replace("\\", "/")}`;
    dependencies[node.source.value] = absoulteFile;
  },
});

這裏,咱們經過 @babel/traverse 來將入口 index.js 依賴的模塊放到 dependencies 中:

// dependencies
{ './intro.js' : './src/intro.js' }

可是,此時 ast 中的代碼仍是初始 ES6 的代碼,因此,咱們須要藉助 @babel/preset-env 來將其轉爲 ES5 的代碼:

const { code } = babel.transformFromAst(ast, null, {
  presets: ["@babel/preset-env"],
});

index.js 轉化後的代碼:

"use strict";
var _introduce = _interopRequireDefault(require("./introduce.js "));
function _interopRequireDefault(obj) { 
  return obj && obj.__esModule ?
    obj : {
        "default": obj
    };
}
console.log(_introduce["default"]);

到此,咱們就完成了對單模塊的解析,完整的代碼以下:

const moduleParse = (file = "") => {
  const rawCode = fs.readFileSync(file, "utf-8");
  const ast = babelParser.parse(rawCode, {
    sourceType: "module",
  });
  const dependencies = {};
  traverse(ast, {
    ImportDeclaration({ node }) {
      const dirname = path.dirname(file);
      const absoulteFile = `./${path
        .join(dirname, node.source.value)
        .replace("\\", "/")}`;
      dependencies[node.source.value] = absoulteFile;
    },
  });
  const { code } = babel.transformFromAst(ast, null, {
    presets: ["@babel/preset-env"],
  });

  return {
    file,
    dependencies,
    code,
  };
};

接下來,咱們就開始模塊依賴圖的構建。

2 構建模塊依賴圖

衆所周知,「Webpack」的打包過程會構建一個模塊依賴圖,它的造成無非就是從入口文件出發,經過它的引用模塊,進入該模塊,繼續單模塊的解析,不斷重複這個過程。大體的邏輯圖以下:

因此,在代碼層面,咱們須要從入口文件出發,先調用 moduleParse() 解析它,而後再遍歷獲取其對應的依賴 dependencies,以及調用 moduleParse()

const buildDependenceGraph = (entry) => {
  const entryModule = moduleParse(entry);
  const rawDependenceGraph = [entryModule];
  for (const module of rawDependenceGraph) {
    const { dependencies } = module;
    if (Object.keys(dependencies).length) {
      for (const file in dependencies) {
        rawDependenceGraph.push(moduleParse(dependencies[file]));
      }
    }
  }
  // 優化依賴圖
  const dependenceGraph = {};
  rawDependenceGraph.forEach((module) => {
    dependenceGraph[module.file] = {
      dependencies: module.dependencies,
      code: module.code,
    };
  });

  return dependenceGraph;
};

最終,咱們構建好的模塊依賴圖會放到 dependenceGraph。如今,對於咱們這個例子,構建好的依賴圖會是這樣:

{ 
  './src/index.js':
   { 
     dependencies: { './introduce.js': './src/introduce.js' },
     code: '"use strict";\n\nvar...'     
    },
  './src/introduce.js':{ 
    dependencies: { 
      './person.js': './src/person.js' 
    },
    code: '"use strict";\n\nObject.defineProperty(exports,...' 
  },
  './src/person.js':
   { 
     dependencies: {},
     code: '"use strict";\n\nObject.defineProperty(exports,...' 
    } 
}

3 生成可執行代碼

構建完模塊依賴圖後,咱們須要根據依賴圖將模塊的代碼轉化成能夠執行的代碼。

因爲 @babel/preset-env 處理後的代碼用到了兩個不存在的變量 requireexports。因此,咱們須要定義好這兩個變量。

require 主要作這兩件事:

  • 根據模塊名,獲取對應的代碼並執行。
eval(dependenceGraph[module].code)
  • 處理模塊名,因爲引用的時候是相對路徑,這裏須要轉成絕對路徑,而且遞歸執行依賴模塊代碼
function _require(relativePath) {
  return require(dependenceGraph[module].dependencies[relativePath]);
}

export 則用於存儲定義的變量,因此咱們定義一個對象來存儲。完整的生成代碼函數 generateCode 定義:

const generateCode = (entry) => {
  const dependenceGraph = JSON.stringify(buildDependenceGraph(entry));
  return `
  (function(dependenceGraph){
    function require(module) {
      function localRequire(relativePath) {
        return require(dependenceGraph[module].dependencies[relativePath]);
      };
      var exports = {};
      (function(require, exports,  code) {
        eval(code);
      })(localRequire, exports, dependenceGraph[module].code);
      return exports;
    }
    require('${entry}');
  })(${dependenceGraph});
  `;
};

4 完整的 bundler 打包機制實現代碼

完整的 Bunlder 打包實現代碼:

const fs = require("fs");
const path = require("path");
const babelParser = require("@babel/parser");
const traverse = require("@babel/traverse").default;
const babel = require("@babel/core");

const moduleParse = (file = "") => {
  const rawCode = fs.readFileSync(file, "utf-8");
  const ast = babelParser.parse(rawCode, {
    sourceType: "module",
  });
  const dependencies = {};
  traverse(ast, {
    ImportDeclaration({ node }) {
      const dirname = path.dirname(file);
      const absoulteFile = `./${path
        .join(dirname, node.source.value)
        .replace("\\", "/")}`;
      dependencies[node.source.value] = absoulteFile;
    },
  });
  const { code } = babel.transformFromAst(ast, null, {
    presets: ["@babel/preset-env"],
  });

  return {
    file,
    dependencies,
    code,
  };
};

const buildDependenceGraph = (entry) => {
  const entryModule = moduleParse(entry);
  const rawDependenceGraph = [entryModule];
  for (const module of rawDependenceGraph) {
    const { dependencies } = module;
    if (Object.keys(dependencies).length) {
      for (const file in dependencies) {
        rawDependenceGraph.push(moduleParse(dependencies[file]));
      }
    }
  }
  // 優化依賴圖
  const dependenceGraph = {};
  rawDependenceGraph.forEach((module) => {
    dependenceGraph[module.file] = {
      dependencies: module.dependencies,
      code: module.code,
    };
  });
  return dependenceGraph;
};

const generateCode = (entry) => {
  const dependenceGraph = JSON.stringify(buildDependenceGraph(entry));
  return `
  (function(dependenceGraph){
    function require(module) {
      function localRequire(relativePath) {
        return require(dependenceGraph[module].dependencies[relativePath]);
      };
      var exports = {};
      (function(require, exports,  code) {
        eval(code);
      })(localRequire, exports, dependenceGraph[module].code);
      return exports;
    }
    require('${entry}');
  })(${dependenceGraph});
  `;
};

const code = generateCode("./src/index.js");

最終,咱們拿到的 code 就是 Bundler 打包後生成的可執行代碼。接下來,咱們能夠將它直接複製到瀏覽器的 devtool 中執行,查看結果。

寫在最後

雖然,這個 Bundler 打包機制的實現,只是簡易版的,它只是大體地實現了整個「Webpack」的 Bundler 打包流程,並非適用於全部用例。可是,在我看來不少東西的學習都應該是從易到難,這樣的吸取效率纔是最高的。

往期文章回顧

深度解讀 Vue3 源碼 | 組件建立過程

深度解讀 Vue3 源碼 | 內置組件 teleport 是什麼「來頭」?

深度解讀 Vue3 源碼 | compile 和 runtime 結合的 patch 過程

❤️愛心三連擊

寫做不易,若是你以爲有收穫的話,能夠愛心三連擊!!!

相關文章
相關標籤/搜索