1. Pero ... ¿por qué?
Hay una gran cantidad de marcos para desarrollar SPA (aplicación de página única).
Existe una gran cantidad de documentación que ilustra cómo crear una aplicación basada en un marco específico.
Sin embargo, dicha documentación pone el marco a la vanguardia. De esta forma, convertir el marco de un detalle de implementación en un factor determinante. Por lo tanto, una parte importante del código está escrita no para satisfacer las necesidades de la empresa, sino para satisfacer las necesidades del marco.
Teniendo en cuenta lo impulsado por el desarrollo de software en la actualidad, puede estar seguro de que en unos años habrá nuevos marcos de moda para el desarrollo de aplicaciones para el usuario. En el momento en que el marco sobre el que se construye la aplicación pasa de moda, se ve obligado a mantener la base del código heredado o iniciar el proceso de transferencia de la aplicación a un nuevo marco.
Ambas opciones son perjudiciales para el negocio. Mantener una base de código obsoleta significa problemas para contratar nuevos desarrolladores y motivar a los actuales. Transferir una aplicación a un nuevo marco cuesta tiempo (y por lo tanto dinero) pero no aporta ningún beneficio comercial.
Este artículo es un ejemplo de cómo construir un SPA utilizando principios de diseño de arquitectura de alto nivel. Al hacerlo, se eligen bibliotecas y marcos específicos para cumplir con las responsabilidades definidas por la arquitectura deseada.
2. Objetivos y limitaciones arquitectónicos
Objetivos:
Un nuevo desarrollador puede comprender el propósito de una aplicación con un vistazo rápido a la estructura del código.
Se promueve la separación de preocupaciones y, por lo tanto, la modularidad del código para que:
Los módulos son fáciles de probar
(boundaries) . « »
-
. ( ) , .
.
. , .
:
. ( ) HTML+CSS JavaScript .
3.
. : (layered), (onion) (hexagonal). .
/ SPA . (domain) (application) . , — .
, .
( Ports and Adapters) . localStorage TodoMVC ( boundaries/local-storage).
4. . SPA ?
. :
1: ,
? 2 .
2: , 1
‘shared’ UI , , , .
( ) . ‘’ ‘parts’. ( 3).
3: ‘parts’
, ’goods catalogue’. ‘goods-catalogue/parts/goods-list/parts/good-details.js’ . — .
«parts» . 4.
4: ‘parts’
‘goods-catalogue/goods-list’ . goods-list.js () — , . , - (js, html, css) , , .
:
— .
goods-list , .
filters , .
( ) — «_». .
_goods-list folder goods-catalogue .
goods-list.js _goods-list .
_good-details.js _goods-list .
5: «_»
! , . . pages components 5. HTML component. components , «» .
5. . JavaScript?
JavaScript. . ( 1-20), ...
, . . 4- . , 4 . . , 2015 , . , , .
JavaScript (babel) JavaScript, « » JavaScript. — , .
, — TypeScript :
- JavaScript, JavaScript
(typings) JavaScript . , npm . , TypeScript . -.
6.
, : HTML, CSS, JavaScript. , 4: , .
[6.1] HTML CSS .
HTML . , underscore.js, handlebars.js. , .
[6.2] TypeScript , (). .
UI . HTML HTML . . . . , .
[6.3] . .
[6.4] :
, .
. .
Domain Application. , Dependency Injection. .
— . . , , ----html-. . , .
, , . , . :
, .. .
.. .
, [6.5] — TypeScript . , .
, :
(Components) — HTML + CSS
(ViewModels) — , , ( ).
(ViewModel facades) — , .
6:
- . .
().
— . / . «shared».
— . /.
? 6 . () . , .
[6.6] — .
7:
7.
. — .
7.1.
- tsx ( jsx). tsx , React, Preact and Inferno. Tsx HTML, / HTML. tsx .. HTML, .
: React. react hooks - . API React , .
, . UI=F(S)
UI —
F —
S — ( — )
:
interface ITodoItemAttributes {
name: string;
status: TodoStatus;
toggleStatus: () => void;
removeTodo: () => void;
}
const TodoItemDisconnected = (props: ITodoItemAttributes) => {
const className = props.status === TodoStatus.Completed ? 'completed' : '';
return (
<li className={className}>
<div className="view">
<input className="toggle" type="checkbox" onChange={props.toggleStatus} checked={props.status === TodoStatus.Completed} />
<label>{props.name}</label>
<button className="destroy" onClick={props.removeTodo} />
</div>
</li>
)
}
todo TodoMVC .
— JSX. . , «».
[6.1] [6.2].
: react TodoMVC .
7.2. ()
, TypeScript -:
.
domain/application dependency injection.
, , .
(reactive UI). . WPF (C#) Model-View-ViewModel. JavaScript , (observable) (stores) flux. , :
.
, .
.
, .
:
, , .
, .
mobx , . :
class TodosVM {
@mobx.observable
private todoList: ITodoItem[];
// use "poor man DI", but in the real applications todoDao will be initialized by the call to IoC container
constructor(props: { status: TodoStatus }, private readonly todoDao: ITodoDAO = new TodoDAO()) {
this.todoList = [];
}
public initialize() {
this.todoList = this.todoDao.getList();
}
@mobx.action
public removeTodo = (id: number) => {
const targetItemIndex = this.todoList.findIndex(x => x.id === id);
this.todoList.splice(targetItemIndex, 1);
this.todoDao.delete(id);
}
public getTodoItems = (filter?: TodoStatus) => {
return this.todoList.filter(x => !filter || x.status === filter) as ReadonlyArray<Readonly<ITodoItem>>;
}
/// ... other methods such as creation and status toggling of todo items ...
}
mobx , .
mobx . mobx. .
{status: TodoStatus}
. [6.6]. . :
interface IVMConstructor<TProps, TVM extends IViewModel<TProps>> {
new (props: TProps, ...dependencies: any[]) : TVM;
}
interface IViewModel<IProps = Record<string, unknown>> {
initialize?: () => Promise<void> | void;
cleanup?: () => void;
onPropsChanged?: (props: IProps) => void;
}
. :
(-).
, ( statefull). .
7, . DOM(mounted) (unmounted). (higher order components).
:
type TWithViewModel = <TAttributes, TViewModelProps, TViewModel> ( moduleRootComponent: Component<TAttributes & TViewModelProps>, vmConstructor: IVMConstructor<TAttributes, TViewModel>, ) => Component<TAttributes>
moduleRootComponent, :
(mount) .
() (unmount).
TodoMVC . .. IoC , .
:
const TodoMVCDisconnected = (props: { status: TodoStatus }) => {
return <section className="todoapp">
<Header />
<TodoList status={props.status} />
<Footer selectedStatus={props.status} />
</section>
};
const TodoMVC = withVM(TodoMVCDisconnected, TodosVM);
( , ), <TodoMVC status={statusReceivedFromRouteParameters} />
. , TodosVM
- TodoMVC
.
, , withVM.
TodoMVCDisconnected
TodoMVC ,
TodosVM . , , mobx .
: , withVM react context API. . , — connectFn .
7.3.
«» , ( ) /, . (slicing function). , , ?
8: ( /slicing function)
( ):
type TViewModelFacade = <TViewModel, TOwnProps, TVMProps>(vm: TViewModel, ownProps?: TOwnProps) => TVMProps
connect Redux. mapStateToProps
, mapDispatchToActions
mergeProps
— , . TodoItemDisconnected
TodosVM
.
const sliceTodosVMProps = (vm: TodosVM, ownProps: {id: string, name: string, status: TodoStatus; }) => {
return {
toggleStatus: () => vm.toggleStatus(ownProps.id),
removeTodo: () => vm.removeTodo(ownProps.id),
}
}
: , ‘OwnProps’ - react/redux.
— . withVM
. , , — , :
type connectFn = <TViewModel, TVMProps, TOwnProps = {}> ( ComponentToConnect: Component<TVMProps & TOwnProps>, mapVMToProps: TViewModelFacade<TViewModel, TOwnProps, TVMProps>, ) => Component<TOwnProps> const TodoItem = connectFn(TodoItemDisconnected, sliceTodosVMProps);
todo : <TodoItem id={itemId} name={itemName} status={itemStatus} />
connectFn
:
TodoItemDisconnected
sliceTodosVMProps
— JSX.
, , , .
connectFn TodoMVC , .
8.
, , . TypeScript , , TSX — .
SPA . SPA « » « ».
, ?
- mobx, react mobx-react , :
mobx
- , . TodoMVC react-router react-router-dom.
, , JSX.
, .
, .
. React , .
P.S. SPA:
React/Redux: reducers, action creators middlewares. ( stateful). time-travel. . connect . Redux-dirven connected . , .
vue: TSX. , , . Vue.js ‘data’,’methods’, .. vue- .
angular: TSX. angular- . (two-way data binding). : , , .
react (hooks, useState/useContext): . , - . :
.
useEffect ‘deps’ .
.
.
, ( — useEffect) . , «», « (mental model)» « (best practices)». react. :
react-mobx . react-mobx . . .
En comparación con mobx-state-tree : Viewmodels son clases regulares y no requieren el uso de funciones de bibliotecas de terceros, ni tienen que satisfacer la interfaz definida por marcos de terceros. La definición de tipo dentro del árbol de estado de mobx se basa en las funciones específicas de este paquete. El uso de mobx-state-tree junto con TypeScript provoca la duplicación de información: los campos de tipo se declaran como una interfaz TypeScript separada, pero deben incluirse en el objeto utilizado para definir el tipo.
El artículo original en inglés en el blog del autor (yo)