騰訊原生小程序框架 OMIX 2.0 發佈

寫在前面

數據觸手可及,狀態無處遁形css

特性

  • 全局狀態管理
  • 無狀態視圖設計
  • 對小程序零入侵
  • 只有一個 API
  • 支持計算屬性
  • 輕鬆駕馭小項目、中項目和大型項目
  • 也適用小遊戲,是的沒錯,使用 小程序開發小遊戲,本文第二個案例使用 OMIX 實現一個小遊戲
  • 【更新】支持有狀態(data)的 Page,看文章最後的 QA

OMIX 2.0 是 westore 的進化版,westore 使用的是數據變動先後的 diff,diff 出的 json 就是 setData 的 patch,omix 2.0 使用的是 observer 監聽數據的變動獲得 setData 的 patch。 和 omix 對比,westore 運行時須要更多的計算,omix 初始化時須要更多的內存和計算,可是數據變動時 omix 速度比 westore 快,編程體驗方面,omix 不須要手動 update,westore 須要手動 update。html

好的設計只有一種,咱們認爲 OMIX 2.0 的設計剛恰好。react

快速入門

API

  • create(store, option) 建立頁面, store 從頁面注入,可跨頁面跨組件共享,若是 option 定義了 data,store 的 data 會掛載在 this.data.$ 下面
  • create(option) 建立組件
  • this.store.data 和 data,頁面和頁面全部組件能夠拿到, 操做 data 會自動更新視圖

不須要注入 store 的頁面或組件用使用PageComponent 構造器, Component 經過 triggerEvent 與上層通信或與上層的 store 交互git

簡單實戰

實現一個簡單的 log 列表的展現github

定義全局 store:編程

export default {
  data: {
    logs: []
  }
}
複製代碼

定義頁面:json

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.store.data.logs[0] = 'Changed!'
    }, 1000)

    setTimeout(() => {
      //響應式,自動更新視圖
      this.store.data.logs.push(Math.random(), Math.random())
    }, 2000)

    setTimeout(() => {
      //響應式,自動更新視圖
      this.store.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>
複製代碼

定義 test-store 組件, 組件內也能夠組件使用全局的 logs,組件源碼:小程序

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>
複製代碼

其餘可選配置說明

修改 store.js 的 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
}
複製代碼

全局更新開發默認是關閉的,調試開關默認打開,能夠在store.data 的全部變更都會出如今開發者工具 log 面板,以下圖所示:架構

其餘

這裏須要注意,改變數組的 length 不會觸發視圖更新,須要使用 size 方法:

this.store.data.arr.size(2) //會觸發視圖更新
this.store.data.arr.length = 2 //不會觸發視圖更新

this.store.data.arr.push(111) //會觸發視圖更新
//每一個數組的方法都有對應的 pure 前綴方法,好比 purePush、pureShift、purePop 等
this.store.data.arr.purePush(111) //不會觸發視圖更新

this.store.set(this.store.data, 'newProp', 'newPropVal')  //會觸發視圖更新
this.store.data.newProp = 'newPropVal' //新增屬性不會觸發視圖更新,必須使用 create.set
複製代碼

計算屬性

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

計算屬性定義在頁面或者組件的 computed 裏,如上面的 reverseMotto, 它能夠直接綁定在 wxml 裏,motto 更新會自動更新 reverseMotto 的值。

store 變化監聽

const handler = function (evt) {
  console.log(evt)
}
//監聽,容許綁定多個
store.onChange(handler)
//移除監聽
store.offChange(handler) 
複製代碼

複雜小程序 store 管理

當小程序變得很是複雜的時候,單文件單一的 store 會變得很是臃腫,這裏有兩種方案:

  • 拆分單一 store 到多個文件
  • 拆分單一 store 到多個 store

兩種方案能夠視狀況任選一種,或者兩種混合使用,好比對於超過100個頁面的小程序來講,多頁面多 store 應該是很常見的。

拆分單一 store 到多個文件

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++
}
複製代碼

store.js 合併因此子 store 到對應模塊(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
複製代碼

數據綁定:

<view>
  <text>{{a.name}}</text>
  <text>{{b.name}}-{{b.age}}</text>
</view>
複製代碼

數據使用:

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)
  }
})
複製代碼

多 store 注入的完整的案例能夠 點擊這裏

拆分單一 store 到多個 store

Page A:

import create from '../../utils/create'
import store from '../../store/store-page-a.js'

create(store, {
 
})
複製代碼

Page B:

import create from '../../utils/create'
import store from '../../store/store-page-b.js'

create(store, {
 
})
複製代碼

Page A 的 Page B 的 store 徹底是兩個不一樣的 store。

