手寫一個CommonJS打包工具(一)

本文首發於知乎專欄:
http://zhuanlan.zhihu.com/starkwang前端


CommonJS 是一個流行的前端模塊化規範,也是目前 NodeJS 以及其模塊託管倉庫 npm 使用的規範,但目前暫無瀏覽器支持 CommonJS 。要想讓瀏覽器用上這些模塊,必須轉換格式。node

這個系列的文章,咱們會一步步完成一個基於 CommonJS 的打包工具,相似於一個簡單版的 Browserify 或者 Webpack 。webpack


1、原理

與 NodeJS 環境不一樣,瀏覽器中不支持 CommonJS 的主要緣由是缺乏瞭如下幾個環境變量:web

  • modulenpm

  • exports瀏覽器

  • require模塊化

  • global函數

換句話說,打包器的原理就是模擬這四個變量的行爲。工具

好比咱們有一個index.js文件,依賴了module1module2兩個模塊,而且module1依賴module2ui

//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 !!!");
    }
};

把它放入一個匿名函數內,經過這個匿名函數注入 requiremodulesexportglobal變量(咱們暫時不實現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);

2、小結

咱們在這裏實現了一個最簡單的 CommonJS 標準的執行器,接下來的文章中咱們會作如下事情:

一、實現 global 變量

二、用 moduleID 替代 moduleName

三、寫一個命令行小工具

四、支持 node_modules 和多層級文件

相關文章
相關標籤/搜索