- 小程序三级联动,实现三级分类,顶部导航栏,左侧分类栏,右侧数据列表
如果大家一直读石头哥的文章,或者看石头哥的视频,肯定知道,石头哥的点餐小程序有实现二级菜品或者商品分类。 如下图 [图片] 但是有时候我们想实现三级分类,该怎么做呢,今天就来教大家如何实现三级分类。随便教下大家如何把excel数据批量的导入到云开发数据库 一,老规矩,先看效果图 [图片] 先来给大家分析下原理 二,原理分析 首先来分析下有那三级 [图片] 可以看出,我们最顶部是一级菜单,左侧是二级菜单,右侧是最终的三级列表。 我们来理一理层级关系 =宿舍楼号 ====宿舍号 ========学生 聪明的人肯定知道,我们是一个宿舍楼里包含很多宿舍,宿舍里有包含很多学生。这样我们的三级就是 楼号》宿舍》学生。 当我们切换楼号时,就会重新获取当前楼号包含的宿舍。 比如下图左为惠兰楼,右为学苑楼的数据,可以看出每个楼里的宿舍和学生信息。 [图片] 分析完原理,我们就来看技术实现了。 三,获取分类数据 这里先给大家说下,我这里是用一张表来存的所有信息 [图片] 既然是一张表存所有数据,我们就要做下分组,看数据里都有哪些楼号。 3-1,借助group实现楼号分组(一级数据) [图片] 具体代码如下 [图片] 然后获取到的数据如下 [图片] 可以看出我们一共有11个宿舍楼。就是我们顶部的这些区域 [图片] 3-2,借助group和match实现宿舍分组(二级数据) [图片] 这个时候,我们就要根据用户选择的楼号,来对当前楼号下所有数据进行分组了 [图片] 分组后的数据如下 [图片] 可以看出,前进楼有两个宿舍 3-3,借助where获取宿舍里的学生数据(三级) [图片] 获取的数据如下 [图片] 到这里我们的三级分类就实现了 四,完整项目代码 下面把完整项目代码,贴出来给大家 4-1,wxml [代码]<!-- 导航栏 --> <scroll-view scroll-x class="navbar" scroll-with-animation scroll-left="{{scrollLeft}}rpx"> <view class="nav-item" wx:for="{{tabs}}" wx:key="id" bindtap="tabSelect" data-id="{{index}}"> <view class="nav-text {{index==tabCur?'tab-on':''}}">{{item._id}}</view> </view> </scroll-view> <view class="container"> <!-- 左边的 --> <scroll-view class='nav_left' scroll-y='true'> <block wx:for="{{lefts}}" wx:key="{{index}}"> <view class="nav_left_items {{leftCur==index?'active':''}}" bindtap="switchLeftTab" data-index='{{index}}'> {{item._id}}</view> </block> </scroll-view> <!-- 右边的 --> <scroll-view class="nav_right" scroll-y="true"> <view class="{{topx}}"> <block wx:for="{{rights}}" wx:key="index"> <view class="nav_right_items" data-id="{{item._id}}"> <image src="{{item.touxiang}}"></image> <text>{{item.mingzi}}</text> </view> </block> </view> </scroll-view> </view> [代码] 4-2,wxss样式 [代码]/* 导航栏布局相关 */ .navbar { width: 100%; height: 90rpx; /* 文本不换行 */ white-space: nowrap; display: flex; box-sizing: border-box; border-bottom: 1rpx solid #eee; background: #fff; align-items: center; /* 固定在顶部 */ position: fixed; left: 0rpx; top: 0rpx; } .nav-item { padding-left: 25rpx; padding-right: 25rpx; height: 100%; display: inline-block; /* 普通文字大小 */ font-size: 28rpx; } .nav-text { width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; letter-spacing: 4rpx; box-sizing: border-box; } .tab-on { color: #000080; /* 选中放大 */ font-size: 38rpx !important; font-weight: 600; border-bottom: 4rpx solid #000080 !important; } /* 正文部分 */ .container { position: fixed; width: 100%; height: 90%; top: 100rpx; background-color: #FFF; } .nav_left { width: 25%; height: 100%; background: #F2F2F2; text-align: center; position: absolute; top: 0; left: 0; } .nav_left .nav_left_items { height: 100rpx; line-height: 100rpx; font-size: 28rpx; } .nav_left .nav_left_items.active { position: relative; background: #FFF; color: #000080; } .nav_left .nav_left_items.active::before { display: inline-block; width: 6rpx; height: 60rpx; background: #000080; content: ''; position: absolute; top: 20rpx; left: 0; } .nav_right { position: absolute; top: 0; right: 0; width: 75%; height: 100%; } .nav_right .nav_right_items { float: left; width: 33.33%; text-align: center; padding: 30rpx 0; } .nav_right .nav_right_items image { width: 120rpx; height: 160rpx; } .nav_right .nav_right_items text { display: block; margin-top: 20rpx; font-size: 28rpx; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; } .nocate { padding: 100rpx; text-align: center; } .nocate image { width: 70rpx; height: 70rpx; } .nocate text { font-size: 28rpx; display: block; color: #BBB; } /*隐藏滚动条*/ ::-webkit-scrollbar { width: 0; height: 0; color: transparent; } .topx { width: 90%; /* height: 700rpx; */ margin: 30rpx auto; z-index: 1; border-radius: 10rpx; background-size: cover; } [代码] 4-3,js实现分类逻辑 [代码]const db = wx.cloud.database() const $ = db.command.aggregate Page({ data: { tabs: [], tabCur: 0, //默认选中 lefts: [], leftCur: 0, rights: [], }, onLoad: function (options) { db.collection('demo').aggregate() .group({ _id: '$louhao' }) .end() .then(res => { console.log('楼号列表', res) this.setData({ tabs: res.list }) this.sushehao(res.list[0]._id) }) }, //加载当前楼号所有的宿舍号 sushehao() { let louhao = this.data.tabs[this.data.tabCur]._id db.collection('demo').aggregate() .match({ louhao }) .group({ _id: '$sushe' }) .sort({ sushe: -1 //宿舍号升序排列 }) .end() .then(res => { console.log(louhao + '宿舍号列表', res) this.setData({ lefts: res.list }) this.xuesheng() }) }, //加载当前宿舍号里所有的学生 xuesheng() { let louhao = this.data.tabs[this.data.tabCur]._id let sushe = this.data.lefts[this.data.leftCur]._id db.collection('demo') .where({ louhao, sushe }).get() .then(res => { console.log(louhao + sushe + '室学生列表', res) this.setData({ rights: res.data }) }) }, //顶部选择分类条目 tabSelect(e) { this.setData({ tabCur: e.currentTarget.dataset.id, scrollLeft: (e.currentTarget.dataset.id - 2) * 200 }, success => { this.sushehao() }) }, //左侧条目选择 switchLeftTab(e) { let index = e.target.dataset.index; this.setData({ leftCur: index, }, success => { this.xuesheng() }) }, }) [代码] 4-4,记得修改数据表权限 修改权限为所有用户可读,仅创建者可读写 [图片] 到这里我们的三级分类就完整的实现了。关于excel数据批量导入,我下节再做讲解的。欢迎关注,欢迎留言交流。
2021-03-28 - 微信小程序自定义导航栏组件(完美适配所有手机),可自定义实现任何你想要的功能
背景 在做小程序时,关于默认导航栏,我们遇到了以下的问题: Android、IOS手机对于页面title的展示不一致,安卓title的显示不居中 页面的title只支持纯文本级别的样式控制,不能够做更丰富的title效果 左上角的事件无法监听、定制 路由导航单一,只能够返回上一页,深层级页面的返回不够友好 探索 小程序自定义导航栏已开放许久>>了解一下,相信不少小伙伴已使用过这个功能,同时不少小伙伴也会发现一些坑: 机型多如牛毛:自定义导航栏高度在不同机型始终无法达到视觉上的统一 调皮的胶囊按钮:导航栏元素(文字,图标等)怎么也对不齐那该死的胶囊按钮 各种尺寸的全面屏,奇怪的刘海屏,简直要抓狂 一探究竟 为了搞明白原理,我先去翻了官方文档,>>飞机,点过去是不是很惊喜,很意外,通篇大文尽然只有最下方的一张图片与这个问题有关,并且啥也看不清,汗汗汗… 我特意找了一张图片来 [图片] 分析上图,我得到如下信息: Android跟iOS有差异,表现在顶部到胶囊按钮之间的距离差了6pt 胶囊按钮高度为32pt, iOS和Android一致 动手分析 我们写一个状态栏,通过wx.getSystemInfoSync().statusBarHeight设置高度 Android: [图片] iOS:[图片] 可以看出,iOS胶囊按钮与状态栏之间距离为:4px, Android为8px,是不是所有手机都是这种情况呢? 答案是:苹果手机确实都是4px,安卓大部分都是7和8 也会有其他的情况(可以自己打印getSystemInfo验证)如何快速便捷算出这个高度,请接着往下看 如何计算 导航栏分为状态栏和标题栏,只要能算出每台手机的导航栏高度问题就迎刃而解 导航栏高度 = 胶囊按钮高度 + 状态栏到胶囊按钮间距 * 2 + 状态栏高度 注:由于胶囊按钮是原生组件,为表现一致,其单位在各种手机中都为px,所以我们自定义导航栏的单位都必需是px(切记不能用rpx),才能完美适配。 解决问题 现在我们明白了原理,可以利用胶囊按钮的位置信息和statusBarHeight高度动态计算导航栏的高度,贴一个实现此功能最重要的方法 [代码]let systemInfo = wx.getSystemInfoSync(); let rect = wx.getMenuButtonBoundingClientRect ? wx.getMenuButtonBoundingClientRect() : null; //胶囊按钮位置信息 wx.getMenuButtonBoundingClientRect(); let navBarHeight = (function() { //导航栏高度 let gap = rect.top - systemInfo.statusBarHeight; //动态计算每台手机状态栏到胶囊按钮间距 return 2 * gap + rect.height; })(); [代码] gap信息就是不同的手机其状态栏到胶囊按钮间距,具体更多代码实现和使用demo请移步下方代码仓库,代码中还会有输入框文字跳动解决办法,安卓手机输入框文字飞出解决办法,左侧按钮边框太粗解决办法等等 胶囊信息报错和获取不到 问题就在于 getMenuButtonBoundingClientRect 这个方法,在某些机子和环境下会报错或者获取不到,对于此种情况完美可以模拟一个胶囊位置出来 [代码]try { rect = Taro.getMenuButtonBoundingClientRect ? Taro.getMenuButtonBoundingClientRect() : null; if (rect === null) { throw 'getMenuButtonBoundingClientRect error'; } //取值为0的情况 if (!rect.width) { throw 'getMenuButtonBoundingClientRect error'; } } catch (error) { let gap = ''; //胶囊按钮上下间距 使导航内容居中 let width = 96; //胶囊的宽度,android大部分96,ios为88 if (systemInfo.platform === 'android') { gap = 8; width = 96; } else if (systemInfo.platform === 'devtools') { if (ios) { gap = 5.5; //开发工具中ios手机 } else { gap = 7.5; //开发工具中android和其他手机 } } else { gap = 4; width = 88; } if (!systemInfo.statusBarHeight) { //开启wifi的情况下修复statusBarHeight值获取不到 systemInfo.statusBarHeight = systemInfo.screenHeight - systemInfo.windowHeight - 20; } rect = { //获取不到胶囊信息就自定义重置一个 bottom: systemInfo.statusBarHeight + gap + 32, height: 32, left: systemInfo.windowWidth - width - 10, right: systemInfo.windowWidth - 10, top: systemInfo.statusBarHeight + gap, width: width }; console.log('error', error); console.log('rect', rect); } [代码] 以上代码主要是借鉴了拼多多的默认值写法,android 机子中 gap 值大部分为 8,ios 都为 4,开发工具中 ios 为 5.5,android 为 7.5,这样处理之后自己模拟一个胶囊按钮的位置,这样在获取不到胶囊信息的情况下,可保证绝大多数机子完美显示导航头 吐槽 这么重要的问题,官方尽然没有提供解决方案…竟然提供了一张看不清的图片??? 网上有很多ios设置44,android设置48,还有根据不同的手机型号设置不同高度,通过长时间的开发和尝试,本人发现以上方案并不完美,并且bug很多 代码库 Taro组件gitHub地址详细用法请参考README 原生组件npm构建版本gitHub地址详细用法请参考README 原生组件简易版gitHub地址详细用法请参考README 由于本人精力有限,目前只计划发布维护好这2种组件,其他组件请自行修改代码,有问题请联系 备注 上方2种组件在最下方30多款手机测试情况表现良好 iPhone手机打电话和开热点导致导航栏样式错乱,问题已经解决啦,请去demo里测试,这里特别感谢moments网友提出的问题 本文章并无任何商业性质,如有侵权请联系本人修改或删除 文章少量部分内容是本人查询搜集而来 如有问题可以下方留言讨论,微信zhijunxh 比较 斗鱼: [图片] 虎牙: [图片] 微博: [图片] 酷狗: [图片] 知乎: [图片] [图片] 知乎是这里边做的最好的,但是我个人认为有几个可以优化的小问题 打电话或者开启热点导致样式错落,这也是大部门小程序的问题 导航栏下边距太小,看起来不舒服 搜索框距离2侧按钮组距离不对等 自定义返回和home按钮中的竖线颜色重了,并且感觉太粗 如果您看到了此篇文章,请赶快修改自己的代码,并运用在实践中吧 扫码体验我的小程序: [图片] 创作不易,如果对你有帮助,请移步Taro组件gitHub原生组件gitHub给个星星 star✨✨ 谢谢 测试信息 手机型号 胶囊位置信息 statusBarHeight 测试情况 iPhoneX 80 32 281 369 48 88 44 通过 iPhone8 plus 56 32 320 408 24 88 20 通过 iphone7 56 32 281 368 24 87 20 通过 iPhone6 plus 56 32 320 408 24 88 20 通过 iPhone6 56 32 281 368 24 87 20 通过 HUAWEI SLA-AL00 64 32 254 350 32 96 24 通过 HUAWEI VTR-AL00 64 32 254 350 32 96 24 通过 HUAWEI EVA-AL00 64 32 254 350 32 96 24 通过 HUAWEI EML-AL00 68 32 254 350 36 96 29 通过 HUAWEI VOG-AL00 65 32 254 350 33 96 25 通过 HUAWEI ATU-TL10 64 32 254 350 32 96 24 通过 HUAWEI SMARTISAN OS105 64 32 326 422 32 96 24 通过 XIAOMI MI6 59 28 265 352 31 87 23 通过 XIAOMI MI4LTE 60 32 254 350 28 96 20 通过 XIAOMI MIX3 74 32 287 383 42 96 35 通过 REDMI NOTE3 64 32 254 350 32 96 24 通过 REDMI NOTE4 64 32 254 350 32 96 24 通过 REDMI NOTE3 55 28 255 351 27 96 20 通过 REDMI 5plus 67 32 287 383 35 96 28 通过 MEIZU M571C 65 32 254 350 33 96 25 通过 MEIZU M6 NOTE 62 32 254 350 30 96 22 通过 MEIZU MX4 PRO 62 32 278 374 30 96 22 通过 OPPO A33 65 32 254 350 33 96 26 通过 OPPO R11 58 32 254 350 26 96 18 通过 VIVO Y55 64 32 254 350 32 96 24 通过 HONOR BLN-AL20 64 32 254 350 32 96 24 通过 HONOR NEM-AL10 59 28 265 352 31 87 24 通过 HONOR BND-AL10 64 32 254 350 32 96 24 通过 HONOR duk-al20 64 32 254 350 32 96 24 通过 SAMSUNG SM-G9550 64 32 305 401 32 96 24 通过 360 1801-A01 64 32 254 350 32 96 24 通过
2019-11-17 - web性能优化之渲染性能优化
引子: 笔者在业务开发过程中,需要一个vue版的无限滚动组件,从github上找了一些组件后发现效果都不太好(主要是卡顿),最后自己查阅一些渲染性能优化的文章后,基于iScroll二次开发了一个组件,自己觉得效果还不错,主要是利用了硬件渲染加速和dom元素的复用,有同样需求的朋友可以试一下。(https://github.com/zuolei828/vue-virtual-infinite-scroll) 针对这次组件的优化,记录一下渲染性能优化的比较系统的知识,个人能力所限,很多方面理解的可能不对,欢迎大家指正! 一个web页面的性能优化,包括加载(loading)性能优化以及渲染(rendering)性能优化,关于加载性能的优化在另一篇文章中讨论(加载优化),这里来整理一下渲染性能优化的相关知识。 浏览器多进程模型 为了方便后面优化知识的阐述,这里先简单介绍下浏览器的多进程模型(以chrome为例)。 [图片] 主要进程如图所示: Browser进程:浏览器的主进程,负责浏览器界面的显示,各个页面的管理,其他各种进程的管理; Renderer进程:页面的渲染进程,负责页面的渲染工作,Blink的工作主要在这个进程中完成(主要分成render主线程和合成器线程); NPAPI插件进程:每种类型的插件只会有一个进程,每个插件进程可以被多个Render进程共享; GPU进程:最多只有一个,当且仅当GPU硬件加速打开的时候才会被创建,主要用于对3D加速调用的实现; Pepper插件进程:同NPAPI插件进程,不同的是为Pepper插件而创建的进程 页面渲染过程 页面渲染中每一帧的渲染最多进行了如下五个步骤。 [图片] JavaScript:通常我们会使用 JavaScript 来实现页面视觉变化的效果。比如做一个动画或者往页面里添加一些 DOM 元素等。 Style:计算样式,这个过程是根据 CSS 选择器,对每个 DOM 元素匹配对应的 CSS 样式。 Layout:在知道对一个元素应用哪些样式之后,浏览器即可开始计算它要占据的空间大小及其在屏幕的位置。网页的布局模式意味着一个元素可能影响其他元素,例如 body 元素的宽度一般会影响其子元素的宽度以及树中各处的节点,因此对于浏览器来说,布局过程是经常发生的。 Paint:绘制是填充像素的过程。它涉及绘出文本、颜色、图像、边框和阴影,基本上包括元素的每个可视部分。绘制一般是在多个层(Layer)上完成的。 Composite:由于页面的各部分可能被绘制到多层,由此它们需要按正确顺序绘制到屏幕上,以便正确渲染页面。对于与另一元素重叠的元素来说,这点特别重要,因为一个错误可能使一个元素错误地出现在另一个元素的上层。 换成这个图来看渲染引擎的处理流程 [图片] 这个过程比较复杂,详细的留在后面介绍Composite优化的时候再阐述。先简单说一下中间步骤,DOM树构建完成后,等待JS和CSS一起合成了Render树,每一个DOM节点对应一个Render Object,根据RenderObject的样式属性,可能将多个或者单个的object转换成RenderLayer,通常,渲染引擎的软件渲染到这就结束了,在开启硬件加速后,某些RenderLayer才会被转换成GraphicsLayer,最后利用GPU来进行合成和最终呈现。 如何检测render性能 上面说的渲染的五个步骤中的每一个都有可能造成卡顿,当然根据css属性的不同,可能会跳过layout或者paint阶段(具体每个css属性影响哪些阶段,请查看css触发器,注意chrome现在用的是blink内核),那么如何知道页面runtime中触发了哪些步骤以及各自性能了,最好的方法就是使用chrome devtool中的performance来记录分析。 打开chrome开发者工具,切换到performance tab,点击record按钮,这时你对页面的操作就会被记录下来,点击stop后就能看到性能火焰图等信息了,点击Frames中的一帧,下方的Main区域就会集中到这一帧的运行过程,如下图所示。(红圈区域即为选中一帧) [图片] 黄色为JS,紫色为Style和Layout,绿色为Paint和Composite部分,选中每个部分会显示各自的花费时间等信息,可以看出这个图片中JS运行的时间太长。目前的显示设备一般刷新率是60FPS,所以理想中每帧的时间最好为16毫秒,利用performance就能很直观的看出渲染中哪一步骤出现问题,下面介绍如何对每个步骤进行优化。 优化JS执行 JS 经常会触发视觉变化。有时是直接通过样式操作,有时是会产生视觉变化的计算,例如搜索数据或将其排序。时机不当或长时间运行的 JS 可能是导致性能问题的常见原因。通常可以通过以下几个方法来优化JS的执行。 对于动画效果的实现,避免使用 setTimeout 或 setInterval,请使用 requestAnimationFrame。 将长时间运行的 JavaScript 从主线程移到 Web Worker。 使用微任务来执行对多个帧的 DOM 更改。 使用requestAnimationFrame来执行视觉变化 先看一张图 [图片] 为了避免显示撕裂,开启垂直同步后,显示器每16ms(假设为60HZ)会发出一个VSync信号,浏览器收到信号后开启一帧的渲染,中间过程可能只用CPU完成软件渲染,也可能利用GPU硬件渲染,最终将渲染结果绘制到帧缓冲区,在下一个VSync信号到来时,显示器显示最新的渲染结果,并通知开启下一帧渲染。 在16ms的间隔中,如果一帧没有渲染完,那么这一帧就会被丢弃,显示器还是显示之前的画面,就会造成掉帧;同时如果16ms内如果完成多次渲染,显示器也只会更新一次画面,多次的渲染就会造成CPU和GPU的资源浪费。所以最理想的情况就是每16ms只渲染一次,一些老的框架会使用setTimeout来实现出这个间隔,但是会出现下图的问题。 [图片] 由于不能保证renderer主线程的运行时间,有可能setTimeout的回调会正好在间隔的中间被执行,如果渲染不能在下次间隔前完成,还是会造成卡帧。为了保证每次渲染都在一帧的开始来执行,requestAnimationFrame是唯一正确的方法,但是在使用时候也要注意一点,在requestAnimationFrame的回调执行之前,如果多次调用requestAnimationFrame,也会导致下一帧开始时多次执行这个回调,造成结果的不正确,所以需要加一下类似下面代码的控制。 [代码]function onScroll (evt) { // Store the scroll value for laterz. lastScrollY = window.scrollY; // Prevent multiple rAF callbacks. if (scheduledAnimationFrame) return; scheduledAnimationFrame = true; requestAnimationFrame(readAndUpdatePage); } window.addEventListener('scroll', onScroll); [代码] 分割长时间的JS的执行 由于长时间的JS执行会阻塞渲染,要尽量缩减一帧中JS的执行时间,不需要DOM权限的操作可以移到web worker中,但是通常我们的JS代码都会造成视觉变化,所以可以将一个耗时任务拆分成若干微任务,并利用requestAnimationFrame来执行,如下代码所示。 [代码]var taskList = breakBigTaskIntoMicroTasks(monsterTaskList); requestAnimationFrame(processTaskList); function processTaskList(taskStartTime) { var taskFinishTime; do { // Assume the next task is pushed onto a stack. var nextTask = taskList.pop(); // Process nextTask. processTask(nextTask); // Go again if there’s enough time to do the next task. taskFinishTime = window.performance.now(); } while (taskFinishTime - taskStartTime < 3 && taskList.length > 0); if (taskList.length > 0) requestAnimationFrame(processTaskList); } [代码] 优化样式的计算过程 通过添加和删除元素,更改属性、类或通过动画来更改 DOM,全都会导致浏览器重新计算元素样式。计算样式通过两个阶段来完成,首先浏览器计算出给指定元素应用哪些类、伪选择器和 ID,然后从匹配选择器中获取所有样式规则,并计算出此元素的最终样式。在Chrome的Performance记录区域,可以看到每一帧的渲染中,都有一个recalculate style的紫色矩形,记录的就是此次重新计算的耗时及影响到的元素数量等信息。通常采用下述两个方法来优化计算过程: 降低选择器的复杂性 减少必须计算其样式的元素数量 有时候我们喜欢用p:nth-of-type(2),:nth-child(n)等选择器来书写css内容,因为这样方便我们在一个父元素的所有子元素中找出一个特例来修改样式,但是这样会增加计算的复杂度,浏览器要知道其它所有子元素的情形,通常还是建议给元素一个明确的类选择器,例如BEM。 优化布局 布局是浏览器计算各元素几何信息的过程:元素的大小以及在页面中的位置。如何优化需要做到以下几点。 尽可能避免触发布局 因为布局几乎总是作用到整个文档。 如果有大量元素,将需要很长时间来算出所有元素的位置和尺寸。修改元素的几何属性(大小,位置等)都会导致整个文档重新布局,这个时候可以利用tranform的位移,放大缩小等操作来避免重新布局(前提是开启了硬件加速),这部分会在后面的composite优化部分详细描述,下面看两个demo demo1(更改top属性导致重新布局) demo2(利用translate不会导致重新布局) [图片] [图片] 利用performance分析能看出demo2没有触发layout 使用flex布局而不是浮动 早些年因为兼容性的问题,喜欢用float来实现布局,现在请使用flexbox,布局的性能会得到显著提升,看一下两个demo demo1(使用float布局) demo2(使用flex布局) 利用performance来分析,为了模拟手机上的效果,请将cpu 4x down降速 [图片] float是26.77ms [图片] flex是13.43ms 提升了一倍,看下flexbox目前的兼容性 [图片] 非IE的情况下,大家请安心使用吧(吐槽下,为啥还有人用IE),再贴一张最近一年桌面浏览器占有率 [图片] 避免强制同步布局 回忆下帧的渲染步骤,JS先运行,然后计算样式,再来布局,然而,JS可以强制布局提前,这被称为强制同步布局,看下代码。 [代码]// Schedule our function to run at the start of the frame. requestAnimationFrame(logBoxHeight); function logBoxHeight() { box.classList.add('super-big'); // Gets the height of the box in pixels and logs it out. console.log(box.offsetHeight); } [代码] JS运行时,来自上一帧的浏览器的布局信息是已知的,但是例子中的回调方法先增加了一个类,这个时候浏览器必须先应用样式修改,再重新布局,然后才能输出高度信息。通常上一帧的布局信息已经够用,这种强制同步布局会造成性能浪费。 4. 避免布局抖动 有一种情况会频繁的强制同步布局,看一下代码。 [代码] function resizeAllParagraphsToMatchBlockWidth() { // Puts the browser into a read-write-read-write cycle. for (var i = 0; i < paragraphs.length; i++) { paragraphs[i].style.width = box.offsetWidth + 'px'; } } [代码] 此代码循环处理一组段落,并设置每个段落的宽度以匹配一个称为“box”的元素的宽度。这看起来没有害处,但问题是循环的每次迭代读取一个样式值 (box.offsetWidth),然后立即使用此值来更新段落的宽度 (paragraphs[i].style.width)。在循环的下次迭代时,浏览器必须考虑样式已更改这一事实,因为 offsetWidth 是上次请求的(在上一次迭代中),因此它必须应用样式更改,然后运行布局。每次迭代都将出现此问题! [代码]此示例的修正方法还是先读取值,然后写入值: [代码] [代码]// Read. var width = box.offsetWidth; function resizeAllParagraphsToMatchBlockWidth() { for (var i = 0; i < paragraphs.length; i++) { // Now write. paragraphs[i].style.width = width + 'px'; } } [代码] 优化绘制与合成 绘制是填充像素的过程,像素最终合成到用户的屏幕上。 它往往是渲染过程中运行时间最长的任务,应尽可能避免此任务。合成是将页面的已绘制部分放在一起以在屏幕上显示的过程。这两个过程通常需要放在一起优化,而且是渲染过程中最需要关注的优化点,所以一起来详细阐述下。在介绍优化之前,我们要了解一下Blink的渲染基础知识,再来回顾一下之前放的一张图。 [图片] 这张图展示了Blink从最初的DOM树如何转换到最终的用于合成的Graphics Layer树,具体是如下步骤: Nodes 和 DOM树 网页内容在Blink内部以Node为节点的树形结构存储,称为DOM树。网页中的每一个HTML 元素,包括元素之间的text都和一个Node相关联。DOM tree的最顶层Node 永远是Document Node. From Nodes to RenderObjects DOM树中每一个可视化的Node 节点都对应着一个RenderObject。RenderObject 也存储在一棵对应的树结构中,称为Render树。 RenderObject 知道如何在一个显示设备上绘制(paint) Node 节点的内容。它通过调用GraphicsContext提供的绘制接口来完成绘制过程。GraphicsContext最终负责将像素写入一块bitmap,这块bitmap会被显示在屏幕上。在Chrome中,GraphicsContext 封装了Skia( 2D图形库)。 之前对GraphicsContext的大多数调用都转变成对SkCanvas或SkPlatformCanvas的接口调用。不过为了把绘制的实际过程移出主线程(后面会详细讲),现在这些调用命令被替换成记录到SkPicture。SkPicture是一个能够记录command,最后可以replay这些command的有序数据结构,类似于display list。 From RenderObjects to RenderLayers 每一个RenderObject 都关联着RenderLayer。这种关联是通过祖先RenderObject 节点直接或间接地建立的。分享同一坐标系的RenderObject(比如被同一CSS transform属性影响的元素)必然位于同一RenderLayer。 正是由于RenderLayer的存在,网页上的元素才可以按照正确的顺序合成,从而恰当的显示有交叠的内容,和半透明元素等效果。通常来讲,满足下列条件之一时,RenderObject就会创建RenderLayer: 根节点 有明确的CSS定位属性(relative, absolute) 透明的(opacity 小于 1) 有overflow, an alpha mask or reflection 有CSS filter 有2D加速Context或者3D(webGL)context的 canvas 元素对应的 有video元素的 需要注意的是RenderObject和RenderLayer之间并不是一一对应的。 RenderObject 或者与它所创建的RenderLayer相关联(如果它创建了的话),或者与它的第一个拥有RenderLayer的祖先RenderObject创建的RenderLayer相关联。 RenderLayer 也会形成一个树型层次结构。这个树结构的根节点是与网页的根元素相对应的RenderLayer。每一个RenderLayer 节点的后代都是包含在父亲RenderLayer内的可视化的RenderLayer. 每一个RenderLayer的子节点都被存储在两个按升序排列的有序表中。negZOrderList 有序表中存储的子节点是z-index值为负的子RenderLayer,所以这些RenderLayer在当前RenderLayer的下面;posZOrderList有序表中存储的子节点是z-index值为正的RenderLayer,所以这些RenderLayer在当前RenderLayer的上面。 事实上,在老版本的chrome里(15年之前),有一个软件渲染路径的概念,就是不需要硬件加速的情况下,渲染到这里结束了,放一张图来简单了解一下。 [图片] 所有的RenderLayer构建完成后,浏览器渲染进程调用Skia递归的将layer树绘制到共享内存中的单个位图,然后通过IPC传递到Browser Process,最终由Browser Process负责将位图drawing到屏幕。 4. From RenderLayers to GraphicsLayers 为了有效利用GPU硬件加速渲染,Blink又引入了一个新的GraphicsLayer,并且专门独立了一个专门的Compositor(合成器) Thread来管理GraphicsLayer以及协调帧的生命周期(后面会专门介绍这个合成器)。作为一个前端开发,你会经常听到用transform: translateZ(0)来开启所谓的硬件加速,实质上就是提升成了GraphicsLayer。 每一个RenderLayer或者拥有自己的GraphicsLayer(如果这个RenderLayer是compositing Layer的话),或者是使用它的第一个拥有GraphicsLayer的祖先节点的GraphicsLayer. RenderLayer与GraphicsLayer的关系类似于RenderObject与RenderLayer之间的关系。每个GraphicsLayer都拥有一个GraphicsContext,与这个GraphicsLayer相对应的每个RenderLayer都绘制到这个GraphicsContext上。合成器会负责将多个的GraphicsContext输出的位图最终合成一个最终的image。 理论上讲,每一个RenderLayer都可以将自己绘制到一个单独的backing surface上以避免不必要的重绘。但是在实际中,这种做法会导致内存的大量浪费(尤其是VRAM)。在当前的Blink实现中,只有满足以下条件之一,RenderLayer才会拥有它自己的compositing layer。 layer 有3D或者perspective transform 属性值 layer是硬解码的video 元素使用的 layer是拥有3D context或2D加速context的Canvas标签使用的 layer是一个合成的插件使用的 layer使用了动画表示它的透明度,或者使用了动画形式的webkit 变换 layer 使用了加速的CSS 滤镜 拥有compositing layer后代的layer 渲染在compositing layer之上的layer(overlap) 最后一个overlap为啥会产生合成层了?看一个例子。 [图片] 图中蓝色矩形覆盖在绿色矩形之上,同时它们的父元素是一个GraphicsLayer,假设绿色矩形也是一个GraphicsLayer,如果蓝色不是,那么它将和父元素公用一个合成层,既变成如下图情形。 [图片] 绿色矩形覆盖了蓝色矩形,渲染的顺序就发生了错误,所以为了保证正确,overlap也必须提升为合成层。 5. Layer Squashing overlap引起的合成层提升经常出现,就会导致有很多的合成层,岂不是会造成内存大量浪费,所以Blink专门有Layer Squashing(层压缩)的处理。看一下demo(层压缩)。 打开chrome的Performance工具来分析,选中一帧后,会看到下方工具栏出现一个layer tab,选中这个tab就能看到页面对应的合成层信息。 [图片] 红色圈中部分是显示有几个合成层,右侧绿色圈中部分显示这个合成层形成的原因和大小等信息。很明显,中间可视区域的深蓝色的矩形因为开启3D加速的原因被提升为合成层,绿色,红色,浅蓝三个矩形因为overlap的原因被提升成了合成层。 当我们把鼠标移到绿色矩形上,对应的CSS属性也修改成3Dtransform,所以绿色矩形也被提升为合成层,剩下的红色和浅蓝还是因为overlap被提升为另一个合成层,如下图所示。 [图片] 每一个GraphicsLayer都有对应的Composite Layer,这样Chrome的合成器才知道如何对这个GraphicsLayer进行处理,下面我们就来阐述下什么是合成器。 合成器(Compositor) Chrome的合成器是一个用来管理GraphicsLayer树和协调帧的生命周期的软件库。最初合成器也是被设计在渲染进程的主线程中的,现在合成器被拆成了两部分,一半在主线程里面,负责绘制(painting),主要工作就是把layer树的信息记录到SkPicture中,并没有实际上产生像素;另一半变成了单独的Compositor Thread(简称为cc),也被称为impl thread,这部分是真正的drawing,负责将painting中记录的layer信息经过光栅,合成等操作,最终显示到屏幕。下面分步骤来详细阐述合成器的工作。 Recording: Painting from Blink’s Perspective 兴趣区域(interest area)是要被记录到SkPicture中的viewport附近的区域。每当DOM元素改变,Blink会把兴趣区域中失效的部分layer树信息记录到 SkPicture-backed GraphicsContext。记住,这一步并没有真正的绘制像素,只是记录了可以replay出像素的命令的一个display list。 The Commit: Handoff to the Compositor Thread 合成器线程的一个关键特性就是它维护了主线程状态的一个复制,因此可以根据这个复制来生成帧而不用去询问主线程。主线程的状态信息就是一个LayerChromiumtree,对应的合成器线程复制的是CCLayerImpltree,这两棵树理论上是彼此独立的,这就意味着合成器线程可以在主线程阻塞的情况下使用当前的复制信息执行drawing内容到屏幕。 而当主线程产生了新的兴趣区域,合成器线程如何知道去修改它所维持的树的状态了?合成器线程有一个专门的调度器,使用commit来定期同步两棵树的状态。commit会将主线程更新过的LayerChromiumtree的状态以及新的SkPicture命令传给合成器线程,并同时block主线程来达成同步。这也是主线程在一个帧的生成过程中的最后一步。由于合成器线程独立于主线程,而且专门负责实际的drawing,所以浏览器传来的用户输入都是直接传到合成器线程的,一些不需要主线程参与的交互,例如用户键盘输入等,合成器线程可以直接处理完成页面的更新,但是如果主线程注册了事件的回调,这时候合成器线程就必须将更新的CCLayerImpltree状态以及一些额外任务反向commit给主线程。 Tree Activation 当合成器线程通过主线程的commit同步到更新后的layer tree信息后,会检查哪些layer是失效的并且重新光栅化这些layer。这时active tree是合成器线程保留的上一帧的layer tree信息,而新光栅化的layer tree信息被称为pending tree。为了保持展示内容的一致性,只有当pending tree已经完全光栅化后才会转换成新的active tree,从pending到active的过程被称为tree activation。 需要注意的非常重要的一点是有可能屏幕会滚动到当前的active tree之外,因为主线程只记录viewport周围的兴趣区域。这个时候合成器线程就会询问主线程去记录和commit新区域的信息,但是如果新的pending tree没能及时激活,用户就会滚动到一个所谓的 checkerboard zone。 为了减轻checkerboard zone,chrome将pending tree的光栅化分成低分辨率的部分和高分辨率的部分,当要出现checkerboard zone的时候优先光栅化低分辨率的部分并激活用来展现,这也就是为什么有时候有些页面在快速滚动时候会变模糊(例如google地图)。这部分工作是一个专门的tile manager来管理的(下一节的内容)。 Tiling 光栅化整个页面的layer tree是非常浪费CPU和内存的,所以合成器线程将layer tree分割成多个小的tile,设定好各个tile的优先级(根据离viewport的远近等因素来设置),并且专门创建了tile worker线程(一个或者多个)来执行这些tile的光栅化。在chrome的performance分析中能看到页面的tile,如图所示,勾选rending选项中的红色区域,就能看到页面中绿色border的tile。 [图片] Rasterization: Painting from cc/Skia’s perspective 主线程记录的SkPicture的display list,合成器线程通过两种方式来转变成最终上传到GPU的纹理(texture)。一种是基于CPU、使用Skia库的Software Rasterization,首先绘制进位图里,然后再作为纹理上传至GPU。这一方式中,Compositor Thread会创建出一个或多个Compositor Tile Worker Thread,然后多线程并行执行SkPicture records中的绘画操作,以之前介绍的Graphics Layer为单位,绘制Graphics Layer里的Render Object。同时这一过程是将Layer拆分为多个小tile进行光栅化后写入进tile对应的位图中的。另一种则是基于GPU的Hardware Rasterization,也是基于tile worker线程,也是分tile进行,但是这个过程不是像Software Rasterization那样在CPU里绘制到位图里,然后再上传到GPU中作为纹理。而是借助Skia’s OpenGL backend (Ganesh) 直接在GPU中的纹理中进行绘画和光栅化,填充像素。 Drawing on the GPU 一旦所有的纹理已经被填充,GPU进程就能使用深度优先遍历来遍历layer树的信息,然后调用GL/D3D命令来draw每个layer到帧的缓冲池,当然实际上每个layer的drawing还是分成tiles来进行的。下面这张图展示了GPU进程如何进行drawing。 [图片] 好了,到这里整个Compositor的部分阐述完了,我们也就知道了如何对帧渲染步骤中的绘制和合成来进行优化了–将页面频繁变化的部分提升到合成层,通常使用transform: translateZ(0),利用GPU渲染加速来进行合成。总结下,主要有以下几个优点。 合成层的位图,会交由 GPU 合成,比 CPU 处理要快 当需要 repaint 时,只需要 repaint 本身,不会影响到其他的层 对于 transform 和 opacity 效果,不会触发 layout 和 paint 当然,不能盲目的增加合成层数量,因为增加一个合成层就意味着更多的内存分配(特别是GPU内存)和更复杂的合成管理。我们应该专注于那些频繁变化的区域来进行优化。 帧的整个渲染步骤的优化都阐述完了,下面贴一张完整的流程图来总结一下。 [图片] 注意并不是每一帧中这些步骤都会发生,最多的步骤如下: Frame Start. 合成器线程收到来自浏览器的Vsync信号和Input data,一帧开始。 Input event handlers. Input data被合成器线程传给了主线程,注册的事件回调被执行,注意这里合成器线程做了优化,保证一帧中最多只会触发一次event handler,所以自带了requestAnimationFrame的节流效果。 requestAnimationFrame. 如果之前注册了raf回调,会在这里执行,这是最完美的执行更新视觉的地方。唯一要注意的就是避免发生强制布局,即导致样式计算和布局提前(红线所示)。 Parse HTML. 新增的html会在这里被解析,生成对应DOM元素。大部分你会在page load和appendChild之类操作后见到它。 Recalc Styles. 如果你在JS执行过程中修改了样式或者改动了DOM,那么便会执行这一步,重新计算指定元素及其子元素的样式。 Layout. 如果有涉及元素位置信息的DOM改动或者样式改动,那么浏览器会重新计算所有元素的位置、尺寸信息。 Update Layer Tree. 这一步实际上是更新Render Layer的层叠顺序关系,保证层叠的正确。 Paint. paint操作实际上有两步,第一步是主进程将layer tree的相关信息记录到SkPicture中,类似一个display list;第二部是合成器线程replay这个记录list来光栅化和填充上传纹理。主线程的paint只是第一步。 Composite. 这里其实也分两步,主线程这里计算出每个Graphics Layers的合成时所需要的data,包括位移(Translation)、缩放(Scale)、旋转(Rotation)、Alpha 混合等操作的参数,然后就是图中我们看到的第一个commit,主线程通知合成器线程去同步layer tree的信息。然后主线程此时会去执行requestIdleCallback。这一步并没有真正对Graphics Layers完成位图的composite。 Raster Scheduled and Rasterize。 第8步生成的SkPicture records在这个阶段被执行。合成器线程创建出若干个Compositor Tile Worker Thread,利用CPU软件光栅化或者GPU的硬件光栅化,最终将纹理写入了GPU内存中。 Frame End. 合成器线程已经完成paint和composite的工作,这时会发送一个commit给GPU进程,告诉他可以进行draw了,同时会传达主线程一个commit done,如果一个帧中视觉的变化没有主线程参与,这里合成器线程也会同步更新后的合成器layer tree信息给主线程。 draw. GPU进程按照深度优先遍历将最后的纹理draw到帧缓冲区,等待显示器的下一个Vsync到来时去显示。 结语 整个浏览器页面渲染的过程以及优化都阐述完了,性能优化是一门艺术,本文也只是很浅显的探讨了其中的一些基本概念和设计思想,如果想深入理解具体的架构和实现过程,还是要去阅读一下chrome的内核源码。对于我们前端开发来说,切忌的是为了优化而优化,实际开发过程中碰到了页面卡顿的情况,利用performance来分析找出卡顿的原因,针对卡顿的步骤不断进行改进测试,才是正确的优化方法。 参考引用 GPU Accelerated Compositing in Chrome compositor-thread-architecture rendering-performance the-anatomy-of-a-frame 无线性能优化:Composite
2019-03-15 - 前端加载优化及实践
大家都知道产品体验的重要性,而其中最重要的就是加载速度,一个产品如果打开都很慢,可能也就没有后面更多的事情了。这篇文章是我最近项目中的一些加载优化总结,欢迎大家一起讨论交流。 内容包括: 性能指标及数据采集 性能分析方法 性能优化方法 性能优化具体实践 第一部分:性能指标及数据采集 要优化性能首先需要有一套用来评估性能的指标,这套指标应该是是可度量、可线上精确采集分析的。现在来一起看看如何选择性能指标吧。 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 - 自定义标题栏
使用效果 [图片][图片][图片][图片] 使用方法 属性介绍 属性名 类型 默认值 是否必须 说明 menuSrc String ‘’ 否 按钮图片地址 bgImgSrc String ‘’ 否 背景图片地址 bgImgMode String aspectFill 否 背景图片的显示模式 title String ‘’ 否 标题 titleTextColor String ‘’ 否 字体和按钮以及loading图标的颜色,按钮和loading暂时只有黑白2色 backgroundColor String ‘’ 否 整个标题栏的背景颜色 loading Boolean false 否 是否是加载状态 backProxy Boolean false 否 是否重写了返回键 标题栏中属性的默认数据会自动获取json配置以及系统的默认数据,如果不需要动态更改样式,可以在json中设置,组件中同样起作用 事件介绍 属性名 detail NaviBack 返回的逻辑方法 MenuTap 按钮的点击事件 [代码]"usingComponents": { "toolBar": "/component/toolbar" }, [代码] [代码]<toolBar menuSrc='/image/menu_white.png' bindMenuTap='onMenuTap' bgImgSrc='/image/navi-bg.jpg' /> [代码] 高度说明: 为了方便适配,这里给出自定义标题栏的计算公式: const MenuRect = wx.getMenuButtonBoundingClientRect() const statusBarHeight = wx.getSystemInfoSync().statusBarHeight; const height = (MenuRect.top - statusBarHeight) * 2 + MenuRect.height +MenuRect.top Github地址:https://github.com/Aracy/wx-mini-navigationbar
2019-05-21 - 微信小程序开发经验+, 可能需要了解的一些骚操作
扩展Page / App? 自定义navigationBar? 冗余授权代码? local/session/expire缓存管理混乱? … 总结了一些开发微信小程序过程中遇到问题的解决方式/经验分享,另外共享几个通用组件. star+✨ https://github.com/JoweiBlog/wechat-miniprogram-dev
2019-05-22 - 【U计划】弹幕biubiu小程序开发经验分享
弹幕biubiu小程序开发经验分享 Hello,大家好~我们是来自清华大学软件学院大三的弹幕弹幕团队,我是团队的Leader&Developer。我们团队开发的小程序叫作“弹幕biubiu”,主要应用场景是各类晚会现场。你可能已经发现了,我们的团队名和小程序名是不一样的,这是因为我们在确定了团队名称之后,发现这个名字已经被其他的小程序占用了,所以我们只能将小程序换成另一个名字。这也提醒了大家,在开发小程序的时候,一定要先确认自己起的名字没有被使用哦~ 我们的小程序是从去年10月开始开发的,直至今年3月基本完成。之后在今年4月的清华大学软件学院学生节上,我们的小程序作为观众弹幕互动平台被使用,并取得了广泛好评。这一方面说明我们小程序的实用性,另一方面也说明了弹幕互动及其衍生方向依然有很大的发展空间。 [图片] 我们团队的开发选用了敏捷开发的方式,并采用Scrum框架(下图源自清华大学软件学院刘强老师的软件工程课件)。本文将介绍我们团队在开发过程中所做的一些主要工作,希望能够给大家一些启发与帮助。[图片] 1.立项 万事开头难,开发过程中最困难的地方,往往就是在最开始的地方。一个团队中不缺技术人员,而缺少设计人员,也就是“有思想的人”。而且好的想法一定是来源于生活的,如果不仔细观察生活,只是天马行空地构想,是无法获得好的项目主题的。 我们团队在计划开发一个小程序之后,就开始讨论主题。首先我们确定了我们小程序的大致方向。我们发现在每次举办院系学生节时,都需要科协同学用一天的时间去部署弹幕墙,这样效率较低,而且也常常出现弹幕墙宕机的情况,所以我们决定开发一个学生节小程序。之后我们团队通过头脑风暴,将自己设想为学生节举办方与学生节观众,讨论我们可能需要哪些功能,不需要哪些功能等等,从而将项目目标进一步细化。 在这里我们并不能只单纯地讨论,我们需要一个记录者,将所有人的想法记录下来并进行归整,之后再由每个成员进行修改完善,得到我们开发的第一份文档——产品规划文档(弹幕biubiu的产品规划文档)。如果大家对产品规划文档形式不太清楚的话,可以参照上述我们的文档。在文档中我们对于产品的定位、产品的特性以及产品的路线都有了一个明确的描述。 当我们有了产品规划文档之后,立项的过程还没有结束。因为这个文档只是根据自己团队的想法写成的,但是真正的用户会有成千甚至上万人,并不一定每个人都和团队内的成员想法相同。除此之外,市场上可能已经有一些类似的产品。所以我们必须要进行一个调查,明确其他可能的用户的需求以及现有市场上的产品提供的功能。这样的调查有助于我们跳出团队内固有的思维模式,催生出一些新的点子,同时也能避免无用功。在调查结束之后,我们获得了更多的用户需求。我们需要对需求进一步整理细化,写出用户故事(弹幕biubiu的需求获取与用户故事文档),并画出用户故事地图。 [图片] 到此为止,我们基本完成了所有立项工作。这时整个团队应当对自己的项目开发目标有了明确清晰的认知。 2.设计 当明确需求之后,我们就要开始设计工作,也就是进入到敏捷开发的迭代周期中了。设计主要分为两部分: 系统设计 原型设计 系统设计中最重要的工具是UML,即统一建模语言。你可能会用到其中的类图、活动图、用例图等。 2.1 系统设计 系统设计目的是确立技术开发过程中的总纲,这也是开发过程中极为重要的一步,整个开发过程都是围绕系统设计文档展开的。 我们团队开发的小程序使用的是MVC模式,即模型(model)-视图(view)-控制器(controller)。这种模式满足了高内聚、低耦合的程序结构,便于团队开发与管理。 系统设计要求我们首先对于整体系统结构有一个清晰的构想。我们的弹幕小程序的系统架构图如下所示,可以看到这里我们将系统清晰地分为了数层。 [图片] 之后我们就需要对系统的结构进行细化,主要包括两部分内容: 数据存储结构。也就是数据库的设计。我们用一个数据库来管理数据,那么我们需要构造哪些表,每个表中需要保存那些数据等等,都是我们需要考虑的问题。我们团队采用了Mysql+Redis两种数据库相结合的存储模式,保证了数据的读写效率。 前后端接口设计。接口设计可以方便团队前后端开发的分离,提高开发效率。我们团队采用的是Restful的API接口规范,大家可以自行查阅了解该规范的内容。 因为不同的小程序会有不同的系统,所以我这里的设计思路也仅供大家参考,主要的还是要开发团队自己思索并设计,得到最适合自己的系统结构。当然合适的系统架构不意味着在项目的最初就要将所有细节想得十分透彻。一个好的系统架构主要有两方面的特征:稳定的框架与可扩展性。稳定的框架保证了开发过程中无需对代码进行很大程度的调整重构;可扩展性保证了开发人员可以很轻松地将后续的内容添加进系统,而不会影响系统整体的特性。 2.2 原型设计 原型设计,也就是UI设计。我们团队使用的工具是墨刀。墨刀有着丰富的素材库,并且可以设计控件行为,方便团队成员理解交互过程。 [图片] 原型设计往往会是团队中讨论最激烈的环节,因为每个人的审美是不同的,更何况团队中基本都是理工生(sigh…)。我们在原型设计时也进行了多次讨论与修改,才最终确定其样式。 3.开发 3.1 团队管理 自组织团队是敏捷开发的基础,团队被授权自己管理工作进程,并决定如何完成工作。团队成员在开发的过程中需要各司其职,扮演好自己的角色。但是根据著名的“20%定律”,每个团队中总会有20%的成员是free rider,所以这就需要团队的领导者对团队进行良好管理。 在开发的初期,一个团队需要制定自己团队的开发章程,包括每周的开会时间、开会地点、每个成员负责开发的模块、团建安排等,并在之后的开发过程中严格按照章程的规定管理团队。 其次,团队在每次例会时,需要每名成员汇报之前任务的开发进度,对开发过程中遇到的问题进行讨论思考,并确定下一阶段的开发任务。每次的例会都需要指定一名成员记录会议内容,方便团队日后查看。 团队管理中,最主要的就是任务安排的部分。任务安排主要包括两点:明确分工与时间规划。 明确分工是要让大家清楚自己具体是要做什么,其重点在于“细化”。举个例子,如果你和我说,“你去做一个主办方管理网站”,我肯定一头雾水无从下手;但是如果你和我说:“你去实现一个主办方登录的功能,主办方输入用户名和密码就可以跳转到活动列表页面”,那我就可以很快地完成这一个任务。所以明确分工是团队成员,尤其是组织者,需要重点注意的。 时间规划则是让大家有一个紧迫感。做时间规划最好的方法是,组织者先定一个大概的时间,然后所有团队成员进行协商,定出每个人都满意或至少不反对的时间安排。因为团队成员都会有惰性,就算最好的团队也不例外,所以一个明确的时间规划可以让每个人有计划有安排地完成任务。从这个意义上来讲,时间规划也是调动成员热情的不错的方案。 3.2 代码管理与持续集成 我们团队使用git进行代码版本的控制管理,在开发过程中维护了三个代码库,分别对应于系统后端、微信小程序以及弹幕主墙应用程序。我们也利用Github上的Issues、Projects、Wiki等功能辅助我们进行开发。由于我们团队尚未开源,所以这里也不方便向大家展示代码库的具体细节。 此外,我们团队使用Travis CI辅助我们进行代码的持续集成与自动部署,感兴趣的话大家可以自主学习一下它的使用方法。 3.3 文档管理 通过上文我们也发现了,在开发过程中我们会写很多的文档,所以合理的文档管理也是开发中的重点问题。我们团队使用的在线文档工具是石墨文档。石墨文档有三个优点: 有清晰简洁的界面与丰富的功能 支持多成员在线编辑 可以很方便地导出为word文档与pdf文档 我们团队还维护了一个产品文档目录,这样每次要修改或查阅文档时,都有一个很便捷的入口。 [图片] 4.测试 测试主要有三部分:单元测试、功能测试与性能测试。 单元测试。单元测试,就是对软件中最小可测试单元进行检查和验证。不同软件的单元测试是不同的,比如我们在开发后端时,使用的是Python中的Django框架,这一框架是自带单元测试模块的,所以我们只需在test.py中实现所有测试样例即可。单元测试保证了软件最基本的正确性,最佳的模式是开发者在开发的过程中就将单元测试样例写好。(现在微信小程序还没有单元测试的模块,希望之后小程序团队可以在这方面给出接口。) 功能测试。功能测试就是要测试软件系统的各个功能能否正常执行。功能测试的辅助软件有很多,但最简单也最方便的就是人工手动测试,也就是开发者模拟用户的使用场景测试一遍自己的软件系统。 性能测试。软件性能也是评判一个软件好坏的重要依据。就以我们的弹幕小程序为例,虽然现在的学生节晚会只有三百多人,但是如果要拓展到所有晚会时,就不得不考虑高并发的情况。所以我们团队使用Jmeter对于发送弹幕的功能进行了性能测试,并在测试之后通过图片压缩等方式提高了我们小程序的性能。 除此之外,测试环节还包括安全性测试、易用性测试、兼容性测试等。测试过程中大家需要尤其注意的地方是:一定要将测试场景与测试样例想全面。越是严密的测试,软件系统也就相应越理想。 5.分析与维护 在开发与测试结束之后,小程序也就被正式投入使用了。因为用户行为是多种多样的,所以这个时候不出意外会出现一些奇奇怪怪的bug。作为开发者一定要给出一个用户反馈的途径,并且根据用户反馈的问题,制定下一个迭代周期的任务。这样循环往复,直至软件达到预期。下图为我们小程序为用户提供的反馈接口: [图片] 6.总结 以上就是我在开发过程中的一些经验与体会,希望能够给大家一些帮助与启示。弹幕biubiu小程序的开发,对于我来说是一个特别宝贵的经历。在这个过程中我学到了很多新的知识,接触到了很多新的事物,也发现了其他同学很多的优点。同时也很感谢刘强与刘璘两位老师对我们团队的支持与指导,在这里也推荐一下两位老师在学堂在线上的软件工程课程,如果大家感兴趣的话可以去了解学习一下,相信会给你们很大的帮助。 附言 如果大家对我们的小程序感兴趣的话,也可以使用一下呀~ 使用说明 主办方管理网站 应用程序下载链接(也可在主办方管理网站中下载) 小程序二维码 [图片] 大家有什么问题或者建议的话,也欢迎随时与我交流~ 我的Github地址是:https://github.com/JL-Cheng 我的邮箱是:chengjl16@163.com
2019-05-23 - 小打卡 | 如何基于微信原生构建应用级小程序底层架构(上)
[图片] 大家好,我是小打卡的前端负责人金轩正,今天分享的主题是如何基于微信原生构建应用级小程序底层架构,这个命题看上去好像有些大,不过不要紧,这次分享我把它拆一下,大致从 小程序原生开发面临的问题 小打卡整体架构演进 开发中摸索与实践 这三个方面来看这个讲一下 [图片] 小程序原生开发面临的问题[图片] ok,首先第一个方面原生开发遇到的问题 小程序从17年诞生2年来一直处于互联网风口,不过对于开发者而言的整个开发体验不是特别友好,在17-18年之间我和很多开发小程序的小伙伴们聊过,大多数的反馈可能分为下面大致几类,当然还有更多: 没有父类,无法使用继承挂载全局方法,扩展生命周期没有父类,无法使用继承挂载全局方法,扩展生命周期 不支持跨页面/多页面通讯 setData的性能瓶颈 代码包大小限制 1/2/4/8 M,没有npm包 代码发布流程繁琐 其根本原因是将刚刚诞生的小程序与已经非常成熟的React,vue,angular作对比,而没有将小程序作为一个新的生态来看待,当然这个是一种看待事物的进步,并不是倒退,我在这里说这句话的意思是有更多的问题需要我们开发者主动去解决问题,推动整个生态的前进与发展 [图片] 其实这里可能有些朋友会问,已经有很多优秀的框架已经解决了这些问题,那么为什么还要使用原生开发? 确实在这段时间内出现了很多优秀的解决方案,我们不用并不是因为情怀哈(当然还是有那么一丢丢) 更多的是下面几点: 历史包袱,改造成本过高 小打卡在小程序刚出现的时候就进入开发了,当时框架还不成熟,而且对创业公司来说时间和迭代效率高于一切,在人手不足,业务模式尚未形成,还处于探索阶段的情况下花费大量时间去做对产品影响较小, 甚至delay迭代速度事情不是很赚 减少与第三方沟通成本 高速迭代的情况下,将时间尽可能的覆盖于业务上,避免在整个开发-上线闭环上增加节点 避免开发黑盒,控制风险 虽然整个社区是非常活跃的,fixed一个问题同样是需要花费一定时间,但是很多时候需求是不会等你bug fixed 如非必要,勿增实体 即“简单有效原理”,这句话还是我去年刚来公司的时候和阿赖聊他所说过的 放在项目开发上我的理解是在架构层面要做的尽可能的薄,避免过度设计 这样才有足够的扩展性,灵活性,容错性 这些框架虽好,但是对我们当前业务来说可能过于复杂,比如跨端在之前的阶段还没有这方面需求,而像组件化小程序已经支持,自动化构建我们自己也是可以搭建的并不复杂 相信微信小程序团队 是真正的想把这件事情做好,而且做的是一个生态,不论是小程序对于反馈响应速度,和迭代速度非常给力,还是对开发者社区运营,比如是社区活跃与审核速度挂钩,社区周刊,优质个人和优质企业 对齐web标准,并且更加开放 [图片] 小打卡整体架构演进其实小打卡整个架构并非一蹴而就的,就像前面所说的如非必要,勿增实体,而是大量的实际开发中遇到的共同问题解决方案的集合题 [图片] 常规架构这个是微信小程序给出的快速开发模版的一个开发模式: server模块提供数据,App作为全局对象直连所有的业务模块,工具函数提供api处理业务模块的需求 优点: 整个模型非常简单,上手快,学习成本 低结构清晰,在业务不复杂的情况下可以快速开发 不瞒大家其实小打卡在最初的半年内基本都是这套模式。 当然是在业务不复杂的情况下,复杂情况下会出现哪些问题呢? App作为全局对象在有大量业务模块连接的情况下,代码很容易膨胀,在多人开发的时候问题非常明显,无论是fixed bug还是正常的业务开发都会造成麻烦 页面之间独立,缺少公共模块,唯一的工具函数又要尽可能保持单一职责来提供服务(小打卡当时就是因为这个问题导致很多工具函数内部存储直接修改外部状态,导致大量强耦函数合无法拆分) 业务层直连server层,未拆分数据层的情况下,基本不存在复用性 上面所述的问题,从我接手这个项目到真正的调整持续了挺长一段时间,主要是缺乏一个契机来进行优化 优化的转折点 [图片] 然后突然有一天产品同学跑过来说: 我们要有自己的核心数据仓库,我们要看实时数据 ok,涉及到数据采集的问题了,我这边从浅到深大概列了几项: 最基础的多个页面pv,uv如何监控,不可能每个页面都要手动收集 为了统计页面和事件的分享和回流的数据,需要在分享事件携带大量的参数 微信的wx.previewImage, wx.chooseImage 等api对于用户session的收集造成很大麻烦 我们先解决第一个问题,如何收集页面pv,uv 容易陷入的误区 [图片] 在解决问题之前,我们先说一下开发小程序容易进入的误区 App 和 Page 等函数工厂是微信原生提供,不可修改 小程序项目结构是基于App, Page, 工具函数三个模块构建的 小程序的全局存储只有globalData和本地缓存 其实产生这些误区最根本的原因是小程序没有提供在复杂业务逻辑下的开发范式,比如vue,react有自己的通用开发模版 如果保持这些观念来进行开发的话,很容易将路子走窄,并且难以解决一些实际上的问题, 其实不论小程序和传统web有多少不同, 本质上还是在js环境下开发 小打卡架构图解 [图片] 为了更好的方便理解后面的具体实现,我提前放了一张目前小打卡的架构图 首先很熟悉的server这一边垫了一个数据层,主要将数据层和业务层解耦,提高复用性,并且提供一些通用功能,比如返回格式化数据问题,参数校验,日志监控... 在App对象和业务层同样增加了一个全局模块,提供独立于业务和工具类,只提供api之间双向通讯的渠道 工具模块的话其实就是对业务层的增强,比如常见的请求模块,上传模块,路由拦截等等 业务模块的话基本除了增加Component和中间层外没有太大变化 这个图上可能有两块可能大家觉得比较怪异,一个是global里面的函数重载,还有一个是业务模块的中间层是什么? 函数重载其实就是修改微信提供的App, Page, Component函数,使其更符合我们的业务场景, 业务模块的中间层就是依赖于函数重载的扩展 其实小打卡的整套架构都是基于这两个模块,这两个模块赋予了更多的可能性,然而实现却十分的简单 点击查看:小打卡 | 如何基于微信原生构建应用级小程序底层架构(下)
2019-04-22 - 小程序需要https域名,不会配置HTTPS?给我5分钟,手把手教你
本文针对不会配置HTTPS或者小白开发着,请开发者社区的大佬们自动忽略。非广告,心得分享,勿喷,谢谢。 👇 推荐一个小程序商城,全开源,码云GVP项目,有兴趣的可以了解一下:【点击下载】 👇 👇 正文开始 01、关于 FreeSSL.cn FreeSSL.cn 是一个免费提供 HTTPS 证书申请、HTTPS 证书管理和 HTTPS 证书到期提醒服务的网站,旨在推进 HTTPS 证书的普及与应用,简化证书申请的流程。 当然了,我看重的不是免费(微笑~),而是 FreeSSL 使用起来非常人性化。我是一个计算机常识非常薄弱的程序员(羞愧一下),但通过 FreeSSL,我竟然可以独自完成 Tomcat 的 HTTPS 配置! 很多年以前,公司要做华夏银行的接口对接,需要 HTTPS 访问,大概花了 3000 块买的证书,最后证书还有问题,HTTPS 也没搞定。总之,坑的很! FreeSSL.cn 有很大的不同,申请非常便捷,优点很多,值得推荐一波。毕竟再也不用邮件、电话各种联系了(也许时代进步了)。 100% 永久免费;这要感谢 Let’s Encrypt 与 TrustAsia 提供的免费 SSL 证书。 在 HTTPS 证书到期前,FreeSSL.cn 会及时地提醒更换证书,免费的服务。 私钥不在网络中传播,确保 HTTPS 证书的安全。 02、使用 FreeSSL 申请证书 第一步,填写域名,点击「创建免费的 SSL 证书」 [图片] 第二步,填写邮箱,点击「创建」 [图片] 1)证书类型默认为 RSA RSA 和 ECC 有什么区别呢?可以通过下面几段文字了解一下。 HTTPS 通过 TLS 层和证书机制提供了内容加密、身份认证和数据完整性三大功能,可以有效防止数据被监听或篡改,还能抵御 MITM(中间人)攻击。TLS 在实施加密过程中,需要用到非对称密钥交换和对称内容加密两大算法。 对称内容加密强度非常高,加解密速度也很快,只是无法安全地生成和保管密钥。在 TLS 协议中,应用数据都是经过对称加密后传输的,传输中所使用的对称密钥,则是在握手阶段通过非对称密钥交换而来。常见的 AES-GCM、ChaCha20-Poly1305,都是对称加密算法。 非对称密钥交换能在不安全的数据通道中,产生只有通信双方才知道的对称加密密钥。目前最常用的密钥交换算法有 RSA 和 ECDHE:RSA 历史悠久,支持度好,但不支持 PFS(Perfect Forward Secrecy);而 ECDHE 是使用了 ECC(椭圆曲线)的 DH(Diffie-Hellman)算法,计算速度快,支持 PFS。 2)验证类型默认为 DNS DNS 和文件验证有什么区别呢?我们再来一起了解下。 首先,我们需要明白一点,CA(Certificate Authority,证书颁发机构) 需要验证我们是否拥有该域名,这样才给我们颁发证书。 文件验证(HTTP):CA 将通过访问特定 URL 地址来验证我们是否拥有域名的所有权。因此,我们需要下载给定的验证文件,并上传到您的服务器。 DNS 验证:CA 将通过查询 DNS 的 TXT 记录来确定我们对该域名的所有权。我们只需要在域名管理平台将生成的 TXT 记录名与记录值添加到该域名下,等待大约 1 分钟即可验证成功。 所以,如果对服务器操作方便的话,可以选择文件验证;如果对域名的服务器操作比较方便的话,可以选择 DNS 验证。如果两个都方便的话,请随意选啦。 3)CSR生成默认为离线生成 离线生成、浏览器生成 和 我有 CSR 又有什么区别呢?来,我们继续了解一下。 离线生成 推荐!!!:私钥在本地加密存储,更安全;公钥自动合成,支持常见证书格式转换,方便部署;支持部分 WebServer 的一键部署,非常便捷。 离线生成的时候,需要先安装 KeyManager,可以提供安全便捷的 SSL 证书申请和管理。下载地址如下: https://keymanager.org/ Windows 的话,安装的时候要选择“以管理员身份运行”。 浏览器生成:在浏览器支持 Web Cryptography 的情况下,会使用浏览器根据用户的信息生成 CSR 文件。 Web Cryptography,网络密码学,用于在 Web 应用程序中执行基本加密操作的 JavaScript API。很多浏览器并不支持 我有 CSR:可以粘贴自己的 CSR,然后创建。 第三步,选择离线生成,打开 KeyManager 填写密码后点击「开始」,稍等片刻。 第四步,返回浏览器,点击「下一步」,出现如下界面。 [图片] 第五步,下载文件,并上传至服务器指定目录下。 第六步,点击「验证」,通过后,出现以下界面。 [图片] 第七步,点击「保存到KeyManager」,可以看到证书状态变成了已颁发。 03、为 Tomcat 配置 jks 格式证书 第一步,导出证书。假如服务器选择的 Tomcat,需要导出 Java keystone (简拼为 jks)格式的证书。 [图片] 注意:私钥的密码在配置 Tomcat 的时候用到。 [图片] 第二步,上传证书至服务器。 第三步,配置 Tomcat 的 server.xml。 [代码] <Connector port="81" protocol="HTTP/1.1" maxThreads="250" maxHttpHeaderSize="8192" acceptCount="100" connectionTimeout="60000" keepAliveTimeout="200000" redirectPort="8443" useBodyEncodingForURI="true" URIEncoding="UTF-8" compression="on" compressionMinSize="2048" noCompressionUserAgents="gozilla, traviata" compressableMimeType="text/html,text/xml,application/xml,application/json,text/javascript,application/javascript,text/css,text/plain,text/json,image/png,image/gif"/> <Connector protocol="org.apache.coyote.http11.Http11NioProtocol" port="443" maxThreads="200" scheme="https" secure="true" SSLEnabled="true" keystoreFile="/home/backup/qingmiaokeji.cn.jks" keystorePass="Chenmo" clientAuth="false" sslProtocol="TLS" useBodyEncodingForURI="true" URIEncoding="UTF-8" compression="on" compressionMinSize="2048" noCompressionUserAgents="gozilla, traviata" compressableMimeType="text/html,text/xml,application/xml,application/json,text/javascript,application/javascript,text/css,text/plain,text/json,image/png,image/gif" /> [代码] 其中 keystorePass 为导出证书时私钥的加密密码。 第四步,重启 Tomcat,并在浏览器地址栏中输入 https://你的域名/ 进行测试。 注意到没,浏览器地址栏前面有一个绿色的安全锁,这说明 HTTPS 配置成功了!好了,为自己鼓个掌! 04、最后 你有没有订个五分钟的时间沙漏?如果超过五分钟 HTTPS 还没有配置成功,你过来揍我!反正你又打不来我!我在CRMEB等你! 👇 👇 👇 最后亿遍,再次发一下我的项目:全开源啊!公众号+小程序啊!商城系统啊!免费啊!了解一下啊→→→点我点我!
2019-05-10 - PNG图片压缩原理--屌丝的眼泪
背景 今天凌晨一点,突然有个人加我的qq,一看竟然是十年前被我删掉的初恋。。。。 因为之前在qq空间有太多的互动,所以qq推荐好友里面经常推荐我俩互相认识。。。。谜之尴尬 [图片] 同意好友申请以后,仔细看了她这十年间所有的qq动态和照片。 她变美了,会打扮了,以前瘦瘦的身材配上现在的装扮和妆容,已经是超越我认知的女神了。 而我依然碌碌无为,逐渐臃肿的身体加上日益上扬的发际线,每天为生活操劳和奔波,还穷。 用一句话形容现在的感受就是: “妳已经登上更高的巅峰 而我只能望着妳远去的背影”。 默默点了根烟,把她长得好看的照片都保存了下来。 咦?发现每一张照片都是.png的图片格式。 png??png的图片我们每天都在用,可是png到底是什么,它的压缩原理是什么? 很好,接下来我将会给大家一一阐述。 什么是PNG PNG的全称叫便携式网络图型(Portable Network Graphics)是目前最流行的网络传输和展示的图片格式,原因有如下几点: [代码]无损压缩[代码]:PNG图片采取了基于LZ77派生算法对文件进行压缩,使得它压缩比率更高,生成的文件体积更小,并且不损失数据。 [代码]体积小[代码]:它利用特殊的编码方法标记重复出现的数据,使得同样格式的图片,PNG图片文件的体积更小。网络通讯中因受带宽制约,在保证图片清晰、逼真的前提下,优先选择PNG格式的图片。 [代码]支持透明效果[代码]:PNG支持对原图像定义256个透明层次,使得图像的边缘能与任何背景平滑融合,这种功能是GIF和JPEG没有的。 PNG类型 PNG图片主要有三个类型,分别为 PNG 8/ PNG 24 / PNG 32。 [代码]PNG 8[代码]:PNG 8中的8,其实指的是8bits,相当于用28(2的8次方)大小来存储一张图片的颜色种类,28等于256,也就是说PNG 8能存储256种颜色,一张图片如果颜色种类很少,将它设置成PNG 8得图片类型是非常适合的。 [代码]PNG 24[代码]:PNG 24中的24,相当于3乘以8 等于 24,就是用三个8bits分别去表示 R(红)、G(绿)、B(蓝)。R(0255),G(0255),B(0~255),可以表达256乘以256乘以256=16777216种颜色的图片,这样PNG 24就能比PNG 8表示色彩更丰富的图片。但是所占用的空间相对就更大了。 [代码]PNG 32[代码]:PNG 32中的32,相当于PNG 24 加上 8bits的透明颜色通道,就相当于R(红)、G(绿)、B(蓝)、A(透明)。R(0255),G(0255),B(0255),A(0255)。比PNG 24多了一个A(透明),也就是说PNG 32能表示跟PNG 24一样多的色彩,并且还支持256种透明的颜色,能表示更加丰富的图片颜色类型。 怎么说呢,总的来说,PNG 8/ PNG 24 / PNG 32就相当于我们屌丝心中,把女神分为三类: [代码]一类女神 = PNG 8[代码]:屌丝舔狗们见到第一类女神,顿时会觉得心情愉悦、笑逐颜开,屌丝发黑的印堂逐渐舒展,确认过眼神,是心动的感觉。 [代码]二类女神 = PNG 24[代码]:第二类女神开始厉害了,会给屌丝们一种菊花一紧、振聋发聩的心弦震撼,接触多了第二类女神能让屌丝每天精神抖擞,延年益寿。 [代码]三类女神 = PNG 32[代码]:在第三类女神面前,所有的语言都显得苍白无力。那是一种看了让屌丝上下通透、手眼通天的至尊级存在。超凡脱俗、天神下凡都不足以描摹她美色的二分之一。我曾经只有在梦里才见到过。 哎。。。我的初恋,看着她现在的照片,应该是触及PNG 24这一等级了。 PNG图片数据结构 PNG图片的数据结构其实跟http请求的结构很像,都是一个数据头,后面跟着很多的数据块,如下图所示: [图片] 如果你用vim的查看编码模式打开一张png图片,会是下面这个样子: [图片] 握草,第一眼看到这一坨坨十六进制编码是不是感觉和女神的心思一样晦涩难懂? 老弟 莫慌,讲实话,如果撩妹纸有那一坨坨乱码那么简单,哥哥我早就妻妾成群啦。 接下来我就一一讲解这一堆十六进制编码的含义。 [代码]8950 4e47 0d0a 1a0a[代码]:这个是PNG图片的头,所有的PNG图片的头都是这一串编码,图片软件通过这串编码判定这个文件是不是PNG格式的图片。 [代码]0000 000d[代码]:是iHDR数据块的长度,为13。 [代码]4948 4452[代码]:是数据块的type,为IHDR,之后紧跟着是data。 [代码]0000 02bc[代码]:是图片的宽度。 [代码]0000 03a5[代码]:是高度。 以此类推,每一段十六进制编码就代表着一个特定的含义。下面其他的就不一一分析了,太多了,小伙伴们自己去查吧。 什么样的PNG图片更适合压缩 常规的png图片,颜色越单一,颜色值越少,压缩率就越大,比如下面这张图: [图片] 它仅仅由红色和绿色构成,如果用0代表红色,用1代表绿色,那用数字表示这张图就是下面这个样子: 00000000000000000 00000000000000000 00000000000000000 1111111111111111111111111 1111111111111111111111111 1111111111111111111111111 我们可以看到,这张图片是用了大量重复的数字,我们可以将重复的数字去掉,直接用数组形式的[0, 1]就可以直接表示出这张图片了,仅仅用两个数字,就能表示出一张很大的图片,这样就极大的压缩了一张png图片。 所以!颜色越单一,颜色值越少,颜色差异越小的png图片,压缩率就越大,体积就越小。 PNG的压缩 PNG图片的压缩,分两个阶段: [代码]预解析(Prediction)[代码]:这个阶段就是对png图片进行一个预处理,处理后让它更方便后续的压缩。说白了,就是一个女神,在化妆前,会先打底,先涂乳液和精华,方便后续上妆、美白、眼影、打光等等。 [代码]压缩(Compression)[代码]:执行Deflate压缩,该算法结合了 LZ77 算法和 Huffman 算法对图片进行编码。 预解析(Prediction) png图片用差分编码(Delta encoding)对图片进行预处理,处理每一个的像素点中每条通道的值,差分编码主要有几种: 不过滤 X-A X-B X-(A+B)/2(又称平均值) Paeth推断(这种比较复杂) 假设,一张png图片如下: [图片] 这张图片是一个红色逐渐增强的渐变色图,它的红色从左到右逐渐加强,映射成数组的值为[1,2,3,4,5,6,7,8],使用X-A的差分编码的话,那就是: [2-1=1, 3-2=1, 4-3=1, 5-4=1, 6-5=1, 7-6=1, 8-7=1] 得到的结果为 [1,1,1,1,1,1,1] 最后的[1,1,1,1,1,1,1]这个结果出现了大量的重复数字,这样就非常适合进行压缩。 这就是为什么渐变色图片、颜色值变化不大并且颜色单一的图片更容易压缩的原理。 差分编码的目的,就是尽可能的将png图片数据值转换成一组重复的、低的值,这样的值更容易被压缩。 最后还要注意的是,差分编码处理的是每一个的像素点中每条颜色通道的值,R(红)、G(绿)、B(蓝)、A(透明)四个颜色通道的值分别进行处理。 压缩(Compression) 压缩阶段会将预处理阶段得到的结果进行Deflate压缩,它由 Huffman 编码 和 LZ77压缩构成。 如前面所说,Deflate压缩会标记图片所有的重复数据,并记录数据特征和结构,会得到一个压缩比最大的png图片 编码数据。 Deflate是一种压缩数据流的算法. 任何需要流式压缩的地方都可以用。 还有就是我们前面说过,一个png图片,是由很多的数据块构成的,但是数据块里面的一些信息其实是没有用的,比如用Photoshop保存了一张png图片,图片里就会有一个区块记录“这张图片是由photshop创建的”,很多类似这些信息都是无用的,如果用photoshop的“导出web格式”就能去掉这些无用信息。导出web格式前后对比效果如下图所示: [图片] 可以看到,导出web格式,去除了很多无用信息后,图片明显小了很多。 结语 以上就是我对png的理解了,写的不好,就像一个支离破碎的中老年,杂乱无章。 想起那年跟初恋分手的原因 是因为怕影响到学习。。。可是分开后成绩也还是很烂,不仅错过了女神,而且到现在也依然一事无成。 如今中年已至,身上背负着巨大的房贷,家里还有嗷嗷待哺的孩子,看着身旁呼噜声轰天熟睡中的妻子,突然也就想开了。 就像鲁迅说的: “爱情就像在海滩上捡贝壳,不要捡最大的, 也不要捡最漂亮的,要捡就捡自己最喜欢的, 最重要的是捡到了自己喜欢的 就永远不要再去海边了。” 。。。。。。 凌晨四点写完文章 不知不觉睡着了 梦回到十年前的那个夏天 我们都笑的很甜 看着你哭泣的脸 微笑着对我说再见 再见 作者:第一名的小蝌蚪 github: 文章会第一时间分享在前端屌丝心路历程,欢迎star或者watch,感恩
2019-03-22 - Lottie-前端实现AE动效
项目背景 在海外项目中,为了优化用户体验加入了几处微交互动画,实现方式是设计输出合成的雪碧图,前端通过序列帧实现动画效果: [图片] 序列帧: [图片] 动画效果: [图片] 序列帧: [图片] 帧动画的缺点和局限性比较明显,合成的雪碧图文件大,且在不同屏幕分辨率下可能会失真。经调研发现,Lottie是个简单、高效且性能高的动画方案。 Lottie是可应用于Android, iOS, Web和Windows的库,通过Bodymovin解析AE动画,并导出可在移动端和web端渲染动画的json文件。换言之,设计师用AE把动画效果做出来,再用Bodymovin导出相应地json文件给到前端,前端使用Lottie库就可以实现动画效果。 [图片] Bodymovin插件的安装与使用 关闭AE 下载并安装ZXP installer https://aescripts.com/learn/zxp-installer/ 下载最新版bodymovin插件 https://github.com/airbnb/lottie-web/blob/master/build/extension/bodymovin.zxp 把下载好的bodymovin.zxp拖到ZXP installer [图片] 打开AE,在菜单首选项->常规中勾选☑️允许脚本写入文件和访问网络(否则输出JSON文件时会失败) [图片] 在AE中制作动画,打开菜单窗口->拓展->Bodymovin,勾选要输出的动画,并设置输出文件目录,点击render [图片] 打开输出目录会看到生成的JSON文件,若动画里导入了外部图片,则会在images中存放JSON中引用的图片 前端使用lottie 静态URL https://cdnjs.com/libraries/lottie-web NPM [代码]npm install lottie-web [代码] 调用loadAnimation [代码]lottie.loadAnimation({ container: element, // 容器节点 renderer: 'svg', loop: true, autoplay: true, path: 'data.json' // JSON文件路径 }); [代码] vue-lottie 也可以在vue中使用lottie [代码] import lottie from '../lib/lottie'; import * as favAnmData from '../../raw/fav.json'; export default { props: { options: { type: Object, required: true }, height: Number, width: Number, }, data () { return { style: { width: this.width ? `${this.width}px` : '100%', height: this.height ? `${this.height}px` : '100%', overflow: 'hidden', margin: '0 auto' } } }, mounted () { this.anim = lottie.loadAnimation({ container: this.$refs.lavContainer, renderer: 'svg', loop: this.options.loop !== false, autoplay: this.options.autoplay !== false, animationData: favAnmData, assetsPath: this.options.assetsPath, rendererSettings: this.options.rendererSettings } ); this.$emit('animCreated', this.anim) } } [代码] loadAnimation参数 参数名 描述 container 用于渲染动画的HTML元素,需确保在调用loadAnimation时该元素已存在 renderer 渲染器,可选值为’svg’(默认值)/‘canvas’/‘html’。svg支持的功能最多,但html的性能更好且支持3d图层。各选项值支持的功能列表在此 loop 默认值为true。可传递需要循环的特定次数 autoplay 自动播放 path JSON文件路径 animationData JSON数据,与path互斥 name 传递该参数后,可在之后通过lottie命令引用该动画实例 rendererSettings 可传递给renderer实例的特定设置,具体可看 Lottie动画监听 Lottie提供了用于监听动画执行情况的事件: complete loopComplete enterFrame segmentStart config_ready(初始配置完成) data_ready(所有动画数据加载完成) DOMLoaded(元素已添加到DOM节点) destroy 可使用addEventListener监听事件 [代码]// 动画播放完成触发 anm.addEventListener('complete', anmLoaded); // 当前循环播放完成触发 anm.addEventListener('loopComplete', anmComplete); // 播放一帧动画的时候触发 anm.addEventListener('enterFrame', enterFrame); [代码] 控制动画播放速度和进度 可使用anm.pause和anm.play暂停和播放动画,调用anm.stop则会停止动画播放并回到动画第一帧的画面。 使用anm.setSpeed(speed)可调节动画速度,而anm.goToAndStop(value, isFrame)和anm.goToAndPlay可控制播放特定帧数,也可结合anm.totalFrames控制进度百分比,比如可传anm.totalFrames - 1跳到最后一帧。 [代码]anm.goToAndStop(anm.totalFrames - 1, 1); [代码] 这样的好处是可以把相关联的JSON文件合并,通过anm.goToAndPlay控制动画状态的切换,如下图例中一个JSON文件包含了2个动画状态的数据: [图片] 图片资源 JSON文件里assets设置了对图片的引用: [图片] 若想统一修改静态资源路径或者设置成绝对路径,可在调用loadAnimation时传入assetsPath参数: [代码]lottie.loadAnimation({ container: element, renderer: 'svg', path: 'data.json', assetsPath: 'URL' // 静态资源绝对路径 }); [代码] 功能支持列表 即使用bodymovin成功输出了JSON文件(没有报错),也会出现动效不如预期的情况,比如这是在AE中构建的形象图: [图片] 但在页面中渲染效果是这样的: [图片] 这是因为使用了不支持的Merge Paths功能 [图片] 因此对设计师而言,创建Lottie动画和往常制作AE动画有所不同,此文档记录了Bodymovin支持输出的AE功能列表,动画制作前需跟设计师沟通好,根据动画加载平台来确认可使用的AE功能。 除此之外,尽量遵循官方文档里对设计过程的指导和建议: 动画简单化。创建动画时需时刻记着保持JSON文件的精简,比如尽可能地绑定父子关系,在相似的图层上复制相同的关键帧会增加额外的代码,尽量不使用占用空间最多的路径关键帧动画。诸如自动跟踪描绘、颤动之类的技术会使得JSON文件变得非常大且耗性能。 建立形状图层。将AI、EPS、SVG和PDF等资源转换成形状图层否则无法在Lottie中正常使用,转换好后注意删除该资源以防被导出到JSON文件。 设置尺寸。在AE中可设置合成尺寸为任意大小,但需确保导出时合成尺寸和资源尺寸大小保持一致。 不使用表达式和特效。Lottie暂不支持。 注意遮罩尺寸。若使用alpha遮罩,遮照的大小会对性能产生很大的影响。尽可能地把遮罩尺寸维持到最小。 动画调试。若输出动画破损,通过每次导出特定图层来调试出哪些图层出了问题。然后在github中附上该图层文件提交问题,选择用其他方式重构该图层。 不使用混合模式和亮度蒙版。 不添加图层样式。 全屏动画。设置比想要支持的最宽屏幕更宽的导出尺寸。 设置空白对象。若使用空白对象,需确保勾选可见并设置透明度为0%否则不会被导出到JSON文件。 预览效果 由于以上所说的功能支持问题会导致输出动画效果不确定性,设计师和前端之间有个动画效果联调的过程,为了提高联调效率,设计师可先进行初步的效果预览,再把文件交付给前端。 方法1:输出预览HTML文件 渲染前设置所要渲染的文件 [图片] 勾选☑️Demo选项 [图片] 在输出的文件目录中就可找到可预览的demo.html文件 方法2:LottieFiles分享平台 把生成的JSON文件传到LottieFiles平台,可播放、暂停生成文件的动画效果,可设置图层颜色、动画速度,也可以下载lottie preview客户端在iOS或Android机子上预览。 [图片] LottieFiles平台还提供了很多线上公开的Lottie动画效果,可直接下载JSON文件使用 [图片] 交互hack Lottie的不足之处是没有对应的API操纵动画层,若想做更细化的动画处理,只能直接操作节点来实现。比如当播放完左图动画进入惊讶状态后,若想实现右图随鼠标移动而控制动画层的简单效果: [图片][图片] 开启调试面板可以看到,lottie-web通过使用<g>标签的transform属性来控制动画: [图片] 当元素已添加到DOM节点,找到想要控制的<g>标签,提取其transform属性的矩阵值,并使用rematrix解析矩阵值。 [代码]onIntroDone() { const Gs = this.refs.svg.querySelectorAll('svg > g > g > g'); Gs.forEach((node, i) => { // 过滤需要修改的节点 ... // 获取transform属性值 const styleArr = node.getAttribute('transform').split(','); styleArr[0] = styleArr[0].replace('matrix(', ''); styleArr[5] = styleArr[5].replace(')', ''); const style = `matrix(${styleArr[0]}, ${styleArr[1]}, ${styleArr[2]}, ${styleArr[3]}, ${styleArr[4]}, ${styleArr[5]})`; // 使用Rematrix解析 const transform = Rematrix.parse(style); this.matrices.push({ node, transform, prevTransform: transform }); } } [代码] 监听鼠标移动,设置新的transform属性值。 [代码]onMouseMove = (e) => { this.mouseCoords.x = e.clientX || e.pageX; this.mouseCoords.y = e.clientY || e.pageY; let x = this.mouseCoords.x - (this.props.browser.width / 2); let y = this.mouseCoords.y - (this.props.browser.height / 2); const diffX = (this.mouseCoords.prevX - x); const diffY = (this.mouseCoords.prevY - y); this.mouseCoords.prevX = x; this.mouseCoords.prevY = y; this.matrices.forEach((matrix, i) => { let translate = Rematrix.translate(diffX, diffY); const product = [matrix.prevTransform, translate].reduce(Rematrix.multiply); const css = `matrix(${product[0]}, ${product[1]}, ${product[4]}, ${product[5]}, ${product[12]}, ${product[13]})`; matrix.prevTransform = product; matrix.node.setAttribute('transform', css); }) } [代码] 进一步优化 看到一个方法,在AE中将图层命名为[代码]#id[代码]格式,生成的SVG相应的图层id会被设置为id,命名为[代码].class[代码]格式,相应的图层class会被设置为class [图片] 试了下的确可以,如下图,因此可通过这个方法快速找到需要操作的动画层,进一步简化代码: [图片] 小结 Lottie的缺点在于若在AE动画制作的过程不注意规范,会导致数据文件大、耗内存和性能的问题;Lottie-web的官方文档不够详尽,例如assetsPath参数是在看源码的时候发现的;开放的API不够齐全,无法很灵活地控制动画层。 而优点也很明显,Lottie能帮助提高开发效率,精简代码,易于调试和维护;资源文件小,输出动画效果保真;跨平台——Android, iOS, Web和Windows通用。 总的来说,Lottie的引用可以替代传统的GIF和帧动画,灵活利用好提供的属性和方法可以控制动画的播放,但需注意规范设计和开发的流程,才可以更高效地完成动画的制作与调试。
2019-03-25 - 微盟小程序性能优化实践
微盟小程序性能优化要分享的内容分为三部分,启动性能加载、首屏加载的体验建议和渲染性能优化。 先讲启动性能加载的性能优化实践,先看启动加载过程的流程: [图片] · 公共库注入 · 资源准备(基础UI创建,代码包下载) · 业务代码注入和渲染 · 渲染首屏 · 异步请求 优化方案 1、控制代码包大小 · 开启开发者工具中的 “ 上传代码时自动压缩 ” · 及时清理无用代码和资源文件 · 减少代码包中的图片等资源文件的大小和数量 · 将图片等资源文件放到CND中 · 提取公共样式 · 代码压缩,图片格式,压缩,或者外联 · 公共组件提取,代码复用 2、 分包加载 分包加载过程流程 [图片] 在开发小程序分包项目时,会有一个或者多个分包,其中没有分包小程序必须包含一个主包,即放置启动页面或者tabBar页面,以及一些分包都需要用到的公共资源脚本。 在小程序启动时,默认会下载主包并且启动主包内页面,如果用户打开分包内的页面,客户端会把分包下载下来,下载完之后再进行展示。 · 分包加载流程 [图片] 使用分包加载的优点: · 能够增加小程序更大的代码体积,开发更多的功能 · 对于用户,可以更快地打开小程序,同时不影响启动速度 使用分包加载有哪些限制: · 整个小程序所有分包不能超过8M · 单个主包/分包不能超过2M 3、 运行机制优化 · 代码中减少立即执行的代码数量 · 避免高开销和长时间阻塞代码 · 业务代码都写入页面的生命周期中 · 做好缓存策略 4、 数据管理优化 · 首屏请求数量尽量不能超过5个,超过的可以做接口合并(node层,服务端都可以处理) · 对多次提交的数据可以做合并处理 接下来和大家聊一聊首屏加载的体验建议和渲染性能优化。 二、首屏加载的体验建议 · 提前请求 异步数据请求不需要等待页面渲染完成。 · 利用缓存 利用storage API对异步请求数据进行缓存,二次渲染页面,再进行后台更新。 · 避免白屏 先展示页面骨架和基础内容。 三、渲染性能优化 · 每次 setData 的调用都是一次进程间通信过程,通信开销与 setData 的数据量正相关 · setData 会引发视图层页面内容的更新,这一耗时操作一定时间中会阻塞用户交互 · setData 是小程序开发使用最频繁,也是最容易引发性能问题的 · 在页面列表中使用懒加载+动态移除非可视区域范围内的内容,让dom小下去 · 耗时比较长的js做到异步,不要阻塞进程(js属于单线程) · 少使用scroll-view,这个组件对性能的影响太大,单纯的只是需要一块区域滚动,可以使用view+css的方式实现 · 在页面频繁滚动触发回调函数,会导致页面卡顿,这时必须和防抖动函数或者节流函数相结合做一些处理 · 页面中的图片可以使用懒加载的方式(添加lazy-load属性,只针对page与scroll-view下的image有效) · 页面跳转要做一下限制,如果页面快速点击会出现跳转多次的情况 避免不正当的使用setData · 使用data在方法间共享数据,可能增加setData传输的数据量。data 应该仅仅包含与页面渲染相关的数据 · 使用setData 传输大量的数据,通讯耗时与数据量成正比,导致页面更新延迟 可能造成页面更新开销增加。所以setData 仅传输页面需要的数据,使用setData 的特殊Key 实现局部更新 · 短时间内频繁调用setData (操作卡顿、交互延迟 阻塞通信、页面渲染延迟),对连续的setData 调用进行合并 · 后台进行页面setData (抢占前台页面的渲染资源) 例如 活动定时器 再页面切入后台时应该将关闭 避免不正当的使用onPageScroll · 只在必要的时候监听pageScroll 事件 · 避免在onPageScroll 中执行复杂的逻辑 · 避免在onPageScroll 中频繁调用setData · 避免频繁查询节点信息(SelectQuery) 部分场景建议使用节点布局相交状态 · 监听( IntersectionObserver) 替代 使用自定义组件 在需要频繁更新的场景下,自定义组件的更新只在组件内部进行,不受页面部分内容的复杂性的影响。 使用体验评分功能 在开发过程中使用体验评分可以测试出代码中一些需要优化的点,准备定位到影响性能的原因,很大程序提高页面的性能。
2019-03-22 - Three.js 文字渲染那些事
THREE.js开发的应用运行在iphone5下发现有些时候会崩溃,跟了几天发现是因为Sprite太多频繁更新纹理占用显存导致的。通常解决纹理频繁更新问题就要用到one draw all方法,放到纹理上就是把所有纹理图片生成一张大图片的方式 一、阻止纹理重复上传 我们需要一张大纹理,先将所有的内容绘制在大纹理上,需要显示局部纹理的时候通过纹理坐标控制去大纹理上取图像。那么这个时候问题来了,THREE.js内部实现方式是将Texture与图片、纹理坐标绑定,即使为所有的Texture对象设置同一张图片,THREE.js仍然会将每个Texture中的图片上传给GPU。每次上传一张大纹理严重阻塞UI渲染进程。[图片] [图片] 首先要解决的是让这张大纹理值上传一次。 这个问题需要我们对THREE.js源码进行深入了解,可以看到setTexture2D函数中有一个properties变量,这个变量是一个WebGLProperties类型的变量,而该类型存储各种东西:Texture、Material、RenderTarget、Object的buffers等。我们继续深入该类的源码,发现get方法会根据对象的uuid来获取相关WebGL属性,比如gl.createTexture、gl.createBuffer创建的各种缓冲区。 [图片] 对应Texture得到的webgl属性如下,其中__webglTexture就是对应的纹理图片创建的缓冲区对象。 [图片] 那么我们可以来一个取巧的方法,将所有纹理的的uuid都设置唯一,那么THREE.js只会对第一个Texture的纹理进行上传,后面的texture对象取到的都是第一个的properties,这样就能避免纹理重复上传。 [图片] 二、建立纹理索引 我们需要自己维护一套索引关系,通过这套索引关系得到每个贴图在大纹理中纹理坐标。这里要为每一个poi记录它的起始位置和区域范围,其中要用到canvasContext.measureText来测量文本的宽度,文本高度可以直接根据fontSize取得。 [图片] 同时索引建立完毕后,需要计算每个poi区域在全局纹理中的纹理坐标范围: [图片] 要注意的是,这里纹理坐标的原点在左下方,有时候原点在左上方。建立索引代码如下 [图片] 三、局部更新 上述方案虽然能够避免频繁上传纹理,但是需要每次将需要绘制的内容准备好,当有内容需要更新时,还是需要重新上传整个全局纹理,反而使得性能下降巨大。经过查阅资料后发现webgl中有一种局部纹理更新技术,简单来说先在内存中开辟一块的纹理区域,将所有内容绘制在这张全局纹理中,每次有更新时,只需要更新它的一个局部区域即可。 但是这里要解决的问题是THREE.js并没有提供局部纹理更新的方式,也没有相应的自定义接口,那么这时候就需要我们自己来处理了。 这里自定义一个Texture的子类 [图片] 开辟一块内存区域 [图片] 在需要的时候动态更新局部纹理,其中src这里是ImageData对象[图片] 具体代码可以参考这里,我这里也是基于它来定制的。 https://github.com/spite/THREE.UpdatableTexture 原文作者通过更改THREE.js源码的方式实现,而我是直接把下面这个函数拷贝到这个子类中 [图片] 四、高清屏的大坑 现在我们的方案是,先在gpu中开辟一块全局纹理区域,然后绘制时将poi绘制到一张与全局纹理同样大小的canvas上,然后从canvas中调用createImageData来获取像素,将像素局部更新到gpu中。那么在pc上我们得到的结果很完美。 [图片] 然而放到移动端上后,我们得到的结果是:[图片] [图片] TMMD中间那块哪去了!找了大半天发现问题出现在高清屏上,挡在高清屏上绘制canvas上时,我们通常会做一些高清处理,比如四像素绘制一像素。 我们做高清处理的方式是利用radio*radio设备像素绘制一css像素,看起来是css像素的大小,但实际在浏览器内部,看起来css上一像素实际在canvas里的像素是radio * radio(radio代表window.devicePixelRatio) [图片] 但实际上在浏览器内部绘制canvas图像的单位是设备像素。那么如果我们还以上面的rectW、rectH来获取像素的话,我们得到的这部分像素并不是这个poi真正占有的像素数目。 [图片] 所以,问题就来了我们需要在gpu开辟的全局纹理的单位跟canvas中获取像素的单位要保持一致,我们统一使用设备像素。 [图片] 我们对canvas也不用使用style来设置样式宽高了。 [图片] 那么获取poi图像的真正像素范围时: [图片] 所以利用getImageData取像素时候,就要小心取到真正的像素区域,(startX * radio,startY * radio)- (poiRectW * radio, poiRectH * radio);否则某些像素就会被丢弃掉,这部分像素才是浏览器真正使用的设备像素。 现在在移动设备上能够获取正确的高清label啦! [图片] 五、局部更新引起的新问题 当全局纹理被占满时候,在继续绘制poi,这时候新的poi区域需要更新到gpu中,那么也就带来了新的问题,在gpu中的纹理还保持着之前的像素,而新的poi会覆盖这部分区域,但有时候往往会与之前的文字叠加起来,效果如下: [图片] 可以看到新更新的poi,在计算纹理坐标时候,有一部分像素包含了其他poi的像素。这个问题是因为新poi的区域刚好叠在了先前poi的边界上,那么我们只要给新的poi加一点buffer,这个buffer是白素透明区域,buffer会把之前的poi像素覆盖掉,而我们计算纹理坐标时,只取poi的边界,那么就可以解决这个问题。 [图片] 那么首先绘制的时候就要保留buffer [图片] 上传的时候使用buffer [图片] 计算纹理坐标时,排除buffer [图片] 六、局部更新带来的性能问题 根据目前的结果,局部更新能后解决crash的问题,但是带来了严重的性能开销,与同事应用局部更新提升性能的结果相反。这个问题还要继续跟踪。 目前发现问题是因为使用了getImageData来获取数据,然后传递到gpu中,非ios设备用这种方式有时候getImageData的开销特别大,而ios设备相对好一些。 测试发现非ios设备直接上传一张大纹理的效果反而比getImageData这种方式更好。但是依然不如之前上传多个canvas的性能。而在iphone5的测试机和iphone6的机器上性能比之前直接上传多个canvas的方式好一些,且没有崩溃问题。但是在岳阳的iphone6 plus 16g内存的手机上发现用具局部纹理更新性能很差,而且经常崩溃。 后来发现原因是因为,虽然getImageData在IOS上性能好过非IOS设备,但性能开销仍然比较大,所以当场景中POI很多时,仍然会引起主线程卡顿,甚至计算太密集引起浏览器崩溃。其中层尝试使用cesium方式,每个poi创建新的canvas,将canvas进行局部上传,本以为这种方式不需要getImageData会更快一些,然而实践发现每次创建canvas设置参数的过程更耗时。 最终的方案是仍然使用getImageData,但是将getImageData的过程分块处理,每50ms处理一次,分块放到场景中,这样就解决密集计算引起的崩溃问题,虽然增加了控制成本,但是能够有效解决IOS崩溃问题。有趣的是在安卓上getImageData方式开销很大,即使分块也不适合,而且安卓用一张大纹理的方式来处理,会发现很多POI绘制效果不好。 [图片] 最终方案是,IOS使用getImageData局部纹理+分块加载方式绘制POI。安卓使用POI独立创建canvas+全量加载方式。(安卓不适用分块加载,是为了尽快把所有POI呈现给用户) 七、文字黑色描边问题 这个问题自始至终困扰我好久一直没找到黑边的原因; [图片] 将原始的canvas导出后发现这是因为原始的canvas就有一层边界 [图片] 曾经怀疑是minFilter的设置不对在pc端纹理使用NEARESTFilter方式取值发现的确能够消除黑边,然而移动端仍然会出现黑边,最后使用颜色混合公式解决问题。 gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA); 在Three.js中需要设置SpriteMaterial的blending为CustomBlending 八、颜色混合新问题 但是使用上述方式同样引来新问题,设计反映poi的icon四周被裁切掉, [图片] 看着没问题是吧,设计同学截了图之后放大了20倍。。。。。 [图片] 刚开始我确实以为这是webgl渲染问题,后来仔细考虑了下这外圈白色的由来(遇到问题还是得静下心分析)。 原因是设置了blendFunc(SrcAlphaFactor,OneMinusSrcAlphaFactor)导致有些icon周围的像素alpha比较低 [图片] 颜色混合后增加了target的颜色分量,导致最终这些区域的颜色范围接近255,所以泛白。从而把原来图片四周有切边的问题充分暴露出 解决方法是设置alphaTest,如果原始纹理的alpha小于这个值则直接discard。最终得到的效果是: [图片][图片][图片] 九、TextureAlta问题 前面因为sprite的旋转中心只能放在sprite纹理区域的中心所以,上面做了很多冗余纹理,有很多空白区域,目前改造了Sprite加了pivot可以动态改变选中中心点,改变后IOS下纹理的使用率提升了60%,安卓下因为是单个纹理上传所以,需要保证纹理的大小是2的n次方,纹理的浪费率降低了50% 上述问题虽然解决了崩溃问题,但是实际使用中每个poi都要getImageData和texSubImage2D这个方法,造成单个poi耗时基本在25ms(iphone5 8.4.4);虽然上面使用setTimeout 50ms分块方式上传,但是如果poi过多比如1000多的停车场,这样会导致停车场数据需要50s才能完全显示出来。这次优化的方案是等待所有poi图片拿到后,绘制所有的poi把画布调用一次getImageData和一次texSubImage2D上传到gpu,同时下次更新时,只会增量一次性上传更新。 [图片] 十、Frstrum增量更新 原来是在每一级别缩放时把所有的poi都生成好,现在的做法是只生成视锥体中能看得到的poi,然后在每次OrbitControl出发change事件时根据视锥体判断poi,做去重后增量更新 [图片] 目前还是有些问题,有时候会碰到视锥体中的poi很少,可能是判断问题,后续会加入空间索引,根据索引和视锥体结合起来做增量更新 后续使用发现在停车场这种大数据的poi全部加载到地图下,使用这种方式每次都要做去重处理,性能开销很大,处理方式是使用{}做hash代替数组includes方法,结果发现性能提示很大,原来3600个节点每次去重处理在iphone 16g 10.3.3上性能基本在28帧每秒,经过优化后数据帧率达到50+(主流iPhone7fps60);iphone5 16g 8.4.1 性能在24左右优化后帧率在44+,安卓华为荣耀9优化前25帧,优化后 40+ [图片]安卓之所以不适用IOS的绘制方式,是因为这种在安卓上的绘制效果不理想,被设计挑战 安卓后面也做了一些优化,之前安卓是每次都会重新创建canvas并上传至gpu纹理中,导致使用视景体增量更新poi时,性能有所下降,后来每一层中的poi都根据icon、文字组成key缓存起来,并且缓存纹理,不但阻止canvas的重复创建,还阻止canvas重复上传至gpu纹理(three中使用同一uuid),使用该方案荣耀9的fps达到50+ 十一、text glyphs该方式还有待尝试 https://webglfundamentals.org/webgl/lessons/webgl-text-glyphs.html 十二、真正解决POI文字黑边问题 由于要做poi渐变出现效果,但是因为之前处理黑边问题用的是颜色混合的方式,所以当动态改变透明度时,受颜色混合影响往往是文字颜色先消失,剩下透明度部分还存在显示先过很差。所以要实现渐变效果,不能使用颜色混合方法,但不适用颜色混合就会有黑边问题,所以要从源头上解决黑边问题。(看到最后会发现有残影) [图片] 那么思考黑边到底是怎么产生的,这与webgl中纹理插值的颜色有关,有的设备像素取纹理时有不同的方案,但一般情况下纹理像素和设备像素都不是一一对应,所以有插值取值问题。 [图片] 这是正常情况下利用canvas绘图时背景颜色不设置,那么可以看到我们绘制出来的canvas的确有一层奇怪的黑边。当设备取到纹理中这些边界时就会产生黑边。那么就要思考怎么不让它取到这层黑边,这个问题想了好久曾经试过用opacity过滤,发现不能解决问题。 [图片] 有一天突然想到如果canvas背景为有颜色,每个设备像素都能取到颜色,那么就不会有这个问题。所以我们能否通过改一下canvas的背景颜色同时有通过透明度过滤掉不合格的像素?最终发现这个问题还真可以。 首先在绘制时将canvas背景设置为白色,但是有很低的透明度 [图片] 这时候canvas绘制出来的效果是 [图片] 可以看到已经没有黑边了,那么这时候设备像素永远不会取到黑色边界,也就彻底解决了黑边问题。 那么就可以利用tween来做动画了
2019-03-14 - 我们是怎么进行前端工程化的
随着业务的不断发展,企鹅电竞的前端规模在不断的疯狂膨胀。越来越多的项目被建立起来,越来越多的体系被架构出来,越来越多的代码变的越来越难维护。我们在不断创造新代码的时候,不断在思考,为啥我们的项目看起来这么的野?我们不禁反复思考,我们干了那么久的前端工程师,我们他喵的到底在干个啥?很自然,有人就开始说,我们要推动前端工程化,那么我们在讨论前端工程化的时候,我们到底在讨论啥? 文章将阐述我们在前端工程化的一些思想和做的一些工具,文章并不主要介绍和推广我们的工具,我们更多的介绍的是我们业务发展过程中,遇到各种问题的时候,我们的思考和想法,以及我们采用的对策和措施。当然,如果大家有更好的想法,希望大家能贴在讨论区,我们一起讨论一波。 什么是前端工程化 我们在思考这个问题之前,我们先思考一个更大的问题,什么叫软件工程化?我依稀记得,在我还有头发的时候,在梧桐树旁的十五栋楼上,上课(睡觉)的日子里,那满脸褶皱的软件工程老师是这么说的: 将系统化的、严格约束的、可量化的方法应用于软件的开发、运行和维护,即将工程化应用于软件。 没记错的话是应该是清华大学出版社出版的《软件工程》这本书里的定义,回去如果我翻到了是哪本书,我再贴出来。不管,这里先套用一下,前端的工程化是不是应该是指: 将系统化的、严格约束的、可量化的方法应用于前端页面的开发、运行和维护,即将工程化应用于前端开发。 所以,当我们要讨论前端工程化的时候,是不是讨论的这么几个基本问题: 如何进行系统化的前端开发、运行和维护? 如何对前端的开发、运行和维护进行严格约束? 如何对前端的开发、运行和维护进行量化? 为了方便的进行行文,在此约定,下文的开发将指代开发、运行和维护。所以,在后面我们将就这三个问题,来谈谈我们对于前端工程化的思考和实践。 大部分情况下,我们在讨论前端工程化的时候,我们其实只讨论CI/CD和脚手架,但是软件开发并不是只有CI/CD,在不同的开发阶段,甚至在不同的项目阶段,面对不同的问题,我们都要采用不用的方案方法。里面有共性,也有个性。共性的问题,我们可以参考社区的解决方案,但是个性的问题,则需要我们自己去探索和思考。 为什么要进行前端工程化 ok,按照三段论的基本套路,我们应该要来讨论一下,为啥要进行前端的工程化? 如果你非要我吹逼的话,我会这样吹:世界的本质是熵增的,而生命的本质是熵减的。为了体现我们作为生命的伟大价值,我们将坚定不移的将负熵进行到各个角落。 说一个比较通俗的答案:当前的开发环境已经复杂到影响我们开心的进行下一步的开发,我们需要对当前的开发环境进行一次整理,而这个整理的过程,就是前端工程化的过程。 那我们现在的开发环境到底有多复杂? 截止到我写这篇文章的这一刻,我们一共要负责9大系统。各系统由于立项时间,定位,运行环境,产品要求等各种乱七八糟的原因,使用了不同的技术架构,不同的技术框架,不同的开发方式,不同的发布方式。更恐怖的是,由于技术的不断发展,框架的推陈出新,我们的项目还在不断的复杂中。。。 分别为:电竞官网(可以看直播的那种),电竞ipad版本(可以看直播的那种),电竞移动端(android 和 ios两端各70+场景页面,还有一堆的分享页面),电竞移动助手端(android和ios双端),电竞pc助手端(主播管理端),电竞运营管理端(内部数据管理,各种个性化需求和数据分析工具),电竞开放平台(提供给外部合作伙伴进行数据管理),公会管理系统(用于辅助公会进行主播管理),无以计数的活动页面。 所以,每次当有新同学加入我们的时候,我们都必须要提供多位资深员工,手把手的说明一下,我们项目到底要怎么跑起来,有的项目因为实在太复杂,已经快要跑不起来了:D 如何进行前端工程化 面对日益复杂的业务场景,日益庞复的代码逻辑我们开始思考我们到底要如何解决我们的前端工程化问题。根据前面我们的思考,我们将在以下三个方面来谈谈我们对于前端工程化的思考: 如何进行系统化的前端开发、运行和维护? 对于这个问题,我们首先思考的是什么是系统化?我们要怎么做才能称的上是系统化? 抛开那些高大上的术语,我们的解是,将日常开发的各个步骤整化为固定的流程,当我们在日常开发中,面对各种情况都会有固定的一二三四五的时候,就说明我们完成了系统化。 那么,我们每天都要做哪些工作呢? 收集一下大家的日常工作,都有这么一堆: [图片] 这些庞杂的工作有没有规律? 我们开始对我们的工作流程进行了一次收归。其实如果按照需求开发的基本流程,我们可以简单的把各种毛细的工作收归为以下四个阶段: [图片] 在不同的阶段,我们需要做不同的事情,而且,我们发现这些事情是重复的,或者说,是每次做需求的时候在相同的阶段都需要重复的。 在相同阶段里,做重复的事情,我们是不是能够自动化起来? 当然可以,我们开发一个流程管理工具是不是就可以了!在开发之前,我们看了一下公司内和业内在这个方向上的实践。这里列几个公司内宣传的比较好的方案:weflow、feflow、leah-cli 当然社区也有很多很不错的方案比如vue-cli,还有ng-cli等等,完成度相当高,非常的靠谱和好用。 但是,我们不难发现,这些cli也好,flow也好,都是基于工具的思路去实现的,和我们的思路并不一样。我们希望是能基于流程去实现的,而且能贯穿整个开发流程,形成一个闭环。让我们的同学能够系统化的去一二三就好了,不要思考太多有的没的。所以,我们基于上述的开源实现和思路,自己实现了一套新的方案:pgg-flow。 pgg-flow是以开发周期为核心,辅以各种工具库。举个栗子: 在需求的准备阶段,不管三七二十一,我们先来个pgg-flow start,然后开发分支就自动从主干拉出,并将本地环境切入到该分支。 在需求的开发阶段,来个pgg-flow dev,代码就跑起来了,dev-server,本地构建就跑起来了。 在需求的测试阶段,来个pgg-flow test,代码就提交远端,触发服务端ci/cd,测试环境立即生效就绪。 在需求的发布阶段,来个pgg-flow release,直接就触发ci/cd ,发起merge request(用于code review),生成ars单,触发发布系统流程,触发codecc的自动代码审查,并回收占用的各种资源。 我们希望开发同学,能够不用关心不同模块间的框架和依附系统差异。只要知道以上四个命令就能够完成所有的开发流程。 我们在CI/CD的思考和实践详情参考我们电竞小哥思名的这篇文章《weex/vue 应用的服务器端构建之路》,利用内部基于docker的orange-ci和发布系统,实现git工作流到外网发布的一条龙打通。 当然,在这个基础上,我们可以继续去支持,new page, new api等等更具体的业务辅助工具。通过如此,我们磨平了不同框架体系间的差异,大家可以很开心的进行需求开发了。但是,很快大家又有新问题来了,不同的框架中,不同的开发体系在相同的事情上,使用了不同的技术选型。比如,pc的同学自己基于fetch进行了封装,但是在h5上使用了axios来进行请求的。 这个主要是很多历史遗留,开发pc的和开发h5的在很长一段时间内都不是同一拨人,虽然在开发相同或者说相近的业务,但是,技术选型并不一致,这其实是之前遗留的一个很大的坑。 不管问题之前如何产生的,但是,就事论事,如何让大家的技术选型和技术实践相贴近? 如何让大家的技术选型和技术实践相贴近? 对于这个问题,我们的思考是这样的: 大家选型和实践有较大差异的原因是没有现成的方案,或者不知道有现成的方案。所以我们是从三个角度来解决这个问题的: 提供横跨框架场景的公共库,包含完整的基础方法 提供自动化更新的文档,帮助大家更好的了解基础方法有哪些 提供完善的脚手架,减小使用代价 公共库的管理是基于tnpm(公司内部的npm镜像)。 文档自动化,可以参考我们电竞一姐的这篇文章《企鹅电竞前端公共库文档自动化建设》。文章详细的介绍了,我们是如何使用蓝盾来进行文档自动化管理的。 脚手架的提供,为的是进一步减少重复工作量。 当我们完成以上的工作的时候,我们发现,我们已经基本实现了,系统化的进行前端的页面需求开发。当同学们开始一个需求的时候,可以很容易的借助既成的体系,进行快速的开发迭代。当然,现在这个工作模式还比较稚嫩,在很多细节上的地方还需要继续打磨。 为啥要采用相同或者相近的的技术实践?有个很大的原因是为了能够将优化的工作更容易的扩散出去。 比如,通过webpck4+babel7 的升级,一下就减少了移动端几乎所有页面的40%体积,具体可以参考思名的《weex/vue 应用的webpack4 升级优化之路》,《真·一行代码优化30%的JS bundle体积》。我们觉得优化应该倾向于底层,脱离于业务,只有如此,才能实现更高层级的复用。 如何对前端的开发、运行和维护进行严格约束? 在实现了系统化的进行开发和迭代之后,我们不得不开始面对开发质量的问题。如何对开发质量进行有效的约束和评估,这并不是一个简单的问题。我们最早的方案使用了业内比较通用的方案: 使用本地的eslint进行代码管控 通过eslint + husky + lint-staged来搭建一套基于本地的代码及风格管控,这个一个很棒的方式。业内对于这个方案也有很多很好的实践,大家自行百度一下就好。但是,这个方案对于我们而言,并不能解决所有的问题。 我们希望能对代码质量有一个整体的认识,我们希望能够对于代码质量的监控有一个比较好的客观衡量。 我们希望这个监控体系能够不阻塞大家正常的业务交付。 我们希望这个体系能够从各个维度对我们的代码进行合理的分析。 很明显,本地化的eslint根本不能满足我们对于代码质量监控的要求,就算,你在这个基础上增加prettier来做你的代码格式化的管理工具。于是我们开始寻找,是否有能够有一个平台来帮助我们完成这个工作。 如何优化的监控的前端代码质量 这个问题,我们最后使用了codecc+蓝盾的解决方案。将质量监控体系建设在远端,能够有效的对代码进行监控,同时,也能很容易的让更多的同学0成本的使用起来。有兴趣的话,大家可以参考我们电竞一姐的《如何优雅的监控前端代码质量》。 codecc是公司内部的在线代码自动审查平台,可以实现在线的代码eslint,coverity,重复性,复杂度等各种纬度的代码审查,能够将审查结果进行很好的分析和数据图形化。蓝盾是公司内部的流程集成系统,可以流水线化的实现代码的自动化构建和二三级子服务的连接。 通过蓝盾和codecc的体系,我们发现并解决了很多的问题,但是,很快,我们又发现了,单纯的依靠系统自动化的检查和检测,并不能解决所有的问题。有些设计思路上的问题,我们没办法简单的通过自动化的代码审查来解决。那么是否可以在必要的时候,引入人工的代码审查来进一步的提高代码质量。 在合适的时候进行code review 不能否认,cr是一个很重要,但是很烦人的工作。如何让大家对代码在合适的时候,进行cr,这个问题让我们思考了很久。我们尝试过很多的方案: 在发布流程中通过人工要求增加cr环境 以代码小课堂的形式,进行cr 在开发前后以技术评审的形式来进行cr 但是,这些方式有好有坏,但是,都不能够像水一样的融入到我们的开发生活中来。我们觉得不自然。我们最后的思考是: 将code review和git 工作流和ci相互融合,通过ci工具来自动触发。利用cli来封装各种常用操作,将原本复杂的命令操作,化繁为简。 简单的介绍一下我们的方案: [图片] 为了能够使这一套工作流变的更智能,我们利用node-git构建了一个轻便的git工具。具体的实现方式可以参考我们电竞小哥锦亮的《基于NodeGit的Git工作流封装》。 我们之前讨论了很久的代码质量的管控,但是除了代码质量,还有一个很大的部分是我们的行为。我们在开发过程中,有很多的行为会改变远端的代码状态和共用资源(测试服务器)的状态。我们怎么才能对这个状态进行一个很好的管理? 如何监控代码的过程? 在电竞小哥的《weex/vue 应用的服务器端构建之路》一文中," 通过工蜂的webhook监控提交" 这段内容详细的介绍了我们的实践。除了这部分介绍的实践,我们还搭建了一个node sever用来记录和查询,我们的测试服务器的使用情况。 简单的说,我们实现了各大系统的过程内容收归,各个不同平台的流程数据能够统一的以企业微信机器人的形式主动推送给你。足不出户,可知天下事。 通过完善的自动化代码监控 + 人工cr的双检查,我们简单的构建起了我们的代码质量监控体系。 对于前端而言,轻薄短小快是基本要求,但是做到轻薄短小快并不是一件简单的事情,更复杂的事情,我们有这么多的工作要做,居然还有一群人在后面用排期表抽我们的屁股,这个就让我们很难受了。 如何对前端的开发、运行和维护进行量化 对工作进行量化是一件很复杂,很难把控的事情。我们在这方面并没有很好的实践。对于如何正确的评估工作量, 我们有一个不怎么成熟的想法: 所有工作量应该包含以下几个部分: 技术方案预研所花费的时间 技术方案落地所耗费的时间 自测的时间 联调的时间 产品修改需求的时间 调试解决问题的时间 在具体的时间分配上,根据各个不同的人自我确定和分配。在具体的实践过程中,我们建议保留一定的buffer,你总会在你的代码上线前,发现这样或那样的问题的。 除了上面的建议,我们还有一些比较通用的建议: 比如实现从设计到结果交付的组件化管理,组件化一定要从设计语言开始,这样才能从根本上减少各个流程的耗时,和实现标准化。 技术预研要早于产品开发。预研一代,落地一代,维护一代。永远不要在业务实际开发中使用未经预研的框架和技术体系,会引入极大的项目风险。 使用graphql这种前端为核心的接口配置体系,能够极大的减少因为接口联调带来的不稳定耗时。 以上,简单的介绍了一下,我们现在的前端工程化的体系进程,和我们的一些思考。很多观点还比较的幼稚,很多实现还特别的落后,希望各位大佬,能够给我们提供更多好的思路和方式方法,大家一起讨论和思考如何更优雅的进行完善的前端工程化。 最后打一波广告: 企鹅电竞——腾讯游戏官方直播平台,欢迎UI开发和前端开发同学加入 https://hr.tencent.com/position_detail.php?id=48298
2019-03-11 - 小程序 地区选择 四联动
----------在线API版 代码片段 https://developers.weixin.qq.com/s/b56aoYm77p4e 请将 https://fts.jd.com/ 加入合法域名 ------离线数据 版 https://developers.weixin.qq.com/s/cn676Ymb7x4o 由于数据1M多 不能上传代码片段 所以压缩 导入 解压 libs/area.zip 地区数据来源于 京东 有港澳台 钓鱼岛 等等 大部分地区只能三联动
2018-12-27 - 微信自定义标题栏
wx-nav-bar小程序自定义标题栏 APIdate-picker【props 】名称描述[代码]theme[代码][说明]:主题,支持 light/dark, 默认light,light主题返回按钮、home按钮、标题颜色为黑色,dark为白色[代码]title[代码][说明]:标题栏title[代码]backEnable[代码][说明]:是否展示返回按钮,默认true[代码]homeEnable[代码][说明]:是否展示主页按钮,默认false[代码]backgroundColor[代码][说明]:标题栏背景色,自行配合theme使用,默认#fff[代码]backProxy[代码][说明]:是否将点击返回按钮的事件冒泡给上层,默认false。如设置为true,则会trigger一个event:onNavigateBack给上层组件,请自行拦截处理返回事件使用可参见 demo 截图[图片] [图片] [图片] 开发工具GitHub 地址 https://github.com/yautah/wx_nav_bar
2018-12-25