omi/packages/mp-mvvm
dntzhang 6d54941a2d update tutorial 2018-12-10 11:21:23 +08:00
..
components update mp-mvvm 2018-12-07 09:43:21 +08:00
model/todo eslint 2018-12-06 11:33:46 +08:00
pages/index update tutorial 2018-12-10 11:21:23 +08:00
utils update tutorial 2018-12-10 11:21:23 +08:00
view-model update mp mvvm - using mapping 2.0 2018-12-09 12:19:07 +08:00
README.md update tutorial 2018-12-10 11:21:23 +08:00
app.js eslint 2018-12-06 11:33:46 +08:00
app.json add mp mvvm 2018-12-06 11:25:51 +08:00
app.wxss add mp mvvm 2018-12-06 11:25:51 +08:00
project.config.json add mp mvvm 2018-12-06 11:25:51 +08:00

README.md

小程序的 MVVM 架构 mp-mvvm

小程序插上 MVVM 的翅膀,和 Omi MVVM 一样强大

mp-mvvm

mappingjs 2.0

先隆重介绍下 mappingjs极小的尺寸只有 100 多行 js因为他是 MVVM 的强力核心驱动。mappingjs 2.0 的 API 减少到了只有一个:

mapping(from, to, [rule])

  • from 是需要映射的对象
  • to 是映射到的对象
  • rule 是可选参数,代表映射规则

mappingjs

先看一个简单的例子:

class TodoItem {
  constructor(text, completed) {
    this.text = text
    this.completed = completed || false

    this.author = {
      firstName: 'dnt',
      lastName: 'zhang'
    }
  }
}

const from = new TodoItem('task')
const to = { author: { age: 18 } }

mapping(from, to, {
  fullName: function () {
    return this.author.firstName + this.author.lastName
  }
})

映射后 to 的值是:

{
  author: {
    firstName: "dnt",
    lastName: "zhang",
    age: 18
  },
  fullName: 'dntzhang',
  completed: false,
  text: "task"
}

可以看到, age 会保留并且被合并,fullName 对原有属性进行了重组。但是这就够了吗当然不够path 命中和映射是一定需要支持的,因为属性往往都会嵌套很深,不能限制要求开发者仅仅使用扁平的数据源。

稍微修改一下上面的 mapping 规则:

mapping(from, to, {
  "author.fullName": function () {
    //请注意这里的 this 指向已变成 author太方便了
    return this.firstName + this.lastName
  }
})

映射后 to 的值是:

{
  author: {
    firstName: "dnt",
    lastName: "zhang",
    fullName: 'dntzhang',
    age: 18
  },
  completed: false,
  text: "task"
}

这就够了吗当然不够支持数组支持JSON List 数组!支持任意对象数组!

mapping({ arr: [{ name: 'zhangsan' }, { name: ' lisi' }] }, {}, {
  'arr[0]': { name: 'wangwu' },
  'arr[1]': function () {
    //请注意这里的 this 已指向 arr超方便
    return { name: this[1].name + '2' }
  }
})

输出:

{
  arr: [{ name: 'wangwu' }, { name: ' lisi2' }]
}

这就够了吗? 数组有有 1000000 条就要写 1000000 条规则?明显不是, * 来帮你。

mapping({ arr: [{ name: 'zhangsan' }, { name: 'lisi' }] }, {}, {
  'arr[*].name2': function () {
    //请注意这里的 this 已指向 item超方便
    return this.name + '2'
  }
})
{
  arr: [{ name: 'zhangsan', name2: 'zhangsan2' }, { name: 'lisi', name2: 'lisi2' }]
}

Mappingjs 的强大不再举例,这里是一个复杂的例子来说明他的大部分能力: → 复杂的例子

const res = mapping({ a: { b: 2, e: [{ f: 3 }, { f: 5 }, { f: 10 }] }, list: list }, {}, {
  'a.test': 1, //支持非函数类型的直接赋值
  'a.squareB': function () { //支持 path
    return this.b * this.b
  },
  'a.e[0].squareF': function () {//支持带有数组 index 的 path
    return this.f * this.f
  },
  'a.e[1].squareF': function () {//支持带有数组 index 的 path
    return this.f * this.f
  },
  'a.e[2]': function () {//支持带有数组 index 的 path
    return 'change array item!'
  },
  'a.e[3]': function () {//支持带有数组 index 的 path
    return 'add array item!'
  },
  'list.items[*].fullName': function () {//支持 * 来映射所有数组项
    return this.author.firstName + this.author.lastName
  },
  'list.items[*].squareTest': function () {//支持 * 来映射所有数组项
    return this.test * this.test
  },
  'list.items[*].arr[*].squareF': function () {//支持任意复杂的 path
    return this.f * this.f
  }
})

