天天看點

「前端」Westore -更好的小程式項目架構

作者:架構思考
「前端」Westore -更好的小程式項目架構
  • Object-Oriented Programming: Westore 強制使用面向對象程式設計,開發者起手不是直接寫頁面,而是使用職責驅動設計 (Responsibility-Driven Design)的方式抽象出類、類屬性和方法以及類之間的關聯關系。
  • Write Once, Use Anywhere(Model): 通過面向對象分析設計出的 Model 可以表達整個業務模型,開發者可移植 100% 的 Model 代碼不帶任何改動到其他環境,并使用其他渲染技術承載項目的 View,比如小程式WebView、小遊戲、Web浏覽器、Canvas、WebGL
  • Passive View: Westore 架構下的 View 非常薄,沒有參雜任何業務邏輯,隻做被動改變。
  • Simple and Intuitive: Westore 内部使用 deepClone + dataDiff 換取最短路徑 setData 和更符合直覺的程式設計體驗,隻需 update,不需要再使用 setData
  • Testability: View 和 Model 之間沒有直接依賴,開發者能夠借助模拟對象注入測試兩者中的任一方
「前端」Westore -更好的小程式項目架構

Westore 架構和 MVP(Model-View-Presenter) 架構很相似:

  • View 和 Store 是雙向通訊,View 和 Store 互相引用
  • View 與 Model 不發生聯系,都通過 Store 傳遞
  • Store 引用 Model 裡對象的執行個體,Model 不依賴 Store
  • View 非常薄,不部署任何業務邏輯,稱為"被動視圖"(Passive View),即沒有任何主動性
  • Store 非常薄,隻複雜維護 View 需要的資料和橋接 View 和 Model
  • Model 非常厚,所有邏輯都部署在那裡,Model 可以脫離 Store 和 View 完整表達所有業務/遊戲邏輯

Store 層可以了解成 中介者模式 中的中介者,使 View 和 Model 之間的多對多關系數量減少為 0,負責中轉控制視圖對象 View 和模型對象 Model 之間的互動。

随着小程式承載的項目越來越複雜,合理的架構可以提升小程式的擴充性和維護性。把邏輯寫到 Page/Component 是一種罪惡,當業務邏輯變得複雜的時候 Page/Component 會變得越來越臃腫難以維護,每次需求變更如履薄冰, westore 定義了一套合理的小程式架構适用于任何複雜度的小程式,讓項目底座更健壯,易維護可擴充。

安裝

npm i westore --save

npm 相關問題參考-小程式官方文檔: npm 支援:https://developers.weixin.qq.com/miniprogram/dev/devtools/npm.html

Packages

  • westore 的核心代碼:https://github.com/Tencent/westore/tree/master/packages/westore
  • westore 官方例子:https://github.com/Tencent/westore/tree/master/packages/westore-example
  • westore 官方例子(ts+scss):https://github.com/Tencent/westore/tree/master/packages/westore-example-ts

舉個例子

開發如下圖所示的重命名 app

「前端」Westore -更好的小程式項目架構

按照傳統的小程式開發三部曲:

寫頁面結構 wxml

寫頁面樣式 wxss

寫頁面邏輯 js/ts

省略 wxml、wxss,js 如下:

Page({
  data: {
    nickName: ''
  },


  async onLoad() {
    const nickName = await remoteService.getNickName()
    this.setData({
      nickName: nickName
    })
  },


  async modifyNickName(newNickName) {
    await remoteService.modifyNickName(newNickName)
  },


  clearInput() {
    this.setData({
      nickName: ''
    })
  }
})           

需求開發全部結束。

使用 Westore 重構

「前端」Westore -更好的小程式項目架構

定義 User 實體:

class User {
  constructor({ nickName, onNickNameChange }) {
    this.nickName = nickName || ''
    this.onNickNameChange = onNickNameChange || function() { }
  }


  checkNickName() {
    // 省略 nickName 規則校驗
  }


