世界上最小卻強大的小程序框架 - 100多行代碼搞定全局狀態管理和跨頁通信前端
Github: https://github.com/dntzhang/westoregit
衆所周知,小程序經過頁面或組件各自的 setData 再加上各類父子、祖孫、姐弟、嫂子與堂兄等等組件間的通信會把程序搞成一團漿糊,若是再加上跨頁面之間的組件通信,會讓程序很是難維護和調試。雖然市面上出現了許多技術棧編譯轉小程序的技術,可是我覺沒有戳中小程序的痛點。小程序無論從組件化、開發、調試、發佈、灰度、回滾、上報、統計、監控和最近的雲能力都很是完善,小程序的工程化簡直就是前端的典範。而開發者工具也在持續更新,能夠想象的將來,組件佈局的話未必須要寫代碼了。因此最大的痛點只剩下狀態管理和跨頁通信。github
受 Omi 框架 的啓發,且專門爲小程序開發的 JSON Diff 庫,因此有了 westore 全局狀態管理和跨頁通信框架讓一切盡在掌握中,且受高性能 JSON Diff 庫的利好,長列表滾動加載顯示變得輕鬆可駕馭。總結下來有以下特性和優點:web
Westore API 只有三個, 大道至簡:json
export default { data: { motto: 'Hello World', userInfo: {}, hasUserInfo: false, canIUse: wx.canIUse('button.open-type.getUserInfo'), logs: [] }, logMotto: function () { console.log(this.data.motto) } }
你不須要在頁面和組件上再聲明 data 屬性。若是申明瞭也不要緊,會被 Object.assign 覆蓋到 store.data 上。後續只需修改 this.store.data 即可。小程序
import store from '../../store' import create from '../../utils/create' const app = getApp() create(store, { onLoad: function () { if (app.globalData.userInfo) { this.store.data.userInfo = app.globalData.userInfo this.store.data.hasUserInfo = true this.update() } else if (this.data.canIUse) { app.userInfoReadyCallback = res => { this.store.data.userInfo = res.userInfo this.store.data.hasUserInfo = true this.update() } } else { wx.getUserInfo({ success: res => { app.globalData.userInfo = res.userInfo this.store.data.userInfo = res.userInfo this.store.data.hasUserInfo = true this.update() } }) } } })
建立 Page 只需傳入兩個參數,store 從根節點注入,全部子組件都能經過 this.store 訪問。api
<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}}</text> </view> <hello></hello> </view>
和之前的寫法沒有差異,直接把 store.data
做爲綁定數據源。數組
this.store.data.any_prop_you_want_to_change = 'any_thing_you_want_change_to' this.update()
import create from '../../utils/create' create({ ready: function () { //you can use this.store here }, methods: { //you can use this.store here } })
和建立 Page 不同的是,建立組件只需傳入一個參數,不須要傳入 store,由於已經從根節點注入了。架構
this.store.data.any_prop_you_want_to_change = 'any_thing_you_want_change_to' this.update()
拿官方模板示例的 log 頁面做爲例子:app
this.setData({ logs: (wx.getStorageSync('logs') || []).map(log => { return util.formatTime(new Date(log)) }) })
使用 westore 後:
this.store.data.logs = (wx.getStorageSync('logs') || []).map(log => { return util.formatTime(new Date(log)) }) this.update()
看似一條語句變成了兩條語句,可是 this.update 調用的 setData 是 diff 後的,因此傳遞的數據更少。
使用 westore 你不用關係跨頁數據同步,你只須要專一 this.store.data 即可,修改完在任意地方調用 update 即可:
this.update()
console.log(getApp().globalData.store.data)
不排除小程序被作大得可能,接觸的最大的小程序有 60+ 的頁面,因此怎麼管理?這裏給出了兩個最佳實踐方案。
export default { data: { commonA: 'a', commonB: 'b', pageA: { a: 1 xx: 'xxx' }, pageB: { b: 2, c: 3 } }, xxx: function () { console.log(this.data) } }
a.js
export default { data: { a: 1 xx: 'xxx' }, aMethod: function (num) { this.data.a += num } }
b.js
export default { data: { b: 2, c: 3 }, bMethod: function () { } }
store.js
import a from 'a.js' import b from 'b.js' export default { data: { commonNum: 1, commonB: 'b', pageA: a.data pageB: b.data }, xxx: function () { //you can call the methods of a or b and can pass args to them console.log(a.aMethod(commonNum)) }, xx: function(){ } }
固然,也能夠不用按照頁面拆分文件或模塊,也能夠按照領域來拆分,這個很自由,視狀況而定。
--------------- ------------------- ----------------------- | this.update | → | json diff | → | setData()-setData()...| → 以後就是黑盒(小程序官方實現,可是 dom/apply diff 確定是少不了) --------------- ------------------- -----------------------
雖然和 Omi 同樣同爲 store.updata 可是卻有着本質的區別。Omi 的以下:
--------------- ------------------- ---------------- ------------------------------ | this.update | → | setState | → | jsx rerender | → | vdom diff → apply diff... | --------------- ------------------- ---------------- ------------------------------
都是數據驅動視圖,但本質不一樣,緣由:
先看一下我爲 westore 專門定製開發的 JSON Diff 庫 的能力:
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 default function diff(current, pre) { const result = {} syncKeys(current, pre) _diff(current, pre, '', result) return result }
同步上一輪 state.data 的 key 主要是爲了檢測 array 中刪除的元素或者 obj 中刪除的 key。
setData 是小程序開發中使用最頻繁的接口,也是最容易引起性能問題的接口。在介紹常見的錯誤用法前,先簡單介紹一下 setData 背後的工做原理。setData 函數用於將數據從邏輯層發送到視圖層(異步),同時改變對應的 this.data 的值(同步)。
其中 key 能夠以數據路徑的形式給出,支持改變數組中的某一項或對象的某個屬性,如 array[2].message,a.b.c.d,而且不須要在 this.data 中預先定義。好比:
this.setData({ 'array[0].text':'changed data' })
因此 diff 的結果能夠直接傳遞給 setData
,也就是 this.update
。
小程序的視圖層目前使用 WebView 做爲渲染載體,而邏輯層是由獨立的 JavascriptCore 做爲運行環境。在架構上,WebView 和 JavascriptCore 都是獨立的模塊,並不具有數據直接共享的通道。當前,視圖層和邏輯層的數據傳輸,實際上經過兩邊提供的 evaluateJavascript 所實現。即用戶傳輸的數據,須要將其轉換爲字符串形式傳遞,同時把轉換後的數據內容拼接成一份 JS 腳本,再經過執行 JS 腳本的形式傳遞到兩邊獨立環境。
而 evaluateJavascript 的執行會受不少方面的影響,數據到達視圖層並非實時的。
常見的 setData 操做錯誤:
上面是官方截取的內容。使用 webstore 的 this.update 本質是先 diff,再執行一連串的 setData,因此能夠保證傳遞的數據每次維持在最小。既然可使得傳遞數據最小,因此第一點和第三點雖有違反但能夠商榷。
這裏區分在頁面中的 update 和 組件中的 update。頁面中的 update 在 onLoad 事件中進行實例收集。
const onLoad = option.onLoad option.onLoad = function () { this.store = store rewriteUpdate(this) store.instances[this.route] = [] store.instances[this.route].push(this) onLoad && onLoad.call(this) } Page(option)
組件中的 update 在 ready 事件中進行行實例收集:
const ready = store.ready store.ready = function () { this.page = getCurrentPages()[getCurrentPages().length - 1] this.store = this.page.store; this.setData.call(this, this.store.data) rewriteUpdate(this) this.store.instances[this.page.route].push(this) ready && ready.call(this) } Component(store)
rewriteUpdate 的實現以下:
function rewriteUpdate(ctx){ ctx.update = () => { const diffResult = diff(ctx.store.data, originData) for(let key in ctx.store.instances){ ctx.store.instances[key].forEach(ins => { ins.setData.call(ins, diffResult) }) } for (let key in diffResult) { updateOriginData(originData, key, diffResult[key]) } } }
MIT @dntzhang