评论

关于全局状态管理、计算属性、组件跨级传递数据、onLaunch延迟加载页面、switchTab传参等功能的最佳实现方案

介绍小程序中全局状态管理,组件跨级传递数据、storage数据状态管理、跨页跨组件通信、input双向绑定字段路径、computed计算属性、page observers等功能的最佳实现方案

在小程序开发中,你是否曾经想要这些功能之一:

  • 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库中,它的名字叫做wxbufgithub仓库地址 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)来注册事件监听,在onUnloaddetached中调用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.getStoragewx.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进行变化监听

你可能不需要响应式的storagestore,但需要监听它们的变化,因此wxbuf提供了onStorageChangeonStoreChange回调钩子,可以使用它们来监听变化

例子:


Page({
  onStorageChange(kvs, oldKvs) {
    console.log(kvs) 
    console.log(oldKvs) 
  },
  onStoreChange(kvs, oldKvs) {
    console.log(kvs) 
    console.log(oldKvs) 
  }
})

跨级传递数据

微信小程序中并没有提供跨组件层级传递数据的方法,要给嵌套较深的组件传递数据只能一层层定义properties来传递,这非常难受,而且使用全局数据来传递数据无法限定视图的组件tree,不过wxbuf提供了跨级传递数据的方法,通过provideinject来进行跨级数据的传递,如果你了解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.extendwxbuf.component.extend 分别给pagecomponent实例挂载公共方法

例子:
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

作者做了十年前端开发,关注我,获取更多实用的小程序开发技巧

最后一次编辑于  04-30  
点赞 1
收藏
评论

3 个评论

登录 后发表内容