JavaScript中依賴注入詳細解析

krasimirtsonev發表於2015-03-15

計算機程式設計的世界其實就是一個將簡單的部分不斷抽象,並將這些抽象組織起來的過程。JavaScript也不例外,在我們使用JavaScript編寫應用時,我們是不是都會使用到別人編寫的程式碼,例如一些著名的開源庫或者框架。隨著我們專案的增長,我們需要依賴的模組變得越來越多,這個時候,如何有效的組織這些模組就成了一個非常重要的問題。依賴注入解決的正是如何有效組織程式碼依賴模組的問題。你可能在一些框架或者庫種聽說過“依賴注入”這個詞,比如說著名的前端框架AngularJS,依賴注入就是其中一個非常重要的特性。但是,依賴注入根本就不是什麼新鮮玩意,它在其他的程式語言例如PHP中已經存在已久。同時,依賴注入也沒有想象種那樣複雜。在本文中,我們將一起來學習JavaScript中的依賴注入的概念,深入淺出的講解如何編寫“依賴注入風格”的程式碼。

目標設定

假設我們現在擁有兩個模組。第一個模組的作用是傳送Ajax請求,而第二個模組的作用則是用作路由。

var service = function() {
    return { name: 'Service' };
}
var router = function() {
    return { name: 'Router' };
}

這時,我們編寫了一個函式,它需要使用上面提到的兩個模組:

var doSomething = function(other) {
    var s = service();
    var r = router();
};

在這裡,為了讓我們的程式碼變得有趣一些,這個引數需要多接收幾個引數。當然,我們完全可以使用上面的程式碼,但是無論從哪個方面來看上面的程式碼都略顯得不那麼靈活。要是我們需要使用的模組名稱變為ServiceXML或者ServiceJSON該怎麼辦?或者說如果我們基於測試的目的想要去使用一些假的模組改怎麼辦。這時,我們不能僅僅去編輯函式本身。因此我們需要做的第一件事情就是將依賴的模組作為引數傳遞給函式,程式碼如下所示:

var doSomething = function(service, router, other) {
    var s = service();
    var r = router();
};

在上面的程式碼中,我們完全傳遞了我們所需要的模組。但是這又帶來了一個新的問題。假設我們在程式碼的哥哥部分都呼叫了doSomething方法。這時,如果我們需要第三個依賴項該怎麼辦。這個時候,去編輯所有的函式呼叫程式碼並不是一個明智的方法。因此,我們需要一段程式碼來幫助我們做這件事情。這就是依賴注入器試圖去解決的問題。現在我們可以來定下我們的目標了:

  • 我們應該能夠去註冊依賴項
  • 依賴注入器應該接收一個函式,然後返回一個能夠獲取所需資源的函式
  • 程式碼不應該複雜,而應該簡單友好
  • 依賴注入器應該保持傳遞的函式作用域
  • 傳遞的函式應該能夠接收自定義的引數,而不僅僅是被描述的依賴項

requirejs/AMD方法

或許你已經聽說過了大名鼎鼎的requirejs,它是一個能夠很好的解決依賴注入問題的庫:

define(['service', 'router'], function(service, router) {       
    // ...
});

requirejs的思想是首先我們應該去描述所需要的模組,然後編寫你自己的函式。其中,引數的順序很重要。假設我們需要編寫一個叫做injector的模組,它能夠實現類似的語法。

var doSomething = injector.resolve(['service', 'router'], function(service, router, other) {
    expect(service().name).to.be('Service');
    expect(router().name).to.be('Router');
    expect(other).to.be('Other');
});
doSomething("Other");

在繼續往下之前,需要說明的一點是在doSomething的函式體中我們使用了expect.js這個斷言庫來確保程式碼的正確性。這裡有一點類似TDD(測試驅動開發)的思想。

現在我們正式開始編寫我們的injector模組。首先它應該是一個單體,以便它能夠在我們應用的各個部分都擁有同樣的功能。

var injector = {
    dependencies: {},
    register: function(key, value) {
        this.dependencies[key] = value;
    },
    resolve: function(deps, func, scope) {

    }
}

這個物件非常的簡單,其中只包含兩個函式以及一個用於儲存目的的變數。我們需要做的事情是檢查deps陣列,然後在dependencies變數種尋找答案。剩餘的部分,則是使用.apply方法去呼叫我們傳遞的func變數:

resolve: function(deps, func, scope) {
    var args = [];
    for(var i=0; i<deps.length, d=deps[i]; i++) {
        if(this.dependencies[d]) {
            args.push(this.dependencies[d]);
        } else {
            throw new Error('Can\'t resolve ' + d);
        }
    }
    return function() {
        func.apply(scope || {}, args.concat(Array.prototype.slice.call(arguments, 0)));
    }        
}

如果你需要指定一個作用域,上面的程式碼也能夠正常的執行。

