腾讯 Omi 5.0 发布-Web 前端 MVVM 归来,mappingjs 强力加持

写在前面

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

腾讯 Omi 5.0 发布-Web 前端 MVVM 归来,mappingjs 强力加持

你可以通过 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 演化而来。

腾讯 Omi 5.0 发布-Web 前端 MVVM 归来,mappingjs 强力加持

目的都是分离视图和模型,但是在 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 形成网状结构

总结如下:

ModelViewModelViewModel多对多多对多无关联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')

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

腾讯 Omi 5.0 发布-Web 前端 MVVM 归来,mappingjs 强力加持

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 TypeCommandDescribeBase Templateomi 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-appMVVM 模板

相关推荐