1. But ... why?
There are a huge number of frameworks for developing SPA (Single Page Application).
There is a huge amount of documentation that illustrates how to create an application based on a specific framework.
However, any such documentation puts the framework at the forefront. Thus, turning the framework from an implementation detail into a determining factor. Thus, a significant part of the code is written not to meet the needs of the business, but to meet the needs of the framework.
Considering how hype-driven software development is nowadays, you can be sure that in a few years there will be new fashionable frameworks for front-end development. At the moment when the framework on which the application is built goes out of fashion, you are forced to either maintain the legacy code base or start the process of transferring the application to a new framework.
Both options are detrimental to the business. Maintaining an outdated codebase means problems with hiring new and motivating current developers. Transferring an application to a new framework costs time (and therefore money) but does not bring any business benefit.
This article is an example of building a SPA using high-level architecture design principles. In doing so, specific libraries and frameworks are selected to meet the responsibilities defined by the desired architecture.
2. Architectural goals and limitations
Objectives:
A new developer can understand the purpose of an application by taking a quick look at the structure of the code.
Separation of concerns is promoted and hence modularity of the code so that:
Modules are easy to test
(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 . . .
Compared to mobx-state-tree : Viewmodels are regular classes and do not require the use of functions from third-party libraries, nor do they have to satisfy the interface defined by third-party frameworks. The type definition inside the mobx-state-tree relies on the specific functions of this package. Using mobx-state-tree in conjunction with TypeScript provokes duplication of information - type fields are declared as a separate TypeScript interface but must be listed in the object used to define the type.
The original article in English in the blog of the author (me)