- 小程序异常监控收集
前言你是否经常碰到业务反馈,线上的小程序某个页面打不开了,订单没法结算了,但是你当时测试的时候都是好好的。 由于线上环境复杂,一些问题只会在特定网络环境或者设备上发生,对于这类问题,异常信息的收集就显得格外重要了,我们不但希望收集错误的堆栈信息,还需要用户操作流程,设备信息等,以便复现错误。 简单收集小程序App()生命周期里提供了onError函数,可以通过在onError里收集异常信息 App({ // 监听错误 onError: function (err) { // 上报错误 wx.request({ url: "https://url", // 自行定义报告服务器 method: "POST", errMsg: err }) } }) 用户操作路径收集一些较隐蔽的错误如果只有错误栈信息,排查起来会比较难,如果有用户操作的路径,在排查时就方便多了。 方法一:暴力打点方法收集 优点:简单直接 缺点:污染业务代码,造成较多垃圾代码 方法二:函数劫持(推荐使用) 需要在App函数中的onLaunch、onShow、onHide生命周期插入监控代码,我们通过重写App生命周期函数来实现。 App = function(app) { ["onLaunch", "onShow", "onHide"].forEach(methodName => { app[methodName] = function(options) { // 构造访问日志对象 var breadcrumb = { type: "function", time: utils.now(), belong: "App", // 来源 method: methodName, path: options && options.path, // 页面路径 query: options && options.query, // 页面参数 scene: options && options.scene // 场景编号 }; self.pushToBreadcrumb(breadcrumb); // 把执行对象加入到面包屑中 }) }但是这样写,会把用户自定义的内容给覆盖掉,所以我们还需要把用户定义的函数和监控代码合并。 var originApp = App // 保存原对象 App = function(app) { // .... 此处省略监控代码 // .... 此处省略监控代码 originApp(app) // 执行用户定义的方法 }记录结果 可以从下面的json看出,用户到了detail页面,执行了onLoad => getDetail => onReady => buy 当执行buy方法的时候报错。 [{"method":"onLoad","route":"pages/film/detail","options":{"id":"4206"}}, {"method":"getDetail","route":"pages/film/detail","options":{"id":"4206"}}, {"method":"onReady","route":"pages/film/detail","options":{"id":"4206"}},{"method":"buy","route":"pages/film/detail","options":{"id":"4206"}}] 上报策略考虑到在大型应用中,日志量比较大,我们采取抽样,合并,过滤三个方法减少日志的输出,代码实现可以参考lib/report.js 代码组织项目使用rollup作为构建工作,实现ES6转ES5,模块加载功能。 项目目录如下: config.js // 配置文件 core.js // 劫持小程序核心代码 events.js // 监听自定义事件 report.js // 上报类 utils.js // 工具类 🌟喜欢的点个star:https://github.com/zhengguorong/xbossdebug-wechat
2018-06-06 - 小程序实现索引列表性能优化
简介 本文主要分享个人最近在小程序里面开发索引列表总结经验,索引列表一种一次性展示庞大的数据,并且可以通过点击某个索引跳转到指定索引的内容区域,方便用户快速找到相关的选项 本文直接使用 真实的数据 来深入分析和讨论索引列表 优化手段,功能的实现逻辑不在本文讨论范围,直接去看代码片段即可 本文也为大家带来平时我们经常遇到 假如服务端一次性返回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 - Painter 一款轻量级的小程序海报生成组件
生成海报相信大家有的人都做过,但是canvas绘图的坑太多。大家可以试试这个组件。然后附上楼下大哥做的可视化拖拽生成painter代码的工具:链接地址https://developers.weixin.qq.com/community/develop/article/doc/000e222d9bcc305c5739c718d56813
2019-09-27 - 如何实现快速生成朋友圈海报分享图
由于我们无法将小程序直接分享到朋友圈,但分享到朋友圈的需求又很多,业界目前的做法是利用小程序的 Canvas 功能生成一张带有小程序码的图片,然后引导用户下载图片到本地后再分享到朋友圈。相信大家在绘制分享图中应该踩到 Canvas 的各种(坑)彩dan了吧~ 这里首先推荐一个开源的组件:painter(通过该组件目前我们已经成功在支付宝小程序上也应用上了分享图功能) 咱们不多说,直接上手就是干。 [图片] 首先我们新增一个自定义组件,在该组件的json中引入painter [代码]{ "component": true, "usingComponents": { "painter": "/painter/painter" } } [代码] 然后组件的WXML (代码片段在最后) [代码]// 将该组件定位在屏幕之外,用户查看不到。 <painter style="position: absolute; top: -9999rpx;" palette="{{imgDraw}}" bind:imgOK="onImgOK" /> [代码] 重点来了 JS (代码片段在最后) [代码]Component({ properties: { // 是否开始绘图 isCanDraw: { type: Boolean, value: false, observer(newVal) { newVal && this.handleStartDrawImg() } }, // 用户头像昵称信息 userInfo: { type: Object, value: { avatarUrl: '', nickName: '' } } }, data: { imgDraw: {}, // 绘制图片的大对象 sharePath: '' // 生成的分享图 }, methods: { handleStartDrawImg() { wx.showLoading({ title: '生成中' }) this.setData({ imgDraw: { width: '750rpx', height: '1334rpx', background: 'https://qiniu-image.qtshe.com/20190506share-bg.png', views: [ { type: 'image', url: 'https://qiniu-image.qtshe.com/1560248372315_467.jpg', css: { top: '32rpx', left: '30rpx', right: '32rpx', width: '688rpx', height: '420rpx', borderRadius: '16rpx' }, }, { type: 'image', url: this.data.userInfo.avatarUrl || 'https://qiniu-image.qtshe.com/default-avatar20170707.png', css: { top: '404rpx', left: '328rpx', width: '96rpx', height: '96rpx', borderWidth: '6rpx', borderColor: '#FFF', borderRadius: '96rpx' } }, { type: 'text', text: this.data.userInfo.nickName || '青团子', css: { top: '532rpx', fontSize: '28rpx', left: '375rpx', align: 'center', color: '#3c3c3c' } }, { type: 'text', text: `邀请您参与助力活动`, css: { top: '576rpx', left: '375rpx', align: 'center', fontSize: '28rpx', color: '#3c3c3c' } }, { type: 'text', text: `宇宙最萌蓝牙耳机测评员`, css: { top: '644rpx', left: '375rpx', maxLines: 1, align: 'center', fontWeight: 'bold', fontSize: '44rpx', color: '#3c3c3c' } }, { type: 'image', url: 'https://qiniu-image.qtshe.com/20190605index.jpg', css: { top: '834rpx', left: '470rpx', width: '200rpx', height: '200rpx' } } ] } }) }, onImgErr(e) { wx.hideLoading() wx.showToast({ title: '生成分享图失败,请刷新页面重试' }) //通知外部绘制完成,重置isCanDraw为false this.triggerEvent('initData') }, onImgOK(e) { wx.hideLoading() // 展示分享图 wx.showShareImageMenu({ path: e.detail.path, fail: err => { console.log(err) } }) //通知外部绘制完成,重置isCanDraw为false this.triggerEvent('initData') } } }) [代码] 那么我们该如何引用呢? 首先json里引用我们封装好的组件share-box [代码]{ "usingComponents": { "share-box": "/components/shareBox/index" } } [代码] 以下示例为获取用户头像昵称后再生成图。 [代码]<button class="intro" bindtap="getUserInfo">点我生成分享图</button> <share-box isCanDraw="{{isCanDraw}}" userInfo="{{userInfo}}" bind:initData="handleClose" /> [代码] 调用的地方: [代码]const app = getApp() Page({ data: { isCanDraw: false }, // 组件内部关掉或者绘制完成需重置状态 handleClose() { this.setData({ isCanDraw: !this.data.isCanDraw }) }, getUserInfo(e) { wx.getUserProfile({ desc: "获取您的头像昵称信息", success: res => { const { userInfo = {} } = res this.setData({ userInfo, isCanDraw: true // 开始绘制海报图 }) }, fail: err => { console.log(err) } }) } }) [代码] 最后绘制分享图的自定义组件就完成啦~效果图如下: [图片] tips: 文字居中实现可以看下代码片段 文字换行实现(maxLines)只需要设置宽度,maxLines如果设置为1,那么超出一行将会展示为省略号 代码片段:https://developers.weixin.qq.com/s/J38pKsmK7Qw5 附上painter可视化编辑代码工具:点我直达,因为涉及网络图片,代码片段设置不了downloadFile合法域名,建议真机开启调试模式,开发者工具 详情里开启不校验合法域名进行代码片段的运行查看。 最后看下面大家评论问的较多的问题:downLoadFile合法域名在小程序后台 开发>开发设置里配置,域名为你图片的域名前缀 比如我文章里的图https://qiniu-image.qtshe.com/20190605index.jpg。配置域名时填写https://qiniu-image.qtshe.com即可。如果你图片cdn地址为https://aaa.com/xxx.png, 那你就配置https://aaa.com即可。
2022-01-20 - [填坑手册]小程序Canvas生成海报(一)--完整流程
[图片] 海报生成示例 最近智酷君在做[小程序]canvas生成海报的项目中遇到一些棘手的问题,在网上查阅了各种资料,也踩扁了各种坑,智酷君希望把这些“填坑”经验整理一下分享出来,避免后来的兄弟重复“掉坑”。 [图片] 原型图 这是一个大致的原型图,下面来看下如何制作这个海报,以及整体的思路。 [图片] 海报生成流程 [代码片段]Canvas生成海报实战demo demo的微信路径:https://developers.weixin.qq.com/s/Q74OU3m57c9x demo的ID:Q74OU3m57c9x 如果你装了IDE工具,可以直接访问上面的demo路径 通过代码片段将demo的ID输入进去也可添加: [图片] [图片] 下面分享下主要的代码内容和“填坑现场”: 一、添加字体 https://developers.weixin.qq.com/miniprogram/dev/api/canvas/font.html [代码]canvasContext.font = value //示例 ctx.font = `normal bold 20px sans-serif`//设置字体大小,默认10 ctx.setTextAlign('left'); ctx.setTextBaseline("top"); ctx.fillText("《智酷方程式》专注研究和分享前端技术", 50, 15, 250)//绘制文本 [代码] 符合 CSS font 语法的 DOMString 字符串,至少需要提供字体大小和字体族名。默认值为 10px sans-serif 文字过长在canvas下换行问题处理(最多两行,超过“…”代替) [代码]ctx.setTextAlign('left'); ctx.setFillStyle('#000');//文字颜色:默认黑色 ctx.font = `normal bold 18px sans-serif`//设置字体大小,默认10 let canvasTitleArray = canvasTitle.split(""); let firstTitle = ""; //第一行字 let secondTitle = ""; //第二行字 for (let i = 0; i < canvasTitleArray.length; i++) { let element = canvasTitleArray[i]; let firstWidth = ctx.measureText(firstTitle).width; //console.log(ctx.measureText(firstTitle).width); if (firstWidth > 260) { let secondWidth = ctx.measureText(secondTitle).width; //第二行字数超过,变为... if (secondWidth > 260) { secondTitle += "..."; break; } else { secondTitle += element; } } else { firstTitle += element; } } //第一行文字 ctx.fillText(firstTitle, 20, 278, 280)//绘制文本 //第二行问题 if (secondTitle) { ctx.fillText(secondTitle, 20, 300, 280)//绘制文本 } [代码] 通过 ctx.measureText 这个方法可以判断文字的宽度,然后进行切割。 (一行字允许宽度为280时,判断需要写小点,比如260) 二、获取临时地址并设置图片 [代码]let mainImg = "https://demo.com/url.jpg"; wx.getImageInfo({ src: mainImg,//服务器返回的图片地址 success: function (res) { //处理图片纵横比例过大或者过小的问题!!! let h = res.height; let w = res.width; let setHeight = 280, //默认源图截取的区域 setWidth = 220; //默认源图截取的区域 if (w / h > 1.5) { setHeight = h; setWidth = parseInt(280 / 220 * h); } else if (w / h < 1) { setWidth = w; setHeight = parseInt(220 / 280 * w); } else { setHeight = h; setWidth = w; }; console.log(setWidth, setHeight) ctx.drawImage(res.path, 0, 0, setWidth, setHeight, 20, 50, 280, 220); ctx.draw(true); }, fail: function (res) { //失败回调 } }); [代码] 在开发过程中如果封面图无法按照约定的比例(280x220)给到: 那么我们就需要处理默认封面图过大或者过小的问题,大致思路是:代码中通过比较纵横比(280/220=1.27)正比例放大或者缩小原图,然后从左上切割,竟可能保证过高的图是宽度100%,过宽的图是高度100%。 在canvas中draw图片,必须是一个(相对)本地路径,我们可以通过将图片保存在本地后生成的临时路径。 微信官方提供两个API: wx.downloadFile(OBJECT)和wx.getImageInfo(OBJECT)。都需先配置download域名才能生效。 三、裁切“圆形”头像画图 [代码]ctx.save(); //保存画图板 ctx.beginPath()//开始创建一个路径 ctx.arc(35, 25, 15, 0, 2 * Math.PI, false)//画一个圆形裁剪区域 ctx.clip()//裁剪 ctx.closePath(); ctx.drawImage(headImageLocal, 20, 10, 30, 30); ctx.draw(true); ctx.restore()//恢复之前保存的绘图上下文 [代码] 使用图形上下文的不带参数的clip()方法来实现Canvas的图像裁剪功能。该方法使用路径来对Canvas话不设置一个裁剪区域。因此,必须先创建好路径。创建完整后,调用clip()方法来设置裁剪区域。 需要注意的是裁剪是对画布进行的,裁切后的画布不能恢复到原来的大小,也就是说画布是越切越小的,要想保证最后仍然能在canvas最初定义的大小下绘图需要注意save()和restore()。画布是先裁切完了再进行绘图。并不一定非要是图片,路径也可以放进去~ 小程序 canvas 裁切BUG [代码]ctx.setFillStyle("#fff"); ctx.fillRect(0, 0, 320, 500); //第一个填充矩形 wx.downloadFile({ url: headUri, success(res) { ctx.beginPath() ctx.arc(50, 50, 25, 0, 2 * Math.PI) ctx.clip() ctx.drawImage(res.tempFilePath, 25, 25); //第二个填充图片 ctx.draw() ctx.restore() ctx.setFillStyle("#fff"); ctx.fillRect(0, 0, 320, 500); ctx.draw(true) ctx.restore() } }) [代码] clip裁切这个功能,如果有超过一张图片/背景叠加,则裁切效果失效。 错误参考:http://html51.com/info-38753-1/ 四、将canvas导出成虚拟地址 [代码]wx.canvasToTempFilePath({ fileType: 'jpg', canvasId: 'customCanvas', success: (res) => { console.log(res.tempFilePath) //为canvas的虚拟地址 } }) res: { errMsg: "canvasToTempFilePath:ok", tempFilePath: "http://tmp/wx02935bb29080a7b4.o6zAJswFAuZuKQ5NZfPr….cGnD1a02PlVC0b3284be3a41d08986c2477579a5fd8e.jpg" } [代码] 这里需要把canvas里面的内容,导出成一个临时地址才能保存在相册,比如: http://tmp/wx02935bb29080a7b4.o6zAJswFAuZuKQ5NZfPr5UfJVR4k.cGnD1a02PlVC0b3284be3a41d08986c2477579a5fd8e.jpg 五、询问并获取访问手机本地相册权限 [代码]wx.getSetting({ success(res) { console.log(res) if (!res.authSetting['scope.writePhotosAlbum']) { //判断权限 wx.authorize({ //获取权限 scope: 'scope.writePhotosAlbum', success() { console.log('授权成功') //转化路径 self.saveImg(); } }) } else { self.saveImg(); } } }) [代码] 判断是否有访问相册的权限,如果没有,则请求权限。 六、保存到用户手机本地相册 [代码]wx.saveImageToPhotosAlbum({ filePath: res.tempFilePath, success: function (data) { wx.showToast({ title: '保存到系统相册成功', icon: 'success', duration: 2000 }) }, fail: function (err) { console.log(err); if (err.errMsg === "saveImageToPhotosAlbum:fail auth deny") { console.log("当初用户拒绝,再次发起授权") wx.openSetting({ success(settingdata) { console.log(settingdata) if (settingdata.authSetting['scope.writePhotosAlbum']) { console.log('获取权限成功,给出再次点击图片保存到相册的提示。') } else { console.log('获取权限失败,给出不给权限就无法正常使用的提示') } } }) } else { wx.showToast({ title: '保存失败', icon: 'none' }); } }, complete(res) { console.log(res); } }) [代码] 保存到本地需要一定的时间,需要加一个loading的状态。 七、关于组件中引用canvas [代码]let ctx = wx.createCanvasContext('posterCanvas',this); //需要加this [代码] 在components中canvas无法选中的问题: 在components自定义组件下,当前组件实例的this,表示在这个自定义组件下查找拥有 canvas-id 的 <canvas> ,如果省略则不在任何自定义组件内查找。
2021-09-13 - 微信小程序分页加载数据~上拉加载更多~小程序云数据库的分页加载
我们在开发小程序时,一个列表里难免会有很多条数据,比如我们一个列表有1000条数据,我们一下加载出来,而不做分页,将会严重影响性能。所以这一节,我们来讲讲小程序分页加载数据的实现。 老规矩,先看效果图 [图片] 可以看到我们每页显示10条数据,当滑动到底部时,会加载第二页的数据,再往下滑动,就加载第三页的数据。由于我们一共21条数据,所以第三页加载完以后,会有一个“已加载全部数据”的提示。 本节知识点 1,小程序分页加载 2,小程序列表显示 3,云数据库的使用 4,云数据库分页请求数据的实现 一,先定义数据 我们做分页数据加载,肯定要先准备好数据,数据已经给大家准备好,如下图,文章末尾会贴出数据源和本节课源码的下载地址。 [图片] 然后把数据导入到我们的云开发的数据库里,关于数据如何导入,这里不再讲解,不知道的同学,请看下面这篇文章。或者去老师历史文章里找一下。 《小程序云开发入门—云数据库数据源的导入与导出》 下面给大家看下我们的数据源,长什么样。其实很简单,就是简单的定义21条数据。 [图片] 然后在看导入到数据库的样子。 [图片] 二,分页请求数据 我们第一步准备好了数据以后,接下来就来讲讲如何在js里做分页加载数据。 首先我们这里用到了小程序云开发数据库的知识点 1,get方法:获取云数据库数据 2,skip方法:跳过前面几条数据,请求后面的数据 3,limit方法:请求多少条数据。 比如下面这段代码,就是跳过前5条,请求从第6条开始往后的10条数据,就是请求6~15的数据,我们做分页加载也就是基于这个原理。 [代码] wx.cloud.database().collection("list") .skip(5) //从第几个数据开始 .limit(10) [代码] 下面把我们index.js的完整代码贴给大家。 [代码]//老师微信:2501902696 let currentPage = 0 // 当前第几页,0代表第一页 let pageSize = 10 //每页显示多少数据 Page({ data: { dataList: [], //放置返回数据的数组 loadMore: false, //"上拉加载"的变量,默认false,隐藏 loadAll: false //“没有数据”的变量,默认false,隐藏 }, //页面显示的事件 onShow() { this.getData() }, //页面上拉触底事件的处理函数 onReachBottom: function() { console.log("上拉触底事件") let that = this if (!that.data.loadMore) { that.setData({ loadMore: true, //加载中 loadAll: false //是否加载完所有数据 }); //加载更多,这里做下延时加载 setTimeout(function() { that.getData() }, 2000) } }, //访问网络,请求数据 getData() { let that = this; //第一次加载数据 if (currentPage == 1) { this.setData({ loadMore: true, //把"上拉加载"的变量设为true,显示 loadAll: false //把“没有数据”设为false,隐藏 }) } //云数据的请求 wx.cloud.database().collection("list") .skip(currentPage * pageSize) //从第几个数据开始 .limit(pageSize) .get({ success(res) { if (res.data && res.data.length > 0) { console.log("请求成功", res.data) currentPage++ //把新请求到的数据添加到dataList里 let list = that.data.dataList.concat(res.data) that.setData({ dataList: list, //获取数据数组 loadMore: false //把"上拉加载"的变量设为false,显示 }); if (res.data.length < pageSize) { that.setData({ loadMore: false, //隐藏加载中。。 loadAll: true //所有数据都加载完了 }); } } else { that.setData({ loadAll: true, //把“没有数据”设为true,显示 loadMore: false //把"上拉加载"的变量设为false,隐藏 }); } }, fail(res) { console.log("请求失败", res) that.setData({ loadAll: false, loadMore: false }); } }) }, }) [代码] 上面的代码就是我们实现分页加载的所有逻辑代码。简单说下代码 1,我们首先进页面时会请求前10条内容 2,10条内容请求成功以后,我们会把请求到的内容加入dataList数组,然后把dataList里的数据显示到页面上。并将currentPage加一,用于请求第二页数据。 3,当用户滑动到底部时,会触发onReachBottom事件,在这个事件里做第二页到请求。然后第二页数据请求成功以后。继续将currentPage加1,这里要记住一定,一定要请求成功以后才将currentPage +1。 三,列表布局和样式 其实index.wxml和index.wxss的代码很简单,给大家把代码贴出来。 1,index.wxml [代码]<scroll-view scroll-y="true" bindscrolltolower="searchScrollLower"> <view class="result-item" wx:for="{{dataList}}" wx:key="item"> <text class="title">{{item.content}}</text> </view> <view class="loading" hidden="{{!loadMore}}">正在载入更多...</view> <view class="loading" hidden="{{!loadAll}}">已加载全部</view> </scroll-view> [代码] 2,index.wxss [代码]page { display: flex; flex-direction: column; height: 100%; } .result-item { display: flex; flex-direction: column; padding: 20rpx 0 20rpx 110rpx; overflow: hidden; border-bottom: 2rpx solid #e5e5e5; } .title { height: 110rpx; } .loading { position: relative; bottom: 5rpx; padding: 10rpx; text-align: center; } [代码] 到这里我们就完整的实现里分页加载功能了。 [图片] 源码和数据源,已经给大家放到网盘里了,有需要的同学请在文章底部留言,或者私信老师。 视频讲解:https://edu.csdn.net/course/detail/9604
2019-11-07 - swiper滑动视图如何修改指示点样式?
swiper滑动视图如何修改指示点样式? 如何找到指示点 并绑定事件?
2020-08-06 - 原生小程序根据当前版本自动切换 `开发版本、体验版本、正式版本`
原生小程序根据当前版本自动切换 [代码]开发版本、体验版本、正式版本[代码] 接口地址 使用微信官方提供的Api 完美解决上传发布手动修改接口地址 wx.getAccountInfoSync 代码实现 [图片] 源代码(方便复制粘贴)0_0 [代码]// 获取小程序当前版本信息 https://developers.weixin.qq.com/miniprogram/dev/api/open-api/account-info/wx.getAccountInfoSync.html // 自动根据版本切换接口请求地址 const { miniProgram: { envVersion } } = wx.getAccountInfoSync(); let url = ''; switch (envVersion) { case 'develop': url = `${defaultConfig.devUrl}${params.url}`; break; case 'trial': url = `${defaultConfig.devUrl}${params.url}`; break; case 'release': url = `${defaultConfig.prodUrl}${params.url}`; break; default: url = `${defaultConfig.baseUrl}${params.url}`; break; } console.log(url, 'url'); console.log(envVersion, 'envVersion'); [代码]
2020-09-24