腾讯 Omi 5.0 发布 - Web 前端 MVVM 王者归来,mappingjs 强力加持
写在前面
腾讯 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 支持
是谷歌工程师,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
上一篇: 设计模式回顾系列之简单工厂模式