1. Tapi ... kenapa?
Ada banyak sekali kerangka kerja untuk mengembangkan SPA (Aplikasi Halaman Tunggal).
Ada banyak sekali dokumentasi yang menggambarkan cara membuat aplikasi berdasarkan kerangka kerja tertentu.
Namun, dokumentasi semacam itu menempatkan kerangka kerja di garis depan. Dengan demikian, mengubah kerangka kerja dari detail implementasi menjadi faktor penentu. Dengan demikian, sebagian besar kode ditulis bukan untuk memenuhi kebutuhan bisnis, tetapi untuk memenuhi kebutuhan kerangka kerja.
Mempertimbangkan bagaimana pengembangan perangkat lunak yang digerakkan oleh hype saat ini, Anda dapat yakin bahwa dalam beberapa tahun akan ada kerangka kerja modis baru untuk pengembangan front-end. Saat kerangka kerja yang menjadi dasar aplikasi dibangun tidak lagi populer, Anda dipaksa untuk mempertahankan basis kode lama atau memulai proses mentransfer aplikasi ke kerangka baru.
Kedua opsi tersebut merugikan bisnis. Mempertahankan basis kode yang ketinggalan zaman berarti masalah dengan mempekerjakan pengembang baru dan memotivasi pengembang saat ini. Mentransfer aplikasi ke kerangka kerja baru membutuhkan waktu (dan karenanya uang) tetapi tidak membawa keuntungan bisnis apa pun.
Artikel ini adalah contoh membangun SPA menggunakan prinsip desain arsitektur tingkat tinggi. Dengan demikian, perpustakaan dan kerangka kerja tertentu dipilih untuk memenuhi tanggung jawab yang ditentukan oleh arsitektur yang diinginkan.
2. Tujuan dan batasan arsitektural
Tujuan:
Pengembang baru dapat memahami tujuan aplikasi dengan melihat sekilas struktur kodenya.
Pemisahan masalah dipromosikan dan karenanya modularitas kode sehingga:
Modul mudah untuk diuji
(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 . . .
Dibandingkan dengan mobx-state-tree : Viewmodels adalah kelas biasa dan tidak memerlukan penggunaan fungsi dari pustaka pihak ketiga, juga tidak harus memenuhi antarmuka yang ditentukan oleh kerangka kerja pihak ketiga. Definisi tipe di dalam mobx-state-tree bergantung pada fungsi spesifik dari paket ini. Menggunakan mobx-state-tree dalam hubungannya dengan TypeScript memicu duplikasi informasi - bidang tipe dideklarasikan sebagai antarmuka TypeScript terpisah tetapi harus terdaftar di objek yang digunakan untuk menentukan tipe.
Artikel asli dalam bahasa Inggris di blog penulis (saya)