在React上进行了一些项目之后,我有机会在Angular 2下开发了一个应用程序。坦率地说,我没有留下深刻的印象。 但是记住了一件事-使用依赖注入管理应用程序的逻辑和状态。 我想知道使用DDD,分层架构和依赖注入在React中管理状态是否方便?
如果您对如何执行此操作感兴趣,最重要的是,为什么-欢迎加入剪切!
老实说,即使在后端,DI也很少被充分利用。 除非在真正的大型应用中使用。 在中小企业中,即使使用DI,每个接口通常也只有一个实现。 但是依赖注入仍然有其优点:
- 代码结构更好,接口充当显式契约。
- 简化了单元测试中存根的创建。
但是,现代的JS测试库(例如Jest )允许您仅基于模块化ES6系统编写moki。 因此,在这里我们不会从DI中获得太多利润。
第二点仍然是-对象范围和生存期的管理。 在服务器上,生存期通常绑定到整个应用程序(Singleton)或请求。 在客户端上,代码的主要单元是组件。 我们将依附于此。
如果需要在应用程序级别使用状态,最简单的方法是在ES6模块级别设置变量,并在必要时将其导入。 如果仅在组件内部需要状态, this.state
其放在this.state
。 对于其他所有内容,都有Context
。 但是Context
太底层了:
- 我们不能在React组件树之外使用上下文。 例如,在业务逻辑层中。
- 我们不能在
Class.contextType
使用多个上下文。 为了确定对几种不同服务的依赖性,我们将必须以新的方式构建“恐怖金字塔”:

