omi/tutorial/omi-mvvm.md

8.0 KiB

Omi 5.0 Released - MVVM comes back bravely

Written in front

Omi officially released 5.0, still focusing on View, but more friendly integration of MVVM architecture, a complete separation of view and business logic architecture.

mvvm

You can quickly experience MVVM through omi-cli:

$ npm i omi-cli -g        
$ omi init-mvvm my-app    
$ cd my-app         
$ npm start                   
$ npm run build             

or:

npx omi-cli init-mvvm my-app (npm v5.2.0+)

Other templates:

Template Type Command Describe
Base Template omi init my-app Basic template for omi project.
TypeScript Template(omi-cli v3.0.5+) omi init-ts my-app Basic template with typescript.
SPA Template(omi-cli v3.0.10+) omi init-spa my-app Single page application template with omi-router.
omi-mp Template(omi-cli v3.0.13+) omi init-mp my-app Developing web with mini program template.

MVVM evolution

MVVM is actually evolved from MVC and MVP.

mvvm

The purpose is to separate views and models, but in MVC, views depend on models, and the degree of coupling is too high, which leads to a great reduction in the portability of views. In MVP, views do not depend on models directly, and P(Presenter) is responsible for completing the interaction between Models and Views. MVVM and MVP are similar. ViewModel plays the role of Presenter and provides the data source needed for UI view, instead of directly letting View use the data source of Model. This greatly improves the portability of View and Model, such as using Flash, HTML, WPF rendering for the same model switch, such as using different models for the same View. As long as the Model and ViewModel are mapped well, View can be changed very little or not.

Mappingjs

Of course, there is a problem with MVVM. Data in Model is mapped to ViewModel to provide the view binding. How to map? Manual mapping? Automatic mapping? In ASP.NET MVC, there are powerful AutoMapper for mapping. For the JS environment, I specially encapsulated mappingjs to map Model to ViewModel.

npm i mappingjs

Usage

Auto Mapping

class TodoItem {
  constructor(text, completed) {
    this.text = text
    this.completed = completed || false

    this.author = {
      firstName: 'dnt',
      lastName: 'zhang'
    }
  }
}

const res = mapping(new TodoItem('task'), { author: { a: 1 } })

deepEqual(res, {
  author: {
    firstName: "dnt",
    lastName: "zhang",
    a: 1
  },
  completed: false,
  text: "task"
})

Omi MVVM Todo

Define TodoItem Model:

let id = 0

export default class TodoItem {
  constructor(text, completed) {
    this.id = id++
    this.text = text
    this.completed = completed || false
  }
}

Define Todo Model:

import TodoItem from './todo-item'
import { getAll, add } from './todo-server'

export default class Todo {
  constructor() {
    this.items = []

    this.author = {
      firstName: 'dnt',
      lastName: 'zhang'
    }
  }

  initItems(list) {
    list.forEach(item => {
      this.items.push(new TodoItem(item.text))
    })
  }

  add(content) {
    const item = new TodoItem(content)
    this.items.push(item)
    add(item)
  }

  updateContent(id, content) {
    this.items.every(item => {
      if (id === item.id) {
        item.content = content
        return false
      }
    })
  }

  complete(id) {
    this.items.every(item => {
      if (id === item.id) {
        item.completed = true
        return false
      }
      return true
    })
  }

  uncomplete(id) {
    this.items.every(item => {
      if (id === item.id) {
        item.completed = false
        return false
      }
      return true
    })
  }

  remove(id) {
    this.items.every((item, index) => {
      if (id === item.id) {
        this.items.splice(index, 1)
        return false
      }
    })
  }

  clear() {
    this.items.length = 0
  }

  getAll(callback) {
    getAll(list => {
      this.initItems(list)
      callback()
    })
  }
}

Define ViewModel:

import mapping from 'mappingjs'
import todo from '../model/todo'

class TodoViewModel {
  constructor() {
    this.data = {
      items: []
    }
  }

  update() {
    //auto mapping here!!!!
    mapping.auto(todo, this.data)
  }

  complete(id) {
    todo.complete(id)
    this.update()
  }

  uncomplete(id) {
    todo.uncomplete(id)
    this.update()
  }

  add(text) {
    todo.add(text)
    this.update()
  }

  getAll() {
    todo.getAll(() => {
      this.update()
    })
  }
}

const vd = new TodoViewModel()

export default vd

VM only focuses on update data, and views are automatically updated

Define View, note that the following inherits from ModelView not WeElement.

import { ModelView, define } from 'omi'
import vm from '../view-model/todo'
import './todo-list'

define('todo-app', class extends ModelView {
  vm = vm

  install() {
    vm.getAll()
  }

  css() {
    return `
    span{
       color: #888;
       font-size: 11px;
     }
    `
  }

  render(props, data) {
    return (
      <div>
        <h3>
          TODO by <span>by {data.author.firstName + data.author.lastName}</span>
        </h3>
        <todo-list items={data.items} />
        <form onSubmit={this.handleSubmit}>
          <input onChange={this.handleChange} value={this.text} />
          <button>Add #{data.items.length + 1}</button>
        </form>
        <other-view />
      </div>
    )
  }

  handleChange = e => {
    this.text = e.target.value
  }

  handleSubmit = e => {
    e.preventDefault()
    if (this.text !== '') {
      //send action to vm
      vm.add(this.text)
      this.text = ''
    }
  }
})
  • All data is injected through VM
  • So instructions are sent through VM

Define TodoList View:

import { define, WeElement } from 'omi'
import vm from '../view-model/todo'

define('todo-list', class extends WeElement {
  css() {
    return `
    .completed{
      color: #d9d9d9;
      text-decoration: line-through;
    }
   `
  }

  onChange = (evt, id) => {
    if (evt.target.checked) {
      vm.complete(id)
    } else {
      vm.uncomplete(id)
    }
  }

  render(props) {
    return (
      <ul>
        {props.items.map(item => (
          <li class={item.completed && 'completed'}>
            <input
              type="checkbox"
              onChange={evt => {
                this.onChange(evt, item.id)
              }}
            />
            {item.text}
          </li>
        ))}
      </ul>
    )
  }
})

→ All the source code

Summary

From a macro perspective, Omi's MVVM architecture also has attributed mesh architecture. At present, mesh architecture has:

  • Mobx + React
  • Hooks + React
  • MVVM (Omi)

General trend! It's a best practice for front-end engineering! It can also be understood as the best way to describe and abstract the world. Where is the net?

  • ViewModel and ViewModel are interdependent or even cyclically dependent mesh structures
  • ViewModel is one-to-one, many-to-one, one-to-many, and many-to-many dependent Models to form a network structure
  • Models and Models form interdependent and even cyclically dependent meshes
  • View relies one-to-one on ViewModel to form a mesh structure

Summarized as follows:

Model ViewModel View
Model many-to-many many-to-many unrelated
ViewModel many-to-many many-to-many one-to-one
View unrelated one-to-one many-to-many

Star & Fork