एक डोमेन मॉडल के रूप में रिएक्ट और MobX स्टेट ट्री में पदानुक्रमित निर्भरता इंजेक्शन

रिएक्ट पर कुछ परियोजनाओं के बाद, मुझे एंगुलर 2 के तहत एक आवेदन पर काम करने का मौका मिला। स्पष्ट रूप से, मैं प्रभावित नहीं था। लेकिन एक बात याद थी - डिपेंडेंसी इंजेक्शन का उपयोग करके तर्क और अनुप्रयोग की स्थिति का प्रबंधन करना। और मुझे आश्चर्य है कि क्या डीडीडी, स्तरित वास्तुकला और निर्भरता इंजेक्शन का उपयोग करके रिएक्ट में राज्य का प्रबंधन करना सुविधाजनक है?


यदि आप इसे करने के तरीके में रुचि रखते हैं, और सबसे महत्वपूर्ण बात, क्यों - कटौती में आपका स्वागत है!


ईमानदार होने के लिए, यहां तक ​​कि बैकएंड पर, डीआई को शायद ही कभी इसका पूर्ण उपयोग किया जाता है। जब तक वास्तव में बड़े अनुप्रयोगों में नहीं। और छोटे और मध्यम में, यहां तक ​​कि एक डीआई के साथ, प्रत्येक इंटरफ़ेस में आमतौर पर केवल एक कार्यान्वयन होता है। लेकिन निर्भरता इंजेक्शन अभी भी अपने फायदे हैं:


  • कोड बेहतर संरचित है, और इंटरफेस स्पष्ट अनुबंध के रूप में कार्य करते हैं।
  • यूनिट परीक्षणों में स्टब्स का निर्माण सरल है।

लेकिन जेएस, जैसे जेएस के लिए आधुनिक परीक्षण पुस्तकालय आपको मॉड्यूलर ईएस 6 प्रणाली के आधार पर केवल मोकी लिखने की अनुमति देते हैं। इसलिए यहां हमें डीआई से अधिक लाभ नहीं मिलेगा।


दूसरा बिंदु रहता है - वस्तुओं के दायरे और जीवनकाल का प्रबंधन। सर्वर पर, जीवनकाल आमतौर पर संपूर्ण एप्लिकेशन (सिंगलटन) या अनुरोध के लिए बाध्य होता है। और क्लाइंट पर, कोड की मुख्य इकाई घटक है। हम इससे जुड़े रहेंगे।


यदि हमें एप्लिकेशन स्तर पर राज्य का उपयोग करने की आवश्यकता है, तो सबसे आसान तरीका ईएस 6 मॉड्यूल स्तर पर चर सेट करना है और जहां आवश्यक हो वहां आयात करना है। और अगर राज्य को केवल घटक के अंदर की आवश्यकता होती है - हम इसे इस में this.state । बाकी सब चीजों के लिए, Context । लेकिन Context बहुत निम्न स्तर का है:


  • हम प्रतिक्रिया घटक के पेड़ के बाहर संदर्भ का उपयोग नहीं कर सकते। उदाहरण के लिए, एक व्यापार तर्क परत में।
  • हम Class.contextType में एक से अधिक संदर्भ का उपयोग नहीं कर सकते हैं। कई अलग-अलग सेवाओं पर निर्भरता निर्धारित करने के लिए, हमें एक नए तरीके से "डरावनी पिरामिड" का निर्माण करना होगा:



नए हुक का useContext() कार्यात्मक घटकों के लिए स्थिति को थोड़ा ठीक करता है। लेकिन हमने कई <Context.Provider> छुटकारा नहीं पाया। जब तक हम अपने संदर्भ को सेवा लोकेटर, और इसके मूल घटक में रचना रूट में नहीं बदल देते। लेकिन यहाँ यह DI से दूर नहीं है, तो चलिए शुरू करते हैं!


आप इस भाग को छोड़ सकते हैं और वास्तुकला के वर्णन पर सीधे जा सकते हैं

डीआई तंत्र कार्यान्वयन


सबसे पहले हमें एक प्रतिक्रिया की आवश्यकता है:


 export const InjectorContext= React.createContext(null); 

चूंकि रिएक्ट अपनी आवश्यकताओं के लिए घटक कंस्ट्रक्टर का उपयोग करता है, इसलिए हम प्रॉपर्टी इंजेक्शन का उपयोग करेंगे। ऐसा करने के लिए, @inject डेकोरेटर को परिभाषित करें, जो:


  • Class.contextType गुण सेट करता है,
  • निर्भरता का प्रकार मिलता है
  • एक Injector ऑब्जेक्ट पाता है और निर्भरता को हल करता है।

