Setelah beberapa proyek tentang Bereaksi, saya memiliki kesempatan untuk mengerjakan aplikasi di bawah Angular 2. Terus terang, saya tidak terkesan. Tapi satu hal yang diingat - mengelola logika dan keadaan aplikasi menggunakan Dependency Injection. Dan saya bertanya-tanya apakah nyaman untuk mengelola keadaan Bereaksi menggunakan DDD, arsitektur berlapis, dan injeksi ketergantungan?
Jika Anda tertarik untuk melakukan ini, dan yang paling penting, mengapa - selamat datang di cut!
Sejujurnya, bahkan di backend, DI jarang digunakan sepenuhnya. Kecuali dalam aplikasi yang sangat besar. Dan dalam ukuran kecil dan menengah, bahkan dengan DI, setiap antarmuka biasanya hanya memiliki satu implementasi. Tetapi ketergantungan injeksi masih memiliki kelebihan:
- Kode terstruktur lebih baik, dan antarmuka bertindak sebagai kontrak eksplisit.
- Pembuatan rintisan dalam unit test disederhanakan.
Tetapi pustaka pengujian modern untuk JS, seperti Jest , memungkinkan Anda untuk menulis moki hanya berdasarkan pada sistem ES6 modular. Jadi di sini kita tidak akan mendapat banyak keuntungan dari DI.
Poin kedua tetap - pengelolaan ruang lingkup dan masa objek. Di server, masa pakai biasanya terikat ke seluruh aplikasi (Singleton), atau ke permintaan. Dan pada klien, unit kode utama adalah komponen. Kami akan melekat padanya.
Jika kita perlu menggunakan status pada level aplikasi, cara termudah adalah mengatur variabel pada level modul ES6 dan mengimpornya jika perlu. Dan jika keadaan hanya diperlukan di dalam komponen - kita cukup letakkan di this.state
. Untuk yang lainnya, ada Context
. Tetapi tingkat terlalu rendah:
- Kami tidak dapat menggunakan konteks di luar pohon komponen React. Misalnya, dalam lapisan logika bisnis.
- Kami tidak dapat menggunakan lebih dari satu konteks di
Class.contextType
. Untuk menentukan ketergantungan pada beberapa layanan yang berbeda, kita harus membangun "piramida horor" dengan cara baru:

