Omi 5.0 发布 - Web 前端 MVVM 王者归来

omi

写在前面

Omi 正式发布 5.0,依然专注于 View,但是对 MVVM 架构更加友好的集成,彻底分离视图与业务逻辑的架构。

mvvm

你可以通过 omi-cli 快速体验 MVVM:

  1. $ npm i omi-cli -g
  2. $ omi init-mvvm my-app
  3. $ cd my-app
  4. $ npm start
  5. $ npm run build

npx omi-cli init-mvvm my-app 也支持(要求 npm v5.2.0+)

其他模板:

Template Type Command Describe
Base Template omi init my-app 基础模板
TypeScript Template(omi-cli v3.0.5+) omi init-ts my-app 使用 TypeScript 的模板
SPA Template(omi-cli v3.0.10+) omi init-spa my-app 使用 omi-router 单页应用的模板
omi-mp Template(omi-cli v3.0.13+) omi init-mp my-app 小程序开发 Web 的模板

MVVM 演化

MVVM 其实本质是由 MVC、MVP 演化而来。

mvvm

目的都是分离视图和模型,但是在 MVC 中,视图依赖模型,耦合度太高,导致视图的可移植性大大降低,在 MVP 模式中,视图不直接依赖模型,由 P(Presenter)负责完成 Model 和 View 的交互。MVVM 和 MVP 的模式比较接近。ViewModel 担任这 Presenter 的角色,并且提供 UI 视图所需要的数据源,而不是直接让 View 使用 Model 的数据源,这样大大提高了 View 和 Model 的可移植性,比如同样的 Model 切换使用 Flash、HTML、WPF 渲染,比如同样 View 使用不同的 Model,只要 Model 和 ViewModel 映射好,View 可以改动很小甚至不用改变。

Mappingjs

当然 MVVM 这里会出现一个问题, Model 里的数据映射到 ViewModel 提供该视图绑定,怎么映射?手动映射?自动映射?在 ASP.NET MVC 中,有强大的 AutoMapper 用来映射。针对 JS 环境,我特地封装了 mappingjs 用来映射 Model 到 ViewModel。

Omi 5.0 发布 - Web 前端 MVVM 王者归来 - 图4

你可以通后 npm 安装使用:

  1. npm i mappingjs

使用方式

  1. var a = { a: 1 }
  2. var b = { b: 2 }
  3. deepEqual(mapping({
  4. from: a,
  5. to: b
  6. }), { a: 1, b: 2 })

自动 Mapping

  1. class TodoItem {
  2. constructor(text, completed) {
  3. this.text = text
  4. this.completed = completed || false
  5. this.author = {
  6. firstName: 'dnt',
  7. lastName: 'zhang'
  8. }
  9. }
  10. }
  11. const res = mapping.auto(new TodoItem('task'))
  12. deepEqual(res, {
  13. author: {
  14. firstName: "dnt",
  15. lastName: "zhang"
  16. },
  17. completed: false,
  18. text: "task"
  19. })

带有初始值{ author: { a: 1 } }的自动映射:

  1. const res = mapping.auto(new TodoItem('task'), { author: { a: 1 } })
  2. deepEqual(res, {
  3. author: {
  4. firstName: "dnt",
  5. lastName: "zhang",
  6. a: 1
  7. },
  8. completed: false,
  9. text: "task"
  10. })

Omi MVVM Todo 实战

定义 TodoItem Model:

  1. let id = 0
  2. export default class TodoItem {
  3. constructor(text, completed) {
  4. this.id = id++
  5. this.text = text
  6. this.completed = completed || false
  7. this.author = {
  8. firstName: 'dnt',
  9. lastName: 'zhang'
  10. }
  11. }
  12. }

定义 Todo Model:

  1. import TodoItem from './todo-item'
  2. import { getAll, add } from './todo-server'
  3. export default class Todo {
  4. constructor() {
  5. this.items = []
  6. this.author = {
  7. firstName: 'dnt',
  8. lastName: 'zhang'
  9. }
  10. }
  11. initItems(list) {
  12. list.forEach(item => {
  13. this.items.push(new TodoItem(item.text))
  14. })
  15. }
  16. add(content) {
  17. const item = new TodoItem(content)
  18. this.items.push(item)
  19. add(item)
  20. }
  21. updateContent(id, content) {
  22. this.items.every(item => {
  23. if (id === item.id) {
  24. item.content = content
  25. return false
  26. }
  27. })
  28. }
  29. complete(id) {
  30. this.items.every(item => {
  31. if (id === item.id) {
  32. item.completed = true
  33. return false
  34. }
  35. return true
  36. })
  37. }
  38. uncomplete(id) {
  39. this.items.every(item => {
  40. if (id === item.id) {
  41. item.completed = false
  42. return false
  43. }
  44. return true
  45. })
  46. }
  47. remove(id) {
  48. this.items.every((item, index) => {
  49. if (id === item.id) {
  50. this.items.splice(index, 1)
  51. return false
  52. }
  53. })
  54. }
  55. clear() {
  56. this.items.length = 0
  57. }
  58. getAll(callback) {
  59. getAll(list => {
  60. this.initItems(list)
  61. callback()
  62. })
  63. }
  64. }

