- 小程序app.onLaunch与page.onLoad异步问题的最佳实践
场景: 在小程序中大家应该都有这样的场景,在onLaunch里用wx.login静默登录拿到code,再用code去发送请求获取token、用户信息等,整个过程都是异步的,然后我们在业务页面里onLoad去用的时候异步请求还没回来,导致没拿到想要的数据,以往要么监听是否拿到,要么自己封装一套回调,总之都挺麻烦,每个页面都要写一堆无关当前页面的逻辑。 直接上终极解决方案,公司内部已接入两年很稳定: 1.可完美解决异步问题 2.不污染原生生命周期,与onLoad等钩子共存 3.使用方便 4.可灵活定制异步钩子 5.采用监听模式实现,接入无需修改以前相关逻辑 6.支持各种小程序和vue架构 。。。 //为了简洁明了的展示使用场景,以下有部分是伪代码,请勿直接粘贴使用,具体使用代码看Github文档 //app.js //globalData提出来声明 let globalData = { // 是否已拿到token token: '', // 用户信息 userInfo: { userId: '', head: '' } } //注册自定义钩子 import CustomHook from 'spa-custom-hooks'; CustomHook.install({ 'Login':{ name:'Login', watchKey: 'token', onUpdate(token){ //有token则触发此钩子 return !!token; } }, 'User':{ name:'User', watchKey: 'userInfo', onUpdate(user){ //获取到userinfo里的userId则触发此钩子 return !!user.userId; } } }, globalData) // 正常走初始化逻辑 App({ globalData, onLaunch() { //发起异步登录拿token login((token)=>{ this.globalData.token = token //使用token拿用户信息 getUser((user)=>{ this.globalData.user = user }) }) } }) //关键点来了 //Page.js,业务页面使用 Page({ onLoadLogin() { //拿到token啦,可以使用token发起请求了 const token = getApp().globalData.token }, onLoadUser() { //拿到用户信息啦 const userInfo = getApp().globalData.userInfo }, onReadyUser() { //页面初次渲染完毕 && 拿到用户信息,可以把头像渲染在canvas上面啦 const userInfo = getApp().globalData.userInfo // 获取canvas上下文 const ctx = getCanvasContext2d() ctx.drawImage(userInfo.head,0,0,100,100) }, onShowUser() { //页面每次显示 && 拿到用户信息,我要在页面每次显示的时候根据userInfo走不同的逻辑 const userInfo = getApp().globalData.userInfo switch(userInfo.sex){ case 0: // 走女生逻辑 break case 1: // 走男生逻辑 break } } }) 具体文档和Demo见↓ Github:https://github.com/1977474741/spa-custom-hooks 祝大家用的愉快,记得star哦
2023-04-23 - channel-video 组件如何隐藏点赞数和视频号昵称?
[图片]如何隐藏视频号昵称和点赞数,并且控制视频logo大小,有赞的是如何实现的[图片]
01-02 - 微信小程序转发朋友圈详解
概述点击右上角分享朋友圈[图片] 分享到朋友圈样式[图片] 朋友圈打开样式[图片] 这个功能目前只支持Android(在IOS高版本微信支持朋友圈打开小程序能力,但不能分享)。 用户打开朋友圈分享的小程序,看到不是真正的小程序,而是原本页面的“单页模式”。 什么是“单页模式”?以下是微信官方对于“单页模式”的描述: “单页模式”下,页面顶部固定有导航栏,标题显示为当前页面 JSON 配置的标题。底部固定有操作栏,点击操作栏的“前往小程序”可打开小程序的当前页面。顶部导航栏与底部操作栏均不支持自定义样式。“单页模式”默认运行的是小程序页面内容,但由于页面固定有顶部导航栏与底部操作栏,很可能会影响小程序页面的布局。因此,请开发者特别注意适配“单页模式”的页面交互,以实现流畅完整的交互体验。限制另外,“单页模式”存在着很多限制。以下是官方给出的禁用能力列表: [图片] 限制主要包括以下几点: 页面无登录态,与登录相关的接口,如 [代码]wx.login[代码] 均不可用不允许跳转到其它页面,包括任何跳小程序页面、跳其它小程序、跳微信原生页面若页面包含 tabBar,tabBar 不会渲染,包括自定义 tabBar本地存储与小程序普通模式不共用这些限制,让“单页模式”只适用于内容展示,不适用于有较多交互。 配置针对“单页模式”,新增了单页模式相关配置。目前这个配置里只有一个navigationBarFit属性: [图片] navigationBarFit属性主要是针对原页面设置了自定义导航栏的情况。也就是原页面的json文件中配置了这个属性: { // ... "navigationStyle":"custom" // ... } 给大家看一下普通导航栏和自定义导航栏的区别,下图是普通导航栏页面: [图片] 下图是自定义导航栏页面,我们在原本的导航栏位置使用了banner: [图片] [代码]"navigationStyle":"custom"[代码]这个设置在“单页模式”下也会生效。前文微信官方对“单页模式”的描述有说到“顶部导航栏与底部操作栏均不支持自定义样式”。如果我们在原页面设置了自定义导航栏。那么“单页模式”样式就会变成这样: [图片] 通过设置navigationBarFit为 [代码]squeezed[代码]就可以解决这个问题: { // ... "singlePage": { "navigationBarFit": "squeezed" } // ... } 设置后的样式: [图片] 开发 接下来介绍如何在小程序中实现这个功能。 第一步在需要转发朋友圈的页面中注册用户点击右上角转发功能,这是实现转发朋友圈功能的必要满足条件。 onShareAppMessage: function () { return { title: '转发标题', path: '/pages/home/index', imageUrl: '自定义图片路径' } } 第二步注册分享朋友圈功能(从基础库 [代码]2.11.3[代码] 开始支持): onShareTimeline: function () { return { title: '转发标题', query: 'from=pyq', imageUrl: '自定义图片路径' } } 注意,这里有个问题,分享朋友圈功能不支持自定义页面路径,意味着只能转发当前页面。如果当前页面存在较多“单页模式”限制功能,就可能让我们的页面不能按预期展示。 当页面存在限制功能时,我们存在两个方案,第一个方案,针对“单页模式”做改动,不调用那些限制的功能。第二个方案,另外写一个针对“单页模式”的页面。 这两种方案都需要能判断当前是否正处在小程序“单页模式”。 我们通过判断场景值(场景值用来描述用户进入小程序的路径)是否等于 1154 来判断当前是否正处在小程序“单页模式”。场景值可以在 [代码]App[代码] 的 [代码]onLaunch[代码] 获取。 // app.js App({ // ... onLaunch(options) { const { scene } = options; this.isSinglePage = scene === 1154; } // ... }) 我们将是否正处在“单页模式”的Boolean值放入App实例,方便全局拿到值。 接下来说说两种方案。 第一种方案,在“单页模式”不调用那些限制功能(这是一种不推荐的方案,代码耦合性太强)。举个例子: const app = getApp(); Page({ // ... onLoad() { if (!app.isSinglePage) { wx.login({ // ... }) } } // ... }) 第二种方案,针对“单页模式”另写一个页面。因为分享朋友圈功能并不支持自定义页面路径,我们只能另外写一个组件来作为“单页模式”的内容承载。 将isSinglePage放入页面的初始数据,方便在wxml中拿到: // pages/home/index.js const app = getApp(); Page({ data: { isSinglePage: app.isSinglePage, } // ... }) home-single-page就是分享到朋友圈的内容承载组件: // pages/home/index.json { // ... "usingComponents": { "home-single-page": "components/home-single-page/index" }, } 当“单页模式”时,我们展示 [代码]home-single-page[代码]组件,否则就展示普通页面内容: // pages/home/index.wxml 样式上虽然搞定了,但是在原本的生命周期中可能会调用一些限制功能,或者跑一些其它“单页模式”用不上的内容。我们得停止原本生命周期函数调用。 建议对传入Page的对象进行统一处理,当“单页模式”时,不调用原本的生命周期: // pages/home/index.js import ExtendPage from 'common/extend-page/index' const app = getApp(); ExtendPage({ data: { isSinglePage: app.isSinglePage, } // ... }) ExtendPage函数针对“单页模式”进行统一处理: // common/extend-page/index.js const app = getApp(); const PAGE_LIFE = [ 'onLoad', 'onReady', 'onShow', 'onHide', 'onError', 'onUnload', 'onResize', 'onPullDownRefresh', 'onReachBottom', 'onPageScroll' ]; export default function(option) { let newOption = {}; if(app.isSinglePage) { newOption = PAGE_LIFE.reduce((res, lifeKey) => { if (option[lifeKey]) { res[lifeKey] = undefined; } return res; }, {}) } return Page({ ...option, ...newOption, }); } 在“单页模式”下,我们将原本的生命周期都停止了调用。这样就能很好的将“单页模式”下的页面和普通页面进行解耦。 如果”单页模式“页面比较复杂,需要使用生命周期。我们也可以添加 [代码]singlePageLife[代码]属性,当处在“单页模式”下,就调用 [代码]singlePageLife[代码]内的生命周期: // pages/home/index.js import ExtendPage from 'common/extend-page/index' const app = getApp(); ExtendPage({ data: { isSinglePage: app.isSinglePage, }, singlePageLife: { onLoad() { // ... }, } // ... }) // common/extend-page/index.js const app = getApp(); const PAGE_LIFE = [ 'onLoad', 'onReady', 'onShow', 'onHide', 'onError', 'onUnload', 'onResize', 'onPullDownRefresh', 'onReachBottom', 'onPageScroll' ]; export default function(option) { let newOption = {}; if(app.isSinglePage) { const { singlePageLife } = option; newOption = PAGE_LIFE.reduce((res, lifeKey) => { if (singlePageLife[lifeKey]) { res[lifeKey] = singlePageLife[lifeKey]; } else if(option[lifeKey]) { res[lifeKey] = undefined; } return res; }, {}) } return Page({ ...option, ...newOption, }); } 文章如有疏漏、错误欢迎批评指正。
2020-10-21 - 隐私协议相关接口实际使用方式
隐私协议相关接口官方在回滚了多天之后终于滚回来并且提供了demo。 我这边实际研究demo后总结了实际的三种使用方法 首先要在设计到隐私协议相关功能的页面中增加隐私协议弹窗,wxml代码如下,wxss可以参考官方的demo [代码]<view wx:if="{{showPrivacy}}" class="privacy"> <view class="popup"> <view>隐私弹窗内容....</view> <view bindtap="openPrivacyAgreement">点击查看隐私协议</view> <button id="disagreeBtn" bindtap="disagreePrivacy">不同意</button> <button id="agreeBtn" open-type="agreePrivacyAuthorization" bindagreeprivacyauthorization="agreePrivacy">同意</button> </view> </view> [代码] 1、全局控制 页面加载时自动弹窗,同意后可以使用对应功能,不同意退出页面或者隐藏相关功能。 这种情况下,需要在onLoad里使用wx.getPrivacySetting获取隐私授权情况,没授权时弹出隐私弹窗。完整代码如下 [代码]Page({ data: { showPrivacy: false // 控制隐私弹窗是否展示 }, openPrivacyAgreement() { // 查看隐私协议 wx.openPrivacyContract() }, disagreePrivacy() { // 关闭隐私弹窗 this.setData({ showPrivacy: false }) // 这里是不同意隐私协议的后续操作,比如退出页面、隐藏相关功能等 }, agreePrivacy() { // 关闭隐私弹窗 this.setData({ showPrivacy: false }) // 这里是同意隐私协议的后续操作,比如展示被隐藏的相关功能 }, onLoad() { if (wx.getPrivacySetting) { wx.getPrivacySetting({ success: res => { if (res.needAuthorization) { // 打开隐私弹窗 this.setData({ showPrivacy: true }) } else { // 用户已经同意过隐私协议,直接执行同意隐私协议的后续操作,比如展示被隐藏的相关功能 } } }) } } }) [代码] 2、按需使用 在使用到隐私接口时弹出隐私弹窗,同时隐私接口的流程会被挂起,用户同意后会继续执行,不同意则中止执行。 这种情况下,需要在onLoad里定义好监听隐私接口时的授权事件,也就是wx.onNeedPrivacyAuthorization,然后在用户点击同意或者不同意后调用回调接口 resolve 进行上报,完整代码如下 [代码]Page({ data: { showPrivacy: false // 控制隐私弹窗是否展示 }, openPrivacyAgreement() { // 查看隐私协议 wx.openPrivacyContract() }, disagreePrivacy() { // 关闭隐私弹窗 this.setData({ showPrivacy: false }) // 上报用户不同意隐私协议,隐私接口操作会被自动中止 this.resolvePrivacyAuthorization({ buttonId: 'disagreeBtn', event: 'disagree' }) }, agreePrivacy() { // 关闭隐私弹窗 this.setData({ showPrivacy: false }) // 上报用户同意隐私协议,隐私接口操作会被自动继续执行 this.resolvePrivacyAuthorization({ buttonId: 'agreeBtn', event: 'agree' }) }, onLoad() { if (wx.onNeedPrivacyAuthorization) { wx.onNeedPrivacyAuthorization(resolve => { // 打开隐私弹窗 this.setData({ showPrivacy: true, }) // 定义上报方法 this.resolvePrivacyAuthorization = resolve }) } } }) [代码] 3、全局控制和按需使用结合使用 这种情况是上面两种方式的结合,页面加载时弹出隐私弹窗,不管用户同意还是不同意都不需要做其他操作。然后用户在用到隐私接口时,根据用户是否同意再按需决定是否再次弹窗。 这种情况下,需要在onLoad里使用wx.getPrivacySetting获取隐私授权情况,没授权时弹出隐私弹窗。同时定义好监听隐私接口时的授权事件,也就是wx.onNeedPrivacyAuthorization,然后在用户点击同意或者不同意后调用回调接口 resolve 进行上报,完整代码如下 [代码]let pageOnload = true // 是否页面加载时弹窗 Page({ data: { showPrivacy: false // 控制隐私弹窗是否展示 }, openPrivacyAgreement() { // 查看隐私协议 wx.openPrivacyContract() }, disagreePrivacy() { // 关闭隐私弹窗 this.setData({ showPrivacy: false }) if (!pageOnload) { // 上报用户不同意隐私协议,隐私接口操作会被自动中止 this.resolvePrivacyAuthorization({ buttonId: 'disagreeBtn', event: 'disagree' }) } else { pageOnload = false } }, agreePrivacy() { // 关闭隐私弹窗 this.setData({ showPrivacy: false }) if (!pageOnload) { // 上报用户同意隐私协议,隐私接口操作会被自动继续执行 this.resolvePrivacyAuthorization({ buttonId: 'agreeBtn', event: 'agree' }) } else { pageOnload = false } }, onLoad() { if (wx.getPrivacySetting) { wx.getPrivacySetting({ success: res => { if (res.needAuthorization) { // 打开隐私弹窗 this.setData({ showPrivacy: true }) } } }) } if (wx.onNeedPrivacyAuthorization) { wx.onNeedPrivacyAuthorization(resolve => { // 打开隐私弹窗 this.setData({ showPrivacy: true, }) // 定义上报方法 this.resolvePrivacyAuthorization = resolve }) } } }) [代码] 4、open-type按钮藕合使用 这个是目前最简单的方式,按照官方文档,在用open-type方式调用隐私功能的按钮的open-type里加上[代码]|agreePrivacyAuthorization[代码]就可以了,js部分代码基本不用做任何修改,示例如下 [代码]<button open-type="getPhoneNumber|agreePrivacyAuthorization" bindgetphonenumber="handleGetPhoneNumber">同意隐私协议并授权手机号</button> [代码] 这种方式,不需要上报同意事件,不需要设计隐私协议弹窗,不需要“不同意”按钮。当然,我建议在现有页面上增加一个查看隐私协议的入口。
2023-09-08 - 适配新版获取昵称头像、隐私保护协议接口
蛮记录一下,虽然官方新版获取昵称、头像的api上线已久 目前的做法是封装了一个auth.js用来专门获取用户信息,想要实现的效果是 1、若用户没有授权过昵称、头像,则弹出授权弹窗 2、若用户有授权过,则直接返回昵称头像等其他用户信息 3、允许更新用户信息,也就是不管之前有没有授权过昵称头像,都会再次弹出授权弹窗 4、支持旧版api,主要是考虑到部分用户的微信客户端可能长期未更新,不支持新版api 另外也适配了最近的隐私接口功能。因为获取昵称组件是一个input,不会触发wx.onNeedPrivacyAuthorization,所以这里是给input加了一个catch:touchstart="requirePrivacyAuthorize",在requirePrivacyAuthorize里面调用wx.requirePrivacyAuthorize,从而触发wx.onNeedPrivacyAuthorization。具体代码可以看下improveUserProfile.wxml、improveUserProfile.js 主要代码如下,核心方法 authProcess: auth.js const api = require('/api.js'); const file = require("/file.js"); /** * 保存或者更新用户信息到云端 * @param userInfo * @returns {Promise<unknown>} * @private */ const _saveOrUpdateUserInfo = function (userInfo) { return new Promise((resolve, reject) => { api.router.callCloudUserCenterFunction("UserInfoHandler/saveOrUpdateUserInfo", userInfo, res => { console.log("保存用户信息到云端完毕=>", JSON.stringify(res)); resolve(res); }, res => { console.error("保存用户信息到云端失败=>", res); reject(); } ); }); }; /** * 发起后端请求前先获取用户信息 * 使用新版获取用户信息接口 * @param app * @param basePage * @param update 等于true时表示更新用户信息 * @returns {Promise<any>} */ const authProcess = function (app, basePage, update) { return new Promise(function (resolve, reject) { if (update) { console.log("更新用户信息,重新授权"); _getUserProfile(app, basePage).then(res => resolve(res)).catch(res => reject(res)); } else if (!app.globalData.userInfo.authEver) { console.log("没有用户授权信息,开始授权"); _getUserProfile(app, basePage).then(res => resolve(res)).catch(res => reject(res)); } else { console.log("用户有授权过,无需重新授权"); resolve(app.globalData.userInfo); } }); }; /** * 使用新版接口获取用户信息 * 兼容新版头像昵称填写能力 * @param app * @param basePage * @private */ const _getUserProfile = function (app, basePage) { return new Promise(function (resolve, reject) { const supportChooseAvatar = wx.canIUse('button.open-type.chooseAvatar'); if (supportChooseAvatar) { console.warn("支持头像昵称填写能力"); const improveUserProfile = basePage.improveUserProfile || basePage.selectComponent('#improveUserProfile'); const toast = basePage.toast || basePage.selectComponent('#toast'); if (!improveUserProfile) { console.error("完善用户资料组件缺失,无法继续"); reject("完善用户资料组件缺失"); } else if (!toast) { console.error("toast组件缺失,无法继续"); reject("toast组件缺失"); } else { improveUserProfile.show(({nickName, avatarUrl}) => { const {openid, avatarFileId: oldAvatarFileId} = app.globalData.userInfo; const targetPath = _getHeaderPath(openid); console.log("上传获取到的临时头像:", avatarUrl); toast.showLoadingToast({text: "开始上传头像"}); file.uploadCloudFile(targetPath, avatarUrl).then(fileId => { file.getTempFileURL(fileId).then(fileUrl => { toast.hideLoadingToast(); _getUserProfileCallback(app, basePage, { userInfo: { nickName, avatarUrl: fileUrl, avatarFileId: fileId } }).then(userInfo => resolve(userInfo)); if (oldAvatarFileId) { console.log("删除旧的云储存头像文件", oldAvatarFileId); file.deleteCloudFile([oldAvatarFileId]).then(); } }).catch(res => { toast.hideLoadingToast(); toast.showToast({text: "保存头像链接失败"}); reject("保存头像链接失败"); }) }).catch(res => { toast.hideLoadingToast(); toast.showToast({text: "头像上传失败"}); reject("头像上传失败"); }); file.onUploadProgressUpdate(progress => { toast.showLoadingToast({text: "上传了" + progress + "%"}); }); }, res => { console.log("用户拒绝授权信息:", res); reject(res); }); //reject("需要使用新api授权用户信息"); } } else { console.warn("不支持头像昵称填写能力"); api.getUserProfile( res => { _getUserProfileCallback(app, basePage, res).then(userInfo => resolve(userInfo)); }, res => { console.error("获取用户信息失败:", res); reject(res); } ); //reject("需要使用旧api授权用户信息"); } }); }; /** * 获取到用户信息的后续逻辑 * @param app * @param basePage * @param result * @returns {Promise<unknown>} * @private */ const _getUserProfileCallback = (app, basePage, result) => { return new Promise(function (resolve, reject) { console.log("获取用户信息结果:", JSON.stringify(result)); let {userInfo} = result; //authEver=true时后端做更新操作,false时新增操作 const {authEver} = app.globalData.userInfo || {}; userInfo = Object.assign(app.globalData.userInfo || {}, userInfo, {ready: true, authEver: true}); basePage.setData({ userInfo, }, () => { _saveOrUpdateUserInfo(Object.assign({}, userInfo, {authEver})).then(res => { console.log("用户信息保存完毕,更新后的用户信息:", res); const result = res.result; if (result.success) { userInfo = Object.assign(app.globalData.userInfo || {}, userInfo); basePage.setData({ userInfo, }); } resolve(userInfo); }).catch(res => reject()); }); }); } /** * 获取头像地址 * 不同项目可以自行调整 * @param openid * @returns {string} * @private */ const _getHeaderPath = (openid) => { return "images/" + openid + "/avatarUrl_" + new Date().getTime() + ".webp"; } module.exports = { setUserInfo2Data, authProcess }; 昵称、头像授权弹窗自定义组件 improveUserProfile.wxml <view class="container" wx:if="{{show}}"> <view class="cover {{showCoverAnimation?'cover-fade-in':''}}" catch:touchmove="return"></view> <view class="improve-box {{showBoxAnimation?'slade-in':''}} {{device.isPhoneX? 'phx_68':''}}" catch:touchmove="return"> <view class="title flex-start-horizontal"> <view class="logo"> <image class="icon" src="xxxx"></image> </view> <view class="mini-name">恋爱小清单 申请</view> </view> <view class="tips"> <view class="tip">获取你的昵称、头像</view> <view class="use-for">用于个人中心、共享清单、恋人圈以及分享时展示</view> </view> <form catchsubmit="saveUserInfo"> <view class="info-item flex-center"> <view class="label">头像</view> <view class="input-content flex-full more"> <button class="reset-btn flex-column-left" style="height:100%" open-type="chooseAvatar" bind:chooseavatar="onChooseAvatar"> <view class="choose-avatar before" wx:if="{{!avatarUrl}}"> <image class="icon" src="/images/me/choose-avatar.png"></image> </view> <image class="choose-avatar after" src="{{avatarUrl}}" wx:else></image> </button> </view> </view> <view class="info-item flex-center no-border"> <view class="label">昵称</view> <input class="input-content flex-full more" model:value="{{nickName}}" type="nickname" placeholder="输入昵称" placeholder-class="edit-placeholder" focus="{{nickNameFocus}}" always-embed="{{false}}" adjust-position="{{true}}" hold-keyboard="{{false}}" name="nickName" catch:touchstart="requirePrivacyAuthorize"></input> </view> <view class="buttons flex-center"> <button class="cancel reset-btn" form-type="reset" bindtap="close">拒绝</button> <button class="save reset-btn" form-type="submit">保存</button> </view> <!--隐藏参数区域开始--> <input disabled style="display:none;" name="avatarUrl" value="{{avatarUrl}}"></input> <!--隐藏参数区域结束--> </form> </view> </view> <!--toast--> <toast id="toast"></toast> improveUserProfile.wxss @import "/app.wxss"; .cover{ background-color: #111; opacity: 0; position: fixed; left: 0; top: 0; right: 0; bottom: 0; z-index: 5000; transition: opacity .3s; } .improve-box{ position: fixed; left: 0rpx; right: 0rpx; bottom: 0rpx; z-index: 5100; 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; } .improve-box .title{ font-size: 32rpx; font-weight: 500; text-align: center; margin-bottom: 40rpx; color: #161616; } .improve-box .title .icon{ width: 46rpx; height: 46rpx; margin-right: 8rpx; vertical-align: bottom; border-radius: 50%; } .improve-box .title .mini-name{ /*margin-bottom: 2rpx;*/ } .improve-box .tips{ margin-bottom: 20rpx; } .improve-box .tips .tip{ color: #121212; font-size: 34rpx; font-weight: 500; margin-bottom: 12rpx; } .improve-box .tips .use-for{ color: #606266; font-size: 28rpx; margin-bottom: 20rpx; } .improve-box .info-item{ /*margin-bottom: 30rpx;*/ border-bottom: 1rpx solid #f0f0f0; } .improve-box .label{ margin-right: 40rpx; font-size: 32rpx; color: #404040; padding: 30rpx 0 30rpx; } .improve-box .choose-avatar.after{ width: 48rpx; height: 48rpx; display: block; border-radius: 10rpx; } .improve-box .choose-avatar.before{ background-color: #f0f0f0; width: 48rpx; height: 48rpx; display: block; border-radius: 10rpx; padding: 8rpx; box-sizing: border-box; } .improve-box .choose-avatar.before .icon{ width: 100%; height: 100%; display: block; } .improve-box .input-content{ width: 100%; font-size: 32rpx; min-height: 80rpx; height: 80rpx; line-height: 80rpx; border-radius: 10rpx; padding-left: 20rpx; box-sizing: border-box; /*border-bottom: 1rpx solid #dcdee1;*/ position: relative; padding-right: 30rpx; } .edit-placeholder{ color: #c7c7cc; font-size: 32rpx; } .improve-box .buttons{ margin-bottom: 40rpx; margin-top: 50rpx; text-align: center; font-size: 34rpx; font-weight: 550; } .improve-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; } .improve-box .buttons .cancel{ width: 220rpx !important; height: 90rpx; line-height: 90rpx; border-radius: 14rpx; color: #07c160; background-color: #F2F2F2; } .more:after{ content: " "; transform: rotate(45deg); height: 14rpx; width: 14rpx; border-width: 2rpx 2rpx 0 0; border-color: #b2b2b2; border-style: solid; position: absolute; margin-top: -8rpx; right: 4rpx; top: 50%; transition: transform .3s; } improveUserProfile.js //获取应用实例 const app = getApp(); const api = require('../../utils/api.js'); const tabbar = require('../../utils/tabbar.js'); Component({ /** * 组件的属性列表 */ properties: {}, /** * 组件的初始数据 */ data: { show: false, showCoverAnimation: false,//显示类别选择窗口动画 showBoxAnimation: false,//显示类别选择窗口动画 avatarUrl: "", nickNameFocus: false, }, attached() { this.toast = this.selectComponent('#toast'); }, observers: { 'show': function (show) { if (!show) { console.log("获取用户昵称头像弹窗关闭,昵称输入框状态复位"); this.setData({ nickNameFocus: false }) } } }, /** * 组件的方法列表 */ methods: { show(successCallback, failCallback) { const device = app.getSystemInfo(); this.setData({ show: true, device, successCallback, failCallback }, () => { this.setData({ showCoverAnimation: true, showBoxAnimation: true }); }) tabbar.hideTab(this); }, hide() { this.setData({ showCoverAnimation: false, showBoxAnimation: false }, () => { const that = this; setTimeout(function () { that.setData({ show: false }) tabbar.showTab(that); }, 300) }) }, close() { const {failCallback} = this.data; typeof failCallback == 'function' && failCallback("用户拒绝授权"); this.hide(); }, onChooseAvatar(e) { console.log("选择头像:", e); const {avatarUrl} = e.detail; this.setData({ avatarUrl }) }, /** * 模拟隐私接口调用,并触发隐私弹窗逻辑 */ requirePrivacyAuthorize() { if (wx.requirePrivacyAuthorize) { console.warn("当前基础库支持api wx.requirePrivacyAuthorize"); wx.requirePrivacyAuthorize({ success: res => { console.log('用户同意了隐私协议 或 无需用户同意隐私协议'); //用户同意隐私协议后给昵称输入框聚焦 this.setData({ nickNameFocus: true }) }, fail: res => { console.log('用户拒绝了隐私协议'); } }) } else { console.warn("当前基础库不支持api wx.requirePrivacyAuthorize"); this.setData({ nickNameFocus: true }) } }, saveUserInfo(e) { console.log("保存用户资料:", e); const {nickName, avatarUrl} = e.detail.value; if (!avatarUrl) { this.toast.showToast({text: "请选择头像"}); return; } if (!nickName) { this.toast.showToast({text: "请输入昵称"}); return; } const {successCallback} = this.data; typeof successCallback == 'function' && successCallback({nickName, avatarUrl}); this.hide(); } } }); 弹窗效果 [图片]
2023-08-30 - 小程序隐私保护授权弹窗组件(Taro和原生版本)
【原生】https://github.com/hsuna/miniprogram-privacy-popup 【Taro】https://github.com/hsuna/taro-privacy-popup 隐私政策弹窗封装: 项目引入privacyPopup组件,在所有使用到隐私授权的页面引入privacyPopup即可,组件内部注册了onNeedPrivacyAuthorization,且通过队列的方式统一管理隐私授权的回调,无需开发者在页面做其他特殊的处理。. 在 page.wxml 中使用组件 针对Input的封装组件 由于Input无法触发onNeedPrivacyAuthorization,使用插槽的形式,通过getPrivacySetting获取用户隐私状态,在用户未同意的情况下,通过catch:touchstart拦截用户点击交互,使用wx.requirePrivacyAuthorize触发onNeedPrivacyAuthorization事件。. 在 page.wxml 中使用组件,并包裹触点区域
2023-09-03 - 隐私协议授权的一种解决思路
现状 登录页使用了获取手机号接口,登录页为单独的页面(非弹窗),所有登录需求都会跳转到此页。 [图片] 分析 小程序提供两种接入方式:一种是主动查询授权状态,如果状态为未授权则进入授权流程,已授权则无需处理;另一种是被动监听何时需要授权,触发事件后进入授权流程,未触发则表示已授权。在登录的流程中,主动查询就是先发起授权后点击登录按钮,被动监听就是先点击登录按钮后发起授权。个人感觉,主动查询的方式先告知用户需要采集隐私数据,比较符合用户习惯,而被动监听的方式事前未告知用户需要授权,且打断了登录的流程,用户体验不佳,因此采取主动查询的方式。 官方的示例采用了弹窗授权的方式,但无论是在页面上覆盖一个模拟弹窗组件,还是使用page-container,控制弹窗都不能像VUE那样优雅,复用也不方便。鉴于登录页也是单独的页面,考虑到以后可能还有其它页面需要授权,因此授权页也采用单独的页面(PS.skyline的自定义路由可以很优雅地实现弹窗,但目前还没有适配,暂不考虑,之后对授权页进行改造即可)。 思路 在登录页的onLoad中主动查询授权状态,如果状态为未授权则调用授权接口跳转到授权页,授权页回调授权状态,如果授权失败则登录也失败。 [图片] 实现 [代码]// 封装的登录接口 function requestLogin() { return new Promise(async (resolve, reject) => { wx.navigateTo({ url: '/pages/user/user-login/user-login', // 登录页 events: { login: (loginStatus) => { // 登录状态回调 if (loginStatus) { // 成功 resolve() } else { // 失败 reject(new Error('login fail')) } } }, fail: () => reject(new Error('navigate fail')) }) }) } // 封装的授权接口 function requestProvicy() { return new Promise(async (resolve, reject) => { wx.getPrivacySetting({ success: (res) => { if (res.needAuthorization) { // 需要授权 wx.navigateTo({ url: '/pages/user/user-login/user-login', // 登录页 events: { privacy: (privacyStatus) => { // 授权状态回调 if (loginStatus) { // 成功 resolve() } else { // 失败 reject(new Error('privacy fail')) } } }, fail: () => reject(new Error('navigate fail')) }) } else { // 无需授权 resolve() } }, fail: () => reject(new Error('get privacy fail')) }) }) } // 第一步:调用封装的登录接口 // 需要登录才能进入的页面 Page({ onLoad: async function(options) { try { await wx.$auth.requestLogin() // 登录失败抛出错误 } catch { return wx.$router.back() // 返回上一页 } // 业务代码 } }) // 第二步:获取手机号登录 // 登录页 <!-- wxml > <button open-type="getPhoneNumber" bindgetphonenumber="handleGetPhoneNumber">获取手机号</button> Page({ handleGetPhoneNumber: async function(e) { const code = e.detail.code // 手机号code // 调用自己后台的登录接口 this._loginStatus = true // 登录成功状态,在onUnload回调 wx.$router.back() // 返回上一页 }, onLoad: async function() { try { await wx.$auth.requestProvicy() // 授权失败抛出错误 } catch { return wx.$router.back() // 返回上一页 } } onUnload: function() { const eventChannel = this.getOpenerEventChannel() if (!eventChannel) return eventChannel.emit('login', !!this._loginStatus) // 回调登录结果 } }) // 第三步:授权 // 授权页 <!-- wxml > <button id="agree-btn" open-type="agreePrivacyAuthorization" bindagreeprivacyauthorization="handleAgreePrivacyAuthorization">我已阅读并同意隐私协议</button> Page({ handleAgreePrivacyAuthorization: async function() { this._privacyStatus = true // 授权成功状态,在onUnload回调 wx.$router.back() // 返回上一页 }, onUnload: function() { const eventChannel = this.getOpenerEventChannel() if (!eventChannel) return eventChannel.emit('privacy', !!this._privacyStatus) // 回调登录结果 } }) [代码] 最终效果 未登录的状态下打开一个需要登录才能使用的页面 [图片] 可以进入小程序实际体验一下,欢迎各位一起讨论 [图片]
2023-09-01