- 适配隐私保护协议接口
1、相关api说明 适配过程中主要是用到了以下几个接口 1. wx.onNeedPrivacyAuthorization:用于监听隐私接口需要用户授权事件,只有当隐私协议需要用户授权时才会由平台触发此事件,然后开发者需要弹窗显示隐私协议说明。如果用户拒绝授权,则隐私接口调用失败,在下次调用到隐私接口时还会继续弹。 2. wx.requirePrivacyAuthorize:用于模拟隐私接口调用,并触发隐私弹窗逻辑,也就是会触发wx.onNeedPrivacyAuthorization,但如果用户之前已经同意过隐私授权,会立即返回success回调,不会触发 wx.onNeedPrivacyAuthorization 。这个api的使用场景目前我是用在获取用户昵称时,在下一篇文章会进行说明。 3. wx.openPrivacyContract:用于打开隐私协议页面。 其他api大家可以根据具体情况选择使用。 2、弹窗思路 其实就是写一个自定义组件,然后在有调用到隐私接口的页面引入,利用自定义组件的attached方法,把各个页面的隐私弹窗组件的显示、隐藏方法保存到自定义组件的全局变量中,当用户点击隐私协议弹窗的同意、拒绝按钮时调用resolve方法,将对应的参数通知给平台。 3、注意点 隐私接口在隐私保护指引中有声明才能调用基础库版本2.32.3及以上开始支持2023.9.15号之前,在 app.json 中配置 __usePrivacyCheck__: true 后,会启用隐私相关功能,如果不配置或者配置为 false 则不会启用。2023.9.15号之后,不论 app.json 中是否有配置 __usePrivacyCheck__,隐私相关功能都会启用wx.onNeedPrivacyAuthorization 是覆盖式注册监听,若重复注册监听,则只有最后一次注册会生效同意授权后如果想取消授权,在开发工具上 清缓存->清除模拟器缓存->清除授权数据,在手机上删除小程序即可隐私弹窗的z-index要设置成最大,避免被其他遮罩挡住导致无法点击,另外如果有调到官方的api例如wx.showLoading,也要注意是否设置了mask,避免在loading的时候弹出隐私弹窗,导致弹窗无法点击,而loading又要等弹窗关闭才会消失的尴尬情况4、代码如下 4.1、app.json "usingComponents": { //全局自定义组件 "privacyPopup": "/components/privacy/privacyPopup" }, //开启隐私相关功能 "__usePrivacyCheck__": true 4.2、自定义组件privacyPopup 4.2.1、privacyPopup.wxml <view class="container" wx:if="{{show}}"> <view class="cover {{showCoverAnimation?'cover-fade-in':''}}" catch:touchmove="return"></view> <view class="privacy-box {{showBoxAnimation?'slade-in':''}} {{device.isPhoneX? 'phx_68':''}}" catch:touchmove="return"> <view class="title flex-start-horizontal"> <view class="logo" wx:if="{{privacyConfig.icon}}"> <image class="icon" src="{{privacyConfig.icon}}"></image> </view> <view class="mini-name">{{privacyConfig.name || '小程序'}}</view> </view> <view class="tips"> <view class="privacy-content"> <view class="start">{{privacyConfig.content.start}}</view> <view class="link" bindtap="openPrivacyContract"> {{privacyConfig.content.mid}} </view> <view class="end">{{privacyConfig.content.end}}</view> </view> </view> <view class="buttons flex-center"> <button class="cancel reset-btn" bindtap="disagree">拒绝</button> <button class="save reset-btn" id="agree-btn" open-type="agreePrivacyAuthorization" bindagreeprivacyauthorization="agree">同意</button> </view> </view> </view> 4.2.2、privacyPopup.wxss @import "/app.wxss"; .cover{ background-color: #111; opacity: 0; position: fixed; left: 0; top: 0; right: 0; bottom: 0; z-index: 50000; transition: opacity .3s; } .privacy-box{ position: fixed; left: 0rpx; right: 0rpx; bottom: 0rpx; z-index: 51000; background-color: #fff; box-sizing: border-box; padding-top: 60rpx; padding-left: 50rpx; padding-right: 50rpx; border-radius: 30rpx 30rpx 0 0; transform: translateY(100%); transition: transform .3s, bottom .3s; } .cover-fade-in{ opacity: 0.7; } .slade-in{ transform: translateY(0); bottom: 0rpx; } .privacy-box .title{ font-size: 32rpx; font-weight: 500; text-align: center; margin-bottom: 40rpx; color: #161616; } .privacy-box .title .icon{ width: 46rpx; height: 46rpx; margin-right: 8rpx; vertical-align: bottom; border-radius: 50%; } .privacy-box .title .mini-name{ /*margin-bottom: 2rpx;*/ } .privacy-box .tips{ margin-bottom: 20rpx; } .privacy-box .tips .privacy-content{ color: #606266; font-size: 30rpx; margin-bottom: 20rpx; line-height: 1.6; } .privacy-box .tips .privacy-content .start, .privacy-box .tips .privacy-content .link, .privacy-box .tips .privacy-content .end { display: inline; } .privacy-box .tips .privacy-content .link{ color: #1890FF; } .privacy-box .buttons{ margin-bottom: 40rpx; margin-top: 50rpx; text-align: center; font-size: 34rpx; font-weight: 550; } .privacy-box .buttons .save{ width: 220rpx !important; height: 90rpx; line-height: 90rpx; border-radius: 14rpx; color: #fff; /*background:#fff linear-gradient(90deg,rgba(246, 120, 121,.9) 10%, rgb(246, 120, 121));*/ background-color: #07c160; margin-left: -50rpx; } .privacy-box .buttons .cancel{ width: 220rpx !important; height: 90rpx; line-height: 90rpx; border-radius: 14rpx; color: #07c160; background-color: #F2F2F2; } 4.2.3、privacyPopup.js //获取应用实例 const tabbar = require("../../utils/tabbar.js"); const app = getApp(); //用来保存各个页面注册的隐私协议回调事件 let privacyHooks = {}; if (wx.onNeedPrivacyAuthorization) { console.warn("当前基础库支持api wx.onNeedPrivacyAuthorization"); wx.onNeedPrivacyAuthorization(resolve => { console.warn("需要隐私协议弹窗:", resolve); const pages = getCurrentPages(); const route = pages[pages.length - 1].route; const hook = privacyHooks[route]; if (hook) { hook.resolve = resolve; hook.show(resolve); } else { console.error("当前页面没有注册隐藏协议弹窗回调:", route); } }) } else { console.warn("当前基础库不支持api wx.onNeedPrivacyAuthorization"); } Component({ data: { show: false, showCoverAnimation: false,//显示类别选择窗口动画 showBoxAnimation: false,//显示类别选择窗口动画 }, lifetimes: { //在组件实例进入页面节点树时执行 attached: function () { console.warn("privacyPopup attached"); const pages = getCurrentPages(); privacyHooks[pages[pages.length - 1].route] = { show: resolve => { this.show(resolve); }, close: () => { this.hide(); } } }, //在组件实例被从页面节点树移除时执行 detached: function () { console.warn("privacyPopup detached"); } }, methods: { /** * 同意隐私协议 * @param e */ agree(e) { Object.values(privacyHooks).forEach(hook => { hook.close(); hook.resolve && hook.resolve({ event: "agree", buttonId: "agree-btn" }); }); }, /** * 不同意隐私协议 * @param e */ disagree(e) { Object.values(privacyHooks).forEach(hook => { hook.close(); hook.resolve && hook.resolve({ event: "disagree" }); }); }, /** * 显示隐私协议授权弹窗 */ show(resolve) { app.fillConfig(this, ["privacyConfig"], data => { console.log("显示隐私协议弹窗"); const device = app.getSystemInfo(); this.setData({ show: true, device, }, () => { this.setData({ showCoverAnimation: true, showBoxAnimation: true }); //自定义隐私弹窗曝光告知平台 resolve({event: "exposureAuthorization"}); }); tabbar.hideTabByPrivacy(this); }); }, /** * 关闭隐私协议授权弹窗 */ hide() { console.log("隐藏隐私协议弹窗"); this.setData({ showCoverAnimation: false, showBoxAnimation: false }, () => { const that = this; setTimeout(function () { that.setData({ show: false }) }, 300) }) tabbar.showTabByPrivacy(this); }, /** * 打开隐私协议 */ openPrivacyContract() { wx.openPrivacyContract({ success: res => { console.log("openPrivacyContract success") }, fail: res => { console.error("openPrivacyContract fail", res) } }) } } }) 5、弹窗效果 [图片]
2023-09-13 - 记录恋爱小清单5.0升级完毕
白天忙着搬砖,晚上回家忙着捣腾小程序,断断续续还是把小清单5.0上线了。 当年搞小清单的时候云开发刚推出不久,那时还不能多表关联查,所以像首页的清单展示,一次要同时查好几个表,最后再组装好吐给小程序。整体交互体验比较差,后期维护起来也很蛋疼,所以这次下决心做了一次重构:表结构重新设计,底层逻辑重写,清洗历史数据,导入新表。 此外还做了一些调整: 1、清单增加完成功能,老版本是只要上传一张照片就表示完成清单,但是挺多用户会提前上传一张照片占坑,让整体看起来显得好看,而且这样有时也会让用户有些困惑。所以这次放开,让用户自己决定是否已完成清单。 2、清单、纪念日的分享海报重新设计,背景采用毛玻璃形式,颜值相比旧版有所提升[狗头]。 3、允许自定义清单主题,可增可减。 4、恋人圈改版,早期是仿朋友圈的熟人社交设计,做的也比较粗糙,这次改版后除了点赞评论以外还加入了祝福的概念,希望在这里大家分享的幸福回忆都可以得到美好祝福。 5、个人中心改版。增加了密码保护可能,开启后离开小程序5分钟重新进入则需要输入密码解锁,保护用户的隐私。 未来还考虑增加支持上传视频的方式记录回忆、匿名分享、日记本等等,时间有限,但想做的事情还挺多... 最后附上几张图片聊表心意,感谢阅读 [图片] [图片] [图片] [图片]
2023-05-30 - 加固插件已激活,加固前,加固配置内没有合法的js文件
最近,微信开发工具的代码加固秃然用不了了。每次预览时都会提示 加固插件已激活,加固前,加固配置内没有合法的js文件,请检查路径 [图片] 尝试了把加固插件卸载重装、开发工具升级最新版、基础库切换到最新版本依然无法解决问题。社区中也有人6月份反馈类似问题:https://developers.weixin.qq.com/community/develop/doc/0000cc339300d0c325ef9570b56c00 配置其实都是正确的,这个提示真是让人一头雾水。 没办法啊,只能自己继续摸索 尝试把开发工具的授权全部清除,这样在预览时加固插件会先请求授权,但是授权的弹窗提示让我觉得有些奇怪: [图片] null extension description。。。这是插件没有安装成功吗? 然后在工具的输出栏可以看到加固插件(appid是wxext871b2e053677418)运行日志,无意间发现插件的安装目录其实是在C:\Users\Yunfay\AppData\Local\微信开发者工具\User Data\,这个目录因为之前因为占用空间太大,已经达到了15G,C盘告警,所以我有更换过储存目录,然后用mklink建了一个软链,这个方法很早前官方人员也有在社区中提及过,这个是帖子链接 https://developers.weixin.qq.com/community/develop/doc/000e8eed8a4448555dc9c877f5b400 最后把软链删除,并将之前移走的文件挪回原来目录,代码加固插件也就恢复正常了。。。 现在只能把开发工具卸载重装,不然C盘的占用空间有些夸张。 不清楚加固插件里面的处理逻辑或者说是mklink有什么副作用(欢迎知道原理的大佬评论指点一下),这里简单把这个问题记录一下,希望那些还在被这个问题困扰的小伙伴也能有所启发。 [图片]
2023-09-17 - 简单写个云函数路由
随着云函数数量的增多,有的并发量不大的云函数目前是用路由的方式访问,但是每次都要搬路由代码维护起来也比较麻烦,之前也有看过tcb-route的介绍,个人感觉不是很优雅【不喜勿喷谢谢】。所以干脆就自己写了一个sdk,打包上传到npm,有需要的小伙伴可以自取 npm i tcb-server-sdk 目前使用到的主要有两个类:RouteBase、RouteHolder,后续会再继续完善 项目开源地址:https://github.com/showms/tcb-server-sdk.git 说明:被路由的处理类要求继承RouteBase云函数入口使用RouteHolder初始化路由对象,允许指定固定前缀RouteBase会自动使用wx-server-sdk初始化云环境其中:RouteBase还会初始化一些云函数所需的操作对象初始化代码如下 /** * 路由基类 * 所有需要路由转发的类都需要继承这个类 */ class RouteBase { static instance; cloud; db; _; $; wxContext; response; event; constructor(event = {}) { const {env} = event; cloud.init({ //允许外部指定云开发环境 env: env || cloud.DYNAMIC_CURRENT_ENV }); this.cloud = cloud; //数据库,具体见云开发API this.db = cloud.database({ throwOnNotFound: false }); //数据库操作符,具体见云开发API this._ = this.db.command; //数据库聚合操作,具体见云开发API this.$ = this.db.command.aggregate; //用户上下文 this.wxContext = cloud.getWXContext(); //响应封装类 this.response = response; this.event = event; } /** * 获取实例 * @param event * @returns {*} */ static getInstance(event = {}) { this.instance = new this(event); //console.log("重置用户上下文、入参信息,当前用户信息:", JSON.stringify(this.instance.wxContext)); return this.instance; } } 所以所有继承RouteBase的子类都可以使用父类里面的这些操作对象 使用示例: 假设云函数代码目录结构如下: \--- functions +--- routeTest +---- service | +---- user | \---- Name.js | \---- Test.js \---- index.js \---- package.json \---- package-lock.json 云函数入口文件 const route = require("./tcb-server-sdk"); // 云函数入口函数 exports.main = async (event, context) => { console.log("入参:", event); //根据目录结构可以指定固定前缀为service //小程序端就可以使用user/Name/get访问到Name处理类里的get方法 //如果不指定固定前缀,则需要使用service/user/Name/get才能访问到Name处理类里的get方法 return await new route.RouteHolder("service").process(event); }; 请求处理类Name.js const route = require("./tcb-server-sdk"); class Name extends route.RouteBase { async index(params) { console.log("index方法:", params); return {}; } async get(params) { console.log("接口请求参数:", params); //这里的this.wxContext来自RouteBase return {"idol": "kobe", "wxContext": this.wxContext}; } } module.exports = Name; 小程序端 //执行云函数 wx.cloud.callFunction({ // 云函数名称 name: "routeTest", // 传给云函数的参数 data: { env: "test",// 环境可以不填,不填就使用当前环境 uri: "user/Name/get",// 请求URL,云函数端的RouteHolder会找到对应的处理类并调用指定方法 //uri: "user/Name",// 请求URL,这种写法会请求到Name类的index方法 params: {"source":"1"} } }).then(res => { }).catch(res => { }).then(res => { });
2023-04-06 - 图片安全检测data exceed max size解决方案
最近在重构小程序恋爱小清单,在用云函数做图片的安全检测时报了一个错:cloud.callFunction:fail Error: data exceed max size 也就是图片超过了大小限制。 早期的版本是通过画布将图片缩小(wx.canvasToTempFilePath),接着读取文件流(wx.getFileSystemManager().readFile),然后再提交云函数检测,过程感觉有些繁琐复杂 最近发现其实有更简单的方法,可以借助临时的CDN,传递大数据,最终在云函数端会收到一个CDN地址,接着通过request-promise读取文件流,然后再做安全检测,相比旧版的方法个人感觉简单清爽不少。 参考官方文档: https://developers.weixin.qq.com/miniprogram/dev/wxcloud/reference-sdk-api/utils/Cloud.CDN.html 代码如下: 小程序端: const api = require("api.js"); /** * 图片安全检测 * 借助临时CDN传递大数据 * @param filePath 图片的临时文件路径 (本地路径) * @returns {Promise<unknown>} */ const imgSecCheckViaCDN = (filePath) => { return new Promise(function (resolve, reject) { api.callCloudFunction("securityCheck", { type: "imgSecCheckViaCDN", imgData: wx.cloud.CDN({ type: "filePath", filePath, }) }, res => { console.log("图片安全检测结果:", JSON.stringify(res)); const result = res.result; if (result.success) { resolve(result); } else { reject(result); } }, reject); }); } api.js /** * 云函数调用 * @param name * @param data * @param success * @param fail * @param complete */ const callCloudFunction = function (name, data, success, fail, complete) { //执行云函数 wx.cloud.callFunction({ // 云函数名称 name: name, // 传给云函数的参数 data: Object.assign({}, data, {env: env.activeEnv}) }).then(res => { typeof success == 'function' && success(res); }).catch(res => { typeof fail == 'function' && fail(res); }).then(res => { typeof complete == 'function' && complete(res); }); }; module.exports = {callCloudFunction} 云函数端: // 云函数入口文件 const cloud = require('wx-server-sdk'); const responce = require('easy-responce'); const requestHelper = require('./utils/requestHelper'); const headers = { encoding: null, headers: { "content-type": "application/octet-stream", // "content-type": "video/mpeg4", }, }; // 云函数入口函数 exports.main = async (event, context) => { cloud.init({ env: event.env }); let result = {}; try { const {type, content, imgData} = event; let {buffer} = event; console.log("检测类型:", type, "文本内容:", content, "图片内容:", imgData); switch (type) { case "imgSecCheckViaCDN": const imageResponse = await requestHelper.request(imgData, headers, {}); buffer = imageResponse.body; case "imgSecCheck": result = await cloud.openapi.security.imgSecCheck({ media: { contentType: 'image/png', // value: Buffer.from(imgBase64, "base64") value: Buffer.from(buffer) } }); break; case "msgSecCheck": result = await cloud.openapi.security.msgSecCheck({content}); break; default: console.log("不支持的检测类型:", type); break; } } catch (e) { console.error(e); result = e; } console.log("检测结果:", result); const {errCode, errMsg} = result; return errCode !== 87014 ? responce.success({errCode}) : responce.fail(errMsg); }; requestHelper.js const rp = require('request-promise'); /** * http请求 * @param url * @param options * @param data * @param autoFollowRedirect * @returns {Promise<unknown>} */ const request = function (url, options, data, autoFollowRedirect = true) { return new Promise(function (resolve, reject) { const p = Object.assign({ json: true, resolveWithFullResponse: true, followRedirect: autoFollowRedirect }, options, data, {url}); console.log("请求参数:", JSON.stringify(p)); return rp(p) .then(async function (repos) { //console.log("获取到最终内容,执行回调函数:", repos); return resolve(repos); }) .catch(async function (err) { if (err && (err.statusCode === 301 || err.statusCode === 302)) { // console.log("停止重定向,重定向信息:", err); console.log("停止重定向"); return resolve(err); } console.error("重定向失败:", err); return reject(err); }); }); } module.exports = {request }
2022-10-21 - 省钱有道之 减少云函数调用次数
由于云函数有一项计费规则是按调用次数计费,在小程序访问量比较小的情况下还比较无所谓,但当体量上来之后不得不考虑控制一下对公共接口的调用次数从而减少一些不必要的开销。比如获取用户信息接口、获取配置信息接口 这里分享一个我自己几个小程序用到的方法,公共接口的调用都放在app.js,然后提供函数供其他页面调用。同时由于异步问题,有可能页面加载完接口还未返回,因此还需能够注册回调函数,在接口返回数据后回调给调用页面 代码示例: app.js App({ onLaunch: async function (options) { //判断是否需要更新小程序 updateCheck.check(); await api.wxCloudInit(); //获取用户信息 this._getUserInfo().catch(res => { console.warn("获取用户信息失败,准备重试"); this._getUserInfo().then(); }); }, /** * 获取用户信息 * @param callback * @param refresh 等于true时表示重新查询用户信息,同时也会更新会员状态 */ getUserInfo: function (callback, refresh) { if (!refresh) { const userInfo = this.globalData.userInfo; if (!userInfo.ready) { if (typeof callback == 'function') { this.callbackFunctions.userInfoReadyCallback.push(callback); } if (!this.userInfoReadyCallback) { this.userInfoReadyCallback = res => { console.log("获取用户信息完毕,开始回调", res); const callbacks = this.callbackFunctions.userInfoReadyCallback; while (callbacks.length) { const callback = callbacks.pop(); typeof callback == 'function' && callback(res); } /*callbacks.forEach(callback => { typeof callback == 'function' && callback(res); })*/ } console.log("注册userInfoReadyCallback成功"); } else { console.log("已经注册了userInfoReadyCallback,不再重复注册"); } } else { typeof callback == 'function' && callback(userInfo); } } else { console.log("准备更新用户信息") this._getUserInfo().then(userInfo => { typeof callback == 'function' && callback(userInfo); }); } }, /** * 执行云函数,获取用户信息 * @returns {Promise<unknown>} * @private */ _getUserInfo: function () { return new Promise((resolve, reject) => { api.callCloudUserCenterFunction("UserInfoHandler/getUserInfo", {}, res => { console.log("获取用户数据完毕:", res.result); const result = res.result; if (result.success) { const data = result.data; this.globalData.userInfo = data; // 由于 getUserInfo 是网络请求,可能会在 Page.onLoad 之后才返回 // 所以此处加入 callback 以防止这种情况 if (this.userInfoReadyCallback) { this.userInfoReadyCallback(data); } resolve(data); } else { console.error("没有获取到用户信息"); reject("没有获取到用户信息"); } }, e => { console.error("获取用户信息失败", e); reject("获取用户信息失败"); }); }); }, /** * 异步事件回调函数列表 * 增加这个列表是为了避免不同地方同时调用,互相覆盖回调函数 */ callbackFunctions: { //用户信息异步回调 userInfoReadyCallback: [], }, globalData: { userInfo: { confirm: false//用来标记用户信息查询动作是否已经结束,等于true时,userInfo才可信 }, } }) 某page.js app.getUserInfo(res => { const isVip = res.isVip; if (isVip) { console.log("已开通会员", res); } });
02-07 - 不同页面通过订阅消息事件的方式互相通信
业务背景是tabbar有一个新增的功能,在不同的页面都可以操作新增,保存后首页以及个人中心页面的清单数据都要马上更新,但又不想通过调用接口的方式更新。因此自己搞了一个简单的消息事件订阅,比如A,B,C三个页面同时订阅更新事件,那么在D页面操作了更新动作后就会通知A,B,C三个页面。 代码示例: event.js //事件集合 const eventInfo = {}; /** * 注册消费者 * 相同事件、相同消费者名称时会被覆盖 * @param eventName 事件名称 * @param callback 回调事件 */ const on = (eventName, callback) => { const pages = getCurrentPages(); const consumerName = pages[pages.length - 1].route; if (!eventInfo[eventName]) { console.warn("广播事件不存在,不处理", eventName, consumerName); return; } const consumerInfo = eventInfo[eventName]; consumerInfo[consumerName] = callback; console.log("注册消费者完毕=>", eventName, consumerName); } /** * 取消消费者 * @param eventName 事件名称 */ const off = (eventName) => { const pages = getCurrentPages(); const consumerName = pages[pages.length - 1].route; if (!eventInfo[eventName]) { console.warn("广播事件不存在,不处理=>", eventName, consumerName); return; } const consumerInfo = eventInfo[eventName]; delete consumerInfo[consumerName]; console.log("取消消费者完毕=>", eventName, consumerName); } /** * 推送事件消息 * @param eventName 事件名称 * @param data 消息体 */ const emit = (eventName, data = {}) => { if (!eventInfo[eventName]) { console.warn("广播事件不存在,不处理=>", eventName, data); return; } const consumerInfo = eventInfo[eventName]; const consumerNames = Object.keys(consumerInfo); consumerNames.forEach(consumerName => { if (typeof consumerInfo[consumerName] == 'function') { console.log("推送事件消息完毕=>", eventName, consumerName, data); consumerInfo[consumerName](data); } else { console.warn("消费者没有回调函数,不处理=>", eventName, consumerName, data); } }) } /** * 批量推送事件消息 * @param eventNames * @param data */ const emitBatch = (eventNames, data) => { eventNames.forEach(eventName => { emit(eventName, data); }) } /** * 初始化 */ const init = () => { try { console.log("初始化广播通道"); const eventNames = Object.keys(EVEN_NAME); eventNames.forEach(eventName => { eventInfo[eventName] = []; }) } catch (e) { console.error("初始化广播通道出现异常:", e) } } /** * 事件列表 * @type {{REFRESH_CARD_LIST: string, REFRESH_USER_INFO: string}} */ const EVEN_NAME = { RELOAD_CARD_LIST: "RELOAD_CARD_LIST",//重新加载清单数据 RELOAD_USER_INFO: "RELOAD_USER_INFO",//重新加载用户信息 SAVE_CARD_LIST: "SAVE_CARD_LIST",//保存清单 JOIN_INFO_FINISH: "JOIN_INFO_FINISH",//共享完成 JOIN_INFO_FINISH_CONFIRM: "JOIN_INFO_FINISH_CONFIRM",//共享完成确认 } module.exports = {init, on, off, emit, emitBatch, EVEN_NAME} 订阅消息: event.on(event.EVEN_NAME.RELOAD_CARD_LIST, data => { console.log("接收到刷新清单事件推送", data); this.loadData(); }); event.on(event.EVEN_NAME.RELOAD_USER_INFO, data => { console.log("接收到刷新用户信息事件推送", data); auth.setUserInfo2Data(app, this).then(); }); event.on(event.EVEN_NAME.JOIN_INFO_FINISH, data => { console.log("接收到共享记录完成事件推送", data); this.loadData(); auth.setUserInfo2Data(app, this).then(); this.toast.showToast({text: "清单共享成功啦"}); }) 发送消息: event.emit(event.EVEN_NAME.SAVE_CARD_LIST, e);
02-07 - 省钱有道之 云开发环境共享小结
#前言 最近为了节省一点小程序的运营成本,一些没啥流量的小程序如果每个月也要19块略微有些肉疼(主要还是穷),研究了一下云环境共享,在这里简单做一下总结。 [图片] 这里有官方的小程序环境共享文档需提前了解一下,具体共享步骤按官方文档操作即可。 https://developers.weixin.qq.com/miniprogram/dev/wxcloud/guide/resource-sharing/introduce.html #注意点 共享环境有几个注意点大致如下: 1、必须是相同主体 2、开通了云开发环境的小程序可以共享给同主体的小程序、公众号,被共享方无需开通云开发环境 3、一个云开发环境最多可以共享给10个小程序/公众号 4、共享后双发均可主动解除 5、按官方文档要求,资源方需有云函数cloudbase_auth,测试时发现没有这个云函数其实也能正常运行,可能我验证的场景还不够多 6、云能力初始化的方式不同,资源方按传统的云环境初始化方式即可,也就是 wx.cloud.init({ env: env.activeEnv, traceUser: true }); 而调用方的初始化方式有所不同 const cloud = new wx.cloud.Cloud({ //资源方AppID resourceAppid, //资源方环境ID resourceEnv, }) // 跨账号调用,必须等待 init 完成 // init 过程中,资源方小程序对应环境下的 cloudbase_auth 函数会被调用,并需返回协议字段(见下)来确认允许访问、并可自定义安全规则 const initRes = await cloud.init(); 后续调用资源方的云函数就用这个cloud就行了:cloud.callFunction({...}); 7、调用方有操作到云存储文件的api也需要用6步骤中的cloud 8、云存储fileId需要用cloud.getTempFileURL转换成临时/永久链接,否则在调用方无法展示 9、一些api的云调用方式也有变化,需指明具体的appid。比如A小程序授权给了B小程序,想给B小程序推送客服消息需要写成 await cloud.openapi({appid:B小程序appid}).customerServiceMessage.send({...}); 10、获取调用方的appid/openid/unionid也有所不同 // 跨账号调用时,由此拿到来源方小程序/公众号 AppID console.log(wxContext.FROM_APPID) // 跨账号调用时,由此拿到来源方小程序/公众号的用户 OpenID console.log(wxContext.FROM_OPENID) // 跨账号调用、且满足 unionid 获取条件时,由此拿到同主体下的用户 UnionID console.log(wxContext.FROM_UNIONID) #适配 基于以上注意点,开始进行适配,由于我是一套代码部署N个小程序,然后一个云环境共享给其他小程序,希望通过配置决定哪个小程序作为资源方,哪些作为调用方 首先是云开发环境的初始化: 1、env.js 环境配置: //云开发环境 const cloudBase = { //使用共享云环境资源,资源方=false,调用方=true useShareResource: false, //资源方AppID resourceAppid: "wx9d2xxxxxxxx0088", //资源方环境ID resourceEnv: "prod-9gxqvi3qb3c257ef", //云环境ID prod: "prod-9gxqvi3qb3c257ef" } 2、api.js 操作模块 const env = require('../env.js'); let cloud; /** * 初始化云能力 * @returns {Promise} */ const wxCloudInit = async function () { const {cloudBase} = env; if (!wx.cloud) { console.error('请使用 2.2.3 或以上的基础库以使用云能力') } else if (cloudBase.useShareResource) { const {resourceAppid, resourceEnv} = cloudBase; // 声明新的 cloud 实例 cloud = new wx.cloud.Cloud({ //资源方AppID resourceAppid, //资源方环境ID resourceEnv, }) // 跨账号调用,必须等待 init 完成 // init 过程中,资源方小程序对应环境下的 cloudbase_auth 函数会被调用,并需返回协议字段(见下)来确认允许访问、并可自定义安全规则 const initRes = await cloud.init(); console.log("初始化云能力完毕:", initRes, "资源方appid:", resourceAppid, "资源方环境ID:", resourceEnv); } else { wx.cloud.init({ env: env.activeEnv, traceUser: true }); console.log("初始化云能力完毕,当前环境:", env.activeEnv); cloud = wx.cloud; } this.cloud = cloud; } /** * 云函数调用 * @param name * @param data * @param success * @param fail * @param complete */ const callCloudFunction = function (name, data, success, fail, complete) { //执行云函数 cloud.callFunction({ // 云函数名称 name: name, // 传给云函数的参数 data: Object.assign({}, data, {env: env.activeEnv}) }).then(res => { typeof success == 'function' && success(res); }).catch(res => { typeof fail == 'function' && fail(res); }).then(res => { typeof complete == 'function' && complete(res); }); }; 3、在app.js中初始化云环境,后续有用到wx.cloud的都需要改成api.cloud const api = require('utils/api.js'); App({ onLaunch: async function (options) { await api.wxCloudInit(); } }); 其次是资源方的获取用户信息调整 每次都要判断wxContext.FROM_OPENID是否为空,不为空则是调用方的用户信息,为空则是资源方的用户信息,略微繁琐,干脆封装了一个npm包wx-server-inherit-sdk,改造了一下getWxContext函数,源码如下,引入这个包后也就可以不用引入官方的wx-server-sdk const cloud = require('wx-server-sdk'); // 保存原始getWXContext方法到另一个变量 const originalGetWXContext = cloud.getWXContext; cloud.getWXContext = function () { //调用原始getWXContext方法 const wxContext = originalGetWXContext.call(this); const {FROM_APPID, FROM_OPENID} = wxContext; //云开发环境共享时获取到的APPID会替换成源方APPID if (FROM_APPID) { Object.assign(wxContext, {APPID: FROM_APPID}); } //云开发环境共享时获取到的OPENID会替换成源方OPENID if (FROM_OPENID) { Object.assign(wxContext, {OPENID: FROM_OPENID}); } return wxContext; } module.exports = cloud; 到此也就大功告成。为了省钱也是够折腾的[哭笑]
2023-08-28