minipack 原始碼簡析

ttys000發表於2018-10-18

背景:其實“打包”對於前端來說再熟悉不過了,但是深入其中的原理,卻不是人人都熟悉。由於webpack功能的強大和盛行,我們大部分都是所謂的“配置工程師”。藉此,特地簡單分析了一官方文件中提到的一個minipack專案的原始碼,以此深入瞭解下什麼是打包?以及打包的原理是什麼?

文章寫的比較平,是按照分析程式碼的順序寫的,細微有些總結,有錯誤或不妥之處,懇請指出。

專案地址:github.com/ronami/mini…

原始程式碼:

// 入口檔案 entry.js
import message from './message.js';

console.log(message);


// message.js
import {name} from './name.js';

export default `hello ${name}!`;


// name.js
export const name = 'world';

複製程式碼

讀取檔案內容,分析依賴,第一步需要解析原始碼,生成抽象語法樹。

第一步,讀取入口檔案,生成 AST,遞迴生成依賴關係物件 graph。 其中,createAsset 函式是解析js文字,生成每個檔案對應的一個物件,其中 code 的程式碼是經過babel-preset-env轉換後可在瀏覽器中執行的程式碼。

const {code} = transformFromAst(ast, null, {
    presets: ['env'],
  });
複製程式碼

createGraph 函式生成依賴關係物件。

[ 
  { id: 0,
    filename: './example/entry.js',
    dependencies: [ './message.js' ],
    code: '"use strict";\n\nvar _message = require("./message.js");\n\nvar _message2 = _interopRequireDefault(_message);\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }\n\nconsole.log(_message2.default);',
    mapping: { './message.js': 1 } },

  { id: 1,
    filename: 'example/message.js',
    dependencies: [ './name.js' ],
    code: '"use strict";\n\nObject.defineProperty(exports, "__esModule", {\n  value: true\n});\n\nvar _name = require("./name.js");\n\nexports.default = "hello " + _name.name + "!";',
    mapping: { './name.js': 2 } },

  { id: 2,
    filename: 'example/name.js',
    dependencies: [],
    code: '"use strict";\n\nObject.defineProperty(exports, "__esModule", {\n  value: true\n});\nvar name = exports.name = \'world\';',
    mapping: {} } 
	
]
複製程式碼

有了依賴關係圖,下一步就是將程式碼打包可以在瀏覽器中執行的包。

首先我們將依賴圖解析成如下字串(其實是物件沒用{}包裹的格式):

關鍵程式碼是這句:

modules += `${mod.id}: [
  function (require, module, exports) {
    ${mod.code}
  },
  ${JSON.stringify(mod.mapping)},
],`;
複製程式碼
    0: [
      function (require, module, exports) {
        // -------------- mod.code --------------
        "use strict";
        var _message = require("./message.js");
        var _message2 = _interopRequireDefault(_message);
        function _interopRequireDefault(obj) { 
          return obj && obj.__esModule ? obj : { default: obj }; 
        }

        console.log(_message2.default);
        // --------------------------------------
      },
      {"./message.js":1},
    ],
    1: [
      function (require, module, exports) {
        // -------------- mod.code --------------
        "use strict";
        Object.defineProperty(exports, "__esModule", {
          value: true
        });
        var _name = require("./name.js");
        exports.default = "hello " + _name.name + "!";
        // --------------------------------------
      },
      {"./name.js":2},
    ],
    
    2: [
      function (require, module, exports) {
        // -------------- mod.code --------------
        "use strict";
        Object.defineProperty(exports, "__esModule", {
          value: true
        });
        var name = exports.name = 'world';
        // --------------------------------------
      },
      {},
    ],
複製程式碼

這裡,我們比較下原始碼:

// 入口檔案 entry.js
import message from './message.js';

console.log(message);

// ---
"use strict";
var _message = require("./message.js");
var _message2 = _interopRequireDefault(_message);
function _interopRequireDefault(obj) { 
  return obj && obj.__esModule ? obj : { default: obj }; 
}

console.log(_message2.default);


// message.js
import {name} from './name.js';

export default `hello ${name}!`;

// ---
"use strict";
Object.defineProperty(exports, "__esModule", {
  value: true
});
var _name = require("./name.js");
exports.default = "hello " + _name.name + "!";


// name.js
export const name = 'world';

// ---
"use strict";
Object.defineProperty(exports, "__esModule", {
  value: true
});
var name = exports.name = 'world';

複製程式碼

可以看出,babel在轉換原始code的時候,引入了require函式來解決模組引用問題。但是其實瀏覽器仍然是不認識的。因此還需要額外定義一個require函式(其實這部分和requirejs原理類似的模組化解決方案,其中原理其實也很簡單)

得到這個字串後,再最後拼接起來即最終結果 => 然後,我們還需要定義一個自執行函式文字,並將上述字串傳入其中,拼接結果如下:

(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);
})({
	0: [
		function (require, module, exports) {
			"use strict";
			var _message = require("./message.js");
			var _message2 = _interopRequireDefault(_message);
			function _interopRequireDefault(obj) {
				return obj && obj.__esModule ? obj : { default: obj };
			}

			console.log(_message2.default);
		},
		{ "./message.js": 1 },
	],
	1: [
		function (require, module, exports) {
			"use strict";
			Object.defineProperty(exports, "__esModule", {
				value: true
			});
			var _name = require("./name.js");
			exports.default = "hello " + _name.name + "!";
		},
		{ "./name.js": 2 },
	],

	2: [
		function (require, module, exports) {
			"use strict";
			Object.defineProperty(exports, "__esModule", {
				value: true
			});
			var name = exports.name = 'world';
		},
		{},
	],
})
複製程式碼

我們執行最後的結果,會輸出"hello world"。

那我們仔細分析下打包後的這段程式碼:

首先這是一個自執行函式,傳入的字串外面包裹上{}後是一個物件,形如<moduleId>: <value>的格式。

自執行函式的主體部分定義了一個require函式:

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;
}
複製程式碼

接收一個模組id,過程如下:

  1. 第一步:解構module(陣列解構),獲取fn和當前module的依賴路徑
  2. 第二步:定義引入依賴函式(相對引用),函式體同樣是獲取到依賴module的id,localRequire 函式傳入到fn中
  3. 第三步:定義module變數,儲存的是依賴模組匯出的物件,儲存在module.exports中,module和module.exports也傳入到fn中
  4. 第四步:遞迴執行,直到子module中不再執行傳入的require函式

簡單來說,模組之間通過requireexports聯絡,至於模組內部的實現,只在模組內可見。


由此,可以看出,其實原理並不是很複雜,但是卻很巧妙,要了解“打包”的原理,也需要了解“模組化”的一些知識。前端發展雖快,但是深入到基礎,會發現其實是一脈相通的。

參考中文資料,裡面有程式碼的逐句翻譯(外國人的註釋寫的是真詳細啊):

(本文始發於知乎專欄:zhuanlan.zhihu.com/ttys000)

相關文章