一个基于原生小程序的mini全局状态管理库,跨页面/组件渲染。
- 全局状态state支持所有Page和Component,状态完全同步渲染,并支持更新,更新时使用独有diff能力,性能更强。
- 周期监听pageLisener能监听所有页面的onLoad、onShow等周期事件,方便埋点、统计等行为。
- 全局事件methods,一处声明,所有wxml直接可用的函数。
- 适合原生小程序,即使后期引入,也只需增加几行代码。
[2020.3.31] A
: 新增debug字段,用于开启/关闭setState时的console。
[2019.11.27] F
: 优化diff能力。
[2019.9.6] F
: 修复删除state中的数组,删除的项会为null的问题。
[2019.6.26] A
: 新增store.prototype.getState,用于读取store.$state的拷贝,防止对原状态进行误操作。
[2019.6.25] F
: 修复setState为引用类型数据时视图可能不会更新。
完整日志点击此处查看
在开始前,你可以clone或下载本项目,用微信开发工具打开demo目录来查看效果。
目前有两种引入方式:
首先你需要npm init 在项目目录下生成 package.json后,再进行安装。
npm init
npm install wxministore -S
然后在微信小程序右上角详情中勾选 使用npm模块
。
接着选择左上角 工具-构建 npm。
这样你就可以在项目中导入了。
//app.js中
import Store from 'wxministore';
//或者 const Store = require('wxministore');
App({
})
如果不太熟悉npm没关系,你可以将本项目中lib/store.js复制到你的项目中,并在app.js第一行
引入:
//app.js中
import Store from './util/store.js';
//或者 const Store = require('./util/store.js');
App({
})
Store为构造函数,所以需要通过new 关键字实例化,参数为object类型,下面我们初始化一个state。
let store = new Store({
state: {
msg: '这是一个全局状态',
user: {
name: "李四"
}
}
})
console.log(store.getState().msg); //这是一个全局状态 1.2.6+
console.log(store.$state.msg); //这是一个全局状态 (不推荐)
App({
})
初始化完成,我们如需在js中获取状态,可使用 store.getState()
获取全局状态,1.2.6+
版本强烈推荐此方式。
store.$state 也可获取,但不建议使用。
这么做是为了在其他页面中使用store。
App({
onLaunch: function () {
},
store: store
})
在所有wxml中,可使用$state.x。 其中$state为全局状态的容器,里面包含了所有的全局状态。
<view>{{$state.user.name}}:{{$state.msg}}</view>
显示为 李四:这是一个全局状态。
如果在template文件中使用,需在属性data中引用$state
<!-- 这是一个template -->
<template name="t1">
<view>{{$state.msg}}</view>
</template>
<!-- 这是引用位置 -->
<template is="t1" data="{{$state,arg1,arg2}}" />
<!-- 相当于<template is="t1" data="{{$state:$state,arg1:arg1,arg2:arg2}}" /> -->
在版本1.2.1+建议使用App.Page和 App.Component创建页面和组件,当然也不是必须。详情查看nonWritable
// 没问题
Page({
//...
})
// 更好
App.Page({
//...
})
如果使用时,页面空白,说明你没有在App创建前new Store。
使用app.store.setState进行更新状态。如:
const app = getApp()
App.Page({
data: {
},
onLoad: function () {
//所有wxml中的$state.msg会同步更新
app.store.setState({
msg: "我被修改了,呜呜..."
});
}
});
// 错误的示范 视图不会更新
let { user } = app.store.$state;
user.name = '张三';
app.store.setState({
user
});
//正确的示范
let { user } = app.store.getState();
user.name = '张三';
app.store.setState({
user
});
获取全局状态需使用app.store.getState()。
在有的场景,我希望每个页面在onLoad时执行一个方法(如统计页面,监听等)。原本做法是一个一个的复制粘贴,很麻烦。
现在我们可以把某个周期,写入pageLisener中,Store会自动在相应周期优先执行pageLisnner然后再执行原页面周期内事件
。
现在以监听onLoad为例, 在Store中新增一个pageLisener对象,将需要监听的周期写入:
// store中
let store = new Store({
//状态
state: {
//...
},
//方法
methods: {
//...
},
//页面监听
pageLisener: {
onLoad(options){
console.log('我在' + this.route, '参数为', options);
}
}
})
就这样所有页面的onLoad,将会优先执行此监听。接下来看页面内代码:
// index/index.js 页面
App.Page({
onLoad(){
console.log(2)
}
})
执行结果为:
// 我在index/index 参数为 {...}
// 2
总结:
- 先执行pageLisener监听,后执行原本页面中周期。
- 还支持其他周期事件 ['onLoad', 'onShow', 'onReady', 'onHide', 'onUnload', 'onPullDownRefresh', 'onReachBottom', 'onShareAppMessage', 'onPageScroll', 'onTabItemTap']
新增methods,全局可使用。 适用于各个wxml中的交互事件(bindtap等), 你可以封装一些常用的交互事件,如 行为埋点,类型跳转等。
在原有状态基础上,新增一个methods对象,写入你的全局方法:
let store = new Store({
//状态
state: {
msg: '这是一个全局状态'
},
//方法
methods: {
goAnyWhere(e){
wx.navigateTo({
url: e.currentTarget.dataset.url
})
},
sayHello(){
console.log('hello')
}
}
})
这里创建了一个全局封装的跳转 goAnyWhere。
在wxml中,直接使用方法名
调用:
<view bindtap="goAnyWhere" data-url="/index/index">
首页
</view>
在js中,直接使用 this.方法名
来调用:
App.Page({
onLoad(){
this.sayHello();
}
})
在非页面的js中,我们不建议使用Store中的全局方法。但你可使用getCurrentPage().pop().sayHello() 来调用。
- 尽量封装复用率高的全局方法
- 非交互型事件(即非bindxx)的公用方法,建议不写入Store中。写入App中更好。
在项目的组件和页面越来越多且复用率越来越高时,全局$state的利用率就很低,这时候就出现了一种情况,页面中的组件和页面达到百千量级,每个内部都有一个$state,而用到它的可能就只有1个或几个。就会引起各种性能问题。比如更新$state十分缓慢,且低效。
这时候你需要将$state调整为部分组件和页面可用,而不是所有。
let store = new Store({
//。
state: {
msg: '这是一个全局状态'
},
openPart: true
})
openPart 字段表示是否开启局部模式,默认值为false。当我们想规定只有某些页面和组件使用$state时,就需开启此模式,设置为true。
在需要使用$state的组件中,加入useStore: true
,表示当前页面或组件可用$state。
// a.js
App.Page({
useStore: true,
onLoad(){
console.log(this.data.$state) // { msg: '这是一个全局状态' }
console.log(getApp().store.getState()) // { msg: '这是一个全局状态' }
}
})
// b.js
App.Page({
onLoad(){
console.log(this.data.$state) // undefined
console.log(getApp().store.getState()) // { msg: '这是一个全局状态' }
}
})
a页面设置了Store可用,所以可以通过this.data.$state获取。 b页面没有设置,所以为undefined,但两个页面均可通过store.getState()读取全局状态。
<--! a页面有效 -->
<view>{{$state.msg}}</view>
<--! b页面无效 -->
<view>{{$state.msg}}</view>
- openPart一旦开启,所有没有设置useStore的页面和组件将不能在wxml中使用$state。
- 组件或页面.js中,我们建议使用getApp().store.getState()去获取全局状态,因为他没有限制。
- 仅在wxml中需要用到$state的页面和组件中开启useStore。
你可以clone或下载本项目,用微信开发工具打开demo目录来查看具体用法。
useProp 用于控制当前页面/组件,使用哪些状态,不传则所有状态均可在当前页面中使用。
观察以下代码及注释:
// App.js
let store = new Store({
state: {
s1: 's1状态',
s2: 's2状态'
}
})
// A页面中
App.Page({
useProp: ['s1'], //指定使用s1
onLoad(){
console.log(this.data.$state) // { s1: 's1状态' }
console.log(getApp().store.getState()) // { s1: 's1状态', s2: 's2状态' }
}
})
// B页面中
App.Page({
useProp: ['s2'], //指定使用s2
onLoad(){
console.log(this.data.$state) // { s2: 's2状态' }
console.log(getApp().store.getState()) // { s1: 's1状态', s2: 's2状态' }
}
})
// C页面中
App.Page({
onLoad(){
console.log(this.data.$state) // { s1: 's1状态', s2: 's2状态' }
console.log(getApp().store.getState()) // { s1: 's1状态', s2: 's2状态' }
}
})
useProp是控制当前组件/页面使用哪些状态,而useStore是控制哪些组件/页面可使用state这个功能,两者可以同时作用。如:
// App.js中
let store = new Store({
state: {
s1: 's1状态',
s2: 's2状态'
},
openPart: true
})
// A页面中
App.Page({
useStore: true,
useProp: ['s1'], //指定使用s1
onLoad(){
console.log(this.data.$state) // { s1: 's1状态' }
console.log(getApp().store.getState()) // { s1: 's1状态', s2: 's2状态' }
}
})
// B页面中
App.Page({
useProp: ['s1'], //指定使用s1 但没设置useStore,所以无效
onLoad(){
console.log(this.data.$state) // undefined
console.log(getApp().store.getState()) // { s1: 's1状态', s2: 's2状态' }
}
})
实例化Store时,提供debug字段,用于开启/关闭框架内部console日志。 默认值为true,即开启状态。如不需要,则设置false即可。
new Store({
debug: false // 关闭内部日志的输出。
})
收到开发者的反馈,在小程序中使用插件时,会报错提示:
// [non-writable] modification of global variable "Page" is not allowed when using plugins at app.json.
// 在app.json中使用插件时,不允许修改全局变量 Page
原因是store源码重写了Page、Component方法。
在你的store配置中,加入 nonWritable: true
。
let store = new Store({
nonWritable: true
})
将你所有页面与组件创建方法改为App.Page(...) 和 App.Component(...)
。
//页面.js
App.Page({
data: {
},
onLoad: function () {
}
});
//组件.js
App.Component({
data: {
}
});
以上就解决了此问题。
这里列举了所有涉及到Store的属性与方法。
该函数使用new关键字返回一个Store类型的实例。
参数options,为配置参数,
options.state 为初始全局状态。
options.methods 为全局方法。
options.openPart 状态局部模式。
options.pageLisener 周期监听。
options.nonWritable 是否重写Page,Componenet。
用于修改全局状态,用法与微信小程序的 Page.prototype.setData完全一致。 提示:页面中应避免使用this.setData({$state: ...})去操作当前页面下的$state。如有相关需求,请使用页面其他状态存储。
该对象为实例.$state, 返回的是全局状态,应避免直接操作修改它。
该对象为所有页面或组件的实例。
该api返回的是全局状态的拷贝。
考虑到后期的app.js内store不直观,可以把整套store单独写入一个js中,通过require引入。如:
// mystore.js中
const Store = require('../util/store.js');
module.exports = new Store({
state: {},
methods: {}
})
//---------------------------
// app.js中
let store = require('store/mystore.js')
App({
store
})
MiniStore非常适合原生小程序。可以随时引入,不影响原有的业务,拓展性强。 欢迎star、欢迎提issue甚至pr...
MIT © Leisure