定义 ViewModel:

  1. import mapping from 'mappingjs'
  2. import shared from './shared'
  3. import todoModel from '../model/todo'
  4. import ovm from './other'
  5. class TodoViewModel {
  6. constructor() {
  7. this.data = {
  8. items: []
  9. }
  10. }
  11. update(todo) {
  12. //这里进行自动映射,一行代码轻松搞定
  13. mapping.auto(todo, this.data)
  14. this.data.projName = shared.projName
  15. }
  16. add(text) {
  17. todoModel.add(text)
  18. this.update(todoModel)
  19. ovm.update()
  20. }
  21. getAll() {
  22. todoModel.getAll(() => {
  23. this.update(todoModel)
  24. ovm.update()
  25. })
  26. }
  27. changeSharedData() {
  28. shared.projName = 'I love omi-mvvm.'
  29. ovm.update()
  30. this.update()
  31. }
  32. }
  33. const vd = new TodoViewModel()
  34. export default vd
  • vm 只专注于 update 数据,视图会自动更新
  • 公共的数据或 vm 可通过 import 依赖

定义 View, 注意下面是继承自 ModelView 而非 WeElement。

  1. import { ModelView, define } from 'omi'
  2. import vm from '../view-model/todo'
  3. import './todo-list'
  4. import './other-view'
  5. define('todo-app', class extends ModelView {
  6. vm = vm
  7. onClick = () => {
  8. //view model 发送指令
  9. vm.changeSharedData()
  10. }
  11. install() {
  12. //view model 发送指令
  13. vm.getAll()
  14. }
  15. render(props, data) {
  16. return (
  17. <div>
  18. TODO by <span>by {data.author.firstName + data.author.lastName}</span>
  19. <todo-list items={data.items} />
  20. <form onSubmit={this.handleSubmit}>
  21. <input onChange={this.handleChange} value={this.text} />
  22. <button>Add #{data.items.length + 1}</button>
  23. </form>
  24. <div>{data.projName}</div>
  25. <button onClick={this.onClick}>Change Shared Data</button>
  26. <other-view />
  27. </div>
  28. )
  29. }
  30. handleChange = e => {
  31. this.text = e.target.value
  32. }
  33. handleSubmit = e => {
  34. e.preventDefault()
  35. if(this.text !== ''){
  36. //view model 发送指令
  37. vm.add(this.text)
  38. this.text = ''
  39. }
  40. }
  41. })
  • 所有数据通过 vm 注入
  • 所以指令通过 vm 发出

定义 TodoList View:

  1. import { define, WeElement } from 'omi'
  2. import vm from '../view-model/todo'
  3. define('todo-list', class extends WeElement {
  4. css() {
  5. return `
  6. .completed{
  7. color: #d9d9d9;
  8. text-decoration: line-through;
  9. }
  10. `
  11. }
  12. onChange = (evt, id) => {
  13. if (evt.target.checked) {
  14. vm.complete(id)
  15. } else {
  16. vm.uncomplete(id)
  17. }
  18. }
  19. render(props) {
  20. return (
  21. <ul>
  22. {props.items.map(item => (
  23. <li class={item.completed && 'completed'}>
  24. <input
  25. type="checkbox"
  26. onChange={evt => {
  27. this.onChange(evt, item.id)
  28. }}
  29. />
  30. {item.text}
  31. </li>
  32. ))}
  33. </ul>
  34. )
  35. }
  36. })

→ 完整代码戳这里

小结

从宏观的角度来看,Omi 的 MVVM 架构也属性网状架构,网状架构目前来看有:

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

大势所趋!简直是前端工程化最佳实践!也可以理解成网状结构是描述和抽象世界的最佳途径。那么网在哪?

  • ViewModel 与 ViewModel 之间相互依赖甚至循环依赖的网状结构
  • ViewModel 一对一、多对一、一对多、多对多依赖 Models 形成网状结构
  • Model 与 Model 之间形成相互依赖甚至循环依赖的网状结构
  • View 一对一依赖 ViewModel 形成网状结构

总结如下:

Model ViewModel View
Model 多对多 多对多 无关联
ViewModel 多对多 多对多 一对一
View 无关联 一对一 多对多

Star & Fork