ReactJS + MobX - Experiencia DI

Me parece que ha llegado el momento de compartir el enfoque para escribir la aplicación ReactJS, no pretendo ser único.

El primer párrafo se puede omitir . He estado involucrado en el desarrollo web durante mucho tiempo, pero durante los últimos cuatro años he estado sentado en ReactJS y todo me conviene, tuve redux en mi vida, pero hace unos dos años conocí a MobX, hace solo un par de meses intenté volver a redux, pero no lo hice. Podría, tenía la sensación de que estaba haciendo algo superfluo, tal vez algo no estaba bien en absoluto, ya se habían traducido muchos bytes en los servidores sobre este tema, el artículo no trataba sobre la frescura de uno antes que el otro, esto es solo un intento de compartir mis mejores prácticas, tal vez alguien realmente este enfoque irá, y hasta el punto.

Las tareas que resolveremos:

  • di conexión para componentes
  • Representación del servidor con carga de datos asincrónica

La estructura del proyecto se puede ver en Github . Por lo tanto, omitiré cómo escribir una aplicación primitiva y el artículo solo resaltará

Introducimos conceptos tales como: modelo de datos, servicio, lado.

Consigamos un modelo simple

TodoModel.ts
import { 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; }; } 


lo que ves es la acción establecida, en el modelo es más una excepción que un buen tono, generalmente en un proyecto hay un modelo básico con ayudantes primitivos y simplemente se hereda de él, en los modelos no debería haber ninguna acción para bien.

Ahora tenemos que aprender a trabajar con este modelo, iniciar un servicio:

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


Nuestro servicio tiene un enlace a

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


En esta tienda almacenaremos el estado de nuestra aplicación, puede haber muchas de esas tiendas, pero como lo ha demostrado la práctica, no tiene sentido entrar en muchas tiendas pequeñas. En la tienda, así como en los modelos, no debe haber ninguna acción.

Tenemos casi todo listo, queda por conectar todo a nuestra aplicación, para esto apretaremos ligeramente el inyector de mobx-react:

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


y obtener un contenedor para nuestra 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') ); 


Para el navegador, siempre tenemos un contenedor, pero para el renderizado del servidor que necesita buscar, es mejor organizar su contenedor para cada solicitud:

server.tsx
 import * as express from 'express'; import * as React from 'react'; import { Container } from 'typedi'; import '../application'; // @ts-ignore import * as mustacheExpress from 'mustache-express'; import * as path from 'path'; import { renderToString } from 'react-dom/server'; import { StaticRouter } from 'react-router'; import { Provider } from 'mobx-react'; import * as uuid from 'uuid'; import { renderRoutes, matchRoutes } from 'react-router-config'; import { routes } from '../application/route'; const app = express(); const ROOT_PATH = process.env.ROOT_PATH; const currentPath = path.join(ROOT_PATH, 'dist', 'server'); const publicPath = path.join(ROOT_PATH, 'dist', 'public'); app.engine('html', mustacheExpress()); app.set('view engine', 'html'); app.set('views', currentPath + '/views'); app.use(express.static(publicPath)); app.get('/favicon.ico', (req, res) => res.status(500).end()); app.get('*', async (request, response) => { const context: any = {}; const id = uuid.v4(); const container = Container.of(id); const branch = matchRoutes(routes, request.url); const promises = branch.map(({ route, match }: any) => { return route.component && route.component.loadData ? route.component.loadData(container, match) : Promise.resolve(null); }); await Promise.all(promises); const markup = renderToString( <Provider container={container}> <StaticRouter location={request.url} context={context}> {renderRoutes(routes)} </StaticRouter> </Provider> ); Container.remove(id); if (context.url) { return response.redirect( context.location.pathname + context.location.search ); } return response.render('index', { markup }); }); app.listen(2016, () => { // tslint:disable-next-line console.info("application started at 2016 port"); }); 


El renderizado del servidor es en realidad algo delicado, por un lado, quiero dejar que todo pase a través de él, pero solo tiene una tarea comercial, dar el contenido a los bots , por lo que es mejor verificar algo como "el usuario inició sesión al menos una vez en el sitio" y omita el procesamiento del servidor con la creación de contenedores en el servidor.

Bueno, ahora a nuestros componentes:

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


Todo resulta muy lógico y hermoso aquí, nuestra vista de "render" para dibujar toma datos de nuestra tienda, los ganchos de componentes dicen en qué momento debemos cargar los datos.

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


En mi opinión, trabajar con formularios es mucho más conveniente a través de modelos / dtos, puede usar los formularios nativos habituales y actualizar el modelo de datos y todos los que lo escuchen se actualizarán instantáneamente.

Algo como esto, uso este grupo de bibliotecas: react, class-transformer, mobx, typedi

Ahora estamos utilizando este enfoque en prod. Estos son proyectos muy grandes con componentes y servicios comunes.

Si este enfoque es interesante, le diré cómo, en la misma línea, validamos el modelo antes de enviarlo al servidor, cómo procesamos los errores del servidor y cómo sincronizamos nuestro estado entre las pestañas del navegador.

De hecho, todo es muy extra: "class-validator", "localStorage + window.addEventListener ('almacenamiento')"

Gracias por leer :-)

Ejemplo

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


All Articles