写在前面

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

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

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

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

MVVM 演化

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

目的都是分离视图和模型,但是在 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。

const testObj = {
same: 10,
bleh: 4,
firstName: 'dnt',
lastName: 'zhang',
a: {
c: 10
}
} const vmData = mapping({
from: testObj,
to: { aa: 1 },
rule: {
dumb: 12,
func: function () {
return 8
},
b: function () {
//可递归映射
return mapping({ from: this.a })
},
bar: function () {
return this.bleh
},
//可以重组属性
fullName: function () {
return this.firstName + this.lastName
},
//可以映射到 path
'd[2].b[0]': function () {
return this.a.c
}
}
})

你可以通后 npm 安装使用:

npm i mappingjs

再举例说明:

var a = { a: 1 }
var b = { b: 2 } assert.deepEqual(mapping({
from: a,
to: b
}), { a: 1, b: 2 })

Deep mapping:


QUnit.test("", function (assert) {
var A = { a: [{ name: 'abc', age: 18 }, { name: 'efg', age: 20 }], e: 'aaa' }
var B = mapping({
from: A,
to: { d: 'test' },
rule: {
a: null,
c: 13,
list: function () {
return this.a.map(function (item) {
return mapping({ from: item })
})
}
}
}) assert.deepEqual(B.a, null)
assert.deepEqual(B.list[0], A.a[0])
assert.deepEqual(B.c, 13)
assert.deepEqual(B.d, 'test')
assert.deepEqual(B.e, 'aaa')
assert.deepEqual(B.list[0] === A.a[0], false)
})

Deep deep mapping:


QUnit.test("", function (assert) {
var A = { a: [{ name: 'abc', age: 18, obj: { f: 'a', l: 'b' } }, { name: 'efg', age: 20, obj: { f: 'a', l: 'b' } }], e: 'aaa' }
var B = mapping({
from: A,
rule: {
list: function () {
return this.a.map(function (item) {
return mapping({
from: item, rule: {
obj: function () {
return mapping({ from: this.obj })
}
}
})
})
}
}
}) assert.deepEqual(A.a, B.list)
assert.deepEqual(A.a[0].obj, B.list[0].obj)
assert.deepEqual(A.a[0].obj === B.list[0].obj, false)
})

Omi MVVM Todo 实战

定义 Model:

let id = 0

export default class TodoItem {
constructor(text, completed) {
this.id = id++
this.text = text
this.completed = completed || false this.author = {
firstName: 'dnt',
lastName: 'zhang'
}
} clone() {
return new TodoItem(this.text, this.completed)
}
}

Todo 就省略不贴出来了,太长了,可以直接 看这里。反正统一按照面向对象程序设计进行抽象和封装。

定义 ViewModel:

import mapping from 'mappingjs'
import shared from './shared'
import todoModel from '../model/todo'
import ovm from './other' class TodoViewModel {
constructor() {
this.data = {
items: []
}
} update(todo) {
//这里进行映射
todo &&
todo.items.forEach((item, index) => {
this.data.items[index] = mapping({
from: item,
to: this.data.items[index],
rule: {
fullName: function() {
return this.author.firstName + this.author.lastName
}
}
})
}) this.data.projName = shared.projName
} add(text) {
todoModel.add(text)
this.update(todoModel)
ovm.update()
} getAll() {
todoModel.getAll(() => {
this.update(todoModel)
ovm.update())
})
} changeSharedData() {
shared.projName = 'I love omi-mvvm.'
ovm.update()
this.update()
}
} const vd = new TodoViewModel() export default vd
  • vm 只专注于 update 数据,视图会自动更新
  • 公共的数据或 vm 可通过 import 依赖

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

import { ModelView, define } from 'omi'
import vm from '../view-model/todo'
import './todo-list'
import './other-view' define('todo-app', class extends ModelView {
vm = vm onClick = () => {
//view model 发送指令
vm.changeSharedData()
} install() {
//view model 发送指令
vm.getAll()
} render(props, data) {
return (
<div>
<h3>TODO</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>
<div>{data.projName}</div>
<button onClick={this.onClick}>Change Shared Data</button>
<other-view />
</div>
)
} handleChange = e => {
this.text = e.target.value
} handleSubmit = e => {
e.preventDefault()
if(this.text !== ''){
//view model 发送指令
vm.add(this.text)
this.text = ''
}
}
})
  • 所有数据通过 vm 注入
  • 所以指令通过 vm 发出
define('todo-list', function(props) {
return (
<ul>
{props.items.map(item => (
<li key={item.id}>
{item.text} <span>by {item.fullName}</span>
</li>
))}
</ul>
)
})

可以看到 todo-list 可以直接使用 fullName

→ 完整代码戳这里

mapping.auto

