手把手系列,100行程式碼搞定微信小程式全域性狀態同步
最近接了一個小程式專案,對於以前只寫過一個小工具的我而言,是時候考察一波小程式的基本功了(認真臉)。

上手先了解了各路大神擼小程式的方式,前有基於vue語法的mpvue ,專職生成小程式;又有基於react的京東團隊的taro 在後,一語多端,支援react語法生成小程式、H5、react-native......;還有官方wepy,仿vue語法,官方支援更穩定......都芥末:ox:的嗎? 趕緊每個都學習了一下。
然鵝——

!!翻開issue頁,似乎都有幾十到上百條的open isuue未解決,同時還有一些詭異的bug夾雜其中,好怕怕。遂放棄......逃
於是手擼原生框架,於是遇到了原生框架中一個最大的問題,全域性狀態同步管理 /(ㄒoㄒ)/~~。
小程式框架提供了許多開箱即用的元件,大大提高我們的開發效率。但是作為一個不能直接引用js npm包的語法 (支援的模式很繁瑣)
,同時小程式本身也沒有提供類似redux、vuex的全域性的狀態管理工具,這簡直違反了mvc(mvvm)黨的一貫作風。
於是想到了手寫一個簡單的全域性狀態管理庫,從各方面考察似乎可行,畢竟是一個接近vue的框架。
心路歷程如上。。。。。。還是不廢話了,上主菜 (可直接翻到文末檢視程式碼完整版)
。
小程式官方提供且推薦的demo中是把全域性資料放在app例項上——示例 ,咋一看似乎很接近我們的全域性狀態管理需求,但這只是一個數據儲存方式,完全沒法做到響應式狀態。
想想我們常見的需求,在個人中心頁點選“去登入”,跳轉到登入頁,測試一番騷操作,好不容易登入成功了,返回個人中心,依舊是一個大大的“去登陸”按鈕在嘲諷著他/她,於是測試打了你一頓並讓你回去加班。