小程序 MVVM Todo 实战

定义 TodoItem Model:

let id = 0

export default class TodoItem {
  constructor(text, completed) {
    this.id = id++
    this.text = text
    this.completed = completed || false
  }
}

定义 Todo Model:

import TodoItem from './todo-item'
import { getAll, add } from './todo-server'

export default class Todo {
  constructor() {
    this.items = []

    this.author = {
      firstName: 'dnt',
      lastName: 'zhang'
    }
  }

  add(content) {
    const item = new TodoItem(content)
    this.items.push(item)
    add(item)
  }

  complete(id) {
    this.items.every(item => {
      if (id === item.id) {
        item.completed = true
        return false
      }
      return true
    })
  }

  uncomplete(id) {
    this.items.every(item => {
      if (id === item.id) {
        item.completed = false
        return false
      }
      return true
    })
  }

  remove(id) {
    this.items.every((item, index) => {
      if (id === item.id) {
        this.items.splice(index, 1)
        return false
      }
    })
  }
}

定义 ViewModel:

import mapping from '../utils/mapping'
import todo from '../model/todo/index'

class TodoViewModel {
  constructor() {
    this.data = {
      items: []
    }
  }

  update() {
    //一行代码,自动映射!!
    mapping(todo, this.data)
  }

  toogleComplete(id) {
    todo.toogleComplete(id)
    this.update()
  }

  add(text) {
    todo.add(text)
    this.update()
  }

  remove(id) {
    todo.remove(id)
    this.update()
  }
}

const vm = new TodoViewModel()

export default vm

说完 mappingjs来看下 mp-mvvm。你会发现mp-mvvm 和 omi-mvvm 的 web 模板有很多代码是一模一样的!

  • Model 一模一样
  • ViewModel 一模一样
  • View 不一样mp-mvvm 使用小程序渲染omi-mvvm 使用了 web 渲染

这也体现了 MVVM 的优势和特点:

领域模型复用不变View 可以移植成任意技术进行渲染

下面来看下小程序的 View

todo-list:

<view>
  <label class="checkbox {{item.completed&&'completed'}}" wx:for="{{items}}">
    <view bindtap="checkboxChange" data-id="{{item.id}}">
      <checkbox checked="{{item.completed}}" />
      {{item.text}}
    </view>
    <image bindtap="remove" data-id="{{item.id}}" src="./remove.png" />
  </label>
</view>
import vm from '../../view-model/todo'

Component({
  properties: {
    items: {
      type: Array,
      value: []
    }
  },

  ready: function() {},

  methods: {
    checkboxChange: function(e) {
      vm.toogleComplete(e.currentTarget.dataset.id)
    },
    remove:function(e){
      vm.remove(e.currentTarget.dataset.id)
    }
  }
})

这里把 todo-list 抽象成了一个自定义组件,因为需要有用户输入,所以把 vm 依赖进来,通过 vm 向 model 发指令并且更新视图。在看 todo page:

<view class="container">
  <view class="title">Hello MVVM</view>
  <view>
    <todo-list items="{{items}}" />
    <view class="form">
      <input class="input" bindinput="inputHandler" type="text" placeholder="Input your task" value="{{text}}" />
      <button class="button" bindtap="tapHandler">Add{{items.length+1}}</button>
    </view>
  </view>

  <other-view />
</view>
import vm from '../../view-model/todo'
import create from '../../utils/create'

create.Page(vm, {
  onLoad: function() {
    vm.getAll()
  },
  inputHandler: function(e) {
    this.text = e.detail.value
  },
  tapHandler: function() {
    vm.add(this.text)
    this.setData({
      text: ''
    })
  }
})

需要注意的是,这是使用的是 create.Page,并传入了 vm,而非以前的 Page。这里需要介绍下两个新的 API

  • create.Page(vm, options)
  • create.Component(vm, options)

那么什么使用使用 create.Page 或者 create.Component什么时候不需要使用 create直接使用 Page 和 Component

  • 如果只需要使用 vm 来发送指令,就不需要 create
  • 如果你的 vm.data 需要作为视图的 data请使用 create

License

MIT @dntzhang