setData和更符合直覺的編程體驗,只需update ,不需要再使用setData 。
Westore 架構和MVP(Model-View-Presenter) 架構很相似:
Store 層可以理解成中介者模式中的中介者,使View 和Model 之間的多對多關係數量減少為0,負責中轉控制視圖對象View 和模型對象Model 之間的交互。
隨著小程序承載的項目越來越複雜,合理的架構可以提升小程序的擴展性和維護性。把邏輯寫到Page/Component 是一種罪惡,當業務邏輯變得複雜的時候Page/Component 會變得越來越臃腫難以維護,每次需求變更如履薄冰, westore 定義了一套合理的小程序架構適用於任何復雜度的小程序,讓項目底座更健壯,易維護可擴展。
npm i westore --savenpm 相關問題參考:小程序官方文檔: npm 支持
| 專案 | 描述 |
|---|---|
| westore | westore 的核心代碼 |
| westore-example | westore 官方例子 |
| westore-example-ts | westore 官方例子(ts+scss) |

其類圖如下所示:

// 平台无关的 Model
import Counter from '../models/counter'
// 平台无关的 Model
import User from '../models/user'
import { Store } from 'westore'
// 页面 store,一个页面一个
class HomeStore extends Store {
constructor ( ) {
super ( )
this . data = {
count : 0 ,
motto : 'Hello World' ,
userInfo : null
}
// 消费 Model
this . counter = new Counter ( )
// 消费 Model
this . user = new User ( {
onUserInfoLoaded : ( ) => {
this . syncUserModel ( )
}
} )
this . syncCountModel ( )
}
// 同步 Model 的数据到 ViewModel 并更新视图
syncCountModel ( ) {
this . data . count = this . counter . count
this . update ( )
}
// 同步 Model 的数据到 ViewModel 并更新视图
syncUserModel ( ) {
this . data . motto = this . user . motto
this . data . userInfo = this . user . userInfo
this . update ( )
}
increment ( ) {
this . counter . increment ( )
this . syncCountModel ( )
}
decrement ( ) {
this . counter . decrement ( )
this . syncCountModel ( )
}
getUserProfile ( ) {
this . user . getUserProfile ( )
}
}
module . exports = new HomeStore通用Model 是框架無關的,對於這樣簡單的程序甚至不值得把這種邏輯分開,但是隨著需求的膨脹你會發現這麼做帶來的巨大好處。所以下面舉一個複雜一點點的例子。
遊戲截圖:

設計類圖:

圖中淺藍色的部分可以在小程序貪吃蛇、小遊戲貪吃蛇和Web貪吃蛇項目復用,不需要更改一行代碼。
應用截圖:

設計類圖:

圖中淺藍色的部分可以在小程序TodoApp 和Web TodoApp項目復用,不需要更改一行代碼。
官方例子把貪吃蛇和TodoApp做進了一個小程序目錄如下:
├─ models // 业务模型实体
│ └─ snake-game
│ ├─ game.js
│ └─ snake.js
│
│ ├─ log.js
│ ├─ todo.js
│ └─ user.js
│
├─ pages // 页面
│ ├─ game
│ ├─ index
│ ├─ logs
│ └─ other.js
│
├─ stores // 页面的数据逻辑,page 和 models 的桥接器
│ ├─ game-store.js
│ ├─ log-store.js
│ ├─ other-store.js
│ └─ user-store.js
│
├─ utils
詳細代碼點擊這裡
掃碼體驗:

回答setData 去哪了?之前先要思考為什麼westore 封裝了這個api,讓用戶不直接使用。在小程序中,通過setData改變視圖。
this . setData ( {
'array[0].text' : 'changed text'
} )但是符合直覺的編程體驗是:
this . data . array [ 0 ] . text = 'changed text'如果data 不是響應式的,需要手動update:
this . data . array [ 0 ] . text = 'changed text'
this . update ( )上面的編程體驗是符合直覺且對開發者更友好的。所以westore 隱藏了setData 不直接暴露給開發者,而是內部使用diffData 出最短更新路徑,暴露給開發者的只有update 方法。
先看一下westore diffData 的能力:
diff ( {
a : 1 , b : 2 , c : "str" , d : { e : [ 2 , { a : 4 } , 5 ] } , f : true , h : [ 1 ] , g : { a : [ 1 , 2 ] , j : 111 }
} , {
a : [ ] , b : "aa" , c : 3 , d : { e : [ 3 , { a : 3 } ] } , f : false , h : [ 1 , 2 ] , g : { a : [ 1 , 1 , 1 ] , i : "delete" } , k : 'del'
} )Diff 的結果是:
{ "a" : 1 , "b" : 2 , "c" : "str" , "d.e[0]" : 2 , "d.e[1].a" : 4 , "d.e[2]" : 5 , "f" : true , "h" : [ 1 ] , "g.a" : [ 1 , 2 ] , "g.j" : 111 , "g.i" : null , "k" : null } 
Diff 原理:
export function diffData ( current , previous ) {
const result = { }
if ( ! previous ) return current
syncKeys ( current , previous )
_diff ( current , previous , '' , result )
return result
}同步上一輪state.data 的key 主要是為了檢測array 中刪除的元素或者obj 中刪除的key。

提升編程體驗的同時,也規避了每次setData 都傳遞大量新數據的問題,因為每次diff 之後的patch 都是setData 的最短路徑更新。
所以沒使用westore 的時候經常可以看到這樣的代碼:

使用完westore 之後:
this . data . a . b [ 1 ] . c = 'f'
this . update ( ) 從目前來看,絕大部分的小程序項目都把業務邏輯堆積在小程序的Page 構造函數里,可讀性基本沒有,給後期的維護帶來了巨大的成本,westore 架構的目標把業務/遊戲邏輯解耦出去,Page 就是純粹的Page,它只負責展示和接收用戶的輸入、點擊、滑動、長按或者其他手勢指令,把指令中轉給store,store 再去調用真正的程序邏輯model,這種分層邊界清晰,維護性、擴展性和可測試性極強,單個文件模塊大小也能控制得非常合適。
MIT