- 小程序app.onLaunch与page.onLoad异步问题的最佳实践
场景: 在小程序中大家应该都有这样的场景,在onLaunch里用wx.login静默登录拿到code,再用code去发送请求获取token、用户信息等,整个过程都是异步的,然后我们在业务页面里onLoad去用的时候异步请求还没回来,导致没拿到想要的数据,以往要么监听是否拿到,要么自己封装一套回调,总之都挺麻烦,每个页面都要写一堆无关当前页面的逻辑。 直接上终极解决方案,公司内部已接入两年很稳定: 1.可完美解决异步问题 2.不污染原生生命周期,与onLoad等钩子共存 3.使用方便 4.可灵活定制异步钩子 5.采用监听模式实现,接入无需修改以前相关逻辑 6.支持各种小程序和vue架构 。。。 //为了简洁明了的展示使用场景,以下有部分是伪代码,请勿直接粘贴使用,具体使用代码看Github文档 //app.js //globalData提出来声明 let globalData = { // 是否已拿到token token: '', // 用户信息 userInfo: { userId: '', head: '' } } //注册自定义钩子 import CustomHook from 'spa-custom-hooks'; CustomHook.install({ 'Login':{ name:'Login', watchKey: 'token', onUpdate(token){ //有token则触发此钩子 return !!token; } }, 'User':{ name:'User', watchKey: 'userInfo', onUpdate(user){ //获取到userinfo里的userId则触发此钩子 return !!user.userId; } } }, globalData) // 正常走初始化逻辑 App({ globalData, onLaunch() { //发起异步登录拿token login((token)=>{ this.globalData.token = token //使用token拿用户信息 getUser((user)=>{ this.globalData.user = user }) }) } }) //关键点来了 //Page.js,业务页面使用 Page({ onLoadLogin() { //拿到token啦,可以使用token发起请求了 const token = getApp().globalData.token }, onLoadUser() { //拿到用户信息啦 const userInfo = getApp().globalData.userInfo }, onReadyUser() { //页面初次渲染完毕 && 拿到用户信息,可以把头像渲染在canvas上面啦 const userInfo = getApp().globalData.userInfo // 获取canvas上下文 const ctx = getCanvasContext2d() ctx.drawImage(userInfo.head,0,0,100,100) }, onShowUser() { //页面每次显示 && 拿到用户信息,我要在页面每次显示的时候根据userInfo走不同的逻辑 const userInfo = getApp().globalData.userInfo switch(userInfo.sex){ case 0: // 走女生逻辑 break case 1: // 走男生逻辑 break } } }) 具体文档和Demo见↓ Github:https://github.com/1977474741/spa-custom-hooks 祝大家用的愉快,记得star哦
2023-04-23 - 小程序中如何加载使用第三方字体
简述:使用开启CORS的woff字体格式(或ttf)的https资源地址,使用[代码]wx.loadFontFace[代码] API(css原生[代码]@font-face[代码]语法)加载使用。 ⚠️ 注意:字体商用需授权许可,目前免费可商用的大部分字体,可见参考索引5。以下示例使用的是开源字体:得意黑和钉钉进步体。 1. wx.loadFontFace使用wx.loadFontFace 加载字体,配合wxss和wxml使用,参见文档 & 官方示例 // -- wxss -- // .DingTalk-JinBuTi { // font-family: 'DingTalk-JinBuTi'; // } wx.loadFontFace({ family: 'DingTalk-JinBuTi', source: 'url("https://xxx/font/DingTalk-JinBuTi.woff")', //此处需替换为真实字体地址 success(res) { console.log(res.status) }, fail: function (res) { console.log(res.status) }, complete: function (res) { console.log(res.status) } }); 注意: 格式支持常见的基本所有类型(ttf、woff、woff2(web主流)、otf、sfnt),建议格式为 TTF 和 WOFF(推荐),WOFF2 在低版本的 iOS 上会不兼容。字体链接仅支持https链接字体链接访问需满足浏览器同源策略,字体文件资源设置CORS的[代码]Access-Control-Allow-Origin[代码]为小程序域名:[代码]servicewechat.com[代码]或者*才可以。经过测试,ios和小米手机对于未设置CORS的字体文件仍然可以正常加载,荣耀和vivo无法正常加载,需要设置正确的CORS即可正常加载在加载成功之后,会自动刷新字体显示不需要设置downloadFile合法域名及业务域名。扩展:一次加载,全局使用如下在app.js中加载添加全局参数(global: true),即可在任意页面中的font-family中使用该字体。 onLaunch() { wx.loadFontFace({ family: 'DingTalk-JinBuTi', global: true, source: 'url("https://xxx/font/DingTalk-JinBuTi.woff")', //此处需替换为真实字体地址 success(res) { console.log(res.status) }, fail: function (res) { console.log(res.status) }, complete: function (res) { console.log(res.status) } }); }, 2. @font-face同css加载字体的规则,见MDN文档, 注意:字体链接访问需满足浏览器同源策略。支持常见格式。 @font-face { font-family: "SmileySans-Oblique"; src: url("https://xxx/font/SmileySans-Oblique.woff"); //此处需替换为真实字体地址 } .SmileySans-Oblique { font-family: 'SmileySans-Oblique'; } 扩展:全局使用在app.wxss中引入字体即可全局使用。 @font-face { font-family: "SmileySans-Oblique"; src: url("https://xxx/font/SmileySans-Oblique.woff"); //此处需替换为真实字体地址 } .SmileySans-Oblique { font-family: 'SmileySans-Oblique'; } 3. face-font base64 因源码过大致小程序增加包体积,暂不考虑。英文字体可以考虑。参见此。 扩展:在canvas中使用小程序canvas渲染时需要字体加载完成,css方式字体加载完成不容易监听,小程序文档提到可使用wx.loadFontFaced的回调中渲染canvas。参考文档及代码示例。 另外wxml-to-canvas中不支持font-family属性,可参考链接4修改源码实现。 参考wx.loadFontFace(Object object) | 微信开放文档微信小程序使用自定义字体的三种方法 - 掘金loadFontFace 支持全局生效 | 微信开放社区wxml-to-canvas没有fontWeight的相关支持? | 微信开放社区光明正大用字体——2023年“不要钱”的字体集合!哔哩哔哩bilibili[图片]
2023-03-21 - 小程序性能优化最佳实践
[视频] 本视频主要介绍小程序的启动流程和关键步骤,以及全新的引擎与框架能力。
10-14 - 使用分包异步化组件实现可变Tab页面
背景和需求 众所周知,在微信小程序内,TabBar 页面必须放主包内,这固然是为了用户体验做出的限制,但是也限制了开发者,如果想要实现不同的客户可以定制不同的TabBar页面,而很多页面又是分散到不同分包内的,那我们能选择的方案也就是在所有可作为TabBar页面上放置自定义TabBar组件,而后根据客户的不同配置,展示不同的TabBar 选项,当客户点击Tab时,使用[代码]navigateTo[代码]或[代码]redirectTo[代码]进行切换页面。 但这个方案存在明显的问题,首先如果使用[代码]navigateTo[代码]进行切换,会有很明显的页面切换动画,很容易到达10层页面栈限制(当然这个可以使用无限路由方案进行缓解,但是无限路由是一种万不得已且体验很差的路由方案),且由于页面未进行销毁,内存占用会比较大,容易造成卡顿;如果使用[代码]redirectTo[代码]进行切换,页面节点状态无法保存(如滚动位置),页面数据倒是可以使用全局状态管理库进行保存,但是每次在切换 Tab 都会有明显的数据重新加载的动画效果。 曙光 在微信小程序支持分包异步化之前,对于上面的问题一直没有好的解决方案,支持分包异步化之后,我们可以将一些组件放入分包内异步加载,这一定程度上解决了主包过大的问题。同时也让我们看到了希望,我们可以将很多组件放入分包内进行异步加载,主包空间空了出来,可以放更多的页面,但不是所有页面都能放入主包,那还有其他方案吗? 我们想,既然组件能从分包异步加载,那页面可以吗? 我们知道,在微信小程序内,通常都会使用Page进行声明页面,但也可以用Component声明页面,也就是说 Component 声明的组件可以当成页面用,那反过来,Page 声明的页面可以当成组件用吗? 答案是可以,但是当这样使用的时候,页面的生命周期方法不会被执行,且实例对象上不存在options(页面路由参数),route(当前页面路由地址)等数据,那我们就不能愉快地玩耍了吗? No! 没有页面该有的属性?那我们就拿到实例对象给他补上去! 生命周期方法不执行?那我们就拿到实例对象后自己去调用! 解决思路 要将现有页面作为组件加载,那我们必须要有一个容器页面,去承载真实页面,在容器页面中去补上已经作为组件的真实页面缺失的属性,在对应的生命周期方法中调用真实页面的生命周期钩子。 我们第一步就需要创建一个容器页面出来,我们可以选择手动创建,也可以自动化创建, 但是已有项目来说,手动创建太费时,且每增加新页面都要修改容器页面代码,故此不考虑。 自动化构建容器页面包含如下步骤: 读取 app.json,获取所有分包页面路径 读取分包页面对应的json文件,将其中内容记录到 [代码]tab-bar-page-config.js[代码] 中,因为我们需要在运行时读取真实页面的标题,背景色等信息,而微信小程序不支持从js中读取json文件,故需要将json内容提前读取出来,为了减少数据量,记录时可以将[代码]usingComponents[代码]等无需运行时使用的数据去掉。效果如下: [代码]// tab-bar-page-config.js module.exports = { "/pack_a/page_1": { "navigationBarTitleText": "页面标题", "navigationBarBackgroundColor": "#ffffff" }, "/pack_b/page_2": { "navigationBarTitleText": "页面标题", "navigationBarBackgroundColor": "#ffffff" } /* 其他页面信息 */ } [代码] 生成 wxml 文件,效果如下: [代码]<pack_a_page_1 id="pack_a_page_1" wx:if="{{ pagePath === '/pack_a/page_1' }}" /> <pack_b_page_2 id="pack_b_page_2" wx:elif="{{ pagePath === '/pack_b/page_2' }}" /> <!-- 其他页面节点 --> [代码] 生成容器页面 json 文件,效果如下: [代码]{ "usingComponents": { "pack_a_page_1": "/pack_a/page_1", "pack_b_page_2": "/pack_b/page_2", /* 其他页面 */ }, "componentPlaceholder": { "pack_a_page_1": "view", "pack_b_page_2": "view", /* 其他页面 */ } } [代码] 编写容器页面 js 逻辑,大体如下: [代码]Page({ data: { // 当前真实页面的路径 pagePath: '', }, // 真实页面的实例 pageInstance: null, onLoad() { // 根据网络接口返回数据,得到当前容器页面应当显示的真实页面路径 this.setData({ pagePath: someDataFromNet.pagePath, }); }, onShow() { this.pageInstance?.onShow?.(); }, onReady() { this.pageInstance?.onReady?.(); }, /* 其他生命周期 */ }) [代码] 我们现在面临一个问题,那就是我们是使用分包异步化组件进行加载真实页面,那真实页面是什么时候加载成功的呢?我们知道当组件加载成功后,会执行组件的 [代码]lifetimes.attached[代码] 生命周期, 那既然页面可以当成组件用,那页面是否也有这个生命周期呢?通过查阅文档,我们知道了可以在页面中使用[代码]Behavior[代码], 我们可以通过[代码]Behavior[代码]中定义 [代码]lifetimes.attached[代码],在其中通过 [代码]this.triggerEvent('pageattached')[代码] 去通知容器页面,现在我们的 wxml 需要做一些修改,如下: [代码]<pack_a_page_1 wx:if="{{ pagePath === '/pack_a/page_1' }}" bind:pageattached="onPageAttached" /> <pack_b_page_2 wx:elif="{{ pagePath === '/pack_b/page_2' }}" bind:pageattached="onPageAttached" /> <!-- 其他页面节点 --> [代码] js 中也要增加 [代码]onPageAttached[代码] 方法,如下: [代码]Page({ /* 其他生命周期方法 */ onPageAttached() { const route = this.data.pagePath.slice(1); const id = route.replace(/\//g, '_'); const page = this.selectComponent(`#${route}`); page.route = route; page.options = {}; // 补全其他信息, // 调用对应生命周期方法, page.onLoad?.(page.options); // 由于组件可能加载得比较晚,容器页面的 onShow 和 onReady 已经执行过了,这里需要手动执行一遍真实页面的 onShow 和 onReady // 还需要额外做一些判断,避免 onShow 连续执行多遍 page.onShow?.(page.options); page.onReady?.(page.options); }, }) [代码] 好了,准备工作基本上做完,现在就差给所有页面加上我们之前写的[代码]Behavior[代码]了,如果项目一开始就封装了[代码]BasePage[代码]之类的方法,我们只需要在 BasePage 将这个[代码]Behavior[代码]加到[代码]BasePage[代码]中就行,如果没有的话,可以通过改写[代码]Page[代码]去实现,这里就不举例了。 现在我们按照上面的步骤生成5个容器页面并且加入到 [代码]app.json[代码] 中了,然后开始下一步了, 等等。。。5个容器页面?生成的代码有5份!不行,这样会平白占用很多主包空间的,我们需要做一些优化:将生成5个容器页面优化成生成一个容器组件,然后在5个容器页面内去引用该组件,并修改上面的一些逻辑,这样生成的代码就基本上少了1/5,还是很可观的。 现在还差封装[代码]switchTab[代码]方法了,在其中将[代码]url[代码]替换成容器页面的地址,然后记录该容器页面需要展示的真实页面地址,在容器页面中加载对应的真实页面即可。亦可改写 [代码]wx.switchTab[代码] 去调用我们封装的 [代码]switchTab[代码] 方法,在此就不举例了。 好了,现在基础步骤已完成,就差看效果了。 咦,好像还差某些东西,页面标题呢?怎么不能下拉刷新了?这个页面好像是没有顶部导航栏的呀。 我们一个一个来。 标题及背景颜色等 还记得之前生成的 [代码]tab-bar-page-config.js[代码] 吗?我们在其中记录了页面的一些信息,现在,我们需要在运行时去调用微信API设置标题,颜色等信息。解决。 下拉刷新 微信没有提供是否启用下拉刷新的API,所以我们只能给所有容器页面都加上下拉刷新,然后 [代码]onPullDownRefresh[代码] 中判断如果当前真实页面没有启用下拉刷新,就调用[代码]wx.stopPullDownRefresh[代码]停止下拉刷新,否则就调用真实页面的[代码]onPullDownRefresh[代码]钩子。额。。。勉强算解决吧。 顶部导航栏 微信同样没提供是否启用顶部导航栏的API,故只能将5个容器页面分成2类,2个是不带顶部导航的,剩下3个是带顶部导航的,在我们封装的 [代码]switchTab[代码] 中增加判断要跳转的页面是否是包含顶部导航的,分别落到不同的容器页面上即可。解决。 至此,动态Tab页面基本上实现了,还有些样式上的兼容问题,如:某个页面的wxss声明了 [代码]page { backgroud: 'red'; } [代码] 那容器页面内的所有页面都会被影响,对此我们只能在页面的[代码]wxss[代码]中不使用[代码]标签选择器[代码],实际上在微信开发者工具中,使用[代码]标签选择器[代码]是会报警告的,但是口头约束是没有用的,还是会有人会写,故我们引入了[代码]postcss[代码],编写插件使在构建时将标签选择器去掉,并且报出警告。 至此,功能基本完成,需要做的就是验证哪些功能出现了问题,做出相应的修改。 结尾 分包异步化作为一个新出现的特性,还存在一些不稳定,如在开发者工具中,经常出现加载失败的问题,ios 真机调试报错等问题,且要求的最低SDK版本为[代码]2.17.3[代码],要在生产环境中使用还需要做很多的验证工作,也希望微信官方能尽早修改开发者工具中的问题。
2021-11-29 - 小程序图片懒加载终极方案
效果图 既然来了,把妹子都给你。 [图片] 定义懒加载,前端人都知道的一种性能优化方式,简单的来说,只有当图片出现在浏览器的可视区域内时,才设置图片正真的路径,让图片显示出来。这就是图片懒加载。 实现原理监听页面的[代码]scroll[代码]事件,判读元素距离页面的[代码]top[代码]值是否是小于等于页面的可视高度 判断逻辑代码如下 [代码]element.getBoundingClientRect().top <= document.documentElement.clientHeight ? 显示 : 默认[代码] 我们知道小程序页面的脚本逻辑是在JsCore中运行,JsCore是一个没有窗口对象的环境,所以不能在脚本中使用window,也无法在脚本中操作组件。 所以关于图片懒加载就需要在数据上面做文章了。 页面页面上面只需要根据数据的某一个字段来判断是否显示图片就可以了,字段为Boolean类型,当为false的时候显示默认图片就行了。 代码大概长成这样 <view wx:for="{{list}}" class='item item-{{index}}' wx:key="{{index}}"> <image class="{{item.show ? 'active': ''}}" src="{{item.show ? item.src : item.def}}"></image> </view> 布局跟简单,[代码]view[代码]组件里面有个图片,并循环[代码]list[代码],有多少就展示多少 [代码]image[代码]组件的[代码]src[代码]字段通过每一项的[代码]show[代码]来进行绑定,[代码]active[代码]是加了个透明的过渡 样式 image{ transition: all .3s ease; opacity: 0; } .active{ opacity: 1; } 逻辑 本位主要讲解懒加载,所以把数据写死在页面上了 数据结构如下: [图片] 我们使用两种方式来实现懒加载,准备好没有,一起来快乐的撸码吧。 WXML节点信息 小程序支持调用createSelectQuery创建一个[代码]SelectorQuery[代码]实例,并使用[代码]select[代码]方法来选择节点,并通过[代码]boundingClientRect[代码]来获取节点信息。 wx.createSelectorQuery().select('.item').boundingClientRect((ret)=>{ console.log(ret) }).exec() 显示结果如下 [图片] 悄悄告诉你,小程序里面有个[代码]onPageScroll[代码]函数,是用来监听页面的滚动的。 还有个[代码]getSystemInfo[代码]函数,可以获取获取系统信息,里面包含屏幕的高度。 接下来,思路就透彻了吧。还是上面的逻辑, 扒拉扒拉直接写代码就行了,这里只写下主要的逻辑,完整代码请戳文末github showImg(){ let group = this.data.group let height = this.data.height // 页面的可视高度 wx.createSelectorQuery().selectAll('.item').boundingClientRect((ret) => { ret.forEach((item, index) => { if (item.top <= height) { 判断是否在显示范围内 group[index].show = true // 根据下标改变状态 } }) this.setData({ group }) }).exec() } onPageScroll(){ // 滚动事件 this.showImg() } 至此,我们完成了一个小程序版的图片懒加载,只是思维转变了下,其实并没有改变实现方式。我们来学些新的东西吧。 节点布局相交状态 节点相交状态是啥?它是一个新的API,叫做[代码]IntersectionObserver[代码], 本文只讲解简单的使用,了解更多请猛戳没错,就是点我 小程序里面给它的定义是节点布局交叉状态API可用于监听两个或多个组件节点在布局位置上的相交状态。这一组API常常可以用于推断某些节点是否可以被用户看见、有多大比例可以被用户看见。 里面设计的概念主要有五个,分别为 参照节点:以某参照节点的布局区域作为参照区域,参照节点可以有多个,多个话参照区域取它们的布局区域的交集目标节点:监听的目标,只能是一个节点相交区域:目标节点与参照节点的相交区域相交比例:目标节点与参照节点的相交比例阈值:可以有多个,默认为[0], 可以理解为交叉比例,例如[0.2, 0.5]关于它的API有五个,依次如下 1、[代码]createIntersectionObserver([this], [options])[代码],见名知意,创建一个IntersectionObserver实例 2、[代码]intersectionObserver.relativeTo(selector, [margins])[代码], 指定节点作为参照区域,margins参数可以放大缩小参照区域,可以包含top、left、bottom、right四项 3、[代码]intersectionObserver.relativeToViewport([margin])[代码],指定页面显示区域为参照区域 4、[代码]intersectionObserver.observer(targetSelector, callback)[代码],参数为指定监听的节点和一个回调函数,目标元素的相交状态发生变化时就会触发此函数,callback函数包含一个result,下面再讲 5、[代码]intersectionObserver.disconnect()[代码] 停止监听,回调函数不会再触发 然后说下callback函数中的result,它包含的字段为 [图片] 我们主要使用[代码]intersectionRatio[代码]进行判断,当它大于0时说明是相交的也就是可见的。 先来波测试题,请说出下面的函数做了什么,并且log函数会执行几次 1、 wx.createIntersectionObserver().relativeToViewport().observer('.box', (result) => { console.log('监听box组件触发的函数') }) 2、 wx.createIntersectionObserver().relativeTo('.box').observer('.item', (result) => { console.log('监听item组件触发的函数') }) 3、 wx.createIntersectionObserver().relativeToViewport().observer('.box', (result) => { if(result.intersectionRatio > 0){ console.log('.box组件是可见的') } }) duang,揭晓答案。 第一个以当前页面的视窗监听了[代码].box[代码]组件,log会触发两次,一次是进入页面一次是离开页面 第二个以[代码].box[代码]节点的布局区域监听了[代码].item[代码]组件,log会触发两次,一次是进入页面一次是离开页面 第三个以当前页面的视窗监听了[代码].box[代码]组件,log只会在节点可见的时候触发 好了,题也做了,API你也掌握了,相信你已经可以使用[代码]IntersectionObserver[代码]来实现图片懒加载了吧,主要逻辑如下 let group = this.data.group // 获取图片数组数据 for (let i in this.data.group){ wx.createIntersectionObserver().relativeToViewport().observe('.item-'+ i, (ret) => { if (ret.intersectionRatio > 0){ group[i].show = true } this.setData({ group }) }) } 最后 至此,我们使用两种方式实现了小程序版本的图片懒加载,可以发现,使用[代码]IntersectionObserver[代码]来实现不要太酸爽
2020-05-12