Выпуск Tencent Omi 5.0 — возвращение короля веб-интерфейса MVVM на базе Mappingjs

внешний интерфейс Командная строка Тенсент MVVM Omi
Выпуск Tencent Omi 5.0 — возвращение короля веб-интерфейса MVVM на базе Mappingjs

написать впереди

Тенсент Оми ФреймворкОфициально выпущена версия 5.0, по-прежнему ориентированная на View, но более дружественная интеграция с архитектурой MVVM, полностью разделяющая архитектуру View и бизнес-логику.

mvvm

Вы можете быстро испытать MVVM с помощью omi-cli:

$ 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.

mvvm

Цель состоит в том, чтобы разделить представление и модель, но в MVC представление зависит от модели, и связь слишком высока, что сильно снижает переносимость представления.В режиме MVP представление напрямую не зависит от модели. модель, а P (Presenter) отвечает за завершение Model.Interaction with View. MVVM и MVP похожи по шаблону. ViewModel играет роль Presenter и предоставляет источник данных, требуемый представлением пользовательского интерфейса, вместо того, чтобы напрямую разрешать представлению использовать источник данных модели, что значительно улучшает переносимость представления и модели.Например, одна и та же модель переключается с использованием Flash, HTML, рендеринг WPF, например, одно и то же представление с использованием разных моделей, если модель и модель представления хорошо сопоставлены, представление может быть изменено незначительно или нет.

Mappingjs

Конечно, с MVVM будут проблемы.Данные в модели сопоставляются с ViewModel, чтобы обеспечить привязку представления.Как это сопоставить? Ручное сопоставление? автоматическое отображение? существуетASP.NETВ MVC есть мощныеAutoMapperиспользуется для карт. Для среды JS я специально инкапсулировалmappingjsИспользуется для сопоставления модели с 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 в действии

Определить модель:

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 фокусируется только на данных обновления, представление обновляется автоматически
  • Публичные данные или vm могут зависеть от импорта

Определение вида, обратите внимание на следующее унаследованное от ModelView, а не заселение.

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спасти тебя!

  • map.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"
})

Вы можете сопоставить любой класс с простым объектом json! Затем начните преобразовывать 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 нет, нам нужно использовать отображенного автора непосредственно в представлении:

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 «один к одному», «многие к одному», «один ко многим» и «многие ко многим» полагаются на модели для формирования структуры сетки.
  • Между Моделью и Моделью формируется сеть взаимозависимых или даже циклических зависимостей.
  • 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 шириной в половину экрана определен выше.

поддержка HTML

htmЭто недавняя работа инженера Google и автора 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')

Вы даже можете запустить следующий код прямо в современных браузерах без каких-либо инструментов сборки:

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')

Если вам не нужен метод эффекта, вы можете использовать его напрямую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 Веб-шаблон для разработки небольших программ
MVVM Template(omi-cli v3.0.22+) omi init-mvvm my-app Шаблон MVVM

Star & Fork