在 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 中元素注入器的进行实现。

虽然这个想法看起来颇有趣,而且可能适用于现实应用程序,可是本文的示例还没有为生产作好准备。 若是您能在下面的评论部分提供反馈和想法,我将不胜感激。

相关文章
相关标签/搜索