前端需要知道的 依賴注入(Dependency Injection, DI)

  • 2019 年 12 月 4 日
  • 筆記

本文作者:IMWeb 黎清龍 原文出處:IMWeb社區 未經同意,禁止轉載

前端需要知道的 依賴注入(Dependency Injection, DI)

1. 前言

XX庫實現了依賴注入,哇塞,好牛X呀~~~

切,依賴注入的實現那麼簡單,不就一個map + 函數參數解析而已嗎?

可是,你真的了解 依賴注入(Dependency Injection, DI) 嗎?

本文將詳細解釋什麼是依賴注入,並解釋屬於前端的依賴注入

注意

  1. 本文專門為前端同學解釋什麼是依賴注入,文中例子也是js,非前端同學可以選擇繞道
  2. 已經知道依賴注入的同學也可以繞道

2. 什麼是 依賴注入

2.1. 它是模式

首先,依賴注入是一個設計模式,因為它解決的是一類問題

2.2. 理解它的作用域

要知道依賴注入是解決什麼問題,最好先了解一個原則:

依賴倒轉原則(Dependence Inversion Priciple, DIP)提倡:

  1. 高層模塊不應該依賴低層模塊。兩個都應該依賴抽象
  2. 抽象不應該依賴細節,細節應該依賴抽象
  3. 針對接口編程,不要針對實現編程

在編程時,我們對系統進行模塊化,它們之間有依賴,比如模塊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包裹可能是構建幫我們寫的

還記得前面說的依賴注入的作用域,它只做兩件事:

  1. 初始化被依賴的模塊
  2. 注入到依賴模塊中

這個時候應該知道了,define就是做這些事的:

  1. 它負責初始化moduleB
  2. 它通過函數參數的形式注入到moduleA裏面去

3. 依賴注入的作用

為什麼需要依賴注入?它的作用和意義是什麼?

關於這個,我們還是要從依賴注入做了什麼事來探索:

1. 初始化被依賴的模塊

如果不通過依賴注入模式來初始化被依賴的模塊,那麼就要依賴模塊自己去初始化了

那麼問題來了:依賴模塊就耦合了被依賴模塊的初始化信息了

2. 注入到依賴模塊中

被依賴模塊已經被其他管理器初始化了,那麼依賴模塊要怎麼獲取這個模塊呢?

有兩種方式:

  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');      }  };

聰明的同學已經看到問題在哪裡了,沒錯,這個模塊依賴了被依賴模塊的名字

這裡會有兩個問題:

  1. 模塊重名問題,還記得那些年我們給模塊起名字的日子嗎?
  2. 改變模塊依賴方式

像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就已經把依賴注入推向頂峰

本文只想向前端同學傳達:依賴注入的思想非常值得學習