騰訊 Omi 框架正式發佈 5.0,依然專一於 View,可是對 MVVM 架構更加友好的集成,完全分離視圖與業務邏輯的架構。css
你能夠經過 omi-cli 快速體驗 MVVM:html
$ 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 其實本質是由 MVC、MVP 演化而來。react
目的都是分離視圖和模型,可是在 MVC 中,視圖依賴模型,耦合度過高,致使視圖的可移植性大大下降,在 MVP 模式中,視圖不直接依賴模型,由 P(Presenter)負責完成 Model 和 View 的交互。MVVM 和 MVP 的模式比較接近。ViewModel 擔任這 Presenter 的角色,而且提供 UI 視圖所須要的數據源,而不是直接讓 View 使用 Model 的數據源,這樣大大提升了 View 和 Model 的可移植性,好比一樣的 Model 切換使用 Flash、HTML、WPF 渲染,好比一樣 View 使用不一樣的 Model,只要 Model 和 ViewModel 映射好,View 能夠改動很小甚至不用改變。git
固然 MVVM 這裏會出現一個問題, Model 裏的數據映射到 ViewModel 提供該視圖綁定,怎麼映射?手動映射?自動映射?在 ASP.NET MVC 中,有強大的 AutoMapper 用來映射。針對 JS 環境,我特意封裝了 mappingjs 用來映射 Model 到 ViewModel。github
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
npm i mappingjs
再舉例說明:json
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) })
定義 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
定義 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 = '' } } })
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
拯救你!
舉個例子:
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 架構也屬性網狀架構,網狀架構目前來看有:
大勢所趨!簡直是前端工程化最佳實踐!也能夠理解成網狀結構是描述和抽象世界的最佳途徑。那麼網在哪?
總結以下:
Model | ViewModel | View | |
---|---|---|---|
Model | 多對多 | 多對多 | 無關聯 |
ViewModel | 多對多 | 多對多 | 一對一 |
View | 無關聯 | 一多一 | 多對多 |
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')
你甚至能夠直接使用下面代碼在現代瀏覽器中運行,不須要任何構建工具:
你也能夠定義成純函數的形式:
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 模板 |