Tencent native applet framework OMIX 2.0 released


Good design is only one, we think OMIX 2.0 design just right.

OMIX 2.0 WeStore is an evolution, WeStore using diff before and after the data changes, diff out json is setData the patch, OMIX 2.0 using the monitor data changes observer setData get the patch.

And OMIX contrast, need WeStore run more calculations, OMIX requires more memory and computing time initialization, but when the data changes OMIX faster than WeStore, aspects of programming experience, OMIX not need to manually update, WeStore need to manually update.


characteristic

  • Stateless View Designer

  • Applet invasion of zero

  • Only one API

  • Support calculate property

  • Easily manage small projects, the projects and large-scale projects

  • Also suitable for small game, is right, use  a small program to develop small game , this second case OMIX achieved using a small game

Getting Started

API

  • create(store, option) Create a page, store page from the injection, cross-component sharing across page

  • create(option) Creating a component

  • this.store And  this.data global data store and, pages and page can get all the components, the operation will automatically update the view data

Need not store page or injection assembly for use Pageand Component constructors,  Component store triggerEvent through interaction with the upper layer or upper layer

Simple combat

Implement a simple log shows a list of

Define a global store:

export default {
  data: {
    logs: []
  }
}

Custom Page:

import create from '../../utils/create'
import util from '../../utils/util'
import store from '../../store'

create(store, {
  // 声明依赖
  use: ['logs'], //也支持复杂路径依赖,比如 ['list[0].name']
  // 计算属性,可以直接绑定在 wxml 里
  computed: {
    logsLength() {
      return this.logs.length
    }
  },
  onLoad: function () {
    this.store.data.logs = (wx.getStorageSync('logs') || []).map(log => {
      return util.formatTime(new Date(log))
    })

    setTimeout(() => {
      //响应式,自动更新视图
      this.data.logs[0] = 'Changed!'
    }, 1000)

    setTimeout(() => {
      //响应式,自动更新视图
      this.data.logs.push(Math.random(), Math.random())
    }, 2000)

    setTimeout(() => {
      //响应式,自动更新视图
      this.data.logs.splice(this.store.data.logs.length - 1, 1)
    }, 3000)
  }
})
<view class="container log-list">
  <block wx:for="{{logs}}" wx:for-item="log">
    <text class="log-item">{{index + 1}}. {{log}}</text>
  </block>
</view>

Defined test-store component, the component may use the global logs component, the source component:

import create from '../../utils/create'

create({
  use: ['logs'],
  //计算属性
  computed: {
    logsLength() {
      return this.logs.length
    }
  }
})
<view class="ctn">
  <view>Log Length: {{logs.length}}</view>
  <view>Log Length by computed: {{logsLength}}</view>
</view>

Other optional configuration instructions

Store.js modify the debug field is used to open and close the debug log:

export default {
  data: {
    motto: 'Hello World',
    userInfo: {},
    hasUserInfo: false,
    canIUse: wx.canIUse('button.open-type.getUserInfo'),
    logs: []
  },
  debug: true, //调试开关,打开可以在 console 面板查看到 store 变化的 log
  updateAll: true //当为 true 时,无脑全部更新,组件或页面不需要声明 use
}

Update global development is off by default, debugging turned on by default at store.data the change it will appear in the log Developer Tools panel, as shown below:

other

It should be noted, length changes in the array does not trigger a view update, use the size method:

this.data.arr.size(2) //会触发视图更新
this.data.arr.length = 2 //不会触发视图更新

this.data.arr.push(111) //会触发视图更新
//每个数组的方法都有对应的 pure 前缀方法,比如 purePush、pureShift、purePop 等
this.data.arr.purePush(111) //不会触发视图更新

 Computed Property

  use: [
    'motto',
    'userInfo',
    'hasUserInfo',
    'canIUse'
  ],
  computed: {
    reverseMotto() {
      return this.motto.split('').reverse().join('')
    }
  }

