- 微信小程序教你使用eventbus一步一步构建全局跨页面通信系统
微信小程序提供了页面通信通信解决方案EventChannel,但实际使用中需要在wx.navigateTo跳转页面时使用,且需要在跳转前声明监听的事件,如下图 [图片] 这是一种页面间的通信,但是局限性过于明显,仅可以在跳转间的页面之间建立通信,A跳转B可以建立通信关系,A不跳转G就不可以建立通信关系,在实际开发中如果某个注册页面的信息想做回显,我们可以使用重新请求、放到storage中、glabalData、eventbus全局通信等,但是肯定不能用navigateTo建立eventbus信道进行传值,从交互层面是完全不可接收的。 这时我们就需要一个全局的eventbus来进行通信了,下面讲解一下微信小程序如何搭建全局eventbus进行通信。 注:eventbus传值,如果没有对引用类型进行深拷贝,那么会将引用传过去导致错误。 首先,我们需要开发页面扩展功能,我们知道每一个页面都是一个Page({}),传入的是一个对象,对象中包含双向绑定的数据、生命周期函数、自定义函数,这里我们需要增加页面的生命周期函数。 原理可以参考这篇文章: 小程序页面(Page)扩展 其中我们需要这5个文件: [图片] 其中config.js是小程序全局构造函数App、Page扩展规则配置项,eventBus是eventbus的实现,index是将eventbus扩展的页面上,然后再app.js中引入index文件即可,pageExtends即页面扩展方法,public是初始化eventbus的方法。 使用方法: A页面声明: [图片] B页面触发: [图片] 以下为源码 config.js源码: [代码]/* * @Author: 徐强国 * @Date: 2022-08-15 15:43:32 * @Description: Page公共方法扩展 */ const EventBus = require('./eventBus') let eventBus // 初始化页面的eventbus,事件用法参照dom2事件 export const initEventBus = (pageObj) => { // let eventBus = new EventBus(); if (!eventBus) { eventBus = new EventBus(); } else { } pageObj['$on'] = function () { let argu = Array.prototype.slice.call(arguments); eventBus.on(...argu) } pageObj['$off'] = function () { let argu = Array.prototype.slice.call(arguments); eventBus.off(...argu) } pageObj['$emit'] = function () { let argu = Array.prototype.slice.call(arguments); eventBus.emit(...argu) } // 创建页面声明的自定义事件 let events = pageObj['events']; if (Array.isArray(events)) { events.forEach((event, index) => { if (typeof event === 'string') { eventBus.createEvent(event) } else { console.error(`==请传入String类型的事件名称== index:${index}`, events) } }) } else if (typeof events !== 'undefined') { console.error('==events字段已被占用,用于声明当前页面需要创建的自定义事件,值为字符串数组== events:', events) } } module.exports = { onLoad(options) { this.$initPage() }, $initPage() { if (!this.$on) { initEventBus(this) } }, } [代码] eventBus.js源码 [代码]/** * @authors 徐强国 * @date 2022-8-8 * eventBus,订阅/发布 * */ // 是否是字符串 function isString(str) { return typeof str === 'string' } // 是否是函数 function isFunction(fn) { return typeof fn === 'function' } // 消息中心 class MessageHub { constructor() { this.pubDictionary = {} } // 创建发布者 createEvent(name, isGlobal) { if (!isString(name)) { console.error(`==请传入创建事件的名称 name==`) return false } let _pub = this.pubDictionary[name] if (_pub) { if (!isGlobal) { console.warn(`==${name} 事件已存在==`) } return _pub } else { let pub = new Publish(name, this) this.pubDictionary[name] = pub return pub } } removeEvent(name) { if (!isString(name)) { console.error(`==请传入删除事件的名称 name==`) return false } delete this.pubDictionary[name] } on(name, callback, mark) { if (!isString(name)) { console.error(`==请传入监听事件的名称 name==`) return false } console.log('ononoonon这里的区文体', this.pubDictionary, callback, mark) if (!isFunction(callback)) { console.error(`==请传入监听事件的回调函数 callback==`) return false } let pub = this.pubDictionary[name] if (pub) { let watcher = new Watcher(pub.dep, callback, mark) pub.dep.addSub(watcher) } else { console.error(`==尚未创建 ${name} 事件==`) } } off(name, callback) { if (!isString(name)) { console.error(`==请传入监听事件的名称 name==`) return false } if (!isFunction(callback)) { console.error(`==请传入监听事件的回调函数 callback==`) return false } let pub = this.pubDictionary[name] pub.dep.removeSub(callback) } emit(name, val) { if (!isString(name)) { console.error(`==请传入触发事件的名称 name==`) return false } console.log('这里的区文体emit', this.pubDictionary) let pub = this.pubDictionary[name] if (pub) { pub.refresh(val) } else { console.warn(`==${name} 事件不存在==`) } } clearEvent() { this.pubDictionary = {} } } // 发布者 class Publish { constructor(name, messageHub) { this.name = name this.messageHub = messageHub this.dep = new Dep(this) } refresh(val) { this.dep.notify(val) } } // 订阅者 class Watcher { constructor(dep, run, mark) { this.dep = dep this.run = run this.mark = mark || '' } update() { let val = this.dep.value let run = this.run run(val) } } // 依赖收集 class Dep { constructor(pub) { this.pub = pub this.subs = [] } addSub(sub) { this.subs.push(sub) } removeSub(run) { let sub = this.subs.filter(item => item.run === run)[0] remove(this.subs, sub) } notify(val) { this.value = val let subs = this.subs.slice() for (let i = 0, l = subs.length; i < l; i++) { subs[i].update() } } } function remove(arr, el) { for (let i = 0; i < arr.length; i++) { if (arr[i] === el) { arr.splice(i, 1) return true } } return false } module.exports = MessageHub [代码] pageExtends.js源码 [代码]/** * @authors 徐强国 * @date 2022-08-15 * 小程序全局构造函数App、Page扩展 */ const { appLiftTimes, pageLiftTimes } = require('./config'); // 判断是否是App的生命周期及原始方法 function isAppLiftTimes (name, fn) { if (typeof fn === 'function') { return appLiftTimes.indexOf(name) > -1 } return false } // 判断是否是Page的生命周期及原始方法 function isPageLiftTimes(name, fn) { if (typeof fn === 'function') { return pageLiftTimes.indexOf(name) > -1 } return false } // 函数混入 function rewriteFn(context, name, fn) { if (context[name]) { let originFn = context[name]; context[name] = function (e) { let argu = Array.prototype.slice.call(arguments); fn.apply(this, argu); return originFn.apply(this, argu) } } else { context[name] = fn } } // 是否是对象 function isObject(obj) { return obj !== null && typeof obj === 'object' } // 重写App const originApp = App; const appExtendsList = []; App = function (obj) { // app拓展方法 appExtendsList.forEach(item => { rewriteFn(obj, item.key, item.value) }) return originApp(obj) } const appExtends = function (key, value) { if (isAppLiftTimes(key, value)) { appExtendsList.push({ key, value }) } else { console.error('==*App暂不支持非生命周期的扩展*==', key) } } // 重写Page const originPage = Page; const pageExtendsList = []; Page = function (obj) { let illegalKeys = Object.keys(obj).filter(key => /^\$+/.test(key)); if (illegalKeys.length) { // throw new Error(`Page中自定义属性禁止以 \$ 开头, ${illegalKeys.join(', ')}`) console.error(`Page中自定义属性禁止以 \$ 开头, ${illegalKeys.join(', ')}`) } // 页面拓展方法 pageExtendsList.forEach(item => { // 非生命周期属性只能拓展一次 if (isPageLiftTimes(item.key, item.value)) { rewriteFn(obj, item.key, item.value) } else { if (typeof obj[item.key] === 'undefined') { obj[item.key] = item.value; } else { console.error(`Page中已拓展 ${item.key} 属性`, obj[item.key]) } } }) return originPage(obj) } const pageExtends = function (key, value) { // Page拓展属性,非生命周期的属性必须以 $ 开头 if (/^\$+/.test(key) || isPageLiftTimes(key, value)) { if (isPageLiftTimes(key, value) || !pageExtendsList.filter(item => item.key === key).length) { pageExtendsList.push({ key, value }) } else { console.warn(`==*Page中已扩展 ${key} 属性*==`) } } else { console.warn(`==*Page中拓展属性必须以 \$ 开头*==`, `\n key: ${key}`) } } const AppPlus = { appExtends: function (mixinObj, value) { if (typeof mixinObj === 'string') { appExtends(mixinObj, value) } else if (isObject(mixinObj)) { Object.keys(mixinObj).forEach(key => { appExtends(key, mixinObj[key]) }) } else { console.warn('==*请传入 对象 或者 key, value*==') } }, pageExtends: function (mixinObj, value) { if (typeof mixinObj === 'string') { pageExtends(mixinObj, value) } else if (isObject(mixinObj)) { Object.keys(mixinObj).forEach(key => { pageExtends(key, mixinObj[key]) }) } else { console.warn('==*请传入 对象 或者 key, value*==') } } } module.exports = AppPlus [代码] public.js源码 [代码]/* * @Author: 徐强国 * @Date: 2022-08-15 15:43:32 * @Description: Page公共方法扩展 */ const EventBus = require('./eventBus') let eventBus // 初始化页面的eventbus,事件用法参照dom2事件 export const initEventBus = (pageObj) => { // let eventBus = new EventBus(); if (!eventBus) { eventBus = new EventBus(); } else { } pageObj['$on'] = function () { let argu = Array.prototype.slice.call(arguments); eventBus.on(...argu) } pageObj['$off'] = function () { let argu = Array.prototype.slice.call(arguments); eventBus.off(...argu) } pageObj['$emit'] = function () { let argu = Array.prototype.slice.call(arguments); eventBus.emit(...argu) } // 创建页面声明的自定义事件 let events = pageObj['events']; if (Array.isArray(events)) { events.forEach((event, index) => { if (typeof event === 'string') { eventBus.createEvent(event) } else { console.error(`==请传入String类型的事件名称== index:${index}`, events) } }) } else if (typeof events !== 'undefined') { console.error('==events字段已被占用,用于声明当前页面需要创建的自定义事件,值为字符串数组== events:', events) } } module.exports = { onLoad(options) { this.$initPage() }, $initPage() { if (!this.$on) { initEventBus(this) } }, } [代码] index.js源码 [代码]/* * @Author: 徐强国 * @Date: 2022-08-15 15:18:12 * @Description: 小程序提供扩展App、Page扩展入口 * * * AppPlus提供拓展App及Page的接口,校验自定义属性命名 * @param appExtends * @parm pageExtends * * 传入一个对象,此对象的属性及方法将混入App或者Page实例中 * 生命周期函数将与自定义的声明周期混合,且先执行, * 其他属性只能以$开头,且不可覆盖、混入,应避免名称重复 */ const AppPlus = require('./pageExtends') const Public = require('./public') AppPlus.pageExtends(Public) [代码] 代码下载:https://github.com/TobinXu/MiniProgramEventBus/tree/main
2022-08-19 - 小程序云开发如何实现 多id查询单表?
//例如有多个 id值 都在一个表里 .doc(???) 用doc吗? .where({}) 用where吗? let arr = ['id1','id2','id3',...] db.collection('todos') .get() .then(res => { console.log(res.data) }) .catch(err => { console.error(err) }) 示例代码 3
2021-07-13 - 简单实现签到日历效果
wxml: <view class="box"> <view class="section"> <picker mode="date" value="{{date}}" fields="month" start="2010-01-01" end="{{cy+'-'+cm}}" bindchange="bindDateChange"> <view class="picker">{{cur_year || "--"}} 年 {{cur_month || "--"}} 月</view> </picker> </view> <view> <!-- 显示星期 --> <view class="week color9b"> <view wx:for="{{weeks_ch}}" wx:key="unique">{{item}}</view> </view> <view class='days'> <!-- 行 --> <view class="rows" wx:for="{{days.length/7}}" wx:for-index="i" wx:key="unique"> <!-- 列 --> <view class="columns" wx:for="{{7}}" wx:for-index="k" wx:key="unique"> <!-- 每个月份的空的单元格 --> <view class='cell' wx:if="{{days[7*i+k].date == null}}"> <text decode="{{true}}"> </text> </view> <!-- 每个月份的有数字的单元格 --> <view class='cell' wx:else> <!-- 当前日期已签到 --> <view wx:if="{{days[7*i+k].isSign == true}}" class='qianbg'> <text class="colorff">{{days[7*i+k].date}}</text> <text class="sourse">+{{days[7*i+k].Score}}</text> </view> <!-- 当前日期未签到 --> <view wx:else> <text>{{days[7*i+k].date}}</text> </view> </view> </view> </view> </view> </view> </view> 简单提下思路,首先默认确定当前年月,cy cm, 初始化:获取days遍历日历的格子,通过获取当前月第一天是星期几来判断前面有几个空格,放入days,再当月天数放入days,然后进行渲染,再通接口去拿签到信息,签到成功的突出显示。这里签到初始化时我默认给了标识isSign,将已签到列表和当前年月日进行比较,符合条件则更新签到状态。切换选择日期,这里我用的是选择器,当然可以写成点击左侧按钮上一月,右侧按钮下一月那种,重新选择日期后,initdata(e) 传入年月,就是当前选择年月的数据。将星期日作为第一日的我也备注上去了。样式根据自己的喜好改就行了,最后看看我写的两个项目效果: [图片][图片] 写了个demo:https://developers.weixin.qq.com/s/SSlwjGmb7Wm9
08-14 - getUserInfo接口如何替换成getUserProfile超详细说明
微信小程序API近期又做了调整,之前用的好好的getUserInfo做了重大调整,无法直接获取用户信息了,比如昵称头像等等,当然2021年4月13日上线前的小程序不受影响,如果想要再次升级新版本,即必须涉及到更换获取用户授权的修改,将getUserInfo改成getUserProfile接口。 [图片] 这就代表着之前用的获取授权信息的方法要做调整了,先看看哪些模块受到影响。 之前使用如下代码,可以获取用户的相关信息: 之前:(使用上面的方式,会出现授权弹窗,同意后可以直接获取到nickName昵称及avatarUrl用户头像) [图片] 现在:(不会弹窗,直接获取用户信息nickName变成了匿名,avatarUrl用户头像变成了灰色头像) [图片] 授权过后鉴定是否授权同样也无法使用了,下面是app.js中的代码同过wx.getSetting获取scope.userInfo判断是否授权,现在获取不该参数,所有这个在app.js中的写法就无法判断用户有没有登录了。 [图片] 针对上面的文字,下面开始来介绍下我的实现方式,如何用好新接口getUserProfile。 先来看看wx.getUserProfile怎么用:https://developers.weixin.qq.com/miniprogram/dev/api/open-api/user-info/wx.getUserProfile.html 上面是官方地址,里面有详细的说明。 getUserProfile不像之前的getUserInfo一样必须放在按钮button上,而且要使用open-type="getUserInfo",新接口直接可以应用在任何标签上,使用点击时间或者其他触发事件直接执行getUserProfile接口,如下所示: 注:desc属性必须填写,不填写可能获取不到数据; 如下图所示,将会出现弹窗授权 [图片] 每次点击都会获取一次授权,这样有个好处就是之前getUserInfo时候拒绝了之后再想同意需要操作很大一圈代码,这个的话,每次点击都会重新弹窗一次这样倒是解决了一大难题。 [图片] 正确的用户信息,包含了昵称及头像等等... 存在的问题: 如果每次点击都授权的话用户体验非常的不友好,比如做了一个博客系统,每次用户想要给某一条信息点赞,点赞前都要授权一次,用户肯定很烦,所以,获取授权第一次的时候就要存储下来,然后再做表单提交或者点赞评论时候,判断数据库中用户信息是否存在就好了。 解决方案: user页面,提醒客户点击登录授权,默认头像及文字提醒,授权过后显示头像及昵称;[图片] user.wxml页面 {{userInfo.nickName}} {{userInfo.country+userInfo.province+userInfo.city}} 点击登录 user.js页面 //没有授权过的话,不要在当前页面存储用户信息,直接跳转到login页面同意处理用户信息 goLogin(){ wx.navigateTo({ url: '/pages/login/login' }) } login登录页面的操作,点击确认授权弹出授权浮窗。[图片] login.wxml页面 确认授权 暂不授权 login.js页面 //获取授权信息 clickUserProfile(){ wx.getUserProfile({ desc: '业务需要', lang:'zh_CN', success:res=>{ this.saveUserInfo(res.userInfo) } }) }, //保存用户信息 saveUserInfo(userInfo){ app.globalData.userInfo=userInfo //使用页面栈的方式,获取了授权信息接着更改用户页面的userInfo属性 var page=getCurrentPages()[getCurrentPages().length-2]; page.setData({ userInfo }) //使用云函数saveuser将用户信息存储到云数据库中 wx.cloud.callFunction({ name:"saveuser", data:{ userInfo } }).then(res=>{ wx.showToast({ title: '授权成功' }) setTimeout(()=>{ this.noLogin(); },1500) }) } saveuser云函数页面 // 云函数入口函数 exports.main = async (event, context) => { const openid = cloud.getWXContext().OPENID const {userInfo}=event; userInfo.openid=openid; //获取数据库中有没有当前用户的信息 var res= await db.collection("userAll").where({ openid:openid }).count() if(res.total>0){ return await db.collection('userAll').where({ openid }).update({ data: userInfo }) }else{ return await db.collection('userAll').add({ data: userInfo }) } } 一旦获取了用户信息,自动会从login页面跳转到user页面,同理user页面中的userInfo就变成了最新的用户数据,user页面也就变成了这样; [图片] 首次进入user页面时候需要从数据库判断是否已经存在该用户信息 app.js页面 //定义hasUserInfo函数,发送云函数,同过返回true和false判断是否已经授权 async hasUserInfo(){ if (this.globalData.userInfo && this.globalData.userInfo.nickName && this.globalData.userInfo.avatarUrl) return true var res= await wx.cloud.callFunction({ name:"getuser" }) if(res.result.code==200){ this.globalData.userInfo=res.result.data[0] return true }else{ return false } } getuser云函数页面 // 云函数入口函数 exports.main = async (event, context) => { const openid = cloud.getWXContext().OPENID var res=await db.collection("userAll").where({ openid }).get(); if(res.data.length){ return {data:res.data,code:200} }else{ return {code:400} } } 在需要的位置就可以使用app.js中的hasUserInfo方法了,比如user页面 user.js页面 //页面载入时 onLoad:async function (options) { await app.hasUserInfo() this.setData({ userInfo:app.globalData.userInfo }) } 比如对一个点赞按钮操作时候先判断有没有用户信息时候: //点赞操作 async clickZan(){ if(await app.hasUserInfo()){ console.log("可以点赞"); }else{ wx.navigateTo({ url: '/pages/login/login' }) } } 还有一种不保存用户信息,只负责在页面中展现的可以直接使用open-data组件,不用授权就可以轻松获取用户信息; 组件地址如下: https://developers.weixin.qq.com/miniprogram/dev/component/open-data.html 演示代码如下: 最终效果 [图片] 如果文章没有看懂,还有视频的介绍 https://www.bilibili.com/video/BV1s64y1i7Rw
2021-04-22