最近接了一個小程序項目,對於之前只寫過一個小工具的我而言,是時候考察一波小程序的基本功了(認真臉)。 html
上手先了解了各路大神擼小程序的方式,前有基於vue語法的 mpvue ,專職生成小程序;又有基於react的京東團隊的 taro 在後,一語多端,支持react語法生成小程序、H五、react-native......;還有官方 wepy,仿vue語法,官方支持更穩定......都芥末🐂的嗎? 趕忙每一個都學習了一下。vue
然鵝—— react
因而手擼原生框架,因而遇到了原生框架中一個最大的問題,全局狀態同步管理 /(ㄒoㄒ)/~~。git
小程序框架提供了許多開箱即用的組件,大大提升咱們的開發效率。可是做爲一個不能直接引用js npm包的語法(支持的模式很繁瑣)
,同時小程序自己也沒有提供相似redux、vuex的全局的狀態管理工具,這簡直違反了mvc(mvvm)黨的一向做風。github
因而想到了手寫一個簡單的全局狀態管理庫,從各方面考察彷佛可行,畢竟是一個接近vue的框架。vuex
(可直接翻到文末查看代碼完整版)
。小程序官方提供且推薦的demo中是把全局數據放在app實例上—— 示例 ,咋一看彷佛很接近咱們的全局狀態管理需求,但這只是一個數據存儲方式,徹底無法作到響應式狀態。npm
想一想咱們常見的需求,在我的中心頁點擊「去登陸」,跳轉到登陸頁,測試一番騷操做,好不容易登陸成功了,返回我的中心,依舊是一個大大的「去登錄」按鈕在嘲諷着他/她,因而測試打了你一頓並讓你回去加班。redux
這時候你徹底能夠在onShow中使用this.setData
刷新每一次頁面展開......前提是你不怕繁瑣,同時願意消耗更多的性能(sex power)。小程序
因此開始手寫,第一步,在項目中生成一個/store/sotre.js
文件。react-native
再放兩個輪子中經常使用的方法
const _toString = Object.prototype.toString
function isFunction(obj) {
return typeof obj === 'function' || false
}
function isObject(obj) {
return _toString.call(obj) === '[object Object]' || false
}
複製代碼
全局狀態管理理索固然須要一個全局的狀態存儲,同時考慮使用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
複製代碼
如今的打算是將_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(options) {
// 存儲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, options)
}
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的方法:
/** * 全部的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(options) {
// 存儲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, options)
}
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端的輕應用,自己的特質就決定了它不適合實現太過複雜的功能。
個人github ,項目已提交到npm/miniprogram-sync-state,歡迎使用,若是能順便給個star就更好了 🌹🌹
—— The End