SPA berbasis browser tanpa framework

1. Tapi ... kenapa?

  1. Ada banyak sekali kerangka kerja untuk mengembangkan  SPA  (Aplikasi Halaman Tunggal).





  2. Ada banyak sekali dokumentasi yang menggambarkan cara membuat aplikasi berdasarkan kerangka kerja tertentu.





  3. 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:





  1. Pengembang baru dapat memahami tujuan aplikasi dengan melihat sekilas struktur kodenya.





  2. Pemisahan masalah dipromosikan dan karenanya modularitas kode sehingga:





    • Modul mudah untuk diuji





    •   (boundaries)          .       « »





  3.     .  ,       .





  4. . ( )     , .





  5.      .





  6.     . ,     .





:





 .   (  ) 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)   ,  ,     .





:





  1.  — .





    • goods-list      , .





    • filters    ,   .





  2. (    )    —   «.     .





    • _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 .   -.





:   asm.jsblazor  elm     





6.  

, : HTML, CSS, JavaScript. ,   4: , .





  [6.1]  HTML  CSS    .





HTML     . ,  underscore.jshandlebars.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 ,  ReactPreact and Inferno. Tsx   HTML,     / HTML.  tsx ..     HTML, .





  ,   , ,   JSX .        React.





:         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.

  , / .   6        .   .





«» , ( )   /, .   (slicing function). , ,   ?





 8: ( /slicing function)





  (  ):





type TViewModelFacade = <TViewModel, TOwnPropsTVMProps>(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:  reducersaction 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)








All Articles