Calculating attributes are defined in a page or component  computed in, as in the above  reverseMotto, it can be directly bound wxml, the updated value is automatically updated Motto of reverseMotto.

monitor changes in store

const handler = function (evt) {
  console.log(evt)
}
//监听,允许绑定多个
store.onChange(handler)
//移除监听
store.offChange(handler)

Store complex split into multiple files

When the applet becomes very complex, single-file single store will become very bloated, so it is necessary to store into multiple new file, here an example:

store-a.js:

export const data = {
  name: 'omix'
}

export function changeName(){
  data.name = 'Omix'
}

store-b.js:

export const data = {
  name: 'omix',
  age: 2
}

export function changeAge(){
  data.age++
}

The combined so store.js store to the corresponding sub-module (a, b):

import { data as dataA, changeName } from 'store-a.js'
import { data as dataB, changeAge } from 'store-b.js'

const store = {
  data:{
    a: dataA,
    b: dataB
  },
  a: { changeName },
  b: { changeAge }
}

export default store

Data Binding:

<view>
  <text>{{a.name}}</text>
  <text>{{b.name}}-{{b.age}}</text>
</view>

Data usage:

import create from '../../utils/create'
import store from '../../store/store'

create(store, {
  //声明依赖
  use: ['a.name', 'b'],
  onLoad: function () {
    setTimeout(_ => {
      store.a.changeName()
    }, 1000)

    setTimeout(_ => {
      store.b.changeAge()
    }, 2000)
  }
})

The full case can be injected into a multi-store Click here

Path hit rules

When  store.data a change occurs, components of dependencies is updated, the hit exemplified Path rules:

Observer Path (generated by the data change) use the path Whether to update
abc abc Update
abc[1] abc Update
abc.a abc Update
abc abc.a Not updated
abc abc[1] Not updated
abc abc[1].c Not updated
abc.b abc.b Update

As long as the injection assembly path statement or declaration in equal use in use in the child node where the path will be updated, as long as the above conditions will be a hit update!

If your applet is really small, then please disregard the above rules, directly to the store updateAll statement is true can be. If the applet page were numerous and complex for better performance, to save each page or component declaration  use.

Snake game combat

Domain Model Design

  • Extracting major entities, such as (snake, game)

  • Summed up the specific business entity attribute method from nouns,

    • It contains the end of the suspended state, maps, scores, frame rate, the protagonist of the game, food

    • It includes start the game, pause the game, the game is over, the production of food, games and other methods to reset

    • Containing the direction of movement, body property

    • Moving and turning method comprising

    • snake

    • game

  • Establish a link between the entity attribute method

    • The only protagonist of the game, that snake

    • Snake eating food, increase the game score

    • Food disappears, the game is responsible for the production of food again

    • Hit the wall or the snake itself, the game ends state

  • The core design cycle

    • Determine whether there is food, not to produce a (low frame rate)

    • Snake with its own collision detection

    • Snake collides with an obstacle detection

    • Snake food and collision detection

    • Snake Mobile

Use Code Description snake entity

class Snake {
  constructor() {
    this.body = [3, 1, 2, 1, 1, 1]
    this.dir = 'right'
  }

  move(eating) {
    const b = this.body
    if (!eating) {
      b.pop()
      b.pop()
    }

    switch (this.dir) {
      case 'up':
        b.unshift(b[0], b[1] - 1)
        break
      case 'right':
        b.unshift(b[0] + 1, b[1])
        break
      case 'down':
        b.unshift(b[0], b[1] + 1)
        break
      case 'left':
        b.unshift(b[0] - 1, b[1])
        break
    }
  }

  turnUp() {
    if (this.dir !== 'down')
      this.dir = 'up'
  }
  turnRight() {
    if (this.dir !== 'left')
      this.dir = 'right'
  }
  turnDown() {
    if (this.dir !== 'up')
      this.dir = 'down'
  }
  turnLeft() {
    if (this.dir !== 'right')
      this.dir = 'left'
  }
}

