最近接了一個小程序項目,對于以前只寫過一個小工具的我而言,是時候考察一波小程序的基本功了(認(rèn)真臉)。

上手先了解了各路大神擼小程序的方式,前有基于vue語法的mpvue ,專職生成小程序;又有基于react的京東團(tuán)隊的taro 在后,一語多端,支持react語法生成小程序、H5、react-native......;還有官方wepy,仿vue語法,官方支持更穩(wěn)定......都芥末:ox:的嗎? 趕緊每個都學(xué)習(xí)了一下。
然鵝——

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

這時候你完全可以在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
}
復(fù)制代碼
|
createStore
全局狀態(tài)管理理索當(dāng)然需要一個全局的狀態(tài)存儲,同時考慮使用react-redux的connect模式做綁定:
let _state = null
function connect(mapStateToData, mapMethodTopPage) {
...
}
/**
* 創(chuàng)建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
復(fù)制代碼
|
connect
現(xiàn)在的打算是將_state作為內(nèi)部存儲,以免暴露出去被直接操作,無法做到響應(yīng)式(單一狀態(tài)樹只讀原則)。接下來的重點當(dāng)然是作為綁定數(shù)據(jù)和修改數(shù)據(jù)相互響應(yīng)了,先來connect:
let _state = null
let _subjects = [] // 用來存儲頁面實例對象
let _observers = [] // 用來存儲狀態(tài)響應(yīng)器
/**
* 仿寫react-redux的connect簡單工廠
*
* @param { Function } mapStateToData
* @param { Function } mapMethodTopPage
* @returns { Function } pageConnect
*/
function connect(mapStateToData, mapMethodTopPage) {
// mapStateToData接收state參數(shù),且必須返回一個綁定對象,key會被綁定到page實例的data中
const dataMap = mapStateToData ? mapStateToData(_state) : {}
// mapMethodTopPage接收setState和state參數(shù),且必須返回一個綁定對象,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周期函數(shù),以便對其做改造
const onLoad = pageObject.onLoad
const onUnload = pageObject.onUnload
pageObject.onLoad = function() {
// 存儲page實例和事件響應(yīng)器,兩者保持同步,一個實例對應(yīng)一個響應(yīng)器
if (!~_subjects.indexOf(this)) {
// 首次load需要修改data
this.setData(mapStateToData ? mapStateToData(_state) : {})
_subjects.push(this)
_observers.push(() => {
// mapStateToData生成新的mapData,并使用this.setData更新page狀態(tài)
this.setData(mapStateToData ? mapStateToData(_state) : {})
})
}
// 觸發(fā)原有生命周期函數(shù)
onLoad && onLoad.call(this)
}
pageObject.onUnload = function() {
// 注銷響應(yīng)器
const index = _subjects.indexOf(this)
if (!~index) {
_subjects.splice(index, 1)
_observers.splice(index, 1)
}
// 觸發(fā)原有生命周期函數(shù)
onUnload && onUnload.call(this)
}
return pageObject
}
}
復(fù)制代碼
|
setState
狀態(tài)存儲和綁定都有了,現(xiàn)在需要一個修改state的方法:
復(fù)制代碼
/**
* 所有的state狀態(tài)修改必須通過setState方法,以完成正常的響應(yīng)
*
* @param { Object | Function } state
*/
function setState(state) {
// state 接收需要更新的state對象或者一個接收state的方法,該方法必須返回一個state更新對象
let newState = state
if (isFunction(state)) {
newState = state(_state)
}
// 合并新狀態(tài)
_state = Object.assign(_state, newState)
// 觸發(fā)響應(yīng)器
_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 = [] // 用來存儲狀態(tài)響應(yīng)器
/**
* 仿寫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參數(shù),且必須返回一個綁定對象,key會被綁定到page實例的data中
const dataMap = mapStateToData ? mapStateToData(_state) : {}
// mapMethodTopPage接收setState和state參數(shù),且必須返回一個綁定對象,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周期函數(shù),以便對其做改造
const onLoad = pageObject.onLoad
const onUnload = pageObject.onUnload
pageObject.onLoad = function() {
// 存儲page實例和事件響應(yīng)器,兩者保持同步,一個實例對應(yīng)一個響應(yīng)器
if (!~_subjects.indexOf(this)) {
// 首次load需要修改data
this.setData(mapStateToData ? mapStateToData(_state) : {})
_subjects.push(this)
_observers.push(() => {
// mapStateToData生成新的mapData,并使用this.setData更新page狀態(tài)
this.setData(mapStateToData ? mapStateToData(_state) : {})
})
}
// 觸發(fā)原有生命周期函數(shù)
onLoad && onLoad.call(this)
}
pageObject.onUnload = function() {
// 注銷響應(yīng)器
const index = _subjects.indexOf(this)
if (!~index) {
_subjects.splice(index, 1)
_observers.splice(index, 1)
}
// 觸發(fā)原有生命周期函數(shù)
onUnload && onUnload.call(this)
}
return pageObject
}
}
/**
* 所有的state狀態(tài)修改必須通過setState方法,以完成正常的響應(yīng)
*
* @param { Object | Function } state
*/
function setState(state) {
// state 接收需要更新的state對象或者一個接收state的方法,該方法必須返回一個state更新對象
let newState = state
if (isFunction(state)) {
newState = state(_state)
}
// 合并新狀態(tài)
_state = Object.assign(_state, newState)
// 觸發(fā)響應(yīng)器
_observers.forEach(function(observer) {
isFunction(observer) && observer()
})
}
/**
* 創(chuàng)建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
復(fù)制代碼
|
確實夠簡單吧,缺點是不支持模塊化和component,也沒有實現(xiàn)reducer和action,但是這些,我統(tǒng)統(tǒng)都不要 。 考慮現(xiàn)有需求和性能影響,目前沒有支持component和模塊化state——“小”程序方向靠攏(其實是懶)。
“小程序是一種不需要下載安裝即可使用的應(yīng)用,它實現(xiàn)了應(yīng)用‘觸手可及’的夢想,用戶掃一掃或搜一下即可打開應(yīng)用;也體現(xiàn)了‘用完即走’的理念,用戶不用關(guān)心是否安裝太多應(yīng)用的問題。應(yīng)用將無處不在,隨時可用,但又無需安裝卸載。”
“微信之父”張小龍的這段話確定了小程序的開發(fā)基調(diào)。鑒于小程序作為Web端的輕應(yīng)用,本身的特質(zhì)就決定了它不適合實現(xiàn)太過復(fù)雜的功能(為我的懶找到了官方支持)。
|