关于全局状态管理、计算属性、组件跨级传递数据、onLaunch延迟加载页面、switchTab传参等功能的最佳实现方案
在小程序开发中,你是否曾经想要这些功能之一:
globalData变化自动更新视图
storage数据变化能自动更新视图
监听globalData和storage的变化
像vue一样的computed计算属性
像component一样的页面observers字段监听器
组件跨级传递数据(类似于react的context.Provider,或vue的provide/inject)
跨页跨组件通信(像eventBus一样)
还没准备好必要数据,希望onLaunch延迟加载页面
全局路由守卫,拦截没权限的页面
全局对page或组件的视图层的事件进行监听或拦截
希望wx.switchTab能够传递参数
希望input双向绑定能支持字段路径
子页面向上一个页面回传数据通过getCurrentPages找到上一个页面再去调方法比较别扭,希望有更好的方式
像vue中 {{ var | filter }} 一样的过滤器功能
组件支持的pageLifetimes太少
全局开启所有页面的分享功能
等等其它更多。。。。
以上所有的这些功能都包含在我写的这个微信小程序js库中,它的名字叫做[代码]wxbuf[代码],github仓库地址 https://github.com/laivv/wxbuf
如果觉得有用,请动动小手点个star,支持我持续维护和更新更好用的功能,谢谢
只需要这一个库,就能减少很多代码量,让写小程序成为一种享受
下面我们来列举一些功能,假定你已经在你的小程序中引入了[代码]wxbuf[代码],看了之后你一定会觉得,原来还可以这样!
wx.switchTab传参
wx.switchTab原本是不支持传参的,但是现在你可以像wx.navigateTo用法一样来传递参数,就是这么简单
[代码]wx.switchTab({
url: '/pages/mytab/index?id=1&type=2'
})
[代码]
[代码]// /pages/mytab/index.js
Page({
// 首次进入页面在onLoad钩子接收wx.switchTab参数
onLoad({ id, type }) {
},
// wxbuf 提供了onSwitchTab钩子,仅针对tabbar页并且在第二次及之后切入页面进行回调,用于接收参数
onSwitchTab({ id, type }) {
}
})
[代码]
双向数据绑定支持字段路径
原生的input是不支持字段路径的,这里我们先封装一个叫c-input的组件
c-input.wxml:
[代码]<!-- c-input.wxml -->
<input
placeholder="{{placeholder}}"
type="text"
bindinput="handleInput"
value="{{value}}"
/>
[代码]
c-input.js:
[代码]//c-input.js
Component({
properties: {
placeholder: String,
vModel: {
type: String,
value: '',
observer() {
this.init()
}
},
},
// wxbuf提供了一些parentLifetimes生命周期
parentLifetimes: {
// 当父组件调用this.setData时
setData() {
this.init()
}
},
lifetimes: {
attached() {
this.init()
}
},
data: {
value: '',
},
methods: {
init() {
// wxbuf给所有实例提供了$parent属性,其值是父组件实例
if (this.$parent && this.data.vModel) {
const value = this.getValueByKeypath(this.$parent.data, this.data.vModel)
this.setData({ value })
}
},
handleInput({ detail: { value } }) {
if (this.$parent && this.data.vModel) {
this.$parent.setData({ [this.data.vModel]: value })
}
this.setData({ value })
},
getValueByKeypath(data, keypath) {
const keys = keypath.split(/\[|\]|\./).filter(Boolean).map(i => i.replace(/\'|\"/g, ''))
let val = undefined
for (let i = 0; i < keys.length; i++) {
const key = keys[i]
data = val = data[key]
}
return val
}
}
})
[代码]
接下来就可以直接使用了:
wxml:
[代码]<c-input v-model="form.name" placeholder="请输入姓名" />
<c-input v-model="form.phone" placeholder="请输入手机号" />
[代码]
js:
[代码]Page({
data: {
form: {
name: '',
phone: ''
}
}
})
[代码]
跨组件通信
需要跨组件或者跨页通信时,在构造器选项中配置[代码]listeners[代码]字段来指定事件接收函数,通过实例方法[代码]fireEvent[代码]来触发一个事件
例子:
[代码]// pageA.js
Page({
listeners: {
updateOk(event) {
console.log(event)
}
}
})
[代码]
[代码]// pageB.js
Page({
handleBtnTap() {
this.fireEvent('updateOk', 'hello')
}
})
[代码]
传统的发布订阅(on、emit)的做法是在page的[代码]onLoad[代码]或组件的[代码]attached[代码]钩子中调用[代码]on(eventName, handler)[代码]来注册事件监听,在[代码]onUnload[代码]或[代码]detached[代码]中调用[代码]off(eventName, handler)[代码]来移除事件监听,若不移除事件监听会造成内存溢出,非常麻烦,而[代码]wxbuf[代码]提供的配置式方式则不需要关心这些,更适合于小程序的开发场景
页面间数据传递,数据回传
使用实例方法[代码]openPage[代码]来打开新页面,新页面通过实例方法[代码]finish[代码]来关闭自身页面并回传数据给上一个页面,[代码]finish[代码]方法包括了关闭页面([代码]wx.navigateBack[代码])与回传数据两项功能于一身
例子:
[代码]// pageA.js
Page({
async handleTap() {
const acceptVal = await this.openPage({
url: '/pages/detail/index?name=wxbuf',
// 使用params字段传递参数,会追加在url后面,最终生成 /pages/detail/index?name=wxbuf&id=123
params: { id: '123' },
// 使用body字段传递参数
body: { value: 1 },
success(page) {
// 给被打开page设置字段
page.setData({ age: 18 })
}
})
console.log(acceptVal) // '这是回传数据'
}
})
[代码]
[代码]// pages/detail/index.js
Page({
onLoad({ name, id, value }) {
console.log(name) // 'wxbuf'
console.log(id) // '123'
console.log(value) // 1
},
async handleOk() {
console.log(this.data.age) // 18
// 调用finish方法回传数据给父page,并且关闭当前页面
this.finish('这是回传数据')
}
})
[代码]
因此,可以指定[代码]params[代码]字段来代替字符串拼接,并且[代码]params[代码]中的字段还可以是对象,比自己拼接更方便;[代码]body[代码]方式则是通过内存传递参数
获取全局数据
[代码]wxbuf[代码]提供了实例方法[代码]getStore[代码]来获取全局数据 ,可以代替 [代码]getApp().globalData[key][代码]
例子:
app.js:
[代码]// app.js
import wxbuf from 'wxbuf'
App({
globalData: {
count: 1
}
})
[代码]
page.js:
[代码]// page.js
Page({
handleTap() {
const count = this.getStore('count') // 相当于getApp().globalData.count
console.log(count) // 1
}
})
[代码]
修改全局数据
使用实例方法[代码]setStore[代码]来修改全局数据以获得响应式更新
例子:
app.js:
[代码]// app.js
import wxbuf from 'wxbuf'
App({
globalData: {
count: 1
}
})
[代码]
page.js:
[代码]// page.js
Page({
handleTap() {
this.setStore('count', 2)
console.log(getApp().globalData.count) // 2
}
})
[代码]
使用响应式的全局数据
响应式全局数据的优点是可以自动更新视图,在构造器选项中配置[代码]mixinStore[代码]字段来将[代码]store[代码]的值设置到当前实例的data字段中,并且后续一直保持同步,即依赖的全局数据的[代码]key[代码]值一但变化,当前实例引用的值也跟着变化!
如果需要实时保持一致,应当用此方式代替传统的[代码]getStore(key)[代码]或[代码]getApp().globalData.xxx[代码]的取值方式
例子:
app.js:
[代码]// app.js
import wxbuf from 'wxbuf'
App({
globalData: {
count: 1,
appVersion: '1.0'
}
})
[代码]
page.js:
[代码]// page.js
Page({
mixinStore: ['count', 'appVersion'],
onLoad() {
console.log(this.data.count) // 1
console.log(this.data.appVersion) // '1.0'
},
handleTap() {
this.setStore('count', 2)
console.log(this.data.count) // 2
console.log(this.getStore('count')) // 2
console.log(getApp().globalData.count) // 2
}
})
[代码]
使用响应式的storage
响应式[代码]storage[代码]的优点是可以自动更新视图,在构造器选项中配置[代码]mixinStorage[代码]字段来将[代码]storage[代码]的值设置到当前实例的data字段中,并且后续一值保持同步,这和[代码]mixinStore[代码]的机制一样,如果需要实时保持一致,应该放弃使用传统的[代码]wx.getStorage[代码]、[代码]wx.getStorageSync[代码]来取值,而应该用此方式
例子:
[代码]// 假如 storage中 count=1, isLogin=true
// page.js
Page({
mixinStorage: ['count', 'isLogin'],
onLoad() {
console.log(this.data.count) // 1
console.log(this.data.isLogin) // true
},
handleTap() {
this.setStorageSync('count', 2)
console.log(this.data.count) // 2
console.log(this.getStorageSync('count')) // 2
console.log(wx.getStorageSync('count')) // 2
}
})
[代码]
全局注入store和storage到所有页面和组件
app.js:
[代码]// app.js
import wxbuf from 'wxbuf'
App({
// 全局注入store到所有页面、组件的实例上
injectStore: {
// 注入到实例的命名空间(前缀)
namespace: '$store',
// 注入globalData中的哪些字段
keys: ['appVersion', 'appCount'],
},
// 全局注入storage到所有页面、组件
injectStorage: {
namespace: '$storage',
// 注入storage中的哪些字段
keys: ['count'],
},
globalData: {
appVersion: 'v1.0',
appCount: 0
},
onLaunch(){
}
})
[代码]
接下来可以在任意页面或组件中访问这些状态数据,并且是响应式的
wxml:
[代码]<view>{{$store.appCount}}</view>
<view>{{$store.appVersion}}</view>
<view>{{$storage.count}}</view>
[代码]
js:
[代码] Page({
onLoad(){
console.log(this.data.$store)
console.log(this.data.$storage)
}
})
[代码]
对store和storage进行变化监听
你可能不需要响应式的[代码]storage[代码]和[代码]store[代码],但需要监听它们的变化,因此[代码]wxbuf[代码]提供了[代码]onStorageChange[代码]与[代码]onStoreChange[代码]回调钩子,可以使用它们来监听变化
例子:
[代码]
Page({
onStorageChange(kvs, oldKvs) {
console.log(kvs)
console.log(oldKvs)
},
onStoreChange(kvs, oldKvs) {
console.log(kvs)
console.log(oldKvs)
}
})
[代码]
跨级传递数据
微信小程序中并没有提供跨组件层级传递数据的方法,要给嵌套较深的组件传递数据只能一层层定义[代码]properties[代码]来传递,这非常难受,而且使用全局数据来传递数据无法限定视图的组件tree,不过[代码]wxbuf[代码]提供了跨级传递数据的方法,通过[代码]provide[代码]与[代码]inject[代码]来进行跨级数据的传递,如果你了解vue,就知道vue也是通过这种方式来实现跨级传递数据的
例子:
宿主页面:
[代码]// 宿主页面
Page({
provide: {
rootName: '这是page数据',
rootFn() {
console.log('this is rootFn')
}
}
})
[代码]
子组件:
[代码]// 子组件
Component({
inject: ['rootName', 'rootFn'],
lifetimes: {
attached(){
console.log(this.data.rootName) // '这是page数据'
this.rootFn() // 'this is rootFn'
}
}
})
[代码]
从上面的例子可以看出,[代码]inject[代码]注入的数据如果是非函数,则会挂载到[代码]this.data[代码]上,否则挂载到[代码]this[代码]上。
[代码]provide[代码]除了可以写成对象以外,还可以写成函数的形式,该函数必须返回一个对象,这样就成为响应式的[代码]provide[代码],即后代组件注入来自上层组件的数据发生变化后,自身也会更新
例子:
宿主页面:
[代码]// 宿主页面
Page({
data: {
number: 1
},
provide() {
return {
pageNumber: this.data.number
}
}
})
[代码]
子组件:
[代码]// 子组件
Component({
inject: ['pageNumber'],
lifetimes: {
attached(){
console.log(this.data.pageNumber) // 1
}
}
})
[代码]
要注意的是,小程序中的父子组件关系并不是[代码]jsx[代码]中的那种父子标签(slot)嵌套的关系,而是父组件在[代码]json[代码]文件中的[代码]usingComponents[代码]里导入了某个子组件,并且在[代码]wxml[代码]里使用了子组件,这样即形成父子组件关系,而将组件标签放在另一个组件的[代码]slot[代码]中并不形成父子关系
另一个要注意的是,只能在组件的[代码]attached[代码]及其之后的生命周期才能获取到[代码]inject[代码]的数据,因为只有在[代码]attached[代码]阶段才能确定其父组件是谁
计算属性
在构造器选项中声明[代码]computed[代码]字段来实现计算属性,计算属性字段会在[代码]this.data[代码]中生成对应的字段,这和[代码]vue[代码]中的[代码]computed[代码]一样
例子:
wxml:
[代码]<view>{{ ageDesc }}</view>
[代码]
js:
[代码]Page({
data: {
age: 18
}
computed: {
// 将在this.data中生成ageDesc
ageDesc() {
return '你的年龄是' + this.data.age
}
},
handleTap() {
this.setData({ age: 19 })
console.log(this.data.ageDesc) // 你的年龄是19
}
})
[代码]
页面的observers
page支持[代码]observers[代码]了,在构造器选项中声明[代码]observers[代码]来监听[代码]data[代码]对象中某个字段值的变化,和[代码]compontent[代码]中的[代码]observers[代码]功能一样
例子:
[代码]Page({
data: {
count: 1
}
observers: {
count(newVal, oldVal) {
//...
}
},
handleTap() {
this.setData({ count: 2 })
}
})
[代码]
类似vue中的 {{ var | filter }} 过滤器功能实现
小程序中视图层变量绑定并没有过滤器功能,[代码]wxs[代码]的语法又比较受限,要想自己实现[代码]{{ var | filter }}[代码]这样的语法是不行的,但我们通过自定义一个组件能达到相似的过滤器效果,从此告别[代码]wxs[代码]
定义一个全局过滤器组件
在[代码]app.json[代码]中声明一个全局组件,就叫[代码]c-text[代码],接下来实现这个组件:
c-text.wxml:
[代码]{{text}}
[代码]
c-text.js:
[代码]
Component({
externalClasses: ["class"],
options: {
virtualHost: true,
},
properties: {
value: {
optionalTypes: [String, Number, Object, Array, Boolean, null]
},
// 过滤器函数名
filter: String,
// 过滤器参数
params: {
optionalTypes: [String, Number, Object, Array, Boolean, null]
}
},
observers: {
"filter,params,value"() {
this.render()
},
},
lifetimes: {
attached() {
this.render()
},
},
data: {
text: "",
},
methods: {
render() {
const { value, filter, params } = this.data
let text = value
if (filter) {
// 获取过滤器函数
const handler = this.$parent[filter]
const _params = Array.isArray(params) ? params : [params]
if (handler) {
text = handler.call(this.$parent, value, ..._params)
}
}
this.setData({ text: text ?? "" })
},
},
})
[代码]
现在我们就可以使用这个组件来使用过滤器功能了
基出用法
wxml:
[代码]<c-text value="{{timeStamp}}" />
[代码]
[代码] Page({
data: {
timeStamp: 1714123672808
}
})
[代码]
以上是一个普通的显示,和以下写法没什么区别:
[代码]{{timeStamp}}
[代码]
指定过滤器
wxml:
[代码]<c-text value="{{timeStamp}}" filter="formatDate" />
[代码]
[代码]Page({
data: {
timeStamp: 1714123672808
},
formatDate(value) {
return dayjs(value).format('YYYY-MM-DD')
}
})
[代码]
指定过滤器的参数
[代码]params[代码]属性指定传递给过滤器的参数
wxml:
[代码]<c-text value="{{timeStamp}}" filter="formatDate" params="YYYY-MM-DD HH:mm:ss" />
[代码]
[代码]Page({
data: {
timeStamp: 1714123672808
},
formatDate(value, format) {
return dayjs(value).format(format)
}
})
[代码]
[代码]params[代码]属性也可以是一个数组:
wxml:
[代码]<c-text value="{{timeStamp}}" filter="formatDate" params="{{ ['YYYY-MM-DD HH:mm:ss', '-'] }}" />
[代码]
[代码]Page({
data: {
timeStamp: '',
},
formatDate(value, format, defaultValue) {
return value ? dayjs(value).format(format) : defaultValue
}
})
[代码]
全局路由拦截
在[代码]app[代码]文件中通过[代码]beforePageEnter[代码]可以进行全局路由守卫,返回[代码]布尔值[代码]来决定是否拦截某个页面
例子:
app.js:
[代码]import wxbuf from 'wxbuf'
App({
beforePageEnter(options) {
return false
}
})
[代码]
注意的是,[代码]beforePageEnter[代码]无法拦截[代码]Launch[代码]进来的页面,即无法拦截通过正常启动或外链打开小程序等其它非js调用进入的页面,只能拦截通过js调用打开的页面
全局对wxml视图层事件监听或拦截
可以在app.js中对所有的page和组件的视图层事件进行监听和拦截
基础的监听
page.wxml:
[代码] <view bindtap="handleTap" data-name="wxbuf"></view>
[代码]
page.js:
[代码]Page({
handleTap(event) {
//...
}
})
[代码]
app.js:
[代码]// app.js
import wxbuf from 'wxbuf'
App({
onEventDispatch(event, next) {
console.log(event.currentTarget.dataset.name) // 'wxbuf'
// 继续执行原始的事件handler
next(event)
},
})
[代码]
当page中的[代码]view[代码]元素被点击时,会先调用[代码]app[代码]中的[代码]onEventDispatch[代码]钩子,[代码]event[代码]对象为原始的事件[代码]event[代码],可以利用此对象获取被点击元素的信息,常见的应用场景如全局埋点上报功能。
[代码]next[代码]是一个函数,调用它并传入[代码]event[代码]对象让页面上的原始的事件[代码]handler[代码]正常执行,并且必须原封不动的传入[代码]event[代码]对象,否则可能引起原始的事件[代码]handler[代码]不能接收到[代码]event[代码]对象参数
对视图层事件进行拦截
page.wxml:
[代码] <view bindtap="handleTap" data-not-allowed="{{true}}"></view>
[代码]
page.js:
[代码]Page({
handleTap(event) {
wx.showToast({ title: '正常执行' })
}
})
[代码]
app.js:
[代码]// app.js
import wxbuf from 'wxbuf'
App({
onEventDispatch(event, next) {
if(event.currentTarget.dataset.notAllowed){
wx.showToast({ title: '没有权限' })
// 不调用next(event)则不执行原始的事件handler
} else {
// 继续执行原始的事件handler
next(event)
}
},
})
[代码]
当page中的[代码]view[代码]元素被点击时,会弹出[代码]没有权限[代码]的toast提示,原始的事件[代码]handler[代码]被拦截无法执行
减少event.currentTarget.dataset解构层数
日常开发中经常会在某个元素上自定义[代码]data-[代码]的数据,并在事件处理函数中通过[代码]event.currentTarget.dataset.xxx[代码]来获取这些数据,每次都很繁琐, 利用[代码]onEventDispatch[代码]钩子可以减少取[代码]dataset[代码]的层数
page.wxml:
[代码] <view bindtap="handleTap" data-name="wxbuf" data-id="123"></view>
[代码]
page.js:
[代码]Page({
handleTap(e, { id, name }) {
console.log(id) // '123'
console.log(name) // 'wxbuf'
}
})
[代码]
app.js:
[代码]// app.js
import wxbuf from 'wxbuf'
App({
onEventDispatch(event, next) {
// 将第二个参数传递给原始的事件handler
next(event, event.currentTarget.dataset)
},
})
[代码]
全局顶层变量定义
可以在app中使用[代码]wxbuf.global.extend[代码]定义一些顶层全局变量,在其它文件中无需import即可使用
例子:
app.js:
[代码]// app.js
import wxbuf from 'wxbuf'
wxbuf.global.extend('getAppVersion',function(){
return 'v1.0.0'
})
App({
globalData: {},
onLaunch() {}
// ...
})
[代码]
pageA.js:
[代码]// pageA.js
Page({
onLoad() {
console.log(getAppVersion()) // 'v1.0.0'
}
})
[代码]
全局给page或componet的实例扩展方法
使用[代码]wxbuf.page.extend[代码] 或 [代码]wxbuf.component.extend[代码] 分别给[代码]page[代码]和[代码]component[代码]实例挂载公共方法
例子:
app.js:
[代码]// app.js
import wxbuf from 'wxbuf'
wxbuf.page.extend({
getData(key){
return this.data[key]
}
})
App({
globalData: {},
onLaunch() {}
// ...
})
[代码]
pageA.js:
[代码]// pageA.js
Page({
data: {
name: 'wxbuf is a library'
},
onLoad() {
const name = this.getData('name')
console.log(name) // 'wxbuf is a library'
}
})
[代码]
组件更多的pageLifetimes支持
现在增加了一些pageLifeTimes,组件逻辑再也不用和页面耦合起来了
pageLifeTimes.pullDownRefresh 所在页面onPullDownRefresh
pageLifeTimes.reachBottom 所在页面onReachBottom
pageLifeTimes.pageScroll 所在页面onPageScroll
pageLifeTimes.switchTab 所在tabbar页面发生onSwitchTab时调用
github仓库
还有更多功能是文章中未列举的,具体请查看 github仓库地址 https://github.com/laivv/wxbuf
作者做了十年前端开发,关注我,获取更多实用的小程序开发技巧