Snake has a steering logic, it is not back in the opposite direction, such as moving upward, not directly turn directly downwards, so  turnUp, turnRight, turnDown, turnLeft has a corresponding determination condition.

Using the code description gaming entity

import Snake from './snake'

class Game {
  constructor() {
    this.map = []
    this.size = 16
    this.loop = null
    this.interval = 500
    this.paused = false
    this._preDate = Date.now()
    this.init()
  }

  init() {

    this.snake = new Snake

    for (let i = 0; i < this.size; i++) {
      const row = []
      for (let j = 0; j < this.size; j++) {
        row.push(0)
      }
      this.map.push(row)
    }
  }

  tick() {

    this.makeFood()
    const eating = this.eat()
    this.snake.move(eating)
    this.mark()

  }

  mark() {
    const map = this.map
    for (let i = 0; i < this.size; i++) {
      for (let j = 0; j < this.size; j++) {
        map[i][j] = 0
      }
    }

    for (let k = 0, len = this.snake.body.length; k < len; k += 2) {
      this.snake.body[k + 1] %= this.size
      this.snake.body[k] %= this.size

      if (this.snake.body[k + 1] < 0) this.snake.body[k + 1] += this.size
      if (this.snake.body[k] < 0) this.snake.body[k] += this.size
      map[this.snake.body[k + 1]][this.snake.body[k]] = 1
    }
    if (this.food) {
      map[this.food[1]][this.food[0]] = 1
    }
  }

  start() {
    this.loop = setInterval(() => {
      if (Date.now() - this._preDate > this.interval) {
        this._preDate = Date.now()
        if (!this.paused) {
          this.tick()
        }
      }
    }, 16)
  }

  stop() {
    clearInterval(this.loop)
  }

  pause() {
    this.paused = true
  }

  play() {
    this.paused = false
  }

  reset() {
    this.paused = false
    this.interval = 500
    this.snake.body = [3, 1, 2, 1, 1, 1]
    this.food = null
    this.snake.dir = 'right'
  }

  toggleSpeed() {
    this.interval === 500 ? (this.interval = 150) : (this.interval = 500)
  }

  makeFood() {
    if (!this.food) {
      this.food = [this._rd(0, this.size - 1), this._rd(0, this.size - 1)]
      for (let k = 0, len = this.snake.body.length; k < len; k += 2) {
        if (this.snake.body[k + 1] === this.food[1]
          && this.snake.body[k] === this.food[0]) {
          this.food = null
          this.makeFood()
          break
        }

      }
    }
  }

  eat() {
    for (let k = 0, len = this.snake.body.length; k < len; k += 2) {
      if (this.snake.body[k + 1] === this.food[1]
        && this.snake.body[k] === this.food[0]) {
        this.food = null
        return true
      }
    }
  }

  _rd(from, to) {
    return from + Math.floor(Math.random() * (to + 1))
  }
}

The figure can be seen using a two-dimensional array to store 16 * 16 snakes, food, map information. Snakes and food occupy lattice is 1, and 0.

