作了一晚上動畫,讓你們十分鐘搞懂Webpack

1、什麼是webpack

webpack是一個打包工具,他的宗旨是一切靜態資源皆可打包。css

image-20210506104502010

2、原理分析

首先咱們經過一個製做一個打包文件的原型。html

假設有兩個js模塊,這裏咱們先假設這兩個模塊是複合commomjs標準的es5模塊。前端

語法和模塊化規範轉換的事咱們先放一放,後面說。vue

咱們的目的是將這兩個模塊打包爲一個能在瀏覽器端運行的文件,這個文件其實叫bundle.js。node

好比webpack

// index.js
var add = require('add.js').default
console.log(add(1 , 2))

// add.js
exports.default = function(a,b) {return a + b}
複製代碼

假設在瀏覽器中直接執行這個程序確定會有問題 最主要的問題是瀏覽器中沒有exports對象與require方法因此必定會報錯。es6

咱們須要經過模擬exports對象和require方法web

1. 模擬exports對象

首先咱們知道若是在nodejs打包的時候咱們會使用fs.readfileSync()來讀取js文件。這樣的話js文件會是一個字符串。而若是須要將字符串中的代碼運行會有兩個方法分別是new Function與Eval。編程

在這裏面咱們選用執行效率較高的eval。json

exports = {}
eval('exports.default = function(a,b) {return a + b}') // node文件讀取後的代碼字符串
exports.default(1,3)
複製代碼

image-20210507102528679

上面這段代碼的運行結果能夠將模塊中的方法綁定在exports對象中。因爲子模塊中會聲明變量,爲了避免污染全局咱們使用一個自運行函數來封裝一下。

var exports = {}
(function (exports, code) {
	eval(code)
})(exports, 'exports.default = function(a,b){return a + b}')
複製代碼

2. 模擬require函數

require函數的功能比較簡單,就是根據提供的file名稱加載對應的模塊。

首先咱們先看看若是隻有一個固定模塊應該怎麼寫。

Kapture 2021-05-09 at 11.20.31.gif

function require(file) {
	var exports = {};
	(function (exports, code) {
		eval(code)
	})(exports, 'exports.default = function(a,b){return a + b}')
  return exports
}
var add = require('add.js').default
console.log(add(1 , 2))
複製代碼

完成了固定模塊,咱們下面只須要稍加改動,將全部模塊的文件名和代碼字符串整理爲一張key-value表就能夠根據傳入的文件名加載不一樣的模塊了。

(function (list) {
  function require(file) {
    var exports = {};
    (function (exports, code) {
      eval(code);
    })(exports, list[file]);
    return exports;
  }
  require("index.js");
})({
  "index.js": ` var add = require('add.js').default console.log(add(1 , 2)) `,
  "add.js": `exports.default = function(a,b){return a + b}`,
});

複製代碼

固然要說明的一點是真正webpack生成的bundle.js文件中還須要增長模塊間的依賴關係。

叫作依賴圖(Dependency Graph)

相似下面的狀況。

{
  "./src/index.js": {
    "deps": { "./add.js": "./src/add.js" },
    "code": "....."
  },
  "./src/add.js": {
    "deps": {},
    "code": "......"
  }
}

複製代碼

另外,因爲大多數前端程序都習慣使用es6語法因此還須要預先將es6語法轉換爲es5語法。

總結一下思路,webpack打包能夠分爲如下三個步驟:

webpack原理.gif

  1. 分析依賴

  2. ES6轉ES5

  3. 替換exports和require

下面進入功能實現階段。

3、功能實現

咱們的目標是將如下兩個個互相依賴的ES6Module打包爲一個能夠在瀏覽器中運行的一個JS文件(bundle.js)

  • 處理模塊化
  • 多模塊合併打包 - 優化網絡請求

/src/add.js

export default (a, b) => a + b 
複製代碼

/src/index.js

import add from "./add.js";
console.log(add(1 , 2))
複製代碼

1. 分析模塊

分析模塊分爲如下三個步驟:

模塊的分析至關於對讀取的文件代碼字符串進行解析。這一步其實和高級語言的編譯過程一致。須要將模塊解析爲抽象語法樹AST。咱們藉助babel/parser來完成。

AST (Abstract Syntax Tree)抽象語法樹 在計算機科學中,或簡稱語法樹(Syntax tree),是源代碼語法結構的一種抽象表示。它以樹狀的形式表現編程語言的語法結構,樹上的每一個節點都表示源代碼中的一種結構。( astexplorer.net/)

yarn add @babel/parser
yarn add @babel/traverse
yarn add @babel/core
yarn add @babel/preset-env
複製代碼
  • 讀取文件

  • 收集依賴

  • 編譯與AST解析

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

function getModuleInfo(file) {
  // 讀取文件
  const body = fs.readFileSync(file, "utf-8");

  // 轉化AST語法樹
  const ast = parser.parse(body, {
    sourceType: "module", //表示咱們要解析的是ES模塊
  });

  // 依賴收集
  const deps = {};
  traverse(ast, {
    ImportDeclaration({ node }) {
      const dirname = path.dirname(file);
      const abspath = "./" + path.join(dirname, node.source.value);
      deps[node.source.value] = abspath;
    },
  });

  // ES6轉成ES5
  const { code } = babel.transformFromAst(ast, null, {
    presets: ["@babel/preset-env"],
  });
  const moduleInfo = { file, deps, code };
  return moduleInfo;
}
const info = getModuleInfo("./src/index.js");
console.log("info:", info);
複製代碼

運行結果以下:

![image-20210507152817221](/Users/xiaran/Library/Application Support/typora-user-images/image-20210507152817221.png)

2. 收集依賴

上一步開發的函數能夠單獨解析某一個模塊,這一步咱們須要開發一個函數從入口模塊開始根據依賴關係進行遞歸解析。最後將依賴關係構成爲依賴圖(Dependency Graph)

/** * 模塊解析 * @param {*} file * @returns  */
function parseModules(file) {
  const entry = getModuleInfo(file);
  const temp = [entry];
  const depsGraph = {};

  getDeps(temp, entry);

  temp.forEach((moduleInfo) => {
    depsGraph[moduleInfo.file] = {
      deps: moduleInfo.deps,
      code: moduleInfo.code,
    };
  });
  return depsGraph;
}

/** * 獲取依賴 * @param {*} temp * @param {*} param1 */
function getDeps(temp, { deps }) {
  Object.keys(deps).forEach((key) => {
    const child = getModuleInfo(deps[key]);
    temp.push(child);
    getDeps(temp, child);
  });
}
複製代碼

3. 生成bundle文件

這一步咱們須要將剛纔編寫的執行函數和依賴圖合成起來輸出最後的打包文件。

function bundle(file) {
  const depsGraph = JSON.stringify(parseModules(file));
  return `(function (graph) { function require(file) { function absRequire(relPath) { return require(graph[file].deps[relPath]) } var exports = {}; (function (require,exports,code) { eval(code) })(absRequire,exports,graph[file].code) return exports } require('${file}') })(${depsGraph})`;
}


!fs.existsSync("./dist") && fs.mkdirSync("./dist");
fs.writeFileSync("./dist/bundle.js", content);
複製代碼

最後能夠編寫一個簡單的測試程序測試一下結果。

<script src="./dist/bundle.js"></script>
複製代碼

image-20210507154015030

ok 學費了。

後面有興趣的話你們能夠在考慮一下如何加載css文件或者圖片base64 Vue SFC .vue。

🔥源碼

微信🔍搜索並關注「前端大班車」,回覆'webpack'得到'vue'源碼

相關文章
相關標籤/搜索