這時候你完全可以在onShow中使用 this.setData
重新整理每一次頁面展開......前提是你不怕繁瑣,同時願意消耗更多的效能(sex power)。
所以開始手寫,第一步,在專案中生成一個 /store/sotre.js
檔案。
再放兩個輪子中常用的方法
const _toString = Object.prototype.toString function isFunction(obj) { return typeof obj === 'function' || false } function isObject(obj) { return _toString.call(obj) === '[object Object]' || false } 複製程式碼
createStore
全域性狀態管理理索當然需要一個全域性的狀態儲存,同時考慮使用react-redux的connect模式做繫結:
let _state = null function connect(mapStateToData, mapMethodTopPage) { ... } /** * 建立store物件 * * @param { Object } store * @returns { Object } _Store */ function createStore(state) { if (_state) { console.warn( 'there are multiple store active. This might lead to unexpected results.' ) } _state = Object.assign({}, state) // 這裡返回_Store的原因是因為想通過app例項直接獲取 // const { connect, setState, createStore } = getApp().Store return _Store } const _Store = { connect, setState, createStore } module.exports = _Store 複製程式碼
connect
現在的打算是將_state作為內部儲存,以免暴露出去被直接操作,無法做到響應式(單一狀態樹只讀原則)。接下來的重點當然是作為繫結資料和修改資料相互響應了,先來connect:
let _state = null let _subjects = [] // 用來儲存頁面例項物件 let _observers = [] // 用來儲存狀態響應器 /** * 仿寫react-redux的connect簡單工廠 * * @param { Function } mapStateToData * @param { Function } mapMethodTopPage * @returns { Function } pageConnect */ function connect(mapStateToData, mapMethodTopPage) { // mapStateToData接收state引數,且必須返回一個繫結物件,key會被繫結到page例項的data中 const dataMap = mapStateToData ? mapStateToData(_state) : {} // mapMethodTopPage接收setState和state引數,且必須返回一個繫結物件,key會被繫結到page例項上 const methodMap = mapMethodTopPage ? mapMethodTopPage(setState, _state) : {} return function(pageObject) { // 接收page物件 // 遍歷繫結data for (let dataKey in dataMap) { if (pageObject.data) { if (pageObject.data[dataKey]) { console.warn( `page class had data ${dataKey}, connect map will cover this prop.` ) } pageObject.data[dataKey] = dataMap[dataKey] } else { pageObject.data = { [dataKey]: dataMap[dataKey] } } } // 遍歷繫結method for (let methodKey in methodMap) { pageObject[methodKey] = methodMap[methodKey] } // 儲存onLoad、onUnload周期函式,以便對其做改造 const onLoad = pageObject.onLoad const onUnload = pageObject.onUnload pageObject.onLoad = function() { // 儲存page例項和事件響應器,兩者保持同步,一個例項對應一個響應器 if (!~_subjects.indexOf(this)) { // 首次load需要修改data this.setData(mapStateToData ? mapStateToData(_state) : {}) _subjects.push(this) _observers.push(() => { // mapStateToData生成新的mapData,並使用this.setData更新page狀態 this.setData(mapStateToData ? mapStateToData(_state) : {}) }) } // 觸發原有生命週期函式 onLoad && onLoad.call(this) } pageObject.onUnload = function() { // 登出響應器 const index = _subjects.indexOf(this) if (!~index) { _subjects.splice(index, 1) _observers.splice(index, 1) } // 觸發原有生命週期函式 onUnload && onUnload.call(this) } return pageObject } } 複製程式碼
setState
狀態儲存和繫結都有了,現在需要一個修改state的方法:
/** * 所有的state狀態修改必須通過setState方法,以完成正常的響應 * * @param { Object | Function } state */ function setState(state) { // state 接收需要更新的state物件或者一個接收state的方法,該方法必須返回一個state更新物件 let newState = state if (isFunction(state)) { newState = state(_state) } // 合併新狀態 _state = Object.assign(_state, newState) // 觸發響應器 _observers.forEach(function(observer) { isFunction(observer) && observer() }) } 複製程式碼
完整的程式碼
最後加上一些報錯資訊:
function isFunction(obj) { return typeof obj === 'function' || false } function isObject(obj) { return obj.toString() === '[object Object]' || false } let _state = null const _subjects = [] // 用來儲存頁面例項物件 const _observers = [] // 用來儲存狀態響應器 /** * 仿寫react-redux的connect簡單工廠 * * @param { Function } mapStateToData * @param { Function } mapMethodTopPage * @returns { Function } constructorConnect */ function connect(mapStateToData, mapMethodTopPage) { if (mapStateToData !== undefined && !isFunction(mapStateToData)) { throw new Error( `connect first param accept a function, but got a ${typeof mapStateToData}` ) } if (mapMethodTopPage !== undefined && !isFunction(mapMethodTopPage)) { throw new Error( `connect second param accept a function, but got a ${typeof mapMethodTopPage}` ) } // mapStateToData接收state引數,且必須返回一個繫結物件,key會被繫結到page例項的data中 const dataMap = mapStateToData ? mapStateToData(_state) : {} // mapMethodTopPage接收setState和state引數,且必須返回一個繫結物件,key會被繫結到page例項上 const methodMap = mapMethodTopPage ? mapMethodTopPage(setState, _state) : {} return function(pageObject) { // 接收page物件 if (!isObject(pageObject)) { throw new Error( `page object connect accept a page object, but got a ${typeof pageObject}` ) } // 遍歷繫結data for (const dataKey in dataMap) { if (pageObject.data) { if (pageObject.data[dataKey]) { console.warn( `page object had data ${dataKey}, connect map will cover this prop.` ) } pageObject.data[dataKey] = dataMap[dataKey] } else { pageObject.data = { [dataKey]: dataMap[dataKey] } } } // 遍歷繫結method for (const methodKey in methodMap) { if (pageObject[methodKey]) { console.warn( `page object had method ${methodKey}, connect map will cover this method.` ) } pageObject[methodKey] = methodMap[methodKey] } // 儲存onLoad、onUnload周期函式,以便對其做改造 const onLoad = pageObject.onLoad const onUnload = pageObject.onUnload pageObject.onLoad = function() { // 儲存page例項和事件響應器,兩者保持同步,一個例項對應一個響應器 if (!~_subjects.indexOf(this)) { // 首次load需要修改data this.setData(mapStateToData ? mapStateToData(_state) : {}) _subjects.push(this) _observers.push(() => { // mapStateToData生成新的mapData,並使用this.setData更新page狀態 this.setData(mapStateToData ? mapStateToData(_state) : {}) }) } // 觸發原有生命週期函式 onLoad && onLoad.call(this) } pageObject.onUnload = function() { // 登出響應器 const index = _subjects.indexOf(this) if (!~index) { _subjects.splice(index, 1) _observers.splice(index, 1) } // 觸發原有生命週期函式 onUnload && onUnload.call(this) } return pageObject } } /** * 所有的state狀態修改必須通過setState方法,以完成正常的響應 * * @param { Object | Function } state */ function setState(state) { // state 接收需要更新的state物件或者一個接收state的方法,該方法必須返回一個state更新物件 let newState = state if (isFunction(state)) { newState = state(_state) } // 合併新狀態 _state = Object.assign(_state, newState) // 觸發響應器 _observers.forEach(function(observer) { isFunction(observer) && observer() }) } /** * 建立store物件 * * @param { Object } store * @returns { Object } _Store */ function createStore(state) { if (_state) { console.warn( 'there are multiple store active. This might lead to unexpected results.' ) } _state = Object.assign({}, state) // 這裡返回_Store的原因是因為想通過app例項直接獲取 // const { connect, setState, createStore } = getApp().Store return _Store } const _Store = { connect, setState, createStore } module.exports = _Store 複製程式碼
確實夠簡單吧,缺點是不支援模組化和component,也沒有實現reducer和action,但是這些,我統統都不要 。 考慮現有需求和效能影響,目前沒有支援component和模組化state——“小”程式方向靠攏(其實是懶)。
“小程式是一種不需要下載安裝即可使用的應用,它實現了應用‘觸手可及’的夢想,使用者掃一掃或搜一下即可開啟應用;也體現了‘用完即走’的理念,使用者不用關心是否安裝太多應用的問題。應用將無處不在,隨時可用,但又無需安裝解除安裝。”
“微信之父”張小龍的這段話確定了小程式的開發基調。鑑於小程式作為Web端的輕應用,本身的特質就決定了它不適合實現太過複雜的功能(為我的懶找到了官方支援)。