在 React 中實現 Angular 的依賴注入

翻譯自implementing Angular's Dependency Injection in React. Understanding Element Injectors.javascript

最近我一直在寫關於Angular的博客,這不是偶然的! Angular是一個了不得的框架,爲前端技術帶來了大量創新,背後有一個偉大的社區。 與此同時,我正在開展的項目有各類不一樣的需求,有時我須要考慮不一樣的選擇。html

我過去使用的另外一項偉大技術是React。 我不想將它與Angular進行比較; 我敢確定,當其中一個比另外一個更適合時,有各類各樣的狀況,反之亦然。 我尊重Angular和React的理念,我喜歡看他們如何推進Web向前發展!前端

這篇博文與我最近作的一個有趣的實驗有關—— 在 React 中實現 Angular 的依賴注入機制。 在個人 GitHub 賬戶上能夠找到一個包含 react-dom 的分支的演示。java

React DI

免責聲明

對於下面的帖子,我並非在暗示在 React 中使用 Angular 的 DI 是一個好主意仍是一個壞主意; 這徹底取決於最適合你的編程風格。 這裏的例子不是我在生產中使用的,我不建議你這樣作,由於它沒有通過很好的測試,而且直接修改了 React 的內部。react

最後,我並非暗示 Angular 的依賴注入是咱們能夠用來編寫徹底解耦的代碼的惟一方法,或者咱們須要面向對象的範例來作到這一點。 若是咱們在設計過程當中投入足夠的精力,咱們能夠在任何範例和框架中編寫高質量的代碼。git

這篇文章是基於我在週日下雨的晚上作的一個小實驗。 這篇文章只是爲了學習。 它能夠幫助你理解依賴注入如何用於現代用戶界面的開發,最終,讓你對 React 和 Angular 的內部結構有一些瞭解。github

依賴注入入門

若是你已經熟悉依賴注入的概念,以及如何使用它,你能夠直接跳到 「Element injectors」.web

依賴注入是一個強大的工具,它帶來了不少好處。 例如,DI 有助於遵循單一責任原則(Single Responsibility Principle,SRP) ,它不會將給定的實體與其依賴關係的實例化邏輯耦合起來。 開閉原則是另外一個 DI 搖滾的地方! 咱們可使給定的類僅依賴於抽象接口,經過配置它的注入器,咱們能夠傳遞不一樣的抽象實現。typescript

接下來,讓咱們來看看依賴反轉原則 是怎麼說的:npm

A.高級模塊不該該依賴於低級模塊。 二者都應該依賴於抽象。

B.抽象不該該依賴於細節。 細節應該依賴於抽象。

雖然 DI 不直接強制執行它,但它可使咱們傾向於編寫遵循這一原則的代碼。

幾天前,我發佈了一個名爲 injection-js 的庫。 這是一個提取的Angular的依賴注入機制。 因爲 injection-js 來自 Angular 的源代碼,它通過了良好的測試而且已經成熟,所以您能夠嘗試一下!

$ npm i injection-js --save
複製代碼

使用依賴注入

如今,讓咱們看看如何使用這個庫! 但在此以前,讓咱們熟悉其背後的核心概念。 injection-js (和 Angular)的依賴注入的是 injector。 它負責爲單個依賴項的實例化保存不一樣的providers。 這些依賴項被稱爲providers。 對於每一個providers,咱們都有一個相關的token。 咱們能夠將標記看做單個依賴關係和提供者的標識符(提供者和依賴關係之間有1:1的映射或雙映射)。 咱們使用注入器的標記來請求任何依賴項的實例。

下面是一個例子:

// 咱們可使用'@angular/core' 導入相同的代碼。
import { ReflectiveInjector, Injectable } from 'injection-js';

class Http {}

@Injectable()
class UserService {
  constructor(private http: Http) {}
}

const injector = ReflectiveInjector.resolveAndCreate([
  { provide: Http, useClass: Http },
  { provide: UserService, useClass: UserService },
]);

injector.get(UserService);
複製代碼

