- wx.uploadFile 上传文件失败
问题补充 安卓5,6,7 安卓最新版微信(8.0.15)基础库2.20.0 该现象发生率 100% 小程序使用wx.uploadFile上传图片到后端,接口调用成功,后端用的MultipartFile接收时候发现缺少boundary参数,后台报错:the request was rejected because no multipart boundary was found,出现的概率大概1% org.springframework.web.multipart.MultipartException: Failed to parse multipart servlet request; nested exception is java.io.IOException: org.apache.tomcat.util.http.fileupload.FileUploadException: the request was rejected because no multipart boundary was found
2021-10-20 - ios系统上 wx.createBLEConnection 手机无限提示设备申请与iPhone配对?
ios手机上调用wx.createBLEConnection后,手机一直弹窗,提示 设备申请与iPhone配对,点击确定后,仍然不断提示,阻碍界面使用。
2021-11-29 - wx.writeBLECharacteristicValue写入数据成功 ,但设备没反应?
wx.writeBLECharacteristicValue连接蓝牙打印写入数据成功 但是设备没反应, 点击设备可以接收到wx.onBLECharacteristicValueChange设备返回值? 代码 https://developers.weixin.qq.com/s/jlMOOhm87NBs writeBLECharacteristicValue() { // 向蓝牙设备发送一个0x00的16进制数据 var senddata = strToHexCharCode('ATK001#'); let buffer = new ArrayBuffer(senddata.length) let dataView = new DataView(buffer) //dataView.setUint8(0, Math.random() * 255 | 0) for (var i = 0; i < senddata.length; i++) { dataView.setUint8(i, senddata.charAt(i).charCodeAt()) } /// let aa=this.strToArrayBuffer() console.log('获取二进制数据',buffer) //向低功耗蓝牙设备特征值中写入二进制数据。 wx.writeBLECharacteristicValue({ deviceId: this._deviceId, serviceId: this._serviceId, characteristicId: this._characteristicId, value: buffer, success (res) { console.log('成功写数据writeBLECharacteristicValue success', res) //如果 uni.writeBLECharacteristicValue 走 success ,证明你已经把数据向外成功发送了,但不代表设备一定就收到了。通常设备收到你发送过去的信息,会返回一条消息给你,而这个回调消息会在 uni.onBLECharacteristicValueChange 触发 }, fail(res) { console.error('失败写数据getBLEDeviceCharacteristics', res) } }) }, [图片] [图片]
2022-08-05 - 渲染层错误ReferenceError: __g is not defined这个问题应该如何解决?
[图片]
2021-12-20 - 现在版本的开发者工具支持蓝牙调试吗
如题所示
2019-07-30 - 微信小程序wx.createInnerAudioContext()在安卓手机不能播放语音文件问题解决
一、问题现象微信小程序已经放弃了基于wx.createAudioContext()的audio组件,要求开发人员使用功能更强大的wx.createInnerAudioContext()组件,新的给件功能的确要强大得多,不需要在页面上布置组件,旧的组件只有play、pause、seek三个事件,功能是比较弱,新的Inner组件增加了很多事件,并且还有监控事件,可以自己定义UI界面,通过监控事件可以在准备好播放、播放完成等,增加用户函数,可以统计用户使用情况,可以进行很多操作,如插播内容、转换等。 但是,将组件按为新的wx.createInnerAudioContext()后遇到了一个大坑,即部分安卓手机播放时后端报错,无法播放语音文件。在电脑上调试、浏览器上访问、苹果手机上都可以,就是安装手机网上提这个问题的人很多,但都没有提供完整的解决方案,微信官方也没有提供解决方案,只是让大家提交代码片断,只说是与机型不兼容。 真机调试时,后台错误代码: errCode: 10001, errMsg: "errCode:62, err:load or init native decode so fail"。 同时音频对象监听错误的事件,也能监听到错误,错误信息: 监听音频播放错误事件 {errCode: 0, errMsg: ""} [图片] 为了项目进展,没有办法,先退回去用旧的wx.createAudioContext()组件,这个组件虽然功能差一些,但基本上能用,先凑合作。后来,有时间后进一步在研究了手机兼容问题、音频文件服务Header、保存文件OSS问题、音频文件内部格式问题后,终于找到了解决方案,特此完整记录一下,希望对遇到类似问题的同行有用。 二、原因分析刚碰到这个问题也是一头雾水,百度上能找到的相关信息很少,微信小程序社区提这个问题的人很多,将这个错误代码在社区上检索,有7000多条信息,但都是反馈问题的,没有找到可用的解决方案,不过线索找到了几条。 2.1 检测组件使用方法刚开始怀疑程序编写方法不对,而微信小程序的官方手册介绍内容很少,还不容易理解,官方手册将对象申明放到头上,并且按const申明,并且只有一个SRC,我的项目中要用到多个动态SRC,并且SRC是用户选择不同景点,播放不同的语音介绍。 const innerAudioContext = wx.createInnerAudioContext(); 刚开始,是怀疑此问题,后专门写了一个页面,按照官方的写法,结果一样,仍然不行。后来反复尝试写各种方案,将对象放到this,this.data仍然不行,测试过将对象所有按钮事件,监控事件都写了来跟踪各种情况 ,结果仍是一样,在测试平台、ios真机测试行,安卓真机测试不行。 2.2 文件请求header问题排除先退回旧组件继续使用,旧的组件需要在页面文件添加组件,为了在实现多个对象用一个后台组件自己定义了play和pause图标,定认了一个audio组,并隐藏起来,实现与inner对象差不多的效果,但控制起来比较麻烦,且没有监控事件,后续做播放统计会有一些问题。 不甘心退回用旧组件,一有时间继续研究该问题,在微信社区发现讨论此问题的很多,但没有明确答案,基本上说是不兼容,问题原因是微信小程序的组件不兼容,只能等微信升级,但是这个问题从2019年3月就广泛出现了啊,一年多了微信还没升级吗。后来找到一个兄弟说解决了是request heder问题,修改请求的heaer即可,但没有给出方法啊,也没有进一步回复。 反复研究request header,在我用的VUE的request确实是判断header,后台header需要调协为octect-stream,但是我后台已经是mp3文件了啊,不论用新旧组件,都只有一个src参数,指向后端地址,没有地方加header啊,除非后台将文件读取到内存,然后再以stream反馈给前端,但是觉得使用OSS就是让前端可以直接通过链接方式,以流方式使用多媒体文件啊,如果再读回服务器内存,再反馈给前端不对啊,应该没有必要。 if (headers['content-type'] === 'application/octet-stream;charset=UTF-8') { return res.data } 继续研究,如果使用OSS或者其它服务器用链接访问,都无法增加header的请求头,解决这个方案的兄弟应该是以流方式提供给前端小程序的,不是以后端OSS对象文件服务提供的小程序的。 2.3 文件服务器问题分析继续下载微信社区上相关的代码片段,发现一个有意思的问题,有iac应用一个代码片段,第一次播放报错,等待一会儿,再点击播放按钮时,就不会报错了。觉得这个问题是否应该是服务器与小程序的冲突所至,文件前端还没有获取完成,前端就播放了,将自动播放去掉,修改为自播放,或者在onCanPlay事件中再播放,还有错,问题没有得到解决。想到自己用的OSS服务器是阿里,小程序是腾讯,是不是这两个公司互相限制造成的问题。沿着这个思路,检查文件服务位置进行测试,果然将其它服务器基本上行,自己在阿里OSS上的不行,以为是阿里和腾讯互相限制的结果,但想想不会啊,计算机上和苹果计算机上行的啊。于是可以播放的文件下载下来,上传到自己的OSS上进行测试,竟然能够播放,于是想明白了,是不是文件本身有问题啊。至此,排除了小程序写法、请求头参数、文件服务器问题,此问题应该是文件本身问题,于此顺着此思路,还真找到了相关资料,解决了此问题。 三、MP3文件标签分析由于我是自己学习,前后端都比较了解,顺着MP3文件格式问题思路继续,查找,果然在微信小程序开发社区找到了下面文章,该文章介绍引用了MP3文件格式标记问题,并且提到了讯飞语音合成的mp3不能播放,转百度语音合成就以。并在这个问题的回复文间中,介绍了一个给mp3文件添加标记的程序,这两个方案结合在一起给了解决问题的思路与方法。 https://developers.weixin.qq.com/community/develop/article/doc/000460e9bd4c982e1609f4f725b013 3.1 MP3文件格式解析MP3 文件大体分为三部分:TAG_V2(ID3V2),音频数据,TAG_V1(ID3V1),详细的见下面文章,不再赘述。 https://blog.csdn.net/datamining2005/article/details/78954367 3.2 讯飞语音合成MP3文件格式根据3.1介绍的内容,下载格式工厂,对讯飞语音合成文件信息进行解析,果然只有文件size,其它信息都没有,如下图。 [图片] 另外找了下MP3音乐文件解析如下,有详细的信息。 [图片] 用十六进制文件找开,头上没有id3,后面没有tag [图片] [图片] 至此,可以判断是讯飞合成的语音文件是使用lame3.1格式对音频进行编码,然后以扩展名MP3进行识别,在游览器、微信小程序调试器、苹果真机调试这两个信息足够解析并播放,但安卓版微信小程序是标签进行识别的,无法正常播放。 对此,对讯飞合成用格式工厂进行转换,然后上传到阿里OSS文件上,测试能够正常播放。至此,问题的原因定位清楚: 1、讯飞语音合成的MP3文件只是用lame3.1格式进行编码,并以mp3扩展名进行标识识别,并没用在文件中添加MP3音频文件前后增加TAG_V2(ID3V2)、TAG_V1(ID3V1)标记。 2、微信小程序安卓上的音频播放器有BUG,是按音频文件内部的TAG来识别和解析文件,对于TAG不正确的,不能播放,但这个问题在计算机浏览器和苹果计算机上存在,只存在安卓系统,因此前端工程师很难解决这个问题,导致微信社区上千个这样的问题没有完整的解决方案回复。特别是,很多偶然现现的问题,原因是文件的问题,不是不兼容问题。 四、修复方案找到根本原因后,参照下面方案进行修复,分析讯飞语音合成文件的资料和参数,没有找到添加TAG的方法,没办法自己添加吧,感谢两位同行兄弟提供的思路。 https://www.cnblogs.com/ztysir/p/5513853.html 4.1 语音文件添加TAG方法添加两个方法,分别根据传入的音频文件名称,作者,专辑名,添加到头和尾部,这两个方法基本上借用上面兄弟的代码。 /** * * 合成mp3文件的tag * * * * @songName 名称 * * @artistName 作者 * * @albumName 专辑 * * @return 128字节的D3V1字符串 * * * @return */ private static byte[] composeD3V1(String songName, String artistName, String albumName) { try { byte[] tagByteArray = new byte[128]; byte[] songNameByteArray = songName.getBytes("GBK"); byte[] artistNameByteArray = artistName.getBytes("GBK"); byte[] albumNameByteArray = albumName.getBytes("GBK"); int songNameByteArrayLength = songNameByteArray.length; int artistNameByteArrayLength = artistNameByteArray.length; int albumNameByteArrayLength = albumNameByteArray.length; songNameByteArrayLength = songNameByteArrayLength > 30 ? 30 : songNameByteArrayLength; artistNameByteArrayLength = artistNameByteArrayLength > 30 ? 30 : artistNameByteArrayLength; albumNameByteArrayLength = albumNameByteArrayLength > 30 ? 30 : albumNameByteArrayLength; System.arraycopy("TAG".getBytes(), 0, tagByteArray, 0, 3); System.arraycopy(songNameByteArray, 0, tagByteArray, 3, songNameByteArrayLength); System.arraycopy(artistNameByteArray, 0, tagByteArray, 33, artistNameByteArrayLength); System.arraycopy(albumNameByteArray, 0, tagByteArray, 63, albumNameByteArrayLength); // 将流派显示为指定音乐的流派 tagByteArray[127] = (byte) 0xFF; return tagByteArray; } catch (Exception e) { log.error("添加MP3文件TAG标签异常", e); } return new byte[0]; } /** * * 合成mp3文件的id * * * * @songName 名称 * * @artistName 作者 * * @albumName 专辑 * * @return 128字节的D3V2字符串 * * * @return */ public static byte[] composeD3V2(String songName, String artistName, String albumName) { try { byte[] encodeByte = {3}; // 03 表示的UTF8编码 byte[] tagByteArray; byte[] tagHeadByteArray; byte[] tagFrameHeadByteArray; byte[] songNameByteArray = songName.getBytes("UTF-8"); byte[] artistNameByteArray = artistName.getBytes("UTF-8"); byte[] albumNameByteArray = albumName.getBytes("UTF-8"); final int tagHeadLength = 10; final int tagFrameHeadLength = 10; final int tagFrameEncodeLength = 1; final int tagFillByteLength = 20; // 这个填充字节是我看到其他MP3文件ID3标签都会在尾端添加的数据,为了保险起见我也加上了 int byteArrayOffset = 0; int songNameByteArrayLength = songNameByteArray.length; int artistNameByteArrayLength = artistNameByteArray.length; int albumNameByteArrayLength = albumNameByteArray.length; int songNameFrameTotalLength = songNameByteArrayLength + tagFrameEncodeLength; int artistNameFrameTotalLength = artistNameByteArrayLength + tagFrameEncodeLength; int albumNameFrameTotalLength = albumNameByteArrayLength + tagFrameEncodeLength; int tagTotalLength = tagHeadLength + tagFrameHeadLength + songNameByteArrayLength + tagFrameHeadLength + artistNameByteArrayLength + tagFrameHeadLength + albumNameByteArrayLength + tagFillByteLength; int tagContentLength = tagTotalLength - tagHeadLength; tagByteArray = new byte[tagTotalLength]; tagHeadByteArray = new byte[tagHeadLength]; System.arraycopy("ID3".getBytes(), 0, tagHeadByteArray, 0, 3); tagHeadByteArray[3] = 3; tagHeadByteArray[4] = 0; tagHeadByteArray[5] = 0; tagHeadByteArray[6] = (byte) ((tagContentLength >> 7 >> 7 >> 7) % 128); tagHeadByteArray[7] = (byte) ((tagContentLength >> 7 >> 7) % 128); tagHeadByteArray[8] = (byte) ((tagContentLength >> 7) % 128); tagHeadByteArray[9] = (byte) (tagContentLength % 128); System.arraycopy(tagHeadByteArray, 0, tagByteArray, byteArrayOffset, tagHeadLength); byteArrayOffset += tagHeadLength; tagFrameHeadByteArray = new byte[tagFrameHeadLength]; System.arraycopy("TIT2".getBytes(), 0, tagFrameHeadByteArray, 0, 4); tagFrameHeadByteArray[4] = (byte) ((songNameFrameTotalLength >> 8 >> 8 >> 8) % 256); tagFrameHeadByteArray[5] = (byte) ((songNameFrameTotalLength >> 8 >> 8) % 256); tagFrameHeadByteArray[6] = (byte) ((songNameFrameTotalLength >> 8) % 256); tagFrameHeadByteArray[7] = (byte) (songNameFrameTotalLength % 256); tagFrameHeadByteArray[8] = 0; tagFrameHeadByteArray[9] = 0; System.arraycopy(tagFrameHeadByteArray, 0, tagByteArray, byteArrayOffset, tagFrameHeadLength); byteArrayOffset += tagFrameHeadLength; System.arraycopy(encodeByte, 0, tagByteArray, byteArrayOffset, tagFrameEncodeLength); byteArrayOffset += tagFrameEncodeLength; System.arraycopy(songNameByteArray, 0, tagByteArray, byteArrayOffset, songNameByteArrayLength); byteArrayOffset += songNameByteArrayLength; tagFrameHeadByteArray = new byte[tagFrameHeadLength]; System.arraycopy("TPE1".getBytes(), 0, tagFrameHeadByteArray, 0, 4); tagFrameHeadByteArray[4] = (byte) ((artistNameFrameTotalLength >> 8 >> 8 >> 8) % 256); tagFrameHeadByteArray[5] = (byte) ((artistNameFrameTotalLength >> 8 >> 8) % 256); tagFrameHeadByteArray[6] = (byte) ((artistNameFrameTotalLength >> 8) % 256); tagFrameHeadByteArray[7] = (byte) (artistNameFrameTotalLength % 256); tagFrameHeadByteArray[8] = 0; tagFrameHeadByteArray[9] = 0; System.arraycopy(tagFrameHeadByteArray, 0, tagByteArray, byteArrayOffset, tagFrameHeadLength); byteArrayOffset += tagFrameHeadLength; System.arraycopy(encodeByte, 0, tagByteArray, byteArrayOffset, tagFrameEncodeLength); byteArrayOffset += tagFrameEncodeLength; System.arraycopy(artistNameByteArray, 0, tagByteArray, byteArrayOffset, artistNameByteArrayLength); byteArrayOffset += artistNameByteArrayLength; tagFrameHeadByteArray = new byte[tagFrameHeadLength]; System.arraycopy("TALB".getBytes(), 0, tagFrameHeadByteArray, 0, 4); tagFrameHeadByteArray[4] = (byte) ((albumNameFrameTotalLength >> 8 >> 8 >> 8) % 256); tagFrameHeadByteArray[5] = (byte) ((albumNameFrameTotalLength >> 8 >> 8) % 256); tagFrameHeadByteArray[6] = (byte) ((albumNameFrameTotalLength >> 8) % 256); tagFrameHeadByteArray[7] = (byte) (albumNameFrameTotalLength % 256); tagFrameHeadByteArray[8] = 0; tagFrameHeadByteArray[9] = 0; System.arraycopy(tagFrameHeadByteArray, 0, tagByteArray, byteArrayOffset, tagFrameHeadLength); byteArrayOffset += tagFrameHeadLength; System.arraycopy(encodeByte, 0, tagByteArray, byteArrayOffset, tagFrameEncodeLength); byteArrayOffset += tagFrameEncodeLength; System.arraycopy(albumNameByteArray, 0, tagByteArray, byteArrayOffset, albumNameByteArrayLength); return tagByteArray; } catch (Exception e) { log.error("添加MP3文件TAG标签异常", e); } return new byte[0]; } 4.2 语音合成文件中添加语音合成文件是分多个byte[]数据,4.1的两上方未能返回的也是byte[]数据,利用ByteUtils.concat,可以将之个byte[]合并起来,再反馈给前端并保存到OSS中,通过这个方法自动处理的文件就具有TAG头和尾,经过测试前端可以正常访问。 byte[] mp3Tag = XunFeiUtil.composeD3V2("景点介绍", "智慧趣游", "趣游信息"); InputStream is = new ByteArrayInputStream(ByteUtils.concat(mp3Tag, result)); ossFileUtils.uploadFile2OSS(is, fileName); 4.3、前端实现方法const app = getApp() Page({ data: { iac: null, value: 0, //正在播放时长 duration: 0, //总时长 isplay: false, isloading: false, isdrag: false, src: "https://xiyoutianxia.oss-cn-hangzhou.aliyuncs.com/upload/000.mp3" }, onLoad: function(options) { let that = this; that.initInnerAudioContext(); }, //播放 play: function() { if (this.data.isplay) { this.data.iac.pause(); } else { this.data.iac.play(); } }, sliderChanging: function(e) { let that = this; if (!that.data.isdrag) { let drplay = that.data.isplay; that.setData({ isdrag: true, drplay: drplay }); if (drplay) { that.data.iac.pause(); } } }, // 进度条拖拽 sliderChange: function(e) { console.log('sliderChange'); let that = this; let value = parseInt(e.detail.value); let iac = that.data.iac; that.setData({ isdrag: false }); iac.seek(value); if (that.data.drplay) { iac.play(); } }, // 页面卸载时停止播放 onUnload() { //停止播放 this.data.iac.stop(); this.data.iac.destroy(); }, initInnerAudioContext() { let that = this; // 创建音频上下文 let iac = wx.createInnerAudioContext(); iac = wx.createInnerAudioContext(); iac.src = that.data.src; // 监听音频播放事件 iac.onPlay(() => { console.log('onPlay'); that.setData({ isplay: true }); }); // 监听音频播放进度更新事件 iac.onTimeUpdate(function() { if (that.data.isdrag) { return; } let duration = iac.duration; let value = iac.currentTime; that.setData({ duration: duration, value: value }); }); iac.onWaiting(function() { console.log('onWaiting'); that.setData({ isloading: true }); }); iac.onCanplay(function() { console.log('onCanplay') that.setData({ isloading: false }); }); // 监听音频暂停事件 iac.onPause(function() { console.log('onPause'); that.setData({ isplay: false }); }); // 监听音频停止事件 iac.onStop(function() { console.log('onStop'); that.setData({ isplay: false }); iac.seek(0); }); // 监听音频自然播放至结束的事件 iac.onEnded(function() { console.log('onEnded'); that.setData({ isplay: false }); iac.seek(0); }); // 监听音频播放错误事件 iac.onError(err => { console.log('监听音频播放错误事件', err, iac.src); that.setData({ isplay: false, isloading: false }); wx.showToast({ icon: 'none', title: '音频播放出错!', }); }); that.setData({ iac: iac }); }, //格式化秒 00:00 format_sec(sec, is_milli) { if (!sec) { return '00:00'; } sec = parseFloat(sec + ''); if (is_milli) { sec = sec / 1000; } let min = Math.floor(sec / 60); min = (min < 10 ? '0' : '') + min; sec = Math.round(sec % 60); sec = (sec < 10 ? '0' : '') + sec; return min + ':' + sec; } }) 五、小结这个问题隐藏得比较深,初一看是前端兼容性问题,深入分析后是小程序前端BUG再加上数据的问题导致的,数据问题不是总会出现,导致这个问题比较难复现和最终解决。
2020-07-01 - 安卓音频无法播放问题,errCode:10001,errCode:62
安卓部分音频无法播放,请问怎么解决,errCode:10001,errCode:62,err:load or init native decode so fail。
2019-01-25 - 【问题排查】小程序闪退
在使用小程序的时候,偶然会发生闪退。这里来讲一下闪退的问题该如何排查。 版本排查 发生闪退的时候,首先,要确认下 版本 是不是最新的。如果不是,建议更新版本再重试。旧版本的问题会在新版本进行修复哦。 微信版本: 微信官网 基础库版本:基础库更新日志小程序自查 确认版本都是最新情况下,还是有闪退的问题的话,建议先进行小程序自查~ 一般情况下,闪退是因为内存使用过多导致的,小程序侧可以通过基础库提供 wx.onMemoryWarning 接口来监听内存不足的告警,当收到告警时,通过回收一些不必要资源避免进一步加剧内存紧张。 反馈官方 如果问题还是会出现的话建议反馈给官方处理,需要附带上以下信息点协助排查(划重点:完整的提供信息才可以加速问题处理进度哦!!!) 示例: 系统及微信版本号:安卓7.0.17、IOS 7.0.17(出现问题的时候,建议两端都测试,给出有问题的case)必现 or 偶现:必现可复现场景:代码片段 或者 线上小程序复现步骤:进入首页,点击添加按钮等等,推荐录制复现的 视频(重点)进行上传。上传日志:提供微信号,复现时间点(操作步骤:手机微信那里上传下日志: 我 -> 设置 -> 帮助与反馈:右上角扳手 -> 上报日志,选择出现问题的日期,上传日志)
2020-11-03 - 同层渲染失败的原因
同层渲染失败一般有两个原因 1.国外渠道下载的微信,目前不支持同层渲染 2.刚更新的微信,还在准备中,请稍后重试 可在组件中监听同层失败回调 bindrendererror 来判断
2020-09-11 - 小程序同层渲染原理剖析
众所周知,小程序当中有一类特殊的内置组件——原生组件,这类组件有别于 WebView 渲染的内置组件,他们是交由原生客户端渲染的。原生组件作为 Webview 的补充,为小程序带来了更丰富的特性和更高的性能,但同时由于脱离 Webview 渲染也给开发者带来了不小的困扰。在小程序引入「同层渲染」之前,原生组件的层级总是最高,不受 [代码]z-index[代码] 属性的控制,无法与 [代码]view[代码]、[代码]image[代码] 等内置组件相互覆盖, [代码]cover-view[代码] 和 [代码]cover-image[代码] 组件的出现一定程度上缓解了覆盖的问题,同时为了让原生组件能被嵌套在 [代码]swiper[代码]、[代码]scroll-view[代码] 等容器内,小程序在过去也推出了一些临时的解决方案。但随着小程序生态的发展,开发者对原生组件的使用场景不断扩大,原生组件的这些问题也日趋显现,为了彻底解决原生组件带来的种种限制,我们对小程序原生组件进行了一次重构,引入了「同层渲染」。 相信已经有不少开发者已经在日常的小程序开发中使用了「同层渲染」的原生组件,那么究竟什么是「同层渲染」?它背后的实现原理是怎样的?它是解决原生组件限制的银弹吗?本文将会为你一一解答这些问题。 什么是「同层渲染」? 首先我们先来了解一下小程序原生组件的渲染原理。我们知道,小程序的内容大多是渲染在 WebView 上的,如果把 WebView 看成单独的一层,那么由系统自带的这些原生组件则位于另一个更高的层级。两个层级是完全独立的,因此无法简单地通过使用 [代码]z-index[代码] 控制原生组件和非原生组件之间的相对层级。正如下图所示,非原生组件位于 WebView 层,而原生组件及 [代码]cover-view[代码] 与 [代码]cover-image[代码] 则位于另一个较高的层级: [图片] 那么「同层渲染」顾名思义则是指通过一定的技术手段把原生组件直接渲染到 WebView 层级上,此时「原生组件层」已经不存在,原生组件此时已被直接挂载到 WebView 节点上。你几乎可以像使用非原生组件一样去使用「同层渲染」的原生组件,比如使用 [代码]view[代码]、[代码]image[代码] 覆盖原生组件、使用 [代码]z-index[代码] 指定原生组件的层级、把原生组件放置在 [代码]scroll-view[代码]、[代码]swiper[代码]、[代码]movable-view[代码] 等容器内,通过 [代码]WXSS[代码] 设置原生组件的样式等等。启用「同层渲染」之后的界面层级如下图所示: [图片] 「同层渲染」原理 你一定也想知道「同层渲染」背后究竟采用了什么技术。只有真正理解了「同层渲染」背后的机制,才能更高效地使用好这项能力。实际上,小程序的同层渲染在 iOS 和 Android 平台下的实现不同,因此下面分成两部分来分别介绍两个平台的实现方案。 iOS 端 小程序在 iOS 端使用 WKWebView 进行渲染的,WKWebView 在内部采用的是分层的方式进行渲染,它会将 WebKit 内核生成的 Compositing Layer(合成层)渲染成 iOS 上的一个 WKCompositingView,这是一个客户端原生的 View,不过可惜的是,内核一般会将多个 DOM 节点渲染到一个 Compositing Layer 上,因此合成层与 DOM 节点之间不存在一对一的映射关系。不过我们发现,当把一个 DOM 节点的 CSS 属性设置为 [代码]overflow: scroll[代码] (低版本需同时设置 [代码]-webkit-overflow-scrolling: touch[代码])之后,WKWebView 会为其生成一个 [代码]WKChildScrollView[代码],与 DOM 节点存在映射关系,这是一个原生的 [代码]UIScrollView[代码] 的子类,也就是说 WebView 里的滚动实际上是由真正的原生滚动组件来承载的。WKWebView 这么做是为了可以让 iOS 上的 WebView 滚动有更流畅的体验。虽说 [代码]WKChildScrollView[代码] 也是原生组件,但 WebKit 内核已经处理了它与其他 DOM 节点之间的层级关系,因此你可以直接使用 WXSS 控制层级而不必担心遮挡的问题。 小程序 iOS 端的「同层渲染」也正是基于 [代码]WKChildScrollView[代码] 实现的,原生组件在 attached 之后会直接挂载到预先创建好的 [代码]WKChildScrollView[代码] 容器下,大致的流程如下: 创建一个 DOM 节点并设置其 CSS 属性为 [代码]overflow: scroll[代码] 且 [代码]-webkit-overflow-scrolling: touch[代码]; 通知客户端查找到该 DOM 节点对应的原生 [代码]WKChildScrollView[代码] 组件; 将原生组件挂载到该 [代码]WKChildScrollView[代码] 节点上作为其子 View。 [图片] 通过上述流程,小程序的原生组件就被插入到 [代码]WKChildScrollView[代码] 了,也即是在 [代码]步骤1[代码] 创建的那个 DOM 节点对应的原生 ScrollView 的子节点。此时,修改这个 DOM 节点的样式属性同样也会应用到原生组件上。因此,「同层渲染」的原生组件与普通的内置组件表现并无二致。 Android 端 小程序在 Android 端采用 chromium 作为 WebView 渲染层,与 iOS 不同的是,Android 端的 WebView 是单独进行渲染而不会在客户端生成类似 iOS 那样的 Compositing View (合成层),经渲染后的 WebView 是一个完整的视图,因此需要采用其他的方案来实现「同层渲染」。经过我们的调研发现,chromium 支持 WebPlugin 机制,WebPlugin 是浏览器内核的一个插件机制,主要用来解析和描述embed 标签。Android 端的同层渲染就是基于 [代码]embed[代码] 标签结合 chromium 内核扩展来实现的。 [图片] Android 端「同层渲染」的大致流程如下: WebView 侧创建一个 [代码]embed[代码] DOM 节点并指定组件类型; chromium 内核会创建一个 [代码]WebPlugin[代码] 实例,并生成一个 [代码]RenderLayer[代码]; Android 客户端初始化一个对应的原生组件; Android 客户端将原生组件的画面绘制到步骤2创建的 [代码]RenderLayer[代码] 所绑定的 [代码]SurfaceTexture[代码] 上; 通知 chromium 内核渲染该 [代码]RenderLayer[代码]; chromium 渲染该 [代码]embed[代码] 节点并上屏。 [图片] 这样就实现了把一个原生组件渲染到 WebView 上,这个流程相当于给 WebView 添加了一个外置的插件,如果你有留意 Chrome 浏览器上的 pdf 预览,会发现实际上它也是基于 [代码]<embed />[代码] 标签实现的。 这种方式可以用于 map、video、canvas、camera 等原生组件的渲染,对于 input 和 textarea,采用的方案是直接对 chromium 的组件进行扩展,来支持一些 WebView 本身不具备的能力。 对比 iOS 端的实现,Android 端的「同层渲染」真正将原生组件视图加到了 WebView 的渲染流程中且 embed 节点是真正的 DOM 节点,理论上可以将任意 WXSS 属性作用在该节点上。Android 端相对来说是更加彻底的「同层渲染」,但相应的重构成本也会更高一些。 「同层渲染」 Tips 通过上文我们已经了解了「同层渲染」在 iOS 和 Android 端的实现原理。Android 端的「同层渲染」是基于 chromium 内核开发的扩展,可以看成是 webview 的一项能力,而 iOS 端则需要在使用过程中稍加注意。以下列出了若干注意事项,可以帮助你避免踩坑: Tips 1. 不是所有情况均会启用「同层渲染」 需要注意的是,原生组件的「同层渲染」能力可能会在特定情况下失效,一方面你需要在开发时稍加注意,另一方面同层渲染失败会触发 [代码]bindrendererror[代码] 事件,可在必要时根据该回调做好 UI 的 fallback。根据我们的统计,目前同层失败率很低,也不需要太过于担心。 对 Android 端来说,如果用户的设备没有微信自研的 [代码]chromium[代码] 内核,则会无法切换至「同层渲染」,此时会在组件初始化阶段触发 [代码]bindrendererror[代码]。而 iOS 端的情况会稍复杂一些:如果在基础库创建同层节点时,节点发生了 WXSS 变化从而引起 WebKit 内核重排,此时可能会出现同层失败的现象。解决方法:应尽量避免在原生组件上频繁修改节点的 WXSS 属性,尤其要尽量避免修改节点的 [代码]position[代码] 属性。如需对原生组件进行变换,强烈推荐使用 [代码]transform[代码] 而非修改节点的 [代码]position[代码] 属性。 Tips 2. iOS 「同层渲染」与 WebView 渲染稍有区别 上文我们已经了解了 iOS 端同层渲染的原理,实际上,WebKit 内核并不感知原生组件的存在,因此并非所有的 WXSS 属性都可以在原生组件上生效。一般来说,定位 (position / margin / padding) 、尺寸 (width / height) 、transform (scale / rotate / translate) 以及层级 (z-index) 相关的属性均可生效,在原生组件外部的属性 (如 shadow、border) 一般也会生效。但如需对组件做裁剪则可能会失败,例如:[代码]border-radius[代码] 属性应用在父节点不会产生圆角效果。 Tips 3. 「同层渲染」的事件机制 启用了「同层渲染」之后的原生组件相比于之前的区别是原生组件上的事件也会冒泡,意味着,一个原生组件或原生组件的子节点上的事件也会冒泡到其父节点上并触发父节点的事件监听,通常可以使用 [代码]catch[代码] 来阻止原生组件的事件冒泡。 Tips 4. 只有子节点才会进入全屏 有别于非同层渲染的原生组件,像 [代码]video[代码] 和 [代码]live-player[代码] 这类组件进入全屏时,只有其子节点会被显示。 [图片] 总结 阅读本文之后,相信你已经对小程序原生组件的「同层渲染」有了更深入的理解。同层渲染不仅解决了原生组件的层级问题,同时也让原生组件有了更丰富的展示和交互的能力。下表列出的原生组件都已经支持了「同层渲染」,其他组件( textarea、camera、webgl 及 input)也会在近期逐步上线。现在你就可以试试用「同层渲染」来优化你的小程序了。 支持同层渲染的原生组件 最低版本 video v2.4.0 map v2.7.0 canvas 2d(新接口) v2.9.0 live-player v2.9.1 live-pusher v2.9.1
2019-11-21 - 小程序 video 组件同层渲染公测
各位开发者: 大家好。 小程序原生组件因脱离 WebView 渲染而存在一些使用上的限制,为了方便开发者更好地使用原生组件进行开发,我们对小程序原生组件引入了 同层渲染 模式。通过同层渲染,小程序原生组件可与其他内置组件处于相同层级,不再有特殊的使用限制。 现阶段,小程序 video 组件 已切换至同层渲染模式。在该模式下,video 组件可以做到: 1、直接通过 z-index 属性对 video 组件进行层级控制; 2、无需使用 cover-view、cover-image 组件来覆盖 video 组件; 3、可在例如 scroll-view、swiper、movable-view 等内置组件中使用 video 组件; 4、可通过 CSS 对 video 组件进行控制; 5、video 组件不会遮挡 vConsole。 基础库 v2.4.0 及以上版本已默认开启 video 同层渲染,其他原生组件如 input、map、canvas、live-player、live-pusher 等也将逐步切换至同层渲染模式。 欢迎广大开发者进行公测,如有问题,可反馈给我们。 微信团队 2019.02.13
2019-02-15 - 小程序canvas第二次渲染不成功问题解决
[图片] 需求场景 因为微信只能使用人民币结算,在海外使用就无法使用线上支付,所以这里只给用户生成的订单展示,在每个订单上会将所使用的优惠券以QRCode的方式展现给线下到场的服务人员 开发遇到的问题 在使用canvas生成二维码过程中,第一次点击二维码很顺利渲染,但是第二次就不会显示 [图片] 问题定位及解决 经过360°问题定位以及扣官方文档的字眼[/手动捂脸] [图片] canvas的渲染对id有唯一性的要求,如果一个id重复渲染就会出现画布隐藏并不再正常工作的问题,在实现过程中,我把优惠券的id和每次点击时间的时间戳拼接起来以保证canvas-id唯一性 [代码]<canvas canvas-id="{{couponqr.id+timestamp}}" class='qr' /> [代码] [代码]onyh:function(e){ var that = this that.setData({ showqrbox: true, timestamp: e.timeStamp, couponqr: e.currentTarget.dataset.qr, }) wxbarcode.qrcode(e.currentTarget.dataset.qr.id + e.timeStamp, 'http://www.mohennet.com/', 230, 230); }, [代码]
2019-08-08 - 为什么无法在组件内使用selectorQuery获取canvas节点?
我的canvas被包含在了一个二级引用的组件内,在页面上的嵌套顺序如下: page-->组件1-->组件2(包括一个slot)-->canvas(在slot中) 组件2的wxml文件如下图: [图片] 在组件2的js,以及运行情况如下: [图片] 根据微信开发文档,在组件中select节点时,需要把wx.createSelectorQuery换成this.createSelectorQuery,于是,我用后者的方法创建了一个selectorQuery,并执行后面的函数。运行时,却返回了null值。 有大神知道这是怎么个情况吗?另外,我用wx.createSelectorQuery也试过,结果一样。。。 [图片]
2020-04-20 - bindtimeupdate 触发频率 250ms 一次
- 当前 Bug 的表现(可附上截图) - 预期表现 - 复现路径 - 提供一个最简复现 Demo [图片] bindtimeupdateeventhandle 否播放进度变化时触发,event.detail = {currentTime, duration} 。触发频率 250ms 一次但是实际开发的时候,实际并不是250ms触发一次,这个能解决吗?
2019-04-25 - 关于video的bindtimeupdate的精确度
video的bindtimeupdate函数 android真机或者模拟器上,250ms一次触发不够稳定,有时候500ms才触发一次,有办法解决么? ios真机上1s触发一次,什么时候能解决? bindtimeupdate触发频率能否再快一点?比如100ms,一秒四次的频率实在寒碜,不觉得没意义么?和视频相关的操作很难进行,比如需要进行精确的暂停。论坛上也有好多同学提到这个问题。 以上三个问题,坐等骚白解答,骚白不在,其他官人也行,哈哈哈
2018-08-29 - 关于InnerAudioContext.onTimeUpdate()不执行的问题
InnerAudioContext.onTimeUpdate不起作用,多方测试,发现一个奇怪的现象:调试基础库设置为2.6.1,onTimeUpdate可以正常工作,高于2.6.1版本则不工作 但若将autoplay设置为true,又都可以正常工作了,郁闷不?难道必须都设置为自动播放吗?
2019-05-25