Hook useContext()
sedikit memperbaiki situasi untuk komponen fungsional. Tapi kami tidak akan menyingkirkan banyak <Context.Provider>
. Hingga kami mengubah konteks kami menjadi Pencari Layanan, dan komponen induknya menjadi Root Komposisi. Tapi ini tidak jauh dari DI, jadi mari kita mulai!
Anda dapat melewati bagian ini dan langsung ke deskripsi arsitektur.
Implementasi mekanisme DI
Pertama kita perlu Konteks Bereaksi:
export const InjectorContext= React.createContext(null);
Karena Bereaksi menggunakan konstruktor komponen untuk kebutuhannya, kami akan menggunakan Injeksi Properti. Untuk melakukan ini, tentukan dekorator @inject
, yang:
- menyetel properti
Class.contextType
, - mendapat jenis ketergantungan
- menemukan objek
Injector
dan menyelesaikan ketergantungan.
inject.js import "reflect-metadata"; export function inject(target, key) {
Sekarang kita dapat mendefinisikan dependensi antara kelas-kelas yang berubah-ubah:
import { inject } from "react-ioc"; class FooService {} class BarService { @inject foo: FooService; } class MyComponent extends React.Component { @inject foo: FooService; @inject bar: BarService; }
Bagi mereka yang tidak menerima dekorator, kami mendefinisikan fungsi inject()
dengan tanda tangan ini:
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); }
Ini akan memungkinkan Anda untuk menentukan dependensi secara eksplisit:
class FooService {} class BarService { foo = inject(this, FooService); } class MyComponent extends React.Component { foo = inject(this, FooService); bar = inject(this, BarService);
Bagaimana dengan komponen fungsional? Bagi mereka, kami dapat menerapkan 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 />; }
Sekarang kita akan menentukan seperti apa tampilan Injector
kita, bagaimana menemukannya, dan bagaimana menyelesaikan dependensi. Injektor harus berisi referensi ke induk, cache objek untuk dependensi yang sudah diselesaikan, dan kamus aturan untuk yang belum diselesaikan.
injector.js type Binding = (injector: Injector) => Object; export abstract class Injector extends React.Component {
Untuk komponen Bereaksi, Injector
tersedia melalui bidang this.context
, dan untuk kelas dependensi, kita dapat sementara menempatkan Injector
dalam variabel global. Untuk mempercepat pencarian injektor untuk setiap kelas, kami akan menembolokkan tautan ke Injector
di bidang tersembunyi.
injector.js 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; }
Untuk menemukan aturan pengikatan tertentu, kita perlu naik pohon injector menggunakan fungsi getInstance()
injector.js 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; }
Akhirnya, mari beralih ke mendaftarkan dependensi. Untuk melakukan ini, kita memerlukan provider()
HOC provider()
, yang mengambil array binding ketergantungan untuk implementasinya, dan mendaftarkan Injector
baru melalui InjectorContext.Provider
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); } }; };
Dan juga, satu set fungsi yang mengikat yang menerapkan berbagai strategi untuk membuat instance ketergantungan.
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)); }); }
Sekarang kita dapat mendaftarkan ikatan ketergantungan pada tingkat komponen arbitrer dalam bentuk seperangkat pasangan [<>, <>]
.
import { provider, toClass, toValue, toFactory, toExisting } from "react-ioc"; @provider(
Atau dalam bentuk singkatan untuk kelas:
@provider(
Karena masa pakai suatu layanan ditentukan oleh komponen penyedia di mana ia terdaftar, untuk setiap layanan kami dapat mendefinisikan metode pembersihan .dispose()
. Di dalamnya, kita dapat berhenti berlangganan dari beberapa acara, menutup soket, dll. Saat Anda menghapus penyedia dari DOM, itu akan memanggil .dispose()
pada semua layanan yang dibuatnya.
provider.js export const provider = (...definitions) => Wrapped => {
Untuk memisahkan kode dan pemuatan malas, kita mungkin perlu membalikkan metode pendaftaran layanan dengan penyedia. Dekorator @registerIn()
akan membantu kami dengan ini.
provider.js export const registrationQueue = []; export const registerIn = (getProvider, binding) => constructor => { registrationQueue.push(() => { getProvider().register(binding ? [constructor, binding] : constructor); }); return constructor; };
injector.js 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 {}

Jadi, untuk 150 baris dan 1 KB kode, Anda dapat menerapkan wadah DI hirarkis yang hampir lengkap.
Arsitektur aplikasi
Akhirnya, mari kita beralih ke hal utama - bagaimana mengatur arsitektur aplikasi. Ada tiga opsi yang mungkin, tergantung pada ukuran aplikasi, kompleksitas area subjek dan kemalasan kita.
1. Jelek
Kami memiliki DOM Virtual, yang berarti harus cepat. Setidaknya dengan saus ini, React dihidangkan pada awal karier. Karena itu, ingat saja tautan ke komponen root (misalnya, menggunakan dekorator @observer
). Dan kami akan memanggil .forceUpdate()
di atasnya setelah setiap tindakan yang mempengaruhi layanan bersama (misalnya, menggunakan dekorator @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 {}
Bahkan akan berhasil. Tapi ... kamu sendiri mengerti :-)
2. Yang Buruk
Kami tidak puas dengan memberikan semuanya untuk setiap bersin. Tapi kami masih ingin menggunakannya hampir objek dan array reguler untuk menyimpan status. Ayo ambil MobX !
Kami memulai beberapa penyimpanan data dengan tindakan standar:
import { observable, action } from "mobx"; export class UserStore { byId = observable.map<number, User>(); @action add(user: User) { this.byId.set(user.id, user); }
Kami mengeluarkan logika bisnis, I / O, dll. Ke lapisan layanan:
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); } }
Dan kami mendistribusikannya ke dalam komponen:
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; }
Hal yang sama berlaku untuk komponen fungsional dan tanpa dekorator 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 => {
Hasilnya adalah arsitektur tiga tingkat klasik.
3. Yang Baik
Kadang-kadang area subjek menjadi sangat kompleks sehingga tidak nyaman untuk bekerja dengannya menggunakan objek sederhana (atau model anemia dalam hal DDD). Ini terutama terlihat ketika data memiliki struktur relasional dengan banyak hubungan. Dalam kasus seperti itu, perpustakaan MobX State Tree datang untuk menyelamatkan, memungkinkan Anda untuk menerapkan prinsip-prinsip Desain Berbasis Domain dalam arsitektur aplikasi front-end.
Merancang model dimulai dengan deskripsi jenis:
model / 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>;
model / 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>;
Dan jenis penyimpanan data:
Jenis entitas berisi keadaan model domain dan operasi dasar dengannya. Skenario yang lebih kompleks, termasuk I / O, diterapkan di lapisan layanan.
layanan / 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> {}
layanan / AuthService.ts import { observable } from "mobx"; import { User } from "../models"; export class AuthService { @observable currentUser: User; }
layanan / 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); } }
Fitur utama MobX State Tree adalah pekerjaan efisien dengan snapshot data. Kapan saja, kita bisa mendapatkan status serial entitas apa pun, koleksi, atau bahkan seluruh status aplikasi menggunakan fungsi getSnapshot()
. Dan dengan cara yang sama, kita dapat menerapkan snapshot ke bagian mana pun dari model menggunakan applySnapshot()
. Ini memungkinkan kita untuk menginisialisasi keadaan dari server dalam beberapa baris kode, memuat dari LocalStorage, atau bahkan berinteraksi dengannya melalui Redux DevTools.
Karena kita menggunakan model relasional yang dinormalisasi, kita membutuhkan pustaka normalizr untuk memuat data. Ini memungkinkan Anda untuk menerjemahkan pohon JSON ke dalam tabel datar objek yang dikelompokkan berdasarkan id
, sesuai dengan skema data. Hanya dalam format itu Pohon Status MobX diperlukan sebagai snapshot.
Untuk melakukan ini, tentukan skema objek yang diunduh dari server:
import { schema } from "normalizr"; const UserSchema = new schema.Entity("users"); const CommentSchema = new schema.Entity("comments", { author: UserSchema }); const PostSchema = new schema.Entity("posts", {
Dan memuat data ke dalam penyimpanan:
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": [] } ]
Akhirnya, daftarkan layanan dalam komponen yang sesuai:
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(); } }
Ternyata semua arsitektur tiga lapis yang sama, tetapi dengan kemampuan untuk mempertahankan negara dan runtime verifikasi tipe data (dalam mode DEV). Yang terakhir memungkinkan Anda untuk memastikan bahwa jika tidak ada pengecualian terjadi, maka keadaan gudang data sesuai dengan spesifikasi.

Bagi mereka yang tertarik, tautan ke github dan demo .