下面的示例使用 injection-js,但咱們也可使用@angular/core。 以上咱們引入 ReflectiveInjector@injectableReflectiveinjector 有一個名爲 resolveAndCreate 的工廠方法,它容許咱們經過傳遞一組providers來建立一個injector。 在這種狀況下,咱們爲類 HttpUserService 提供providers。

咱們經過設置提供者的提供屬性的值來聲明與給定provide關聯的token。 上面咱們指示注入器經過直接調用它們的構造函數來實例化各個依賴項。 這意味着,若是咱們想得到一個 Http 實例,注入器將返回new Http()。 若是咱們想得到一個UserService,注入器將查看其構造函數的參數,並首先建立一個 Http 實例(或者使用一個已經存在的實例,若是它已經可用的話)。 以後,它可使用已經存在的 Http 實例調用 UserService 的構造函數。

最後,decorator@Injectable 什麼也不作。 它只是強制 TypeScript 生成關於 UserService 接受的依賴項類型的元數據。

注意,爲了讓 TypeScript 生成這樣的元數據,咱們須要將 tsconfig.json 中的 emitDecoratorMetadata 屬性設置爲 true

因爲配置注入器的語法看起來有點多餘,咱們可使用如下提供者的定義:

const injector = ReflectiveInjector.resolveAndCreate([
  Http, UserService
]);
複製代碼

在某些狀況下,咱們要聲明的依賴項僅僅是須要注入的值。 例如,若是咱們想注入一個常量,使用該常量的構造函數做爲標記是不方便的。 在這種狀況下,咱們能夠將該標記設置爲任何其餘值 -remember- 該標記只不過是一個標識符:

const BUFFER_SIZE = 'buffer-size';

const injector = ReflectiveInjector.resolveAndCreate([
  Socket,
  { provide: BUFFER_SIZE, useValue: 42 }
]);
複製代碼

在上面的示例中,咱們爲BUFFER_SIZE 標記建立了一個提供者。 咱們聲明,一旦須要token BUFFER_SIZE,咱們但願注入器返回值42。 下面是一個例子:

injector.get(BUFFER_SIZE); // 42
複製代碼

在上面的例子中還有兩個細節:

  1. 若是咱們與另外一個名爲buffer-size的令牌發生名稱衝突怎麼辦?
  2. 若是它的類型不明確,咱們應該如何聲明給定類接受BUFFER_SIZE做爲依賴。

We can handle the first problem by using OpaqueToken. This way our BUFFER_SIZE definition will be: 咱們可使用 OpaqueToken 來處理第一個問題。 這樣咱們的BUFFER_SIZE定義就是:

const BUFFER_SIZE = new OpaqueToken('BufferSize');
複製代碼

OpaqueToken 的實例是 uniques 值,當咱們不能使用類型時,Angular 的 DI 機制使用它們來表示標記。

對於第二個問題,咱們可使用 angular/injection-js@inject 參數修飾符來聲明一個依賴項,該依賴項的令牌不是一個類型:

const BUFFER_SIZE = new OpaqueToken('BufferSize');

class Socket {
  constructor(@Inject(BUFFER_SIZE) public size: number) {}
}

const injector = ReflectiveInjector.resolveAndCreate([
  Socket,
  { provide: BUFFER_SIZE, useValue: 42 }
]);

injector.get(Socket).size; // 42
複製代碼

注入器的層次結構

在 AngularJS 中,全部的提供者都存儲在一個扁平的結構中。在依賴注入機制的Angular2 和以上 有一個大的改進,咱們能夠創建一個分層結構的注入器。 例如,讓咱們看看下面的圖片:

Dependency Injection Hierarchy

咱們有一個根部注入器稱爲House,這是父級的注入器Bathroom, KitchenGarageGarage是父級的CarStorage。 例如,若是咱們須要來自注入器 Storage 的 token tire 依賴項,那麼 Storage 將嘗試在其註冊的提供程序集中找到它。 若是在那裏找不到,它就會去Garage找。 若是它不在那裏,Garage將在House尋找。 若是 House 找到了依賴項,它將返回給 Garage,而後返回給 Storage

