- 打开小程序设置页(wx.openSetting)接口调整
开发者可以通过 [代码]wx.openSetting[代码] 接口来打开小程序设置界面并返回用户的设置结果。在原来的 [代码]wx.openSetting[代码] 接口中,我们允许开发者直接调用此接口,但目前我们发现有不少开发者滥用此接口,使用户在无任何操作时,不断地强行跳转至设置页,导致用户无法正常使用甚至无法退出小程序。 为保证用户获得更顺畅的小程序使用体验,避免此类滥用情况,我们对该接口进行了调整。 调整后“打开小程序设置页”将支持以下两种实现方式: 方法1:使用 [代码]button[代码] 组件来使用此功能,示例代码如下: <button open-type="openSetting" bindopensetting="callback">打开设置页</button>方法2:由点击行为触发[代码]wx.openSetting[代码]接口的调用,示例代码如下: <button bindtap="openSetting">打开设置页</button> openSetting() { wx.openSetting()}方法2已在最新版开发者工具中支持(基础库切到2.2.4及以上),开发者可以尽早适配。 此次调整会对直接调用wx.openSetting接口造成影响 原无需用户点击即可直接调用wx.openSetting接口的实现方式将不再支持,即将废弃的错误使用方式示例如下: onShow() { wx.openSetting()} 10月10日起新提交发布的版本将会受到此调整的影响。 需要各位开发者注意,10月10日起新提交发布的小程序版本将不再支持无需用户点击即可直接调用的“打开小程序设置页”接口,请开发者尽早适配。 调整策略在基础库 2.3.0 及以上版本生效,该基础库版本对应微信客户端6.7.2版本。另外,考虑到兼容性等问题,在基础库版本 2.3.0 以下的环境中不受此策略影响。
2018-09-12 - 安卓 wx.getLocation 获取 altitude 为 0
获取altitude需要: 1.开启gps定位 2.开启高精度定位,因为gps定位一般耗时较久,试着将timeout设置成3-5s 因为getLocation是单次快速定位,如需精确位置可以考虑下持续定位接口,理论上定位点会随着时间越来越准. wx.startLocationUpdate wx.startLocationUpdateBackground
2020-12-16 - wx.getLocation报"errCode":404错误
定位分为网络定位&Gps定位,具体逻辑如下: 1.定位权限&定位开关开启&有sim卡的情况下会使用GPS定位,定位较准 2.不满足上述条件,但连了wifi,会通过网络定位,较不准确 3.网络定位都失败了就会报错
2020-12-04 - 安卓微信 XWeb 111 新内核,部分机型无法显示图片?
安卓微信 XWeb 111 新内核,部分机型无法显示图片?
2023-05-31 - 如何彻底解决小程序滚动穿透问题
背景 俗话说,产品有三宝:弹窗、浮层加引导,足以见弹窗在产品同学心目中的地位。对任意一个刚入门的前端同学来说,实现一个模态框基本都可以达到信手拈来的地步,但是,当模态框里边的内容滚动起来以后,就会出现各种各样的让人摸不着头脑的问题,其中,最出名的想必就是滚动穿透。 什么是滚动穿透? 滚动穿透的定义:指我们滑动顶层的弹窗,但效果上却滑动了底层的内容。 具体解决方案分析如下: 改变顶层:从穿透的思路考虑,如果顶层不会穿透过去,那么问题就解决了,所以我们尝试给蒙层加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 - 自定义组件发生Expect END descriptor...的坑!
今天打算把一个表格做成自定义组件,结果发现了一个大坑!!貌似官方文档上并没有对这里进行说明,所以希望能帮到遇到同样问题的同学。本人小白,技术能力不是很强,望说错的地方大神多多指点~ 下面进入正题 -------------------------------------<华丽的分割线>---------------------------------------------- 首先明确,我们在wxml中作条件判断的时候常用的是标签,所以循环表格组件的时候,我们是这样做的: [代码]<[代码][代码]block[代码] [代码]wx:if="{{goodDetail[attributeBoList].length > 0}}">[代码][代码] [代码][代码]<[代码][代码]view[代码] [代码]class[代码][代码]=[代码][代码]"table"[代码][代码]>[代码][代码] [代码][代码]<[代码][代码]view[代码] [代码]class[代码][代码]=[代码][代码]"tr"[代码][代码]>[代码][代码] [代码][代码]<[代码][代码]block[代码] [代码]wx:key[代码][代码]=[代码][代码]"testList-th"[代码] [代码]wx:for[代码][代码]=[代码][代码]"{{goodDetail[attributeBoList]}}"[代码][代码]>[代码][代码] [代码][代码]<[代码][代码]view[代码] [代码]class[代码][代码]=[代码][代码]"th"[代码][代码]>{{item[attributeModel][displayName]}}[代码]view[代码][代码]>[代码][代码][代码][代码] [代码][代码][代码]block[代码][代码]>[代码][代码][代码][代码][代码][代码] [代码][代码][代码]view[代码][代码]>[代码][代码][代码][代码][代码][代码][代码] [代码][代码]<[代码][代码]block[代码] [代码]wx:key[代码][代码]=[代码][代码]"testList-tr"[代码] [代码]wx:for[代码][代码]=[代码][代码]"{{goodDetail[attributeBoList][0][attributeValueModelList]}}"[代码] [代码]wx:for-index[代码][代码]=[代码][代码]"index"[代码][代码]>[代码][代码][代码][代码][代码][代码] [代码][代码]<[代码][代码]view[代码] [代码]class[代码][代码]=[代码][代码]"tr"[代码][代码]>[代码][代码][代码][代码][代码][代码] [代码][代码]<[代码][代码]block[代码] [代码]wx:key[代码][代码]=[代码][代码]"testList-td"[代码] [代码]wx:for[代码][代码]=[代码][代码]"{{goodDetail[attributeBoList]}}"[代码] [代码]wx:for-index[代码][代码]=[代码][代码]"idx"[代码][代码]>[代码][代码][代码][代码][代码][代码] [代码][代码]<[代码][代码]view[代码] [代码]class[代码][代码]=[代码][代码]"td"[代码][代码]>{{item[attributeValueModelList][index][attributeVal]}}[代码]view[代码][代码]>[代码][代码][代码][代码][代码][代码][代码] [代码][代码][代码]block[代码][代码]>[代码][代码][代码][代码][代码][代码][代码] [代码][代码][代码]view[代码][代码]>[代码][代码][代码][代码][代码][代码][代码] [代码][代码][代码]block[代码][代码]>[代码][代码][代码][代码][代码][代码][代码] [代码][代码][代码]view[代码][代码]>[代码][代码][代码][代码][代码][代码][代码] [代码][代码][代码]block[代码][代码]>[代码][代码][代码][代码][代码][代码][代码]<[代码][代码]block[代码] [代码]wx:else>[代码][代码][代码][代码][代码][代码] [代码][代码]<[代码][代码]text[代码][代码]>此套餐还没有详细信息哦~[代码]text[代码][代码]>[代码][代码][代码][代码][代码][代码][代码][代码]block[代码][代码]>[代码][代码][代码][代码]那这时候我想把class="table"的那一段打成组件。所以,坑开始了。 按照正常的流程,各种数据传递,引用之类的全部操作完毕,组件wxml为: [代码]<[代码][代码]view[代码] [代码]class[代码][代码]=[代码][代码]"table"[代码][代码]>[代码][代码] [代码][代码]<[代码][代码]view[代码] [代码]class[代码][代码]=[代码][代码]"tr"[代码][代码]>[代码][代码] [代码][代码]<[代码][代码]block[代码] [代码]wx:key[代码][代码]=[代码][代码]"testList-th"[代码] [代码]wx:for[代码][代码]=[代码][代码]"{{goodDetail[attributeBoList]}}"[代码][代码]>[代码][代码] [代码][代码]<[代码][代码]view[代码] [代码]class[代码][代码]=[代码][代码]"th"[代码][代码]>[代码][代码] [代码][代码]{{item[attributeModel][displayName]}}[代码][代码] [代码][代码][代码]view[代码][代码]>[代码][代码][代码][代码] [代码][代码][代码]block[代码][代码]>[代码][代码][代码][代码][代码][代码] [代码][代码][代码]view[代码][代码]>[代码][代码][代码][代码][代码][代码][代码] [代码][代码]<[代码][代码]block[代码] [代码]wx:key[代码][代码]=[代码][代码]"testList-tr"[代码] [代码]wx:for[代码][代码]=[代码][代码]"{{goodDetail[attributeBoList][0][attributeValueModelList]}}"[代码] [代码]wx:for-index[代码][代码]=[代码][代码]"index"[代码][代码]>[代码][代码][代码][代码][代码][代码] [代码][代码]<[代码][代码]view[代码] [代码]class[代码][代码]=[代码][代码]"tr"[代码][代码]>[代码][代码][代码][代码][代码][代码] [代码][代码]<[代码][代码]block[代码] [代码]wx:key[代码][代码]=[代码][代码]"testList-td"[代码] [代码]wx:for[代码][代码]=[代码][代码]"{{goodDetail[attributeBoList]}}"[代码] [代码]wx:for-index[代码][代码]=[代码][代码]"idx"[代码][代码]>[代码][代码][代码][代码][代码][代码] [代码][代码]<[代码][代码]view[代码] [代码]class[代码][代码]=[代码][代码]"td"[代码][代码]>[代码][代码][代码][代码][代码][代码] [代码][代码]{{item[attributeValueModelList][index][attributeVal]}}[代码][代码][代码][代码][代码][代码] [代码][代码][代码]view[代码][代码]>[代码][代码][代码][代码][代码][代码][代码] [代码][代码][代码]block[代码][代码]>[代码][代码][代码][代码][代码][代码][代码] [代码][代码][代码]view[代码][代码]>[代码][代码][代码][代码][代码][代码][代码] [代码][代码][代码]block[代码][代码]>[代码][代码][代码][代码][代码][代码][代码][代码]view[代码][代码]>[代码][代码][代码][代码] 页面wxml为: [代码]<[代码][代码]block[代码] [代码]wx:if="{{goodDetail[attributeBoList].length > 0}}">[代码] [代码] [代码][代码]<[代码][代码]nstable[代码] [代码]goodDetail[代码][代码]=[代码][代码]"{{goodDetail}}"[代码] [代码]attributeBoList[代码][代码]=[代码][代码]"attributeBoList"[代码] [代码]attributeModel[代码][代码]=[代码][代码]"attributeModel"[代码] [代码]displayName[代码][代码]=[代码][代码]"displayName"[代码] [代码]attributeValueModelList[代码][代码]=[代码][代码]"attributeValueModelList"[代码] [代码]attributeVal[代码][代码]=[代码][代码]"attributeVal"[代码] [代码]tableTip[代码][代码]=[代码][代码]"{{tableTip}}"[代码][代码]>[代码]nstable[代码][代码]>[代码][代码][代码][代码][代码]block[代码][代码]>[代码][代码][代码][代码][代码][代码]<[代码][代码]block[代码] [代码]wx:else>[代码][代码][代码][代码][代码][代码] [代码][代码]<[代码][代码]text[代码][代码]>此套餐还没有详细信息哦~[代码]text[代码][代码]>[代码][代码][代码][代码][代码][代码][代码][代码]block[代码][代码]>[代码][代码][代码][代码] 结果我每次加载表格,都会报错: Expect END descriptor with depth 0 but get another 经过一次次排查,发现竟然是不能直接在block标签内引用自定义组件! 所以当我把block标签一起打入组件之后: [代码]<[代码][代码]block[代码] [代码]wx:if="{{goodDetail[attributeBoList].length > 0}}">[代码][代码] [代码][代码]<[代码][代码]view[代码] [代码]class[代码][代码]=[代码][代码]"table"[代码][代码]>[代码][代码] [代码][代码]<[代码][代码]view[代码] [代码]class[代码][代码]=[代码][代码]"tr"[代码][代码]>[代码][代码] [代码][代码]<[代码][代码]block[代码] [代码]wx:key[代码][代码]=[代码][代码]"testList-th"[代码] [代码]wx:for[代码][代码]=[代码][代码]"{{goodDetail[attributeBoList]}}"[代码][代码]>[代码][代码] [代码][代码]<[代码][代码]view[代码] [代码]class[代码][代码]=[代码][代码]"th"[代码][代码]>[代码][代码] [代码][代码]{{item[attributeModel][displayName]}}[代码][代码] [代码][代码][代码]view[代码][代码]>[代码][代码][代码][代码] [代码][代码][代码]block[代码][代码]>[代码][代码][代码][代码][代码][代码] [代码][代码][代码]view[代码][代码]>[代码][代码][代码][代码][代码][代码][代码] [代码][代码]<[代码][代码]block[代码] [代码]wx:key[代码][代码]=[代码][代码]"testList-tr"[代码] [代码]wx:for[代码][代码]=[代码][代码]"{{goodDetail[attributeBoList][0][attributeValueModelList]}}"[代码] [代码]wx:for-index[代码][代码]=[代码][代码]"index"[代码][代码]>[代码][代码][代码][代码][代码][代码] [代码][代码]<[代码][代码]view[代码] [代码]class[代码][代码]=[代码][代码]"tr"[代码][代码]>[代码][代码][代码][代码][代码][代码] [代码][代码]<[代码][代码]block[代码] [代码]wx:key[代码][代码]=[代码][代码]"testList-td"[代码] [代码]wx:for[代码][代码]=[代码][代码]"{{goodDetail[attributeBoList]}}"[代码] [代码]wx:for-index[代码][代码]=[代码][代码]"idx"[代码][代码]>[代码][代码][代码][代码][代码][代码] [代码][代码]<[代码][代码]view[代码] [代码]class[代码][代码]=[代码][代码]"td"[代码][代码]>[代码][代码][代码][代码][代码][代码] [代码][代码]{{item[attributeValueModelList][index][attributeVal]}}[代码][代码][代码][代码][代码][代码] [代码][代码][代码]view[代码][代码]>[代码][代码][代码][代码][代码][代码][代码] [代码][代码][代码]block[代码][代码]>[代码][代码][代码][代码][代码][代码][代码] [代码][代码][代码]view[代码][代码]>[代码][代码][代码][代码][代码][代码][代码] [代码][代码][代码]block[代码][代码]>[代码][代码][代码][代码][代码][代码][代码] [代码][代码][代码]view[代码][代码]>[代码][代码][代码][代码][代码][代码][代码][代码]block[代码][代码]>[代码][代码][代码][代码][代码][代码][代码]<[代码][代码]block[代码] [代码]wx:else>[代码][代码][代码][代码][代码][代码] [代码][代码]<[代码][代码]text[代码][代码]>{{tableTip}}[代码]text[代码][代码]>[代码][代码][代码][代码][代码][代码][代码][代码]block[代码][代码]>[代码][代码][代码][代码]ok~~~~一切正常!所以这个坑给大家提个醒,希望能帮助大家解决这种问题,好了老板要过来查岗了,溜了溜了...
2018-04-04 - NFCAdapter.onDiscovered无法获取NFC数据,被系统拦截,应如何设置?
我的小程序目标是通过NFC功能,读写NFC卡,在注册回调函数后,无法被回调,现象是贴卡后,系统会弹出选择使用NFC的应用的对话框,若选择微信,则会跳转到微信公交卡页面,提示“卡片类型不符”,图片如下:[图片] 我的“读NFC卡”方法代码如下: NFCReadCard: function(){ let _this = this let adapter = wx.getNFCAdapter() console.log(adapter) adapter.onDiscovered(function (res){ console.log('读到卡片了', res); //console.log(res.techs); if(res.techs.includes(adapter.tech.nfcA)){ console.log('发现'+adapter.tech.nfcA+'卡'); let nfcA = adapter.getNfcA(); app.nfcA = nfcA; nfcA.connect({ success: function(res){ console.log('设备已连接', res) nfcA.transceive({ data:new ArrayBuffer(0), success: function(res){ console.log('发送数据成功, 接收数据如下:', res); }, fail: function(err){ console.log('发送数据失败', err); } }) }, fail: function(err){ console.log('设备联接错误', err); } }); } }) }
2020-12-22 - 在苹果系统中下载不了图片,一直报错误提示:downloadFile:fail file?
我的苹果7P是可以正常运行的,但客户的iphone X一直提示错误 [图片]
2020-07-06 - canvas2d经多层组件嵌套后,用canvasToTempFilePath输出的图片可能不完整
在iOS上,将canvas2d经多重组件封装后,第一次调用wx.canvasToTempFilePath输出的图片不完整 -- 安卓无此问题。 在代码片段中,操作路径:“首页 - 点击进入海报绘制页面” -> “海报绘制页 - 点我绘制海报 - 生成分享图”,在进入“海报绘制页”后,第一次“生成分享图”有很大概率生成的图片不完整(可能是wx.canvasToTempFilePath导出的问题),只要还停留在“海报绘制页”,第二、第三、第N次“生成分享图”全都是正常的。 如果一次没有复现,可以重新打开小程序再试。 代码片段应该生成的正常海报: [图片] 生成的问题海报: [图片]
2020-11-11 - canvas2d设置画布初始大小安卓画图不全苹果全,不设置画布初始大小苹果画图不全安卓全,要闹哪样?
这个bug很久之前就反馈过了,也没有人管。之前只发现苹果手机有问题,后来进一步发现安卓也可以有问题,区别就是给不给画布设置初始大小。 代码片段之前反馈的帖子里有: https://developers.weixin.qq.com/community/develop/doc/0004c484cb04a0ceca3b201f15b800 帖子里的代码片段直接能复现苹果手机上的问题,像下面这样改一下给画布设置个初始大小(paint一开始是空的),则换成安卓出问题: [图片]
2020-11-19 - 为什么图片链接可正常访问但image组件加载不出来图片?
因为 image 控件的图片拉取本质上是 web 上的 backgroundImage,很多时候是由于图片不规范(content-type / length / 是否302跳转等 )导致拉取不成功,最终表现为加载不出图片。关于这一块我们在持续优化中
2021-12-17 - 前端加载优化及实践
大家都知道产品体验的重要性,而其中最重要的就是加载速度,一个产品如果打开都很慢,可能也就没有后面更多的事情了。这篇文章是我最近项目中的一些加载优化总结,欢迎大家一起讨论交流。 内容包括: 性能指标及数据采集 性能分析方法 性能优化方法 性能优化具体实践 第一部分:性能指标及数据采集 要优化性能首先需要有一套用来评估性能的指标,这套指标应该是是可度量、可线上精确采集分析的。现在来一起看看如何选择性能指标吧。 1. 性能指标 加载的过程是一个用户的感知变化的过程。所以我们的页面性能指标也是要以用户感知为中心的。下面是google定义了几个以用户感知为中心的性能指标。 1.1 以用户感知为中心的性能指标 首先确定页面视觉的变化传递给用户的感知变化关键点: 感知点 说明 发生了吗? 浏览是否成功。 有用了吗? 是否有足够的内容呈现给用户。 可用了吗? 用户是否可以和页面交互了。 好用吗? 用户和应用交互是否流畅自然。 我这里讲的是加载优化,所以第四点暂时不讨论。下面是感知点相关的性能指标。 First paint(FP) and first contentful paint(FCP) FP: Webview跳转到应用的首次渲染时间。 FCP:Webview首次渲染内容的时间:文本,图像(包括背景图像),非白色画布或SVG。这是用户第一次消费内容的时间。 Chrome支持用Paint Timing API获取这两个值: [代码] performance.getEntriesByType("paint") [代码] First meaningful paint(FMP) 首次绘制有效内容的时间,用来表明这个应用是否绘制了有效内容。比如天气应用可以看到天气了,商品列表可以看到商品了。 Time to Interactive(TTI) 应用可交互时间,这时应用渲染完成且可以响应用户输入的时间。这种情况下JS已经加载完成且主线程处于空闲状态。 Speed index 速度指标:代表填充页面内容的速度。要想降低速度指标分数,您需要让加载速度从视觉上显得更快,也就是渐进式展示。 上面指标对应的感知点如下: 感知点 说明 发生了吗? FP/FCP 有用了吗? FMP 可用了吗? TTI Speed index是个整体效果指标所以没有对应上面的任何一个,但也同时对应任何一个。 对于实际项目中我们选取指标要便于采集,下面是针对我的实际项目(APP内的单页面应用)选取的性能指标。 1.2 实际项目选取的性能指标 Webview加载时间 反应Webview性能。这样就可以更真实的知道我们应用的加载情况。 页面下载时间 反应浏览成功时间。 应用启动时间 反应应用启动完成时间,这个时候页面初始化完成,是JS首次执行完成的时间,应用所需异步请求都已经发出去了。 首次有效绘制内容时间 已经有足够的内容呈现给用户,是首屏所需重要接口返回且DOM渲染完成的时间,这个时间由程序员自行判断。 应用加载完成时间 应用完整的呈现给了用户,这个时候页面中所有资源都已经下载好,包括图片等资源。 这里我们的性能指标确定了,下面看看这些数据怎么采集吧。 2. 数据采集 performance.timing为我们提供页面加载每个过程的精确时间,如下图: [图片] 是不是很完美,这足够了?还不够,我们还需要加上原生APP为我们提供的点击我们应用的时间和我们自己确定的FMP才够完美。 下面是每个指标的获取方法: 公用代码部分 [代码]let performance = window.performance || window.msPerformance || window.webkitPerformance; if (performance && performance.timing) { let t = performance.timing; let navigationStart = t.navigationStart; //跳转开始时间 let enterTime = ""; //app提供的用户点击应用的时间,需要和app沟通传递方式 //... 性能指标部分 } [代码] Webview加载时间 [代码] let webviewLoaded = navigationStart - enterTime; [代码] 注意:enterTime应该是客户端ms时间戳,不是服务器时间。 页面下载时间 [代码] let pageDownLoadedTime = t.responseEnd - navigationStart; [代码] 应用启动时间 [代码]let appStartTime = t.domContentLoadedEventStart - navigationStart; [代码] 首次有效绘制内容时间 这里我们需要在有效绘制后调用 [代码]window._fmpTime = +(new Date())[代码]获取当前时间戳。 [代码]let fmpTime = window._fmpTime - navigationStart; [代码] 应用加载完成时间 [代码]let domCompleteTime = t.domComplete - navigationStart; [代码] 最后在document load以后使用上面代码就可以收集到性能数据了,然后就可以上报给后台了。 [代码]if (document.readyState == 'complete') { _report(); } else { window.addEventListener("load", _report, false); } [代码] 这样就封装了一个简单性能数据采集上报组件,这是非常通用的可以用在类似项目中使用只要按照标准提供enterTime和window._fmpTime就可以。 3. 数据分析 有了上面的原始数据,我们需要一些统计方法来观察性能效果和变化趋势,所以我们选取下面一些统计指标。 平均值 注意在平均值计算的时候要设置一个取值范围比如:0~10s以防脏数据污染。 平均值的趋势用折线图展示: [图片] 分布占比 可以清晰的看到用户访问时间的分布,这样你就可以知道有多少用户是秒开的了。 分布占比可以使用折线图、堆积图、饼状图展示: [图片] [图片] [图片] 第二部分:性能分析方法 上面有了性能指标和性能数据,现在我们来学习一下性能分析的一些方法,这样我们才能知道性能到底哪里不行、为什么不行。 1. 影响性能的外部因素 分析性能最重要的一点要确定外部因素。经常会有这种情况,有人反应页面打开速度很慢,而你打开速度很快,其实可能并不是页面性能不好,只是外部因素不同而已。 所以做好性能优化不能只考虑外部因素好的情况,也要让用户能在恶劣条件(如弱网络情况)下也有满足预期的表现。下面看看影响性能的外部因素主要有哪些。 1.1 网络 网络可以说是最影响页面性能最重要的外部因素了,网络的主要指标有: 带宽:表示通信线路传送数据的能力,即在单位时间内通过网络中某一点的最高数据率,单位有bps(b/s)、Kbps(kb/s)、Mbps(mb/s)等。常说的百兆带宽100M就是100Mbps,理论下载最大速度12.5MB/s。 时延:Delay,指数据从网络的一端传送到另一端所需的时间,反应的网络畅通程度。 往返时间RTT:Round-Trip Time,是指从发送端发送数据到接收端接受到确认的总时间。我们经常用的ping命令就是用这个指标表明我们和目标主机的网络顺畅程度。比如我们要对比几个翻墙代理哪里个好,我们就可以ping一下,看看这几个代理哪个RTT低来作出选择。 [图片] 这三个主要指标中后面两个类似,在Chrome中模拟网络主要用设置带宽和网络延迟(往返时间RTT出现最小延迟)来模拟网络。我们电脑一般用的是WI-FI(百兆),那么我们模拟网络,主要模拟常见3G(1兆)、4G(10兆)网络就好,这样我们就覆盖了三个级别的网络情况了。 可以在Chrome的NetWork面板直接选取Chrome模拟好的网络,这个项目network-emulation-conditions中有默认模拟网络的速度。 [图片] 如果默认不满足,你也可以自己配置网络参数,在设置面板的Throttling。 [图片] 上面设置的3G接近100KB/s,4G 0.5MB/s。你可以根据自己的需要来调整这个值,这两个值的差异应该能很好两种不同的网络情况了。设置模拟网络只要能覆盖不同的带宽情况就好,也不用那么真实因为真实情况很复杂。网络部分就介绍完了,接着看其他因素。 1.2 用户机器性能 经常会有这种情况,一个应用在别人手机上打开速度那么快、那么流畅,为啥到我这里就不行了呢?原因很简单人家手机好,自然有更好的配置、更多的资源让程序运行的更快。 Chrome现在非常强大你可以通过performance面板来模拟cpu性能。也可以让你看到应用在低性能机器上的表现。 [图片] 1.3 用户访问次:首次访问、2次访问、发版本访问 用户访问次数也是分析性能的重要外部因素,当用户第一次访问要请求所有资源,后面在访问因为有些资源缓存了访问速度也会不同。当我们开发者又发版本,会更新部分资源,这样访问速度又会跟着变。因为缓存的效果存在,所以这三种情况要分开分析。同时也要注意我们是否要支持用户离线访问。 通过在Chrome中的Network面板中选中Disable cache就可以强制不缓存了,来模拟首次访问。 [图片] 1.4 因素对选取 上面的外部因素虽然只有3种但相乘也有不少情况,为了简化我们性能分析,要选取代表性的因素去分析我们的性能。下面是指导因素对: 网络:WIFI 3G 4G 用户访问状态:首次 2次 这样有6种情况不算特别多,也能很好反应我们应用在不同情况下的性能。 2. devtools具体分析性能 通过devtools可以观察在不同外部因素下代码具体加载执行情况,这个工具是我们性能分析中最重要的工具,加载优化这里我们主要关注两个面板:Network、Performance。 先看Network面板的列表页: [图片] 这是网络请求的列表,右击表头可以增删属性列,根据自己需要作出调整。 下面我介绍网络列表中的几个重点属性: Protocol:网络协议,h2说明你的请求是http2协议的了。 Initiator:可以查到这个资源是哪里引用的。 Status:网络状态码。 Waterfall:资源加载瀑布流。 下面在看看Network面板中单个请求的详情页: [图片] 这里可以看到具体的请求情况,Timing面板是用来观察这次网络的请求时间占用的具体情况,对我们性能分析非常重要。具体每个时间段介绍可以点击Explanation。 虽然Network面板可以让我看到了网络请求的整体和单个请求的具体情况,但Network面板整体请求情况看着并不友好,而且也只有加载情况没有浏览器线程的执行情况。下面看看强大的Performance面板的吧。 [图片] 这里可以清晰看到浏览器如何加载资源如何解析html、解析css、执行js和渲染绘制的。 Performance简直太强大了,所以请你务必要掌握它的使用,这里篇幅有限,只能介绍了个大概,建议到google网站仔细学习一下。 3. Lighthouse整体分析性能 使用Lighthouse可以对应用做整体性能分析评分,并且会给我们专业的指导建议。我们可以安装Lighthouse插件或者安装Lighthouse npm包来使用它。 检测结果中可以看到很多性能指标的分值和建议。你也可以去测试下你的应用表现。 4. 线上用户统计分析性能 虽然使用devtools和Lighthouse可以知道页面的性能情况,但我们还要观察用户的真实访问情况,这才能真实反映我们应用的性能。线上数据采集分析,第一步部分已经介绍过了,这里就不在多说了。优化完看看自己对线上数据到底造成了什么影响。 上面介绍了性能分析的方法,可以很好帮你去分析性能,有了性能分析的基础,下面我们在来看看怎么做性能优化吧。 第三部分:性能优化方法 1. 微观:优化单次网络请求时间 在性能分析知道Network面板可以看到单次网络请求的详情 [图片] 从图可以看出请求包括:DNS时间、TCP时间、SSL时间(https)、TTFB时间(服务器处理时间)、ContentLoaded内容下载时间,所以有下面公式: [代码]requestTime = DNS + TCP + SSL+ TTFB +ContentLoaded [代码] 所以只要我们降低这里面任意一个值就可以降低单次网络请求的时间了。 2. 宏观:优化整体加载过程 加载过程的优化就是不断让第一部分的性能指标感知点提前的过程。通过关键路径优化、渐进式展示、内容效率优化手段,来优化资源调度。 2.1 加载过程 在介绍页面加载过程,先看看渲染绘制过程: [图片] Javascript:操作DOM和CSSOM。 样式计算:根据选择器应用规则并计算每个元素的最终样式。 布局:浏览器计算它要占据的空间大小及其在屏幕的位置。 绘制:绘制是填充像素的过程。 合成。由于页面的各部分可能被绘制到多层,合成是将他们按正确顺序绘制到屏幕上,正确渲染页面。 渲染其实是很复杂的过程这里只简单了解一下,想深入了解可以看看这篇文章。 了解了渲染绘制过程,在学习加载过程的时候就可以把它当作黑盒了,黑盒只包括渲染过程从样式计算开始,因为上面的Javascript主要是用来输入DOM、CSSOM。 浏览器加载过程: Webview加载 下载HTML 解析HTML:根据资源优先级加载资源并构建DOM树 遇到加载同步JS资源暂停DOM构建,等待CSSOM树构建 CSS返回构建CSSOM树 用已经构建的DOM、CSSOM树进行渲染绘制 JS返回执行继续构建DOM树,进行渲染绘制 当HTML中的JS执行完成,DOM树第一次完整构建完成触发:domContentLoaded 当所有异步接口返回后渲染制完成,并且外部加载完成触发:onload 注意点: CSSOM未构建好页面不会进行任何渲染 脚本在文档的何处插入,就在何处执行 脚本会阻塞DOM构建 脚本执行要等待CSSOM构建完成后执行 下面看看如何在加载过程提前感知点。 2.2 优化关键路径 把关键路径定义为:从页面请求到应用启动完成这个过程,也就是到JS执行完domContentLoaded触发的过程。 主要指标有: 关键资源: 影响应用启动完成的资源。 关键资源的数量:这个过程中加载的资源数据。 关键路径长度:关键资源请求的串行长度。 关键字节的数量:关键资源大小总和。 [图片] 上图关键资源有:html、css、3个js。关键资源数量:5个。关键字节的数量:5个资源的总大小。关键路径长度:2,html+剩余其他资源。 关键优化路径优化,就是要降低关键路径长度、关键字节的数量,在http1时代还要降低关键资源的数量,现在http2资源数不用关心。 2.3 优化内容效率 主要是关注的应用加载完成这个时间点,由首页加载完成所需的资源量决定。我们要尽量减少加载资源的大小,避免不必要加载的资源,比如做一些图片压缩懒加载尽快让应用加载完成。 主要指标有: 应用加载完成字节数:应用加载完成,所需的资源大小。 这个指标可以从Chrome上观察到,不过要剔除prefetch的资源。这个指标一般不太稳定,因为页面展示的内容不太相同,所以最好在相同内容相同情况下对比。 2.4 渐进式展示 从上面的加载过程中,可以知道渲染是多次的。那样我们可以先让用户看到一个Loading提示、先展示首屏内容。Loading主要优化的是FP/FCP这两个指标,先展示首屏主要是优化FMP。 3. 缓存:优化多次访问 缓存重点强调的是二次访问、发版访问、离线访问情况下的优化。 通过缓存有效减少二次访问、发版访问所要加载资源,甚至可以让应用支持离线访问,而且是对弱网络环境是最有效的手段,一定要善于使用缓存这是你性能优化的利器。 4. 优化手段 优化手段我归纳为5类:small(更小)、pre(更早)、delay(更晚)、concurrent(并发)、cache(缓存)。性能优化就是将这5种手段应用于上面的优化点:网络请求优化、关键路径优化、内容效率优化、多次访问优化。 5. 构建自己可动态改变的优化方法表和检查表 Checklist包括两部分,一个优化方法表,另外一个优化方法检查表。优化方法表是让我们对我们的性能优化方法有个评估和认识,优化方法检查表的好处是,可以清晰的知道你的项目用了哪些优化方法,还有哪些可以尝试做进一步优化,同时作为一个新项目的指导。 优化名:优化方法的名字。 优化介绍:对优化方法做简单的介绍。 优化点:网络请求优化、关键路径优化、内容效率优化、多次访问优化。 优化手段:small、pre、delay、concurrent、cache。 本地效果:选取合适的因素对,进行效果分析,确定预期作用大小。 线上效果:线上效果对比,确定这个优化方案的有效性及实际作用大小。 这样我们就能大概了解了这个效果的好处。我们新引入了一种优化方法都要按这张表的方法进行操作。 优化方法表: 名称 内容 优化名 JS压缩 优化介绍 压缩JS 优化点 关键路径优化 优化手段 small 本地效果 具体本地效果对比 线上效果 线上数据效果 上面是以JS压缩为例的优化方法表。 优化方法检查表: 分类 优化点 是否使用 不适用 问题说明 small JS压缩 √ pre preload/prefetch √ 不需要 通过这张表就能看出我们使用了哪些方法,还有哪些没使用,哪些方法不适用我们。可以很方便的应用于任何一个新项目。 第四部分:性能优化具体实践 现在就看看我在项目中的具体实践吧,项目中使用的技术栈是:Webpack3+Babel7+Vue2,下面我按照优化手段介绍: 1. small(更小) scope-hoisting scope-hoisting(作用域提升):Webpack分析出模块之间的依赖关系,把可以合并到一起模块合并到一起,但不造成冗余,因此只有被一个地方引用的代码可以合并到一起。这样做函数声明会变少,可以让代码更小、执行更快。 这个功能从Webpack3开始引入,依赖于ES2015模块的静态分析,所以要把Babel的preset要设置成[代码]"modules": false[代码]: [代码] ... [ "@babel/preset-env", { "modules": false ... [代码] Webpack3要引入ModuleConcatenationPlugin插件,Webpack4 product模式已经预置该插件: [代码]... new webpack.optimize.ModuleConcatenationPlugin(), ... [代码] [图片] 如上图,不压缩的JS中可以文件中看到CONCATENATED MODULE这就说明生效了。 tree-shaking 摇树:通常用于描述移除JavaScript上下文中的未引用代码,在webpack2中开始内置。依赖于ES2105模块的静态分析,所以我们使用babel同样要设置成 [代码]"modules": false[代码]。 [图片] 如上图,不压缩的JS中可以文件中看到unused harmony这就说明摇树成功了。 code-splitting(按需加载) 代码分片,将代码分离到不同的js中,进行并行加载和按需加载。 代码分片主要有两种: 按需加载:动态导入 vendor提取:业务代码和公共库分离 这里只介绍按需加载部分,动态导入Webpack提供了两个类似的技术。1. Webpack特定的动态导入require.ensure。2.ECMAScript提案[代码]import()[代码]。这里我只介绍我使用的[代码]import()[代码]这种方法。因为是推荐方法。 代码如下: Babel配置支持动态导入语法: [代码]... "@babel/plugin-syntax-dynamic-import", ... [代码] 代码中使用: [代码]... if(isDevtools()){ import(/* webpackChunkName: "devtools" */'./comm/devtools').then((devtools)=>{ let initDevtools = devtools.default; initDevtools(); }); } ... [代码] polyfill按需加载 我们代码是ES2015以上版本的要真正能在浏览器上能使用要通过babel进行编译转化,还要使用polyfill来支持新的对象方法,如:Promise、Array.from等。对于不同环境来说需要polyfill的对象方法是不一样的,所以到了Babel7支持了按需加载polyfill。 下面是我项目中的配置,看完以后我会介绍一下几个关键点: [代码]module.exports = function (api) { api.cache(true); const sourceType = "unambiguous"; const presets = [ [ "@babel/preset-env", { "modules": false, "useBuiltIns": "usage", // "debug": true, "targets": { "browsers": ["Android >= 4.0", "ios >= 8"] } } ] ]; const plugins= [ "@babel/plugin-syntax-dynamic-import", "@babel/plugin-transform-strict-mode", "@babel/plugin-proposal-object-rest-spread", [ "@babel/plugin-transform-runtime", { "corejs": false, "helpers": true, "regenerator": false, "useESModules": false } ] ]; return { sourceType, presets, plugins } } [代码] @babel/preset-env preset是预置的语法转化插件的集合。原来有很多preset如:@babel/preset-es2015。直到出现了@babel/preset-env,它可以根据目标环境来动态的选择语法转化插件和polyfill,统一了preset众多的局面。 [代码]targets[代码]:是我们用来设置环境的,我的应用支持移动端所以设置了上面那样,这样就可以只加载这个环境需要的插件了。如果不设置[代码]targets[代码]通过@babel/preset-env引入的插件是 @babel/preset-es2015、@babel/preset-es2016和@babel/preset-es2017插件的集合。 [代码]"useBuiltIns": "usage"[代码]:将useBuiltIns设置为usage就会根据执行环境和代码按需加载polyfill。 @babel/plugin-transform-runtime 和polyfill不同,@babel/plugin-transform-runtime可以在不污染全局变量的情况下,使用新的对象和方法,并且可以移除内联的Babel语法转化时候的辅助函数。 我们这里只用它来移除辅助函数,不需要它来帮我处理其他对象方法,因为我们在开发应用不是做组件不怕全局污染。 sourceType:“unambiguous” 一个文件混用了ES2015模块导入导出和CJS模块导入导出。需要设置[代码]sourceType:"unambiguous"[代码],需要让babel自己猜测类型。如果你的代码都很合规不用加这个的。 压缩:js、css js、css压缩应该最基本的了。我在项目中使用的是[代码]UglifyJsPlugin[代码]和[代码]optimize-css-assets-webpack-plugin[代码],这里不做过多介绍。 压缩图片 通过对图片压缩来进行内容效率优化,可以极大的提前应用加载完成时间,我在项目中做了下面两件事。 广告图片,限制大小50K以内。原来基本会上传超过100K的广告图。 项目中图片使用的[代码]img-loader[代码]对图片进行压缩。 HTTP2支持,去掉css中base64图片 先看看HTTP1.1中的问题: 同一域名浏览器做了TCP连接数的限制,如:Chrome中只能有6个。 一个TCP连接只能同时处理一个请求响应。 在看看HTTP2的优势: 二进制分帧:HTTP2的性能增强的核心在于新的二进制分帧层。帧是最小传输单位,帧组成消息,数据以消息形式发送。 多路复用:所有请求在一个连接上完成,可以支持多数据流混合传输,在接收端拼接。 头部压缩:使用HPACK对头部压缩,网络中可以传递更少的数据。 服务端推送:服务端可以主动向客户端推送资源。 有了HTTP2我们在也不用担心资源数量,不用在考虑减少请求了。像:base64图片打到css、合并js、域名分片、精灵图都不要去做了。 这里我把原来base64压缩图片从css中去除了。 2. pre(更早) preload prefetch preload:将资源加载和执行分离,你可以根据你的需要指定要强制加载的资源,比如后面css要用到一个字体文件就可以在preload中指定加载,这样提高了页面展示效果。建议把首页展示必须的资源指定到preload中。 prefetch:用来告诉浏览器我将来会用到什么资源,这样浏览器会在空闲的时候加载。比如我在列表页将详情页js设置成prefetch,这样在进入详情页的时候速度就会快很多,因为我提前加载好了。 这里我用的是来使用[代码]preload-webpack-plugin[代码]preload和prefetch的。 代码: [代码]... const PreloadWebpackPlugin = require('preload-webpack-plugin'); ... new PreloadWebpackPlugin({ rel: 'prefetch', include: ['devtools','detail','VideoPlayer'] }), ... [代码] dns-prefetch preconnect dns-prefetch:在页面中请求该域名下资源前提前进行dns解析。preconnect:比dns-prefetch更近一步连TCP和SSL都为我们处理好了。 使用注意点:1. 考虑到兼容性问题,我们对一个域名两个都设置 2. 对于应用中不一定会使用的域名我们设置dns-prefetch就好以防占用资源。 代码如下: [代码]... <link rel="preconnect" href="//game.gtimg.cn"> ... <link rel="dns-prefetch" href="//game.gtimg.cn"> ... [代码] 3. delay(更晚) lazyload 对图片进行懒加载,我使用的是[代码]vue-lazyload[代码]。 代码如下: [代码]... import VueLazyload from 'vue-lazyload' ... Vue.use(VueLazyload, { preLoad: 1.3, error: '...', loading: '...', attempt: 1 }); ... <div class='v-fullpage' v-lazy:background-image="item.roomPic" :key="item.roomPic"></div> ... [代码] 这里的:key特别注意,如果你的列表数据是动态变化的一定要设置,否则图片是最开始一次的。 code-splitting(按需加载) code-splitting(按需加载)前面已经介绍过这里只是强调下它的delay作用,不使用的部分先不加载。 4. concurrent(并发) HTTP2 HTTP2前面已经应用在了css体积减少,这里主要强调它的多路复用。需要大家看看自己的项目是否升级到HTTP2,是否所有资源都是HTTP2的,如果不是的,需要推进升级。 code-splitting(vendor提取) vendor提取是把业务代码和公共库分离并发加载,这样有两个好处: 下次发版本这部分不用在加载(缓存的作用)。 JS并发加载:让先到并在前面的部分先编译执行,让加载和执行并发。 Webpack配置: [代码] ... entry:{ "bundle":["./src/index.js"], "vendor":["vue","vue-router","vuex","url","fastclick","axios","qs","vue-lazyload"] }, ... new webpack.optimize.CommonsChunkPlugin({ name: "vendor", minChunks: Infinity }), new webpack.optimize.CommonsChunkPlugin({ name: 'manifest' }), ... [代码] 5. cache(缓存) HTTP缓存 HTTP缓存对我们来说是非常有用的。 下面介绍下HTTP缓存的重点: Last-Modified/ETag:用来让服务器判断文件是否过期。 Cache-Control:用来控制缓存行为。 max-age: 当请求头设置max-age=deta-time,如果上次请求和这次请求时间小于deta-time服务端直接返回304。当响应头设置max-age=deta-time,客户端在小于deta-time使用客户端缓存。 强制缓存:这主要把不经常变化的文件设置强制缓存,这样就不需要在发起HTTP请求了。通过设置响应头Cache-Control的max-age设置。 如果像缓存很久设置一个很大的值,如果不想缓存设置成:Cache-Control:no-cahce。 协商缓存:如果没有走强制缓存就要走协商缓存,服务器根据Last-Modified/ETag来判断文件是否变动,如果没变动就直接返回304。 这里我们做的就是让运维调整资源的强制缓存时间,前端在结合文件hash命名就可以进行资源更新了。 ServiceWorker ServiceWorker是Web应用和浏览器之间的代理服务器,可以用来拦截网络来进行资源缓存、离线体验,还可以进行推送通知和后台同步。功能非常强大,我们这里使用的是资源缓存功能,看看和HTTP缓存比有什么优势: 功能多:支持离线访问、资源缓存、推送通知、后台同步。 控制力更强:缓存操作+络拦截功能都由开发者控制,可以做出很多你想做的事情比如动态缓存。 仅HTTPS下可用,更安全。 看看我在项目中的使用: js使用HTTP缓存和ServiceWorker双重缓存在cacheid变化后依然可以缓存。 不得对service-worker.js缓存,因为我们要用这个更新应用。在Chrome中看到请求的cache-control被默认设置了no-cache。 我们项目中使是Google的Workbox,Webpack中插件是 workbox-webpack-plugin。 [代码]... const WorkboxPlugin = require('workbox-webpack-plugin'); ... new WorkboxPlugin.GenerateSW({ cacheId: 'sw-wzzs-v1', // 缓存id skipWaiting: true, clientsClaim: true, swDest: './html/service-worker.js', include: [/\.js(.*)$/,/\.css$/], importsDirectory:'./swmainfest', importWorkboxFrom: 'local', ignoreUrlParametersMatching: [/./] }), ... [代码] localStorage localStorage项目中主要做接口数据缓存。通常localStorage是没有缓存时间的我们将其封装成了有时间的缓存,并且在应用启动的时候对过期的缓存清理。 code-splitting(vendor提取) 这里在提vendor提取主要是说明它发版本时候的缓存价值,前面介绍过了。 6. 整体优化效果评价 经过上面的优化,看看效果提升吧。 主要增长点来源: 关键路径资源:698.6K降低到538.6K降低22.9% 内容效率提升:广告图由原来的基本100K以上降低到现在50K以下,页面内图片全部走强制缓存。 缓存加快多次访问速度:js+css强制缓存加ServiceWorker。 线上数据效果: 页面下载时间: 平均值下降:25.74%左右 应用启动完成时间: 平均值下降:33.45%左右 秒开占比提高:23.42%左右 应用加载完成时间: 平均值下降:48.02%左右 第六部分:总结 以上就是我在加载优化方面的一些总结,希望对您有所帮助,个人理解有限,欢迎一起讨论交流。
2019-03-11