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 pagecreate(option)
Creating a componentthis.store
Andthis.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
Page
andComponent
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.