上面的那棵樹看起來眼熟嗎? 最近大多數用於構建用戶界面的框架都將其結構爲組件樹。 這意味着咱們能夠有一個負責實例化各個組件及其依賴關係的注入器樹。 這樣的注入器稱爲element injectors

Element injectors

讓咱們搭建一個簡短的示例看看element injectors在Angular怎麼作的。 咱們將在 React 實現中重用相同的模型,因此讓咱們探索一個簡單的例子: 假設咱們有一個有兩種模式的博弈:

  • 單人模式
  • 多人模式

當用戶以單人模式玩遊戲時,咱們但願經過 websocket 嚮應用程序服務器發送一些元數據。 可是,若是咱們的用戶與另外一個玩家對戰,咱們但願在兩個玩家之間創建 WebRTC 數據通道,以便同步遊戲。 固然,將數據發送到應用服務器也是有意義的。 使用 angular/injection-js,咱們能夠在多個提供者中處理這個問題,可是爲了簡單起見,讓咱們假設對於多玩家,咱們只須要 p2p 鏈接。

所以,咱們有了咱們的 DataChannel,它是一個抽象類,只有一個方法和一個Observable:

abstract class DataChannel {
  dataStream: Observable<string>;
  abstract send(data: string);
}
複製代碼

稍後,這個抽象類能夠由 WebRTCDataChannelWebSocketDataChannel 類實現。 SinglePlayerGameComponent將使用 WebSocketDataChannel,而MultiPlayerGameComponent將使用 webtcdatachannel。 可是GameComponent呢? 能夠依賴於 DataChannel。 這樣,根據使用的上下文,其關聯的元素注入器能夠經過其父組件配置的正確實現。 咱們能夠用下面的僞代碼片斷來看看 Angular 中的效果如何:

@Component({
  selector: 'game-cmp',
  template: '...'
})
class GameComponent {
  constructor(private channel: DataChannel) { ... }
  ...
}

@Component({
  selector: 'single-player',
  providers: [
    { provide: DataChannel, useClass: WebSocketDataChannel }
  ],
  template: '<game-cmp></game-cmp>'
})
class SinglePlayerGameComponent { ... }


@Component({
  selector: 'multi-player',
  providers: [
    { provide: DataChannel, useClass: WebRTCDataChannel }
  ],
  template: '<game-cmp></game-cmp>'
})
class MultiPlayerGameComponent { ... }
複製代碼

在上面的例子中,咱們有 GameComponentSinglePlayerGameComponent 和 MultiPlayerGameComponent 的聲明。 Gamecomponent 只有一個 DataChannel 類型的依賴項(咱們不須要@injectable decorator,由於@Component 已經強制 TypeScript 生成元數據)。 在後面的 SinglePlayerGameComponent 中,咱們將類WebSocketDataChannelGameComponent 接受的依賴標記(即 DataChannel)關聯起來。 最後,在 MultiPlayerGameComponent 中,咱們將 DataChannelwebtcdatachannel 關聯起來。 What will happen behind the scene is shown on the image below:

Element Injectors

SinglePlayerGameComponentMultiPlayerGameComponent的組件注入器將有一個父注入器。 爲了簡單起見,讓咱們假設二者都有相同的父節點,由於這對於咱們的討論來講並不有趣。 Singleplayergamecomponent 將註冊一個提供者,該提供者將把 DataChannel 令牌與 WebSocketDataChannel 類關聯起來。 這個提供程序,連同 SinglePlayerGameComponent 組件的提供程序,將做爲single注入器註冊到圖中顯示的注入器中(Angular 寄存器在元素注入器中有更多的提供程序,但爲了簡單起見,咱們能夠忽略它們)。 另外一方面,在圖中的multi注入器中,咱們將註冊一個用於 MultiPlayerGameComponent 的提供者,以及一個將 DataChannelwebtcdatachannel 關聯的提供者。

