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