inject.js
 import "reflect-metadata"; export function inject(target, key) { //  static cotextType target.constructor.contextType = InjectorContext; //    const type = Reflect.getMetadata("design:type", target, key); //  property Object.defineProperty(target, key, { configurable: true, enumerable: true, get() { //  Injector       const instance = getInstance(getInjector(this), type); Object.defineProperty(this, key, { enumerable: true, writable: true, value: instance }); return instance; }, // settet     Dependency Injection set(instance) { Object.defineProperty(this, key, { enumerable: true, writable: true, value: instance }); } }); } 

अब हम मनमानी वर्गों के बीच निर्भरता को परिभाषित कर सकते हैं:


 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); //   static contextType = InjectorContext; } 

कार्यात्मक घटकों के बारे में क्या? उनके लिए हम हुक उपयोग लागू कर सकते हैं 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 कैसा दिख सकता है, इसे कैसे खोजें, और निर्भरता को कैसे हल करें। इंजेक्टर में माता-पिता का एक संदर्भ, निर्भरता के लिए ऑब्जेक्ट कैश जो पहले से ही हल हैं, और अभी तक हल नहीं करने के लिए एक नियम शब्दकोश होना चाहिए।


injector.js
 type Binding = (injector: Injector) => Object; export abstract class Injector extends React.Component { //    Injector _parent?: Injector; //    _bindingMap: Map<Function, Binding>; //      _instanceMap: Map<Function, Object>; } 

प्रतिक्रिया घटकों के लिए, Injector इस. this.context फ़ील्ड के माध्यम से उपलब्ध है, और निर्भरता वर्गों के लिए, हम अस्थायी रूप से Injector को एक वैश्विक चर में डाल सकते हैं। प्रत्येक वर्ग के लिए एक इंजेक्टर की खोज को तेज करने के लिए, हम एक छिपे हुए क्षेत्र में Injector लिए लिंक को कैश करेंगे।


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

एक विशिष्ट बाध्यकारी नियम खोजने के लिए, हमें 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; } 