[
  [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
  [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
  [0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
  [0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
  [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
  [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
  [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
  [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
  [0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0],
  [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
  [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
  [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
  [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
  [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
  [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
  [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
]

Therefore, the above represents a length of 5 and a snake food, you can find it in the image above?

Defined store

import Game from '../models/game'

const game = new Game
const { snake, map } = game

game.start()

class Store {
  data = {
    map,
    paused: false,
    highSpeed: false
  }
  turnUp() {
    snake.turnUp()
  }
  turnRight() {
    snake.turnRight()
  }
  turnDown() {
    snake.turnDown()
  }
  turnLeft() {
    snake.turnLeft()
  }
  pauseOrPlay = () => {
    if (game.paused) {
      game.play()
      this.data.paused = false
    } else {
      game.pause()
      this.data.paused = true
    }
  }
  reset() {
    game.reset()
  }
  toggleSpeed() {
    game.toggleSpeed()
    this.data.highSpeed = !this.data.highSpeed
  }
}

export default new Store

Will find, store very thin, only responsible action transit View of the Model, as well as data on the hidden automatically mapped to the Model View.

Rendering the game board

WXML:

<view class="game">
  <view class="p" wx:for="{{map}}" wx:for-item="row" wx:for-index="index">
    <block wx:for="{{row}}" wx:for-item="col">
      <block wx:if="{{col}}">
        <view class="b s"></view>
      </block>
      <block wx:else>
        <view class="b"></view>
      </block>
    </block>
  </view>
</view>

Format with a class to s is black, such as food, snake's body, and the rest will gray background.

The corresponding js:

import create from '../../utils/create'

create({
  use: ['map']
})

map On behalf of dependent store.data.map, map updates will automatically update the view.

Control panel main interface

<view>
  <game />
  <view class="ctrl">
    <view class="btn cm-btn cm-btn-dir up" bindtap="turnUp"><i></i><em></em><span>上</span></view>
    <view class="btn cm-btn cm-btn-dir down" bindtap="turnDown"><i></i><em></em><span>下</span></view>
    <view class="btn cm-btn cm-btn-dir left" bindtap="turnLeft"><i></i><em></em><span >左</span></view>
    <view class="btn cm-btn cm-btn-dir right" bindtap="turnRight"><i></i><em></em><span >右</span></view>
    <view class="btn cm-btn space" bindtap="toggleSpeed"><i></i><span >{{highSpeed? '减速': '加速'}}</span></view>
    <view class="btn reset small" bindtap="reset"><i ></i><span >重置</span></view>
    <view class="btn pp small" bindtap="pauseOrPlay"><i></i><span >{{paused ? '继续' : '暂停'}}</span></view>
  </view>
</view>

The main interface using the page, a reference component:

{
  "usingComponents": {
    "game": "/components/game/index"
  }
}

Correspondence JS:

import create from '../../utils/create'
import store from '../../store/index'

create(store, {
  use: ['paused', 'highSpeed'],
  turnUp() {
    store.turnUp()
  },
  turnDown() {
    store.turnDown()
  },
  turnLeft() {
    store.turnLeft()
  },
  turnRight() {
    store.turnRight()
  },
  toggleSpeed() {
    store.toggleSpeed()
  },
  reset() {
    store.reset()
  },
  pauseOrPlay() {
    store.pauseOrPlay()
  }
})

Frame rate control

How local control of the main frame rate and frame rate. Under normal circumstances, we believe that 60 FPS is smooth, so we timer interval is 16ms, the smaller the core circle of computation, the closer the 60 FPS:

this.loop = setInterval(() => {
  //
}, 16)

However, some calculation is not necessary to calculate the time 16 seconds, this will reduce the frame rate, the recording can be performed on a time for controlling the frame rate:

this.loop = setInterval(() => {
  //执行在这里是大约 60 FPS
  if (Date.now() - this._preDate > this.interval) {
    //执行在这里是大约  1000/this.interval FPS
    this._preDate = Date.now()
    //暂停判断
    if (!this.paused) {
      //核心循环逻辑
      this.tick()
    }
  }
}, 16)

Since applets are not supported in JSCore  requestAnimationFrame, so here use setInterval. Course also be used raf-interval loop execution tick:

this.loop = setRafInterval(() => {
  //执行在这里是大约 60 FPS
  if (Date.now() - this._preDate > this.interval) {
    //执行在这里是大约  1000/this.interval FPS
    this._preDate = Date.now()
    //暂停判断
    if (!this.paused) {
      //核心循环逻辑
      this.tick()
    }
  }
}, 16)

Usage and setInterval consistent, if only for internal use setTimeout and support  requestAnimationFrame will give priority to use  requestAnimationFrame.

Snake architecture

So the whole project is MVC, MVP or MVVM?

As can be seen from the Snake source: view (components, pages) and models (Models) are separated, there is no interdependencies, in MVC, the view-dependent model, the coupling is too high, resulting in greatly view portability reduce, so be sure not MVC architecture.

In MVP mode, the view does not depend directly on the model, the Presenter responsible for the completion of interactive Model and View. MVP and MVVM pattern closer. ViewModel assume the role of this Presenter, and provide the required UI view the data source, rather than directly using the Model View data source, which greatly enhanced the Model View and portability, such as the same Model switch between Flash, HTML, WPF rendering, such as the same Model View using different, as long as the Model and ViewModel mapping good, even very small changes View can not be changed.

Snake can be seen from the source, View (components) used directly in the Presenter (stores) the rendering attribute data, the attribute data from the Model (models) of the property, and no Model ViewModel mapped to. So be sure not MVVM architecture.

So the above Snake belong  MVP  ! Is just an evolution of the MVP, because of changes in the map M will be even more custom View, from M-> P-> V loop is automated, can not see the code any logic. Just need to rely on the statement:

use: ['map']

This also circumvents the MVVM biggest problem: M to VM overhead map.

MVP advantage evolutionary version

1. reusability

Decoupling, Model View, or between one and the Model View changes, Presenter same interface, there is no need to make other modifications to the above changes, the business logic Model layer has good flexibility and reusability .

2. Flexibility

Presenter's data automatically mapped to change the view, such Presenter very very thin, View a passive view. Presenter's data and may be used on any platform, any framework, any technique for rendering.

3. testability

If tight coupling between the View and Model, wherein one of the tests is not possible before the Model and View simultaneously developed. For the same reason, to View or Model for unit testing difficult. Now, MVP mode to solve all the problems. MVP mode, there is no direct dependency between the View and Model, the developer can be injected into either one of the two test objects by means of simulation.

For example logical multiplexing, such as OMI team project initiated snake-mvp, the following model and presenter several projects almost identical, complete reuse, but rendered views adaptation layer made different depending on the frame.

For example the view layer react:

import React from 'react'
import Game from '../game'

import store from '../../stores/index'
import { $ } from 'omis'
require('../../utils/css').add(require('./_index.css'))

export default $({
  render() {
    const { store } = $
    const { paused } = store.data
    return <div className="container">
      <h1>[P]REACT + OMIS SNAKE</h1>

      <Game></Game>

      <div className="ctrl">
        <div className="btn cm-btn cm-btn-dir up" onClick={store.turnUp}><i></i><em></em><span>Up</span></div>
        <div className="btn cm-btn cm-btn-dir down" onClick={store.turnDown}><i></i><em></em><span>Down</span></div>
        <div className="btn cm-btn cm-btn-dir left" onClick={store.turnLeft}><i></i><em></em><span >Left</span></div>
        <div className="btn cm-btn cm-btn-dir right" onClick={store.turnRight}><i></i><em></em><span >Right</span></div>
        <div className="btn cm-btn space" onClick={store.toggleSpeed}><i></i><span >Gear</span></div>
        <div className="btn reset small" onClick={store.reset}><i ></i><span >Reset</span></div>
        <div className="btn pp small" onClick={store.pauseOrPlay}><i></i><span >{paused ? 'Play' : 'Pause'}</span></div>
      </div>
    </div>
  },
  useSelf: ['paused'],
  store
})

Q & A

Q: For example, I pop a component may be used in a lot of pages, a page may be used multiple times in the same; if using a store as the communication between components, then, how applications can be achieved without the component is a pure component associated with the business of it?

A: do not create pure components to create and use triggerEvent notify the parent components within the assembly to change store.data or call the store method of communication with the outside world.

发布了43 篇原创文章 · 获赞 17 · 访问量 5万+

Guess you like

Origin blog.csdn.net/tencent__open/article/details/102830212