微信原生小程序中引入redux,支持async, await
微信原生小程序官方提供了全局变量globalData,进行页面组件之间的通信,这在小型应用中足以,然而在复杂的应用中,管理全局状态变得无力。
redux在react项目中使用广泛,我们希望引入到微信小程序中。
实现思路
要实现引入redux到微信小程序中,我们需要完成三个函数
- setStore(store) 存储redux store,方便小程序调用
- connect(mapStateToPage, mapDispatchToPage)(Page) 用于链接state到微信小程序页面组件中
- connect(mapStateToComponent, mapDispatchToComponent)(Component) 用于链接到微信自定义组件中
实现setStore方法
setStore的实现相当的简单,我们只需要保存传递过来的store对象即可,详细定义如下:
let _store
function setStore(store) {
// 这里仅仅是验证传递的store是否符合redux store规则
assert(
isReduxStore(store),
'the store you provider not a standrand redux store',
)
_store = store
}
实现connect方法
要实现conenct方法,我们需要利用微信小程序页面组件的生命周期函数onLoad以及onUnLoad,思路如下:
- 在微信小程序页面onLoad时,通过store.getState()获取全局state状态
- 传递状态值到用户注册的mapStateToPage函数中,塞选出需要链接到页面的状态值
- 调用微信小程序this.setData合并上一步获取的状态值到页面data属性中
- 利用store.subscribe监听store中状态改变,当数据改变时利用mapStateToPage获取需要链接的数据,浅对比上一次值,如果改变则重新合并到data中,未改变则放弃合并
- 在微信小程序onShow时,判断onLoad是否执行完成,且,监听器不存在,重新建立监听器
- 调用mapDispatchToPage函数,传递store.dispatch,合并到页面组件自定中,即可通过this调用
- 当微信小程序页面onHide时,取消store的监听器
- 当微信小程序页面onUnload时,取消store的监听事件
首先我们看一下整体函数定义
function connect(mapStateToPage, mapDispatchToPage) {
const shouldMapStateToPage = isFunction(mapStateToPage)
const shouldMapDispatchToPage = isFunction(mapDispatchToPage)
return function(pageConfig) {
// defaultMapToPage是一个空函数
const mapState = shouldMapStateToPage ? mapStateToPage : defaultMapToPage
const mapDispatch = shouldMapDispatchToPage
? mapDispatchToPage
: defaultMapToPage
let unsubscribe = null
let ready = false
function subscribe() {}
function onLoad() {}
function onShow() {}
function onUnload() {}
function onHide() {}
// 这里返回一个重新组装后的pageCofing对象,用于注册到Page()
return { ...pageConfig, ...mapDispatch(_store.dispatch), onLoad, onUnLoad }
}
}
subscribe函数定义
function subscribe(options) {
if (!isFunction(unsubscribe)) return null
// 获取该页面需要链接的state
const mappedState = mapState(_store.getState(), options)
// 进行浅比较,一致则不改变
if (isShallowInclude(this.data, mappedState)) return null
// 调用微信方法更新页面data
this.setData(mappedState)
}
onLoad函数定义
function onLoad(options) {
assert(_store !== null, 'we should call setStore before the connect')
if (shouldMapStateToPage) {
// 建立store的状态改变监听事件
unsubscribe = _store.subscribe(subscribe.bind(this, options))
// 初始化绑定页面的数据
subscribe.call(this, options)
}
// 检测用户是否定义了onLoad函数
// 如果用户定义了该函数,触发该函数
if (isFunction(pageConfig.onLoad)) {
config.onLoad.call(this, options)
}
ready = true
}
onShow函数定义
function onShow() {
// 这里之所以需要检测ready及unsubscribe
// 主要是防止重复监听改变,重复绑定
if (ready && !isFunction(unsubscribe) && shouldMapStateToPage) {
unsubscribe = _store.subscribe(subscribe.bind(this))
subscribe.call(this)
}
if (isFunction(config.onShow)) {
config.onShow.call(this)
}
}
onHide函数定义
function onHide() {
if (isFunction(config.onHide)) {
config.onHide.call(this)
}
if (isFunction(unsubscribe)) {
unsubscribe()
unsubscribe = null
}
}
onUnload函数定义
function onUnload() {
// 检测用户是否定义了onUnload函数
// 如果定义,触发该函数
if (isFunction(pageConfig.onUnload)) {
pageConfig.onUnload.call()
}
// 取消store的变化监听
if (isFunction(unsubscribe)) {
unsubscribe()
unsubscribe = null
}
}
这样一个完整的connect方法定义已经完成,查看完整代码请移步weapp-redux
实现connectComponent
有了connect的实现,要实现connenctComponent,原理就是一致的,利用微信小程序自定义组件生命周期函数,attached, detached, pageLifetimes.show, pageLifetimes.hide实现
注意:在低版本微信中不支持pageLifetimes,因此会造成页面隐藏时,监听器还在运行的浪费
完整代码这里不再列出了,请移步createConnectComponent
引入redux到微信小程序中
上面我们仅仅是实现了链接redux到page,component中,还记得setStore函数么?它的参数需要传递一个redux store,我们可以选择使用redux原生创建store,移步redux 官方文档,也可以利用一些基于redux封装的redux框架库,简化redux使用
这里我们选择使用第三方redux框架zoro,快速搭建我们的redux应用
初始化我们的项目
第一步,通过微信开发者工具,建立一个快速启动模版
第二步,我们拷贝weapp-redux.js,zoro.js,到项目目录utils中
第三步,引入zoro和redux到app中,打开app.js
import zoro from './utils/zoro'
import { setStore } from './utils/weapp-redux'
const app = zoro()
const store = app.start(false)
setStore(store)
App({
onLaunch() {
app.setup()
...
}
})
至此项目的初始化完成了,打开调试工具预览(需开启es6转换)
一个简单的hello world项目,在debug工具中,我么看到如下报错
Store does not have a valid reducer. Make sure the argument passed to combineReducers is an object whose values are reducers.
这是因为我们并没有建立任何的reducer,我们查看zoro使用文档,修改微信hello world模版,通过redux实现
在项目根目录下建立models文件夹
建立models/user.js
export default {
namespace: 'user',
state: {},
}
引入model
import zoro from './utils/zoro'
import { setStore } from './utils/weapp-redux'
// 新增引入user model
import user from './models/user'
const app = zoro()
// 新增引入user model
app.model(user)
const store = app.start(false)
setStore(store)
App({
onLaunch() {
app.setup()
...
}
})
接下来我们分析微信空白模版功能,首先改造登录
// models/user.js文件中
// 由于该文件中需要使用到async, await,因此需要引入regeneratorRuntime
import { regeneratorRuntime } from '../utils/zoro'
import { promise } from '../utils/util'
// Promise化微信登录接口
const wxLogin = promise(wx.login)
export default {
namespace: 'user',
state: {},
effects: {
async login() {
// 阻塞调用微信登录
const { code } = await wxLogin()
// 发送code到后台服务器中获取openId, sessionKey, unionId
}
}
}
// app.js文件中
// 新增导出dispatcher,用于触发redux action
import zoro, { dispatcher } from './utils/zoro'
App({
onLaunch() {
app.setup()
// 触发微信登录
dispatcher.user.login()
/* 删除原有代码逻辑
wx.login({
success: res => {
// 发送 res.code 到后台换取 openId, sessionKey, unionId
}
})
*/
// 省略其他代码
},
})
接下来我们改造微信获取用户信息
// models/user.js文件中
import { regeneratorRuntime } from '../utils/zoro'
import { promise } from '../utils/util'
// 首先对于需要使用的接口进行promise化
const wxGetSetting = promise(wx.getSetting)
const wxGetUserInfo = promise(wx.getUserInfo)
export default {
namespace: 'user',
state: {
userInfo: {}, // 给用户信息一个默认值
canGetUserInfo: false, // 标记用户是否已经授权
},
effects: {
async getUserInfo() {
const { authSetting } = await wxGetSetting()
if (authSetting['scope.userInfo']) {
const { userInfo } = await wxGetUserInfo()
// 获取到了用户信息,我们需要存储在redux的state中
// 请看下面reducers.update的定义
put({ type: 'update', payload: { userInfo, canGetUserInfo: true } })
} else {
put({ type: 'update', payload: { canGetUserInfo: false } })
}
},
},
reducers: {
update({ payload }, state) {
return { ...state, ...payload }
},
},
}
// app.js文件中
import zoro, { dispatcher } from './utils/zoro'
App({
onLaunch() {
app.setup()
// 触发微信登录
dispatcher.user.login()
// 触发获取用户信息
dispatcher.user.getUserInfo()
/* 删除原有代码逻辑
wx.getSetting({
success: res => {
if (res.authSetting['scope.userInfo']) {
// 已经授权,可以直接调用 getUserInfo 获取头像昵称,不会弹框
wx.getUserInfo({
success: res => {
// 可以将 res 发送给后台解码出 unionId
this.globalData.userInfo = res.userInfo
// 由于 getUserInfo 是网络请求,可能会在 Page.onLoad 之后才返回
// 所以此处加入 callback 以防止这种情况
if (this.userInfoReadyCallback) {
this.userInfoReadyCallback(res)
}
}
})
}
}
})
*/
// 省略其他代码
},
/* 删除原有逻辑
globalData: {
userInfo: null
}
*/
})
成功拿到了用户信息,我们还需要改造index页面,以保证用户数据可以链接到页面中
// pages/index/index.js
import { dispatcher } from '../../utils/zoro'
import { connect } from '../../utils/weapp-redux'
// 链接state到页面,返回值用于注册到微信Page中
const config = connect(state => ({
userInfo: state.user.userInfo,
hasUserInfo: state.user.hasUserInfo,
}))({
data: {
motto: 'Hello World',
canIUse: wx.canIUse('button.open-type.getUserInfo'),
},
bindViewTap: function() {
wx.navigateTo({
url: '../logs/logs'
})
},
getUserInfo: function(e) {
// 更新用户数据
dispatcher.user.update({ userInfo: e.detail.userInfo, hasUserInfo: true })
}
})
Page(config)
整个页面是不是变得非常简洁,conenctComponent使用与conenct一致这里不在讨论
如果你需要在页面或者组件中使用async, await,请在文件头部引入如下代码
// 路径请根据实际情况而定
import { regeneratorRuntime } from '../utils/zoro'
该项目代码托管于github,weapp-combine-redux
最后,混口饭吃,支付宝扫码领个红包吧