前端需要知道的 依賴注入(Dependency Injection, DI)
- 2019 年 12 月 4 日
- 筆記
本文作者:IMWeb 黎清龍 原文出處:IMWeb社區 未經同意,禁止轉載
前端需要知道的 依賴注入(Dependency Injection, DI)
1. 前言
XX庫實現了依賴注入,哇塞,好牛X呀~~~
切,依賴注入的實現那麼簡單,不就一個map + 函數參數解析而已嗎?
可是,你真的了解 依賴注入(Dependency Injection, DI)
嗎?
本文將詳細解釋什麼是依賴注入,並解釋屬於前端的依賴注入
注意
- 本文專門為前端同學解釋什麼是依賴注入,文中例子也是js,非前端同學可以選擇繞道
- 已經知道依賴注入的同學也可以繞道
2. 什麼是 依賴注入
2.1. 它是模式
首先,依賴注入是一個設計模式,因為它解決的是一類問題
2.2. 理解它的作用域
要知道依賴注入是解決什麼問題,最好先了解一個原則:
依賴倒轉原則(Dependence Inversion Priciple, DIP)
提倡:
- 高層模塊不應該依賴低層模塊。兩個都應該依賴抽象
- 抽象不應該依賴細節,細節應該依賴抽象
- 針對接口編程,不要針對實現編程
在編程時,我們對系統進行模塊化,它們之間有依賴,比如模塊A依賴模塊B
那麼依據DIP,模塊A應該依賴模塊B的接口,而不應該依賴模塊B的實現
這樣做的好處就不詳敘了
下圖描述了這個關係圖:
這裡需要注意一點,雖然模塊A只依賴接口編程,但在運行的時候,它還是需要有一個具體的模塊來負責模塊A需要的功能的,所以模塊A在【運行時】是需要一個【真的】模塊B,而不是它的接口
所以上圖中,Module和Interface之間的線是包含,而不是關聯
也就是說,模塊A在【運行時】需要有一個接口的實現模塊作為它的屬性
那麼這個實現模塊怎麼來?它是怎麼初始化,然後怎麼傳給模塊A的?
解決這個問題的就是依賴注入
,這就是它的作用域
上面的結構圖再擴展一下就是非常著名的設計模式——橋接
2.3. 前端的依賴注入
對於前端來說,很少有抽象,更別說有接口了
但是,依賴注入卻是一直都存在,只是許多同學沒有認出來而已
下面來看看前端最常見的一個依賴注入:
// moduleA.js define('moduleA', ['moduleB'], function(moduleB) { return { init: function() { this.I_need = ModuleB.someFun(); } }; });
這是個很普通的代碼,太正常了,我們每天都會寫這些代碼,即使define包裹可能是構建幫我們寫的
還記得前面說的依賴注入的作用域,它只做兩件事:
- 初始化被依賴的模塊
- 注入到依賴模塊中
這個時候應該知道了,define就是做這些事的:
- 它負責初始化moduleB
- 它通過函數參數的形式注入到moduleA裏面去
3. 依賴注入的作用
為什麼需要依賴注入?它的作用和意義是什麼?
關於這個,我們還是要從依賴注入做了什麼事來探索:
1. 初始化被依賴的模塊
如果不通過依賴注入模式來初始化被依賴的模塊,那麼就要依賴模塊自己去初始化了
那麼問題來了:依賴模塊就耦合了被依賴模塊的初始化信息了
2. 注入到依賴模塊中
被依賴模塊已經被其他管理器初始化了,那麼依賴模塊要怎麼獲取這個模塊呢?
有兩種方式:
- 自己去問
- 別人主動給你
沒用依賴注入模式的話是1,用了之後就是2
想想,你需要某個東西的時候,你去找別人要,你需要提供別人什麼信息?
最簡單的就是那個東西叫什麼,是的,正式一點,你需要一個名稱
沒錯,方式1的問題是:依賴模塊耦合了被依賴模塊的【名稱】還有那個【別人】
而方式2解決了這個問題,讓依賴模塊只依賴需要的模塊的接口
可以看到,注入的兩個方式的主動權是相反的 因此,依賴注入(Dependency Injection, DI) 有時候也被稱為 控制反轉(Inversion of Control, IoC) 它們不是一個東西,有興趣的同學可以深入學習
3.1. 代碼解釋
文字比較抽象,那麼我們用代碼來說明依賴注入的作用以及好處
// config.js require.config = { path: { jquery: 'common/jquery' } }; // moduleA.js define('moduleA', ['jquery'], function($) { return { init: function() { this.$dom = $('#id'); } }; });
用過模塊加載器的都知道,一般我們可以配置怎樣去獲取模塊的定義,也就是模塊的實現代碼
一般是通過配置文件的形式
上面的代碼很簡單,moduleA依賴了jquery庫,在模塊加載器中,我們配置了jquery模塊在哪裡初始化
可以看到,jquery模塊的代碼是在本地的
現在,不管什麼原因,我們想要使用一個線上代碼庫版本的jquery,怎麼辦?簡單:
// config.js require.config = { path: { jquery: 'http://path/to/online/jquery' } };
可以看到,我們只需要修改模塊加載器的配置就可以了
這個配置就是被依賴模塊(jquery)的初始化信息
這個就是依賴注入的第一個好處:依賴模塊與被依賴模塊的初始化信息解耦
這個例子也是很常見的代碼:
// moduleA.js var $ = require('jquery'); module.exports = { init: function() { this.$dom = $('#id'); } };
聰明的同學已經看到問題在哪裡了,沒錯,這個模塊依賴了被依賴模塊的名字
這裡會有兩個問題:
- 模塊重名問題,還記得那些年我們給模塊起名字的日子嗎?
- 改變模塊依賴方式
像jquery這種庫,有許多都是最先加載,並且全局使用的:
// moduleA.js module.exports = { init: function() { this.$dom = $('#id'); } };
對於這種情況,我們的組件代碼就得改動了
不同的模塊依賴方式給通用組件的實現造成了很大的困擾
為了不改動組件代碼,通常我們這樣做:
// jquery.js module.exports = window.$;
當然,這是題外話了
從上面的例子應該可以知道,依賴注入幫助我們解決了依賴模塊對被依賴模塊的初始化解耦
4. 依賴注入模式的實現細節
4.1. 組件容器(模塊管理器)
一般依賴注入模式都實現在某個容器中,在前端我們可以管它為模塊管理器
組件容器負責管理所有的組件,管理他們的初始化,以及依賴,並提供接口獲取組件
通常容器會把組件的初始化信息聚集在某個配置文件中,比如xml文件或者json文件等
這樣做的好處是可以很輕易的修改組件的初始化信息,並且可以實現組件的熱啟動
對於前端來說,模塊管理器,比如requireJs,就是負責模塊的初始化工作的
但是模塊加載器的重心不是依賴注入
因此這裡提供一個依賴注入容器的簡單例子:
// injector // APP Instance -- Global & Singleton var injector = { set: function(name, factory) { // name: the dependency name // factory: can be a factory function // or just a value }, get: function(name) {} }; // a.js injector.set('env', 'dev'); // b.js injector.set('b', function() { return { sayYes: function() { console.log('Yes!'); }, sayNo: function() { console.log('No!'); } }; }); // c.js injector.set('c', function(env, b) { if (env === 'dev') { b.sayYes(); } else { b.sayNo(); } });
實現起來並沒有難點,injector其實就只是個map
用factory函數的好處是可以延遲模塊的初始化
另外一個難點是要讀取函數的形參名,但是我們也可以這樣改來避開這個難點:
// injector var injector = { set: function(name, array) { // name: the dependency name }, get: function(name) {} }; // c.js injector.set('c', ['env', 'b', function(env, b) { if (env === 'dev') { b.sayYes(); } else { b.sayNo(); } }]);
4.2. 初始化
可以看到模塊管理器實際上只是一個容器
現在我們需要一個初始化模塊,下面提供一個小栗子:
// initializer.js function initializer() { // to load the module in initializer.config } initializer.config = { initList: ['./a.js', './b.js', 'http://path/to/other/module.js'], map: { 'jquery': 'http://path/to/online/jquery.js' } }; initializer();
可以看到,如果文件內容本身就有註冊模塊的代碼的話,initializer只需要加載js文件即可,比如上面的a.js和b.js文件
當然也可以加載線上資源
如果文件內容沒有註冊模塊的代碼的話,就需要initializer自己幫忙註冊了
比如栗子中的jquery
如果系統是服務器端的nodejs代碼的話,就可以實現模塊的熱插拔了
4.3. 注入方式
被依賴模塊怎樣賦值給依賴模塊,主要有三種方式
4.3.1. 構造函數注入
前面define和angular的依賴注入都是使用構造函數的注入方式,如下:
// define define('moduleA', ['moduleB'], function(moduleB) { return { init: function() { this.I_need = ModuleB.someFun(); } }; }); // anguler someModule.controller('MyController', ['$scope', 'greeter', function($scope, greeter) { // ... }]);
4.3.2. setter注入
直接上例子:
// moduleA.js var moduleA = { do: function() { this.helper.doSomething(); }, setHelper: function(helper) { this.helper = helper; } }; // initializer.js function initializer() { // ... moduleA.setHelper(new Helper()); }
4.3.3. 接口注入
接口注入主要是把注入過程抽象成接口的形式,讓注入方式可以被輕易擴展
在前端並不怎麼使用接口,因此這種注入方式就不詳述
5. 對比——服務定位模式 (Service Locator, SL)
讀者可能對服務定位模式不太了解,但是看了下面的代碼就知道了
var fs = require('fs'); var path = require('path'); var moduleB = require('./moduleB'); var moduleC = require('path/to/moduleC');
沒錯,require就是一個服務定位模式
所謂的服務定位模式就是把所有服務(模塊)資源的管理都放到一個定位者那裡
所有需要服務的模塊都找它要就行了,就是這麼簡單
服務定位模式也能解決依賴注入的作用域問題
服務定位者負責初始化服務,它也提供服務資源
只是依賴注入是被動,服務定位模式需要模塊自己主動去請求,詳見【3. 依賴注入的作用】
對於前端來說,
服務定位模式肯定更常見,它的優點就是簡單
,缺點是所有模塊都需要依賴定位者
依賴注入模式的優點是控制反轉,更利於組件化
,缺點是不是前端的基礎能力(誰讓require是基礎。。。)
6. 結語
依賴注入模式並不神秘,也不是什麼高大上
Java時代的Spring就已經把依賴注入推向頂峰
本文只想向前端同學傳達:依賴注入的思想非常值得學習