腾讯 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 支持
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
腾讯 Omi 5.0 发布 - Web 前端 MVVM 王者归来,mappingjs 强力加持的更多相关文章
- IIS7.0发布Web服务器0002
		asp.net发布到IIS中出现错误:处理程序“PageHandlerFactory-Integrated”在其模块列表中有一个错误模块“ManagedPipelineHandler” 分类: BS学 ... 
- 腾讯 AlloyCrop 1.0 发布
		写在前面 AlloyCrop 这个项目是8个月前发布的,作为AlloyFinger 的典型案例,发布之后被BAT等其他公司广泛使用.但是发布之后,有两个问题一直没有抽出时间去解决: 裁剪图像的分辨率太 ... 
- IIS7.0发布Web服务-0001
		配置错误 不能在此路径中使用此配置节.如果在父级别上锁定了该节,便会出现这种情况.锁定是默认设置的 (overrideModeDefault="Deny"),或者是通过包含 ove ... 
- Eclipse如何发布web项目
		目录结构: // contents structure [-] 需要的环境 下载和配置JDK 下载和配置Tomcat 下载Eclipse Eclipse 4.4.0 发布Web步骤 创建server ... 
- 腾讯发布新版前端组件框架 Omi,全面拥抱 Web Components
		Omi - 合一 下一代 Web 框架,去万物糟粕,合精华为一 → https://github.com/Tencent/omi 特性 4KB 的代码尺寸,比小更小 顺势而为,顺从浏览器的发展和 AP ... 
- Omi v1.0震撼发布 - 令人窒息的Web组件化框架
		原文链接--https://github.com/AlloyTeam/omi 写在前面 Omi框架经过几十个版本的迭代,越来越简便易用和强大. 经过周末的连续通宵加班加点,Omi v1.0版本终于问世 ... 
- Omi v1.0震撼发布 - 开放现代的Web组件化框架
		原文链接--https://github.com/AlloyTeam/omi 写在前面 Omi框架经过几十个版本的迭代,越来越简便易用和强大. 经过周末的连续通宵加班加点,Omi v1.0版本终于问世 ... 
- 2015腾讯web前端笔试题
		1 请实现,鼠标点击页面中的任意标签,alert该标签的名称.(注意兼容性) 2 请指出一下代码的性能问题,并经行优化. var info="腾讯拍拍网(www.paipai.com)是 ... 
- TFC2017 腾讯Web前端大会参会小结
		简述 上周有幸参加TFC腾讯Web前端大会,见识了各路前端大神的精彩演讲,干货满满的.会议流程分为上午主会场,以及下午的三个分会场.分享的主题涵盖Web新技术.Node.js.框架.工程化. 图形处理 ... 
随机推荐
- Spring MVC HelloWorld入门及运行机制 (一)
			完整的项目案例: springmvc.zip 介绍 SpringMVC是一款Web MVC框架. 它跟Struts框架类似,是目前主流的Web MVC框架之一. 文章通过实例来介绍SpringMVC的 ... 
- 关于getdate()的不同的日期格式
			在使用Sql Server查询数据库时,我们经常会需要查询日期格式的数据,对于日期在sql语言中的格式有一定的要求,通过修改convert中的最后一位参数,可以返回不通格式的时间,具体实现如下: Se ... 
- 洗礼灵魂,修炼python(73)--全栈项目实战篇(1)——【转载】前提准备之学习ubuntu
			本篇是为项目实战做准备,学习Linux是必备的,不然都不好意思叫全栈对吧?下面是一位资深大神写的文章,够详细,我也不用浪费时间再写了 原文链接:Ubuntu学习——第一篇 内容: 一. Ubuntu简 ... 
- 确认是否是因为做了物理I/O而导致的性能不佳
			要获取语句是否进行了I/O,需要打开set statistics on 和set statistics on. 
- SQL Server datetime类型转换超出范围的报错
			一个很基础的插入语句: insert into table1 select col1,convert(datetime,col2),convert(datetime,col3),col4,col5 f ... 
- [Hive_add_11] Hive 使用 UDTF 实现日志降维
			0. 说明 对日志进行降维处理,将日志分为几个小表 通过编写 UDTF ,对日志降维,将日志聚合体相关字段抽取出来,形成新表. 1. 操作流程 1.0 日志部分内容 ##{\"appChan ... 
- python opencv SIFT,获取特征点的坐标位置
			备注:SIFT算法的实质是在不同的尺度空间上查找关键点(特征点),并计算出关键点的方向.SIFT所查找到的关键点是一些十分突出,不会因光照,仿射变换和噪音等因素而变化的点,如角点.边缘点.暗区的亮点及 ... 
- layui中,同一个页面动态加载table数据表格
			效果图: 前端代码: <div class="layui-fluid" id="record-user" hidden="hidden" ... 
- Ubuntu 16.04安装JDK(转载)
			1.简单的安装方法 安装JDK的最简单方法应该就是使用apt-get来安装了,但是源一般是OpenJDK,如果需要安装Oracle的JDK这种方法就不合适了,直接跳过看下面的章节. 1.使用ctrl+ ... 
- 模拟placeholder
			把这个记下来,主要是因为这里的 defaultValue 我之前竟然不知道 <input type="text" value="提示内容" onFocus ... 