  modifyNickName(nickName) {
    if(this.checkNickName(nickName) && nickName !== this.nickName) {
      this.nickName = nickName
      this.onNickNameChange(nickName)
    }
  }
}


module.exports = User           

定義 UserStore:

const { Store } = require('westore')
const User = require('../models/user')


class UserStore extends Store {
  constructor(options) {
    super()
    this.options = options
    this.data = {
      nickName: ''
    }
  }


  init() {
    const nickName = await remoteService.getNickName()
    this.user = new User({ 
      nickName,
      onNickNameChange: (newNickName)=>{
        this.data.nickName = newNickName
        this.update()
        await remoteService.modifyNickName(newNickName)
      } 
    })
  }


  async saveNickName(newNickName) {
    this.user.modifyNickName(newNickName)
  },


  modifyInputNickName(input) {
    this.data.nickName = input
    this.update()
  }
}


module.exports = new UserStore           

頁面使用 UserStore:

const userStore = require('../../stores/user-store')


Page({
  data: userStore.data,


  onLoad() {
    /* 綁定 view 到 store 
      也可以給 view 取名 userStore.bind('userPage', this)
      取名之後在 store 裡可通過 this.update('userPage') 更新 view
      不取名可通過 this.update() 更新 view
    */
    userStore.bind(this)
  },


  saveNickName(newNickName) {
    userStore.saveNickName(newNickName)
  },


  onInputChange(evt) {
    userStore.modifyInputNickName(evt.currentTarget.value)
  },


  clearInput() {
    userStore.modifyInputNickName('')
  }
})           

通用 Model 是架構無關的,對于這樣簡單的程式甚至不值得把這種邏輯分開,但是随着需求的膨脹你會發現這麼做帶來的巨大好處。是以下面舉一個複雜一點點的例子。

貪吃蛇案例

遊戲截圖:

「前端」Westore -更好的小程式項目架構

設計類圖:

「前端」Westore -更好的小程式項目架構

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

TodoApp 案例

應用截圖:

「前端」Westore -更好的小程式項目架構

設計類圖:

「前端」Westore -更好的小程式項目架構

圖中淺藍色的部分可以在小程式 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           

詳細代碼(複制到浏覽器檢視):https://github.com/Tencent/westore/tree/master/packages/westore-example

原理

setData 去哪了?

回答 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 方法。

Diff Data

先看一下 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 原理:

  • 同步所有 key 到目前 store.data
  • 攜帶 path 和 result 遞歸周遊對比所有 key value
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。

Westore 實作細節

「前端」Westore -更好的小程式項目架構

提升程式設計體驗的同時,也規避了每次 setData 都傳遞大量新資料的問題,因為每次 diff 之後的 patch 都是 setData 的最短路徑更新。

是以沒使用 westore 的時候經常可以看到這樣的代碼:

「前端」Westore -更好的小程式項目架構

使用完 westore 之後:

this.data.a.b[1].c = 'f'
this.update()           

小結

從目前來看,絕大部分的小程式項目都把業務邏輯堆積在小程式的 Page 構造函數裡,可讀性基本沒有,給後期的維護帶來了巨大的成本,westore 架構的目标把業務/遊戲邏輯解耦出去,Page 就是純粹的 Page,它隻負責展示和接收使用者的輸入、點選、滑動、長按或者其他手勢指令,把指令中轉給 store,store 再去調用真正的程式邏輯 model,這種分層邊界清晰,維護性、擴充性和可測試性極強,單個檔案子產品大小也能控制得非常合适。

文章來源:

https://mp.weixin.qq.com/s?src=11×tamp=1684414832&ver=4536&signature=wtDP1JoB*oJB4fAJC6jMOatCxHKUGB3TKbdnzePILGXQRIZeuHI-cPd8xaSHcyKE5cEj6iHYyFi0jXfXH6JLG8NQGHveiIJimdhlwIkdWgIc9vhyDK4RclJ2XkAnBZlI&new=1

繼續閱讀