最後,咱們有兩個game注入器。 一個是在SinglePlayerGameComponent的背景下,另外一個是在MultiPlayerGameComponent的背景下。 兩個game注入器將註冊相同的一套供應商,但將有不一樣的父母。 在這種狀況下,game中惟一的提供者就是 GameComponent。 當咱們須要遊戲注入器中與 DataChannel 令牌相關聯的依賴項時,首先它將查看其註冊的提供程序集。 由於咱們在遊戲中沒有 DataChannel 的提供者,它會詢問它的父提供者。 若是game的父注入器是single注入器(若是咱們使用 GameComponent 做爲 SinglePlayerGameComponent 的視圖子注入器,就會發生這種狀況) ,那麼咱們將得到 WebSocketDataChannel 的一個實例。 若是咱們須要來自game注入器的與 DataChannel 令牌相關的依賴關係,做爲父注入器的是多注入器,咱們將得到一個 WebRTCDataChannel 的實例。

就是這樣! 如今是時候將這些知識應用到「React」的環境中了。

在React中實現依賴注入

咱們須要爲 React 應用程序中的組件實例化實現一個控制反轉控制器(IoC)。 這意味着注入器應該負責實例化用戶界面的各個構建塊。 整個過程以下:

  • 每一個組件僅經過在其構造函數中指定其類型,或使用@Inject參數裝飾器
  • 咱們將爲每一個部件建立一個注入器,並稱之爲元素注入器。 這個注入器將負責相應組件的實例化及其依賴項的實例化(它能夠查詢其父注入器)
  • 每一個組件能夠聲明一組提供程序,這些提供程序將被包含到相關的元件注入器中
  • 咱們將爲經常使用的注入到任何 React 組件(例如,props、 context 和 updateQueue)的屬性添加一組預約義的提供程序
  • 對於每一個嵌套組件,咱們將其設置爲其父注入器,即其最近父親的注入器

就是這樣! 如今讓咱們實現它。

聲明組件的服務提供商(providers)

爲了聲明給定組件的服務提供商,咱們將使用相似於在 Angular 中使用的方法。 Angular 的組件將它們的提供者聲明爲傳遞給@Componentdecorator 的 object literal 的 providers 屬性的值:

@Component({
  selector: 'foo-bar',
  providers: [Provider1, Provider2, ..., ProviderN]
})
class Component {...}
複製代碼

We will declare a class decorator called @ProviderConfig which using the ES6 Reflect API associates the providers to the corresponding component. 咱們將聲明一個名爲@ProviderConfig 的類裝飾器,它使用 ES6 Reflect API 將服務提供商關聯到相應的組件。

export function ProviderConfig(config: any[]) {
  return function (target: any) {
    Reflect.set(target, 'providers', config);
    return target;
  };
};
複製代碼

可使用以下裝飾器:

@ProviderConfig([ Provider1, Provider2, ..., ProviderN ])
class Component extends React.Component {
  ...
}
複製代碼

建立元素注入器

本節的目的是應用前一節中列出的要點,並在 React 代碼中進行最小量的更改。 此外,修改應該儘量隔離,以即可以將它們做爲單獨的模塊分發,從而容許使用 React with injection-js。 最後,實現是不完整的,它忽略了工廠組件的狀況。 支持工廠組成部分是可能的,但不是必要的。

在內部,React 將每一個組件包裝在一塊兒,再加上一大堆其餘的東西,造成了一個ReactElement。 而後,它使用單個的 ReactElement 來建立特定的組件實例。

這兩種狀況都發生在如下文件中(咱們將只探索 react-dom,忽略其餘平臺) :

  • react/lib/ReactElement.js - 包含用於實例化 ReactElement (createElement).
  • react-dom/lib/ReactCompositeComponent.js - 包含用於構造組件的方法

出於咱們的目的,咱們只對 react-dom/lib/ReactCompositeComponent.js 進行一些修改。 Js. 讓咱們一塊兒探索吧!

require('reflect-metadata');
var ReflectiveInjector = require('injection-js').ReflectiveInjector;
...