新的Hook useContext()
稍微纠正了功能组件的情况。 但是我们不会摆脱许多<Context.Provider>
。 直到我们将上下文转换为服务定位器,并将其父组件转换为合成根。 但是这里离DI不远,所以让我们开始吧!
您可以跳过这一部分,直接转到体系结构的描述。
DI机制的实现
首先,我们需要一个React Context:
export const InjectorContext= React.createContext(null);
由于React使用组件构造函数来满足其需求,因此我们将使用属性注入。 为此,定义@inject
装饰器,该装饰器:
inject.js import "reflect-metadata"; export function inject(target, key) {
现在我们可以定义任意类之间的依赖关系:
import { inject } from "react-ioc"; class FooService {} class BarService { @inject foo: FooService; } class MyComponent extends React.Component { @inject foo: FooService; @inject bar: BarService; }
对于那些不接受装饰器的人,我们使用以下签名定义inject()
函数:
type Constructor<T> = new (...args: any[]) => T; function inject<T>(target: Object, type: Constructor<T> | Function): T;
inject.js export function inject(target, keyOrType) { if (isFunction(keyOrType)) { return getInstance(getInjector(target), keyOrType); }
这将允许您显式定义依赖项:
class FooService {} class BarService { foo = inject(this, FooService); } class MyComponent extends React.Component { foo = inject(this, FooService); bar = inject(this, BarService);
功能组件呢? 对于他们,我们可以实现Hook useInstance()
hooks.js import { useRef, useContext } from "react"; export function useInstance(type) { const ref = useRef(null); const injector = useContext(InjectorContext); return ref.current || (ref.current = getInstance(injector, type)); }
import { useInstance } from "react-ioc"; const MyComponent = props => { const foo = useInstance(FooService); const bar = useInstance(BarService); return <div />; }
现在,我们将确定Injector
外观,如何找到它以及如何解决依赖关系。 注入程序必须包含对父项的引用,用于已解决依赖关系的对象缓存以及用于未解决依赖关系的规则字典。
喷射器 type Binding = (injector: Injector) => Object; export abstract class Injector extends React.Component {
对于React组件,可以通过this.context
字段使用Injector
,对于依赖项类,我们可以将Injector
临时放在全局变量中。 为了加快每个类别的注射器的搜索速度,我们将在隐藏字段中缓存指向Injector
的链接。
喷射器 export const INJECTOR = typeof Symbol === "function" ? Symbol() : "__injector__"; let currentInjector = null; export function getInjector(target) { let injector = target[INJECTOR]; if (injector) { return injector; } injector = currentInjector || target.context; if (injector instanceof Injector) { target[INJECTOR] = injector; return injector; } return null; }
为了找到特定的绑定规则,我们需要使用getInstance()
函数来使注入器树上升
喷射器 export function getInstance(injector, type) { while (injector) { let instance = injector._instanceMap.get(type); if (instance !== undefined) { return instance; } const binding = injector._bindingMap.get(type); if (binding) { const prevInjector = currentInjector; currentInjector = injector; try { instance = binding(injector); } finally { currentInjector = prevInjector; } injector._instanceMap.set(type, instance); return instance; } injector = injector._parent; } return undefined; }
最后,让我们继续注册依赖项。 为此,我们需要HOC provider()
,它将对它们的实现进行一系列依赖项绑定,并通过InjectorContext.Provider
注册一个新的Injector
。
provider.js export const provider = (...definitions) => Wrapped => { const bindingMap = new Map(); addBindings(bindingMap, definitions); return class Provider extends Injector { _parent = this.context; _bindingMap = bindingMap; _instanceMap = new Map(); render() { return ( <InjectorContext.Provider value={this}> <Wrapped {...this.props} /> </InjectorContext.Provider> ); } static contextType = InjectorContext; static register(...definitions) { addBindings(bindingMap, definitions); } }; };
另外,还有一组绑定函数,它们实现用于创建依赖项实例的各种策略。
bindings.js export const toClass = constructor => asBinding(injector => { const instance = new constructor(); if (!instance[INJECTOR]) { instance[INJECTOR] = injector; } return instance; }); export const toFactory = (depsOrFactory, factory) => asBinding( factory ? injector => factory(...depsOrFactory.map(type => getInstance(injector, type))) : depsOrFactory ); export const toExisting = type => asBinding(injector => getInstance(injector, type)); export const toValue = value => asBinding(() => value); const IS_BINDING = typeof Symbol === "function" ? Symbol() : "__binding__"; function asBinding(binding) { binding[IS_BINDING] = true; return binding; } export function addBindings(bindingMap, definitions) { definitions.forEach(definition => { let token, binding; if (Array.isArray(definition)) { [token, binding = token] = definition; } else { token = binding = definition; } bindingMap.set(token, binding[IS_BINDING] ? binding : toClass(binding)); }); }
现在我们可以以成对[<>, <>]
对的形式在任意组件级别注册依赖项绑定。
import { provider, toClass, toValue, toFactory, toExisting } from "react-ioc"; @provider(
或类的缩写形式:
@provider(
由于服务的生存期由注册该服务的提供程序组件决定,因此,对于每个服务,我们都可以定义.dispose()
清洁方法。 在其中,我们可以取消订阅某些事件,关闭套接字等。 当您从DOM中删除提供程序时,它将在其创建的所有服务上调用.dispose()
。
provider.js export const provider = (...definitions) => Wrapped => {
为了分离代码和延迟加载,我们可能需要反转向提供者注册服务的方法。 装饰器@registerIn()
将帮助我们解决这个问题。
provider.js export const registrationQueue = []; export const registerIn = (getProvider, binding) => constructor => { registrationQueue.push(() => { getProvider().register(binding ? [constructor, binding] : constructor); }); return constructor; };
喷射器 export function getInstance(injector, type) { if (registrationQueue.length > 0) { registrationQueue.forEach(registration => { registration(); }); registrationQueue.length = 0; } while (injector) {
import { registerIn } from "react-ioc"; import { HomePage } from "../components/HomePage"; @registerIn(() => HomePage) class MyLazyLoadedService {}

因此,对于150行和1 KB的代码,您可以实现几乎完整的分层DI容器 。
应用架构
最后,让我们继续讨论主要内容-如何组织应用程序体系结构。 根据应用程序的大小,主题区域的复杂性和我们的懒惰,这里可以提供三个选项。
1.丑陋
我们有一个虚拟DOM,这意味着它应该很快。 至少用这种调味料,React在职业生涯的黎明就被送达了。 因此,只需记住到根组件的链接即可(例如,使用@observer
装饰器)。 在影响共享服务的每个操作之后,我们将在其上调用.forceUpdate()
(例如,使用@action
装饰器)
Observer.js export function observer(Wrapped) { return class Observer extends React.Component { componentDidMount() { observerRef = this; } componentWillUnmount() { observerRef = null; } render() { return <Wrapped {...this.props} />; } } } let observerRef = null;
action.js export function action(_target, _key, descriptor) { const method = descriptor.value; descriptor.value = function() { let result; runningCount++; try { result = method.apply(this, arguments); } finally { runningCount--; } if (runningCount === 0 && observerRef) { observerRef.forceUpdate(); } return result; }; } let runningCount = 0;
class UserService { @action doSomething() {} } class MyComponent extends React.Component { @inject userService: UserService; } @provider(UserService) @observer class App extends React.Component {}
它甚至可以工作。 但是...你自己明白:-)
2.坏
我们不满意每次打喷嚏时都要渲染所有内容。 但是我们还是想用 差不多 用于存储状态的常规对象和数组。 让我们以MobX为例 !
我们以标准动作开始几个数据存储:
import { observable, action } from "mobx"; export class UserStore { byId = observable.map<number, User>(); @action add(user: User) { this.byId.set(user.id, user); }
我们将业务逻辑,I / O等带到服务层:
import { action } from "mobx"; import { inject } from "react-ioc"; export class AccountService { @inject userStore userStore; @action updateUserInfo(userInfo: Partial<User>) { const user = this.userStore.byId.get(userInfo.id); Object.assign(user, userInfo); } }
我们将它们分发到组件中:
import { observer } from "mobx-react"; import { provider, inject } from "react-ioc"; @provider(UserStore, PostStore) class App extends React.Component {} @provider(AccountService) @observer class AccountPage extends React.Component{} @observer class UserForm extends React.Component { @inject accountService: AccountService; }
没有装饰器的功能组件也是如此 import { action } from "mobx"; import { inject } from "react-ioc"; export class AccountService { userStore = inject(this, UserStore); updateUserInfo = action((userInfo: Partial<User>) => { const user = this.userStore.byId.get(userInfo.id); Object.assign(user, userInfo); }); }
import { observer } from "mobx-react-lite"; import { provider, useInstance } from "react-ioc"; const App = provider(UserStore, PostStore)(props => {
结果就是经典的三层体系结构。
3.善良
有时,主题区域变得如此复杂,以至于使用简单的对象(或DDD中的贫血模型)来使用它已经不方便了。 当数据具有具有许多关系的关系结构时,这一点尤其明显。 在这种情况下, MobX状态树库将为我们提供帮助,使我们能够将域驱动设计的原理应用于前端应用程序的体系结构中。
设计模型始于对类型的描述:
型号/User.ts import { types as t, Instance } from "mobx-state-tree"; export const User = t.model("User", { id: t.identifier, name: t.string }); export type User = Instance<typeof User>;
型号/Comment.ts import { types as t, Instance } from "mobx-state-tree"; import { User } from "./User"; export const Comment = t .model("Comment", { id: t.identifier, text: t.string, date: t.Date, rating: t.number, author: t.reference(User) }) .actions(self => ({ voteUp() { self.rating++; }, voteDown() { self.rating--; } })); export type Comment = Instance<typeof Comment>;
以及数据存储的类型:
实体类型包含域模型的状态及其基本操作。 在服务层中实现了更复杂的方案,包括I / O。
服务/ DataContext.ts import { Instance, unprotect } from "mobx-state-tree"; import Models from "../models"; export class DataContext { static create() { const models = Models.create(); unprotect(models); return models; } } export interface DataContext extends Instance<typeof Models> {}
服务/ AuthService.ts import { observable } from "mobx"; import { User } from "../models"; export class AuthService { @observable currentUser: User; }
服务/ PostService.ts import { inject } from "react-ioc"; import { action } from "mobx"; import { Post } from "../models"; export class PostService { @inject dataContext: DataContext; @inject authService: AuthService; async publishPost(postInfo: Partial<Post>) { const response = await fetch("/posts", { method: "POST", body: JSON.stringify(postInfo) }); const { id } = await response.json(); this.savePost(id, postInfo); } @action savePost(id: string, postInfo: Partial<Post>) { const post = Post.create({ id, rating: 0, date: new Date(), author: this.authService.currentUser.id, comments: [], ...postInfo }); this.dataContext.posts.put(post); } }
MobX状态树的主要功能是高效处理数据快照。 在任何时候,我们都可以使用getSnapshot()
函数获取任何实体,集合甚至应用程序的整个状态的序列化状态。 同样,我们可以使用applySnapshot()
将快照应用于模型的任何部分。 这使我们可以用几行代码初始化服务器的状态,从LocalStorage加载,甚至通过Redux DevTools与之交互。
由于我们使用规范化的关系模型,因此我们需要normalizr库来加载数据。 它允许您根据数据方案将树JSON转换为按id
分组的对象的平面表。 只是采用MobX状态树作为快照的格式。
为此,请定义从服务器下载的对象方案:
import { schema } from "normalizr"; const UserSchema = new schema.Entity("users"); const CommentSchema = new schema.Entity("comments", { author: UserSchema }); const PostSchema = new schema.Entity("posts", {
并将数据加载到存储中:
import { inject } from "react-ioc"; import { normalize } from "normalizr"; import { applySnapshot } from "mobx-state-tree"; export class PostService { @inject dataContext: DataContext;
posts.json [ { "id": 123, "title": " React", "body": " - React...", "date": "2018-12-10T18:18:58.512Z", "rating": 0, "author": { "id": 12, "name": "John Doe" }, "comments": [{ "id": 1234, "text": "Hmmm...", "date": "2018-12-10T18:18:58.512Z", "rating": 0, "author": { "id": 12, "name": "John Doe" } }] }, { "id": 234, "title": "Lorem ipsum", "body": "Lorem ipsum dolor sit amet...", "date": "2018-12-10T18:18:58.512Z", "rating": 0, "author": { "id": 23, "name": "Marcus Tullius Cicero" }, "comments": [] } ]
最后,在相应的组件中注册服务:
import { observer } from "mobx-react"; import { provider, inject } from "react-ioc"; @provider(AuthService, PostService, [ DataContext, toFactory(DataContext.create) ]) class App extends React.Component { @inject postService: PostService; componentDidMount() { this.postService.loadPosts(); } }
事实证明,所有相同的三层体系结构都具有维护数据类型的状态和运行时验证的能力(在DEV模式下)。 后者使您可以确保如果没有异常发生,那么数据仓库的状态与规范相对应。

对于那些感兴趣的人,请指向github的链接和一个demo 。