在上面的程式碼中,Array.prototype.slice.call(arguments, 0)的作用是將arguments變數轉換為一個真正的陣列。到目前為止,我們的程式碼可以完美的通過測試。但是這裡的問題是我們必須要將需要的模組寫兩次,而且不能夠隨意排列順序。額外的引數總是排在所有的依賴項之後。

反射(reflection)方法

根據維基百科中的解釋,反射(reflection)指的是程式可以在執行過程中,一個物件可以修改自己的結構和行為。在JavaScript中,簡單來說就是閱讀一個物件的原始碼並且分析原始碼的能力。還是回到我們的doSomething方法,如果你呼叫doSomething.toString()方法,你可以獲得下面的字串:

"function (service, router, other) {
    var s = service();
    var r = router();
}"

這樣一來,只要使用這個方法,我們就可以輕鬆的獲取到我們想要的引數,以及更重要的一點就是他們的名字。這也是AngularJS實現依賴注入所使用的方法。在AngularJS的程式碼中,我們可以看到下面的正規表示式:

/^function\s*[^\(]*\(\s*([^\)]*)\)/m

我們可以將resolve方法修改成如下所示的程式碼:

resolve: function() {
    var func, deps, scope, args = [], self = this;
    func = arguments[0];
    deps = func.toString().match(/^function\s*[^\(]*\(\s*([^\)]*)\)/m)[1].replace(/ /g, '').split(',');
    scope = arguments[1] || {};
    return function() {
        var a = Array.prototype.slice.call(arguments, 0);
        for(var i=0; i<deps.length; i++) {
            var d = deps[i];
            args.push(self.dependencies[d] && d != '' ? self.dependencies[d] : a.shift());
        }
        func.apply(scope || {}, args);
    }        
}

我們使用上面的正規表示式去匹配我們定義的函式,我們可以獲取到下面的結果:

["function (service, router, other)", "service, router, other"]

此時,我們只需要第二項。但是一旦我們去除了多餘的空格並以,來切分字串以後,我們就得到了deps陣列。下面的程式碼就是我們進行修改的部分:

var a = Array.prototype.slice.call(arguments, 0);
...
args.push(self.dependencies[d] && d != '' ? self.dependencies[d] : a.shift());

在上面的程式碼中,我們遍歷了依賴專案,如果其中有缺失的專案,如果依賴專案中有缺失的部分,我們就從arguments物件中獲取。如果一個陣列是空陣列,那麼使用shift方法將只會返回undefined,而不會丟擲一個錯誤。到目前為止,新版本的injector看起來如下所示:

var doSomething = injector.resolve(function(service, other, router) {
    expect(service().name).to.be('Service');
    expect(router().name).to.be('Router');
    expect(other).to.be('Other');
});
doSomething("Other");

在上面的程式碼中,我們可以隨意混淆依賴項的順序。

但是,沒有什麼是完美的。反射方法的依賴注入存在一個非常嚴重的問題。當程式碼簡化時,會發生錯誤。這是因為在程式碼簡化的過程中,引數的名稱發生了變化,這將導致依賴項無法解析。例如:

var doSomething=function(e,t,n){var r=e();var i=t()}

因此我們需要下面的解決方案,就像AngularJS中那樣:

var doSomething = injector.resolve(['service', 'router', function(service, router) {

}]);

這和最一開始看到的AMD的解決方案很類似,於是我們可以將上面兩種方法整合起來,最終程式碼如下所示:

var injector = {
    dependencies: {},
    register: function(key, value) {
        this.dependencies[key] = value;
    },
    resolve: function() {
        var func, deps, scope, args = [], self = this;
        if(typeof arguments[0] === 'string') {
            func = arguments[1];
            deps = arguments[0].replace(/ /g, '').split(',');
            scope = arguments[2] || {};
        } else {
            func = arguments[0];
            deps = func.toString().match(/^function\s*[^\(]*\(\s*([^\)]*)\)/m)[1].replace(/ /g, '').split(',');
            scope = arguments[1] || {};
        }
        return function() {
            var a = Array.prototype.slice.call(arguments, 0);
            for(var i=0; i<deps.length; i++) {
                var d = deps[i];
                args.push(self.dependencies[d] && d != '' ? self.dependencies[d] : a.shift());
            }
            func.apply(scope || {}, args);
        }        
    }
}

這一個版本的resolve方法可以接受兩個或者三個引數。下面是一段測試程式碼:

var doSomething = injector.resolve('router,,service', function(a, b, c) {
    expect(a().name).to.be('Router');
    expect(b).to.be('Other');
    expect(c().name).to.be('Service');
});
doSomething("Other");

你可能注意到了兩個逗號之間什麼都沒有,這並不是錯誤。這個空缺是留給Other這個引數的。這就是我們控制引數順序的方法。

結語

在上面的內容中,我們介紹了幾種JavaScript中依賴注入的方法,希望本文能夠幫助你開始使用依賴注入這個技巧,並且寫出依賴注入風格的程式碼。

相關文章