अंत में, चलो निर्भरता दर्ज करने के लिए आगे बढ़ते हैं। ऐसा करने के लिए, हमें HOC provider() , जो उनके कार्यान्वयन के लिए निर्भरता की एक सीमा लेता है, और Injector माध्यम से एक नया 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( //    [FirstService, toClass(FirstServiceImpl)], //     [SecondService, toValue(new SecondServiceImpl())], //    [ThirdService, toFactory( [FirstService, SecondService], (first, second) => ThirdServiceFactory.create(first, second) )], //      [FourthService, toExisting(FirstService)] ) class MyComponent extends React.Component { // ... } 

या कक्षाओं के लिए संक्षिप्त रूप में:


 @provider( // [FirstService, toClass(FirstService)] FirstService, // [SecondService, toClass(SecondServiceImpl)] [SecondService, SecondServiceImpl] ) class MyComponent extends React.Component { // ... } 

चूंकि एक सेवा का जीवनकाल प्रदाता घटक द्वारा निर्धारित किया जाता है जिसमें यह पंजीकृत है, प्रत्येक सेवा के लिए हम एक .dispose() सफाई विधि को परिभाषित कर सकते हैं। इसमें, हम कुछ घटनाओं, करीबी सॉकेट्स आदि से सदस्यता समाप्त कर सकते हैं। जब आप प्रदाता को DOM से हटाते हैं, तो यह सभी सेवाओं पर .dispose() कॉल करेगा।


provider.js
 export const provider = (...definitions) => Wrapped => { // ... return class Provider extends Injector { // ... componentWillUnmount() { this._instanceMap.forEach(instance => { if (isObject(instance) && isFunction(instance.dispose)) { instance.dispose(); } }); } // ... }; }; 

कोड और आलसी लोडिंग को अलग करने के लिए, हमें प्रदाताओं के साथ सेवाओं को पंजीकृत करने की विधि को पलटना पड़ सकता है। डेकोरेटर @registerIn() हमारी मदद करेगा।


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 {} 


तो, 150 लाइनों और 1 KB कोड के लिए, आप लगभग पूर्ण श्रेणीबद्ध डीआई कंटेनर को लागू कर सकते हैं।


अनुप्रयोग वास्तुकला


अंत में, चलो मुख्य बात पर चलते हैं - एप्लिकेशन आर्किटेक्चर को कैसे व्यवस्थित करें। आवेदन के आकार, विषय क्षेत्र की जटिलता और हमारे आलस्य के आधार पर तीन संभावित विकल्प हैं।


1. कुरूप


हमारे पास एक वर्चुअल डोम है, जिसका अर्थ है कि यह तेज होना चाहिए। कम से कम इस चटनी के साथ, करियर की शुरुआत में रिएक्ट परोसा गया। इसलिए, बस रूट घटक के लिंक को याद रखें (उदाहरण के लिए, @observer डेकोरेटर का उपयोग करके)। और हम साझा सेवाओं को प्रभावित करने वाली प्रत्येक कार्रवाई के बाद (उदाहरण के लिए, @action डेकोरेटर का उपयोग करके .forceUpdate() पर .forceUpdate() कॉल करेंगे


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. बुरा


हम हर छींक के लिए सब कुछ प्रदान करने से संतुष्ट नहीं हैं। लेकिन हम अभी भी उपयोग करना चाहते हैं लगभग नियमित वस्तुओं और भंडारण राज्य के लिए arrays। चलो MobX ले लो !


हम मानक क्रियाओं के साथ कई डेटा स्टोरेज शुरू करते हैं:


 import { observable, action } from "mobx"; export class UserStore { byId = observable.map<number, User>(); @action add(user: User) { this.byId.set(user.id, user); } // ... } export class PostStore { // ... } 

हम व्यापार तर्क, 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 => { // ... }); const AccountPage = provider(AccountService)(observer(props => { // ... })); const UserFrom = observer(props => { const accountService = useInstance(AccountService); // ... }); 

परिणाम एक क्लासिक त्रि-स्तरीय वास्तुकला है।


3. द गुड


कभी-कभी विषय क्षेत्र इतना जटिल हो जाता है कि सरल वस्तुओं (या डीडीसी के संदर्भ में एनीमिक मॉडल) का उपयोग करके इसके साथ काम करना पहले से ही असुविधाजनक है। यह विशेष रूप से ध्यान देने योग्य है जब डेटा में कई संबंधों के साथ संबंधपरक संरचना होती है। ऐसे मामलों में, MobX स्टेट ट्री लाइब्रेरी बचाव में आती है, जिससे आप फ्रंट-एंड एप्लिकेशन की वास्तुकला में डोमेन-चालित डिजाइन के सिद्धांतों को लागू कर सकते हैं।


एक मॉडल का डिजाइन प्रकारों के विवरण के साथ शुरू होता है:


 // models/Post.ts import { types as t, Instance } from "mobx-state-tree"; export const Post = t .model("Post", { id: t.identifier, title: t.string, body: t.string, date: t.Date, rating: t.number, author: t.reference(User), comments: t.array(t.reference(Comment)) }) .actions(self => ({ voteUp() { self.rating++; }, voteDown() { self.rating--; }, addComment(comment: Comment) { self.comments.push(comment); } })); export type Post = Instance<typeof Post>; 

मॉडल / उपयोगकर्ता
 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>; 

मॉडल / टिप्पणी
 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>; 

और डेटा स्टोर का प्रकार:


 // models/index.ts import { types as t } from "mobx-state-tree"; export { User, Post, Comment }; export default t.model({ users: t.map(User), posts: t.map(Post), comments: t.map(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() का उपयोग करके स्नैपशॉट लागू कर सकते हैं। यह हमें सर्वर से राज्य को कोड की कई लाइनों में शुरू करने की अनुमति देता है, लोकलस्टोरेज से लोड करता है, या यहां तक ​​कि Redux DevTools के माध्यम से इसके साथ बातचीत भी करता है।


चूंकि हम एक सामान्यीकृत संबंधपरक मॉडल का उपयोग करते हैं, इसलिए हमें डेटा लोड करने के लिए सामान्य पुस्तकालय की आवश्यकता होती है। यह आपको डेटा योजना के अनुसार id द्वारा वर्गीकृत वस्तुओं के फ्लैट टेबल में पेड़ JSON का अनुवाद करने की अनुमति देता है। बस प्रारूप में है कि 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", { //   - //      author: UserSchema, comments: [CommentSchema] }); export { UserSchema, PostSchema, CommentSchema }; 

और डेटा को स्टोरेज में लोड करें:


 import { inject } from "react-ioc"; import { normalize } from "normalizr"; import { applySnapshot } from "mobx-state-tree"; export class PostService { @inject dataContext: DataContext; // ... async loadPosts() { const response = await fetch("/posts.json"); const posts = await response.json(); const { entities } = normalize(posts, [PostSchema]); applySnapshot(this.dataContext, entities); } // ... } 

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

यह सभी समान तीन-परत वास्तुकला को बदल देता है, लेकिन डेटा प्रकारों की स्थिति और रनटाइम सत्यापन (डीईवी मोड में) बनाए रखने की क्षमता के साथ। उत्तरार्द्ध आपको यह सुनिश्चित करने की अनुमति देता है कि यदि कोई अपवाद नहीं होता है, तो डेटा वेयरहाउस की स्थिति विनिर्देश से मेल खाती है।







जो लोग रुचि रखते थे, उनके लिए एक लिंक github और एक डेमो

Source: https://habr.com/ru/post/hi431942/


All Articles