Path 命中規則

store.data 發生變化,相關依賴的組件會進行更新,舉例說明 Path 命中規則:

Observer Path(由數據更改產生) use 中的 path 是否更新
abc abc 更新
abc[1] abc 更新
abc.a abc 更新
abc abc.a 不更新
abc abc[1] 不更新
abc abc[1].c 不更新
abc.b abc.b 更新

只要注入組件的 path 等於 use 裏聲明 或者在 use 裏聲明的其中 path 子節點下就會進行更新,以上只要命中一個條件便進行更新!

若是你的小程序真的很小,那麼請無視上面的規則,直接把 store 的 updateAll 聲明爲 true 即可。若是小程序頁面不少很複雜,爲了更優的性能,請給每個頁面或非存組件聲明 use

貪吃蛇遊戲實戰

領域模型設計

  • 提取主要實體,好比(蛇、遊戲)
  • 從實體名詞中總結出具體業務屬性方法,
      • 包含運動方向、body屬性
      • 包含移動和轉向方法
    • 遊戲
      • 包含結束暫停狀態、地圖、分數、幀率、遊戲主角、食物
      • 包含開始遊戲、暫停遊戲、結束遊戲、生產食物、重置遊戲等方法
  • 創建實體屬性方法之間的聯繫
    • 遊戲主角惟一,即蛇
    • 蛇吃食物,遊戲分數增長
    • 食物消失,遊戲負責再次生產食物
    • 蛇撞牆或撞自身,遊戲狀態結束
  • 核心循環設計
    • 判斷是否有食物,沒有就生產一個(低幀率)
    • 蛇與自身碰撞檢測
    • 蛇與障礙物碰撞檢測
    • 蛇與食物碰撞檢測
    • 蛇移動

使用代碼描述蛇實體

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'
  }
}
複製代碼

蛇的轉向有個邏輯,就是不能反方向後退,好比正在向上移動,不能直接直接向下轉向,因此在 turnUp,turnRight,turnDown,turnLeft 中都有對應的條件判斷。

使用代碼描述遊戲實體

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))
  }
}
複製代碼

能夠看到上圖使用了 16*16 的二維數組來存儲蛇、食物、地圖信息。蛇和食物佔據的格子爲 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, 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]
]
複製代碼

因此上面表明了一條長度爲 5 的蛇和 1 個食物,你能在上圖中找到嗎?

定義 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
複製代碼

會發現, store 很薄,只負責中轉 View 的 action,到 Model,以及隱藏式自動映射 Model 上的數據到 View。

遊戲面板渲染

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>
複製代碼

帶有 class 爲 s 的格式是黑色的,好比食物、蛇的身體,其他的會灰色底色。

對應 js:

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

create({
  use: ['map']
})
複製代碼

map 表明依賴 store.data.map,map 更新會自動更新視圖。

控制主界面面板

<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>
複製代碼

主界面使用 page,引用 component:

{
  "usingComponents": {
    "game": "/components/game/index"
  }
}
複製代碼

對應 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()
  }
})
複製代碼

幀率控制

怎麼控制主幀率和局部幀率。通常狀況下,咱們認爲 60 FPS 是流暢的,因此咱們定時器間隔是有 16ms,核心循環裏的計算量越小,就越接近 60 FPS:

this.loop = setInterval(() => {
  //
}, 16)
複製代碼

可是有些計算沒有必要 16 ms計算一次,這樣會下降幀率,因此能夠記錄上一次執行的時間用來控制幀率:

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)
複製代碼

因爲小程序 JSCore 裏不支持 requestAnimationFrame,因此這裏使用 setInterval。固然也可使用 raf-interval 循環執行 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)
複製代碼

用法和 setInterval 一致,只是內部使用 setTimeout 且若是支持 requestAnimationFrame 會優先使用 requestAnimationFrame

→ 貪吃蛇源碼

貪吃蛇架構

那麼是整個項目是 MVC、MVP 仍是 MVVM?

從貪吃蛇源碼能夠看出:視圖(components,pages)和模型(models)是分離的,沒有相互依賴關係,可是在 MVC 中,視圖依賴模型,耦合度過高,致使視圖的可移植性大大下降,因此必定不是 MVC 架構。

在 MVP 模式中,視圖不直接依賴模型,由 Presenter 負責完成 Model 和 View 的交互。MVVM 和 MVP 的模式比較接近。ViewModel 擔任這 Presenter 的角色,而且提供 UI 視圖所須要的數據源,而不是直接讓 View 使用 Model 的數據源,這樣大大提升了 View 和 Model 的可移植性,好比一樣的 Model 切換使用 Flash、HTML、WPF 渲染,好比一樣 View 使用不一樣的 Model,只要 Model 和 ViewModel 映射好,View 能夠改動很小甚至不用改變。