是不是感觉映射写起来略微麻烦?? 简单的还好,复杂对象嵌套很深就会很费劲。没关系 mapping.auto 拯救你!

  • mapping.auto(from, [to]) 其中 to 是可选参数

举个例子:

class TodoItem {
constructor(text, completed) {
this.text = text
this.completed = completed || false this.author = {
firstName: 'dnt',
lastName: 'zhang'
}
}
} const res = mapping.auto(new TodoItem('task')) deepEqual(res, {
author: {
firstName: "dnt",
lastName: "zhang"
},
completed: false,
text: "task"
})

你可以把任意 class 映射到简单的 json obj!那么开始改造 ViewModel:

class TodoViewModel {
constructor() {
this.data = {
items: []
}
} update(todo) {
todo && mapping.auto(todo, this.data) this.data.projName = shared.projName
}
...
...
...

以前的一堆映射逻辑变成了一行代码: mapping.auto(todo, this.data)。当然由于没有 fullName 属性了,这里需要在视图里直接使用映射过来的 author:

define('todo-list', function(props) {
return (
<ul>
{props.items.map(item => (
<li key={item.id}>
{item.text} <span>by {item.author.firstName + item.author.lastName}</span>
</li>
))}
</ul>
)
})

小结

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

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

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

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

总结如下:

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

其余新增特性

单位 rpx 的支持

import { render, WeElement, define, rpx } from 'omi'

define('my-ele', class extends WeElement {
css() {
return rpx(`div { font-size: 375rpx }`)
} render() {
return (
<div>abc</div>
)
}
}) render(<my-ele />, 'body')

比如上面定义了半屏幕宽度的 div。

htm 支持

htm 是谷歌工程师,preact作者最近的作品,不管它是不是未来,先支持了再说:

import { define, render, WeElement } from 'omi'
import 'omi-html' define('my-counter', class extends WeElement {
static observe = true data = {
count: 1
} sub = () => {
this.data.count--
} add = () => {
this.data.count++
} render() {
return html`
<div>
<button onClick=${this.sub}>-</button>
<span>${this.data.count}</span>
<button onClick=${this.add}>+</button>
</div>`
}
}) render(html`<my-counter />`, 'body')

你甚至可以直接使用下面代码在现代浏览器中运行,不需要任何构建工具:

Hooks 类似的 API

你也可以定义成纯函数的形式:

import { define, render } from 'omi'

define('my-counter', function() {
const [count, setCount] = this.use({
data: 0,
effect: function() {
document.title = `The num is ${this.data}.`
}
}) this.useCss(`button{ color: red; }`) return (
<div>
<button onClick={() => setCount(count - 1)}>-</button>
<span>{count}</span>
<button onClick={() => setCount(count + 1)}>+</button>
</div>
)
}) render(<my-counter />, 'body')

如果你不需要 effect 方法, 可以直接使用 useData:

const [count, setCount] = this.useData(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 Template(omi-cli v3.0.22+) omi init-mvvm my-app MVVM 模板

Star & Fork

最新文章

  1. Jquery 仿 android Toast效果
  2. pict(Pairwise Independent Combinatorial Testing)工具使用
  3. 【Alpha阶段】第7.5次Scrum例会
  4. Xib文件的使用
  5. Python变量类型
  6. Note_Master-Detail Application(iOS template)_05_ YJYMasterViewController.m
  7. 父&lt;IFRAME&gt;获取子页属性以及子页中&lt;IFRAME&gt;的方法
  8. zzuoj 10409 10409: D.引水工程
  9. iOS svn版本回退 cornerstone
  10. hdu 1331 Function Run Fun
  11. 禁用与启用Button点击
  12. MYSQL—加写锁,加读锁,解锁
  13. JavaScript快速入门(三)——JavaScript语句
  14. PHP下CodeIgniter框架连接读取MS Access数据库文件
  15. java操作Jacoco合并dump文件
  16. Markdown的基本语法记录
  17. 解决input框黄色背景问题(转)
  18. BZOJ.2806.[CTSC2012]Cheat(广义后缀自动机 DP 单调队列)
  19. Hat&#39;s Fibonacci
  20. 使用 Azure CLI 创建和管理 Linux VM

热门文章

  1. PQA组织的设置与运作
  2. 下载安装Emacs和基本配置--待更新中
  3. 关于JBoss -“Closing a connection for you,please close them yourself”
  4. 4. svg学习笔记-文档结构元素和样式的使用
  5. node.js cluster模式启用方式
  6. #007 C语言大作业学生管理系统第四天
  7. S/4 HANA中的MATDOC和MATDOC_EXTRACT
  8. Cassandra联手Spark 大数据分析将迎来哪些改变?
  9. ES5-ES6-ES7_解构赋值
  10. xpath 解析 及案例