Writing in the front
Tencent Omi framework was officially released in 5.0, still focusing on View, but more friendly integration of MVVM architecture, completely separating the architecture of View and business logic.
You can quickly experience MVVM with Omi – CLI:
$ npm i omi-cli -g
$ omi init-mvvm my-app
$ cd my-app
$ npm start
$ npm run build
Copy the code
NPX Omi – CLI init-mvVM my-app is also supported (requires NPM V5.2.0 +)
The MVVM evolution
MVVM is essentially evolved from MVC and MVP.
In MVP mode, views do not directly depend on models, and P(Presenter) is responsible for the interaction between Model and View. The MVVM and MVP models are relatively close. The ViewModel serves as a Presenter and provides the data source for the UI View, rather than having the View directly use the Model data source. This greatly improves the portability of the View and the Model. For example, the same Model switch uses Flash, HTML, WPF rendering, for example, the same View uses a different Model, as long as the Model and ViewModel mapping is good, the View can be changed very little or no change.
Mappingjs
Of course, MVVM will have a problem here, the data in the Model is mapped to the ViewModel that provides the view binding, how to map? Manual mapping? Automatic mapping? In ASP.NET MVC, there is a powerful AutoMapper for mapping. For the JS environment, I specifically encapsulated MappingJS to map the Model to the 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 () {
// Recursive mapping
return mapping({ from: this.a })
},
bar: function () {
return this.bleh
},
// Properties can be regrouped
fullName: function () {
return this.firstName + this.lastName
},
// Can be mapped to path
'd[2].b[0]': function () {
return this.a.c
}
}
})
Copy the code
You can use it after NPM installation:
npm i mappingjs
Copy the code
Here are some examples:
var a = { a: 1 }
var b = { b: 2 }
assert.deepEqual(mapping({
from: a,
to: b
}), { a: 1.b: 2 })
Copy the code
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)})Copy the code
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)})Copy the code
Omi MVVM Todo combat
Define the 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)
}
}
Copy the code
I’m not going to post the Todo, it’s too long, you can just look at it here. In any case, unified in accordance with object-oriented programming for abstraction and encapsulation.
Define the 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) {
// Do the mapping here
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((a)= > {
this.update(todoModel)
ovm.update())
})
}
changeSharedData() {
shared.projName = 'I love omi-mvvm.'
ovm.update()
this.update()
}
}
const vd = new TodoViewModel()
export default vd
Copy the code
- The VM focuses only on the update data, and the view is automatically updated
- Common data or VMS can be relied on by import
Define the View, noting that it is inherited from ModelView, not 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 = (a)= > {
// View model to send the command
vm.changeSharedData()
}
install() {
// View model to send the command
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 ! = = ' ') {/ / the view model send instructions vm. The add (). This text. This text = '}}})Copy the code
- All data is injected through the VM
- So instructions are issued through the 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>)})Copy the code
You can see that todo-list can just use fullName.
→ Complete code stamp here
mapping.auto
Is the sensory mapping a bit cumbersome to write? Simple ones are fine, but complex objects that are deeply nested can be taxing. Mapping.auto saves you!
- Mapping. auto(from, [to]) Where to is optional
Here’s an example:
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"
})
Copy the code
You can map any class to a simple JSON obj! So start modifying the ViewModel:
class TodoViewModel {
constructor() {
this.data = {
items: []
}
}
update(todo) {
todo && mapping.auto(todo, this.data)
this.data.projName = shared.projName } ... . .Copy the code
What used to be a bunch of mapping logic is now a single line of code: mapping.auto(todo, this.data). Of course, since there is no fullName attribute, we need to use the mapped author directly in the view:
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>)})Copy the code
summary
From a macro perspective, Omi’s MVVM architecture also attributes to network architecture, which currently includes:
- Mobx + React
- Hooks + React
- MVVM (Omi)
The general trend! Front end engineering best practices! It can also be understood that a network is the best way to describe and abstract the world. So where’s the web?
- A network of interdependencies and even cyclic dependencies between viewModels
- Viewmodels form a network structure with one-to-one, many-to-one, one-to-many, and many-to-many dependencies on Models
- A network structure of interdependence and even cyclic dependence is formed between models
- Views rely on viewModels one to one to form a network
Summary:
Model | ViewModel | View | |
---|---|---|---|
Model | Many to many | Many to many | No relationship |
ViewModel | Many to many | Many to many | One to one |
View | No relationship | More than one | Many to many |
Other new features
Unit RPX support
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')
Copy the code
For example, the div defined above is half the screen width.
HTM support
HTM is the recent work of Google engineer, preact author, whether it is future or not, first support later:
import { define, render, WeElement } from 'omi'
import 'omi-html'
define('my-counter'.class extends WeElement {
static observe = true
data = {
count: 1
}
sub = (a)= > {
this.data.count--
}
add = (a)= > {
this.data.count++
}
render() {
return html`
<div>
<button onClick=The ${this.sub}>-</button>
<span>The ${this.data.count}</span>
<button onClick=The ${this.add}>+</button>
</div>`
}
})
render(html`<my-counter />`.'body')
Copy the code
You can even use the following code to run directly in a modern browser, without any build tools:
Hooks similar API
You can also define it as a pure function:
import { define, render } from 'omi'
define('my-counter'.function() {
const [count, setCount] = this.use({
data: 0.effect: function() {
document.title = `The num is The ${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')
Copy the code
If you don’t need effect, you can use useData:
const [count, setCount] = this.useData(0)
Copy the code
More template options
Template Type | Command | Describe |
---|---|---|
Base Template | omi init my-app |
Based on the template |
TypeScript Template (omi – cli v3.0.5 +) | omi init-ts my-app |
Use TypeScript templates |
SPA Template(omi – cli v3.0.10 +) | omi init-spa my-app |
The template of the omi-router single-page application is used |
Omi – mp Template (omi – cli v3.0.13 +) | omi init-mp my-app |
Small program development Web template |
MVVM Template (omi – cli v3.0.22 +) | omi init-mvvm my-app |
The MVVM template |
Star & Fork
- github.com/Tencent/omi