Es scheint mir, dass es an der Zeit ist, den Ansatz zum Schreiben der ReactJS-App zu teilen.
Ich behaupte nicht, einzigartig zu sein.Der erste Absatz kann übersprungen werden . Ich bin schon lange in der Webentwicklung tätig, aber seit vier Jahren sitze ich fest auf ReactJS und alles passt zu mir. Ich hatte Redux in meinem Leben, aber vor ungefähr zwei Jahren habe ich MobX getroffen, erst vor ein paar Monaten habe ich versucht, zu Redux zurückzukehren, aber ich habe es nicht getan Ich konnte, es gab das Gefühl, dass ich etwas Überflüssiges tat, vielleicht stimmte etwas überhaupt nicht, viele Bytes auf den Servern waren bereits zu diesem Thema übersetzt worden, der Artikel handelte nicht von der Coolness des einen vor dem anderen, dies ist nur ein Versuch, meine Best Practices zu teilen, vielleicht jemand wirklich Dieser Ansatz wird gehen und so auf den Punkt.
Die Aufgaben, die wir lösen werden:- di Anschluss für Komponenten
- Server-Rendering mit asynchronem Laden von Daten
Die Struktur des Projekts kann auf
Github eingesehen werden. Daher überspringe ich das Schreiben einer primitiven Anwendung und der Artikel wird nur hervorheben
Wir führen Konzepte ein wie: Datenmodell, Service, Seite.
Lassen Sie uns ein einfaches Modell bekommen
TodoModel.tsimport { observable, action } from 'mobx'; export class TodoModel { @observable public id: number; @observable public text: string = ''; @observable public isCompleted: boolean = false; @action public set = (key: 'text' | 'isCompleted', value: any): void => { this[key] = value; }; }
Was Sie sehen, ist die festgelegte Aktion. Im Modell ist dies eher eine Ausnahme als ein guter Ton. In der Regel gibt es in einem Projekt ein Basismodell mit primitiven Helfern, das nur von ihm geerbt wurde. In Modellen sollten keine Aktionen für immer vorhanden sein.
Jetzt müssen wir lernen, wie man mit diesem Modell arbeitet, einen Service starten:
TodoService.ts import { Service, Inject } from 'typedi'; import { plainToClass, classToClass } from 'class-transformer'; import { DataStorage } from '../storage/DataStorage'; import { action } from 'mobx'; import { TodoModel } from '../models/TodoModel'; const responseMock = { items: [ { id: 1, isCompleted: false, text: 'Item 1' }, { id: 2, isCompleted: true, text: 'Item 2' } ] }; @Service('TodoService') export class TodoService { @Inject('DataStorage') public dataStorage: DataStorage; @action public load = async () => { await new Promise(resolve => setTimeout(resolve, 300)); this.dataStorage.todos = plainToClass(TodoModel, responseMock.items); }; @action public save(todo: TodoModel): void { if (todo.id) { const idx = this.dataStorage.todos.findIndex(item => todo.id === item.id); this.dataStorage.todos[idx] = classToClass(todo); } else { const todos = this.dataStorage.todos.slice(); todo.id = Math.floor(Math.random() * Math.floor(100000)); todos.push(todo); this.dataStorage.todos = todos; } this.clearTodo(); } @action public edit(todo: TodoModel): void { this.dataStorage.todo = classToClass(todo); } @action public clearTodo(): void { this.dataStorage.todo = new TodoModel(); } }
Unser Service hat einen Link zu
DataStorage.ts import { Service } from 'typedi'; import { observable } from 'mobx'; import { TodoModel } from '../models/TodoModel'; @Service('DataStorage') export class DataStorage { @observable public todos: TodoModel[] = []; @observable public todo: TodoModel = new TodoModel(); }
In diesem Geschäft werden wir den Status unserer Anwendung speichern, es kann viele solcher Geschäfte geben, aber wie die Praxis gezeigt hat, macht es keinen Sinn, in viele kleine Geschäfte einzubrechen. Sowohl im Laden als auch in den Modellen sollte es keine Aktion geben.
Wir haben fast alles fertig, es bleibt noch alles mit unserer Anwendung zu verbinden, dafür werden wir den Injektor von mobx-react leicht festziehen:
DI import { inject } from 'mobx-react'; export function DI(...classNames: string[]) { return (target: any) => { return inject((props: any) => { const data: any = {}; classNames.forEach(className => { const name = className.charAt(0).toLowerCase() + className.slice(1); data[name] = props.container.get(className); }); data.container = props.container; return data; })(target); }; }
und holen Sie sich einen Container für unseren DI
browser.tsx import 'reflect-metadata'; import * as React from 'react'; import { hydrate } from 'react-dom'; import { renderRoutes } from 'react-router-config'; import { Provider } from 'mobx-react'; import { BrowserRouter } from 'react-router-dom'; import { Container } from 'typedi'; import '../application'; import { routes } from '../application/route'; hydrate( <Provider container={Container}> <BrowserRouter>{renderRoutes(routes)}</BrowserRouter> </Provider>, document.getElementById('root') );
Für den Browser haben wir immer einen Container, aber für das Server-Rendering, das Sie suchen müssen, ist es besser, Ihren Container für jede Anforderung zu organisieren:
server.tsx import * as express from 'express'; import * as React from 'react'; import { Container } from 'typedi'; import '../application';
Das Server-Rendering ist eigentlich eine heikle Sache. Einerseits möchte ich alles durchlassen,
aber es hat nur eine Geschäftsaufgabe, den Inhalt den Bots zu geben. Daher ist es besser, nach etwas zu suchen, das sich "Hat sich der Benutzer mindestens einmal auf der Site angemeldet?" und überspringen Sie das Server-Rendering, indem Sie Container auf dem Server erstellen.
Nun zu unseren Komponenten:
MainRoute.tsx import * as React from 'react'; import { TodoService } from '../service/TodoService'; import { observer } from 'mobx-react'; import { DI } from '../annotation/DI'; import { DataStorage } from '../storage/DataStorage'; import { Todo } from '../component/todo'; import { Form } from '../component/form/Form'; import { ContainerInstance } from 'typedi'; interface IProps { todoService?: TodoService; dataStorage?: DataStorage; } @DI('TodoService', 'DataStorage') @observer export class MainRoute extends React.Component<IProps> { public static async loadData(container: ContainerInstance) { const todoService: TodoService = container.get('TodoService'); await todoService.load(); } public componentDidMount() { this.props.todoService.load(); } public render() { return ( <div> <Form /> <ul> {this.props.dataStorage.items.map(item => ( <li key={item.id} ><Todo model={item} /></li> ))} </ul> </div> ); } }
Hier wird alles sehr logisch und schön. In unserer „Render“ -Ansicht zum Zeichnen werden Daten aus unserem Geschäft entnommen. Komponenten-Hooks geben an, zu welchem Zeitpunkt wir die Daten laden sollen.
Todo.tsx import * as React from 'react'; import { TodoModel } from '../../models/TodoModel'; import { TodoService } from '../../service/TodoService'; import { DI } from '../../annotation/DI'; import { observer } from 'mobx-react'; interface IProps { model: TodoModel; todoService?: TodoService; } @DI('TodoService') @observer export class Todo extends React.Component<IProps> { public render() { const { model, todoService } = this.props; return ( <> <input type='checkbox' checked={model.isCompleted} onChange={e => model.set('isCompleted', e.target.checked)} /> <h4>{model.text}</h4> <button type='button' onClick={() => todoService.edit(model)}>Edit</button> </> ); } }
Form.tsx import * as React from 'react'; import { observer } from 'mobx-react'; import { DI } from '../../annotation/DI'; import { TodoService } from '../../service'; import { DataStorage } from '../../storage'; import { TextField } from '../text-field'; interface IProps { todoService?: TodoService; dataStorage?: DataStorage; } @DI('TodoService', 'DataStorage') @observer export class Form extends React.Component<IProps> { public handleSave = (e: any) => { e.preventDefault(); this.props.todoService.save(this.props.dataStorage.todo); }; public handleClear = () => { this.props.todoService.clearTodo(); }; public render() { const { dataStorage } = this.props; return ( <form onSubmit={this.handleSave}> <TextField name='text' model={dataStorage.todo} /> <button>{dataStorage.todo.id ? 'Save' : 'Create'}</button> <button type='button' onClick={this.handleClear}> Clear </button> </form> ); } }
Meiner Meinung nach ist das Arbeiten mit Formularen über models / dtos viel bequemer. Sie können die üblichen nativen Formulare verwenden und das Datenmodell aktualisieren, und jeder, der es hört, wird sofort aktualisiert.
So etwas benutze ich diese Reihe von Bibliotheken: Reagieren, Klassentransformator, Mobx, Typedi
Wir verwenden diesen Ansatz jetzt in prod. Dies sind sehr große Projekte mit gemeinsamen Komponenten und Diensten.
Wenn dieser Ansatz interessant ist, erkläre ich Ihnen, wie wir Modelle auf die gleiche Weise validieren, bevor wir sie an den Server senden, wie wir Serverfehler verarbeiten und wie wir unseren Status zwischen Browser-Registerkarten synchronisieren.
In der Tat ist alles sehr Bonus: "Klassenvalidator", "localStorage + window.addEventListener ('Speicher')"
Danke fürs Lesen :-)
Beispiel