從貪吃蛇源碼能夠看出,View(components) 裏直接使用了 Presenter(stores) 的 data 屬性進行渲染,data 屬性來自於 Model(models) 的屬性,並無出現 Model 到 ViewModel 的映射。因此必定不是 MVVM 架構。

因此上面的貪吃蛇屬於 MVP !只不過是進化版的 MVP,由於 M 裏的 map 的變動會自定更是 View,從 M->P->V的迴路是自動化的,代碼裏看不到任何邏輯。僅僅須要聲明依賴:

use: ['map']
複製代碼

這樣也規避了 MVVM 最大的問題: M 到 VM 映射的開銷。

進化版 MVP 優點

一、複用性

Model 和 View 之間解耦,Model 或 View 中的一方發生變化,Presenter 接口不變,另外一方就不必對上述變化作出改變,那麼 Model 層的業務邏輯具備很好的靈活性和可重用性。

二、靈活性

Presenter 的 data 變動自動映射到視圖,使得 Presenter 很薄很薄,View 屬於被動視圖。並且基於 Presenter 的 data 可使用任何平臺、任何框架、任何技術進行渲染。

三、測試性

假如 View 和 Model 之間的緊耦合,在 Model 和 View 同時開發完成以前對其中一方進行測試是不可能的。出於一樣的緣由,對 View 或 Model 進行單元測試很困難。如今,MVP模式解決了全部的問題。MVP 模式中,View 和 Model 之間沒有直接依賴,開發者可以藉助模擬對象注入測試二者中的任一方。

舉個邏輯複用的例子,好比 OMI 團隊發起的 snake-mvp 項目,下面的幾個項目的 model 和 presenter 幾乎如出一轍,徹底複用,只是渲染視圖層根據不一樣的框架作了不一樣的適配。

好比 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>
  },
![](https://user-gold-cdn.xitu.io/2019/10/31/16e1f8959beb0fea?w=1344&h=950&f=png&s=110201)
  useSelf: ['paused'],
  store
})
複製代碼

Q & A

  • 好比我一個彈窗組件,可能在不少頁面使用,也可能在同一個頁面使用屢次;若是使用store來做爲組件間通訊的話,怎麼應用能夠實現組件是純組件而不跟業務相關呢?

純組件不用不用 create 建立,且該組件內使用 triggerEvent 通知父組件改變 store.data 或者調用 store 的方法與外界通信。

  • TypeScript 版本有嗎?

TypeScript 版本的例子能夠點擊這裏 omix-ts

  • 關於 omix 不支持定義私有 data 的疑惑

項目一開始的時候沒有使用 omix 作狀態管理,後來因爲業務需求引用了 omix,可是使用過程當中發現 omix v2 不容許定義私有 data。

若是不支持定義私有 data 的話,那麼在項目上使用 omix 的時候,是否就須要把全部頁面原有的 data 都合併到 store 中?

在頁面數量過多的時候,我想這是一件比較痛苦的事。

對比了一下 omix、omix-v一、westore、dd-store,發現後三者都支持定義私有 data。

想請教一下,是什麼緣由,致使 omix 捨棄了 omix-v1 支持定義私有 data 的特性呢?

這是否違背了 omix 對小程序零入侵的特性?

答:支持頁面擁有私有 data:

舉個例子:

create(store, {
  use: [
    'motto',
    'userInfo',
    'hasUserInfo',
    'canIUse'
  ],
  computed: {
    reverseMotto() {
      return this.motto.split('').reverse().join('')
    }
  },
  data: {
    name: 'omix'
  },
複製代碼

這個時候全局 store 的 data 會掛在在 data.$ 下,因此綁定 wxml 的時候須要加上前綴,好比:

<!--index.wxml-->
<view class="container">
  <view class="userinfo">
    <button wx:if="{{!$.hasUserInfo && $.canIUse}}" open-type="getUserInfo" bindgetuserinfo="getUserInfo"> 獲取頭像暱稱 </button>
    <block wx:else>
      <image bindtap="bindViewTap" class="userinfo-avatar" src="{{$.userInfo.avatarUrl}}" mode="cover"></image>
      <text class="userinfo-nickname">{{$.userInfo.nickName}}</text>
    </block>
  </view>
  <view class="usermotto">
    <text class="user-motto">{{$.motto}}-{{reverseMotto}}-{{name}}</text>
  </view>
  <test-store />
</view>
複製代碼

注意,data 和 computed 的屬性不須要 $ 前綴。

Github

任何意見建議歡迎反饋。

相關文章
相關標籤/搜索