- input键盘弹出时,滚动页面,输入框内容错位问题
在ios9.+的版本上,遇到了一个问题。在键盘弹出的时候,快速滚动页面,输入的内容或input提示语会错位。且有时无法恢复到原有位置。当然,前提是你这个页面是超过一屏需要滚动的时候 [图片] 解决办法: always-embed=true [图片]
2022-04-18 - setData 学问多
为什么不能频繁 setData 先科普下 setData 做的事情: 在数据传输时,逻辑层会执行一次 JSON.stringify 来去除掉 setData 数据中不可传输的部分,之后将数据发送给视图层。同时,逻辑层还会将 setData 所设置的数据字段与 data 合并,使开发者可以用 this.data 读取到变更后的数据。 因此频繁调用,视图会一直更新,阻塞用户交互,引发性能问题。 但频繁调用是常见开发场景,能不能频繁调用的同时,视图延迟更新呢? 参考 Vue,我们能知道,Vue 每次赋值操作并不会直接更新视图,而是缓存到一个数据更新队列中,异步更新,再触发渲染,此时多次赋值,也只会渲染一次。 [代码]let newState = null; let timeout = null; const asyncSetData = ({ vm, newData, }) => { newState = { ...newState, ...newData, }; clearTimeout(timeout); timeout = setTimeout(() => { vm.setData({ ...newState, }); newState = null }, 0); }; [代码] 由于异步代码会在同步代码之后执行,因此,当你多次使用 asyncSetData 设置 newState 时,newState 都会被缓存起来,并异步 setData 一次 但同时,这个方案也会带来一个新的问题,同步代码会阻塞页面的渲染。 同步代码会阻塞页面的渲染的问题其实在浏览器中也存在,但在小程序中,由于是逻辑、视图双线程架构,因此逻辑并不会阻塞视图渲染,这是小程序的优点,但在这套方案将会丢失这个优点。 鱼与熊掌不可兼得也! 对于信息流页面,数据过多怎么办 单次设置的数据不能超过 1024kB,请尽量避免一次设置过多的数据 通常,我们拉取到分页的数据 newList,添加到数组里,一般是这么写: [代码]this.setData({ list: this.data.list.concat(newList) }) [代码] 随着分页次数的增加,list 会逐渐增大,当超过 1024 kb 时,程序会报 [代码]exceed max data size[代码] 错误。 为了避免这个问题,我们可以直接修改 list 的某项数据,而不是对整个 list 重新赋值: [代码]let length = this.data.list.length; let newData = newList.reduce((acc, v, i)=>{ acc[`list[${length+i}]`] = v; return acc; }, {}); this.setData(newData); [代码] 这看着似乎还有点繁琐,为了简化操作,我们可以把 list 的数据结构从一维数组改为二维数组:[代码]list = [newList, newList][代码], 每次分页,可以直接将整个 newList 赋值到 list 作为一个子数组,此时赋值方式为: [代码]let length = this.data.list.length; this.setData({ [`list[${length}]`]: newList }); [代码] 同时,模板也需要相应改成二重循环: [代码]<block wx:for="{{list}}" wx:for-item="listItem" wx:key="{{listItem}}"> <child wx:for="{{listItem}}" wx:key="{{item}}"></child> </block> [代码] 下拉加载,让我们一夜回到解放前 信息流产品,总避免不了要做下拉加载。 下拉加载的数据,需要插到 list 的最前面,所以我们应该这样做: [代码]this.setData({ `list[-1]`: newList }) [代码] 哦不,对不起,上面是错的,应该是下面这样: [代码]this.setData({ list: this.data.list.unshift(newList) }); [代码] 这下好,又是一次性修改整个数组,一夜回到解放前… 为了解决这个问题,这里需要一点奇淫巧技: 为下拉加载维护一个单独的二维数组 pullDownList 在渲染时,用 wxs 将 pullDownList reverse 一下 此时,当下拉加载时,便可以只修改数组的某个子项: [代码]let length = this.data.pullDownList.length; this.setData({ [`pullDownList[${length}]`]: newList }); [代码] 关键在于渲染时候的反向渲染: [代码]<wxs module="utils"> function reverseArr(arr) { return arr.reverse() } module.exports = { reverseArr: reverseArr } </wxs> <block wx:for="{{utils.reverseArr(pullDownList)}}" wx:for-item="listItem" wx:key="{{listItem}}"> <child wx:for="{{listItem}}" wx:key="{{item}}"></child> </block> [代码] 问题解决! 参考资料 终极蛇皮上帝视角之微信小程序之告别 setData, 佯真愚, 2018年08月12日
2019-04-11 - 复杂瀑布流长列表页踩坑记录,内存不足问题【2】
第二期来啦,带来了新的方案和代码片段~ 第一期点此查看 上期问题 经过一系列的实践,上期的方案有些问题,其中最麻烦的就是,需要对外传递一个当前index,然后控制前后数据展示;这里对于每个用到[代码]skeleton[代码]组件的页面来说,都要重复的写一个方法来承接这个index,然后渲染页面对应的数据。 优化 依然是监听[代码]skeleton[代码]曝光,这里监听的方案变为出现在屏幕上下[代码]n[代码]屏的内容块进行展示,此范围外的内容块就卸载掉。 核心代码 [代码] // 修改了监听是否显示内容的方法,改为前后showNum屏高度渲染 // 监听进入屏幕的范围relativeToViewport({top: xxx, bottom: xxx}) let info = SystemInfo.getInfo() //获取系统信息 let { windowHeight = 667 } = info.source.system let showNum = 2 //超过屏幕的数量,目前这个设置是上下2屏 let listItemContainer = this.createIntersectionObserver() listItemContainer.relativeToViewport({ top: showNum * windowHeight, bottom: showNum * windowHeight }) .observe(`#list-item-${this.data.skeletonId}`, (res) => { // 此处来控制slot展示,详见代码片段 }) [代码] 干货 话不多说,干货放后面,点击获取代码片段
2019-12-05 - 小程序端会话场景下长列表实现
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 - 小程序实现高性能虚拟列表优化+节流+分页请求(固定高度)
场景引入 为什么需要用到高性能虚拟列表+节流+分页请求的优化? 当有场景需求为需要将大量数据(10000条)呈现在一页上,我们不断下拉访问,页面中有大量的数据列表的时候,用户会不会有不好的体验?会不会出现滚动不流畅而卡顿的情况?会不会因卡顿而出现短暂的白屏现象(数据渲染不成功)? 通过微信开发者工具自带的调试器->Network页面,我们可以观察到当有长列表时如果不使用高性能虚拟列表+节流+分页请求的优化,会出现以下问题: FPS:每秒帧数,图表上的红色块表示长时间帧,很可能会出现卡顿。 CPU:CPU消耗占用,实体图越多消耗越高。 NET:网络请求效率低,一次性请求10000条的渲染效率远远低于分1000次,每次请求10条数据 内存:滑动该列表时明显能看到内存消耗大。 总结:如果需要将大量数据(10000条)呈现在一页上,可以通过高性能虚拟列表+节流+按需请求分页数据并追加显示。 优化的具体实现可拆分为以下需求(将一个大问题拆分为一个个小问题并逐个去解决): 不把长列表数据一次性全部直接显示在页面上。 截取长列表一部分数据用来填充屏幕容器区域。 长列表数据不可视部分使用使用空白占位填充。 监听滚动事件根据滚动位置动态改变可视列表。 监听滚动事件根据滚动位置动态改变空白填充。 分页从服务器请求数据,将一次性请求所有数据变为滚动到底部才再次向服务器发送获取数据的请求 开始实战 此次实例我设定每个元素的固定高度为210rpx [图片] (1)首先计算屏幕内的容积最大容量(即屏幕一次性可以容纳多少个高度为210rpx的元素) 调用wx.createSelectorQuery()的api [图片] 上图是id为scrollContainer的组件,滚动时触发函数handleScroll() [代码]// 计算容器的最大容积,onReady中触发,即初次渲染时触发 getContainSize() { wx.createSelectorQuery().select('#scrollContainer').boundingClientRect(function (rect) { rect.id // 节点的ID rect.dataset // 节点的dataset rect.left // 节点的左边界坐标 rect.right // 节点的右边界坐标 rect.top // 节点的上边界坐标 rect.bottom // 节点的下边界坐标 rect.width // 节点的宽度 rect.height // 节点的高度 }).exec((option) => { // console.log(~~(option[0].height / this.data.oneHeight) + 2); this.data.containSize = ~~(option[0].height / this.data.oneHeight) + 2; }) }, [代码] 调用api后返回的option中的height就是小程序页面的视口高度,除以oneHeight(210rpx)就是能容纳的个数,用[代码]~~[代码]来对结果进行向下取整(实际能容纳的应+1),由于在滚动时会出现上一个元素在上边界还没完全消失,第四个元素就从下边界进入视口了,因此最大容纳量应再+1。即实际容纳量应该如图最后一行代码所示+2. [图片] (2)监听滚动事件动态截取数据 监听用户滚动、滑动事件,根据滚动位置,动态计算当前可视区域起始数据的索引位置 startIndex,再根据containsize,计算结束数据的索引位置 endIndex,最后根据 startIndex与endIndex截取长列表所有数据a11Datalist 中需显示的数据列表 showDatalist。 PS:下列代码中函数handleScroll()最下面的[代码]this.setDataStartIndex(data);[代码]才是滚动时真正进行的滚动事件动态截取数据,上面那些代码用途在文章后面部分再详细介绍(节流)。 [代码]// 定义滚动行为事件方法 handleScroll(data) { if (this.data.isScrollStatus) { this.data.isScrollStatus = false; // 节流,设置一个定时器,1秒以后,才允许进行下一次scroll滚动行为 let mytimer = setTimeout(() => { this.data.isScrollStatus = true; clearTimeout(mytimer); }, 17) this.setDataStartIndex(data); } }, [代码] [代码]// 执行数据设置的相关任务, 滚动事件的具体行为 setDataStartIndex(data) { // console.log("scroll active") this.data.startIndex = ~~(data.detail.scrollTop / this.data.oneHeight); // 通过scrollTop滑动后距离顶部的高度除以每个元素的高度,即可知道目前到第几个元素了 this.setData({ showDataList: this.data.allDatalist.slice(this.data.startIndex, this.data.endIndex) }) // 动态截取实际拥有10000条数据的数组中下标为startIndex到endIndex的数组出来呈现在前端页面上 // 容器最后一个元素的索引 if (this.data.startIndex + this.data.containSize <= this.data.allDatalist.length - 1) { this.setData({ endIndex: (!this.data.allDatalist[this.data.endIndex]) ? this.data.allDatalist.length - 1 : this.data.startIndex + this.data.containSize // 滚动到底部了吗,是的话那就将endIndex设置为9999,不然的话设置为startIndex+视口最大容量 }) } else { console.log("滚动到了底部"); this.data.pageNumNow++; // 例如一次性从数据库拿10条数据赋值到allDataList,如果滚动到底部(即allDataList所有数据都已经呈现了),那就再次向服务器发送请求获取数据库中的下10条数据 this.addMes(); // 该函数内就写你实际向数据库请求时的代码,请求成功后拼接到allDataList即可 console.log(this.data.allDatalist.length) } }, [代码] (3)使用计算属性动态设置上下空白占位 我们设置了根据容器滚动位移动态截取ShowDatalist 数据,现在我们滚动一下发现滚动2条列表数据后,就无法滚动了,这个原因是什么呢? 在容器滚动过程中,因为动态移除、添加数据节点丢失,进而强制清除了顶部列表元素DOM节点,导致滚动条定位向上移位一个列表元素高度,进而出现了死循环根据 startIndex和endIndex的位置,使用计算属性,动态的计算并设置,上下空白填充的高度样式blankFi11Sty1e,使用padding或者margin 进行空白占位都是可以的 PS:由于小程序没有computed,所以为了使用计算属性,得另外引入封装好computed的包,引入computed组件的教程我放在最后,我使用的是官方推荐的computed,且注意使用该插件时,不要加[代码]this -> this.data[代码],直接data即可 [代码]computed: { // 定义上空白高度 topBlankFill(data) { // console.log("change") return data.startIndex * data.oneHeight; }, // 定义下空白高度 BottomBlankFill(data) { return (data.allDatalist.length - data.endIndex) * data.oneHeight; }, // 定义一个 待显示的数组列表元素 showDataList(data) { // console.log(data.allDatalist.slice(data.startIndex, data.endIndex)) return data.allDatalist.slice(data.startIndex, data.endIndex) }, }, [代码] (4)下拉置底自动请求加载数据 下方代码中[代码]setDataStartIndex()[代码]函数末尾的if-else便是判断是否已经滚动到现有全部数据的allDataList数组是否已经滚动到底部,全部呈现完了。 如果是那就执行else部分,请求的页数pageNumNow+1,然后调用addMes()请求数据,然后将新请求到的数据进行拼接到allDataList上 [代码]// 执行数据设置的相关任务, 滚动事件的具体行为 setDataStartIndex(data) { // console.log("scroll active") this.data.startIndex = ~~(data.detail.scrollTop / this.data.oneHeight); this.setData({ showDataList: this.data.allDatalist.slice(this.data.startIndex, this.data.endIndex) }) // 容器最后一个元素的索引 if (this.data.startIndex + this.data.containSize <= this.data.allDatalist.length - 1) { this.setData({ endIndex: (!this.data.allDatalist[this.data.endIndex]) ? this.data.allDatalist.length - 1 : this.data.startIndex + this.data.containSize }) } else { console.log("滚动到了底部"); this.data.pageNumNow++; this.addMes(); console.log(this.data.allDatalist.length) } }, // 根据接口数据来给数组添加真实数据 addMes: function () { this.list() .then(res => { // console.log(res.result.data); // 将接口获取到得所有数据存储起来 this.data.allDatalist = this.data.allDatalist.concat(res.result.data); // 设置初始显示列表 this.setData({ showDataList: this.data.allDatalist.slice(0, 5) }) }) }, // 以下为获取数据 list: function () { let pageNum = this.data.pageNumNow; let pageSize = 20; // console.log('当前请求的页码为:' + pageNum); return new Promise((resolve, reject) => { wx.cloud.callFunction({ name: 'teacher', data: { action: 'list', pageNum, pageSize } }).then(res => { resolve(res) }).catch(err => { reject(err) }) }) }, [代码] 到此处,高性能虚拟列表+分页请求的优化已经搞定了,下面开始节流的优化 (5)滚动事件节流定时器优化 由于监听滚动事件触发对应函数方法的频率是极高的,因此页面节流优化是必须的。 方法:在data中声明一个属性scro11State用来记录滚动状态,只有scro11State值为true的时候才会具体执行 PS:下面代码中定时器设置为17ms的原因是,由于小程序没有web的requestAnimationFrame的api,无法作滚动事件节流请求动画帧优化,因此可以手动计算显示器的帧率,大概一帧在17ms,因此定时器设置为17ms [代码]// 定义滚动行为事件方法 handleScroll(data) { //节流部分代码 if (this.data.isScrollStatus) { this.data.isScrollStatus = false; // 节流,设置一个定时器,1秒以后,才允许进行下一次scroll滚动行为 let mytimer = setTimeout(() => { this.data.isScrollStatus = true; clearTimeout(mytimer); }, 17) //节流部分代码 this.setDataStartIndex(data); } }, [代码] 到这为止,节流也搞定啦,觉得从中学到了许多的小伙伴不妨点个赞。 小程序如何引入computed计算属性请参考我的这篇文章:https://developers.weixin.qq.com/community/develop/article/doc/000a4442bd44c84e740d6b6b051413 在后续我也会将高性能虚拟列表+节流的优化封装成一个插件,给小伙伴们直接使用,欢迎关注我以便及时获取到我文章的更新~ 在这里也推荐一篇防抖和节流的性能优化知识介绍的文章:https://segmentfault.com/a/1190000018428170 觉得有帮助的小伙伴欢迎点赞,有其他问题也欢迎在评论区提出
2021-11-11 - 微信开发者工具下载的 sourcemaps 怎么用。
什么是 Sourcemaps uglifyjs、bable 等工具会对 源代码 进行编译处理生成编译后的代码(下称目标代码),而 sourcemaps 就是保留了目标代码在源代码中的 位置信息 --------- 大神分割线 --------- 如何解读 Sourcemaps Sourcemaps 是一个 json [代码]{ "version": 3, "sources": ["a.js", "b.js"], // 源文件列表,这个表示是由 a.js 和 b.js 合并生成 "names": ["myFn", "test"], // 如果开启了变量名混淆,这里会保留变量名在源文件中名字信息 "sourcesContent: [], // 可选项,保存源码信息,顺序与 sources 字段对应,chrome 的 sources 面板中源码使用了这个字段的内容进行展示 "sourceRoot": "", // 源文件所在的目录信息 "file": "dist.js", // 可选,编译后的文件名 "mappings": "" // 这个是重点,是目标代码和源文件的位置的映射关系 } [代码] mappings 目标文件"行"的信息 mappings 是使用 ; 分隔的,每个部分对应目标代码的行 如: “;AAAA;AAAA,BBBB;;” 本例子目标文件有 4 行 第 0 行和第 3 行没有源文件对应信息,所以这两行是编译过程中加入的代码 目标文件的"列"信息 如: “AAAA,CAEA,CAEA;” ‘,’ 表示行内的位置信息分隔符 本例表示目标文件的这一行有三个有效的位置信息。 位置信息的第一位表示目标文件的列的 偏移 信息 本例中,表示列的信息是 ‘A’、‘C’、‘C’,对应的数字为 0、+1、+1,(vlq 编码,在线编解码工具) 注意,这个是偏移信息; 列数从 0 开始,依次累加偏移值可以算出当前的位置信息对应的真正的列 所以本例中表示的是目标文件的第 n 行中的第 0 列,第 1 列,第 2 列(没错是第 2 列) 源文件的信息 如:‘AAAA;ACAA;ADAA;’ 位置信息的第二位表示源文件的信息,本例子中是 ‘A’、‘C’、‘D’,对应数字是 0、+1、-1 如果 sourcemaps 中的 sources 字段只有一个文件的话,那么位置信息中第二位一直是 A(不需要偏移) 假设 sourcemaps 中 sources: [‘a.js’, ‘b.js’] 本例的意思是 AAAA: 目标文件第 0 行第 0 列 对应 第 0 个文件 a.js ACAA; 目标文件第 1 行第 0 列 对应 第 1 个文件 b.js ADAA; 目标文件第 2 行第 0 列 对了 第 0 个文件 a.js (偏移是 -1 又回到了 a.js) 源文件的行信息 位置信息的第三位表示源文件中的行的信息, 理解了位置偏移的概念,我们很容易理解 如:‘AACA,CACA;AACA;‘ 那么 AACA: 目标文件的第 0 行第 0 列 对应 第 0 个文件的第 1 行 CACA: 目标文件的第 0 行第 0+1 列 对应 第 0 个文件的第 1+1 行 AACA:目标文件的第 1 行第 0 列 对应 第 0 个文件的第 1 行 (注意:’;’ 后的行列偏移信息归 0) 源文件中的列信息 位置信息的第四位表示源文件中的列的信息 如:'AAAA,CAAC;' 那么 AAAA: 目标文件的第 0 行第 0 列 对应 第 0 个文件的第 0 行第 0 列 CAAC: 目标文件的第 0 行第 0+1 列 对应 第 0 个文件的第 0 行第 0+1 列 位置信息的第五位 第五位表示变量的偏移,对应 sourcemaps 中的 names 字段,表示目标文件中的变量名对应域源文件中的变量 如:’AAAA,CAACC;AAAAD;' sourcemaps 中 names 字段是 [‘a’, ‘b’] 那么 AAAA: 目标文件的第 0 行第 0 列 对应 第 0 个文件的第 0 行第 0 列,没有变量的信息 CAACC: 目标文件的第 0 行第 0+1 列 对应 第 0 个文件的第 0 行第 0+1 列,有变量信息,变量在源文件中是 ‘b’ (0+1=1) AAAAD: 目标文件的第 1 行第 0 列 对应 第 0 个文件的第 0 行第 0 列,有变量信息,变量在源文件中是 ‘a’ (1-1=0) --------- 大神分割线 --------- 怎么使用 Sourcemaps Q: 线上小程序报错,我怎么通过 sourcemaps 还原到源代码中? A: 如报错 appservice.js 1:15000, 表示目标文件第一行 第 15000 列位置报错。根据上文介绍的,通过 mappings 字段算。 Q: 不会。 A: 如果你会写代码的话,参考下边 [代码]import fs = require('fs') import {SourceMapConsumer} from 'source-map' async function originalPositionFor(line, column) { const sourceMapFilePath = '如果你不真的替换的成 sourcemaps 在硬盘中的位置,那你还是放弃自己写代码吧。 ' const sourceMapConsumer = await new SourceMapConsumer(JSON.parse(fs.readFileSync(sourceMapFilePath, 'utf8'))) return sourceMapConsumer.originalPositionFor({ line, column, }) } originalPositionFor(出错的行,出错的列) [代码] Q: 不会写代码 A: 下载最新版的开发者工具,菜单-设置-拓展设置-调试器插件 [图片] [图片] Q: 为啥都是 null? A: 每个小程序版本都应该对应一个sourcemap文件。 运营中心那里下载的 sourcemap 是对应线上最新的小程序版本。但运营中心的报错集合了多个小程序版本。拿旧小程序版本的报错信息,和最新版本的 sourcemap,是匹配不出的。开发者工具和ci 上传的时候,会提示下载对应版本的 sourcemap 信息,可以自助保存。 [图片] Q: 怎么确定有没有版本对应上 A: 下载的 sourcemap 中有个 wx 字段,标明了该 sourcemap 文件对应小程序版本号。 [图片] [图片] 前提 1.确保发生错误的小程序版本和下载回来的 sourcemap 版本是一致的。 a. 下载 sourceMap 文件,可在 mp 后台或开发者工具上传成功弹窗下载 2.确保 map 文件和发生错误的 js 文件是对应的。sourcemap 的目录和文件说明 a. APP 是主包,FULL 是整包(仅在不支持分包的低版本微信中使用),其他目录是分包 b. 每个分包下都有对应的 app-service.js.map 文件。 c. 如果是使用了按需注入特性(app.json中配置了lazyCodeLoading),那么每个分包下还会有 appservice.app.js.map(对应分包下非页面的js),和所有页面的 xxx.js.map 以上事情都确保正确之后,还是出现行列号匹配不出来的情况。那就需要进一步排查。 线上运行的小程序 sourcemap 文件是怎么生成的? 处理流程:源码 [ a.js a.js.map b.js b.js.map ] -> 开发者工具(JS转 ES5,压缩)-> 微信后台(合并 js 文件)[ appservice.app.js appservice.app.js.map]。 注意:如果源码在交给工具之前是经过了 webpack 等打包工具的处理,那源码这里需要有 map 文件。否则不需要存在 map 文件。 可以看出,map 文件经过三个步骤的处理,每个步骤都有可能导致出错,因此开发者需要先排查,是否是前两个步骤出错导致的 map 文件失效的。 如何排查前两个步骤产生的 map 文件是否有问题。 1.排查 a.js.map 文件是否有问题。 a. 可以在 a.js 的代码中写一下 throw new Error(‘test sourcemap’)。 b. 使用了 webpack 的情况下,要构建为生产环境的版本。 c. 在开发者工具模拟器中运行对应的页面,看看控制台中的报错,错误行列号是否能正常映射到源文件。 2.排查 开发者工具(JS转 ES5,压缩)步骤是否有问题。 在排查完第一步的基础上,点击预览,用微信上扫码预览,并打开调试 vConsole 功能,检查 vConsole 中是否有报错信息,检查报错信息中的行列号是否能正常映射到源文件。 如何排查 微信后台(合并 js 文件)是否有问题。 a. 一定要先排查完前两个步骤再来排查这一步,一般情况下,这一步是不会出错的。 b. 如果有问题,也只会导致 map 文件中的行号信息出现偏移。比如 Error 信息中显示报错地址是 100: 200,行号是 100。那么你可能直接用 100: 200 在 map 文件中搜索不出信息,但是如果 用 150: 200 就可以搜索出来,说明行号偏移了 50。那其他报错也可以偏移 50 后再进行搜索就找到结果。 c. 怎么排查偏移了多少?可以结合 error.message 的内容,初步判断大概错误的内容是什么。把对应的 map 文件放到这个网站上 source-map-visualization 进行搜索,找出哪些相同列号的地方。再结合 error.message 的内容进行判断。 d. 如果排查到是这一步导致的问题,请在社区上联系我们,我们会在后续版本进行修复。 依旧排查不出原因? 先整理一下按照上述步骤排查的结论,再在社区上联系我们协助
2023-02-10 - 微信小程序如何实现页面传参?
前言 只要你的小程序超过一个页面那么可能会需要涉及到页面参数的传递,下面我总结了 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 - 如何彻底解决小程序滚动穿透问题
背景 俗话说,产品有三宝:弹窗、浮层加引导,足以见弹窗在产品同学心目中的地位。对任意一个刚入门的前端同学来说,实现一个模态框基本都可以达到信手拈来的地步,但是,当模态框里边的内容滚动起来以后,就会出现各种各样的让人摸不着头脑的问题,其中,最出名的想必就是滚动穿透。 什么是滚动穿透? 滚动穿透的定义:指我们滑动顶层的弹窗,但效果上却滑动了底层的内容。 具体解决方案分析如下: 改变顶层:从穿透的思路考虑,如果顶层不会穿透过去,那么问题就解决了,所以我们尝试给蒙层加catchtouchmove,但是发现部分场景无效果,那么就不再赘述了。 改变底层:既然是顶层影响了底层,要是底层不会滚动,那就没这个问题了。 如何改变底层解决该问题呢? 不成熟方案: 底部页面最外层view设置position: fixed;页面不可滚动,但是这个时候会导致页面回到顶部。 滚动时监听滚动距离,弹窗时记录滚动位置,关闭弹窗后使用wx.pageScrollTo回滚到记录的位置。 成熟方案 使用page-meta组件,通过该组件我们可以操作Page的style样式,类似于h5里body设置overflow: hidden; 控制页面不可滚动。文档地址:https://developers.weixin.qq.com/miniprogram/dev/component/page-meta.html 使用wx.setPageStyle设置overflow: hidden, 也可以实现给Page组件设置样式。) page-meta组件: 通过该组件我们可以直接操作[代码]Page[代码]组件 ,我们给它的wxss样式overflow动态设置[代码]hidden[代码]or[代码]visible[代码]or[代码]auto[代码] 就可以控制整个页面是否可以滚动。 [图片] wx.setPageStyle方法: 调用这个api,动态设置它为hidden/auto,用于控制页面是否可滚动,主要用于页面组件内使用,比如封装好的弹窗组件,就不用单独写page-meta组件了。。 [代码]wx.setPageStyle({ style: { overflow: 'hidden' // ‘auto’ } }) [代码] 老规矩,结尾放代码片段: https://developers.weixin.qq.com/s/U6ItgQmP7upQ 拓展 支付宝小程序虽然存在page-meta组件,但是由于内核为69版本,给page设置overflow: hidden 也无法控制底部元素不可滚动,目前已联系支付宝的底层开发同学提供API控制页面disableScroll,目前正在封装Appx,近期开放。
08-06 - SAUI-关于input滚动问题
关于input的问题,好像系统或微信一升级,问题就不同了,可能解决这个,下一次就又有问题了。关于我前面发的《input键盘弹出时,滚动页面,输入框内容错位问题》的文章。 尤其是在有textarea,placeholder问题更严重。 目前有第三个解决办法: input的父级不要用到overscroll或scroll-view之类的。好像所有问题都不存在了 经常ios+Android各一手机的测试,是没有问题的 大家可以试试 [图片]
2020-08-10 - (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 - 小程序中图片二维码、小程序码,长按识别支持的情况
因为看到最近还有人刷到这篇文章还有收藏的,所以特别说明一下: 以下是2021年5月31日时候测试的结果,并不一定与现在的情况相符。现在啥情况,我也不知道,已经不咋做小程序了。所以大家实际使用时候,请大家还是再测测。 上面这段话更新于2021年10月11日 下面是原文 ==================================================================================================================== 最近小程序中的图片支持长按识别了,总结一下几种情况下: 测试时间:2021-5-31 微信版本:8.0.6 当前时间最新 image标签 + show long press menu <image src="https://img.qr.com/qr.jpg" style="width: 100%;" mode="widthFix" show-menu-by-longpress="{{true}}"></image> ✅ 识别小程序码 - ✅ 跳转小程序 ✅ 识别群二维码 - ❌ 跳转到加群页面 ✅ 识别名片二维码 - ❌ 跳转到加好友页面 ❌ 识别小程序二维码 wx.previewImage ✅ 识别小程序码 - ✅ 跳转小程序 ✅ 识别群二维码 - ✅ 跳转到加群页面 ✅ 识别名片二维码 - ✅ 跳转到加好友页面 ❌ 识别小程序二维码 web-view ✅ 识别小程序码 - ✅ 跳转小程序 ✅ 识别群二维码 - ✅ 跳转到加群页面 ✅ 识别名片二维码 - ✅ 跳转到加好友页面 ❌ 识别小程序二维码 总结,目前微信已经开放了在小程序中长按识别。但是似乎还有一些bug,image标签可以识别到,但是点了没反应。
2021-10-11 - 微信 schema 跳转之非官方文档
微信“应该”是最近开放了 schema 跳转小程序 的能力,大大方便了短信、邮件、外部网页等唤起微信小程序。 schema 链接格式大体是这样:[代码]weixin://dl/business/?ticket=l69894d682fa8dbafe724a0ca3950741e[代码],但是这段文本在安卓端无法识别。小规模测试结果如下: [图片] 后来想到用一个正常能够识别的网页地址,内容是重定向到指定的 schema 链接。这就是擅长的领域了,query 参数上带上 schema 链接,location.href 一下不就行了。这里就不 show 代码了,能看到文章的你一定行。 但是,发现在部分安卓手机下(如小米)还是没反应,原来简单的 schema 跳转水这么深的,于是百度谷歌了一下,找到了下面两份关键材料: H5唤起APP进行分享的尝试 AlanZhang001/H5CallUpNative: H5端唤醒移动客户端程序 看源码也不多,总结下来,因不同系统和浏览器对 schema 规范的理解不同,还有一些商业因素,不同环境下面需要用不同的方式进行跳转,甚至有的环境你根本就跳不了。 时间紧,任务重。简单处理吧,不同方式都来一遍,谁好使就用谁。所以简单总结了下,能用的几种方式: location 跳转 a 链接跳转 iframe 跳转 以上三种方式,逐一试用,最后实在不行就不行吧,简单处理,看有没有大神补充的。 相关代码如下: location [代码]location.href = "weixin://dl/business/?ticket=l69894d682fa8dbafe724a0ca3950741e"; [代码] a 链接跳转 [代码]var aLink = document.createElement("a"); aLink.className = 'call_up_a_link'; aLink.href = "weixin://dl/business/?ticket=l69894d682fa8dbafe724a0ca3950741e"; aLink.style.cssText = "display:none;width:0px;height:0px;"; document.body.appendChild(aLink); aLink.click(); [代码] iframe [代码]var iframe = document.createElement('iframe'); iframe.className = 'call_up_iframe'; iframe.src = "weixin://dl/business/?ticket=l69894d682fa8dbafe724a0ca3950741e"; iframe.style.cssText = "display:none;width:0px;height:0px;"; document.body.appendChild(iframe); [代码] 以上代码均可从参考资料中找到出处,感谢 是直接一进来就执行,还是事件触发,都可以。或者是一开始进来就执行,失败了显示几个可选跳转按钮让用户手动触发跳转。 但是关键问题还有一个,如何判断是可以成功唤起了呢?上述 github 代码里提到了一个根据页面 hidden 状态,但不够精准,如果用户没有选择跳转到微信呢?这是另一个需要深究的问题。 出于时间考虑,先以业务交付优先,如果有朋友知道的也可以一起讨论下。 另行文时间短,以技术交流为主,若有瑕疵,欢迎指出。 附上 vue 版本源码:微信 schema 跳转 参考链接: 微信官方文档:urlscheme.generate H5唤起APP进行分享的尝试 AlanZhang001/H5CallUpNative: H5端唤醒移动客户端程序 安卓端,微信schema无法跳转微信小程序?
2021-01-04 - #小程序 小程序和公众号内长按识别哪些码是有效的
Tip:2021-05-21 测试了小程序图片长按识别个人微信码、群聊码、企业微信码可以直接添加。 须知:以下结果均在微信IOS最新版(8.0.2)测试所得!!! 视频号二维码 公众号内长按识别结果:可以 小程序内长按识别结果:不可以 小程序内webview(公众号文章):不可以 小程序内webview(自定义H5):不可以 小程序客服消息长按识别:可以 [图片] 个人赞赏码 公众号内长按识别结果:可以 小程序内长按识别结果:不可以 小程序内webview(公众号文章):不可以 小程序内webview(自定义H5):不可以 小程序客服消息长按识别:可以 官方回复小程序因策略调整不能识别:https://developers.weixin.qq.com/community/develop/doc/0008ea7edb8f4845c39be413456c00?highLine=%25E8%25B5%259E%25E8%25B5%258F%25E7%25A0%2581%25E8%25AF%2586%25E5%2588%25AB [图片] 个人微信号二维码 公众号内长按识别结果:可以 小程序内长按识别结果:不可以 小程序内webview(公众号文章):不可以 小程序内webview(自定义H5):不可以 小程序客服消息长按识别:可以 [图片] 个人收款二维码 公众号内长按识别结果:可以 小程序内长按识别结果:不可以 小程序内webview(公众号文章):不可以 小程序内webview(自定义H5):不可以 小程序客服消息长按识别:可以 [图片] 公众号(订阅号)二维码 公众号内长按识别结果:可以 小程序内长按识别结果:不可以 小程序内webview(公众号文章):可以 小程序内webview(自定义H5):不可以 小程序客服消息长按识别:可以 [图片] 小程序码 公众号内长按识别结果:可以 小程序内长按识别结果:可以 小程序内webview(公众号文章):可以 小程序内webview(自定义H5):可以 小程序客服消息长按识别:可以 [图片] 小商店码 公众号内长按识别结果:可以 小程序内长按识别结果:可以 小程序内webview(公众号文章):可以 小程序内webview(自定义H5):可以 小程序客服消息长按识别:可以 [图片] 企业微信码 公众号内长按识别结果:可以 小程序内长按识别结果:不可以 小程序内webview(公众号文章):不可以 小程序内webview(自定义H5):不可以 小程序客服消息长按识别:可以 [图片] 普通网址二维码 公众号内长按识别结果:可以 小程序内长按识别结果:不可以 小程序内webview(公众号文章):不可以 小程序内webview(自定义H5):不可以 小程序客服消息长按识别:可以 [图片]
2021-05-21 - 小程序海报生成工具,可视化编辑直接生成代码使用,你的海报你自己做主
开门见山 工具地址 点我直达>>painter-custom-poster 由于挂载在github page上,打开速度会慢一些,请耐心等待或自行解决git网速问题 背景 在做小程序时候,我们经常会有一个需求,需要将小程序分享到朋友圈,但是朋友圈是不允许直接分享小程序,那我们还有其他的办法解决吗?答案肯定是有的,即 canvas 生成个性化海报分享图片到朋友圈 分析 小程序中有大量的生成图片需求,但是使用过 canvas 的人,都会发现一些难以预料的问题>>有关小程序的坑 直接在 canvas 上绘制图形,对于普通开发者来说代码会特别凌乱并且难以维护,经常会花费很久的时间去优化代码 不同的环境渲染问题,例如在开发者工具看起来好好的,一到 Android 真机,就出现图片不显示,位置不对应等等问题 解决 那可不可以开发一款生成海报的插件库呢? 首先,只需要提供一份简单的参数配置文件即可 解决掉小程序Canvas遇到的一些大大小小的坑 有严苛的测试环节,解决各种环境和各种机型遇到的问题,并提供稳定的线上版本 长期维护,并有专人更新迭代更新颖的功能 以上的要求当然是可以的,曾经的我也想尝试开发一款出来,但是后来尝试了几款现成的工具之后就放弃了,毕竟轮子这个东西,是需要不断维护更新的,另外已经有这么多优秀现成的插件了,我为何还要费力去写呢,贡献代码岂不更美哉,以下是我收集的几款 小程序生成图片库,轻松通过 json 方式绘制一张可以发到朋友圈的图片>>Painter 小程序组件-小程序海报组件>>wxa-plugin-canvas 微信小程序:一个 json 帮你完成分享朋友圈图片>>mp_canvas_drawer 我想干什么 唠了这么多,好像提供给大家插件就没我什么事情了…想走是不可能的 为了能够制作出更酷炫的海报,我思考了许久 虽然有了插件后,只需要提供配置代码就能够制作出一款海报来,但是我发现还是有些许问题 制作海报效率还是不够高,微调一个元素的大小和位置,就需要不断的修改保存代码,等待片刻,查看效果,真的烦 一个小小的位置调整可能就需要来回调整无数次,这种最简单的机械化劳动,这辈子是不可能的 拿着完美的稿子,递给设计师看,这个位置不对,这个线太粗,这个颜色太重…你信不信我打死你 对于一些精美复杂的海报,实现起来真的不太现实 那我需要怎么做呢,请点击这个链接体验>>painter-custom-poster 点击左侧例子展示中的任意一个例子,然后导入代码就能看到效果图,这下你应该能猜到了我的想法了 如何实现 刚开始我想用简单的html和css加拖动功能实现,通过简单尝试之后就放弃了,因为这个功能真的太复杂了,简单的工具肯定是不行的 中间这个计划停滞了很长时间,一度已经放弃 直到发现了这个库fabric.js,真的太太优秀了,赞美之词无以言表,唯一的缺点就是中文教程太少,必须生啃英文加谷歌翻译 fabric介绍,你可以很容易地创建任何一个简单的形状,复杂的形状,图像;将它们添加到画布中,并以任何你想要的方式进行修改:位置、尺寸、角度、颜色、笔画、不透明度等 How To Use 目前工具一共分成4部分 例子展示 用来将一些用户设计的精美海报显示出来,通过点击对应的例子并将代码导入画布中 画布区 显示真实的海报效果,画布里添加的元素,都可以直接用鼠标进行拖动,旋转,缩放操作 操作区 第一排四个按钮 复制代码 将画布的展示效果转化成小程序海报插件库所需要的json配置代码,目前我使用的是Painter库,默认会转化成这个插件的配置代码,将代码直接复制到card.js即可 查看代码 这个功能用不用无所谓,可以直观的看到生成的代码 导出json 将画布转化成fabric所需要的json代码,方便将自己设计的海报代码保存下来 导入json 将第3步导出的json代码导入,会在画布上显示已设计的海报样式 第二排五个按钮 画布 画布的属性参数 详解见下方 文字 添加文字的属性参数 详解见下方 矩形 添加矩形的属性参数 详解见下方 图片 添加图片的属性参数 详解见下方 二维码 添加二维码的属性参数 详解见下方 第三排 各种元素的详细设置参数 激活区 激活对象是指鼠标点击画布上的元素,该对象会被蓝色的边框覆盖,此时该对象被激活,可以执行拖动 旋转 缩放等操作 激活区只有对象被激活才会出来,用来设置激活对象的各种配置参数,修改value值后,实时更新当前激活对象的对应状态,点击其他区域,此模块将隐藏 快捷键 ‘←’ 左移一像素 ‘→’ 右移一像素 ‘↑’ 上移一像素 ‘↓’ 下移一像素 ‘ctrl + z’ 撤销 ‘ctrl + y’ 恢复 ‘delete’ 删除 ‘[’ 提高元素的层级 ‘]’ 降低元素的层级 布局属性 通用布局属性 属性 说明 默认 rotate 旋转,按照顺时针旋转的度数 0 width、height view 的宽度和高度 top、left 如 css 中为 absolute 布局时的作用 0 background 背景颜色 rgba(0,0,0,0) borderRadius 边框圆角 0 borderWidth 边框宽 0 borderColor 边框颜色 #000000 shadow 阴影 ‘’ shadow 可以同时修饰 image、rect、text 等 。在修饰 text 时则相当于 text-shadow;修饰 image 和 rect 时相当于 box-shadow 使用方法: [代码]shadow: 'h-shadow v-shadow blur color'; h-shadow: 必需。水平阴影的位置。允许负值。 v-shadow: 必需。垂直阴影的位置。允许负值。 blur: 必需。模糊的距离。 color: 必需。阴影的颜色。 举例: shadow:10 10 5 #888888 [代码] 渐变色支持 你可以在画布的 background 属性中使用以下方式实现 css 3 的渐变色,其中 radial-gradient 渐变的圆心为 中点,半径为最长边,目前不支持自己设置。 [代码]linear-gradient(-135deg, blue 0%, rgba(18, 52, 86, 1) 20%, #987 80%) radial-gradient(rgba(0, 0, 0, 0) 5%, #0ff 15%, #f0f 60%) [代码] !!!注意:颜色后面的百分比一定得写。 画布属性 属性 说明 默认 times 控制生成插件代码的宽度大小,比如画布宽100,times为2,生成的值为200 1 文字属性 属性名称 说明 默认值 text 字体内容 别跟我谈感情,谈感情伤钱 maxLines 最大行数 不限,根据 width 来 lineHeight 行高(上下两行文字baseline的距离) 1.3 fontSize 字体大小 30 color 字体颜色 #000000 fontWeight 字体粗细。仅支持 normal, bold normal textDecoration 文本修饰,支持none underline、 overline、 linethrough none textStyle fill: 填充样式,stroke:镂空样式 fill fontFamily 字体 sans-serif textAlign 文字的对齐方式,分为 left, center, right left 备注: fontFamily,工具中的第一个例子支持文字字体,但是导入小程序为什么看不到呢,小程序官网加载网络字体方法>> 加载字体教程>> 文字高度 是maxLines lineHeight2个字段一起计算出来的 图片属性 属性 说明 默认 url 图片路径 mode 图片裁剪、缩放的模式 aspectFill mode参数详解 scaleToFill 缩放图片到固定的宽高 aspectFill 图片裁剪显示对应的宽高 auto 自动填充 宽度全显示 高度自适应居中显示 Tips(一定要看哦~) 本工具不考虑兼容性,如发现不兼容请使用google浏览器 painter现在只支持这几种图形,所以暂不支持圆,线等 如果编辑过程,一个元素被挡住了,无法操作,请选择对象并通过[ ]快捷键提高降低元素的层级 文字暂不支持直接缩放操作,因为文字大小和元素高度不容易计算,可以通过修改激活栏目maxLines lineHeight fontSize值来动态改变元素 如发现导出的代码一个元素被另一个元素挡住了,请手动调整元素的位置,json数组中元素越往后层级显示就越高,由于painter没有提供层级参数,所以目前只能这样做 本工具导出代码全是以px为单位,为什么不支持rpx, 因为painter在rpx单位下,阴影和边框宽会出现大小计算问题,由于原例子没有提供px生成图片方案,可以下载我这里修改过的demo>>Painter即可解决 文本宽度随着字数不同而动态变化,想在文本后面加个图标根据文本区域长度布局, 请参考Painter文档这块教程直接修改源码 由于本工具开发有些许难度,如出现bug,建议或者使用上的问题,请提issue,源码地址>>painter-custom-poster 海报贡献 如果你设计的海报很好看,并且愿意开源贡献,可以贡献你的海报代码和缩略图,例子代码文件在example中,按顺序排列,例如现在库里例子是example2.js,那你添加example3.js和example3.jpg图片,事例可以参考一下文件夹中源码,然后在index.js中导出一下 导出代码 代码不要格式化,会报错,请原模原样复制到json字段里 生成缩略图 刚开始我想在此工具中直接生成图片,但是由于浏览器图片跨域问题导致报错失败 所以请去小程序中生成保存图片,图片质量设置0.2,并去tinypng压缩一下图片 找到painter.js,替换下边这个方法,可以生成0.2质量的图片,代码如下 [代码] saveImgToLocal() { const that = this; setTimeout(() => { wx.canvasToTempFilePath( { canvasId: 'k-canvas', fileType: 'jpg', quality: 0.2, success: function(res) { that.getImageInfo(res.tempFilePath); }, fail: function(error) { console.error(`canvasToTempFilePath failed, ${JSON.stringify(error)}`); that.triggerEvent('imgErr', { error: error }); } }, this ); }, 300); } [代码] TODO 颜色值选择支持调色板工具 文字padding支持 缩放位置弹跳问题优化 假如需求大的话,支持其他几款插件库代码的生成 ~ 创作不易,如果对你有帮助,请给个星星 star✨✨ 谢谢 ~
2019-09-27 - 小程序迁移,AppId、AppSecret、openid、unionID的变化情况?
小程序迁移,AppId、AppSecret、openid都不会变,unionID会视情况而定: 1、如果只是单纯的小程序迁移,不涉及open帐号变化,unionID就不会变。但是有可能会因为主体改变而影响unionID的获取逻辑,请开发者根据实际情况调整UnionID的获取方式。 2、如果涉及到open帐号变化,unionID就会改变,因为unionID就是根据open帐号的信息直接计算出来的。
2019-11-20 - 使用网页获取公众号的openid,并将网页部署到腾讯云开发的静态托管,
1,准备条件:(1):你已经至少开通了一个云开发环境,并且开通了公众号。 (2):电脑安装了node.js。 2,安装cloudbase-cli,并使用cloudbase-cli新建一个项目。 安装教程以及使用见如下链接:https://i.hackweek.org/handbook/tcb/1402.html 3,配置公众号信息: (1),在腾讯云官网中,静态托管页面查看静态托管的域名。 (2),打开微信公众号的微信公众平台,在 开发-接口权限-网页服务-网页授权-修改 中,将静态托管域名设置入网页授权域名中 [图片] (3),点击https://cloud.tencent.com/apply/p/iaqs555bdma这个链接,申请一个云函数的固定ip。 待审核通过后,可在腾讯云控制台-云开发cloudbase-选择环境-云函数 中,随意点入一个函数,可查看到公网固定ip。(若没有,请点编辑-启用公网ip,就出现了) [图片] 将公网ip配置到公众号-设置-安全中心-ip白名单中。 4,开启VSCode,打开之前新建的项目,并且安装对应的插件,如html,css插件,以及Tencent-Cloudbase-Toolkit插件(可用于编辑云开发)。 项目目录如下: [图片] 5,写代码 具体而言,网页授权流程分为四步: a、引导用户进入授权页面同意授权,获取code b、通过code换取网页授权access_token(与基础支持中的access_token不同) c、如果需要,开发者可以刷新网页授权access_token,避免过期 d、通过网页授权access_token和openid获取用户基本信息(支持UnionID机制) 具体见如下网址:https://developers.weixin.qq.com/doc/offiaccount/OA_Web_Apps/Wechat_webpage_authorization.html 这边只做到获取openid。 代码如下:h5页面 [图片] js页面:[图片] 注意,appId和appSecret请到开发平台去获取 云函数代码: [图片] 值得注意的是,获取openid这个接口中,若直接使用data,则在传回值时会报错。 6,将代码上传。 将写好的代码上传到静态托管页面中。详见如下网址:https://i.hackweek.org/handbook/tcb/1403.html 以上仅为个人折腾的结果,若还有坑,请大家多多包涵和指教。
2020-08-08 - 小程序的视频内容流自动播放
小程序的视频内容流自动播放 啊啊啊,又解决一个问题 0、起因 这个需求产生的起因,是在做内容流(包含文本,图片,视频)的时候,需要如果流里面有视频,则滚动到一定位置时自动播放视频,类似朋友圈、微博等等的自动播放效果。 [图片] 1、第一版尝试 第一版的思路是: 收集当前所有内容流相对于页面头部的高度,做成一个Array 滚动过程中,监听页面滚动事件,当达到某个高度要求,则播放对应的索引视频 这个操作缺点太多了,捡几个主要的说 缺点: 内容流是一个个的组件,获取距离顶部高度不方便,也不太准。并且组件内需要通过事件传播到列表页,在列表页进行高度Array整理、事件监听、切换索引等等(如果有几种列表页,就要写几遍,很麻烦) 监听滚动事件本身就消耗性能,做了节流也不是那么优秀 2、第二版尝试 突然,就发现了[代码]wx.createIntersectionObserver[代码]这个属性,它的作用是:返回[代码]intersectionObserver[代码]对象,用于推断某些节点是否可以被用户看见、有多大比例可以被用户看见(创建一个目标元素,根据目标元素和视窗的相交距离来判断当前页面滚动的情况。通常这个方案也用于页面图片的懒加载)。参考https://developers.weixin.qq.com/miniprogram/dev/api/wxml/IntersectionObserver.html 怎么解释呢,就是可以理解为,做一个监听,如果当前被监听的元素,进入了你规定的视界或者离开你规定的视界,就触发。 那么,怎么做到监听呢,参考如下代码: [代码]/** 监控视频是否需要播放 */ let {screenWidth, screenHeight} = this.extData.systemInfo //获取屏幕高度 let topBottomPadding = (screenHeight - 80)/2 //取屏幕中间80的高度作为播放触发区域,然后计算上下视窗的高度 topBottomPadding // 80这个高度可以根据UI样式调整,我这边基本两个视频间隔高度在100左右,超过了两个视频之间的间隔,就会冲突,两个视频会同时播放,不建议过大 const videoObserve = wx.createIntersectionObserver() videoObserve.relativeToViewport({bottom: -topBottomPadding, top: -topBottomPadding}) .observe(`#emotion${this.data.randomId}`, (res) => { let {intersectionRatio} = res if(intersectionRatio === 0) { //离开视界,因为视窗占比为0,停止播放 this.setData({ playstart: false }) }else{ //进入视界,开始播放 this.setData({ playstart: true }) } }) [代码] 其中,[代码]observe[代码] 是对应你需要监听的视频(也就是滚动进入视窗的元素) 那么,为什么选择[代码]relativeToViewport[代码]呢,是因为我们需要对它进入某一个视窗进行监听,而不是对进入整个屏幕视窗监听(因为可能整个视窗里会有多个视频)。 以上,就是整个逻辑思路。 最开始用的[代码]relativeTo[代码]监听视频进入某个元素(如[代码].view-port[代码]),但是后来发现每个页面都要写这个元素,太麻烦,并且容易遮盖操作区域 [代码]// 太麻烦,后来舍弃了这个方案 <view class="view-port" style="height: 100rpx; position: fixed; z-index: 1;width: 100%;letf:0;top:50%;transform: translateY(-50%);"></view> [代码]
2019-12-01 - 一个奇葩思路实现的瀑布流双列布局
传统的瀑布流布局实现一般关键是去计算每一列的高度,从而判断下一个元素应该插入到哪一列(当然是最短的那列)。 这个奇葩思路没有任何计算,主要思路如下: 在瀑布流容器底部加入一根细线 利用微信小程序的IntersectionObserver,为每一列和细线添加监听 逐个加入要插入的item元素 根据监听相交变化结果判断下一个item应该插入哪一列(简单来说就是插入到当前不与细线相交的那一列,因为比较短) 这个思路实际上就是把计算高度换成了监听判断哪列更高,因此也不必知道每个元素的高度。 目前只能支持两列布局的情况,如果列数更多我没办法不通过计算来知道哪列最短,如果有思路或想法的童鞋欢迎交流~ 实现过程也比较简单,就分享个思路,不贴代码了(问就是懒!) 感兴趣的童鞋可以看代码片段,里面有完整的实现代码: https://developers.weixin.qq.com/s/nH5pg4mE78dG
2019-11-23 - 微信小程序UI组件库合集
UI组件库合集,大家有遇到好的组件库,欢迎留言评论然后加入到文档里。 第一款: 官方WeUI组件库,地址 https://developers.weixin.qq.com/miniprogram/dev/extended/weui/ 预览码: [图片] 第二款: ColorUI:地址 https://github.com/weilanwl/ColorUI 预览码: [图片] 第三款: vantUI(又名:ZanUI):地址 https://youzan.github.io/vant-weapp/#/intro 预览码: [图片] 第四款: MinUI: 地址 https://meili.github.io/min/docs/minui/index.html 预览码: [图片] 第五款: iview-weapp:地址 https://weapp.iviewui.com/docs/guide/start 预览码: [图片] 第六款: WXRUI:暂无地址 预览码: [图片] 第七款: WuxUI:地址https://www.wuxui.com/#/introduce 预览码: [图片] 第八款: WussUI:地址 https://phonycode.github.io/wuss-weapp/quickstart.html 预览码: [图片] 第九款: TouchUI:地址 https://github.com/uileader/touchwx 预览码: [图片] 第十款: Hello UniApp: 地址 https://m3w.cn/uniapp 预览码: [图片] 第十一款: TaroUI:地址 https://taro-ui.jd.com/#/docs/introduction 预览码: [图片] 第十二款: Thor UI: 地址 https://thorui.cn/doc/ 预览码: [图片] 第十三款: GUI:https://github.com/Gensp/GUI 预览码: [图片] 第十四款: QyUI:暂无地址 预览码: [图片] 第十五款: WxaUI:暂无地址 预览码: [图片] 第十六款: kaiUI: github地址 https://github.com/Chaunjie/kai-ui 组件库文档:https://chaunjie.github.io/kui/dist/#/start 预览码: [图片] 第十七款: YsUI:暂无地址 预览码: [图片] 第十八款: BeeUI:git地址 http://ued.local.17173.com/gitlab/wxc/beeui.git 预览码: [图片] 第十九款: AntUI: 暂无地址 预览码: [图片] 第二十款: BleuUI:暂无地址 预览码: [图片] 第二十一款: uniydUI:暂无地址 预览码: [图片] 第二十二款: RovingUI:暂无地址 预览码: [图片] 第二十三款: DojayUI:暂无地址 预览码: [图片] 第二十四款: SkyUI:暂无地址 预览码: [图片] 第二十五款: YuUI:暂无地址 预览码: [图片] 第二十六款: wePyUI:暂无地址 预览码: [图片] 第二十七款: WXDUI:暂无地址 预览码: [图片] 第二十八款: XviewUI:暂无地址 预览码: [图片] 第二十九款: MinaUI:暂无地址 预览码: [图片] 第三十款: InyUI:暂无地址 预览码: [图片] 第三十一款: easyUI:地址 https://github.com/qq865738120/easyUI 预览码: [图片] 第三十二款 Kbone-UI: 地址 https://wechat-miniprogram.github.io/kboneui/ui/#/ 暂无预览码 第三十三款 VtuUi: 地址 https://github.com/jisida/VtuWeapp 预览码: [图片] 第三十四款 Lin-UI 地址:http://doc.mini.talelin.com/ 预览码: [图片] 第三十五款 GraceUI 地址: http://grace.hcoder.net/ 这个是收费的哦~ 预览码: [图片] 第三十六款 anna-remax-ui npm:https://www.npmjs.com/package/anna-remax-ui/v/1.0.12 anna-remax-ui 地址: https://annasearl.github.io/anna-remax-ui/components/general/button 预览码 [图片] 第三十七款 Olympus UI 地址:暂无 网易严选出品。 预览码 [图片] 第三十八款 AiYunXiaoUI 地址暂无 预览码 [图片] 第三十九款 visionUI npm:https://www.npmjs.com/package/vision-ui 预览码: [图片] 第四十款 AnimaUI(灵动UI) 地址:https://github.com/AnimaUI/wechat-miniprogram 预览码: [图片] 第四十一款 uView 地址:http://uviewui.com/components/quickstart.html 预览码: [图片] 第四十二款 firstUI 地址:https://www.firstui.cn/ 预览码: [图片]
2023-01-10 - (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 - 解决view包裹image时底部间隙问题
问题 view 包裹 image时,view的底部有间隙,就好像view有padding-bottom。 原因 这是由于image默认是display: inline-block的,inline元素默认是跟父级元素的baseline对齐,而baseline又和父级底边有必定间距。 详情了解vertical-align属性 解决方法一 把image改为 display: block 解决方法二 给image设置vertical-align: bottom,让他对齐父级元素的底边。
2019-10-20 - 使用wx.downloadFile下载图片 失败 提示fail download fail?
使用wx.downloadFile下载图片一直失败,在3月9号还是可以触发的,今天一直出现失败的情况,也没有具体原因
2020-03-10 - wx.downloadFile 失败 file data is empty
我使用wx.downloadFile方法下载图片,开发工具模拟器和android手机上都可以下载图片保存,苹果手机不可以,试了好几个ios真机都是报一样的错误:errMsg: "downloadFile:fail file data is empty",手机版本号是:ios 11.3.*;[图片] 直接跑到fail中了,求大家给帮忙看一下,感谢! [图片]
2018-05-30 - IOS wx.downloadFile 下载 doc,xls 都无法下载
IOS wx.downloadFile 下载 doc,xls 都无法下载,苹果手机上,而docx,xlsx这些是可以下载的,麻烦官方解决下 错误提示:downloadFile:fail file data is empty
2019-08-20 - ios系统版本为9.3.5不支持原生js的Object.values()方法
手机系统版本:ios 9.3.5 微信版本:7.0.4 问题描述:不支持JS的Object.values()。 解决办法的思路(还未尝试): 1、引进第三方的js库,比如underscore,放弃js的原生写法。 2、用到js原生方法的地方加上判断,如果不支持 那么抛出异常 或者 自己写一些工具函数来替代。 疑惑点: 微信的版本为当前的最新版(问题发布时间为:2019-05-31),那么按理来说,最新版本的微信应该会为我们检测系统版本等一系列的支持。 大家有什么思路吗,我还挺喜欢JS的原生写法的,有没有思路往原生写法上靠的?
2019-05-31 - 一个商户号可以绑定几个小程序,一个小程序可以绑定几个商户号?
问题1:一个商户号可以绑定几个小程序,一个小程序可以绑定几个商户号 问题2:一个主体可以申请几个商户号
2019-11-27 - 小程序canvas的那些事
背景 业务场景需要在小程序内生成活动的分享海报,图片中的某些数据需动态展示。可行的方案有️二: 服务端合成:直接返回给前端图片URL 客户端合成:客户端利用canvas绘制 在当前业务场景下,使用客户端合成会优于服务端合成,避免造成不必要的服务器CPU浪费。 下面主要谈谈**客户端(canvas)**合成的过程。 实现思路 小程序端发起请求,获取需动态展示的数据; 利用canvas绘制画布; 导出图片保存到相册。 小技巧&那些坑 理想很丰满,现实很骨感。 实现思路很简单,然而,在实现过程中,发现会趟一些坑,也有一些小技巧,遂记录下来,以供参考。 promise化 画布的绘制依赖系统信息(自适应和优化图片清晰度)和动态数据。故画布需要在所有前置条件都准备完成时,方可绘制。为了提高代码优雅度和维护性,建议用promise化,避免回调地狱(Callback Hell)。 [代码] let promise1 = new Promise((resolve, reject) => { this.getData(resolve, reject) }); let promise2 = new Promise((resolve, reject) => { this.getSystemInfo(resolve, reject) }); Promise.all([promise1, promise2]).then(() => { this.drawCanvas() }).catch(err => { console.log(err) }); [代码] 自适应 1、为了在各个机型下保持大小自适应,需要计算出缩放比: [代码] getSystemInfo(resolve, reject) { try { const res = wx.getSystemInfoSync() //缓存系统信息 systemInfo = res //这里视觉基于iPone6(375*667)设计,2x图视觉,可以填写750,1x图视觉,可以填写375 zoom = res.windowWidth / 750 * 1 resolve() } catch (e) { // Do something when catch error reject("获取机型失败") } } [代码] 2、绘制时进行按缩放比进行缩放,如: [代码]ctx.drawImage(imgUrl, x * zoom, y * zoom, w * zoom, h * zoom) [代码] 绘制网络图片 经测试,绘制CDN图片需要先将图片下载到本地,在进行绘制: [代码]wx.downloadFile({ url: imgUrl, success: res => { if (res.statusCode === 200) { ctx.drawImage(res.tempFilePath, 326 * zoom, 176 * zoom, 14 * zoom, 14 * zoom) } } }) [代码] 绘制base64图片 因为业务上某些原因,依赖的图片数据,后端只能以base64格式返回给前端,而小程序在真机上无法直接绘制(开发工具OK)。 解决思路(存在兼容性问题,fileManager**基础库 1.9.9 **开始支持): 1、调用fileManager.writeFile存储base64到本地; 2、绘制本地图片。 实现代码如下: [代码]// 先获得一个文件实例 fileManager = wx.getFileSystemManager() // 把图片base64格式转存到本地,用于canvas绘制 fileManager.writeFile({ filePath: `${wx.env.USER_DATA_PATH}/qrcode.png`, data: self.data.qrcode, encoding: 'base64', success: () => { //此处需先调用wx.getImageInfo,方可绘制成功 wx.getImageInfo({ src: `${wx.env.USER_DATA_PATH}/qrcode.png`, success: () => { //绘制二维码 ctx.drawImage(`${wx.env.USER_DATA_PATH}/qrcode.png`, 207 * zoom, 313 * zoom, 148 * zoom, 148 * zoom) ctx.draw() } }) } }) [代码] 保存到本地相册 wx.saveImageToPhotosAlbum这个API需用户授权,故开发者需做好拒绝授权的兼容。此处实现对拒绝授权的场景进行引导。 [代码]canvas2Img(e) { wx.getSetting({ success(res) { if (res.authSetting['scope.writePhotosAlbum'] === undefined) { //呼起授权界面 wx.authorize({ scope: 'scope.writePhotosAlbum', success() { save() } }) } else if (res.authSetting['scope.writePhotosAlbum'] === false) { //引导拒绝过授权的用户授权 wx.showModal({ title: '温馨提示', content: '需要您授权保存到相册的权限', success: res => { if (res.confirm) { wx.openSetting({ success(res) { if (res.authSetting['scope.writePhotosAlbum']) { save() } } }) } } }) } else { save() } } }) function save() { wx.canvasToTempFilePath({ x: 0, y: 0, width: 562*zoom, height: 792*zoom, destWidth: 562*zoom*systemInfo.pixelRatio, destHeight: 792*zoom*systemInfo.pixelRatio, fileType: 'png', quality: 1, canvasId: 'shareImg', success: res => { wx.saveImageToPhotosAlbum({ filePath: res.tempFilePath, success: () => { wx.showModal({ content: '保存成功', showCancel: false, confirmText: '确认' }) }, fail: res => { if (res.errMsg !== 'saveImageToPhotosAlbum:fail cancel') { wx.showModal({ content: '保存到相册失败', showCancel: false }) } } }) }, fail: () => { wx.showModal({ content: '保存到相册失败', showCancel: false }) } }) } [代码] 导出清晰图片 wx.canvasToTempFilePath有destWidth(输出的图片的宽度)和destHeight(输出的图片的高度)属性。若此处以canvas的宽高去填写的话,在高像素手机下,导出的图片会模糊。 原因:destWidth和destHeight单位是物理像素(pixel),canvas绘制的时候用的是逻辑像素(物理像素=逻辑像素 * density),所以这里如果只是使用canvas中的width和height(逻辑像素)作为输出图片的长宽的话,生成的图片width和height实际上是缩放了到canvas的 1 / density大小了,所以就显得比较模糊了。 这里应该乘以设备像素比,实现如下: [代码]wx.canvasToTempFilePath({ x: 0, y: 0, width: 562*zoom, height: 792*zoom, destWidth: 562*zoom*systemInfo.pixelRatio, destHeight: 792*zoom*systemInfo.pixelRatio, fileType: 'png', quality: 1, canvasId: 'shareImg' )} [代码] 特殊字体的绘制 研究发现,目前小程序canvas无法支持设置特殊字体,而业务生成的海报,又期望以特殊字体去呈现,最终取了个折中方案——保留数字部分的特殊样式。 实现方式为:把0-9这10个数字单独切图,用ctx.drawImage API,以图片形式去绘制。 [代码]drawNum(num, x, y, w, h) { return new Promise(function (resolve, reject) { //这里存储0-9的图片CDN链接 let numMap = [] wx.downloadFile({ url: numMap[num], success: res => { if (res.statusCode === 200) { ctx.drawImage(res.tempFilePath, x * zoom, y * zoom, w * zoom, h * zoom) resolve() } }, fail: () => { reject() } }) }) } [代码] 安卓机型图片绘制锯齿化问题 测试发现,同样的绘制方案,在安卓下,调用ctx.drawImage方法,图片会出现锯齿问题。测试还发现,原像素越高,锯齿化程度降低(但业务上使用太大像素的素材也不合理),这里需要客户端底层进行优化,目前没有找到合适的解决方案。 总结 个人觉得,目前小程序canvas就底层能力上相比web还有一些不足。所以应注意两点: 提前从业务出发,考虑当前实现的可行性,以便采取更优方案(如特殊字体,像素要求等); 若绘制canvas导出图片是个高频场景,可参考html2canvas进行封装,以便提高效能(SelectorQuery节点查询需1.9.90以上)。 ps:之前有想过利用web-view方式,在传统网页去绘制,然后通过web-view和小程序的通信来实现的方式。时间原因,并未尝试,感兴趣同学可以尝试下。
2019-03-07 - 小程序多端框架全面测评:chameleon、Taro、uni-app、mpvue、WePY
作者:coldsnap 原文:小程序多端框架全面测评 Fundebug经授权转载,版权归原作者所有。 最近前端届多端框架频出,相信很多有代码多端运行需求的开发者都会产生一些疑惑:这些框架都有什么优缺点?到底应该用哪个? 作为 Taro 开发团队一员,笔者想在本文尽量站在一个客观公正的角度去评价各个框架的选型和优劣。但宥于利益相关,本文的观点很可能是带有偏向性的,大家可以带着批判的眼光去看待,权当抛砖引玉。 那么,当我们在讨论多端框架时,我们在谈论什么: 多端 笔者以为,现在流行的多端框架可以大致分为三类: 1. 全包型 这类框架最大的特点就是从底层的渲染引擎、布局引擎,到中层的 DSL,再到上层的框架全部由自己开发,代表框架是 Qt 和 Flutter。这类框架优点非常明显:性能(的上限)高;各平台渲染结果一致。缺点也非常明显:需要完全重新学习 DSL(QML/Dart),以及难以适配中国特色的端:小程序。 这类框架是最原始也是最纯正的的多端开发框架,由于底层到上层每个环节都掌握在自己手里,也能最大可能地去保证开发和跨端体验一致。但它们的框架研发成本巨大,渲染引擎、布局引擎、DSL、上层框架每个部分都需要大量人力开发维护。 2. Web 技术型 这类框架把 Web 技术(JavaScript,CSS)带到移动开发中,自研布局引擎处理 CSS,使用 JavaScript 写业务逻辑,使用流行的前端框架作为 DSL,各端分别使用各自的原生组件渲染。代表框架是 React Native 和 Weex,这样做的优点有: 开发迅速 复用前端生态 易于学习上手,不管前端后端移动端,多多少少都会一点 JS、CSS 缺点有: 交互复杂时难以写出高性能的代码,这类框架的设计就必然导致 [代码]JS[代码] 和 [代码]Native[代码] 之间需要通信,类似于手势操作这样频繁地触发通信就很可能使得 UI 无法在 16ms 内及时绘制。React Native 有一些声明式的组件可以避免这个问题,但声明式的写法很难满足复杂交互的需求。 由于没有渲染引擎,使用各端的原生组件渲染,相同代码渲染的一致性没有第一种高。 3. JavaScript 编译型 这类框架就是我们这篇文章的主角们:[代码]Taro[代码]、[代码]WePY[代码] 、[代码]uni-app[代码] 、 [代码]mpvue[代码] 、 [代码]chameleon[代码],它们的原理也都大同小异:先以 JavaScript 作为基础选定一个 DSL 框架,以这个 DSL 框架为标准在各端分别编译为不同的代码,各端分别有一个运行时框架或兼容组件库保证代码正确运行。 这类框架最大优点和创造的最大原因就是小程序,因为第一第二种框架其实除了可以跨系统平台之外,也都能编译运行在浏览器中。(Qt 有 Qt for WebAssembly, Flutter 有 Hummingbird,React Native 有 [代码]react-native-web[代码], Weex 原生支持) 另外一个优点是在移动端一般会编译到 React Native/Weex,所以它们也都拥有 Web 技术型框架的优点。这看起来很美好,但实际上 React Native/Weex 的缺点编译型框架也无法避免。除此之外,编译型框架的抽象也不是免费的:当 bug 出现时,问题的根源可能出在运行时、编译时、组件库以及三者依赖的库等等各个方面。在 Taro 开源的过程中,我们就遇到过 Babel 的 bug,React Native 的 bug,JavaScript 引擎的 bug,当然也少不了 Taro 本身的 bug。相信其它原理相同的框架也无法避免这一问题。 但这并不意味着这类为了小程序而设计的多端框架就都不堪大用。首先现在各巨头超级 App 的小程序百花齐放,框架会为了抹平小程序做了许多工作,这些工作在大部分情况下是不需要开发者关心的。其次是许多业务类型并不需要复杂的逻辑和交互,没那么容易触发到框架底层依赖的 bug。 那么当你的业务适合选择编译型框架时,在笔者看来首先要考虑的就是选择 DSL 的起点。因为有多端需求业务通常都希望能快速开发,一个能够快速适应团队开发节奏的 DSL 就至关重要。不管是 React 还是 Vue(或者类 Vue)都有它们的优缺点,大家可以根据团队技术栈和偏好自行选择。 如果不管什么 DSL 都能接受,那就可以进入下一个环节: 生态 以下内容均以各框架现在(2019 年 3 月 11 日)已发布稳定版为标准进行讨论。 1. 开发工具 就开发工具而言 [代码]uni-app[代码] 应该是一骑绝尘,它的文档内容最为翔实丰富,还自带了 IDE 图形化开发工具,鼠标点点点就能编译测试发布。 其它的框架都是使用 CLI 命令行工具,但值得注意的是 [代码]chameleon[代码] 有独立的语法检查工具,[代码]Taro[代码] 则单独写了 ESLint 规则和规则集。 在语法支持方面,[代码]mpvue[代码]、[代码]uni-app[代码]、[代码]Taro[代码] 、[代码]WePY[代码] 均支持 TypeScript,四者也都能通过 [代码]typing[代码] 实现编辑器自动补全。除了 API 补全之外,得益于 TypeScript 对于 JSX 的良好支持,Taro 也能对组件进行自动补全。 CSS 方面,所有框架均支持 [代码]SASS[代码]、[代码]LESS[代码]、[代码]Stylus[代码],Taro 则多一个 [代码]CSS Modules[代码] 的支持。 所以这一轮比拼的结果应该是: uni-app > Taro > chameleon > WePY、mpvue [图片] 2. 多端支持度 只从支持端的数量来看,[代码]Taro[代码] 和 [代码]uni-app[代码] 以六端略微领先(移动端、H5、微信小程序、百度小程序、支付宝小程序、头条小程序),[代码]chameleon[代码] 少了头条小程序紧随其后。 但值得一提的是 [代码]chameleon[代码] 有一套自研多态协议,编写多端代码的体验会好许多,可以说是一个能戳到多端开发痛点的功能。[代码]uni-app[代码] 则有一套独立的条件编译语法,这套语法能同时作用于 [代码]js[代码]、样式和模板文件。[代码]Taro[代码] 可以在业务逻辑中根据环境变量使用条件编译,也可以直接使用条件编译文件(类似 React Native 的方式)。 在移动端方面,[代码]uni-app[代码] 基于 [代码]weex[代码] 定制了一套 [代码]nvue[代码] 方案 弥补 [代码]weex[代码] API 的不足;[代码]Taro[代码]则是暂时基于 [代码]expo[代码] 达到同样的效果;[代码]chameleon[代码] 在移动端则有一套 SDK 配合多端协议与原生语言通信。 H5 方面,[代码]chameleon[代码] 同样是由多态协议实现支持,[代码]uni-app[代码] 和 [代码]Taro[代码] 则是都在 H5 实现了一套兼容的组件库和 API。 [代码]mpvue[代码] 和 [代码]WePY[代码] 都提供了转换各端小程序的功能,但都没有 h5 和移动端的支持。 所以最后一轮对比的结果是: chameleon > Taro、uni-app > mpvue > WePY [图片] 3. 组件库/工具库/demo 作为开源时间最长的框架,[代码]WePY[代码] 不管从 Demo,组件库数量 ,工具库来看都占有一定优势。 [代码]uni-app[代码] 则有自己的插件市场和 UI 库,如果算上收费的框架和插件比起 [代码]WePy[代码] 也是完全不遑多让的。 [代码]Taro[代码] 也有官方维护的跨端 UI 库 [代码]taro-ui[代码] ,另外在状态管理工具上也有非常丰富的选择(Redux、MobX、dva),但 demo 的数量不如前两个。但 [代码]Taro[代码] 有一个转换微信小程序代码为 Taro 代码的工具,可以弥补这一问题。 而 [代码]mpvue[代码] 没有官方维护的 UI 库,[代码]chameleon[代码] 第三方的 demo 和工具库也还基本没有。 所以这轮的排序是: WePY > uni-app 、taro > mpvue > chameleon [图片] 4. 接入成本 接入成本有两个方面: 第一是框架接入原有微信小程序生态。由于目前微信小程序已呈一家独大之势,开源的组件和库(例如 [代码]wxparse[代码]、[代码]echart[代码]、[代码]zan-ui[代码] 等)多是基于原生微信小程序框架语法写成的。目前看来 [代码]uni-app[代码] 、[代码]Taro[代码]、[代码]mpvue[代码] 均有文档或 demo 在框架中直接使用原生小程序组件/库,[代码]WePY[代码] 由于运行机制的问题,很多情况需要小改一下目标库的源码,[代码]chameleon[代码] 则是提供了一个按步骤大改目标库源码的迁移方式。 第二是原有微信小程序项目部分接入框架重构。在这个方面 Taro 在京东购物小程序上进行了大胆的实践,具体可以查看文章《Taro 在京东购物小程序上的实践》。其它框架则没有提到相关内容。 而对于两种接入方式 Taro 都提供了 [代码]taro convert[代码] 功能,既可以将原有微信小程序项目转换为 Taro 多端代码,也可以将微信小程序生态的组件转换为 Taro 组件。 所以这轮的排序是: Taro > mpvue 、 uni-app > WePY > chameleon 流行度 从 GitHub 的 star 来看,[代码]mpvue[代码] 、[代码]Taro[代码]、[代码]WePY[代码] 的差距非常小。从 NPM 和 CNPM 的 CLI 工具下载量来看,是 Taro(3k/week)> mpvue (2k/w) > WePY (1k/w)。但发布时间也刚好反过来。笔者估计三家的流行程度和案例都差不太多。 [代码]uni-app[代码] 则号称有上万案例,但不像其它框架一样有一些大厂应用案例。另外从开发者的数量来看也是 [代码]uni-app[代码] 领先,它拥有 20+ 个 QQ 交流群(最大人数 2000)。 所以从流行程度来看应该是: uni-app > Taro、WePY、mpvue > chameleon [图片] 5. 开源建设 一个开源作品能走多远是由框架维护团队和第三方开发者共同决定的。虽然开源建设不能具体地量化,但依然是衡量一个框架/库生命力的非常重要的标准。 从第三方贡献者数量来看,[代码]Taro[代码] 在这一方面领先,并且 [代码]Taro[代码] 的一些核心包/功能(MobX、CSS Modules、alias)也是由第三方开发者贡献的。除此之外,腾讯开源的 [代码]omi[代码] 框架小程序部分也是基于 Taro 完成的。 [代码]WePY[代码] 在腾讯开源计划的加持下在这一方面也有不错的表现;[代码]mpvue[代码] 由于停滞开发了很久就比较落后了;可能是产品策略的原因,[代码]uni-app[代码] 在开源建设上并不热心,甚至有些部分代码都没有开源;[代码]chameleon[代码] 刚刚开源不久,但它的代码和测试用例都非常规范,以后或许会有不错的表现。 那么这一轮的对比结果是: Taro > WePY > mpvue > chameleon > uni-app 最后补一个总的生态对比图表: [图片] 未来 从各框架已经公布的规划来看: [代码]WePY[代码] 已经发布了 [代码]v2.0.alpha[代码] 版本,虽然没有公开的文档可以查阅到 [代码]2.0[代码] 版本有什么新功能/特性,但据其作者介绍,[代码]WePY 2.0[代码] 会放大招,是一个「对得起开发者」的版本。笔者也非常期待 2.0 正式发布后 [代码]WePY[代码] 的表现。 [代码]mpvue[代码] 已经发布了 [代码]2.0[代码] 的版本,主要是更新了其它端小程序的支持。但从代码提交, issue 的回复/解决率来看,[代码]mpvue[代码] 要想在未来有作为首先要打消社区对于 [代码]mpvue[代码]不管不顾不更新的质疑。 [代码]uni-app[代码] 已经在生态上建设得很好了,应该会在此基础之上继续稳步发展。如果 [代码]uni-app[代码] 能加强开源开放,再加强与大厂的合作,相信未来还能更上一层楼。 [代码]chameleon[代码] 的规划比较宏大,虽然是最后发的框架,但已经在规划或正在实现的功能有: 快应用和端拓展协议 通用组件库和垂直类组件库 面向研发的图形化开发工具 面向非研发的图形化页面搭建工具 如果 [代码]chameleon[代码] 把这些功能都做出来的话,再继续完善生态,争取更多第三方开发者,那么在未来 [代码]chameleon[代码] 将大有可为。 [代码]Taro[代码] 的未来也一样值得憧憬。Taro 即将要发布的 [代码]1.3[代码] 版本就会支持以下功能: 快应用支持 Taro Doctor,自动化检查项目配置和代码合法性 更多的 JSX 语法支持,1.3 之后限制生产力的语法只有 [代码]只能用 map 创造循环组件[代码] 一条 H5 打包体积大幅精简 同时 [代码]Taro[代码] 也正在对移动端进行大规模重构;开发图形化开发工具;开发组件/物料平台以及图形化页面搭建工具。 结语 那说了那么多,到底用哪个呢? 如果不介意尝鲜和学习 DSL 的话,完全可以尝试 [代码]WePY[代码] 2.0 和 [代码]chameleon[代码] ,一个是酝酿了很久的 2.0 全新升级,一个有专门针对多端开发的多态协议。 [代码]uni-app[代码] 和 [代码]Taro[代码] 相比起来就更像是「水桶型」框架,从工具、UI 库,开发体验、多端支持等各方面来看都没有明显的短板。而 [代码]mpvue[代码] 由于开发一度停滞,现在看来各个方面都不如在小程序端基于它的 [代码]uni-app[代码] 。 当然,Talk is cheap。如果对这个话题有更多兴趣的同学可以去 GitHub 另行研究,有空看代码,没空看提交: chameleon: https://github.com/didi/chameleon mpvue: https://github.com/Meituan-Dianping/mpvue Taro: https://github.com/NervJS/taro uni-app: https://github.com/dcloudio/uni-app WePY: https://github.com/Tencent/wepy (按字母顺序排序)
2019-06-18