本文首發於知乎專欄:
http://zhuanlan.zhihu.com/starkwang前端
CommonJS 是一個流行的前端模塊化規範,也是目前 NodeJS 以及其模塊託管倉庫 npm 使用的規範,但目前暫無瀏覽器支持 CommonJS 。要想讓瀏覽器用上這些模塊,必須轉換格式。node
這個系列的文章,咱們會一步步完成一個基於 CommonJS 的打包工具,相似於一個簡單版的 Browserify 或者 Webpack 。webpack
與 NodeJS 環境不一樣,瀏覽器中不支持 CommonJS 的主要緣由是缺乏瞭如下幾個環境變量:web
modulenpm
exports瀏覽器
require模塊化
global函數
換句話說,打包器的原理就是模擬這四個變量的行爲。工具
好比咱們有一個index.js
文件,依賴了module1
和module2
兩個模塊,而且module1
依賴module2
:ui
//index.js var module1 = require("./module1"); var module2 = require("./module2"); module1.foo(); module2.foo(); function hello(){ console.log("Hello!"); } module.exports = hello;
//module1.js var module2 = require(module2); console.log("initialize module1"); console.log("this is module2.foo() in module1:"); module2.foo(); console.log("\n") module.exports = { foo: function(){ console.log("module1 foo !!!"); } };
//module2.js console.log("initialize module2"); module.exports = { foo: function(){ console.log("module2 foo !!!"); } };
把它放入一個匿名函數內,經過這個匿名函數注入 require
、modules
、export
、global
變量(咱們暫時不實現global)
function(module, exports, require, global){ var module1 = require("./module1"); var module2 = require("./module2"); module1.foo(); module2.foo(); function hello(){ console.log("Hello!"); } module.exports = hello; }
如今咱們用一個 modules
對象來存入這些匿名函數:
//modules { "entry": function(module, exports, require, global){ //index.js var module1 = require("./module1"); var module2 = require("./module2"); module1.foo(); module2.foo(); function hello(){ console.log("Hello!"); } module.exports = hello; }, "./module1": function(module, exports, require, global){ var module2 = require("./module2"); console.log("initialize module1"); console.log("this is module2.foo() in module1:"); module2.foo(); console.log("\n") module.exports = { foo: function(){ console.log("module1 foo !!!"); } }; }, "./module2": function(module, exports, require, global){ console.log("initialize module2"); module.exports = { foo: function(){ console.log("module2 foo !!!"); } }; } }
下面咱們實現一個簡單的 require
函數:
//這個對象用於儲存已導入的模塊 var installedModules = {}; function require(moduleName) { //若是模塊已經導入,那麼直接返回它的exports if(installedModules[moduleName]){ return installedModules[moduleName].exports; } //模塊初始化 var module = installedModules[moduleName] = { exports: {}, name: moduleName, loaded: false }; //執行模塊內部的代碼,這裏的 modules 變量即爲咱們在上面寫好的 modules 對象 modules[moduleName].call(module.exports, module, module.exports,require); //模塊導入完成 module.loaded = true; //將模塊的exports返回 return module.exports; }
最後只要把咱們上面寫好的 modules
對象以當即執行函數的形式傳入這個 require
函數就能夠了,如下是完整的代碼:
(function(modules){ //這個對象用於儲存已導入的模塊 var installedModules = {}; function require(moduleName) { //若是模塊已經導入,那麼直接返回它的exports if(installedModules[moduleName]){ return installedModules[moduleName].exports; } //模塊初始化 var module = installedModules[moduleName] = { exports: {}, name: moduleName, loaded: false }; //執行模塊內部的代碼,這裏的 modules 變量即爲咱們在上面寫好的 modules 對象 modules[moduleName].call(module.exports, module, module.exports,require); //模塊導入完成 module.loaded = true; //將模塊的exports返回 return module.exports; } //入口函數 return require("entry"); })({ "entry": function(module, exports, require, global){ //index.js var module1 = require("./module1"); var module2 = require("./module2"); module1.foo(); module2.foo(); function hello(){ console.log("Hello!"); } module.exports = hello; }, "./module1": function(module, exports, require, global){ var module2 = require("./module2"); console.log("initialize module1"); console.log("this is module2.foo() in module1:"); module2.foo(); console.log("\n") module.exports = { foo: function(){ console.log("module1 foo !!!"); } }; }, "./module2": function(module, exports, require, global){ console.log("initialize module2"); module.exports = { foo: function(){ console.log("module2 foo !!!"); } }; } });
事實上,咱們短短的這幾十行代碼模仿了 Webpack 的部分實現。但咱們依然在使用諸如 "./module1"
這樣的字符串做爲模塊的惟一識別碼,這是一個明顯的缺陷,存在多層級文件時,這個名稱很容易衝突。
在 Browserify 或 Webpack 這樣的生產級工具裏,通常使用數字做爲函數的惟一識別碼,例如它可能會把(以 Webpack 爲例):
var module1 = require("./module1");
編譯成:
var module1 = __webpack_require__(1);
咱們在這裏實現了一個最簡單的 CommonJS 標準的執行器,接下來的文章中咱們會作如下事情:
一、實現 global 變量
二、用 moduleID 替代 moduleName
三、寫一個命令行小工具
四、支持 node_modules 和多層級文件