- lucky-wheel 开发转盘层级太高,弹窗都被挡住了?
网址 https://100px.net/usage/wx.html,开发的转盘,我需要点击活动规则 弹窗显示规则内容,但是转盘的层级太高,将规则内容挡住了?请问怎么解决? 黑色背景 我是需要层级 高于 转盘 里面显示具体内容的,现在是转盘将黑色背景挡住了,麻烦大家帮忙看下 [图片]
2023-10-16 - 小程序端会话场景下长列表实现
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 - 小程序配置分享的一种实践
在微信浏览器中,H5借助微信的JS-SDK即可配置分享功能。 小程序分享配置介绍在小程序中,同样有分享功能,官方文档中包含了分享到朋友圈及 “转发”给好友及群聊两种 这里主要探讨小程序的分享。 首先看下普通分享,即“转发”,通过阅读文档发现小程序没有提供配置全局分享信息的方法,且默认是没有开启页面分享的 开启方式: page添加onShareAppMessage参数(函数)触发方式: 点击右上角 --> 选择转发给朋友 (menu)点击含有open-type="share"属性的button,会触发页面的onShareAppMessage 事件(事件触发时执行) (button) onShareAppMessage() { const app_version_info = wx.getStorageSync('app_version_info'); return { title: app_version_info.title, imageUrl: "https://xxx/wxapp/share-img.png" }; }, onShareAppMessage函数在点击分享菜单或点击button时触发。 当onShareAppMessage为空函数时,即没有返回配置对象时,微信小程序会自动设置默认分享配置,小程序名作为转发标题,高度为80% 屏幕宽度的图像作为转发图片。 小程序不同场景需求配置分享的策略1. 官方配置思路官方的配置思路是,哪个页面需要,哪个页面添加,不添加则不开启分享 适用场景:当前项目仅部分页面需要配置分享,如活动页面,并可携带动态参数。如果在打开分享后,需要获取更多场景信息,如群标识,见此官方文档。 onShareAppMessage() { return { title: app_version_info.title, imageUrl: "https://xxx/wxapp/share-img.png" path:'xxxx?xx=aa' }; }, 2. 全局默认分享、全局自定义分享、全局开启分享且部分页面自定义分享[8]//在App.js 开头处写入如下立即执行函数表达式 (function(){ var PageTmp = Page; Page =function (pageConfig) { // 设置全局默认分享 pageConfig = Object.assign({ onShareAppMessage:function () { return { title:'默认文案', path:'默认分享路径+id', imageUrl:'默认分享图片', }; } },pageConfig); PageTmp(pageConfig); }; })(); //解释下上述代码做了什么 //在小程序初始化启动时,在上下文中立即执行上述代码,Page函数被赋值给PageTmp变量 //然后Page函数被重写,该函数接受原有初始化Page的Object参数,并添加了onShareAppMessage参数(函数) //然后再调用PageTmp(真正的Page函数),并传修改后的配置对象参数 //IIEF在初始化执行完后立即被销毁,所以功能上完成的重写Page的作用,类似于执行前的拦截 //当在某个页面写了onShareAppMessage时,由于assign在后面,所以页面配置会替换默认配置 开启全局默认分享即不自定义分享信息、部分页面自定义分享需要在页面自行添加onShareAppMessage函数。 3. 仅开启部分页面分享且分享信息一致,一次配置策略// 使用behaviors 统一配置,在需要的页面引入 // ./behaviors/share-config.js // eslint-disable-next-line no-undef module.exports = Behavior({ data: {}, methods: { onShareAppMessage() { return { title: '测试分享标题', imageUrl: 'https://xxx/wxapp/share-img.png', }; }, }, }); //./pages/home/home.js var shareConfigBehavior = require('../../behaviors/share-config.js') Page({ behaviors: [shareConfigBehavior], //... }) 参考: 微信小程序原理分析 - 匠心博客从源码看微信小程序启动过程 - 有赞coder 公众号taro - issues-7470微信小程序全局分享转发控制实现——从此无需页面单独配置 - 简书小程序的全局分享方法onShareAppMessage - segmentfault谈一谈隐藏路由回调API wx.onAppRoute - 微信开放社区从源码看微信小程序启动过程 - 有赞团队关于小程序全局分享 - 微信开放社区如果错误,敬请指出!
2023-01-30 - 微信商户号的千6的手续费具体是指什么?
微信商户号的千6的手续费具体是指什么? 是这样的,我做迎新春的福利红包活动,如果我充值1000元,最后能发给用户的金额是1000还是996(1000-6)? [图片]
2024-02-13 - 官方隐私弹窗拒绝授权回调怎么用?
官方的隐私授权弹窗同意可以没有回调,但是拒绝的回调在哪里?怎么用呢? [图片]
2023-10-20 - 关于小程序隐私保护指引设置的公告
为规范开发者的用户个人信息处理行为,保障用户的合法权益,自2023年9月15日起,对于涉及处理用户个人信息的小程序开发者,微信要求,仅当开发者主动向平台同步用户已阅读并同意了小程序的隐私保护指引等信息处理规则后,方可调用微信提供的隐私接口。 开发者首先需确定小程序是否涉及处理用户个人信息,如涉及,则需配置用户隐私授权弹窗,且仅有在平台《小程序用户隐私保护指引》中声明了所处理的用户个人信息,才可以调用平台提供的对应接口或组件。(隐私相关接口) 隐私协议设置整体流程参考下方指引: 一、设置《小程序用户隐私保护指引》 开发者需在「小程序管理后台」设置《小程序用户隐私保护指引》 [图片] [图片] 二、填写《小程序用户隐私保护指引》 [图片] 只有在指引中声明所处理的用户个人信息,才可以调用平台提供的对应接口或组件。若未声明,对应接口或组件将无法调用成功。隐私接口与对应的处理的用户个人信息关系可见:小程序用户隐私保护指引内容介绍 三、配置用户隐私授权弹窗 微信提供了wx.onNeedPrivacyAuthorization(function callback) 接口,意为用户触发了一个微信侧未记录过同意的隐私接口调用,开发者可通过响应该事件选择提示用户的时机。此外,微信还提供了 wx.requirePrivacyAuthorize(Object object) 接口,可用于模拟触发 onNeedPrivacyAuthorization 事件。 小程序开发者可自行设计提示方式与触发时机,详细文档可查看隐私协议开发指南。 仅有在指引中声明所处理的用户个人信息,才可以调用平台提供的对应接口或组件。若未声明,对应接口或组件将直接禁用。 [图片] (参考样例) 四、如要进行代码提审,开发者需先自行声明是否有采集用户隐私,如有,则需在提审页面-「用户隐私保护设置」选择“采集用户隐私” [图片]
2023-09-18 - Donut多端-云开发API_V2微信支付案例
Donut多端-云开发API_V2微信支付案例 使用的是小程序原生-多端支付 使用条件 1.微信开放平台 2.微信支付(app支付) 3.多端SDK(含支付) 云开发回调这个不知道怎么解决 只能在微信确认支付返回app后确认 有解决方法的可以分享一下 前端js // 调用云开发 //自定义支付信息 const payData = {price,body,attach} cloud('Ger_pay',{ type2:'pay_token', payData }).then(res =>{ console.log('获取sign信息',res) const {timestamp,prepayid,partnerid,appId,noncestr,sign} = res.result; wx.miniapp.requestPayment({ timeStamp: timestamp, mchId: partnerid, prepayId: prepayid, package: 'Sign=WXPay', nonceStr: noncestr, sign: sign, success: (res) => { console.warn('wx.miniapp.requestPayment success:', res) wx.showLoading({ title: '支付中...', mask: true }) // 支付成功返回 cloud('Ger_pay',{ type2:'app_vip', prepayid, payData }).then(res =>{ console.log(res) wx.reLaunch({ url: '../index/index' }) }) }, fail: (res) => { console.error('取消支付', res) wx.showToast({ title: '取消支付', }) } }) }) 云开发index.js // 云函数入口文件 const cloud = require('wx-server-sdk') const pay_token = require('./pay_token.js') cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV }) // 使用当前云环境 const USER_LIST = 'UserList' // 修改付款记录 async function getpre_order(db,prepayid){ try { await db.collection('pre_order').where({ prepayid:prepayid }).update({ data: { payTime: new Date().getTime(), payStatus: 1 // 0:未支付, 1:已支付 }, }) } catch (error) { console.error(error); } } exports.main = async (event,context) => { try { const wxContext = cloud.getWXContext() const db = cloud.database() const unionid = wxContext.UNIONID const time = Date.parse(new Date()); const timestamp = time / 1000; var e = event; if (e.type2 === 'pay_token') { return await pay_token.main(event,context); } if (e.type2 === 'app_vip') { const {prepayid,payData} = e await getpre_order(db,prepayid) } }catch (error) { console.error(error); return { code: -1, message: '服务器错误' + error}; } } [图片] //云开发代码pay_token.js //以下全部照抄即可 const key = "253050DD68FF61CC99D56232E002489" //换成你的商户key,32位 const mch_id = "160755748" //换成你的商户号 const cloud = require('wx-server-sdk') const rp = require('request-promise') const crypto = require('crypto') cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV }) // 使用当前云环境 const db = cloud.database() // 签名接口 function getSign(args) { let arr = new Array(); let num = 0; for (let i in args) { arr[num] = i; num++; } let sortArr = arr.sort(); let sortObj = {}; for (let i in sortArr) { sortObj[sortArr[i]] = args[sortArr[i]]; } // 对传入的对象进行拼接 let sortStr = '' sortArr.forEach(key => { sortStr += key + "=" + sortObj[key] + "&" }); // 拼接密钥 sortStr += "key=" + key // 使用MD5进行加密, 并将加密结果的英文字母全部转换成大写 const sign = crypto.createHash('md5').update(sortStr, 'utf8').digest('hex').toUpperCase(); return sign; } // 转换成xml function getXml(args) { let sa = [] for (let k in args) sa.push('<' + k + '>' + args[k] + '</' + k + '>') sa.push('<sign>' + getSign(args) + '</sign>') return '<xml>' + sa.join('') + '</xml>' } exports.main = async (event, context) => { //前端传入payData const {total_fee,body,attach} = event.payData //total_fee金额 body商品描述 attach附加数据 const wxContext = cloud.getWXContext() const openid = wxContext.OPENID const unionid = wxContext. unionid const appId = appid = 'wx8c699bc1e33333fa9' const attach = attach || 'app支付' //attach 附加数据 const notify_url = "https://api.XXX.com/notify" const spbill_create_ip = "127.0.0.1" const noncestr = nonceStr = nonce_str = Math.random().toString(36).substr(2, 15) const timestamp = timeStamp = parseInt(Date.now() / 1000) + '' const out_trade_no = "otn" + nonce_str + timeStamp const trade_type = "APP" //trade_type支付类型 const sign_type = 'MD5' // 统一下单数据 const xmlArgs = { appid, attach, body: body, mch_id, nonce_str, notify_url, out_trade_no, spbill_create_ip, total_fee, fee_type:'CNY', trade_type, sign_type } // 统一下单 let xml = (await rp({ url: "https://api.mch.weixin.qq.com/pay/unifiedorder", method: 'POST', body: getXml(xmlArgs) })) // 下单完成,返回XML if (xml.indexOf('prepay_id') < 0) return xml // 取出 预支付交易会话标识 let prepay_id = xml.split("<prepay_id><![CDATA[")[1].split("]]></prepay_id>")[0] // 第二次签名 数据 let payArgs = { appid, noncestr, partnerid: mch_id, prepayid:prepay_id, package: 'Sign=WXPay', timestamp, } //保存支付信息到云开发数据库 await db.collection("pre_order").add({ data: { ...payArgs, // 保存支付信息 body, trade_type, openid: openid, // 用户openid unionid:unionid, orderTime: new Date().getTime(), payStatus: 0 ,// 0:未支付, 1:已支付, -1:失效 totalFee:total_fee } }) return { ...payArgs, // 返回数据 把payArgs里面的sign替换成第二次签名sign sign: getSign(payArgs) } }
2023-07-24 - 下发统一消息------公众号模板通知first和remark突然没看到了
[图片] [图片] [图片] [图片] first和remark突然没看到了
2023-05-29 - 关于使用“添加到我的小程序”功能的规范
小程序菜单提供了“添加到我的小程序”功能,目的是为了方便用户快速找到常用小程序,提升使用体验。 然而部分开发者通过诱导或强提示的方式,诱导用户添加至我的小程序,此类情况违反了《微信小程序平台运营规范》5.15 “滥用接口能力行为:微信小程序应当合法、合规、正当、善意地使用腾讯提供的各项功能、接口和能力等,使用时应当遵循微信小程序相关的协议规则,保护微信用户和其他第三方的合法权益,并符合腾讯对微信小程序所提倡的各项要求和建议。不得自行或协助他人滥用、不正当使用、非善意使用上述功能、接口、能力等。” 相关违规类型包括但不限于以下类型: 违规类型1:利益诱导添加小程序 通过优惠券/积分等奖励,诱导添加至我的小程序。 [图片] 违规类型2:强制诱导添加小程序 弹层提示添加至我的小程序,遮挡小程序使用界面,且不可关闭。 [图片] 违规类型3:频繁提示添加小程序 提示弹层每次打开都出现,过度提示影响用户体验。 [图片] 为了保障小程序用户良好的使用体验,请开发者及时调整“添加至我的小程序”提示方式,调整参考指引如下: 1. 添加提示不涉及利益诱导 [图片] 2. 添加提示不遮挡正常界面,可关闭(非弹层类提示体验更佳) [图片] 3. 添加提示进行频率限制 添加提示仅每日首次打开出现,或出现数秒后自动消失。 [图片]
2021-08-12 - 小程序实现索引列表性能优化
简介 本文主要分享个人最近在小程序里面开发索引列表总结经验,索引列表一种一次性展示庞大的数据,并且可以通过点击某个索引跳转到指定索引的内容区域,方便用户快速找到相关的选项 本文直接使用 真实的数据 来深入分析和讨论索引列表 优化手段,功能的实现逻辑不在本文讨论范围,直接去看代码片段即可 本文也为大家带来平时我们经常遇到 假如服务端一次性返回XXX条数据,前端该怎么处理 的真实案例 [图片] 一. 索引列表功能分析 从上面的图中可以分析出有以下需求: 每个分组中都带有一个索引,每个索引分组展示相关的索引数据 每次滚动中,如果当前索引的区域与页面顶部相交,则当前索引区域的标题固定到顶部,同时右边的索引需要变成橙色 每次点击右边的索引,需要滚动到当前索引区域的顶部,同时当前索引区域标题固定到顶部 从上面的功能中,我遇到最难的一个地方就是 如何处理性能优化,如果一次性请求所有数据并且一次性渲染出来,则一些配置不好的手机需要很长的时间才能把页面渲染出来 二. 索引列表性能优化分析 前端处理与列表相关的功能时,通常会选择以下三种性能优化方案: 分页请求:通过 滚动到底部 或 点击某个分页加载按钮 请求指定的页码,避免一次性请求过多数据和一次性渲染过多数据 虚拟列表:通过 对象池 的设计理念(个人观点),一次性创建指定的节点数量同时不断去判断数据列表的数量来增加当前滚动区域,每次滚动到指定区域时直接替换原有对象的数据,页面始终只有指定数量的节点 分片渲染:如果 屏幕一次性必须要展示太多的数据 时,控制在同一时刻触发页面渲染节点数量,通常是列表的每一条数据存在图片,防止一次性渲染过多图片触发太多的图片请求,设计理念有点类似我们平时实现多文件上传时模拟实现的多线程上传逻辑 但是在索引列表的需求中,这些优化的方案实现起来并不是那么简单,下面我来分享我在开发过程中在思考列表性能优化方案的一些问题 2.1 分页请求的问题 索引列表数据量肯定是非常庞大的,第一时间肯定会想到分页请求,然而分页请求在这个需求中存在以下问题: 索引列表通常用户希望每次操作能够更快的看到内容,如果频繁出现 [代码]Loading[代码] 或多次出现请求服务端然后再渲染的卡顿,用户体验会极差 索引列表的分页时机难以决定,交互的方式有右边的索引点击和滚动,然后视图中索引区域会出现交叉的情况 2.2 虚拟列表的问题 索引列表通常一个索引分组下会渲染当前索引组相关的数据,列表要渲染多条数据肯定会想到虚拟列表,然而虚拟列表在这个需求中存在以下问题: 每个索引的区域会有一个可能固定定位的标题展示,在出现两个索引交叉展示的情况,虚拟列表的逻辑就会很难处理 点击右边的索引字母滚动的时候是有动画的,如果使用虚拟列表去实现的话,每次点击索引字母触发快速滚动时,可能会频繁触发虚拟列表滚动的代码逻辑,处理不好容易导致 [代码]UI[代码] 产生更多的 [代码]BUG[代码] 2.3 分片渲染的问题 在分页请求和虚拟列表不好使的情况下,最后能想到的只有分片渲染的优化手段,分片渲染也有以下需要考虑的问题: 要考虑好分片渲染的时机,首先不能直接简单粗暴直接从第一条数据一直分片渲染到最后一条数据,否则用户一旦进入页面就点击最后的字母,然后数据没有渲染出来那就尴尬了 分片渲染在小程序的应用中就是使用多个 [代码]setTimeout()[代码] 和多次调用 [代码]this.setData()[代码] 来在多个时间段触发多次更新,利用多次更新来实现一次 [代码]UI[代码] 展示来优化渲染,在索引列表中频繁触发滚动是很正常的,所以还需要注意 [代码]setTimeout()[代码] 数量以及 [代码]this.setData()[代码] 的调用频率问题 三. 索引列表性能优化基本实现 通过上面对各种性能优化的分析之后,我们还需要结合 索引列表的功能 和 用户的使用习惯 来决定我们的性能优化的最终实现方案: 索引列表功能通常是供用户快速找到自己需要的选项,比如在汽车相关业务的场景中,希望给用户提供一个更快找到自己的车型信息的渠道,通常索引列表比搜索功能更好用(因为搜索需要用户自己输,每个车型还有各种系列,用户肯定是记不住的) 索引列表在用户使用的过程中,会频繁的使用右边的关键词点击快速定位到自己想要的区域,然后再通过滚动找到指定的数据,所以 用户能把所有数据看完的几率几乎不可能 结合 索引列表的操作和展示特点 和 用户几乎不可能看完所有数据 两点我总结出一个思路:每当页面的视图滚动到指定索引区域时,开始渲染指定索引区域的数据 3.1 初始化索引数据和索引区域高度 获取每个索引区域的高度把整个滚动的页面撑起来,目前我们索引列表每一项高度是固定且一致的,所以只要通过每个索引分组的数据量计算出来即可 [代码]const indexListItemHeight = 120 Page({ /** * 初始化索引列表分组数据 */ initIndexListGroupData() { const list: IIndexListGroup[] = indexListKeys.map((key, index) => ({ key, index, height: `${indexListGroups[key].length * indexListItemHeight}rpx`, // 当前区域高度 list: [], })) this.setData({ indexListGroups: list, }) // 初始化索引分组渲染监听和索引分组的物理信息 wx.nextTick(() => { this.initChangeIndexListGroupsRender() this.initIndexListGroupsRect() }) }, }) [代码] 3.2 实现滚动时触发索引区域的当前索引选中 把所有索引区域高度渲染出来后,同时获取所有索引区域的真实节点物理信息,然后在滚动的过程中判断当前的滚动距离并把当前区域的标题变成固定定位 [代码]Page({ /** * 初始化获取索引区域的 top */ initIndexListGroupsRect() { const query = this.createSelectorQuery() query.selectAll('.index-list-column').boundingClientRect() query.exec((res: WechatMiniprogram.BoundingClientRectCallbackResult[][] = []) => { if (res[0]?.length) { this.data._indexListRects = res[0] } }) }, /** * 监听滚动 */ changeScroll(e: WechatMiniprogram.ScrollViewScroll) { const select = this.data.select const scrollTop = e.detail.scrollTop const _indexListRects = this.data._indexListRects const active = _indexListRects.findIndex((item) => (scrollTop - item.top) < item.height) if (select === active) { return } this.setData({ select: active }) }, }) [代码] 3.3 根据屏幕展示当前的索引区域展示对应列表数据 监听当前滚动的位置对应的索引区域,把当前在屏幕展示的区域的数据展示出来,这样就实现了只有屏幕展示相应的索引区域,相关区域的数据才能展示出来 [代码]Page({ /** * 初始化监听索引分组渲染 */ initChangeIndexListGroupsRender() { const observer = this.createIntersectionObserver({ observeAll: true }) observer.relativeToViewport({ top: 0, bottom: 0 }).observe('.index-list-column', (res) => { if (res.intersectionRatio > 0) { const index = res.dataset.index as number const { key, list } = this.data.indexListGroups[index] if (!list.length) { this.setData({ [`indexListGroups[${index}].list`]: indexListGroups[key] }) } } }) } }) [代码] 四. 索引列表性能优化进阶实现 在上面的实现步骤中,已经实现了最基本的性能优化展示(起码比一次渲染全部数据的体验好多了),但是目前还有一些细节需要深入去思考,上面的基本实现只是单纯把 一次性展示索引列表的全部分组的列表数据 优化成 当屏幕展示当前索引列表的某个分组时展示该分组的列表数据,如果当前索引分组的列表数据过多时(如 [代码]A[代码] 字母相关的数据有 [代码]100[代码] 条这样的情况),这样会在一瞬间触发全部图片的 [代码]http[代码] 请求,导致当前能看到的图片列表渲染大概率加载慢而展示不出来 [图片] 对于上面的问题我想到了以下解决思路:在渲染指定索引分组的列表时加入 分片渲染 的优化手段,使用前必须要注意一个问题,不能单纯的从该分组从上往下渲染,因为我们是可以往上滚动的,如我在 [代码]A[代码] 的分组点击了 [代码]Z[代码] 的索引,我往上面滚动我还需要渲染出 [代码]Y[代码] 分组的数据,这时候还需要考虑 从最底部开始渲染 的场景 4.1 处理分片渲染初始化逻辑 当滚动到指定的索引分组区域时,给该分组的列表数据一个 [代码]isLoading[代码] 字段来给列表先展示一个默认的样式,这样就能避免滚动到该分组时会触发该分组的列表所有图片请求 [代码]<block wx:for="{{ group.list}}" wx:key="id" > <view style="height: {{ _indexListItemHeight }}" class="index-list-column-item"> <!-- 默认不触发图片加载 --> <image wx:if="{{ item.isLoading }}" class="index-list-column-item-image" src="{{ item.imageUrl }}" mode="aspectFill" /> <text class="index-list-column-item-name">{{ item.isLoading ? item.name : '加载中...' }}</text> </view> </block> [代码] [代码]Page({ /** * 处理索引分组分片渲染 */ indexListGroupFragmentRender(index: number) { const { key } = this.data.indexListGroups[index] const currentList = indexListGroups[key] // 在初始化列表中增加 isLoading 字段 const initList: ICustomCarModel[] = currentList.map((item) => ({ ...item, isLoading: false })) this.setData({ [`indexListGroups[${index}].list`]: initList }) // 触发分组渲染 wx.nextTick(() => { this.handleFragmentRender(index) }) } }) [代码] 4.2 处理分片渲染的上下加载逻辑 上面已经分析过了一个问题,就是触发一个索引分组数据的分片渲染时,不能简单的从该分组的上面到下面开始渲染,因为这里有个操作是:先点击右边的索引跳转到某个索引分组,然后向上滚动,如果按照上面的简单处理则往上面滚动时不能及时看到下面的数据触发更新,所以我们要做到 同时上下一起触发分片渲染,具体的实现思路为以下: 给某个索引分组增加 [代码]prevIndex[代码] 和 [代码]lastIndex[代码] 来记录两个指针的渲染进度并指定一个 [代码]fragmentCount[代码] 字段为当前上下同时渲染的数量 然后判断当前剩余可渲染的数量来决定 [代码]fragmentCount[代码] 的数量,先去触发上面的渲染,然后再触发下面的渲染 如果每次渲染完毕后发现还有数据可渲染,则使用 [代码]setTimeout[代码] 实现异步递归渲染,这样就能陆陆续续的把所有分组列表渲染完毕 [代码]Page({ /** * 初始化索引列表分组数据 */ initIndexListGroupData() { const list: IIndexListGroup[] = indexListKeys.map((key, index) => ({ key, index, height: `${indexListGroups[key].length * INDEX_LIST_ITEM_HEIGHT}rpx`, prevIndex: 0, // 分片渲染的顶部索引 lastIndex: indexListGroups[key].length - 1, // 分片渲染的底部索引 timer: 0, list: [], })) this.setData({ indexListGroups: list, }) wx.nextTick(() => { this.initChangeIndexListGroupsRender() this.initIndexListGroupsRect() }) }, /** * 分片渲染 */ handleFragmentRender(index: number) { const { prevIndex, lastIndex } = this.data.indexListGroups[index] const dataObj: Record<string, any> = {} let fragmentCount = 4 // 分片渲染的数量 let currentPrevIndex = prevIndex let currentLastIndex = lastIndex // 判断是否有数据分片 if (lastIndex - prevIndex < 0) { return } // 判断是否剩余的数量不够初始分片的数量 if ((currentLastIndex - currentPrevIndex) + 1 < fragmentCount) { fragmentCount = (currentLastIndex - currentPrevIndex) + 1 } while (currentPrevIndex < prevIndex + fragmentCount) { const _key = `indexListGroups[${index}].list[${currentPrevIndex}]` dataObj[`${_key}.isLoading`] = true currentPrevIndex++ } // 判断是否底部还能渲染 if (lastIndex - currentPrevIndex >= 0) { // 判断是否剩余的数量不够初始分片的数量 if ((currentLastIndex - currentPrevIndex) + 1 < fragmentCount) { fragmentCount = (currentLastIndex - currentPrevIndex) + 1 } while (currentLastIndex > lastIndex - fragmentCount) { const _key = `indexListGroups[${index}].list[${currentLastIndex}]` dataObj[`${_key}.isLoading`] = true currentLastIndex-- } } // 分组指针直接使用同步方式记录 this.data.indexListGroups[index].prevIndex = currentPrevIndex this.data.indexListGroups[index].lastIndex = currentLastIndex // 触发页面分片渲染 this.setData(dataObj) // 判断是否还能继续分片,如果有采用异步递归 if (currentLastIndex - currentPrevIndex >= 0) { setTimeout(() => { this.handleFragmentRender(index) }, 500) } } }) [代码] 五. 索引列表性能优化最终实现 经过上面的进阶优化手段,现在的索引列表在开始展示当然索引区域的数据之前,会有一个 分片渲染 的效果,先会触发列表数据的 [代码]Loading[代码] 基本展示,然后再按固定的数量上下分别一起开始加载,这样就能避免当前区域列表中一次性加载 [代码]N[代码] 次的图片导致用户在当前区域的图片加载速度被其他看不到区域的图片给占了 经过再次分析,虽然按照用户的使用习惯来说,通常是不会预览太多的分组区域,更不会把所有的分组区域的数据都看完,但是作为开发者还是要考虑一下这个问题:当前页面的数据存在的节点数量过多导致性能卡顿 目前通过我的分析之后,总结了以下思路: 每次滚动时把其他没有在屏幕展示的分组区域的列表数据清空 每次在数据清空时要注意要清空的区域列表是否在进行分页渲染,如果有则需要把分页渲染停止掉 5.1 给每个索引分组加上一个记录 [代码]setTimeout[代码] 的属性 当 [代码]timer[代码] 的属性存在时,说明该索引分组正在进行分页渲染,分页渲染完毕时需要给该属性设置为 [代码]0[代码]: [代码]Page({ /** * 初始化索引列表分组数据 */ initIndexListGroupData() { const list: IIndexListGroup[] = indexListKeys.map((key, index) => ({ key, index, height: `${indexListGroups[key].length * INDEX_LIST_ITEM_HEIGHT}rpx`, prevIndex: 0, lastIndex: indexListGroups[key].length - 1, timer: 0, // 增加一个 timer 字段记录该分组的延时器 list: [], })) this.setData({ indexListGroups: list, }) wx.nextTick(() => { this.initChangeIndexListGroupsRender() this.initIndexListGroupsRect() }) }, /** * 分片渲染 */ handleFragmentRender(index: number) { // ... // 判断是否还能继续分片,如果有采用异步递归 if (currentLastIndex - currentPrevIndex >= 0) { this.data.indexListGroups[index].timer = setTimeout(() => { this.handleFragmentRender(index) }, 500) } } }) [代码] 5.2 每次滚动时把没有在区域渲染的索引分组数据清空 索引列表是可以上下滚动的,往上面滚动时可能会看到上面分组的最底部的列表数据同时会触发上面分组的标题固定定位;往下面滚动时可以看到下面分组的最顶部的列表数据,所以根据索引列表的特性来分析出来我们要对当前索引分组的上下其他分组制定不同的清空时机: 上面分组列表:上面分组列表是肯定见不到的,所以直接对当前索引分组的上面所有分组列表直接清空即可 下面分组列表:从当前所在的索引分组区域开始往下判断是下面分组列表否在滚动区域(这是因为下面可能会展示多个索引分组区域) 这里还有两个细节处理: 删除列表数据时一定要判断当前索引分组是否在进行分页渲染,否则会出现触发删除后分页渲染还会在继续渲染的 [代码]Bug[代码] 删除列表数据是在监听滚动事件时候执行的,所以一定要做一个防抖处理,否则太过于频繁也会造成不必要的回收影响体验 [代码]Page({ /** * 监听滚动 */ changeScroll(e: WechatMiniprogram.ScrollViewScroll) { const select = this.data.select const scrollTop = e.detail.scrollTop const _indexListRects = this.data._indexListRects const active = _indexListRects.findIndex((item) => Math.floor(scrollTop) - item.top < item.height) // 触发监听分组列表数据清空 this.changeIndexListGroupsClear(this, scrollTop, active) if (select === active) { return } this.setData({ select: active }) }, /** * 监听滚动时 */ changeIndexListGroupsClear: debounce((_this: any, scrollTop: number, index: number) => { const windowHeight = wx.getSystemInfoSync().windowHeight const _indexListRects: BoundingClientRectCallbackResult[] = _this.data._indexListRects const groupList: IIndexListGroup[] = _this.data.indexListGroups const dataObj: Record<string, any> = {} for (let i = 0; i < groupList.length; i++) { const { key, timer } = groupList[i] let isView = true if (i > index) { // 判断当前下面的分组是否在可视区域中 isView = Math.floor(scrollTop) > (_indexListRects[i].top - windowHeight) } // 如果是在当前分组上面或在当前分组下面且不在显示视图区域中,则清空列表数据 if (i < index || !isView) { // 如果当前有分片渲染 if (timer) { clearTimeout(timer) dataObj[`indexListGroups[${i}].timer`] = 0 } dataObj[`indexListGroups[${i}].list`] = [] dataObj[`indexListGroups[${i}].prevIndex`] = 0 dataObj[`indexListGroups[${i}].lastIndex`] = indexListGroups[key].length - 1 } } _this.setData(dataObj) }) }) [代码] 六. 总结 经过上面三个大步骤的实现之后,索引列表的性能优化已经完全实现,而优化后的效果在真实的测试中出现很明显的表现出来,接下来总结一下整体流程: 获取每个索引分组区域的高度,把高度先撑起来实现布局的稳定性 高度撑起之后开始监听当前滚动到哪个分组区域,然后进行分组列表的 初始化渲染 在 初始化渲染 完毕之后,触发当前索引分组的 分片渲染,防止一次性渲染多个图片互抢 [代码]http[代码] 网络资源,导致用户优先要看的数据图片第一时间展示不出来 在渲染太多列表数据后,然后找好时机回收不可见区域的列表数据:当前分组上面看不到的所有分组列表数据可直接清空,当前分组下面的需要判断是否展示在视图里面然后再去清空指定的分组列表数据 七. 最后 本文章以 [代码]Demo[代码] 形式提供本人目前想到的小程序索引列表的优化,这个优化分成三个大步骤,现实的开发场景中可能不需要使用到所有的步骤,这些步骤只是本人能想出来的优化点,帮大家提供小程序索引列表的优化思路 本文章单纯是本人对于小程序索引列表优化开发经验的分享,如果大家有更完善的优化思路务必请在评论区分享出来一起学习分享 该文章的代码形式是以 小程序代码片段 的形式分享,请注意一定要使用自己的手机体验一番,不要用微信开发工具在电脑上体验,因为电脑的性能肯定是比手机好的,如果你同样像我使用 [代码]2000[代码] 块不到的手机去尝试 优化前 和 优化后 的效果,你肯定很明显就能看出来 github:https://github.com/qq31311137/van-wechat-index-list 代码片段:https://developers.weixin.qq.com/s/G9Axg3m77hIu 微信工具版本:[代码]1.02.1812180^[代码](太低的版本无法导入) 基础库:[代码]2.31.0[代码]
2023-05-09 - wasm表格使用
wasm表格使用源码 wasm源码 定义表格,包含2个函数,元素从0开始,定义函数类型,然后导出调用表格的函数,通过表格索引去调用具体函数。函数1返回42,函数2返回13。 [代码](module (table 2 funcref) (func $f1 (result i32) i32.const 42) (func $f2 (result i32) i32.const 13) (elem (i32.const 0) $f1 $f2) (type $return_i32 (func (result i32))) (func (export "callByIndex") (param $i i32) (result i32) local.get $i call_indirect (type $return_i32) ) ) [代码] 小程序调用 定义全局变量 [代码]let inst;[代码] 定义加载wasm函数,并在onload中调用 [代码] loadWasm() { var importObj = { }; WXWebAssembly.instantiate("/wasm/wasm-table.wasm", importObj).then( (result) => { console.log("初始化成功"); inst = result.instance.exports; }, (err) => { console.log("初始化失败", err); } ); }, [代码] 调用wasm的函数,这里写了两个按钮,分别调用不同函数。 [代码] callWasm2() { let i = inst.callByIndex(1); console.log(i); // 返回 13 wx.showToast({ title: ''+i, }) }, callWasm1() { let i = inst.callByIndex(0); console.log(i); // 返回 42 wx.showToast({ title: ''+i, }) }, [代码] 这样就可以了,可以在小程序五位字节码查看效果,有任何问题,欢迎提问讨论。
2022-09-04 - 小程序页面加水印,防止用户截图分享隐私数据
为了防止用户将小程序内的隐私数据进行截图或者录屏分享导致信息泄露,我们会在小程序全局添加一个水印浮层。这样即使被截图或者拍照,也能轻松地确定泄露的源头。 小程序防止用户截屏的方法有很多,其中一种常见的方法就是在小程序的页面中添加水印。具体实现方法如下: 在小程序中的页面中添加水印浮层,一般通过绝对定位来实现,这样可以使水印在页面的最上层,无法被其他元素覆盖。设计水印的样式和位置,通常可以在小程序的样式文件中设置,例如设置水印的位置为右下角,样式为半透明的字体,以免影响正常的内容展示。对于不同类型的页面,可以根据需要添加不同的水印,例如在敏感信息页面添加比较醒目的水印,而在其他普通页面只添加轻微的水印。如果需要防止用户截屏或者拍照,可以在小程序中添加截屏监听事件,并在用户进行截屏或者拍照操作时,自动添加水印。 总的来说,小程序防止用户截屏的方法还有很多,例如使用安全键盘等,但是添加水印仍然是最为常见的一种方法。通过添加水印,可以有效地防止用户截屏和分享敏感信息,保护用户和单位的信息安全。 下面是一个简单的示例,position选择固定定位fixed,固定定位会固定在浏览器窗口某个位置,不会随滚动条滚动。用z-index将元素的层级设置为最低,将view旋转45度,效果就出来啦 [图片] <view style="position: fixed;top: -10vh; left:-100vw;width: 250vw; height: 100vh; z-index: -999;transform: rotate(-45deg);"> <block wx:for="{{30}}" wx:key="index" wx:if="{{userInfo}}"> <view style="color:gray; margin:30rpx; padding:20rpx; opacity: 0.15;"> {{userInfo.user_name + ' ' + userInfo.user_phone}} {{userInfo.user_name + ' ' + userInfo.user_phone}} {{userInfo.user_name + ' ' + userInfo.user_phone}} {{userInfo.user_name + ' ' + userInfo.user_phone}} </view> </block> </view>
2023-02-23 - 小程序一次性订阅消息详解
简介 相对来说小程序发送通知用得更多的还是一次性订阅模版,发现社区挺多同学对一次性订阅的一些细节设定存在不少疑问。所以这里整理了一些常见的问题。 订阅框长这样,本文也围绕这个弹窗展开: [图片] 常见问题Q&A 1.获取模版ID 1.1 如何获取到合适的模版ID? 答:进入MP后台,https://mp.weixin.qq.com, 可以在【功能】->【订阅消息】->【公共模板库】选择合适的模版。选择后在【我的模版】 查看到模版ID。 注意: 不同小程序,选用同一个模版,生成的模版ID是不一样的。 [图片] 1.2 如何申请新的模版? 答:在MP后台申请,入口比较隐蔽。如要在【公共模版库】搜索任意模版,然后翻到最后一页,会出现【帮助我们完善模板库】的选项,点击进入申请新模版。比如此处我搜索了签到 [图片] 2.订阅次数 2.1 勾选了【总是保持以上选择,不再询问】,是不是就可以多次向发送通知了? 答:不能,用户没有再次点击触发订阅的按钮就不会增加订阅次数。勾选之后只是触发订阅时不再弹窗,保持上一次用户的选项进行订阅接口的调用。 2.2 用户一天内多次点击订阅后,是不是就能发送多条通知? 答: 是的,用户一天内点了N次订阅,就可以向用户发送多条通知。 2.3 用户点击订阅后,此次发通知机会的有效期是多久? 答: 永久有效。 2.4 勾选部分模版,再选择勾选 【总是保持以上选择,不再询问】,点击【确定】后,下次订阅哪几个模版的订阅次数会增加? [图片] 答: 如上图,只勾选了AB,然后保持选项点击【确定】,下次订阅时只有A,B模版的订阅次数会增加。 2.5 勾选部分模版,再选择勾选 【总是保持以上选择,不再询问】,点击【取消】后,下次订阅哪几个模版的订阅次数会增加? 答: 如果选择了【取消】+ 【保持选项】,所有订阅消息次数都不会再增加。如上图,不管是否勾选,A、B、C三个模版次数都不会增加。 2.6 勾选【总是保持以上选择,不再询问】选项后,如何修改选择? 答: 可以在小程序设置页面,配置是否接收模版消息。(小程序右上角三点按钮,可以唤起进入设置页面的面板) [图片] 2.7 为什么我设置的通知页面没有显示MP后台配置的通知模版? 答:只有勾选过【总是保持以上选择,不再询问】的模版消息才会进入这个配置页面。 2.8 关闭接口通知按钮后(上图红框内按钮),再开启,订阅次数怎么计算? 答: 关闭按钮后,所有通知都无法接收,订阅次数全部清零。所以重新开启后,也需要用户重新订阅才能再次发送通知。 3.弹窗相关 3.1 模版ABC已经勾选【保持选项】,在另一处同时订阅ADF,此时还会弹窗吗? 答: 会,但是弹窗中只会有两个模版DF,模版A不显示。 3.2 开发者勾选【总是保持以上选择,不再询问】后,怎么重新唤起弹窗? 答:微信开发者工具,清除全部缓存。重新生成二维码,测试机扫码后即可重新唤起弹窗。 Tips:登陆开发者工具的微信账号和需要重置弹窗的微信账号需要保持一致。
2024-09-04 - PHP项目微信提现功能
第一步:使用composer安装EasyWeChat https://www.easywechat.com/docs/3.x/installation 第二步:在前面我们已经讲过,初始化 SDK 的时候方法就是创建一个 EasyWeChat\Foundation\Application 实例: use EasyWeChat\Foundation\Application; $options = [ // ...]; $app = new Application($options); /** * 如果想要在Application实例化完成之后, 修改某一个options的值, * 比如服务商+子商户支付回调场景, 所有子商户订单支付信息都是通过同一个服务商的$option 配置进来的, * 当oauth在微信端验证完成之后, 可以通过动态设置merchant_id来区分具体是哪个子商户 */$app['config']->set('oauth.callback','wechat/oauthcallback/'. $sub_merchant_id->id); 那么配置的具体选项有哪些,下面是一个完整的列表: <?php return [ /** * 账号基本信息,请从微信公众平台/开放平台获取 */ 'app_id' => 'your-app-id', // AppID 'secret' => 'your-app-secret', // AppSecret 'token' => 'your-token', // Token 'aes_key' => '', // EncodingAESKey,安全模式与兼容模式下请一定要填写!!! /** * OAuth 配置 * * scopes:公众平台(snsapi_userinfo / snsapi_base),开放平台:snsapi_login * callback:OAuth授权完成后的回调页地址 */ 'oauth' => [ 'scopes' => ['snsapi_userinfo'], 'callback' => '/examples/oauth_callback.php', ], /** * 微信支付 */ 'payment' => [ 'merchant_id' => 'your-mch-id', 'key' => 'key-for-signature', 'cert_path' => 'path/to/your/cert.pem', // XXX: 绝对路径!!!! 'key_path' => 'path/to/your/key', // XXX: 绝对路径!!!! ], * 更多请参考: http://docs.guzzlephp.org/en/latest/request-options.html */ 'guzzle' => [ 'timeout' => 3.0, // 超时时间(秒) ],]; 第三步:使用EasyWeChat完成提现到零钱功能(企业付款到零钱) 你在阅读本文之前确认你已经仔细阅读了:微信支付 | 企业付款文档 。 与其他支付接口一样,企业支付接口也需要配置如下参数,需要特别注意的是,企业支付相关的全部接口 都需要使用 SSL 证书,因此 cert_path 以及 cert_key 必须正确配置。 <?php use EasyWeChat\Foundation\Application; $options = [ 'app_id' => 'your-app-id', // payment 'payment' => [ 'merchant_id' => 'your-mch-id', 'key' => 'key-for-signature', 'cert_path' => 'path/to/your/cert.pem', 'key_path' => 'path/to/your/key', // ... ],]; $app = new Application($options); $merchantPay = $app->merchant_pay; 企业付款 企业付款使用的余额跟微信支付的收款并非同一账户,请注意充值。 <?php $merchantPayData = [ 'partner_trade_no' => str_random(16), //随机字符串作为订单号,跟红包和支付一个概念。 'openid' => $openid, //收款人的openid 'check_name' => 'NO_CHECK', //文档中有三种校验实名的方法 NO_CHECK OPTION_CHECK FORCE_CHECK 're_user_name'=>'张三', //OPTION_CHECK FORCE_CHECK 校验实名的时候必须提交 'amount' => 100, //单位为分 'desc' => '企业付款', 'spbill_create_ip' => '192.168.0.1', //发起交易的IP地址 ]; $result = $merchantPay->send($merchantPayData); 查询付款信息 用于商户对已发放的企业支付进行查询企业支付的具体信息。 $partnerTradeNo = "商户系统内部的订单号(partner_trade_no)"; $merchantPay->query($partnerTradeNo); 这里以crmeb知识付费系统为例的微信功能基本类extend\service\WechatService;按照文档说明我们在WechatService类中添加企业付款的接口,接口名称为merchantPayService,若是存在就不需要添加; /** * 企业付款 * @return \EasyWeChat\Material\Material */ public static function merchantPayService() { return self::application()->merchant_pay; } 那么我们所需要的就是调用merchantPayService方法里的send方法,并且传入我们转账的参数即可。 我们需要把功能加到提现审核通过之后执行。admin\model\user\UserExtract;找到这个类中的changeSuccess方法,这个方法是审核通过后的相关操作。 (1).先引入WechatService; use service\WechatService; (2).changeSuccess方法里加下面代码,并且根据最后的返回判断企业付款是否成功。 $payData = [ 'partner_trade_no' => str_random(16), //随机字符串作为订单号,跟红包和支付一个概念。 'openid' => $openid, //收款人的openid 'check_name' => 'NO_CHECK', //文档中有三种校验实名的方法 NO_CHECK OPTION_CHECK FORCE_CHECK 're_user_name'=>'张三', //OPTION_CHECK FORCE_CHECK 校验实名的时候必须提交 'amount' => 100, //单位为分 'desc' => '企业付款', 'spbill_create_ip' => '192.168.0.1', //发起交易的IP地址 ]; $result=WechatService::merchantPayService()->send($payData);
2022-10-24 - 小程序安全检测上线公告
为进一步提升小程序的安全性和用户体验,目前平台将对提审的小程序进行安全检测,以便能及时帮助开发者发现小程序可能存在的安全漏洞。 一、背景介绍 小程序在开发过程中若存在安全漏洞的情况,如敏感数据篡改、拖库信息泄露、WEB攻击等,容易造成小程序的安全隐患,可能带来代码易被反编译、核心业务逻辑被破译、算法易被二次打包等风险。因此,平台将对提审的小程序进行安全检测,以协助开发者提升小程序服务的安全性,同时开发者也应加强自身小程序安全漏洞监测能力,保证可及时消除潜在的安全风险。 二、审核过程 安全检测过程中,平台会模拟真实业务场景,向提审小程序的后台发送服务请求,服务器会收到来自平台(显示为:Tencent Security Team,请求IP为106.55.202.118;113.96.223.69;125.39.132.125;43.139.209.119)的请求。该请求均以较低速率进行,正常情况下不会影响小程序的正常服务。若确实出现了影响小程序正常业务的特殊情况,如用户无法进行小程序的正常访问,开发者可基于自身业务情况,对相应请求加以限频,如有其他疑问,欢迎随时通过官方社区进行反馈。 三、审核结果 安全检测的结果是小程序审核的重要参考。若小程序在安全检测中被检测到存在安全漏洞,该小程序的审核将不予通过。开发者可根据扫描报告中的修改指引,对安全漏洞进行相应修复后,再重新进行提审。 其他常见问题 Q1:可以选择不进行安全检测吗,是否会影响小程序代码提审结果? A1:安全检测是小程序审核的环节之一,所有提审的小程序均需进行,若检测中发现安全漏洞或小程序故意采取措施规避检测,该小程序的审核将不予通过。 Q2:若在小程序代码审核已结束或审核已撤销的情况下,可以停止安全检测吗? A2:若在小程序代码审核已结束的情况下,平台将持续进行未完成的安全检测直至完成,如有需要,开发者可通过平台提供的相应链接(在【小程序管理后台 → 通知中心】查看站内信即可)自行中止安全检测;若在小程序审核已撤销的情况下,平台将自动中止未完成的安全检测。 附表:安全检测内容详情 [图片]
2024-03-08 - 微信云开发计费调整公告
各位微信云开发用户: 感谢大家一直以来对微信云开发的支持。由于云计算成本整体上升,为了继续为各位用户提供稳定可靠的服务,微信云开发将于 2022 年 08 月 18 日,对计费方式进行如下变更,部分指标价格将有所上浮。 新计费模式下,新用户免费使用 1 个月后,统一使用 “基础套餐+按量付费” 模式:购买带有一定配额的基础套餐后,超出套餐配额部分再按照实际使用量付费。 基础套餐 [图片] *新用户首月免费配额:与基础套餐一致,可满足大部分情况下的开发体验需求 *5 折折扣有效期至少延续至 2022 年底,后续折扣如有变化将另行通知 按量付费 [图片] 【指标说明】 1、调用次数:合并原多项指标统一计价,具体为 “云存储上传操作”、“云存储下载操作”;“数据库读操作”、“数据库写操作” 及新增指标 “云函数调用操作”; 2、容量:合并原多项指标统一计价,具体为 “存储空间" 及 "数据库容量"; 3、云函数日志服务:本次新增计费指标,建议通过优化日志存储策略降低该指标费用。 新用户手动触发确认开始使用后,将拥有 1 个月免费权益,体验期间的环境配额与基础套餐相同,可选择是否进行超额按量付费。 现有用户可以在「微信开发者工具-云开发控制台」或「微信云服务助手小程序」查询过往用量情况后,使用 价格计算器 预估新价格。 在计费方式升级生效之日(2022.08.08)起,对现有用户均提供至少 1 个月的操作缓冲期,期间会推送提醒及提供切换入口,用户可自由选择是否切换新的计费方式,超时未切换的云开发环境将会停服释放。缓冲期内切换成功的用户将会获得额外代金券。 对于当前月调用次数 >100 万次的帐号,本次调整将不直接生效,后续将由专业架构师联系或独立推送指引切换到合适配额的企业旗舰版本。 自本公告发布之日起,对于原已购预付费套餐、资源包的用户,仍然可正常使用原已购资源,直至原约定套餐到期或用完。现有旧预付费套餐不再支持续费超过一个月,现有旧资源包不再支持新购。 微信云开发将继续为各位用户提供简单易用安全的专业 Serverless 开发服务,感谢大家的支持。 Q&A 1、此次调整,意味着我的费用会增加吗? 是的,由于云计算整体成本的持续上升,为了能够长期给各位开发者提供低门槛的开发服务,我们对于部分计费指标进行了价格调整,在调整后大部分的开发者费用会一定程度的上浮。 2、我如何预估调整之后的费用是多少呢? 新计费生效后,在缓冲期内,开发者可查看新指标下的用量情况,或者根据过往用量,使用 价格计算器 估算调整之后的费用。 我们根据测算,为大部分开发者提供基础套餐配额以满足线上服务,如你的实际用量没有超过基础配额,也没有使用扩展功能,那么每月消费即为 19.9 元。 3、我需要在哪里操作切换新计费呢? [图片] 微信云服务助手小程序码 在计费方式升级生效之日(2022.08.08)起,开发者可前往「微信开发者工具-云开发控制台」或「微信云服务助手小程序」进行操作切换。按照过往的计费信息下发惯例,我们通过「微信公众平台公众号」向管理员推送计费变更信息与切换入口。 4、现在的环境是否可以继续使用呢? 新计费生效后,已购买的预付费套餐可正常使用至原到期时间,已购买资源包的按量付费环境可正常使用至原资源包到期时间,均不会存在直接失效或自动退费的情况。在切换到新计费方案后,原有环境即可持续正常使用。 5、如果超时切换会有什么影响呢? 若原环境到期且超出切换缓冲时间,则该环境将进入停服释放流程,数据不可找回,请开发者及时操作。 6、以上新计费方案生效后,我有多长时间可以决定是否继续使用云开发呢? 对于当前月调用次数 <100 万次的帐号,从公告发布日至新方案生效后的一个月,期间可进行是否继续使用的决策;计费方式升级生效之日(2022.08.08)起,我们也会通过上述 Q&A 3 说明的方式中提供切换入口与下发消息,此时你仍然有至少 1 个月的缓冲期做出决定。对于当前月调用次数 >100 万次的帐号,将有专业架构师联系指引切换到合适配额的企业旗舰版本,期间产生的消耗仍然按旧方案计费。我们会为大额消耗用户提供适当优惠,同时对于不希望继续使用云开发的此类帐号提供不低于 3 个月的缓冲时间。如有其他问题,可前往「微信开发者工具-云开发控制台-帮助-工单」主动联系我们。 7、我自行测算预估后觉得新方案有点贵,有什么好建议吗? 本次调整,价格上涨指标为 “调用次数”、“容量”和“CDN 流量”,建议相应进行技术优化,减少不必要的多次调用或存储内容,可有效控制费用;此外新增计费指标为日志服务,可在「微信开发者工具-云开发控制台-云函数-日志」中查看日志使用情况,并优化日志使用逻辑,避免上报大量冗余的日志信息导致不必要的费用支出。 若评估后认为新的费用方案已不适用于你的业务,可在上述充足的缓冲期内自行选择迁移业务至更合适的服务。 微信云开发团队 2022年7月4日
2022-08-08 - 小程序地理位置相关接口调整
为进一步规范开发者调用涉用户信息相关接口或功能,保障用户合法权益,平台将对如下地理位置相关接口调用实行准入开通: wx.getLocation、wx.onLocationChange、wx.chooseAddress、wx.chooseLocation、wx.choosePoi 自2022年4月18日开始,如使用以上接口,在代码审核环节将检测该接口是否已完成准入开通(申请路径:小程序管理后台 -「开发」-「开发管理」-「接口设置」),如未开通,将在代码提审环节进行拦截,请涉及相关接口的开发者尽快进行接口权限申请,第三方开发者申请方式:可通过 apply_privacy_interface 接口完成。 请广大涉及相关接口的开发者尽快进行相关接口准入申请,如未申请,后续将影响线上小程序相关接口的使用。
2023-09-26 - SDK jsApiLIst数组返回空
config:ok, jsApiLIst数组返回空,公众号后台有对应接口权限 ready回调里调用对应接口报错 the permission value is offline verifying [图片] [图片]
2022-01-18 - 云函数资源4万GBS是多少?能否举例量化说明?
假设一个云函数使用256M内存,平均运行时长为500ms 那么每次调用需要消耗0.25G*0.5S=0.125GBS 4万GBS一共能支撑这个云函数运行32万次
2020-10-28