browserify執行原理分析

Cson發表於2014-10-22

目前對於前端工程師而言,如果只針對瀏覽器編寫程式碼,那麼很簡單,只需要在頁面的script指令碼中引入所用js就可以了。

但是某些情況下,我們可能需要在服務端也跑一套類似的邏輯程式碼,考慮如下這些情景(以node作為後端為例):

1.spa的應用,需要同時支援服務端直出頁面以及客戶端pjax拉取資料渲染,客戶端和伺服器公用一套渲染模板並執行大部分類似的邏輯。

2.一個通過websocket對戰的遊戲,客戶端和服務端可能需要進行類似的邏輯計算,兩套程式碼分別用於對使用者客戶端的展示以及服務端實際數值的計算。

這些情況下,很可能希望我們客戶端程式碼的邏輯能夠同時無縫執行在服務端。

解決方法1:UMD

一種解決方法是使用UMD的方式,前端使用requirejs,同時相容nodejs的情況,例如:

(function (window, factory) {
    if (typeod exports === 'object') {

        module.exports = factory();
    } else if (typeof define === 'function' && define.amd) {

        define(factory);
    } else {

        window.eventUtil = factory();
    }
})(this, function () {
    //module ...
});

解決方案2:使用browerify,使程式碼能同時執行於服務端和瀏覽器端。

什麼是browserify?

Browserify 可以讓你使用類似於 node 的 require() 的方式來組織瀏覽器端的 Javascript 程式碼,通過預編譯讓前端 Javascript 可以直接使用 Node NPM 安裝的一些庫。

例如我們可以這樣寫js,同時執行在服務端和瀏覽器中:

mo2.js:

exports.write2 = function(){
    //write2
}

mo.js:

var t = require("./mo2.js");
exports.write = function(){
    t.write2();
}

test.js:

var mo = require("./mo.js");
mo.write();

程式碼可以完全已node的形式編寫。

原理分析:

總體過程其實可以分為以下幾個步驟:

階段1:預編譯階段

1.從入口模組開始,分析程式碼中require函式的呼叫

2.生成AST

3.根據AST找到每個模組require的模組名

4.得到每個模組的依賴關係,生成一個依賴字典

5.包裝每個模組(傳入依賴字典以及自己實現的export和require函式),生成用於執行的js

階段2:執行階段

從入口模組開始執行,遞迴執行所require的模組,得到依賴物件。

具體步驟分析:

1.從入口模組開始,分析程式碼中require函式的呼叫

由於瀏覽器端並沒有原生的require函式,所以所有require函式都是需要我們自己實現的。因此第一步我們需要知道一個模組的程式碼中,哪些地方用了require函式,依賴了什麼模組。

browerify實現的原理是為程式碼檔案生成AST,然後根據AST找到require函式依賴的模組。

2.生成AST

檔案程式碼:

var t = require("b");
t.write();

生成的js描述的AST為:

{
    "type": "Program",
    "body": [
        {
            "type": "VariableDeclaration",
            "declarations": [
                {
                    "type": "VariableDeclarator",
                    "id": {
                        "type": "Identifier",
                        "name": "t"
                    },
                    "init": {
                        "type": "CallExpression",
                        "callee": {
                            "type": "Identifier",
                            "name": "require"
                        },
                        "arguments": [
                            {
                                "type": "Literal",
                                "value": "b",
                                "raw": "\"b\""
                            }
                        ]
                    }
                }
            ],
            "kind": "var"
        },
        {
            "type": "ExpressionStatement",
            "expression": {
                "type": "CallExpression",
                "callee": {
                    "type": "MemberExpression",
                    "computed": false,
                    "object": {
                        "type": "Identifier",
                        "name": "t"
                    },
                    "property": {
                        "type": "Identifier",
                        "name": "write"
                    }
                },
                "arguments": []
            }
        }
    ]
}

可以看到我們程式碼中呼叫的require函式,對應AST中的物件為上面紅字部分。

3.根據AST找到每個模組require的模組名

生成了AST之後,我們下一部就需要根據AST找到require依賴的模組名了。再次看看上面生成的AST物件,要找到require的模組名,實質上就是要:

找到type為callExpression,callee的name為require所對應的第一個argument的value。

關於生成js描述的AST以及解析AST物件,可以參考:

https://github.com/ariya/esprima 程式碼生成AST

https://github.com/substack/node-detective 從AST中提取reqiure

https://github.com/Constellation/escodegen AST生成程式碼

4.得到每個模組的依賴關係,生成一個依賴字典

從上面的步驟,我們已經可以獲取到每個模組的依賴關係,因此可以生成一個以id為鍵的模組依賴字典,browerify生成的字典示例如下(根據之前的範例程式碼生成):

{
    1:[
    function(require,module,exports){
        var t = require("./mo2.js");
        exports.write = function(){
            document.write("test1");
            t.write2();
        }
    },
    {"./mo2.js":2}
    ],
    2:[
    function(require,module,exports){
        exports.write2 = function(){
            document.write("=2=");
        }
    },
    {}
    ],
    3:[
    function(require,module,exports){
        var mo = require("./mo.js");
        mo.write();
    },
    {"./mo.js":1}
    ]}

字典記錄了擁有那些模組,以及模組各自依賴的模組。

5.包裝每個模組(傳入依賴字典以及自己實現的export和require函式),生成用於執行的js

擁有了上面的依賴字典之後,我們相當於知道了程式碼中的依賴關係。為了讓程式碼能執行,最後一步就是實現瀏覽器中並不支援的export和require。因此我們需要對原有的模組程式碼進行包裝,就像上面的程式碼那樣,外層會傳入自己實現的export和require函式。

然而,應該怎樣實現export和require呢?

export很簡單,我們只要建立一個物件作為該模組的export就可以。

對於require,其實我們已經擁有了依賴字典,所以要做的也很簡單了,只需要根據傳入的模組名,根據依賴字典找到所依賴的模組函式,然後執行,一直重複下去(遞迴執行這個過程)。

在browerify生成的js中,會新增以下require的實現程式碼,並傳遞給每個模組函式:

(function e(t,n,r){
    function s(o,u){
        if(!n[o]){
            if(!t[o]){
                var a=typeof require=="function"&&require;
                if(!u&&a)
                    return a(o,!0);
                if(i)
                    return i(o,!0);

                var f=new Error("Cannot find module '"+o+"'");
                throw f.code="MODULE_NOT_FOUND",f
            }
            var l=n[o]={exports:{}};
            t[o][0].call(l.exports,function(e){
                var n=t[o][1][e];
                return s(n?n:e)
            },l,l.exports,e,t,n,r)
        }
        return n[o].exports
    }
    var i=typeof require=="function"&&require;
    for(var o=0;o<r.length;o++)
        s(r[o]);
    return s
})

我們主要關注的紅字部分,其中t是傳入的依賴字典(之前提到的那塊程式碼),n是一個空物件,用於儲存所有新建立的模組(export物件),對比之前的依賴字典來看就比較清晰了:

首先我們建立module物件(包含一個空物件export),並分別把module和export傳入模組函式作為瀏覽器自己實現的module和export,然後,我們自己實現一個require函式,該函式獲取模組名,並遞迴尋找依賴的模組執行,最後獲取到所有被依賴到的模組物件,這個也是browerify生成的js在執行中的整個執行過程。

相關文章