_constructComponentWithoutOwner: function (doConstruct, publicProps, publicContext, updateQueue) {
  var Component = this._currentElement.type;
  var providers = [
    Component, {
      provide: 'props',
      useValue: publicProps
    }, {
      provide: 'context',
      useValue: publicContext
    }, {
      provide: 'update',
      useValue: updateQueue
    }
  ].concat(Reflect.get(Component, 'providers') || []);
  var injector;
  if (!this._currentElement._owner) {
    injector = ReflectiveInjector.resolveAndCreate(providers);
  } else {
    injector = Reflect.get(this._currentElement._owner._currentElement.type, 'injector').resolveAndCreateChild(providers);
  }
  Reflect.set(Component, 'injector', injector);

  if (doConstruct) {
    if (process.env.NODE_ENV !== 'production') {
      return measureLifeCyclePerf(function () {
        return injector.get(Component);
      }, this._debugID, 'ctor');
    } else {
      return injector.get(Component);
    }
  }
  ...
複製代碼

這是 React 15.4.2的分支。 上面的代碼展現了我爲了爲每一個組件建立一個注入器而必須進行的全部修改,而後使用相應的注入器實例化該組件。 讓咱們一步一步地研究這個代碼片斷。

首先,咱們得到對組件類的引用。 這是經過獲取屬性值來實現的。 this._currentElement.type。 後來咱們註冊了一組提供程序。 默認設置包含組件的classproviderspropscontextupdateQueue。 後三個提供程序在實例化期間默認傳遞給每一個組件的構造函數。 稍後,咱們還向這組提供程序添加@ProviderConfig 聲明的提供程序。 爲此,咱們使用 ES6 Reflect API

做爲下一步,咱們檢查當前組件的元素是否具備全部者。 若是沒有,這意味着咱們處於根組件,咱們須要建立根注入器。 爲此,咱們使用 ReflectiveInjector 類的靜態 resolveAndCreate 方法。若是當前元素具備全部者,咱們經過調用全部者的注入器的 resolveAndCreateChild 實例化一個子注入器。

由於咱們但願建立的注入器對子組件可用,因此咱們將其做爲 Reflected API 中的一個條目。

請注意,這段代碼操縱 React 的內部並使用私有屬性,前綴爲_ 我不推薦它用於生產,由於它沒有通過很好的測試,不包括工廠組件,而且極可能在未來的 React 版本中不起做用。

使用 React with DI

下面是一個快速演示,演示了咱們如何在 React 中使用 DI 和所描述的實現:

import * as React from 'react';
import {Inject} from 'injection-js';
import {ProviderConfig} from '../providers';
import {WebSocketService} from '../websocket.service';

@ProviderConfig([ WebSocketService ])
export default class HelloWorldComponent extends React.Component<any, any> {
  constructor(@Inject('update') update, ws: WebSocketService, @Inject('props') props: any) {
    super(props);
  }
  
  render(){
    return <div>Hello world!</div>;
  }
}
複製代碼

與傳統方法相比,咱們使用啓用 DI 的組件的方式有一個重要的區別——目標組件接受的參數不是定位注入的,而是基於它們在構造函數中聲明的順序。

從上面的例子能夠看出,HelloWorldComponent 接受 樹 參數,全部參數都經過注入injection-js 的 DI 機制注入。 與原始組件 API 不一樣,依賴項將按照其聲明的順序注入。

總結

在這個實驗中,咱們看到了如何使用反應中的角依賴注入機制。 咱們解釋了 DI 是什麼以及它帶來的好處。 咱們還看到了如何在使用元素注入器開發用戶界面的上下文中應用它。

在此以後,咱們經過直接修改庫的源代碼來完成 React 中元素注入器的進行實現。

雖然這個想法看起來頗有趣,而且可能適用於現實應用程序,可是本文的示例還沒有爲生產作好準備。 若是您能在下面的評論部分提供反饋和想法,我將不勝感激。

相關文章
相關標籤/搜索