- 部分安卓机型下 wx.getAppAuthorizeSetting 返回值错误
一. 机型 手机型号: HUAWEI nova 11 Pro手机系统: HarmonyOS 4.2.0二. 复现方式 (1)开启以下4个权限 手机系统对应用的设置 允许微信使用蓝牙(有的手机此项叫附近设备)手机系统对应用的设置 允许微信使用定位开关微信对小程序的设置 授权蓝牙微信对小程序的设置 授权精确地理位置(2)调用 wx.getAppAuthorizeSetting 和 wx.getSettingminiprogramAuthSetting API 并打印返回值 [图片] 在返回值中可以看到4个权限对应返回值: wx.getAppAuthorizeSetting bluetoothAuthorized (允许微信使用蓝牙的开关 未开启)wx.getAppAuthorizeSetting locationAuthorized (允许微信使用定位的开关 已开启)wx.getSettingminiprogramAuthSetting.scope.bluetooth (当前宿主小程序的用户授权结果,是否授权蓝牙 已授权)wx.getSettingminiprogramAuthSetting.scope.userLocation (当前宿主小程序的用户授权结果,是否授权精确地理位置 已授权)可以看出,手机操作系统已给微信开启蓝牙权限,但小程序 wx.getAppAuthorizeSetting 返回的 bluetoothAuthorized 是 denied, 是不对的。 (3)在当前状态执行业务逻辑 系统设置页的状态和微信小程序API调用出现了矛盾,依旧存在一种可能性:wx.getAppAuthorizeSetting 对的,系统设置页展示错误,那么继续进行蓝牙逻辑,即可知道微信使用蓝牙的权限是否真正开启了。 在当前状态下,执行了公司的蓝牙相关业务逻辑,结果是正常运行(此处不放代码和截图了,可以自行验证)。所以确定是 wx.getAppAuthorizeSetting 返回的值异常。 考虑到该错误只在特定机型下出现,猜测可能是微信小程序API对安卓底层的兼容方向出的问题。后续就无法继续排查了,要交给小程序官方了。 (4)退出小程序;关闭手机系统对微信蓝牙权限的授权开关,再重新打开; 执行这一步操作后,wx.getAppAuthorizeSetting 返回值又正常了。 此步骤4可以给广大微信小程序开发者,作为一种临时的解决手段。但用户体验较差。即便我们给了引导提示,我们的客户也没有意愿去重启手机权限,而是跟倾向于向公司投诉我们研发团队…… 三. 文档问题 wx.getAppAuthorizeSetting 其实是有两份文档的,官方回复开发者的留言里经常把文档搞混,其实正确的只有一个。 (1) 文档A 该文档属于多端能力目录下,未特指小程序,但官方引用了这部分使用范围:【仅 iOS 有效,所以返回 'undefined', Android 端可忽略】原始链接:https://developers.weixin.qq.com/miniprogram/dev/platform-capabilities/miniapp/api/diffapi/getAppAuthorizeSetting.html(2)文档B 小程序文档目录下使用范围:iOS和安卓均有效,对安卓基础库版本有限制原始链接:https://developers.weixin.qq.com/miniprogram/dev/api/base/system/wx.getAppAuthorizeSetting.html(3)结论 结合我司测试团队对多种机型多次的测试,可以确定:该API在安卓和iOS下均有效,且所有的BUG都出现在安卓机型。所以文档B是对的。 四. BUG 修复进度问题 微信官方也承认了该问题,并于2024年8月9日发布公告,称该问题在修复中。 但截止今天(2024-12-18)微信小程序官方还没有发布修复完毕的版本公告。 公告地址:https://developers.weixin.qq.com/community/develop/doc/00084ac3458228a822f17fa6d66c01?highLine=bluetoothAuthorized [图片] 五. 可复现机型 我司测试团队使用了几十种机型进行测试,在以下机型也复现了问题,此处提供给微信小程序团队,希望能帮助到官方团队更高效定位问题 华为 nova 12 Ultra (HarmonyOS版本 4.2.0.156)华为 nova 9/nova 7/nova11 seoppo reno8,reno 9oppo A2荣耀100华为 mate 60 pro一加 Ace Pro全部问题出现在安卓,并且可以锁定华为(包括荣耀)和OPPO(包括一加)两大品牌。 六. 附录 (1)附社区反馈地址,供测试复现问题和查找机型的参考: https://developers.weixin.qq.com/community/develop/doc/000a08eb204588ae03f1b21b26b400?highLine=bluetoothAuthorized https://developers.weixin.qq.com/community/develop/doc/000062204348c06eea225031e69c00?highLine=wx.getAppAuthorizeSetting https://developers.weixin.qq.com/community/develop/doc/00020c823746809591e1b18166bc00?highLine=bluetoothAuthorized (2)附官方文档接口API地址: https://developers.weixin.qq.com/miniprogram/dev/platform-capabilities/miniapp/api/diffapi/getAppAuthorizeSetting.html https://developers.weixin.qq.com/miniprogram/dev/api/base/system/wx.getAppAuthorizeSetting.html
2024-12-19 - iOS13怎么区分蓝牙的不同状态,是系统蓝牙未打开或者未授权微信蓝牙权限?
在iOS13中,蓝牙变成了和位置,通知服务等同样的可以针对单个app授权的服务。用户可以在设置中关闭微信的Bluetooth Sharing,这样即使系统蓝牙打开了,小程序仍然无法使用BLE服务。 三种情况: a.手机蓝牙未打开 b.手机蓝牙已打开,微信的蓝牙服务关闭 c.两者均打开 在目前的小程序API中,我们可以通过获取system Info来查看蓝牙是否打开(bluetoothEnabled).然而在情况a和b中,该参数均为false。这样我们无法准确提示用户去打开系统蓝牙或者打开微信的蓝牙授权了。 另一种方式是在wx.openBluetoothAdapter(Object object) 这个接口中,情况a返回的error为: {"state": 4,"errCode":10001,"errMsg":"openBluetoothAdapter:fail open fail"} 情况b返回的error为: {"state": 3,"errCode":10001,"errMsg":"openBluetoothAdapter:fail open fail"} 开发者或许可以通过这个state来区分这两种情况,但是在官方文档中并未给出state的具体定义。 我相信在iOS13中,小程序API会有所更新。希望能够补充这种state的定义,同时给出更多蓝牙的选项。 下面是iOS中关于state的定义,我猜测微信直接引用这部分定义: typedef NS_ENUM(NSInteger, CBCentralManagerState) { CBCentralManagerStateUnknown = CBManagerStateUnknown, CBCentralManagerStateResetting = CBManagerStateResetting, CBCentralManagerStateUnsupported = CBManagerStateUnsupported, CBCentralManagerStateUnauthorized = CBManagerStateUnauthorized, CBCentralManagerStatePoweredOff = CBManagerStatePoweredOff, CBCentralManagerStatePoweredOn = CBManagerStatePoweredOn, } NS_DEPRECATED(10_7, 10_13, 5_0, 10_0, "Use CBManagerState instead"); 这个问题已经有用户遇到,应该是购买的iOS13的新手机,希望能够尽早处理。
2019-11-08 - 一键完成小程序国际化
随着小程序使用的人数增长,小程序管理后台陆续收到非中文母语的用户要求支持英文的请求。小程序一开始是直接在程序里使用中文字符串的方式,要做国际化只能把这些硬编码中文的地方全部替换为i18n调用。写了个脚本跑了下,发现小程序里涉及到需要转换硬编码的地方有2000+处。。。手动修改不太科学,于是写了个名为mina-i18n的工具,用于小程序的国际化转换,有兴趣的同学可以用自己的小程序项目实验一下: npm install -g mina-i18n mina-i18n /path/to/origin/mina/project /path/to/i18n/mina/project工具主要包含对JS和WXML两种文件的处理,JS使用babel处理,WXML使用htmlparser库处理,json和css由于没法使用函数调用,这个需要用户自行处理了。 JS处理: JS文件的处理思路:找到中文字符串,将其改为以中文字符串为参数的i18n函数调用。我们的目标是一键转换后可以直接运行,而替换的行为发生在任何一个文件里,所以我们需要一个全局可访问的函数。在小程序里,有两个可以全局访问的变量,一个是getApp(),一个是wx,为了代码的简洁性,我们决定在wx上挂载一个L函数作为全局i18n函数。即: "中文" 转换为 wx._t("中文")直接使用正则表达式匹配是无法做到的符合语法的替换,这里使用babel完成保留程序上下文的替换,替换逻辑写在babel 的插件里的 StringLiteral 的回调里(所有JS程序里解析到的字符串都会进入这个回调),代码如下: const visitor = { StringLiteral(path) { const parentPath = path.parent // 每个中文字符串只处理一次,识别到已处理过就退出,不然会死循环 if (t.isCallExpression(parentPath)) { const callee = parentPath.callee if (t.isMemberExpression(callee)) { const { object, property } = callee if ( t.isIdentifier(object) && object.name === MINA_I18N_JS_FUNCTION_CALLEE && property.name === MINA_I18N_FUNCTION_NAME ) { return } } } const reg = /\s*[\u4E00-\u9FA5]+\s*/g const stringValue = path.node.value if (reg.test(stringValue)) { path.replaceWith(t.CallExpression( t.MemberExpression( t.identifier(MINA_I18N_JS_FUNCTION_CALLEE), t.identifier(MINA_I18N_FUNCTION_NAME) ), [t.stringLiteral(stringValue)] )) } } } WXML处理: 对WXML的处理思路与JS类似,就是找出WXML文件里的中文文本,替换为以文本为参数的i18n函数调用。在小程序里使用函数调用需要用到 WXS语言 ,这个语言无法使用小程序API,只有极少数的类库,反正你当成是个纯运算逻辑的WXML语法助手就行。因为WXS语言没法从外部获取数据,或者WXML里的i18n函数调用只能是类似 i18nFunc("中文","en") 这种形式,就是说具体要转为哪种语言,必须明确地告知WXS的函数。一般用户的语言都是从系统信息中获取,或者用户手动选择后存在服务端,这个语言的变量信息我们在JS里可以获取,而且这个变量每个页面都会用到,而WXML里是没法访问到getApp()和wx这些全局变量的,我们只能在每个页面的Page函数的data变量里加上这个语言变量Lang,才能在每个WXML里使用类似 i18nFunc("中文", Lang) 的形式来做文本替换。处理代码如下: const visitor = { ObjectProperty(path, state) { if (path.node.key.name === 'data') { const parentPath = path.parentPath || {} const parentParentPath = parentPath.parentPath || {} const node = parentParentPath.node if ( t.isObjectExpression(parentPath) && t.isCallExpression(parentParentPath) && node && node.callee && t.isIdentifier(node.callee) && node.callee.name === 'Page' ) { // 变量插入只处理一次,识别到已处理过就退出,不然会死循环 if ( t.isCallExpression(path.node.value) && t.isObjectExpression(path.node.value.arguments[0]) && path.node.value.arguments[0].properties[0].key.name === MINA_I18N_LANG_NAME ) { return } path.replaceWith( t.objectProperty( path.node.key, t.CallExpression( t.MemberExpression( t.identifier('Object'), t.identifier('assign') ), [ t.objectExpression([ t.objectProperty( t.identifier(MINA_I18N_LANG_NAME), t.CallExpression( t.MemberExpression( t.identifier(MINA_I18N_JS_FUNCTION_CALLEE), t.identifier(MINA_I18N_GETLANG_FUNCTION_NAME) ), [] ) ) ]), path.node.value ] ) ) ) } } } } 有了可在WXML里运行的 i18n函数,接下来就是对WXML源文件里的中文文本进行替换。WXML的转换处理要比JS复杂一些,处理JS的时候babel帮我们做好了语法树的解析、遍历和代码生成过程,我们只要实现语法树访问的回调就行,WXML就没有这么好用的工具了,所以需要自己做多点工作。WXML的解析用了 htmlparser2 ,这个库会返回给你一棵DOM树,按着根节点就能遍历整棵树。不过在实际解析生成WXML的时候,发现这个库有个地方没法满足,就是它解析出来的节点属性无法区分开是为空还是 boolean attributes ,也就是说<view hidden></view>和<view hidden=""></view>解析出来的结果都是一样的{hidden: ""},这其实有很大的歧义,因为在小程序里<view hidden></view>会表示这个view的hidden=true,而<view hidden=""></view>这会被解释为hidden=false,因此按照这个解析结果生成的话,就发现原来小程序里应该隐藏的view都被显示了。。。搜了下其他有其他人给作者提过这个问题的 issue ,我看作者的意思大概是这个库不是用来处理序列化或者生成代码这类事的,在github搜了下好像这个库又确实是JS里HTML解析库里排名第一的,于是就自己fork出来 一份,加了个选项来做boolean attributes 的区分。 在做WXML处理的时候,我犯了个错误,就是把处理WXML跟处理HTML这两件事等同起来了,可能之前做过一些HTML相关的处理工作,所以写着写着就很顺地按着原来用正则表达式处理的方式去做了,结果发现在处理节点属性为{{xxx}}的动态属性时总是有edge case处理不到。中间隔了两天去做其他需求后再回来一想,这{{xxx}}里的xxx就是JS代码啊,直接用babel不就完事了吗?困在原来处理HTML的思路里调试了大半天浪费时间。正确的处理WXML的方式就是用htmlparser解析静态文本,解析到的{{xxx}}语法交给babel处理,最后再重新合成WXML代码。这里面还有属性的单双引号处理、uincode与汉字的一些处理等细节,具体就不展开了,代码里都有: function buildWXML(root) { if (Array.isArray(root)) { let xmlString = '' root.forEach(node => { xmlString += buildWXML(node) }) return xmlString } else if (root.type === 'text') { const text = root.data return processMinaTemplateText(text) } else if (root.type === 'tag') { const tagName = root.name.replace('wx-', '') let tagString = `<${tagName}` const attr = root.attribs || {} Object.keys(attr).forEach(key => { if (attr[key] === null) { tagString += ` ${key}` } else { const attrValue = processMinaTemplateText(attr[key], { isAttrValue: true }) tagString += ` ${key}="${attrValue}"` } }) const children = root.children || [] if (isSelfCloseTag(tagName) && children.length === 0) { tagString += '/>' } else { tagString += '>' children.forEach(node => { tagString += buildWXML(node) }) tagString += `</${tagName}>` } return tagString } else if (root.type === 'comment') { return `<!--${root.data}-->` } else { return '' } } function processMinaTemplateText(text, options = {}) { const textArray = text.split(/({{[^}]*}})/g) let returnText = '' textArray.forEach(item => { if (item.startsWith('{{') && item.endsWith('}}')) { let expression = item.substring(2, item.length - 2) returnText += '{{' + processMinaTemplateExpression(expression) + '}}' } else { returnText += processPlainText(item) } }) return returnText } function processMinaTemplateExpression(code) { const visitor = { StringLiteral(path) { const parentPath = path.parent if (t.isCallExpression(parentPath)) { const callee = parentPath.callee if (t.isIdentifier(callee) && callee.name === MINA_I18N_FUNCTION_NAME) { return } } const reg = /\s*[\u4E00-\u9FA5]+\s*/g const stringValue = path.node.value if (reg.test(stringValue)) { path.replaceWith(t.CallExpression( t.identifier(MINA_I18N_FUNCTION_NAME), [t.stringLiteral(stringValue), t.identifier(MINA_I18N_LANG_NAME)] )) createI18NData(stringValue) } } } try { const result = babel.transform(code, { plugins: [ { visitor } ], generatorOpts: { quotes: 'single', compact: false } }) const i18nScriptContent = transformText(toSingleQuotes(result.code)) return i18nScriptContent.replace(/[\r\n]+$/g, '').replace(/^;/g, '').replace(/;$/g, '') } catch (e) { console.log(`parser expression : ${code}, error: ${JSON.stringify(e)}`) return code } } 翻译:一开始做这个工具的目的,就是要做到一键转换后就可以在微信开发者工具里跑起来。经过上面两步的处理,我们已经把项目里出现过的所有中文字符串都提取并替换,接下来我们就要实现 i18n 翻译函数。 最简单的是简体和繁体的转换,因为已经有 opencc 这个优秀的开源库可以处理 。 const OpenCC = require('opencc') const opencc = new OpenCC() const hant_text = opencc.convertSync(text) 中英文的翻译没法离线,只能找线上服务。在找了google,有道,金山,必应,百度等翻译服务后,发现微软的必应是最方便的,不需要申请token或者去对抗防爬虫,翻译效果也不错,非常适合用在一个可以无条件使用的工具里,接口调用非常简洁: request.post({ url: 'https://cn.bing.com/ttranslatev3', form: { fromLang: 'zh-Hans', to: 'en', text: text }, json: true, timeout: 2000 }).then(res => { return res[0].translations[0].text }) 绝大多数中文项目的i18n做到繁体+英文就够了,外贸项目等特殊情况的话,改一下调用bing API的语言参数就行,bing支持多种语言翻译。 至此,工具也算基本完成了,使用了自己小程序和github上找的几个开源小程序项目做测试,基本都是一键转换后就能直接在开发者工具上跑的,真机预览也没出问题。有兴趣的同学可以在自己项目的小程序上跑跑看。我看了下日常使用的各个小程序,可以说几乎100%都是没有提供切换语言选项的,万一以后有需求的话,希望这个工具可以方便到大家。 对源码有兴趣的同学可以到 github 看一下,如果转换的小程序跑起来有错误的话也欢迎提issue,我会尽快解决的。
2019-08-13 - 基于加速度计判断横竖屏
也许有人会问,小程序中都是竖直app形态,要横竖屏判断有什么用?即使判断出了横屏状态,你能把小程序横过来?答案是不能的,但是判断当前设备处于横屏或者竖屏状态来实现一些友好的用户体验交互方式的需求确实存在。例如手机横屏,让视频播放自动全屏,手机竖屏,让视频切换回来小屏。 然而,截止至目前,小程序官方的API中并没有提供这样的横竖屏判断的方法。那么我们只能自己想办法实现这样的判断。小程序的设备API中提供了加速度计的监听方法,使用方法如下: [代码]wx.onAccelerometerChange([代码][代码]function[代码][代码](res) {[代码] [代码] [代码][代码]console.log(res.x)[代码] [代码] [代码][代码]console.log(res.y)[代码] [代码] [代码][代码]console.log(res.z)[代码] [代码]})[代码] 加速度计的三轴以下是一般移动设备的加速度计三轴坐标系示例图: [图片] 以手机竖直面向用户为例,加速计的三轴坐标系统的X、Y、Z轴定义如下: 沿着手机屏幕顶部向上是Y轴正方向,向下是Y轴负方向; 当手机顶部朝上时,沿着手机屏幕向右是X轴正方向,向左是X轴负方向; 正对手机时,垂直屏幕向外是Z轴正方向,垂直屏幕向里是Z轴负方向; 当手机处于静止状态时,手机此时只受一个重力加速度(1g=9.8m/s²)的作用,加速度计返回的res.x、res.y、res.z的值就是设备的三轴受到的加速度的值,取值范围从[-1g,1g]。设备以不同方式放置时,x/y/z的值如下: [图片] 计算姿态角在stackoverflow上找到了根据加速度计三轴的值计算姿态角公式(https://stackoverflow.com/questions/3755059/3d-accelerometer-calculate-the-orientation),经过结合设备的三轴坐标方向对公式进行调整,最终得出了公式: [代码]Pitch = atan2(Y, Z) * 180/M_PI;Roll = atan2(-X, sqrt(Y*Y + Z*Z)) * 180/M_PI;[代码] [代码][代码]Roll = atan2(-X, sqrt(Y*Y + Z*Z)) * 180/M_PI;[代码][代码] Roll(绕Y轴旋转的角度) 当设备绕着自身Y轴旋转时(表示手机左侧或右侧翘起的角度),该角度值将会发生变化,取值范围是-90到90度。 Pitch(绕X轴旋转的角度) 当手机绕着自身的Y轴旋转(表示手机顶部或尾部翘起的角度),该角度会发生变化,值的范围是-180到180度。 [图片] 接下来就是根据自己对横竖屏角度的观测,再结合微信小程序中,视频全屏只能以手机向左旋转方式全屏的特性,只对用户左侧横屏判断为横屏状态,实现代码片段如下: [代码] // 0为竖屏,1为横屏[代码][代码] [代码][代码]let lastState = 0;[代码][代码] [代码][代码]let lastTime = Date.now();[代码] [代码] [代码][代码]wx.startAccelerometer();[代码] [代码] [代码][代码]wx.onAccelerometerChange((res) => {[代码][代码] [代码][代码]const now = Date.now();[代码][代码] [代码] [代码] [代码][代码]// 500ms检测一次[代码][代码] [代码][代码]if[代码] [代码](now - lastTime < 500) {[代码][代码] [代码][代码]return[代码][代码];[代码][代码] [代码][代码]}[代码][代码] [代码][代码]lastTime = now;[代码] [代码] [代码][代码]let nowState;[代码] [代码] [代码][代码]// 57.3 = 180 / Math.PI[代码][代码] [代码][代码]const Roll = Math.atan2(-res.x, Math.sqrt(res.y * res.y + res.z * res.z)) * 57.3;[代码][代码] [代码][代码]const Pitch = Math.atan2(res.y, res.z) * 57.3;[代码] [代码] [代码][代码]// console.log('Roll: ' + Roll, 'Pitch: ' + Pitch)[代码] [代码] [代码][代码]// 横屏状态[代码][代码] [代码][代码]if[代码] [代码](Roll > 50) {[代码][代码] [代码][代码]if[代码] [代码]((Pitch > -180 && Pitch < -60) || (Pitch > 130)) {[代码][代码] [代码][代码]nowState = 1;[代码][代码] [代码][代码]} [代码][代码]else[代码] [代码]{[代码][代码] [代码][代码]nowState = lastState;[代码][代码] [代码][代码]}[代码] [代码] [代码][代码]} [代码][代码]else[代码] [代码]if[代码] [代码]((Roll > 0 && Roll < 30) || (Roll < 0 && Roll > -30)) {[代码][代码] [代码][代码]let absPitch = Math.abs(Pitch);[代码] [代码] [代码][代码]// 如果手机平躺,保持原状态不变,40容错率[代码][代码] [代码][代码]if[代码] [代码]((absPitch > 140 || absPitch < 40)) {[代码][代码] [代码][代码]nowState = lastState;[代码][代码] [代码][代码]} [代码][代码]else[代码] [代码]if[代码] [代码](Pitch < 0) { [代码][代码]/*收集竖向正立的情况*/[代码][代码] [代码][代码]nowState = 0;[代码][代码] [代码][代码]} [代码][代码]else[代码] [代码]{[代码][代码] [代码][代码]nowState = lastState;[代码][代码] [代码][代码]}[代码][代码] [代码][代码]}[代码][代码] [代码][代码]else[代码] [代码]{[代码][代码] [代码][代码]nowState = lastState;[代码][代码] [代码][代码]}[代码] [代码] [代码][代码]// 状态变化时,触发[代码][代码] [代码][代码]if[代码] [代码](nowState !== lastState) {[代码][代码] [代码][代码]lastState = nowState;[代码][代码] [代码][代码]if[代码] [代码](nowState === 1) {[代码][代码] [代码][代码]console.log([代码][代码]'change:横屏'[代码][代码]);[代码][代码] [代码][代码]} [代码][代码]else[代码] [代码]{[代码][代码] [代码][代码]console.log([代码][代码]'change:竖屏'[代码][代码]);[代码][代码] [代码][代码]}[代码][代码] [代码][代码]}[代码][代码] [代码][代码]});[代码] 然后就可以在横竖屏切换的状态下,去切换视频的横竖屏了 [代码]if[代码] [代码](state === 1) {[代码][代码] [代码][代码]video.requestFullScreen();[代码][代码]} [代码][代码]else[代码] [代码]{[代码][代码] [代码][代码]video.exitFullScreen();[代码][代码]}[代码] 其他 另外,在这里发现小程序的一个小bug,就是当进入一个页面,马上就调用requestFullScreen()方法去拉起视频全屏时,会破坏整个页面的布局,并且再调用全屏方法时,视频就无法再全屏了,像这样: [图片] 所以为了防止用户直接以横屏的状态进入一个视频播放页,而我们的横屏判断检测生效立即触发全屏引发bug,我将监听横竖屏的事件通过setTimeout(listener, 3000)延迟3s监听,这样横屏才不会触发bug。 最后 文中的很多知识点很多都是从网络文章学来,可能存在错误的理解,如有错误,欢迎各位指正。 最后再打个广告,欢迎喜欢看游戏直播的小伙伴来试用我们的《TG电竞》直播小程序,这里聚合各大平台的知名主播,总有一款适合你哦。 [图片]
2018-01-25