- 『隐私设置』小程序如何设置隐私保护权限接口,防止线上登录失败,点击按钮没反应
很多用户突然遇到线上版本报错、登录失败、点击没有反应等等情况,其实都是由于最近隐私协议生效了的原因,需要按照以下步骤自行设置更新一下隐私协议,然后提交版本重新发版。 一、设置《小程序用户隐私保护指引》 开发者需登录小程序后台https://mp.weixin.qq.com/,在左侧菜单底部“设置”里找到 “服务内容声明“,在这里更新《用户隐私保护指引》。 注意:需要把用到的隐私接口都加上,提交后是需要审核的,通过才可以测试用,大约2-5个小时左右就会有结果,隐私接口可参考: https://developers.weixin.qq.com/miniprogram/dev/framework/user-privacy/miniprogram-intro.html [图片] [图片] 二、填写《小程序用户隐私保护指引》 [图片] 注意:上述第一条里需要把用到的隐私接口都加上才管用,点击没反应用不了都是因为缺了 提交后是需要审核的,大约2-5个小时左右就会有结果,隐私接口可参考: https://developers.weixin.qq.com/miniprogram/dev/framework/user-privacy/miniprogram-intro.html 审核通过后也不是立马生效的,会根据用户的使用情况慢慢覆盖所有用户,所以通过后耐心等待30分钟到1个小时之后再尝试,如果还不行,大概率配置的接口拉下了。 还有一点很重要,下边最后一条: 三、如要进行代码提审,需要勾选 “采集用户隐私” [图片] 很多人遇到隐私接口审核通过了,还是不能用的情况,一是需要等待1个小时左右覆盖到你才可以用,二是以下介绍的: 如果之前发版没有勾选这个《采集用户隐私》,即使隐私协议接口审核通过了,也是不能正常调用的,这时候需要你重新提交代码审核发布一版了,记得勾选这个采集用户隐私。 最好是隐私接口审核通过后,用开发者工具自己多测测,没问题再提交版本审核,开发者工具里“清除授权数据”就会重置隐私接口权限。
2023-11-04 - 为什么图片链接可正常访问但image组件加载不出来图片?
因为 image 控件的图片拉取本质上是 web 上的 backgroundImage,很多时候是由于图片不规范(content-type / length / 是否302跳转等 )导致拉取不成功,最终表现为加载不出图片。关于这一块我们在持续优化中
2021-12-17 - 小程序性能优化实践
小程序性能优化课程基于实际开发场景,由资深开发者分享小程序性能优化的各项能力及应用实践,提升小程序性能表现,满足用户体验。
10-09 - 微信小程序使用科大讯飞语音评测,保姆级教程!
最近微信小程序项目中,需要添加语音评测功能,就选用了科大讯飞的语音评测流式版接口,但在使用过程中,遇到了很多问题,再网上搜资料,搜了好多,也没直接能用的,好在后来参考了许多资料后,终于调试成功了,接下来,跟大家分享一下我是怎么处理的。 1.第一步,准备所用到的工具,下载官方jsdemo,将 base64js 文件复制到自己的小程序项目中,用npm安装crypto-js xmldom这2个工具 然后,将工具导入到页面中 const CryptoJS = require('crypto-js') const Base64 = require('../../tools/base64js').Base64; var DOMParser = require('xmldom').DOMParser; 2.第二步,初始化用到的变量,定义用到的关键函数 const APPID = '替换成你自己的' const API_SECRET = '替换成你自己的' const API_KEY = '替换成你自己的' let audioData = [] //存储音频流的数组 let socketTask = null //小程序的socketTask let handlerInterval = null // 定时器,用来定时发送数据流 function getWebSocketUrl() {//生成socket使用的url return new Promise((resolve, reject) => { var url = 'wss://ise-api.xfyun.cn/v2/open-ise' var host = 'ise-api.xfyun.cn' var apiKey = API_KEY var apiSecret = API_SECRET var date = new Date().toGMTString() var algorithm = 'hmac-sha256' var headers = 'host date request-line' var signatureOrigin = `host: ${host}\ndate: ${date}\nGET /v2/open-ise HTTP/1.1` var signatureSha = CryptoJS.HmacSHA256(signatureOrigin, apiSecret) var signature = CryptoJS.enc.Base64.stringify(signatureSha) var authorizationOrigin = `api_key="${apiKey}", algorithm="${algorithm}", headers="${headers}", signature="${signature}"` var authorization =Base64.encode(authorizationOrigin) url = `${url}?authorization=${authorization}&date=${date}&host=${host}` resolve(url) }) } 3.开始录音 //开始录音 startVoiceRecord(){ let that = this that.setData({recordState:'recording'}) recorderManager.onStart(() => { console.log('recorder start') }) recorderManager.onPause(() => { console.log('recorder pause') }) recorderManager.onStop((res) => { console.log('recorder stop', res) const { tempFilePath } = res that.startUpRecord()//录音完成,准备调用讯飞接口 }) recorderManager.onFrameRecorded((res) => { const { frameBuffer } = res console.log('frameBuffer.byteLength', frameBuffer.byteLength) let u8Arr = new Uint8Array(frameBuffer) audioData.push(u8Arr) //将每一帧的数据取出,放到audioData中,准备使用 }) const options = { duration: 180000, sampleRate: 16000, numberOfChannels: 1, encodeBitRate: 44100, frameSize: 2, format: 'pcm', } recorderManager.start(options) }, 4. 开始socket连接,准备上传数据并处理 startUpRecord(){ let that = this getWebSocketUrl().then(( url)=>{ let newURL = encodeURI(url) socketTask = wx.connectSocket({ url: newURL, }) socketTask.onOpen(()=>{ console.log('打开了socket') that.webSocketSend() }) socketTask.onMessage((e)=>{ // result 在这里做信息处理 console.log('收到了结果:',e) that.result(e.data) }) socketTask.onError((err)=>{ //结束录音 console.log('socket 出错:',err) }) socketTask.onClose(()=>{ // 结束录音 console.log('socket 关闭:') }) }) }, webSocketSend() { console.log('开始发送数据',audioData) let that = this let audioDataUp = audioData.splice(0, 1) var params = { common: { app_id:APPID, }, business: { category: 'read_sentence', // read_syllable/单字朗读,汉语专有 read_word/词语朗读 read_sentence/句子朗读 https://www.xfyun.cn/doc/Ise/IseAPI.html#%E6%8E%A5%E5%8F%A3%E8%B0%83%E7%94%A8%E6%B5%81%E7%A8%8B rstcd: 'utf8', group: 'pupil', sub: 'ise', ent: 'cn_vip', tte: 'utf-8', cmd: 'ssb', auf: 'audio/L16;rate=16000', aus: 1, aue: 'raw', text: '\uFEFF' + '今天天气怎么样?' }, data: { status: 0, encoding: 'raw', data_type: 1, data: that.toBase64(audioDataUp[0]), }, } console.log(JSON.stringify(params)) socketTask.send({data: JSON.stringify(params)}) handlerInterval = setInterval(() => { // websocket未连接 if (!socketTask) { clearInterval(handlerInterval) return } // 最后一帧 if (audioData.length === 0) { console.log('数据发送完毕') socketTask.send( {data: JSON.stringify({ business: { cmd: 'auw', aus: 4, aue: 'raw' }, data: { status: 2, encoding: 'raw', data_type: 1, data: '', }, })} ) audioData = [] clearInterval(handlerInterval) return false } audioDataUp = audioData.splice(0, 1) // 中间帧 console.log('audioDataUp:',audioDataUp[0]) socketTask.send( { data: JSON.stringify({ business: { cmd: 'auw', aus: 2, aue: 'raw' }, data: { status: 1, encoding: 'raw', data_type: 1, data: that.toBase64(audioDataUp[0]), }, })} ) }, 40) }, result(resultData) { // 识别结束 let jsonData = JSON.parse(resultData) if (jsonData.data && jsonData.data.data) { let data = Base64.decode(jsonData.data.data) const doc=new DOMParser().parseFromString(data,'text/xml'); let sentence = doc.getElementsByTagName('read_sentence')[1] let accuracy_score = sentence.getAttribute('accuracy_score') let emotion_score = sentence.getAttribute('emotion_score') let fluency_score = sentence.getAttribute('fluency_score') let total_score = sentence.getAttribute('total_score') let integrity_score = sentence.getAttribute('integrity_score') let phone_score = sentence.getAttribute('phone_score') let tone_score = sentence.getAttribute('tone_score') let is_rejected = sentence.getAttribute('is_rejected') console.log('parseRes:',accuracy_score,emotion_score,fluency_score,total_score) //评测结果在这里,就出来了,然后就可以拿评测数据去使用了 } if (jsonData.code === 0 && jsonData.data.status === 2) { // 在这里结束socket socketTask.close() } if (jsonData.code !== 0) { socketTask.close() console.log(`${jsonData.code}:${jsonData.message}`) } }, 到这里,整个流程就完了,祝愿大家都能一次调用成功,有什么问题的话,咱们再讨论!
2023-06-13 - RecorderManager录音结束后的文件内容会全部丢失?
经用户反馈发现:录音完成后,播放回显的录音文件发现没声音 时长为 0 录音逻辑是: 用户进入页面 点击录音 录音完成后点击停止 通过 RecorderManager.onStop 获取 录音文件临时地址 通过 wx.uploadFile 将临时文件上传至业务服务器 业务服务器响应录音文件 cdn 地址 页面回显录音文件供用户播放 通过埋点我们可查到,用户在点击停止时通常已录音 10s 以上,并且通过 onStop 回调参数的 duration 也可以印证 但是,将文件上传至服务器后,对服务器上的文件进行播放后发现, 没有任何声音,并且通过系统对文件信息查看发现时长确实为 0 小程序端开始录音相关参数 [代码] this[代码][代码].recorderManager.start({[代码][代码] [代码][代码]// 3 分钟 实测总时长会少大约 60ms 导致最终时长为 2‘59“ 因此加 1s[代码][代码] [代码][代码]duration: 3 * 60 * 1000 + 1000,[代码][代码] [代码][代码]sampleRate: 16000,[代码][代码] [代码][代码]encodeBitRate: 96000,[代码][代码] [代码][代码]});[代码] 下面是同一个用户,在一段时间内多次上传录音 发现服务端记录时长都为 0,但回调参数中的时长正常(ext.file 为onStop 回调参数, ext.res 为服务端返回文件信息) 经分析,该用户使用同一设备,录音时长在 19s - 68s 的 6 段录音文件大小全部为 24600 左右;完全不符合在相同采样率,码率,设备情况下 时长越长,文件越大的原则 现在需要官方帮忙解答,为什么会出现这种问题,以及作为开发者如何解决? ps.这个问题无法稳定复现,无法稳定复现,无法稳定复现,没有可复现的代码片段提供。请提出一些针对性的问题 [图片]该用户系统信息 systemInfo.SDKVersion 2.8.3 systemInfo.albumAuthorized true systemInfo.batteryLevel 71 systemInfo.bluetoothEnabled true systemInfo.brand iPhone systemInfo.cameraAuthorized true systemInfo.deviceOrientation portrait systemInfo.fontSizeSetting 17 systemInfo.language zh_CN systemInfo.locationAuthorized true systemInfo.locationEnabled true systemInfo.microphoneAuthorized true systemInfo.model iPhone 7 Plus<iPhone9,2> systemInfo.notificationAlertAuthorized true systemInfo.notificationAuthorized true systemInfo.notificationBadgeAuthorized true systemInfo.notificationSoundAuthorized true systemInfo.pixelRatio 3 systemInfo.platform ios systemInfo.safeArea.bottom 736 systemInfo.safeArea.height 716 systemInfo.safeArea.left 0 systemInfo.safeArea.right 414 systemInfo.safeArea.top 20 systemInfo.safeArea.width 414 systemInfo.screenHeight 736 systemInfo.screenWidth 414 systemInfo.statusBarHeight 20 systemInfo.system iOS 12.1.2 systemInfo.version 7.0.5 systemInfo.wifiEnabled true systemInfo.windowHeight 672 systemInfo.windowWidth 414
2019-09-23 - 小程序 RecorderManager 录制的录音文件存在快进、播放倍速问题
最近经用户反馈发现:录音完成后,播放回放的录音文件存在被快进、倍速播放的情况 通过日志和对比recorderManager录制源文件发现,通过recorderManager录制完成的源文件时长duration会出现比本地计时要短的情况,这种情况下播放录音内容被严重快进倍速播放,无法听清内容 以下为几个日志记录的本地计时和recorderManager返回的文件时长duration对比的例子,实际去听文件确实存在快进、被倍速播放的情况 [图片] [图片] [图片] 可以发现出现问题和录音时长没有明显关联,录音时间长短都有可能出现文件快进的情况 再通过查询近6天出现这些情况的用户的机型和微信信息: 用户设备集中在小米品牌 Xiaomi、Redmi 2种 6.10日更新:出现问题的用户设备不止小米品牌,新排查到存在vivo、华为机型 小程序基础库版本为2.24.1、2.24.2 这个问题无法稳定复现,无法稳定复现,无法稳定复现,没有可稳定复现的代码片段提供。 同一个用户,同一手机,在一段时间内多次上传录音不是必现该问题,也有可能是正常的。但确实会存在不可预计的某次录音文件出现倍速播放的问题 现在需要官方帮忙解答,为什么会出现这种问题,以及作为开发者如何解决? 提供一个出现该问题的用户的系统信息 Model:Mi 10 Pro brand:Xiaomi screen_dpi:2 Name:Android Version:12 SDKVersion:2.24.1 app:wechat fontSizeSetting:16 language:zh_CN platform:android screenHeight:835 screenWidth:393 statusBarHeight:33 version:8.0.22 windowHeight:756 windowWidth:393
2022-06-10 - wx.getLaunchOptionsSync()场景值有BUG?
如题,看了论坛很多帖子,很多人都碰到这个问题了的,但是目前还没有微信官方出来解释,能不能出来解释一下? 是需要对方的公众号后台配置什么?还是我们的小程序后台配置什么?好像不需要配置什么吧?因为对方的公众号文章,我们就可以拿到appid,就是自定义菜单栏拿不到appid。
2020-09-17 - 小程序奇技淫巧之 -- 日志能力
日志与反馈 前端开发在进行某个问题定位的时候,日志是很重要的。因为机器兼容性问题、环境问题等,我们常常无法复现用户的一些bug。而微信官方也提供了较完整的日志能力,我们一起来看一下。 用户反馈 小程序官方提供了用户反馈携带日志的能力,大概流程是: 开发中日志打印,使用日志管理器实例 LogManager。 用户在使用过程中,可以在小程序的 profile 页面(【右上角胶囊】-【关于xxxx】),点击【投诉与反馈】-【功能异常】(旧版本还需要勾选上传日志),则可以上传日志。 在小程序管理后台,【管理】-【反馈管理】,就可以查看上传的日志(还包括了很详细的用户和机型版本等信息)。 这个入口可能对于用户来说过于深入(是的,官方也发现这个问题了,所以后面有了实时日志),我们小程序也可以通过[代码]button[代码]组件,设置[代码]openType[代码]为[代码]feedback[代码],然后用户点击按钮就可以直接拉起意见反馈页面了。利用这个能力,我们可以监听用户截屏的操作,然后弹出浮层引导用户主动进行反馈。 [代码]<view class="dialog" wx:if="{{isFeedbackShow}}"> <view>是否遇到问题?</view> <button open-type="feedback">点击反馈</button> </view> wx.onUserCaptureScreen(() => { // 设置弹窗出现 this.setData({isFeedbackShow: true}) }); [代码] LogManager 关于小程序的 LogManager,大概是非常实用又特别低调的一个能力了。它的使用方式其实和 console 很相似,提供了 log、info、debug、warn 等日志方式。 [代码]const logger = wx.getLogManager() logger.log({str: 'hello world'}, 'basic log', 100, [1, 2, 3]) logger.info({str: 'hello world'}, 'info log', 100, [1, 2, 3]) logger.debug({str: 'hello world'}, 'debug log', 100, [1, 2, 3]) logger.warn({str: 'hello world'}, 'warn log', 100, [1, 2, 3]) [代码] 打印的日志,从管理后台下载下来之后,也是很好懂: [代码]2019-6-25 22:11:6 [log] wx.setStorageSync api invoke 2019-6-25 22:11:6 [log] wx.setStorageSync return 2019-6-25 22:11:6 [log] wx.setStorageSync api invoke 2019-6-25 22:11:6 [log] wx.setStorageSync return 2019-6-25 22:11:6 [log] [v1.1.0] request begin 2019-6-25 22:11:6 [log] wx.request api invoke with seq 0 2019-6-25 22:11:6 [log] wx.request success callback with msg request:ok with seq 0 2019-6-25 22:11:6 [log] [v1.1.0] request done 2019-6-25 22:11:7 [log] wx.navigateTo api invoke 2019-6-25 22:11:7 [log] page packquery/pages/index/index onHide have been invoked 2019-6-25 22:11:7 [log] page packquery/pages/logs/logs onLoad have been invoked 2019-6-25 22:11:7 [log] [v1.1.0] logs | onShow | | [] 2019-6-25 22:11:7 [log] wx.setStorageSync api invoke 2019-6-25 22:11:7 [log] wx.setStorageSync return 2019-6-25 22:11:7 [log] wx.reportMonitor api invoke 2019-6-25 22:11:7 [log] page packquery/pages/logs/logs onShow have been invoked 2019-6-25 22:11:7 [log] wx.navigateTo success callback with msg navigateTo:ok [代码] LogManager 最多保存 5M 的日志内容,超过5M后,旧的日志内容会被删除。基础库默认会把 App、Page 的生命周期函数和 wx 命名空间下的函数调用写入日志,基础库的日志帮助我们定位具体哪些地方出了问题。 实时日志 小程序的 LogManager 有一个很大的痛点,就是必须依赖用户上报,入口又是右上角胶囊-【关于xxxx】-【投诉与反馈】-【功能异常】这么长的路径,甚至用户的反馈过程也会经常丢失日志,导致无法查问题。 为帮助小程序开发者快捷地排查小程序漏洞、定位问题,微信推出了实时日志功能。从基础库 2.7.1 开始,开发者可通过提供的接口打印日志,日志汇聚并实时上报到小程序后台。 使用方式如下: 使用 wx.getRealtimeLogManager 在代码⾥⾯打⽇志。 可从小程序管理后台【开发】-【运维中心】-【实时日志】进入日志查询页面,查看开发者打印的日志信息。 开发者可通过设置时间、微信号/OpenID、页面链接、FilterMsg内容(基础库2.7.3及以上支持setFilterMsg)等筛选条件查询指定用户的日志信息: [图片] 由于后台资源限制,实时日志使用规则如下: 为了定位问题方便,日志是按页面划分的,某一个页面,在onShow到onHide(切换到其它页面、右上角圆点退到后台)之间打的日志,会聚合成一条日志上报,并且在小程序管理后台上可以根据页面路径搜索出该条日志。 每个小程序账号每天限制500万条日志,日志会保留7天,建议遇到问题及时定位。 一条日志的上限是5KB,最多包含200次打印日志函数调用(info、warn、error调用都算),所以要谨慎打日志,避免在循环里面调用打日志接口,避免直接重写console.log的方式打日志。 意见反馈里面的日志,可根据OpenID搜索日志。 setFilterMsg 可以设置过滤的 Msg。这个接口的目的是提供某个场景的过滤能力,例如[代码]setFilterMsg('scene1')[代码],则在 MP 上可输入 scene1 查询得到该条日志。比如上线过程中,某个监控有问题,可以根据 FilterMsg 过滤这个场景下的具体的用户日志。FilterMsg 仅支持大小写字母。如果需要添加多个关键字,建议使用 addFilterMsg 替代 setFilterMsg。 日志开发技巧 既然官方提供了 LogManager 和实时日志,我们当然是两个都要用啦。 log.js 我们将所有日志的能力都封装在一起,暴露一个通用的接口给调用方使用: [代码]// log.js const VERSION = "0.0.1"; // 业务代码版本号,用户灰度过程中观察问题 const canIUseLogManage = wx.canIUse("getLogManager"); const logger = canIUseLogManage ? wx.getLogManager({level: 0}) : null; var realtimeLogger = wx.getRealtimeLogManager ? wx.getRealtimeLogManager() : null; /** * @param {string} file 所在文件名 * @param {...any} arg 参数 */ export function DEBUG(file, ...args) { console.debug(file, " | ", ...args); if (canIUseLogManage) { logger!.debug(`[${VERSION}]`, file, " | ", ...args); } realtimeLogger && realtimeLogger.info(`[${VERSION}]`, file, " | ", ...args); } /** * * @param {string} file 所在文件名 * @param {...any} arg 参数 */ export function RUN(file, ...args) { console.log(file, " | ", ...args); if (canIUseLogManage) { logger!.log(`[${VERSION}]`, file, " | ", ...args); } realtimeLogger && realtimeLogger.info(`[${VERSION}]`, file, " | ", ...args); } /** * * @param {string} file 所在文件名 * @param {...any} arg 参数 */ export function ERROR(file, ...args) { console.error(file, " | ", ...args); if (canIUseLogManage) { logger!.warn(`[${VERSION}]`, file, " | ", ...args); } if (realtimeLogger) { realtimeLogger.error(`[${VERSION}]`, file, " | ", ...args); // 判断是否支持设置模糊搜索 // 错误的信息可记录到 FilterMsg,方便搜索定位 if (realtimeLogger.addFilterMsg) { try { realtimeLogger.addFilterMsg( `[${VERSION}] ${file} ${JSON.stringify(args)}` ); } catch (e) { realtimeLogger.addFilterMsg(`[${VERSION}] ${file}`); } } } } // 方便将页面名字自动打印 export function getLogger(fileName: string) { return { DEBUG: function(...args) { DEBUG(fileName, ...args); }, RUN: function(...args) { RUN(fileName, ...args); }, ERROR: function(...args) { ERROR(fileName, ...args); } }; } [代码] 通过这样的方式,我们在一个页面中使用日志的时候,可以这么使用: [代码]import { getLogger } from "./log"; const PAGE_MANE = "page_name"; const logger = getLogger(PAGE_MANE); [代码] autolog-behavior 现在有了日志组件,我们需要在足够多的地方记录日志,才能在问题出现的时候及时进行定位。一般来说,我们需要在每个方法在被调用的时候都打印一个日志,所以这里封装了一个 autolog-behavior 的方式,每个页面(需要是 Component 方式)中只需要引入这个 behavior,就可以在每个方法调用的时候,打印日志: [代码]// autolog-behavior.js import * as Log from "../utils/log"; /** * 本 Behavior 会在小程序 methods 中每个方法调用前添加一个 Log 说明 * 需要在 Component 的 data 属性中添加 PAGE_NAME,用于描述当前页面 */ export default Behavior({ definitionFilter(defFields) { // 获取定义的方法 Object.keys(defFields.methods || {}).forEach(methodName => { const originMethod = defFields.methods![methodName]; // 遍历更新每个方法 defFields.methods![methodName] = function(ev, ...args) { if (ev && ev.target && ev.currentTarget && ev.currentTarget.dataset) { // 如果是事件类型,则只需要记录 dataset 数据 Log.RUN(defFields.data.PAGE_NAME, `${methodName} invoke, event dataset = `, ev.currentTarget.dataset, "params = ", ...args); } else { // 其他情况下,则都记录日志 Log.RUN( defFields.data.PAGE_NAME, `${methodName} invoke, params = `, ev, ...args); } // 触发原有的方法 originMethod.call(this, ev, ...args); }; }); } }); [代码] 我们能看到,日志打印依赖了页面中定义了一个[代码]PAGE_NAME[代码]的 data 数据,所以我们在使用的时候可以这么处理: [代码]import { getLogger } from "../../utils/log"; import autologBehavior from "../../behaviors/autolog-behavior"; const PAGE_NAME = "page_name"; const logger = getLogger(PAGE_NAME); Component({ behaviors: [autologBehavior], data: { PAGE_NAME, // 其他数据 }, methods: { // 定义的方法会在调用的时候自动打印日志 } }); [代码] 页面如何使用 Behavior 看看官方文档:事实上,小程序的页面也可以视为自定义组件。因而,页面也可以使用[代码]Component[代码]构造器构造,拥有与普通组件一样的定义段与实例方法。但此时要求对应[代码]json[代码]文件中包含[代码]usingComponents[代码]定义段。 完整的项目可以参考wxapp-typescript-demo。 参考 LogManager 实时日志 Component构造器 behaviors 结束语 使用自定义组件的方式来写页面,有特别多好用的技巧,behavior 就是其中一个比较重要的能力,大家可以发挥自己的想象力来实现很多奇妙的功能。
2019-12-10