- 小程序端会话场景下长列表实现
1 前言 腾讯云医小程序中有医生和患者聊天的场景,在处理该场景的列表过程中遇到两个问题: 一是下拉加载历史消息时需要在容器顶部进行衔接导致的界面抖动问题;二是大量的会话内容导致的长列表问题。 问题一:插入历史消息带来的抖动问题是因为在已有dom的前面插入dom。如果能够在已有dom的后面插入新增dom并且在视觉上看起来是在顶部插入的则可以解决该问题。前端开发中聊天场景的体验优化一文中给出的方案是[代码]transform:rotate(180deg);[代码]。另外[代码]flex-direction:reverse[代码]也是可以做到的。 由于会话场景的一些其他特点如列表初始化时定位在底部(新消息在底部),本文的实现采用了[代码]transform:rotateX(180deg)[代码]方式处理进行处理。由于只需要在垂直方向进行翻转,所以在实现时使用rotateX代替了rotate。 下面简易demo说明该样式应用后的效果 [代码] .container { height: 100px; overflow: auto; } .item { width: 100px; border: 1px solid black; text-align: center; } /*关键*/ .x_reverse { transform: rotateX(180deg); } [代码] [代码]<div class="container x_reverse"> <div id="item-1" class="x_reverse item">数据项-1</div> ... <div id="item-9" class="x_reverse item">数据项-9</div> </div> [代码] 添加[代码].x-reverse[代码]样式前后的初始状态对比 翻转前 [图片] 翻转后 [图片] 问题二: 长列表问题。 我们先在h5端看下大量的dom会有哪些问题,如下demo验证 [代码]<button id="button">button</button><br> <ul id="container"></ul> [代码] [代码]document.getElementById('button').addEventListener('click',function(){ let now = Date.now(); const total = 10000; let ul = document.getElementById('container'); for (let i = 0; i < total; i++) { let li = document.createElement('li'); li.innerText = Math.random() ul.appendChild(li); } }) [代码] 在chrome的开发者工具performance栏下记录点击button后的运行过程,可以看到包含脚本运行在内的整个运行过程中 Rendering部分占用时间最多(包含[代码]Recalculate Style[代码]、[代码]Layout[代码]、[代码]Update Layer Tree[代码])。当列表项数越多并且列表项结构越复杂的时候,会在Recalculate Style和Layout阶段消耗大量的时间,所以有必要减少列表项的同时渲染。 [图片] 小程序的架构决定着小程序端该问题相较于h5端更为突出。在微信小程序官方文档 -> 指南 -> 性能与体验部分提到一些点如:setData数据大小、WXML节点数等原因都会影响到小程序的性能。以及图片资源的主要性能问题在于大图片和长列表图片上,这两种情况都有可能导致 iOS 客户端内存占用上升,从而触发系统回收小程序页面。显然在长列表场景下如果一次性将所有的数据全部加载出来就会有WXML节点过多,setData数据量过大的问题、图片资源过度等问题。 这些问题不仅仅是列表在初始化的时候存在,如在插入新数据(unshift)需要将整个数组进行传递,以及更新列表项数据时diff时间也会增大。 微信小程序官方提供了recycle-view组件来解决等高列表项的情况。但是对于会话场景下消息的高度是不等的,因此我们得自己实现一套符合这种特性的长列表组件。 2 接入前后对比 2.1 视频效果对比 对比腾讯云医小程序会话接入长列表组件前后的效果,优化前滚动过程中有卡顿的感觉,并且在发送消息的时候,消息输入框进入到列表中的延迟能够比较明显的感受到,优化后滚动较丝滑,并且发送消息没有明显的延迟。 接入前:https://baike-med-1256891581.file.myqcloud.com/yidian/production/article-john/after-chat.mp4" 接入后:https://baike-med-1256891581.file.myqcloud.com/yidian/production/article-john/before-chat.mp4" 对比腾讯云医小程序->群发助手下的患者列表初始化和选中时接入长列表组件前后的对比 接入前 :https://baike-med-1256891581.file.myqcloud.com/yidian/production/article-john/group-send-before.mp4 接入后:https://baike-med-1256891581.file.myqcloud.com/yidian/production/article-john/group-send-after.mp4" 2.2 数据对比 这里对比下群发助手接入前后的setData(发起到回到)时间的对比 初始化用时对比 [图片] 选中item用时对比 [图片] 上面两张图的横坐标是数据条数,纵坐标是setData时间,可以看到无论是初始化还是选中操作二者的轨迹都是相似的 明显的看到接入前,setData的时间随着数据量的增大越来越大,接入后则没有这个问题。显然,接入后通信的数据量,diff时间,浏览器渲染时间都会较少。 3 基础实现 关于长列表实现的基本思路是只渲染可视区域及其附近的几屏数据,但是由于小程序端和h5端架构的差异导致二者在具体实现上存在差异。 3.1 如何模拟滚动条? h5实现长列表的常规思路 [代码]<div id="list-container"> <div id="list-phantom"></div> <div id="list"> <!-- item-1 --> <!-- ... --> <!-- item-n --> </div> </div> [代码] #list-container 滚动容器 通过引入#list-phantom来占位,高度为列表项高度之和用于撑开容器形成滚动条 #list用来装载列表数据 当有新的列表项添加后,则更新#list-phantom高度从而达到模拟滚动条的目的,然后通过监听#list-container的scroll事件在其回调中根据scrollTop来计算出现在可视区域的内容。 浏览器是多进程多线程架构,浏览器中打开一个tab页时可以认为是打开一个渲染进程,渲染进程中包含了GUI渲染线程(包括了html、css解析,dom树构建等工作)和js引擎线程等等。我们知道GUI渲染线程和JS引擎线程是互斥的,js引擎发起界面更新到渲染完成是同步的。 而小程序架构的通信是异步的,比如逻辑层setData发起通信到渲染层,通信过程中渲染层依然在执行的。如果按照h5的思路去计算,逻辑层计算的结果到达渲染层后就已经不是正确的结果了即界面中的数据和滚动条的位置是对不上的。 为了保证滚动条的位置和数据项所在位置是正确对应,起初的想法是列表项消失后通过一个同等高度的div元素进行代替,这样做带来的问题是依然会产生大量的dom元素。进一步的想法是通过对列表数据进行分组并且每个分组在界面中会存在一个真实的dom(称为分组dom)来包裹该分组内的所有列表项,并且认为每个分组算是一屏数据,当每个分组从界面中消失时,分组dom不会删除,只会删除内部的列表项,并且将消失的列表项高度之和赋值给该分组dom。这样解决了滚动条高度的问题,并且不需要计算具体哪些列表项数据需要被加载出来,只需要知道加载哪(些)个分组即可。 分组的想法既简化了计算又保证了数组项和滚动条的位置是正确对应的。 高度的获取和赋值是在wxs里面做的,由于wxs是在渲染层执行的,相比在逻辑层减少了通信的成本。 下面给出简易(伪)代码来描述这段过程 视图层 [代码]<scroll-view clearingids="{{clearingGroupIds}}" renderingids="{{renderedGroupIds}}" change:clearingids="{{module.clearingHandle}}" change:renderingids="{{module.renderingHandle}}" class="list-wrapper x_reverse"> <!-- 分组dom --> <view class="piece-container" wx:for="groups" wx:for-item="group" id="piece-container-{{group.id}}"> <view class="x_reverse" wx:for="group.data"> {{item.content}} </view> </view> </scroll-view> [代码] 逻辑层 [代码]// 分组数据结构(二维数组) groups:[ { id: 1, // 分组id data:[{ content:{a:'a'} },...] } ,...], // 当前需要渲染的分组 renderingids:[], // 需要移除的分组 clearingGroupIds:[] [代码] wxs 更新分组dom高度,用法参考官方文档WXS响应事件 [代码]module.exports = { clearingHandle:function(clearingGroupIds, oldV, ownerInstance){ clearingGroupIds.forEach(function(groupId){ // 1. 根据 groupId 找到对应的分组dom // 2. 获取分组dom高度 // 3. 设置分组dom样式:height }) }, renderingHandle: function (renderingGroupIds, oldGroup, ownerInstance) { renderingGroupIds.forEach(function(groupId){ // 1. 根据 groupId 找到对应的分组dom // 2. 移除height样式 }) } } [代码] 3.2 如何知道渲染哪些数据 当有新的数据需要渲染到列表中时,首先是对数据进行分组,然后通过小程序提供的IntersectionObserver能力对分组dom进行监听,在其回调中判断该dom是否进入scroll-vew从而来更新正在渲染的分组和需要移除的分组。 [代码] // 滚动容器domId const containerId = '#scroll-container-xxx' // 创建监听 _createObserver(groupIds = []) { groupIds.forEach(groupId => { const observer = wx.createIntersectionObserver(this).relativeTo(containerId); observer.observe(domId, this._observerCallback); }) } // 监听回调 _observerCallback(result) { // 1. 根据result拿到domId然后解析拿到groupId(domId包含了groupId信息 // 2. 判断当前分组是否在视口内,如果不在视口内直接返回 // 3. 如果分组在视口内,则计算需要渲染的分组ids和需要移除的分组ids // 4. 通信至视图层,渲染目标分组数据和移除失效的分组数据( // 4.1 移除的优先级不高,不应该阻塞渲染目标分组,因此可以通过debounce/throttle处理) // 4.2 短时间内多次setData会导致通信通道阻塞,比如可以将setData放在队列中处理,一个一个来(中间可能有些失效则可以跳过 } [代码] 总结:基于2.1和2.2已经可以完成基本的雏形,另外有些其他的点需要优化 4 优化 4.1 unshift带来的问题 在小程序中通常将列表数据存储到数组中,由于小程序setData的数据量越小越好,更新数组时通常不会将整个数组对象进行setData,而只是更新数组对象的某个属性,如下: [代码]// 在数组尾部插入数据时 小程序支持下面方式 this.setData({ [array[array.length]]: newObj }) // 更新数组中某项的属性时 this.setData({ [array[0].a]: 'a' }) [代码] 如果要向数组顶部插入数据,做不到只传递新增的数据 [代码]array.unshift({}) this.setData({array}) // => 缺点是 逻辑层到渲染层会传递整个数组对象 [代码] 本文的背景是要解决会话场景下的长列表问题,对于会话即存在插入历史消息的场景,又存在插入新消息的场景,相当于我们数组两端都需要有插入数据的能力。需要对数据进行push/unshift操作。但是前面提到unshift效果不好。因此本文通过两个数组,一个数组存放历史消息,一个数组存放新消息,并在dom结构上也增加了对应的结构。 dom结构如下 [代码]<scroll-view class="x_reverse"> <view class="next-item-wrapper"> <!--多了一层--> <view class="x_reverse"> <!--新消息区域--> <view wx:for=“new-groups” wx:for-item="group"> <view wx:for="group.data"> {{item.content}} </view> </view> </view> </view> <view class="history-item-wrapper"> <!--历史消息区域--> <view wx:for=“his-groups” wx:for-item="group"> <view class="x_reverse" wx:for="group.data"> {{item.content}} </view> </view> </view> </scroll-view> [代码] 区域定义: 历史消息区域:初始化的消息以及插入的历史消息 新消息区域:列表初始化完成之后新来的消息 制定了如下规则 分组id越大表示分组的消息越久远,分组id越小表示分组的消息越新 历史分组id从1开始递增,新消息区域分组id从0开始递减 新消息区域自身未做任何的翻转,就像正常的列表一样,有新的消息或者新的分组push就行 历史消息区域的分组受到翻转的影响,在历史消息分组中push新的消息或者新的分组表现为插入历史消息 其原理如下图 [图片] 与上面dom结构对应的数据结构如下 [代码]class fuse { constructor() { // 存储历史消息 this.histGroups = []; // groupId >= 1 // 存储新消息 this.newGroups = []; // groupId <= 0 } // 插入新消息 push(listGroups){ this.newGroups.push(...listGroups) } // 插入历史消息 unshift(listGroups){ this.histGroups.push(...listGroups) } } [代码] 4.2 白屏问题 4.2.1 白屏现象的解释 滚动过程中长列表组件会进行setData操作以更新视口区域的数据,在快速滚动的情况下,假设此时逻辑层的计算结果是需要渲染第3屏幕的数据,但是由于从逻辑层通信到视图层是需要时间,这段时间中第三屏的界面可能已经滚动到视口外,此时的渲染是无效的,用户看到的可能已经是第8屏的数据,但是这个时间点第8屏幕的数据并没有渲染,这就会导致白屏现象的出现。 如果我们能根据屏幕滚动的速率和通信的时间去预测下一帧哪一屏出现在视口区域,那么就可以避免白屏问题。显然这是个难题,因为你不知道用户什么时候会调整滚动的速度,并且setData的时间也受限于很多因素。因此小程序架构下长列表组件带来的白屏问题是无解的。但可以通过预加载上下几屏的数据等一些其他优化方案降低白屏出现的几率以及给出一些骨架效果来缓解用户的焦虑。 4.2.2 骨架效果模拟 由于WXML节点过多也会影响长列表性能,因此否定了渲染真实dom来实现骨架,目前是通过图片作为背景通过在垂直方向平铺的方式来模拟骨架效果。 这种方式对于列表项是等高的场景是完美的解决方案,对于列表项非等高的场景可能会看到背景有被’截断‘情况。不过实际体验来看在快速滚动的情况下,这种’截断‘被看到的概率是偏低的,从实际效果来看是可以接受的。 等高列表项(患者列表 ):https://baike-med-1256891581.file.myqcloud.com/yidian/production/article-john//video-1.mp4 非等高列表项(会话 ):https://baike-med-1256891581.file.myqcloud.com/yidian/production/article-john//video-2.mp4 4.3 图片高度异步确定带来的麻烦 加载图片资源需要经过网络,属于异步加载,因此img标签的高度的确定也是异步的。假设一种场景,当前分组中的图片资源尚未加载完成,由于滚动的发生需要将该分组中的列表项移除,显然这个时候给分组dom设置的高度是不准确的,当下一次重新渲染该分组时,图片重新加载到完成后,该分组的高度会发生生变化,此时会发生界面的跳动,该如何处理呢? 通过添加滚动锚定特性处理。滚动锚定是指当前视区上面的内容突然出现的时候,浏览器自动改变滚动高度,让视区窗口区域内容固定,就像滚动效果被锚定一样。因此通过设置滚动锚定特性可以解决界面跳动的问题 也可以通过动画的过渡效果来缓解跳动现象,这依赖于height相关的样式属性,因此需要给分组dom设置相关的样式值。 可以显示的给分组dom设置height样式:比如可以在图片加载完成后通知长列表组件去更新分组dom的高度,当高度设置了css3过渡动画,就会以动画形式展开。 也可以通过给分组dom设置min-height/max-height代替height,并给min-height/max-height设置css3动画。上面使用height方式存在一个问题,分组的高度只有在增高的前提下才会被感知,没有降低的可能性;而通过min-height/max-height组合(min-height:0,max-height:height + 1000px),分组高度的增加和降低都会被感知到 本文的实现是:滚动锚定 + min-height/max-height 下面是更新min-height/max-height的核心代码,通过监听 renderingids & clearingids属性的变化,在change回到中处理相关逻辑。 [代码]<scroll-view clearingids="{{clearingGroupIds}}" renderingids="{{renderedGroupIds}}" change:clearingids="{{chat.clearingHandle}}" change:renderingids="{{chat.renderingHandle}}" /> [代码] wxs [代码]// 分组消失时 设置mix-height/max-height = 实际高度 clearingHandle: function (clearingGroupIds, oldV, ownerInstance) { clearingGroupIds.forEach(function (groupId) { // 获取分组dom var pieceContainer = ownerInstance.selectComponent('#piece-container-' + groupId) var res = pieceContainer.getComputedStyle(['height']) pieceContainer.setStyle({ 'min-height': res.height, 'max-height': res.height }) }) // 分组重新渲染时 // min-height设置为0,实际的高度由分组中的列表项撑开 renderingHandle: function(renderingGroupIds, oldV, ownerInstance) { renderingGroupIds.forEach(function (groupId) { // 获取分组dom var pieceContainer = ownerInstance.selectComponent('#piece-container-' + groupId) var res = pieceContainer.getComputedStyle(['height']) // 高度大于一瓶 足够视口区域的内容发挥了 var maxHeight = parseInt(res.height) + 1000 + 'px' pieceContainer.setStyle({ 'min-height': '0' }) pieceContainer.setStyle({ 'max-height': maxHeight }) }) } [代码] 事实上最完美的方式是在上传图片的时候记录图片的宽高比例等信息,在渲染时计算好img标签高度,而不是依赖图片的加载结果,这样可以保证img标签高度是同步确定的。退一步的做法是可以在图片第一次加载完成后缓存宽高,再次渲染的时候显示的设置img标签宽高。 5 其他 5.1 由于翻转带来的其他副作用 ios下transform:rotate会导致z-index无效 Safari 3D transform变换z-index层级渲染异常的研究–张鑫旭。在Safari浏览器下,此Safari浏览器包括iOS的Safari,iPhone上的微信浏览器,以及Mac OS X系统的Safari浏览器,当我们使用3D transform变换的时候,如果祖先元素没有overflow:hidden/scroll/auto等限制,则会直接忽略自身和其他元素的z-index层叠顺序设置,而直接使用真实世界的3D视角进行渲染。 scroll-into-view无效问题 该问题在另一篇文章中说到过并且给出了解决方案。 小程序scroll-view翻转后 scroll-into-view的替代方案 5.2 根据groupNums计算待渲染/移除的分组id 本文实现的长列表组件提供了groupNums属性,该属性用来指定每个分组包含多个列表项。上文说到我们在IntersectionObserver监听的回调中来计算需要渲染的下一屏分组id。 如果长列表组件不存在删除元素的操作,那么假设当前进入视口的分组id是x,并且总是额外显示上一屏和下一屏的分组。那么当x是边缘分组时,目标分组就是[x,x+1] 或 [x-1,x];当x不是边缘分组的情况,目标分组是[x-1, x, x+1] 由于本文实现的长列表组件提供了删除中间列表项的方法,假设x,x-1,x+1这三个分组都被删除只剩下1一个列表项,那么按照上述计算方式计算返回的分组渲染出来后实际上可能还不够一屏。这个时候我们需要利用groupNums这个指标进行计算,比如当分组在中间时,得确保有3 * groupNums个列表项被渲染出来。 5.3 scroll-view底部回弹区域setData时跳动问题 问题:滑动页面到底部,使其出现橡皮筋效果,处于橡皮筋效果时SetData数据,会使页面跳动一下,处于橡皮筋效果时SetData会使页面跳动闪屏 解决方案:关闭橡皮筋效果即可 示例代码: [代码]<scroll-view enhanced="{{true}}" bounces="{{false}}" /> [代码] 5.4 一条消息的布局 问题:当滚动区域只有少数列表项,这些列表项高度之和小于滚动容器高度时,由于对滚动容器应用了翻转样式,此时列表项会布局在底部(应该在顶部) 解决方案:通过包裹在一个div内,应用如下样式解决 示例代码: [代码]<scroll-view class="x_reverse"> <view class="all-container"> <view class="next-item-wrapper">...</view> <view class="history-item-wrapper">...</view> </view> </scroll-view> [代码] [代码].all-container { display: flex; flex-direction: column; justify-content: flex-end; height: auto; min-height: 100%; } [代码] 5.5 自动弹出加载更多组件 问题:以加载历史消息为例,当消息滚动到顶部下拉开始加载历史消息时,如果只是设置showLoadMore为true,视觉上会看不到loadmore组件(原因是scroll-view设置了滚动锚定),需要再次向下拉一次,才能把该组件拉入到视区内。显然这样的体验不够好,如果拉到顶部开始加载历史消息时,该组件自动出现在用户的视觉内效果会好些。 示例代码(old): [代码]<scroll-view class="x_reverse"> <view class="all-container"> <view class="next-item-wrapper">...</view> <view class="history-item-wrapper">...</view> </view> <view class="x_reverse"> <load-more wx-if={{showLoadMore}}/> </view> </scroll-view> [代码] 解决方案:通过两个变量loadingDone&loading来维护该组件,loading为true时显示上面的组件,loadingDone为true时显示内部的组件 示例代码(new): [代码]<block> <!--正在加载,显示这里--> <load-more wx-if={{loading}}/> <scroll-view class="x_reverse"> <view class="all-container"> <view class="next-item-wrapper">...</view> <view class="history-item-wrapper">...</view> </view> <view class="x_reverse"> <!--没有更多数据了,显示这里--> <load-more wx-if={{loadingDone}}/> </view> </scroll-view> </block> [代码] 5.6 计算reccordIndex 在不删除中间列表项的情况下,传递的recordIndex是准确的,通过数学关系在wxs中实时进行计算 [代码]<list-item recordIndex="{{chat.calculateIndex(group, groupNums, index, renderedHistorySum)}}" /> [代码] wxs [代码]// index 当前列表项在当前分组的索引 // groupNums 单个分组列表项数 // renderedHistoryGroups是历史区域的列表项数 // group 用于获取groupId calculateIndex: function (group, groupNums, index, renderedHistorySum) { if (group.id > 0) { // 历史区域 return renderedHistorySum - ((group.id - 1) * groupNums + index) - 1 } return renderedHistorySum + (-group.id) * groupNums + index } [代码] [代码]observers: { 'renderedHistoryGroups.**'() { let renderedHistorySum = 0; const { renderedHistoryGroups, groupNums } = this.data; if (renderedHistoryGroups.length) { const { data: endGroupData } = getEndElement(renderedHistoryGroups); renderedHistorySum = (renderedHistoryGroups.length - 1) * groupNums + endGroupData.length; } this._setDataWrapper({ renderedHistorySum }); }, }, [代码] 5.7 抽象节点 列表项组件是通过抽象节点注入给长列表组件的 6 总结 下面是基于文中所述实现的目录,所有逻辑层代码放在behavior中以共享,normal-scroll针对普通场景的长列表,而chat-scroll针对会话场景的长列表。 [图片]
2022-02-17 - 微信小程序如何实现页面传参?
前言 只要你的小程序超过一个页面那么可能会需要涉及到页面参数的传递,下面我总结了 4 种页面方法。 路径传递 通过在url后面拼接参数,参数与路径之间使用 ? 分隔,参数键与参数值用 = 相连,不同参数用 & 分隔;如 ‘path?key=value&key2=value2’。 案例:A页面带参数跳转到B页面 A页面跳转代码 [代码]goB(){ wx.navigateTo({ url: '/pages/B/index?id=value', }) }, [代码] B页面接收代码 [代码]onLoad: function (options) { console.log('id', options.id) } [代码] 上面的案例是字符串参数,但是很多情况下需要传递对象,如下方代码。 [代码]Page({ data: { userInfo:{ name:'cym', age:16 } }, goB(){ wx.navigateTo({ url: '/pages/B/index?id='+this.data.userInfo, }) }, }) [代码] 如果使用上面同样的方式结构,输出的结果是:[object Object] 这个时候需要先把对象通过JSON.stringify(obj)将 object 对象转换为 JSON 字符串进行参数传递,再到接收页面通过JSON.parse解析使用。 A页面跳转代码 [代码] goB(){ let userStr = JSON.stringify(this.data.userInfo) wx.navigateTo({ url: '/pages/B/index?id='+userStr, }) } [代码] B页面接收代码 [代码]onLoad: function (options) { console.log('id', JSON.parse(options.id)) } [代码] 全局变量 通过App全局对象存放全局变量。 app.js代码 [代码]App({ // 存放对象的全局变量 globalData:{}, }) [代码] A页面跳转代码 [代码]// 获取App对象 const app = getApp() Page({ /** * 页面的初始数据 */ data: { userInfo: { name: 'cym', age: 16 } }, goB() { app.globalData.userInfo = this.data.userInfo wx.navigateTo({ url: '/pages/B/index', }) }, }) [代码] B页面接收代码 [代码]// 获取全局对象 const app = getApp() Page({ onLoad: function (options) { console.log(app.globalData.userInfo) } }) [代码] 存放在 App 全局变量里面,可以被多个页面使用,直接从 App 对象获取即可。这个数据是保持在内测中,每次小程序销毁就没有了。 数据缓存 通过存储到数据缓存中。 A页面跳转代码 [代码] goB() { wx.setStorageSync('userInfo', this.data.userInfo) wx.navigateTo({ url: '/pages/B/index', }) } [代码] B页面接收代码 [代码] onLoad: function (options) { let userInfo = wx.getStorageSync('userInfo', this.data.userInfo) console.log(userInfo) } [代码] 存放在数据缓存里面,可以被多个页面使用,直接用 getStorageSync 获取即可。这个数据是保持在数据缓存中,除非清楚数据缓存或者删除小程序否则一直存在。 事件通信 通过事件通信通道。 A页面跳转代码 [代码]goB() { wx.navigateTo({ url: '/pages/B/index', success:(res)=>{ // 发送一个事件 res.eventChannel.emit('toB',{ userInfo: this.data.userInfo }) } }) } [代码] B页面接收代码 [代码]onLoad: function (options) { // 获取所有打开的EventChannel事件 const eventChannel = this.getOpenerEventChannel(); // 监听 index页面定义的 toB 事件 eventChannel.on('toB', (res) => { console.log(res.userInfo) }) } [代码] 总结 大家可以针对具体业务场景来进行选择合适自己的传参方式。
2022-02-19 - (5)小程序插件
背景 小程序插件简介小程序插件是可以被开发者添加到小程序内直接使用的,能为用户提供具体服务的功能组件。 插件开发者可像开发小程序一样开发插件,除了在自己的小程序内使用,还能提供给其他小程序直接使用。 插件使用者无需独立开发小程序内的所有服务,无需理解插件内部逻辑和实现方式,可直接使用别人开发好的插件,为用户提供相应服务。 推出小程序插件的原因小程序的初衷是希望更好地连接用户和服务。自上线以来,越来越多的商户、开发者开发自己的小程序。 早晨起来在小区电梯内,扫小程序码,即可叫个早餐外卖; 走到门口,扫共享单车的二维码,骑车即走; 到了公司,即可打开小程序轻松打卡,开始美好的一天上班生活…… 随着小程序的普及,越来越多的路边小摊、餐饮小店、夫妻店也希望接入小程序。许多商家会给我们留言: “我只会简单开发,不会做复杂的功能怎么办?” “我也想给餐馆小程序做一个预约订餐功能,要怎么搞?” “客户可以在我的小程序里查询快递信息吗?” “我没有数据,可以在小程序里做地图查找功能吗?”…… 我们总结出大家目前遇到的几个难题: 1.开发技术有限,实现复杂功能难度大 2.人力、设备、资源有限,实现服务成本高 3.缺乏某些类目的资质,如电商、打车 小程序插件的应用1.开发共享,避免重复开发 在小程序内使用插件,可免去小程序开发者重复开发,一定程度上减少了开发工作量。 如:在旅游景区的小程序可以使用地图插件,开发者无需在小程序内独立开发地图内导航、出行指引、周边服务推荐等能力,直接使用地图插件即可为用户提供导航服务,免去很大量的开发工作。 2.插件可提供针对行业的完整服务,同时覆盖线上线下 商家希望借助小程序更好地连接用户和服务,包括线上、线下服务。线下服务往往人力成本、硬件设备等要求较高,商家在小程序内提供完整服务的门槛较高。而插件不仅可以解决线下服务的问题,还能针对特定行业提供全套完整服务。 以景区服务小程序为例,使用门票购买插件,用户可在小程序内完成门票线上预订、购买等流程,在到达景区后,通过插件服务提供商提供的移动终端或硬件设备,可完成门票兑换、核销。 而对于餐饮、零售等线下行业而言,插件更是极大降低了商家的成本,商家可以使用预订、排队、外卖等插件,由插件开发者提供线下服务,商家只需在小程序内引用插件,即可使用由插件开发者提供的预订、外卖等服务,节省了成本。 3. 降低服务门槛的问题,实现“服务共享” 具有开发能力及服务资质的开发者,可将自己的服务封装成插件,提供给其他小程序使用,实现“服务共享”。 插件的技术原理插件本身的技术原理并不复杂。插件代码由一些自定义组件和 JS 代码文件构成,插件开发者在发布插件时,这些代码被上传到微信后台保存起来。 当小程序使用插件时,使用者需填写插件的 AppID 和版本号,以便从后台获取相应的插件代码。小程序代码编译时,插件代码会被嵌入到小程序中,与小程序一起编译运行。 小程序与插件的交互根据需要,插件代码可以提供 JS 接口或自定义组件供小程序调用。JS 接口可以用于界面无关的逻辑,自定义组件可以嵌入界面中展示。 对于插件使用者来说,插件的调用方法很简单。 1.使用插件 JS 接口 与普通 JS 文件间 [代码]require[代码] 的方式一样,只需要将[代码]require(FILE_NAME)[代码] 改成[代码]requirePlugin(PLUGIN_NAME)[代码] 这样的调用形式。 2.使用插件提供的自定义组件 与使用普通自定义组件的方法非常相似。在[代码]json[代码]文件的 [代码]usingComponents[代码] 段落中,按照[代码]plugin://PLUGIN_NAME/COMPONENT_NAME[代码] 的形式使用即可。 对于插件开发者而言,这里给出几点建议: 1.提供充分的接口文档和示例,方便插件使用者了解插件的使用方法; 2.注意保持插件不同版本间接口的稳定,使插件使用者能顺利升级插件; 3.计算小程序代码包大小时,插件代码也会一并计算在内,所以应当控制好插件代码的大小。 插件代码运行环境插件代码的运行环境与小程序代码有一定的区别,主要是以下两点: 1.插件可以发起网络请求的域名与小程序不同,因此插件开发者需要注意在插件后台的设置中正确配置网络请求域名。 2.插件可以调用的 API 有一定的限制,并不是所有的接口插件都可以调用,如页面跳转相关的接口是不允许插件调用的。插件开发者在使用一些特殊接口前,请查询文档中的插件可调用接口列表。 此外,目前一个小程序最多可使用5个插件,赶紧将插件用起来,节约开发者成本吧。 详情可参考《小程序插件接入指南》。
2018-08-17 - 小程序迁移,AppId、AppSecret、openid、unionID的变化情况?
小程序迁移,AppId、AppSecret、openid都不会变,unionID会视情况而定: 1、如果只是单纯的小程序迁移,不涉及open帐号变化,unionID就不会变。但是有可能会因为主体改变而影响unionID的获取逻辑,请开发者根据实际情况调整UnionID的获取方式。 2、如果涉及到open帐号变化,unionID就会改变,因为unionID就是根据open帐号的信息直接计算出来的。
2019-11-20 - (10)群聊能力
我们在后台收到很多朋友的反馈,希望更好的运用小程序群聊的能力。于是我们想写写群聊的故事。 微信群是小程序在微信这个社交工具下传播的重要途径,我们经常能通过群聊看见小程序的身影。我们希望开发者在实现小程序逻辑的时候,能理解每一个群聊,可实现小程序与各个群聊紧密相关的功能。 基于此,我们开放了群聊 ID(openGID)的功能,供开发者区分标识每个群聊。对于每个群聊而言,小程序所获取到的 openGID 是不变的。但对于同一个群,不同的小程序内获得的 openGID 是不一样的。这一特性类似于标识用户身份的 openID。 拥有了群聊 ID,开发者可以把用户的操作按照群聊 ID 来聚合、沉淀信息,实现群协作功能。此外,通过 openID+openGID 的方式,还可以实现群排行的功能。 例如“群影”小程序以群聊ID聚合用户上传的图片,实现群相册的功能。 [图片] (“群影”小程序) 01 如何获取群聊ID 开发者获取 openGID 要依托于用户转发到群聊的小程序卡片,具体步骤如下: 1 设置带 shareTicket 的分享 在小程序内,开发者调用接口wx.updateShareMenu 带参数withShareTicket:true ,设置当前页面分享到群聊时能获取 openGID。而shareTicket本身就是获取 openGID 的凭证。 [图片] 而 iOS/Android App 分享场景当中,微信SDK也支持把所分享的消息设置成带 shareTicket。 值得注意的是,带 shareTicket 的分享卡片会被固定在某个群聊的,也就是说分享卡片会变成不能被长按转发。 2 由启动参数获取 shareTicket 当用户从某个带 shareTicket 的卡片进入小程序时,开发者可以在App.onLaunch 或者App.onShow 获取 shareTicket,而在小游戏上开发者可以通过监听 wx.onShow 或者同步调用wx.getLaunchOptionsSync 获取shareTicket。 shareTicket 实际上是小程序启动时临时生成的变量,在小程序生命周期内仅作为调用接口的凭证。生命周期结束后 shareTicket 就没有意义了。 3 通过 shareTicket 获取 openGID 开发者调 wx.getShareInfo 接口以 shareTicket 换取 openGID 的加密数据包,这是为了保证开发者服务器收到的 openGID 是可信的,开发者需要把加密数据交由后台解密,拿到真实的 openGID。数据加密机制更多请参看[数据加密相关文档]。 注意事项 ▷▷ 由于2018年7月5日起,新提交发布的小程序版本将无法通过用户分享获得群ID,即开发者通过wx.onShareAppMessage获取群 ID 的方式将不再支持,后续仅支持通过启动参数获取群 ID。请开发者及时调整。 02 群聊名称组件 除了群聊 ID 以外,开发者还能使用群聊对应的名称。出于保护用户隐私的考虑,我们不会把真实的群聊名称暴露给开发者,而是通过 open-data 组件让开发者在小程序前端展示某个 openGID 对应的群名称。 [图片] 其中 openGID 就是小程序获取到的群聊 ID。 open-data 组件只展示那些用户所在群聊ID对应的名称。如果设置了非微信提供的群聊 ID,将无法展现群聊名称。 03 群聊功能 1 分享设置 wx.updateShareMenu : [查看文档] 2 小程序启动参数 App.onLaunch / App.onShow : [查看文档] 3 小游戏启动参数 wx.onShow : [查看文档] getLaunchOptionsSync : [查看文档] 4 获取 openGID wx.getShareInfo : [查看文档] 5 群名称组件 open-data>/open-data>: [查看文档]
2018-08-17