菠萝油王子

菠萝油王子 查看完整档案

深圳编辑中国民航大学  |  民航运输 编辑  |  填写所在公司/组织填写个人主网站
编辑

前端开发,面向api编程。

个人动态

菠萝油王子 赞了文章 · 8月18日

《Flutter in action》开放下载!闲鱼Flutter企业级实践精选

复制链接到浏览器https://yq.aliyun.com/download/3792?utm_content=g_1000081730
下载。

闲鱼是国内最早使用Flutter的团队,也是Flutter业务线渗入最深的团队之一。
现在承载亿级流量的闲鱼将多年最佳实践经验整理成册,《Flutter in action》 正式面世!

√ Google Flutter产品经理强烈推荐 !

√ 超全Flutter企业级实践指南 !

Flutter开源工具大集合

闲鱼Flutter应用框架Fish Redux、开发利器AspectD、FlutterBoost等一众开发工具正式开源,现在《Flutter in action》一次帮你找全了。
开源地址+技术解析,手把手教你Flutter应用框架和混合开发原理!

闲鱼Flutter企业级应用实践

随着无线,IoT的发展,5G的到来,移动研发越发向多端化发展。传统的基于Native+Web+服务端的开发方式,研发效率低下,显然已经无法适应发展需要。

而Flutter是Google开源的跨端便携UI工具包,除了具有非常优秀的跨端渲染一致性,还具备非常高效的研发体验,丰富的开箱即用的UI组件,以及跟Native媲美的性能体验。由于它的众多优势,也使得Flutter成为了近些年来热门的新技术。

在这本书中将详细讲解闲鱼Flutter&FaaS云端一体化架构,和闲鱼基于Flutter的架构演进与创新,学习一套全面的Flutter架构应用方案。

混合开发实践指南

你将在这里看到闲鱼技术团队利用Flutter技术改造和上线复杂业务的混合工程改造实践、抽取Flutter依赖到远程的实现细节、以及使用Plugin桥接获取设备信息、使用基础网络库等混合开发实践指南。

这些实践遍布闲鱼各大业务线和应用场景,为你使用Flutter打造自己的研发体系探索一条实践之路。

Flutter的深入进阶教程

在获得开源工具与开发实践指南后,你还将在本书中学到Flutter的更多应用场景。

它们包括了如何低成本实现Flutter富文本、设计一个高准确率的Flutter埋点框架、Flutter外接纹理、可定制化的Flutter相册组件等等深入进阶内容。

这可能是一本最全面的Flutter实践开发手册了!


本文作者:闲鱼技术

阅读原文

本文为云栖社区原创内容,未经允许不得转载。

查看原文

赞 4 收藏 2 评论 0

菠萝油王子 赞了文章 · 6月19日

Vue 中的Mixins

1. Mixins的使用场景

页面的风格不用,但是执行的方法和需要的数据类似时,可提取公共部分混入使用

2. 使用方法
提炼出公用钩子,数据,方法
image.png

在需要使用mixins的组件中挂载mixins, 即可直接使用mixins的数据和方法
image.png

3. Mixins的特点

1 方法和参数在各组件中不共享
2 **数据对象**
  mixin的数据对象和组件的数据发生冲突时以组件数据优先。
3 **钩子函数**
  同名钩子函数将会混合为一个数组,都将被调用到,但是混入对象的钩子将在组件自身钩子之前调用。
4 **值为对象的选项**
  值为对象的选项,例如`methods`,`components`和`directives`,将被混合为同一个对象。两个对象键名冲突时,取组件对象的键值对。

4. 结论

混合对于封装一小段想要复用的代码来讲是有用的。对你来说它们当然不是唯一可行的。混合很好,它不需要传递状态,但是这种模式当然也可能会被滥用。所以我们还是需要仔细斟酌使用
查看原文

赞 1 收藏 0 评论 0

菠萝油王子 赞了文章 · 5月9日

JavaScript 装逼指南

如何写JavaScript才能逼格更高呢?怎样才能、让别人一眼看出你不简单呢?是否很期待别人在看完你的代码之后感叹一句“原来还可以这样写”呢?下面列出一些在JavaScript时的装逼技巧,也可说是非常实用的写法。

转Boolean类型

这个较为常用。

!!'a'//true

通过两个取反,可以强制转换为Boolean类型。

转Number类型

String转化为Number;日期输出时间戳。

+'45'//45  
+new Date//13位时间戳

会自动转化为Number类型的。日期取时间戳不用new Date().getTime()。

parseInt

parseInt这个函数太普通了,怎么能装逼。答案是~~,这种方法还可以将字符串转换成数字类型。向下取整。

~~3.14159//3  
~~'5.678'//5  
-2.33 | 0 //-2  
2.33 >> 0 //2

原理是~是一个叫做按位非的操作,会返回数值的反码,两次取反就是原数。|为位运算符,两个位只要有一个为1,那么结果都为1,否则就为0。>>运算符执行有符号右移位运算。都是二进制操作。 原因在于JavaScript中的number都是double类型的,在位操作的时候要转化成int。

短路表达式,弃用if-else

反面示例:

if () {  
 // ...  
} else if () {  
 // ...  
} else {  
 // ...  
}

用 || 和 &&来简化if-else 。有时候用 !! 操作符也能简化if-else模式。例如这样:

let a = b || 1;//b为真,a=b;b为假,a=1;  
let c = b && 1;//b为真,c=1;b为假,c=b;  
// 使用!!符号  
let isValid = !!(value && value !== 'error');

“!”是取反操作,两个“!”自然是负负得正了。比较常用的是||。

另外一种undefined

let data = void 0; // undefined
void 运算符 对给定的表达式进行求值,然后返回 undefined。

那为什么要用void 0,不直接undefined呢? undefined在javascript中不是保留字。因此在IE5.5~8中我们可以将其当作变量那样对其赋值(IE9+及其他现代浏览器中赋值给undefined将无效)。采用void方式获取undefined更准确。

保留指定位数的小数点

let num = 2.443242342;  
num = num.toFixed(4); //"2.4432"

注意, toFixed() 方法返回的是字符串而不是一个数字。

单行写一个评级组件

let rate = 3;  
"★★★★★☆☆☆☆☆".slice(5 - rate, 10 - rate);//"★★★☆☆"

slice() 方法可提取字符串的某个部分,并以新的字符串返回被提取的部分stringObject.slice(start,end)

金钱格式化

//正则  
let cash = '1234567890'  
cash.replace(/\B(?=(\d{3})+(?!\d))/g, ',');//"1,234,567,890"  
//非正则的优雅实现  
function formatCash(str) {  
 return str.split('').reverse().reduce((prev, next, index) => {  
 return ((index % 3) ? next : (next + ',')) + prev  
 })  
}  
formatCash(cash);//"1,234,567,890"  
​

非正则的方法,先把字符串转成了数组,反转了一下变成了[0,9,8,7,6,5,4,3,2,1]。再对新的数组进行reduce操作,数组元素位置除3取余,是3的倍数的位置就增加’,’,最后返回累加的字符串。

标准JSON的深拷贝

let a = {  
 a1: 1,  
 b1: { c: 1, d: 2 }  
};  
let b=JSON.parse(JSON.stringify(a));  
b;//{a1: 1, b1: {…}}

不考虑IE的情况下,标准JSON格式的对象蛮实用,不过对于undefined和function的会忽略掉。

数组去重

阿里面试官,喜欢问这个问题。

let array=[1, "1", 2, 1, 1, 3];  
//拓展运算符(...)内部使用for...of循环  
[...new Set(array)];//[1, "1", 2, 3]  
//利用Array.from将Set结构转换成数组  
Array.from(new Set(array));//[1, "1", 2, 3]

传统的方法,循环遍历:排序sort()后前一下与后一个比较==;在数组中用indexOf判断,利用includes,利用filter;这些方法感觉都过时了,还是用ES6中利用Set去重比较牛。

取数组中的最大值和最小值

Math.max方法可以求出给定参数中最大的数。

 Math.max('1','2','3.1','3.2');//3.2  
 Math.min(1,0,-1);//-1

但如果是数组,就不能这样调用了。此时就用到了apply方法。Function.apply()是JS的一个OOP特性,一般用来模拟继承和扩展this的用途。所有函数都有apply(作用域链,参数)这个方法,当作用域链为null时,默认为上文,这个函数的“参数”,接收一个数组。

let arr = ['1','2','3.1','3.2',0,-1];  
//用apply方法  
Math.max.apply(null, arr);//3.2  
Math.min.apply(Math, arr);//-1  
//用拓展运算符  
Math.max(...arr);//3.2  
Math.min(...arr);//-1
查看原文

赞 52 收藏 38 评论 10

菠萝油王子 赞了文章 · 4月29日

基于 HTML5 WebGL 的污水处理厂泵站自控系统

前言

一道残阳铺水中,半江瑟瑟半江红。随着城市建设的迅速发展,每年都有大量新建管网水管通水运行。城市中有大量的排水设备,形成相应的城市排水系统,排水系统由检查井、排水泵站、污水处理厂、雨水口、排放口等等组成,排水设备中的检查井、雨水口、排放口等通过排水管网进行连接,日常的雨污水进入排水管网进行运输排放。目前,城市排水管网具有结构庞大,结构错综复杂等特点,现有技术中,对城市排水管网的监测、监控仍停留在大量依靠人工的模式,智能化水平,准确率都不能符合现有技术的需求。

http://www.hightopo.com/demo/drainage-pump-station/

传统城市水务行业发展现状及需求

近年来,物联网、云计算技术不断创新完善,为创建智慧城市提供了有利条件。同时,为了确保水安全,节约有限的资源,传统的水务行业正不断地探索智慧水务建设。以实现智慧生产、智慧经营、智慧服务和智慧管控,智慧水务应运而生。
1、在城市供水、排水体系中,基本依靠人工管理和巡检,确保各环节供水、排水正常,效率较低,反馈周期较长;
2、人工抄表存在扰民、“抄不准”、“估值”等多种情况,客户服务体验较差;
3、水质、水压、及关联环境数据等不能清晰掌握,不利于水务的精细化管理;
4、对于水泵、水闸、取水栓、污水处理器等水务设备状态不能实现统一管理,设备管理难度较大;
5、排水管网、污水处理设施、再生水回用等排水体系不健全。

针对水务行业市场需求,我们用 HT for Web  来打造一个污水处理厂泵站自控系统。

该系统基于物联网理念,采用信息化手段,实现对管网运行指标的实时监测,包括管网液位、流量、水质、气体、泵站运行状态等信息,保证管网安全高效地发挥作用,通过构建安全预警平台,提高应急指挥及快速处置能力,保障城市排水管线的安全运行,使得整个城市排水管线的安全运行可把握、可控制、可预测,提供科学、先进的城市级水力分析能力,更全面评估城市排水管网能力。以城市排水设施数据为基础,结合管网监控数据,气象雨情状况,实现为城市汇水区智能划分、排水规划、防涝预测提供决策依据。通过智能水表、压力传感器、开关控制阀、水质传感器、流量传感器采集数据,然后对供、排水系统中所有管线、设备 (水泵、阀门、取水栓、消防栓等) 与构筑物 (水厂、水池、水塔等) 的智能控制管理,全面掌握水务系统的运行情况如管网水位、流量、水质、水压、泵站运行状态等,保证水务系统安全高效运行。

代码实现

首先,是对一些基础参数进行设置来获得更好的体验效果:

// 禁止拖动
gv.setMovableFunc(() => { return false }) 
// 隐藏选中边框
gv.getWireframe = (d) => { d.s('wf.visible', false) }  
// 设置眼睛
gv.setEye([583, -212, -789]) 
// 设置中心点
gv.setCenter([-76, -654, -133]) 
// 设置远端位置
gv.setFar(100000) 
// 设置近端位置
gv.setNear(10) 
// 设置交互限制
gv.setInteractors([ new ht.graph3d.MapInteractor(gv) ]) 
// 设置天空球
gv.setSkyBox(dm.getDataByTag('skyBox')) 
// 全局设置右键菜单禁用
window.document.oncontextmenu = () => { return false }
// 复制初始位置 
gv.scene = { 
    eye: ht.Default.clone(gv.getEye()),
    center: ht.Default.clone(gv.getCenter()),
    far: ht.Default.clone(gv.getFar()),
    near: ht.Default.clone(gv.getNear()),
}

复制一下整个场景的初始视角情况方便做稍后的处理。通过监听部分鼠标事件来形成自己的操作风格(比如双击背景还原视角以及双击模型拉近视角):

gv.mi(e => {
    let data = e.data
    let kind = e.kind
    // 双击背景
    if (kind === 'doubleClickBackground') { 
        // 恢复视角
        gv.moveCamera(this.gv.scene.eye, this.gv.scene.center, {duration : 1000}) 
    }
    // 双击模型
    else if (kind === 'doubleClickData') { 
        // 拉近视角
        gv.flyTo(data, {animation : {duration : 500}, distance : 800}) 
    }
})

场景中有一些面板,它们是之前封装好的 2D 图标,在 3D 场景中是一个 billboard 类型,我们通过对 data.s('shape3d.autorotate') 这个属性进行设置即可实现面板是否可以随视角自动旋转,值类型为布尔值。

同时,这些面板中数值等参数我们都可以绑定好自定义的属性方便我们后期来实现数据的驱动:

data.a('name', value)
data.a('state', value)
data.a('num', value)
...

细心的小伙伴还发现墙上还有部分 billboard,可以通过点击拉近来浏览,这个小交互也增强了用户体验感:

gv.mi(e => {
    let data = e.data,
        kind = e.kind
    // 单击模型
    if (kind === 'clickData') { 
        // 拉近视角
        gv.flyTo(data, {animation : {duration : 1000}, distance : 5000}) 
    }
})

flyTo 方法即相机看向具体的节点或者节点列表,其 options 参数我们可以设置动画效果,direction 为眼睛处于目标的方向,是相对于目标的,受目标自身旋转的影响,distance 表示眼睛跟中心的固定距离。

最后一部分也是这个 demo 中最特别的交互效果,不难发现我们在通过滚轮缩放整个场景时,会有层次的显隐部分建筑,如房盖地板等,以此来使我们能观看到建筑内部的细节构造及设备。

我们通过判断镜头垂直视角距离来进行模型的显示或隐藏,改变 data.s('3d.visible') 属性的布尔值来实现。

this.gv.mp(e => {
    const { property } = e
    if (property === 'eye') {
        // 当 Y 轴视角小于等于一定数值,进行隐藏逻辑
        const [cameraX, cameraY, cameraZ] = e.newValue
        if (cameraY <= 500) {
            // hide()
            console.log('应该隐藏房顶')
        }
    }
})

方案价值

1、无线自动抄表
大大提高抄表准确率和抄表效率,人力成本明显降低;
2、快速定位管网漏损
通过水表的网格化、实时监控、快速定位管网问题等实现漏损分析,从而降低漏损。
3、水环境数据精细化管理
通过水质、水压、温度等数据的实时回传,实现精细化、可视化管理,提升管理能力。
4、无人值守,远程统一管控
通过物联网传感设备,对于水泵、水闸、取水栓、污水处理器等水务设备状态实现统一远程管理。可实现无人现场值守,管理员和技术员可随时随地远程监控水务系统的实时状态;
5、故障主动上报,降低意外故障带来的损失
设备异常提前知晓、故障主动上报,及时发现和尽快维护,避免该设备故障导致上下游设备和工况环境的连锁故障,最大化降低设备非计划性停机频率和故障损失。

总结

污水处理厂泵站自控系统,以 3D 画面人机交互的形式展现数据,实时数据以控制面板的形式体现。同时可设定各监测数据的预警值,当数据达到预警值时具有提醒的功能。其核心模块包括:数据处理服务、管网监测点管理、监测实时数据、监测历史管理、监测数据对比、管网数据分析、监测数据接入等设计和开发。通过 HT for Web 平台,使得交互界面的设计变得更加的简单与便捷。本文为针对水务处理一块做出的一些场景分析及数据可视化的设计,若有见解及填充还望指出,为水务事业及国家的发展略尽绵薄之力。欢迎大家评论区下留言互相探讨,共勉进步!

2019 我们也更新了数百个工业互联网 2D/3D 可视化案例集,在这里你能发现许多新奇的实例,也能发掘出不一样的工业互联网:《分享数百个 HT 工业互联网 2D 3D 可视化应用案例之 2019 篇》

更多好玩有趣的 demo 尽在其中,欢迎来访~~

查看原文

赞 10 收藏 4 评论 0

菠萝油王子 赞了文章 · 4月16日

目标是最完善的微前端解决方案 - qiankun 2.0

原文地址
距 qiankun 开源已过去了 11 个月,距上次官方 发声 已过去 8 个月。

Announcing qiankun@2.0

2019 年 6 月,微前端框架 qiankun 正式发布了 1.0 版本,在这一年不到的时间内,我们收获了 4k+ star,收获了来自 single-spa 官方团队的问候,支撑了阿里 200+ 线上应用,也成为社区很多团队选用的微前端解决方案。

在今天,qiankun 将正式发布 2.0 版本。

qiankun@2.0 带来了一些新能力的同时,只做了很小的 API 调整,1.x 的用户可以很轻松的迁移到 2.x 版本,详细信息见下方 升级指南 小节。

qiankun 简介

可能有的朋友还不太了解 微前端 和 qiankun 是什么。

微前端是最近一年国内前端领域被频繁提及的关键字,虽然它并不是一个全新的领域/技术,但很显然在当今越来越多的前端应用即将步入第 3 个、第 5 个甚至更久的年头的背景下,如何给 巨石应用/遗产应用 注入新鲜的技术血液已经成为我们不得不正视的问题,而微前端正是解决这类问题的一个非常合适的解决方案。

qiankun 是一个生产可用的微前端框架,它基于 single-spa,具备 js 沙箱、样式隔离、HTML Loader、预加载 等微前端系统所需的能力。qiankun 可以用于任意 js 框架,微应用接入像嵌入一个 iframe 系统一样简单。

更多信息可以查阅我们的 官方站点

定位变化

qiankun 2.0 带来的最大变化便是 qiankun 的定位将由 微前端框架 转变为 微应用加载器

此前 qiankun 的典型应用场景是 route-based 的控制台应用,做为一个微应用的聚合框架而被使用。

image-20200415232001094

如上图所示,在这种场景下,一个负责聚合与切换的主应用 与 多个相互独自的微应用 一起构成了整个大的微前端应用,一般来说页面上活跃着的也往往只有一个微应用。

而这是微前端的场景之一,在另外一些场景下,你应该可以在同一个页面中,加载多个不同的微应用,每个微应用都是主应用的组成部分 或者是 提供一些增强能力,这种场景可以说是微应用粒度的前端组件化。

因此,qiankun@2.0 将跳出 route-based 的微前端场景,提供更加通用的微应用加载能力,让用户可以更加自由的组合微应用来搭建产品。

本次升级带来了什么?

新功能

  • 支持多应用并行及多实例沙箱
  • 支持手动 加载/卸载 微应用
  • 支持 IE11 沙箱兼容
  • 官方的极简微应用通信方案
  • 支持基于 Shadow DOM 的样式隔离

此外我们还做了

  • 升级 single-spa 到 5.x 版本
  • 更灵活的 prefetch 的定制策略
  • 配套的 webpack 插件
  • 更友好的部署场景支持,如自动为微应用注入运行时 publicPath 等
  • 更简单易懂的 API,重构了许多代码,使其更清晰和更具扩展性
  • 修复了一些 bug

另外我们还升级了相应的 umi qiankun plugin,在 umi 场景下你可以这样去加载一个微应用:

import { MicroApp } from 'umi';

function MyPage() {
  return (
      <div>
      <MicroApp name="qiankun"/>
    </div>
  );
}

发布日志

多应用支持

在 qiankun@1.x 中,我们的沙箱、样式隔离等机制只能对单一微应用场景生效,多个微应用共存的支持能力尚不完备。

而在 2.0 版本中,我们终于完善了这一功能,现在,你可以同时激活多个微应用,而微应用之间可以保持互不干扰。

在多应用场景下,每个微应用的沙箱都是相互隔离的,也就是说每个微应用对全局的影响都会局限在微应用自己的作用域内。比如 A 应用在 window 上新增了个属性 test,这个属性只能在 A 应用自己的作用域通过 window.test 获取到,主应用或者其他微应用都无法拿到这个变量。

但是注意,页面上不能同时显示多个依赖于路由的微应用,因为浏览器只有一个 url,如果有多个依赖路由的微应用同时被激活,那么大概率会导致其中一个 404。

为了更方便的同时装载多个微应用,我们提供了一个全新的 API loadMicroApp ,用于手动控制微应用:

import { loadMicroApp } from 'qiankun';

/** 手动加载一个微应用 */
const microApp = loadMicroApp(
  {
    name: "microApp",
    entry: "https://localhost:7001/micro-app.html",
    container: "#microApp"
  }
)

// 手动卸载
microApp.mountPromise.then(() => microApp.unmount());

这也是 qiankun 作为一个应用加载器的使用方式。

基于这个 api,你可以很容易的封装一个自己的微应用容器组件,比如:

class MicroApp extends React.Component {
  
  microAppRef = null;
  
  componentDidMount() {
    const { name, entry } = this.props;
    this.microAppRef = loadMicroApp({ name, entry, container: '#container' });
  }
  
  componentWillUnmount() {
    this.microAppRef.mountPromise.then(() => this.microAppRef.unmount());
  }
  
  render() {
    return <div id="container"/>;
  }
}

兼容 IE11 的沙箱能力

在 qiankun issue 区域呼声最高的就是 IE 的兼容,有不少小伙伴都期待 qiankun 能够在 IE 下使用。

qiankun 1.x 在 IE 使用的主要阻碍就是 qiankun 的沙箱使用了 ES6 的 Proxy,而这无法通过 ployfill 等方式弥补。这导致 IE 下的 qiankun 用户无法开启 qiankun 的沙箱功能,导致 js 隔离、样式隔离这些能力都无法启用。

为此,我们实现了一个 IE 特供的快照沙箱,用于这些不支持 Proxy 的浏览器;这不需要用户手动开启,在代理沙箱不支持的环境中,我们会自动降级到快照沙箱。

注意,由于快照沙箱不能做到互相之间的完全独立,所以 IE 等环境下我们不支持多应用场景, singlur 会被强制设为 true。

基于 shadow DOM 的样式隔离

样式隔离也是微前端面临的一个重要问题,在 qiankun@1.x 中,我们支持了微应用之间的样式隔离(仅沙箱开启时生效),这尚存一些问题:

  1. 主子应用之间的样式隔离依赖手动配置插件处理
  2. 多应用场景下微应用之间的样式隔离亟待处理

为此,我们引入了一个新的选项, sandbox: { strictStyleIsolation?: boolean }

在该选项开启的情况下,我们会以 Shadow DOM 的形式嵌入微应用,以此来做到应用样式的真正隔离:

import { loadMicroApp } from 'qiankun'

loadMicroApp({xxx}, { sandbox: { strictStyleIsolation: true } });

Shadow DOM 可以做到样式之间的真正隔离(而不是依赖分配前缀等约定式隔离),其形式如下:

image.png

图片来自 MDN

在开启 strictStyleIsolation 时,我们会将微应用插入到 qiankun 创建好的 Shadow Tree 中,微应用的样式(包括动态插入的样式)都会被挂载到这个 Shadow Host 节点下,因此微应用的样式只会作用在 Shadow Tree 内部,这样就做到了样式隔离。

但是开启 Shadow DOM 也会引发一些别的问题:

一个典型的问题是,一些组件可能会越过 Shadow Boundary 到外部 Document Tree 插入节点,而这部分节点的样式就会丢失;比如 antd 的 Modal 就会渲染节点至 ducument.body ,引发样式丢失;针对刚才的 antd 场景你可以通过他们提供的 ConfigProvider.getPopupContainer API 来指定在 Shadow Tree 内部的节点为挂载节点,但另外一些其他的组件库,或者你的一些代码也会遇到同样的问题,需要你额外留心。

此外 Shadow DOM 场景下还会有一些额外的事件处理、边界处理等问题,后续我们会逐步更新官方文档指导用户更顺利的开启 Shadow DOM。

所以请根据实际情况来选择是否开启基于 shadow DOM 的样式隔离,并做好相应的检查和处理。

官方的极简通信方案

微前端场景下,我们认为最合理的通信方案是通过 URL 及 CustomEvent 来处理。但在一些简单场景下,基于 props 的方案会更直接便捷,因此我们为 qiankun 用户提供这样一组 API 来完成应用间的通信:

主应用创建共享状态:

import { initGloabalState } from 'qiankun';

initGloabalState({ user: 'kuitos' });

微应用通过 props 获取共享状态并监听:

export function mount(props) {
  props.onGlobalStateChange((state, prevState) => {
    console.log(state, prevState);
  });
};

更详细的 API 介绍可以查看官方文档

我们会继续为大家带来什么

除了基本的日常维护、bugfix 之外,我们还会尝试走的更远:

  1. 官方支持的 qiankun webpack 插件,解决一些由于配置不当出现的问题
  2. 自定义的沙箱规则
  3. 微应用嵌套支持
  4. 更友好的调试体验
  5. 与 Webpack5 Module Federation 的结合,提供官方的使用指导或插件
  6. 更多的实验性(experimental)尝试,如基于原生 Portal 标签的微应用渲染,基于运行时的更轻量的样式隔离方案。

升级指南

2.0 版本 调整了相当多的内部 API 名字,但大家使用的外部 API 变化并不大(基本完全兼容 1.x),你可以在十分钟内完成升级。

render 更改为 container

import { registerMicroApps } from 'qiankun'

registerMicroApps(
  [
    {
      name: 'react16',
      entry: '//localhost:7100',
-     activeRule: location => location.pathname.startsWith('/react'),
+     activeRule: '/react',
-     render: renderFn,
+     container: '#subapp-viewport',
    },
  ]
)

现在你可以简单的指定一个挂载节点即可,而不用自己手写对应的 render 函数了。简单场景下 activeRule 配置也不需要再手写函数了(当然还是支持自定义函数),只需要给出一个前缀规则字符串即可,同时支持 react-router 类的动态规则,如 /react/:appId/name (来自 single-spa 5.x 的支持)。

同时,微应用收到的 props 中会新增一个 container 属性,这就是你的挂载节点的 DOM,这对处理动态添加的容器以及开启了 Shadow DOM 场景下非常有用。

注意,旧的 render 配置依然可以使用,我们做了兼容处理方便不想升级的用户;但 render 存在时,container 就不会生效。

start 的配置变化

因为我们引入了一些新的能力,因为 start 的配置也发生了一些变化:

import { start } from 'qiankun'

start({
-  jsSandbox: true,
+  sandbox: {
+    strictStyleIsolation: true
+  }
})

新的 API loadMicroApp

这个 API 用于手动挂载一个微应用

/** 用于加载一个微应用 */
loadMicroApp(app: LoadableApp, configuration?: FrameworkConfiguration)

使用详情可见上面 多应用支持 小节。

查看原文

赞 20 收藏 12 评论 5

菠萝油王子 赞了文章 · 4月14日

框架设计:如何基于 Egg 设计 Node 的服务框架

著作权归作者所有。商业转载请联系 Scott 获得授权,非商业转载请注明出处[务必保留全文,勿做删减]。

Node 的工具化价值自不多言,而服务化价值需要长期探索,小菜前端在服务化路上依然是小学生,目前的尝试是是 Cross 框架,尝到了一些甜头。

我想,几乎没有前端工程师会对 Node 不感兴趣,但用它适合干哪些事情,每个人的答案都不同了,比如小菜前端,我们对于 Node 的深度尝试,可以在这里找到答案:《技术栈:为什么 Node 是前端团队的核心技术栈》,但关于让 Node 做服务端的事情,却只有少数团队有这样的勇气。

之所以缺乏自信和勇气,本质的原因在于 Node 还没有一个足够顺手的框架来让你快速证明驱动业务的价值,也在于对 Node 缺乏足够的了解和信心,以及相对于服务端的强势,往往前端在侵蚀服务端领域的时候,会受到这般那样的挑战甚至刁难,这也成为了在团队推广 Node 常遇到的阻力,希望大家从小菜团队身上可以找到一些答案,其中答案的一部分就是要对 Node 要有足够的了解和认知,才可以为通用问题抽象出通用的方案去实施,在小菜,就是对于 Node 框架的封装,这个框架尚未开源,名叫 Cross,寓意没有迈不过的技术门槛。

分清楚 Node 的边界

前后端的团队本身是相爱相杀的关系,是左右手的双十合十,既有接口联调上的上下游数据立场,也有必须与对方精诚合作才能一次次拿下项目的战役,而在服务这件事情,前端就直接介入到了服务端的领域,而且从整个行业来看,这种介入在大中型公司已成为不可阻挡的趋势,无论是淘宝、天猫、支付宝、腾讯、网易、百度,包括创业独角兽大搜车、贝贝网、Rokid,海内外不分国籍不分领域的众多公司都有一个个团队在深度耕耘,所以这里的第一个边界是前后端的边界。

一千家公司可能就有一千种商业模式,一千种用户画像,一千种业务特征,既有高依赖算法的高实时计算的井喷式访问场景,也有日均几十 UV 几百 PV 的 toB 大客户产品,什么场景用 Node 合适,什么不合适,这第二个边界就是 Node 在业务领域里的服务边界。

只有弄清楚这两个边界,才有 Node 的生存土壤,脱离了这两个边界,就难免处处碰壁无法落地,针对前后端边界,我从前写过这样一段话:

数据的控制权和与视图所依赖的 API,这里就是目前前后端的边界,数据控制权属于后端,API 属于后端,把前后端简单看做是一个完整的系统,这个系统中自 API 向下自然是后端的,API 向上则属于前端。

在 API 下面,对于数据的业务流转流转逻辑,在上面对于数据的调用和组装,这就是数据层面的天然分界点,而 Node 植入进去,也必须在 API 这一层与 Java 保持规范的统一和兼容,通过 RPC 无缝的调用才能来谈边界,而这个边界我的理解它可以是非强业务耦合的,比如独立的内部协同系统,也可以是非高计算型的,可以是相对独立的异步的高并发的模块,比如消息堆栈的频繁拉取推送,比如日志的收集整理等等,总结起来就是非复杂业务流程的,非高计算型的这个地方可以作为 Node 进入的边界。

而对于业务的服务边界,只要的小而美的相对独立的系统,只要不是核心业务,都可以用 Node 快速开发,比如小菜这里就有报表系统、打包系统、发布系统、市调系统、日志系统、可视化平台、招聘面试系统、Bug 跟踪系统等等。

以上的两个边界,大家在仔细评估的时候,一定不要忘了自己团队人员的能力配置,能不能 Hold 住 Node,有没有 Node 技术专家坐镇,不然仓促使用可能还适得其反。

为什么要封装 Cross

在弄清楚上述的边界后,小菜前端在 1 年多的时间里,对 Node 进行深度的使用,从基建系统到相对独立的业务系统,整个走下来,团队更多同学掌握了 Node 的使用,同时每个系统之间的差异性也越来越大,有的用的是 Koa 有的是 Koa2,有的是 Thinkjs 有的是 Express,还有的是原生 NodeJS。

显然每个人的偏好都不同,代码质量也不同,工程架构方式也不同,这为后期的维护带来巨大的麻烦,尤其是做 Node 监控时候,发现没法用一套方案做批量的部署,也同样不能做水平的快速扩展,需要挑选一个框架基于它做统一的封装,从而把前端参与的所有服务端建设可以统一起来,而且现实是我们的前端和 Node 应用由于整个工程的构建与服务部署方式的不同,已经散落到各个服务器上,导致维护成为了瓶颈,也必须到做出改变的时候了,这是当时的部分零散的应用图:

image.png

为什么选择 Eggjs

小菜前端在使用 Eggjs 作为 Nodejs 的基础服务框架之前使用过诸如 Koa、Express、Koa2、Thinkjs 等框架,其中与 Eggjs 最接近的当属奇舞团开源的 Thinkjs , 同样的约定大于配置,同样的基于 Koa2 进行包装完善,同样的采用多级分层的设计方式(Controller, Service 等等),让应用开发变得更加清晰明了,然而有趣的是, Thinkjs 的开源时间(2013)早于 Eggjs 的开源时间,其在 github 上的 star 的增长速度却是远远落后于 Eggjs,NPM 下载数亦然,虽然 thinkjs 开发体验也不错,小菜最后会选定 Eggjs 作为 Nodejs 服务框架的原因,除了上述提到的优点之外,还有如下几点 :

  • 高度可扩展的插件机制
  • 方便定制上层框架
  • 丰富且活跃的社区生态
  • 渐进式开发
  • 多进程管理

小菜前端从 18 年年初就开始使用 Eggjs 了,我们的很多项目都是基于 Eggjs 搭建的,其中包括我们的报表系统、GraphQL 网关、小程序后台服务等。在使用 Eggjs 开发这些项目的过程中我们逐渐形成了自己的一套适用于宋小菜的基于 Eggjs 的上层框架,基于小菜特定业务场景长出来的 Framework,它的定制程度很高,大家可以参考我们实现这套框架时用到的技巧与方法,这些套路应该是通用的。

秉承怎样的设计理念

考虑授人以鱼不如授人以渔嘛,我们先分享下我们的设计理念,这是最简单却也最重要的开始部分,我们的目标是风格统一、上手容易、维护方便:

image.png

然后就是整体需求的整理和开发集成,在开发集成个过程中不断调优:

image.png

定完目标,设计好流程,就要准备具体的实施了,我们实施涉及到过程,主要从下面四个方面着手:

  • 框架关系
  • 通用 API
  • 插件定制
  • 工程管理

如何设计 Framework

框架关系

我们将所有通用的 API 和常用工具函数以及常用的插件(redis、gateway)等统一集成在基础框架 baseFramework 中,由于 Egg 支持多级框架继承,所以我们可以根据基础框架 baseFramework 衍生出其他框架如 GraphQL 相关的框架、微服务相关的框架,它相当于是一颗框架种子,可以往不同的方向定制:

image.png

通用 API

1. 请求参数统一获取

假定某个 HomeController 有成员函数 testAction 既要处理 post 请求又要处理 get 请求,就有可能出现以下情况:

const { Controller } = require('egg');

module.exports = class HomeController extends Controller {
    testAction(){
    const { ctx } = this;
    const { method } = ctx.request;
    const id = method === 'GET'? ctx.request.query.id : ctx.request.body.id;
    ...
    }
}

我们可以将其优化为:

/* yourapp/app/controller/home.js */
const { BaseController } = require('egg');
// 或者
const { BaseController } = require('your-egg-framework');

module.exports = class HomeController extends BaseController {
    testAction(){
    const id = this.getParam('id');//
    ...
    }
}
  
/* egg-baseframework/core/base_controller.js */
const { Controller } = require('egg')

module.exports = class BaseController extends Controller {
    getParam(key) {
      const { ctx } = this;
    const { method } = ctx.request;
    if (method === 'GET') {
        if(key) {
          ...
      } else {
          ...
      }
    } else {
            ...
    }
  }
}
/* your-egg-baseframework/lib/index.js */
const { BaseController } = require('../core/base_controller');

module.exports = {
  BaseController,
  ...
}

/* your-egg-framework/app.js */
module.exports = (app) => {
    require('egg').BaseController = BaseController
}

2. 返回数据格式化

方法同上,我们可以在 BaseController 中定义统一的调用成功和调用失败返回函数,并在函数中处理返回数据从而避免返回数据不规范的问题

3. 通用工具函数

我们可以将平时业务开发中可能会用到的工具函数统一通过框架扩展的额形式定义到内置对象 helper 上,这些都可以以框架扩展(extend)的方式集成进来,比如参数转化啊,错误信息格式化等等。

4. 增加参数校验层

我们可以将参数校验这一步抽离出来成为 logic 层。有两种方式可以做到:

  • 在框架加载时调用 app.loader.loadToContext 将所有 controller 对应的参数校验函数挂载到 context 上,在 controller  执行相应的处理函数时调用
  • 在你的框架继承的 appWorkerLoader  中覆写 eggjs 的 loadController , 对每一个 controller 的处理函数都使用对应的 logic  进行代理

插件定制

Egg 的拥有着丰富的插件生态,然而总有些我们需要用到的插件不太符合我们的要求,比如:

  • egg-redis 长久不支持哨兵模式
  • egg-graphql 不支持连接其他 graphql 服务
  • egg-kafka 长久没有维护

这个时候就需要我们自己动手编写或修改相应的插件了,而有些在公司层面上通用的功能如:Java 服务端网关请求(egg-gateway)、用户鉴权(egg-auth)等我们也将其封装为插件集成到基础框架中,讲实话,整个框架开发中,让人最开心最后成就感的部分就是写插件的时候:

image.png

工程管理

由于插件和插件之间,插件和框架之间,框架和框架之间存在相互依赖的关系,代码管理就成为了比较头疼的问题,推荐使用目前比较火的 monorepo 来进行管理。规范版本发布流程,避免出现不兼容问题。

总结

关于 Cross 的建设我们差不多投入了一个多月的周期,从投入产出比来看还是很划算的一次尝试,但是在落地时候也会遇到不少问题,从人和团队的角度来看,这样的一套 Framework 需要有一定的 Node 编程能力的同学才能较好的用起来,对于所有人依然有一定的心智成本,有没有可能把这个成本继续降低呢,走向 Pass 跟高阶的只关心业务逻辑不关心背后实现的阶段呢,这是一个很值得研究的课题,另外就是从事情的角度,如果业务中没有那么多的场景来承载这个框架,事实上它是很难继续进阶的,因为没有足够的应用和测试场景来暴露问题,这也是我们当下遇到的一个实际困难,缺少 Node 好手掣肘了我们前进的步子,不过好消息是接下来的业务场景已经铺开了,团队也刚刚进了一个 Node 选手,接下来看看应用后发力效果如何。

Scott 近年面试或线下线上技术分享,遇到太多前端同学,由于团队原因/个人原因/职业成长/技术与管理通道,甚至家庭城市等等原因,在理想国与现实之间,在放弃与坚守之间,摇摆不停,心酸硬扛,大家可以找我聊聊南聊聊北,对工程师的宿命和价值有更多的看见与了解,Scott 微信: codingdream,也可以来关注 Scott 跟进我的动态

2.png
1.png

查看原文

赞 9 收藏 4 评论 0

菠萝油王子 赞了文章 · 4月11日

技术管理:如何带领管理少于 10 人的前端小团队

著作权归作者所有。商业转载请联系 Scott 获得授权,非商业转载请注明出处[务必保留全文,勿做删减]。

Scott 近年面试或线下线上技术分享,遇到太多前端同学,由于团队原因/个人原因/职业成长/技术与管理通道,甚至家庭城市等等原因,在理想国与现实之间,在放弃与坚守之间,摇摆不停,心酸硬扛,大家可以找我聊聊南聊聊北,对工程师的宿命和价值有更多的看见与了解,Scott 微信: codingdream。

近期面试了一个工作 10 年的前端主管,带领了一个 9 人的前端团队,从他身上明显发现了两个致命的问题:一、他没有意识到成员拥有成长空间的重要性,二、他没有以培养接班人的心态打造团队腰部力量,最终事无巨细亲力亲为但整个团队并没有持续发生更快更好的变化,总结出一句话就是带领不是领导,而是领着冲刺成绩,管理不是权力,而是激励授权培养更多骨干。

抛开场景谈管理,跟抛开场景谈技术一样,很容易沦为干巴巴的教条,我这一篇会以小菜前端为样板,结合实际的案例,给大家引入我的管理思路,以及通用的方法论和观察视角,只要能给大家带来一点点启发,最终带团队少走了那么一点点弯路,本文的价值就足够大了,因为带团队本身就是一项因人而异的艺术,而艺术很难加上刻度进行量化。

另外,我会换用问答的形式,来跟各位对话,在每次往下看我的观点之前,请先在内心酝酿你的答案,酝酿成熟后,再往下浏览,这样可以保留属于你自己的思辨,避免被我先入为主,因为管理本身并非是寻求答案,更多是训练如何寻求答案形成落地 Action 的思考习惯,那我们先思考第一个问题:前端工程师这个群体身上有什么特质?

重新认识前端工程师

前端工程师虽是工程师,却与主流技术从业者有诸多不同,最大不同就是想象力创造力爆棚,浑身散发纯真的文艺气息,天真美好浪漫但是一放到荒漠沙洲却常常不堪一击,这是我两年前带团队时分享的一页 PPT:

image.png

以及关于前端主要工作场景的一页 PPT:

image.png

一群人会塑造和改善环境,而环境反过来也会重塑它之中的人,这也是为什么程序员在人群中很容易被一眼识别的原因,那就是特定的工种塑造出我们特定的性格、喜好、气质、观人看事的角度,这样的塑造在前端的行业中更加的深刻更加的剧烈,因此一个前端团队往往比想象中好带,也可能比想象中难带的多,这一群人真的可能会千人千面,与众不同,拢在一起别说团结一致,有时能否听从指挥都是问题,彼此的感情连接紧密程度可弱可强,前端的技术面积宽深度浅,这都是客观存在的事实,并且这个群体可千万不可小觑,如果不用心带不仅会让你目标错失,API 无法完成,甚至整个团队会分崩离散,那接下来让我们来面对这个课题吧,小型的前端团队到底应该怎么带?

要讨论怎么带,我们还要看看一个前端团队在不断成长的过程中,从 1 个人到 5 个人,到 10 个人,到 30 个人,到 80 个人,到 100 个人,你认为这个团队会经历哪些阶段,会有什么样的能力结构,也就是你现在 10 人团队的当下如何,未来怎样?大家对这个充分思考后再看下文。

小菜前端的几个时代

上文聊了前端的个性化特质,再来看下 10 人团队可能处于哪个阶段,2 年前看到蚂蚁金服的玉伯分享了前端的几个时代,非常认同,我把它进行了调整进一步转化到小菜的业务和组织架构上,最后草拟了这样的一张规划图:

image.png

这几个时代词如其意:

  • 人工时代偏人肉,工具设施薄弱,业务繁重,资源紧张
  • 工具时代偏效率,各种轮子往上凑,不太成体系还忙中带乱,业务支撑可圈可点
  • 工程时代偏规模,规模化的用户和业务背后可复用的工程化体系与方案,业务支撑稳定有序
  • 智能时代偏算法,通过对业务深刻的理解和运用来打造出非常好用的产品和服务,极大的解放人力

那我们带的团队在 10 人以内,如果不是在 BAT 这种大厂,往往处于工具和工程能力偏弱的阶段,也就是人工时代,业务越重基建容易越薄弱,如果是有很好的团队规划意识以及公司的资源支撑,那么 10 人团队往往会处在人工时代往工具时代的转型期,此时基建往往与业务并进,彼此互相影响,但总体依然会偏向人工,而人工时代的团队特征是什么,这是我们需要思考的问题。人工时代或者说人肉时代,我们对于人的依赖程度很高,这时候如果是纯资源管理的视角,那先思考下你最多能直接管理多少人,你觉得 10 人团队应该管理什么?好管理么?

管理半径与团队阶段

一周五天,一天重点照顾两个人,10 人的团队可以勉强覆盖,根据我的过往经验,一个管理者如果要对每一个成员都负责,都能拿到结果,基本上 10 个人就是一道坎,在 10 人以内,是有机会也有能力对每一个成员负责的,但高度负责后的团队主管会很累很忙,也就是人越少越依赖主管帮带和规划的个人能力,带小团队往往是这样的背景。

还是以小菜为例,我按照阿里的 P 系列来对团队能力层次的几个阶段做了整理,这些不是严格意义上的小菜前端层级分布,只做示意,供大家观察趋势和问题(18 人是当下,25 人是未来小半年后),看完后请思考每个阶段都有什么问题?

image.png

第一个阶段,7 人左右,整体工程师能力偏弱,极少数的技术熟练工,资深前端会成为团队的能力天花板,这样的状态如果没有新变化会磨削掉所有人的成长空间,逐渐沦为纯粹做事的工具或者资源,新同学没成长,老同学到天花板,离职率就会变高;
第二个阶段,10 人左右,有了带队的专家,但有大量的初级前端,而资深前端数量偏少,这样在腰部的传承这块就会断层,所以此时专家就必须对很多具体的事项负责,也需要对人才的成长和资源的解放负责,带队专家会成为团队瓶颈,如果不得到进一步的团队能力提高和解放,专家的综合能力也会遇到天花板;
第三个阶段,13 人左右,补充了更多的资深前端,整体分布看上去趋于健康,但专家变得紧缺,因为有更多的资深前端嗷嗷待哺,依然是在腰部短板,此时会有个别有管理能力的同学涌现,但管理意识和方式都很初级,需要有一些制度性培训性的流程来保障他们融入组织成长;
第四个阶段,18 人左右,有了更高层级的专家,专家整体数量变多,但初级前端也变得更多,此时需要有更多具备管理能力的腰部主管出现,这一层职责就落到了专家层,需要专家成为教练,来主导自己的小组方向,将更好的方法论实施起来,帮助同学更快成长,所以这个阶段对于资深前端和专家的需求依然旺盛;
第五个阶段,25 人左右,有了更多具备管理能力的资深前端和专家,整个团队的能力传递形成了梯度,整体分布趋于健康,但在初级前端和资深前端交接的这一层 P5+/P6- 成为了能力洼地,需要更多同学被发掘和帮带上来,通常这个时期会有大量意识好主动强有担当,并且能扛风险拿业务结果的同学快速晋升,这个阶段是一个团队真正意义的黄金成长期;

这里提供给大家一种从能力来看团队健康度的视角,实际上的团队所处环境会比图上复杂很多,比如有掉队同学的劝退,优秀同学的离职,有新同学的加入冲击,再加上各种不同的业务交叉时资源的冲突,技术栈的发展畸形,公司大组织架构的变化等等,每个阶段的侧重点都不同, 所有这些都考察者团队管理者的反应速度和决断能力,而正向的业务会带来人员数量的快速增长,因此管理能力与增长的动态变化,会变成团队能力的核心坐标,稍有不慎就会出现管理故障。

虽然每个阶段都不同,但每个阶段都是承接与上一个阶段的成果之上的,越早期时候的方向修正的正确,越后期的扩展和成长才会更顺利,否则团队整体的心智在这些阶段中历经冲击,反而会伤害到整体的团队心理状态和能力梯度,我们切回去到 10 人之前的团队状态,这个时期是整个团队最早期也是最最重要的原子阶段,这个时期所奠定的研发底蕴、工程师文化、分享与影响力、看待业务与技术的态度、自我认识与工程师价值判断等等都是在这个时期慢慢形成雏形,所以千万不要小看人数不足 10 人的时期,这个底子没打好,后面会变灾难。

对于小菜来说,10 人时期主要的管理工作,除了业务,就是技术栈的提前规划预研、工程师的稳定性、团队内技术资产的透明化、一定程度的效率与质量基建,包括工程师的成就感培养和有潜力员工的刻意栽培,这就是此时管理二字此时的含义。

我们花了大量篇幅讨论前端的职业特征、团队的发展阶段,你作为事实上的管理者,或者未来潜在的管理者,要拥有哪些能力,以及从哪些方面来带领这个团队快速成长,奠定必要(至少是不差)的早期基础呢?大家思考一下先。

技术 TL 必备能力清单

管理是一门很复杂的学问,团队越大越依赖管理手段,人越少越依赖带领能力,我们把 10 人团队的管理弱化,同时把此时主管需要的能力简化为带领能力,那么这样的能力如何训练呢?让他成为你技术管理生涯中的雏形能力,为未来你带领 20 人、 50 人团队做好铺垫。

大家可以先思考下,思考后再看下面这几张图,图上是 Scott 刚接手 7 人团队后半年多主要关注的事情(图是半年汇报 PPT 的截图),那时对于管理和带领的认识是没有今天深刻的,后面我会把当时半年多的过程整理成观点:

image.png
image.png

来到今天,我回头去看过往走过的路,把 10 人团队内需要训练的几个核心能力总结为:敢于担当、建立信任、善于规划、探索创新、培养成就感、扩大影响力。

为什么是这几个,做不做又有什么区别呢?对于我做管理意味着什么?大家可以先思考下,然后我们再针对每一个适当展开一些讨论。

担当是拥抱组织的前提条件

一个团队的管理者,如果没有担当,那就是一场巨大的灾难,这是对管理者最基本的要求。而担当讲着容易,做起来却很难,一旦你带领一个团队的时候,你就需要拥抱这个团队,无论他做的多好的地方还是多烂的地方,无论团队的能力和业绩是出众的,还是扶不上墙的,当你的屁股坐在这里的时候,当组织把你放到这个位置的时候,你对上级主管和组织就是背负着他们的信任和期待,对下面的员工则背负着责任与担当,除非你永远也不坐到具有管理职能的位置上,否则担当是你永远也无法回避的管理门槛。

信任是做好管理的黄金基石

当你要带领一个团队时,你的任何决定都是需要团队的大多数人支持和推进的,那么你就需要争取到绝大多数人的信任,即便一个事情他认为是百分百错的,但是你这里必须紧急执行,那么基于信任他也会尽心执行,否则就容易产生对抗或者过程偏离,而对抗和未尽心尽力伤害的不仅仅是管理者和当事员工,也最终会伤害到公司既定目标的实现,反之,员工需要获得你的信任,你也要获得你老板的信任,这样一层层的双向信任,才有可能在整体推进时候遇到最小的阻力。

规划是带领团队的必备素质

对于管理者来说,要把全团队人的时间用出相对较高的价值,就必须有规划能力,所谓规划,就是把想做的事情坐标化和里程碑化,坐标里的 x 轴是基于时间尺度的团队不同阶段,纵轴是不同阶段需要重点做的事情,而里程碑就是每个特定阶段每个重点事情,它在特定时间点需要达成的目标,这个能力是最容易被技术管理者忽略的技能,大家往往觉得脑海里有个模糊的概念和方向就行了,剩下所有的事情就靠 Coding,一直 Coding,这对于团队是很大的伤害,其实就算不是作为管理者,越是技术走向资深,越是承担更大的挑战,规划能力就越刚需。

探索是驱动组织的不二法宝

技术往往是有趣的,而技术做的事情往往是无趣的,在无趣中找有趣是一个永恒的课题,摆在管理者的面前。如果你是一个喜欢求稳的墨守成规的管理者,那么所有跟着你的同学,他们的成长空间就会受限,反之如果你是一个非常激进敢于冒险的管理者,所有跟着你的同学空间就会快速放大,但同时也会伴随较大的技术冒进风险,所以探索的速度和深度需要结合严禁的规划来实现,一定是基于深思熟虑的有实际价值的规划路径来做探索,而既然要探索,就要解放思想,放开手脚,不要犹犹豫豫左右徘徊。

成就是选拔人才的通关门卡

虽然工作只是混口饭(PS: 惨兮兮的说法而已..),但对于现代的年轻人,表里一致兴趣契合以及技术开发所能带来的成就感,这一点必须单独拎出来说,这里的成就感不仅仅指技术上实现所带来的成就感,更是他职业生涯上有所突破,做的事情能看到实际价值,包括技术深度广度上真真实实的收获,这些收获是个人努力得到的,但这些机会和道路是你为组员设定规划或者是指引的,因此这时候成就他人就是你必须培养的一种能力,也只有通过成就他人才能最终成就业务,成就团队,从而也成就自己。

影响是培育文化的终极魔法

我们通常讲影响力,都很容易联想到技术分享带来的影响力,所谓技术网红的样子,这一点没错,我们在前面专门有一篇文章探讨过影响力的意义和如何做影响力,那放到管理的视角来看,如果团队整体是一个社会化的人,那么影响力就是它的综合口碑,它的外部形象,它给外界所传达的人设,这些虚的东西往往发挥着着非常实的作用,比如对于你的招人,比如对于你团队内文化的促进,比如对于你团队与其他团队的合作关系等等,扩大影响力并且不断的塑造正向的影响力,也是一个技术 TL 不能回避的课题。

这几项能力在职业生涯中不断历练,一个 10 人的前端团队就能带的越来也有章法,但知易行难,在大家当下实际的环境中也一定会遇到种种的困难,我的建议是,选择一到两个作为主线,在特定的时期内重点推动,同时尽可能兼顾其他,通过周期性的轮转,来让团队不断的磨合前进,形成的合力越来越强,就像小菜前端 2019 年的主轴就是认知升级:

image.png

从问题抽象到行动聚焦

一旦我们对团队梳理后,脑海中有了明确的主线,那么就可以把团队现存的所有问题进行分类,分类是为了聚焦,在特定时间周期内聚焦到特定问题,投入主要精力进行改善解决,就比如前文提到的,小菜在初期也会有下面的 8 大类问题,此时的小菜刚好是在 10 人规模之内:

image.png
针对这些问题分别聚焦后,以半月到单月的长度,进行了持续半年的推进解决,有的是靠口头,有的靠文档,有的靠工具,有的靠分享,有的靠影响力,有的靠业务赋能,所以小菜才这样一步步克服过来:

image.png

当问题都陆续解决的时候,实际上就是管理更上一个台阶的时候,也是团队基础打好,准备打下一场大战役的时候,在这样的一个套路上,我们一直这样操作,2019 年的认知升级,也是为了 2020 年能更好的能力升级,只不过此时面临的挑战是人数超过 20 人的团队它的境况,而它的基础是当初 10 人规模的阶段。

最最后,大家在带这样规模团队时候,需要真的把心放进去,刻意练习担当、信任、规划、探索、成就感、影响力这几个关键词背后的能力,同时把眼光往后面多展望半年,聚焦当下的问题,形成具体的计划逐步推动,相信这个团队在你的带领下,一定会每一周都有一个新面貌。

近两年 Scott 观察到前端行业已经完全进入竞争的深水区,各大小公司的前端 TL 刚刚上任,初带团队,针对前端工程师这个群体,应该怎么管人理事,搭台拿结果,帮带有成长,就成立了这个全国的前端技术主管学习交流群,在人的选用育留上互相学习成长,入群的门坎是你有实线或者虚线在带团队,请加 Scott 微信: codingdream 邀请入群,二维码见下方:

2.png
1.png

查看原文

赞 15 收藏 8 评论 2

菠萝油王子 赞了文章 · 3月31日

如何处理浏览器的断网情况?

offline.jpg

好的断网处理会让人很舒适:lol的断线重连,王者荣耀的断线重连 可以确保游戏的继续进行

坏的断网处理甚至不处理会出bug:比如我手上的项目就出了个bug 业务人员表示非常苦恼

网络问题一直是一个很值得关注的问题。

比如在慢网情况下,增加loading避免重复发请求,使用promise顺序处理请求的返回结果,或者是增加一些友好的上传进度提示等等。

那么大家有没有想过断网情况下该怎么做呢?比如说网络正常->断网->网络正常。

其实我一直也没想过,直到组里的测试测出一个断网导致的bug,让我意识到重度依赖网络请求的前端,在断网情况下可能会出现严重的bug。

因此我将在这里记录一下自己对系统断网情况下的处理,一方面避免bug产生,一方面保证用户及时在应用内知道网络已经断开连接

  • 概览
  • 用于检测浏览器是否连网的navigator.onLine
  • 用于检测网络状况的navigator.connection
  • 断网事件"offline"和连网事件"online"
  • 断网处理项目实战

    • 思路和效果
    • 断网处理组件使用
    • 断网处理组件详情
    • 发现
  • 参考资料

概览

为了构建一个 “断网(offline)可用”的web应用,你需要知道应用在什么时候是断网(offline)的。
不仅仅要知道什么时候断网,更要知道什么时候网络恢复正常(online)。
可以分解陈本下面两种常见情况:

  1. 你需要知道用户何时online,这样你可以与服务器之间re-sync(重新同步)。
  2. 你需要知道用户何时offline,这样你可以将你未发出的请求过一段时间再向服务器发出。

通常可以通过online/offline事件去做这个事情。

用于检测浏览器是否连网的navigator.onLine

navigator.onLine

  • true online
  • false offline

可以通过network的online选项切换为offline,打印navigator.onLine验证。

当浏览器不能连接到网络时,这个属性会更新。规范中是这样定义的:

The navigator.onLine attribute must return false if the user agent will not contact the network when the user follows links or when a script requests a remote page (or knows that such an attempt would fail)...

用于检测网络状况的navigator.connection

在youtube观看视频时,自动检测网络状况切换清晰度是如何做到的呢?
国内的视频网站也会给出一个切换网络的提醒,该如何去检测呢?
也就是说,有没有办法检测网络状况?判断当前网络是流畅,拥堵,繁忙呢?
可以通过navigator.connection,属性包括effectiveType,rtt,downlink和变更网络事件change。继承自NetworkInformation API。

navigator.connection

online状态下运行console.log(navigator.connection);

{
    onchange: null,
    effectiveType: "4g",
    rtt: 50,
    downlink: 2,
    saveData: false
}

通过navigator.connection可以判断出online,fast 3g,slow 3g,和offline,这四种状态下的effectiveType分别为4g,3g,2g,4g(rtt,downlink均为0)。

rtt和downlink是什么?NetworkInformation是什么?

这是两个反映网络状况的参数,比type更加具象且更能反映当前网络的真实情况。

常见网络情况rtt和downlink表

网络状况rtt(ms)downlink(Mbit/s)
online1002.2
fast 3g6001.55
slow 3g21500.4
offline00

注意:rtt和downlink不是定值,而是实时变化的。online时,可能它现在是rtt 100ms,2.2Mb/s,下一秒就变成125ms,2.1Mb/s了。

rtt
  • 连接预估往返时间
  • 单位为ms
  • 值为四舍五入到25毫秒的最接近倍数(就是说这个值x%25===0,可以观察常见网络情况rtt和downlink表)
  • 值越小网速越快。类似ping的time吧
  • 在Web Worker中可用
downlink
  • 带宽预估值
  • 单位为Mbit/s(注意是Mbit,不是MByte。)
  • 值也是四舍五入到最接近的25比特/秒的倍数(就是说这个值x%25===0,可以观察常见网络情况rtt和downlink表)
  • 一般越宽速度越快,也就是,信道上可以传输更多数。(吐槽一句,学过的通信原理还蛮有用。)
  • 值越大网速越快。类似高速一般比国道宽。
  • 在Web Worker中可用
草案(Draft)阶段NetworkInformation API

无论是rtt,还是downlink,都是这个草案中的内容。
除此之外还有downlinkMax,saveData,type等属性。
更多资料可以查询:NetworkInformation

如何检测网络变化去做出响应呢?

NetworkInformation继承自EventTarget,可以通过监听change事件去做一些响应。

例如可以获得网络状况的变更?

var connection = navigator.connection;
var type = connection.effectiveType;

function updateConnectionStatus() {
  console.log("网络状况从 " + type + " 切换至" + connection.effectiveType);
  type = connection.effectiveType;
}

connection.addEventListener('change', updateConnectionStatus);

监听变更之后,我们可以弹一个Modal提醒用户,也可以出一个Notice通知用户网络有变化,或者可以更高级得去自动切换清晰度(这个应该比较难)。

引出NetworkInformation的概念,只是想起一个抛砖引玉的作用。这种细粒度的网络状况检测,可以结合具体需求去具体实现。

在这篇博文中,我们只处理断网和连网两种情况,下面来看断网事件"offline"和连网事件"online"。

断网事件"offline"和连网事件"online"

浏览器有两个事件:"online" 和 "offline".
这两个事件会在浏览器在online mode和offline mode之间切换时,由页面的<body>发射出去。

事件会按照以下顺序冒泡:document.body -> document -> window。

事件是不能去取消的(开发者在代码上不能手动变为online或者offline,开发时使用开发者工具可以)。

注册上下线事件的几种方式

最最建议window+addEventListener的组合。

  • 通过window或document或document.body和addEventListener(Chrome80仅window有效)
  • 为document或document.body的.ononline或.onoffline属性设置一个js函数。(注意,使用window.ononline和window.onoffline会有兼容性的问题)
  • 也可以通过标签注册事件<body ononline="onlineCb" onoffline="offlineCb"></body>

例子

image

image

<div id="status"></div>
<div id="log"></div>
window.addEventListener('load', function() {
  var status = document.getElementById("status");
  var log = document.getElementById("log");

  function updateOnlineStatus(event) {
    var condition = navigator.onLine ? "online" : "offline";
    status.innerHTML = condition.toUpperCase();

    log.insertAdjacentHTML("beforeend", "Event: " + event.type + "; Status: " + condition);
  }

  window.addEventListener('online',  updateOnlineStatus);
  window.addEventListener('offline', updateOnlineStatus);
});

其中insertAdjacentHTML是在标签节点的邻近位置插入,可以查阅:DOM进阶之insertAdjacentHTML

断网处理项目实战

基于vue以及iView的Spin,Notice组件封装出离线处理组件,在需要到的页面引入即可。

思路和效果

只要做到断网提醒+遮罩,上线提醒-遮罩即可。

  • 监听offline,断网给出提醒和遮罩:网络已断开,请检查网络连接。
  • 监听online,连网给出提醒和遮罩:网络已连接。

offline.gif

断网处理组件使用

<OfflineHandle
    :offlineTitle = "断网处理标题"
    :desc="断网处理描述"
    :onlineTitle="连网提醒"
>
</OfflineHandle>

断网处理组件详情

<!--OfflineHandle.vue-->
<template>
  <div v-if="spin" class="offline-mark">
    <Spin size="large" fix>
      <h2>{{offlineTitle}}</h2>

      <p>{{desc}}</p>
    </Spin>
  </div>
</template>

<script>
export default {
  name: 'offline-handle',
  props: {
    offlineTitle: {
      type: String,
      default: '网络已断开,请检查网络连接。',
    },
    onlineTitle: {
      type: String,
      default: '网络已连接',
    },
    desc: {
      type: String,
      default: '',
    },
    duration: {
      type: Number,
      default: 4.5,
    },
  },
  data() {
    return {
      spin: false,
    };
  },
  mounted() {
    window.addEventListener('offline', this.eventHandle);
    window.addEventListener('online', this.eventHandle);
  },
  beforeDestroy() {
    window.removeEventListener('offline', this.eventHandle);
    window.removeEventListener('online', this.eventHandle);
  },
  methods: {
    eventHandle(event) {
      const type = event.type === 'offline' ? 'error' : 'success';
      this.$Notice[type]({
        title: type === 'error' ? this.offlineTitle : this.onlineTitle,
        desc: type === 'error' ? this.desc : '',
        duration: this.duration,
      });
      setTimeout(() => {
        this.spin = event.type === 'offline';
      }, 1500);
    },
  },
};
</script>

<style lang="scss" scoped>
.offline-mark {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background-color: #ccc;
  z-index: 9999;
  transition: position 2s;
}
/deep/.ivu-spin-fix {
  text-align: left;
  font-size: 20px;
  h2 {
    color: rgba(0, 0, 0, 0.8);
  }
  p {
    margin-top: 20px;
    color: red;
    font-weight: bold;
  }
}
</style>

发现

  • offline和online事件:window有效,document和document.body设置无效

手上的项目只运行在Chrome浏览器,只有为window设置offline和online才生效。
运行环境:"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.149 Safari/537.36

  • 为position增加2s的transition的避免屏闪

参考资料:

期待和大家交流,共同进步,欢迎大家加入我创建的与前端开发密切相关的技术讨论小组:

努力成为优秀前端工程师!
查看原文

赞 22 收藏 19 评论 8

菠萝油王子 赞了文章 · 3月27日

京喜小程序的高性能打造之路

背景

京喜小程序自去年双十一上线微信购物一级入口后,时刻迎接着亿级用户量的挑战,细微的体验细节都有可能被无限放大,为此,“极致的页面性能”、“友好的产品体验” 和 “稳定的系统服务” 成为了我们开发团队的最基本执行原则。

首页作为小程序的门户,其性能表现和用户留存率息息相关。因此,我们对京喜首页进行了一次全方位的升级改造,从加载、渲染和感知体验几大维度深挖小程序的性能可塑性。

除此之外,京喜首页在微信小程序、H5、APP 三端都有落地场景,为了提高研发效率,我们使用了 Taro 框架实现多端统一,因此下文中有部分内容是和 Taro 框架息息相关的。

怎么定义高性能?

提起互联网应用性能这个词,很多人在脑海中的词法解析就是,“是否足够快?”,似乎加载速度成为衡量系统性能的唯一指标。但这其实是不够准确的,试想一下,如果一个小程序加载速度非常快,用户花费很短时间就能看到页面的主体内容,但此时搜索框却无法输入内容,功能无法被流畅使用,用户可能就不会关心页面渲染有多快了。所以,我们不应该单纯考虑速度指标而忽略用户的感知体验,而应该全方位衡量用户在使用过程中能感知到的与应用加载相关的每个节点。

谷歌为 Web 应用定义了以用户为中心的性能指标体系,每个指标都与用户体验节点息息相关:

体验指标
页面能否正常访问?首次内容绘制 (First Contentful Paint, FCP)
页面内容是否有用?首次有效绘制 (First Meaningful Paint, FMP)
页面功能是否可用?可交互时间 (Time to Interactive, TTI)

其中,“是否有用?” 这个问题是非常主观的,对于不同场景的系统可能会有完全不一样的回答,所以 FMP 是一个比较模糊的概念指标,不存在规范化的数值衡量。

小程序作为一个新的内容载体,衡量指标跟 Web 应用是非常类似的。对于大多数小程序而言,上述指标对应的含义为:

  • FCP:白屏加载结束;
  • FMP:首屏渲染完成;
  • TTI:所有内容加载完成;

综上,我们已基本确定了高性能的概念指标,接下来就是如何利用数值指标来描绘性能表现。

小程序官方性能指标

小程序官方针对小程序性能表现制订了权威的数值指标,主要围绕 渲染表现setData 数据量元素节点数网络请求延时 这几个维度来给予定义(下面只列出部分关键指标):

  • 首屏时间不超过 5 秒;
  • 渲染时间不超过 500ms;
  • 每秒调用 setData 的次数不超过 20 次;
  • setData 的数据在 JSON.stringify 后不超过 256kb;
  • 页面 WXML 节点少于 1000 个,节点树深度少于 30 层,子节点数不大于 60 个;
  • 所有网络请求都在 1 秒内返回结果;
详见 小程序性能评分规则

我们应该把这一系列的官方指标作为小程序的性能及格线,不断地打磨和提升小程序的整体体验,降低用户流失率。另外,这些指标会直接作为小程序体验评分工具的性能评分规则(体验评分工具会根据这些规则的权重和求和公式计算出体验得分)。

我们团队内部在官方性能指标的基础上,进一步浓缩优化指标系数,旨在对产品体验更高要求:

  • 首屏时间不超过 2.5 秒;
  • setData 的数据量不超过 100kb;
  • 所有网络请求都在 1 秒内返回结果;
  • 组件滑动、长列表滚动无卡顿感;

体验评分工具

小程序提供了 体验评分工具(Audits 面板) 来测量上述的指标数据,其集成在开发者工具中,在小程序运行时实时检查相关问题点,并为开发者给出优化建议。

体验评分面板

以上截图均来自小程序官方文档

体验评分工具是目前检测小程序性能问题最直接有效的途径,我们团队已经把体验评分作为页面/组件是否能达到精品门槛的重要考量手段之一。

小程序后台性能分析

我们知道,体验评分工具是在本地运行小程序代码时进行分析,但性能数据往往需要在真实环境和大数据量下才更有说服力。恰巧,小程序管理平台小程序助手 为开发者提供了大量的真实数据统计。其中,性能分析面板从 启动性能运行性能网络性能 这三个维度分析数据,开发者可以根据客户端系统、机型、网络环境和访问来源等条件做精细化分析,非常具有考量价值。

小程序助手性能分析

其中,启动总耗时 = 小程序环境初始化 + 代码包加载 + 代码执行 + 渲染耗时

第三方测速系统

很多时候,宏观的耗时统计对于性能瓶颈点分析往往是杯水车薪,作用甚少,我们需要更细致地针对某个页面某些关键节点作测速统计,排查出暴露性能问题的代码区块,才能更有效地针对性优化。京喜小程序使用的是内部自研的测速系统,支持对地区、运营商、网络、客户端系统等多条件筛选,同时也支持数据可视化、同比分析数据等能力。京喜首页主要围绕 页面 onLoadonReady数据加载完成首屏渲染完成各业务组件首次渲染完成 等几个关键节点统计测速上报,旨在全链路监控性能表现。

内部测速系统

另外,微信为开发者提供了 测速系统,也支持针对客户端系统、网络类型、用户地区等维度统计数据,有兴趣的可以尝试。

了解小程序底层架构

为了更好地为小程序制订性能优化措施,我们有必要先了解小程序的底层架构,以及与 web 浏览器的差异性。

微信小程序是大前端跨平台技术的其中一种产物,与当下其他热门的技术 React Native、Weex、Flutter 等不同,小程序的最终渲染载体依然是浏览器内核,而不是原生客户端。

而对于传统的网页来说,UI 渲染和 JS 脚本是在同一个线程中执行,所以经常会出现 “阻塞” 行为。微信小程序基于性能的考虑,启用了双线程模型

  • 视图层:也就是 webview 线程,负责启用不同的 webview 来渲染不同的小程序页面;
  • 逻辑层:一个单独的线程执行 JS 代码,可以控制视图层的逻辑;

双线程模型图

上图来自小程序官方开发指南

然而,任何线程间的数据传输都是有延时的,这意味着逻辑层和视图层间通信是异步行为。除此之外,微信为小程序提供了很多客户端原生能力,在调用客户端原生能力的过程中,微信主线程和小程序双线程之间也会发生通信,这也是一种异步行为。这种异步延时的特性会使运行环境复杂化,稍不注意,就会产出效率低下的编码。

作为小程序开发者,我们常常会被下面几个问题所困扰:

  • 小程序启动慢;
  • 白屏时间长;
  • 页面渲染慢;
  • 运行内存不足;

接下来,我们会结合小程序的底层架构分析出这些问题的根本原因,并针对性地给出解决方案。

小程序启动太慢?

小程序启动阶段,也就是如下图所示的展示加载界面的阶段。

小程序加载界面

在这个阶段中(包括启动前后的时机),微信会默默完成下面几项工作:

1. 准备运行环境:

在小程序启动前,微信会先启动双线程环境,并在线程中完成小程序基础库的初始化和预执行。

小程序基础库包括 WebView 基础库和 AppService 基础库,前者注入到视图层中,后者注入到逻辑层中,分别为所在层级提供其运行所需的基础框架能力。

2. 下载小程序代码包:

在小程序初次启动时,需要下载编译后的代码包到本地。如果启动了小程序分包,则只有主包的内容会被下载。另外,代码包会保留在缓存中,后续启动会优先读取缓存。

3. 加载小程序代码包:

小程序代码包下载好之后,会被加载到适当的线程中执行,基础库会完成所有页面的注册。

在此阶段,主包内的所有页面 JS 文件及其依赖文件都会被自动执行。

在页面注册过程中,基础库会调用页面 JS 文件的 Page 构造器方法,来记录页面的基础信息(包括初始数据、方法等)。

4. 初始化小程序首页:

在小程序代码包加载完之后,基础库会根据启动路径找到首页,根据首页的基础信息初始化一个页面实例,并把信息传递给视图层,视图层会结合 WXML 结构、WXSS 样式和初始数据来渲染界面。

综合考虑,为了节省小程序的“点点点”时间(小程序的启动动画是三个圆点循环跑马灯),除了给每位用户发一台高配 5G 手机并顺带提供千兆宽带网络之外,还可以尽量 控制代码包大小,缩小代码包的下载时间。

无用文件、函数、样式剔除

经过多次业务迭代,无可避免的会存在一些弃用的组件/页面,以及不被调用的函数、样式规则,这些冗余代码会白白占据宝贵的代码包空间。而且,目前小程序的打包会将工程下所有文件都打入代码包内,并没有做依赖分析。

因此,我们需要及时地剔除不再使用的模块,以保证代码包空间利用率保持在较高水平。通过一些工具化手段可以有效地辅助完成这一工作。

  • 文件依赖分析

在小程序中,所有页面的路径都需要在小程序代码根目录 app.json 中被声明,类似地,自定义组件也需要在页面配置文件 page.json 中被声明。另外,WXML、WXSS 和 JS 的模块化都需要特定的关键字来声明依赖引用关系。

WXML 中的 importinclude

<!-- A.wxml -->
<template name='A'>
  <text>{{text}}</text>
</template>

<!-- B.wxml -->
<import data-original="A.wxml"/>
<template is="A" data="{{text: 'B'}}"/>
<!-- A.wxml -->
<text> A </text>

<!-- B.wxml -->
<include data-original="A.wxml"/>
<text> B </text>

WXSS 中的 @import

@import './A.wxss'

JS 中的 require/import

const A = require('./A')

所以,可以说小程序里的所有依赖模块都是有迹可循的,我们只需要利用这些关键字信息递归查找,遍历出文件依赖树,然后把没用的模块剔除掉。

  • JS、CSS Tree-Shaking

JS Tree-Shaking 的原理就是借助 Babel 把代码编译成抽象语法树(AST),通过 AST 获取到函数的调用关系,从而把未被调用的函数方法剔除掉。不过这需要依赖 ES module,而小程序最开始是遵循 CommonJS 规范的,这意味着是时候来一波“痛并快乐着”的改造了。

而 CSS 的 Tree-Shaking 可以利用 PurifyCSS 插件来完成。关于这两项技术,有兴趣的可以“谷歌一下”,这里就不铺开细讲了。

题外,京东的小程序团队已经把这一系列工程化能力集成在一套 CLI 工具中,有兴趣的可以看看这篇分享:小程序工程化探索

减少代码包中的静态资源文件

小程序代码包最终会经过 GZIP 压缩放在 CDN 上,但 GZIP 压缩对于图片资源来说效果非常低。如 JPGPNG 等格式文件,本身已经被压缩过了,再使用 GZIP 压缩有可能体积更大,得不偿失。所以,除了部分用于容错的图片必须放在代码包(譬如网络异常提示)之外,建议开发者把图片、视频等静态资源都放在 CDN 上。

需要注意,Base64 格式本质上是长字符串,和 CDN 地址比起来也会更占空间。

逻辑后移,精简业务逻辑

这是一个 “痛并快乐着” 的优化措施。“痛” 是因为需要给后台同学提改造需求,分分钟被打;“快乐” 则是因为享受删代码的过程,而且万一出 Bug 也不用背锅了...(开个玩笑)

通过让后台承担更多的业务逻辑,可以节省小程序前端代码量,同时线上问题还支持紧急修复,不需要经历小程序的提审、发布上线等繁琐过程。

总结得出,一般不涉及前端计算的展示类逻辑,都可以适当做后移。譬如京喜首页中的幕帘弹窗(如下图)逻辑,这里共有 10+ 种弹窗类型,以前的做法是前端从接口拉取 10+ 个不同字段,根据优先级和 “是否已展示”(该状态存储在本地缓存) 来决定展示哪一种,最后代码大概是这样的:

// 检查每种弹窗类型是否已展示
Promise.all([
  check(popup_1),
  check(popup_2),
  // ...
  check(popup_n)
]).then(result => {
  // 优先级排序
  const queue = [{
    show: result.popup_1
    data: data.popup_1
  }, {
    show: result.popup_2
    data: data.popup_2
  }, 
  // ...
  {
    show: result.popup_n
    data: data.popup_n
  }]
})

逻辑后移之后,前端只需负责拿幕帘字段做展示就可以了,代码变成这样:

this.setData({
  popup: data.popup
})

首页幕帘弹窗

复用模板插件

京喜首页作为电商系统的门户,需要应对各类频繁的营销活动、升级改版等,同时也要满足不同用户属性的界面个性化需求(俗称 “千人千面”)。如何既能减少为应对多样化场景而产生的代码量,又可以提升研发效率,成为燃眉之急。

类似于组件复用的理念,我们需要提供更丰富的可配置能力,实现更高的代码复用度。参考小时候很喜欢玩的 “乐高” 积木玩具,我们把首页模块的模板元素作颗粒度更细的划分,根据样式和功能抽象出一块块“积木”原料(称为插件元素)。当首页模块在处理接口数据时,会启动插件引擎逐个装载插件,最终输出个性化的模板样式,整个流程就好比堆积木。当后续产品/运营需要新增模板时,只要在插件库中挑选插件排列组合即可,不需要额外新增/修改组件内容,也更不会产生难以维护的 if / else 逻辑,so easy~

当然,要完成这样的插件化改造免不了几个先决条件:

  • 用户体验设计的统一。如果设计风格总是天差地别的,强行插件化只会成为累赘。
  • 服务端接口的统一。同上,如果得浪费大量的精力来兼容不同模块间的接口字段差异,将会非常蛋疼。

下面为大家提供部分例程来辅助理解。其中,use 方法会接受各类处理钩子最终拼接出一个 Function,在对应模块处理数据时会被调用。

// bi.helper.js

/**
 * 插件引擎
 * @param {function} options.formatName 标题处理钩子
 * @param {function} options.validList 数据校验器钩子
 */ 
const use = options => data => format(data)

/**
 * 预置插件库
 */ 
nameHelpers = {
  text: data => data.text,
  icon: data => data.icon
}
listHelpers = {
  single: list => list.slice(0, 1),
  double: list => list.slice(0, 2)
}

/**
 * “堆积木”
 */
export default {
  1000: use({
    formatName: nameHelpers.text,
    validList: listHelpers.single
  }),

  1001: use({
    formatName: nameHelpers.icon,
    validList: listHelpers.double
  })
}
<!-- bi.wxml -->
<!-- 各模板节点实现 -->
<template name="renderName">
  <view wx:if="{{type === 'text'}}"> text </view>
  <view wx:elif="{{type === 'icon'}}"> icon </view>
</template>

<view class="bi__name">
  <template is="renderName" data="{{...data.name}"/>
</view>
// bi.js
Component({
  ready() {
    // 根据 tpl 值选择解析函数
    const formatData = helper[data.tpl]
    this.setData({
      data: formatData(data)
    })
  }
})

分包加载

小程序启动时只会下载主包/独立分包,启用分包可以有效减少下载时间。(独立)分包需要遵循一些原则,详细的可以看官方文档:

部分页面 h5 化

小程序提供了 web-view 组件,支持在小程序环境内访问网页。当实在无法在小程序代码包中腾出多余空间时,可以考虑降级方案 —— 把部分页面 h5 化。

小程序和 h5 的通信可以通过 JSSDK 或 postMessage 通道来实现,详见 小程序开发文档

白屏时间过长?

白屏阶段,是指小程序代码包下载完(也就是启动界面结束)之后,页面完成首屏渲染的这一阶段,也就是 FMP (首次有效绘制)。

FMP 没法用标准化的指标定义,但对于大部分小程序来说,页面首屏展示的内容都需要依赖服务端的接口数据,那么影响白屏加载时间的主要由这两个元素构成:

  • 网络资源加载时间
  • 渲染时间

启用本地缓存

小程序提供了读写本地缓存的接口,数据存储在设备硬盘上。由于本地 I/O 读写(毫秒级)会比网络请求(秒级)要快很多,所以在用户访问页面时,可以优先从缓存中取上一次接口调用成功的数据来渲染视图,待网络请求成功后再覆盖最新数据重新渲染。除此之外,缓存数据还可以作为兜底数据,避免出现接口请求失败时页面空窗,一石二鸟。

但并非所有场景都适合缓存策略,譬如对数据即时性要求非常高的场景(如抢购入口)来说,展示老数据可能会引发一些问题。

小程序默认会按照 不同小程序不同微信用户 这两个维度对缓存空间进行隔离。诸如京喜小程序首页也采用了缓存策略,会进一步按照 数据版本号用户属性 来对缓存进行再隔离,避免信息误展示。

数据预拉取

小程序官方为开发者提供了一个在小程序冷启动时提前拉取第三方接口的能力:数据预拉取

关于冷启动和热启动的定义可以看 这里

数据预拉取的原理其实很简单,就是在小程序启动时,微信服务器代理小程序客户端发起一个 HTTP 请求到第三方服务器来获取数据,并且把响应数据存储在本地客户端供小程序前端调取。当小程序加载完成后,只需调用微信提供的 API wx.getBackgroundFetchData 从本地缓存获取数据即可。这种做法可以充分利用小程序启动和初始化阶段的等待时间,使更快地完成页面渲染。

京喜小程序首页已经在生产环境实践过这个能力,从每日千万级的数据分析得出,预拉取使冷启动时获取到接口数据的时间节点从 2.5s 加速到 1s(提速了 60%)。虽然提升效果非常明显,但这个能力依然存在一些不成熟的地方:

  • 预拉取的数据会被强缓存

    由于预拉取的请求最终是由微信的服务器发起的,也许是出于服务器资源限制的考虑,预拉取的数据会缓存在微信本地一段时间,缓存失效后才会重新发起请求。经过真机实测,在微信购物入口冷启动京喜小程序的场景下,预拉取缓存存活了 30 分钟以上,这对于数据实时性要求比较高的系统来说是非常致命的。

  • 请求体和响应体都无法被拦截

    由于请求第三方服务器是从微信的服务器发起的,而不是从小程序客户端发起的,所以本地代理无法拦截到这一次真实请求,这会导致开发者无法通过拦截请求的方式来区分获取线上环境和开发环境的数据,给开发调试带来麻烦。

    小程序内部接口的响应体类型都是 application/octet-stream,即数据格式未知,使本地代理无法正确解析。

  • 微信服务器发起的请求没有提供区分线上版和开发版的参数,且没有提供用户 IP 等信息

如果这几个问题点都不会影响到你的场景,那么可以尝试开启预拉取能力,这对于小程序首屏渲染速度是质的提升。

跳转时预拉取

为了尽快获取到服务端数据,比较常见的做法是在页面 onLoad 钩子被触发时发起网络请求,但其实这并不是最快的方式。从发起页面跳转,到下一个页面 onLoad 的过程中,小程序需要完成一些环境初始化及页面实例化的工作,耗时大概为 300 ~ 400 毫秒。

实际上,我们可以在发起跳转前(如 wx.navigateTo 调用前),提前请求下一个页面的主接口并存储在全局 Promise 对象中,待下个页面加载完成后从 Promise 对象中读取数据即可。

这也是双线程模型所带来的优势之一,不同于多页面 web 应用在页面跳转/刷新时就销毁掉 window 对象。

分包预下载

如果开启了分包加载能力,在用户访问到分包内某个页面时,小程序才会开始下载对应的分包。当处于分包下载阶段时,页面会维持在 “白屏” 的启动态,这用户体验是比较糟糕的。

幸好,小程序提供了 分包预下载 能力,开发者可以配置进入某个页面时预下载可能会用到的分包,避免在页面切换时僵持在 “白屏” 态。

非关键渲染数据延迟请求

这是关键渲染路径优化的其中一个思路,从缩短网络请求时延的角度加快首屏渲染完成时间。

关键渲染路径(Critical Rendering Path) 是指在完成首屏渲染的过程中必须发生的事件。

以京喜小程序如此庞大的小程序项目为例,每个模块背后都可能有着海量的后台服务作支撑,而这些后台服务间的通信和数据交互都会存在一定的时延。我们根据京喜首页的页面结构,把所有模块划分成两类:主体模块(导航、商品轮播、商品豆腐块等)和 非主体模块(幕帘弹窗、右侧挂件等)。

在初始化首页时,小程序会发起一个聚合接口请求来获取主体模块的数据,而非主体模块的数据则从另一个接口获取,通过拆分的手段来降低主接口的调用时延,同时减少响应体的数据量,缩减网络传输时间。

京喜首页浮层模块

分屏渲染

这也是关键渲染路径优化思路之一,通过延迟非关键元素的渲染时机,为关键渲染路径腾出资源。

类似上一条措施,继续以京喜小程序首页为例,我们在 主体模块 的基础上再度划分出 首屏模块(商品豆腐块以上部分) 和 非首屏模块(商品豆腐块及以下部分)。当小程序获取到主体模块的数据后,会优先渲染首屏模块,在所有首屏模块都渲染完成后才会渲染非首屏模块和非主体模块,以此确保首屏内容以最快速度呈现。

京喜首页分屏渲染

为了更好地呈现效果,上面 gif 做了降速处理

接口聚合,请求合并

在小程序中,发起网络请求是通过 wx.request 这个 API。我们知道,在 web 浏览器中,针对同一域名的 HTTP 并发请求数是有限制的;在小程序中也有类似的限制,但区别在于不是针对域名限制,而是针对 API 调用:

  • wx.request (HTTP 连接)的最大并发限制是 10 个;
  • wx.connectSocket (WebSocket 连接)的最大并发限制是 5 个;

超出并发限制数目的 HTTP 请求将会被阻塞,需要在队列中等待前面的请求完成,从而一定程度上增加了请求时延。因此,对于职责类似的网络请求,最好采用节流的方式,先在一定时间间隔内收集数据,再合并到一个请求体中发送给服务端。

图片资源优化

图片资源一直是移动端系统中抢占大流量的部分,尤其是对于电商系统。优化图片资源的加载可以有效地加快页面响应时间,提升首屏渲染速度。

  • 使用 WebP 格式

WebP 是 Google 推出的一种支持有损/无损压缩的图片文件格式,得益于更优的图像数据压缩算法,其与 JPG、PNG 等格式相比,在肉眼无差别的图片质量前提下具有更小的图片体积(据官方说明,WebP 无损压缩体积比 PNG 小 26%,有损压缩体积比 JPEG 小 25-34%)。

小程序的 image 组件 支持 JPG、PNG、SVG、WEBP、GIF 等格式。
  • 图片裁剪&降质

鉴于移动端设备的分辨率是有上限的,很多图片的尺寸常常远大于页面元素尺寸,这非常浪费网络资源(一般图片尺寸 2 倍于页面元素真实尺寸比较合适)。得益于京东内部强大的图片处理服务,我们可以通过资源的命名规则和请求参数来获取服务端优化后的图片:

裁剪成 100x100 的图片:https://{host}/s100x100_jfs/{file_path}

降质 70%:https://{href}!q70

  • 图片懒加载、雪碧图(CSS Sprite)优化

这两者都是比较老生常谈的图片优化技术,这里就不打算细讲了。

小程序的 image 组件 自带 lazy-load 懒加载支持。雪碧图技术(CSS Sprite)可以参考 w3schools 的教程。

  • 降级加载大图资源

在不得不使用大图资源的场景下,我们可以适当使用 “体验换速度” 的措施来提升渲染性能。

小程序会把已加载的静态资源缓存在本地,当短时间内再次发起请求时会直接从缓存中取资源(与浏览器行为一致)。因此,对于大图资源,我们可以先呈现高度压缩的模糊图片,同时利用一个隐藏的 <image> 节点来加载原图,待原图加载完成后再转移到真实节点上渲染。整个流程,从视觉上会感知到图片从模糊到高清的过程,但与对首屏渲染的提升效果相比,这点体验落差是可以接受的。

下面为大家提供部分例程:

<!-- banner.wxml -->
<image data-original="{{url}}" />

<!-- 图片加载器 -->
<image
  style="width:0;height:0;display:none"
  data-original="{{preloadUrl}}"
  bindload="onImgLoad"
  binderror="onErrorLoad"
/>
// banner.js
Component({
  ready() {
    this.originUrl = 'https://path/to/picture'  // 图片源地址
    this.setData({
      url: compress(this.originUrl)             // 加载压缩降质的图片
      preloadUrl: this.originUrl                // 预加载原图
    })
  },
  methods: {
    onImgLoad() {
      this.setData({
        url: this.originUrl                       // 加载原图
      })
    }
  }
})
注意,具有 display: none 样式的 <image> 标签只会加载图片资源,但不渲染。

京喜首页的商品轮播模块也采用了这种降级加载方案,在首屏渲染时只会加载第一帧降质图片。以每帧原图 20~50kb 的大小计算,这一措施可以在初始化阶段节省掉几百 kb 的网络资源请求。

Banner 大图降级加载

为了更好地呈现效果,上面 gif 做了降速处理

骨架屏

一方面,我们可以从降低网络请求时延、减少关键渲染的节点数这两个角度出发,缩短完成 FMP(首次有效绘制)的时间。另一方面,我们也需要从用户感知的角度优化加载体验。

“白屏” 的加载体验对于首次访问的用户来说是难以接受的,我们可以使用尺寸稳定的骨架屏,来辅助实现真实模块占位和瞬间加载。

骨架屏目前在业界被广泛应用,京喜首页选择使用灰色豆腐块作为骨架屏的主元素,大致勾勒出各模块主体内容的样式布局。由于微信小程序不支持 SSR(服务端渲染),使动态渲染骨架屏的方案难以实现,因此京喜首页的骨架屏是通过 WXSS 样式静态渲染的。

有趣的是,京喜首页的骨架屏方案经历了 “统一管理”“(组件)独立管理” 两个阶段。出于避免对组件的侵入性考虑,最初的骨架屏是由一个完整的骨架屏组件统一管理的:

<!-- index.wxml -->
<skeleton wx:if="{{isLoading}}"></skeleton>
<block wx:else>
  页面主体
</block>

但这种做法的维护成本比较高,每次页面主体模块更新迭代,都需要在骨架屏组件中的对应节点同步更新(譬如某个模块的尺寸被调整)。除此之外,感官上从骨架屏到真实模块的切换是跳跃式的,这是因为骨架屏组件和页面主体节点之间的关系是整体条件互斥的,只有当页面主体数据 Ready(或渲染完毕)时才会把骨架屏组件销毁,渲染(或展示)主体内容。

为了使用户感知体验更加丝滑,我们把骨架屏元素拆分放到各个业务组件中,骨架屏元素的显示/隐藏逻辑由业务组件内部独立管理,这就可以轻松实现 “谁跑得快,谁先出来” 的并行加载效果。除此之外,骨架屏元素与业务组件共用一套 WXML 节点,且相关样式由公共的 sass 模块集中管理,业务组件只需要在适当的节点挂上 skeletonskeleton__block 样式块即可,极大地降低了维护成本。

<!-- banner.wxml -->
<view class="{{isLoading ? 'banner--skeleton' : ''}}">
  <view class="banner_wrapper"></view>
</view>
// banner.scss
.banner--skeleton {
  @include skeleton;
  .banner_wrapper {
    @include skeleton__block;
  }
}

京喜首页骨架屏

上面的 gif 在压缩过程有些小问题,大家可以直接访问【京喜】小程序体验骨架屏效果。

如何提升渲染性能?

当调用 wx.navigateTo 打开一个新的小程序页面时,小程序框架会完成这几步工作:

1. 准备新的 webview 线程环境,包括基础库的初始化;

2. 从逻辑层到视图层的初始数据通信;

3. 视图层根据逻辑层的数据,结合 WXML 片段构建出节点树(包括节点属性、事件绑定等信息),最终与 WXSS 结合完成页面渲染;

由于微信会提前开始准备 webview 线程环境,所以小程序的渲染损耗主要在后两者 数据通信节点树创建/更新 的流程中。相对应的,比较有效的渲染性能优化方向就是:

  • 降低线程间通信频次;
  • 减少线程间通信的数据量;
  • 减少 WXML 节点数量;

合并 setData 调用

尽可能地把多次 setData 调用合并成一次。

我们除了要从编码规范上践行这个原则,还可以通过一些技术手段降低 setData 的调用频次。譬如,把同一个时间片(事件循环)内的 setData 调用合并在一起,Taro 框架就使用了这个优化手段。

在 Taro 框架下,调用 setState 时提供的对象会被加入到一个数组中,当下一次事件循环执行的时候再把这些对象合并一起,通过 setData 传递给原生小程序。

// 小程序里的时间片 API
const nextTick = wx.nextTick ? wx.nextTick : setTimeout;

只把与界面渲染相关的数据放在 data

不难得出,setData 传输的数据量越多,线程间通信的耗时越长,渲染速度就越慢。根据微信官方测得的数据,传输时间和数据量大体上呈正相关关系:

数据传输时间与数据量关系图

上图来自小程序官方开发指南

所以,与视图层渲染无关的数据尽量不要放在 data 中,可以放在页面(组件)类的其他字段下。

应用层的数据 diff

每当调用 setData 更新数据时,会引起视图层的重新渲染,小程序会结合新的 data 数据和 WXML 片段构建出新的节点树,并与当前节点树进行比较得出最终需要更新的节点(属性)。

即使小程序在底层框架层面已经对节点树更新进行了 diff,但我们依旧可以优化这次 diff 的性能。譬如,在调用 setData 时,提前确保传递的所有新数据都是有变化的,也就是针对 data 提前做一次 diff。

Taro 框架内部做了这一层优化。在每次调用原生小程序的 setData 之前,Taro 会把最新的 state 和当前页面实例的 data 做一次 diff,筛选出有必要更新的数据再执行 setData

附 Taro 框架的 数据 diff 规则

去掉不必要的事件绑定

当用户事件(如 ClickTouch 事件等)被触发时,视图层会把事件信息反馈给逻辑层,这也是一个线程间通信的过程。但,如果没有在逻辑层中绑定事件的回调函数,通信将不会被触发。

所以,尽量减少不必要的事件绑定,尤其是像 onPageScroll 这种会被频繁触发的用户事件,会使通信过程频繁发生。

去掉不必要的节点属性

组件节点支持附加自定义数据 dataset(见下面例子),当用户事件被触发时,视图层会把事件 targetdataset 数据传输给逻辑层。那么,当自定义数据量越大,事件通信的耗时就会越长,所以应该避免在自定义数据中设置太多数据。

<!-- wxml -->
<view
  data-a='A'
  data-b='B'
  bindtap='bindViewTap'
>
  Click Me!
</view>
// js
Page({
  bindViewTap(e) {
    console.log(e.currentTarget.dataset)
  }
})

适当的组件颗粒度

小程序的组件模型与 Web Components 标准中的 ShadowDOM 非常类似,每个组件都有独立的节点树,拥有各自独立的逻辑空间(包括独立的数据、setData 调用、createSelectorQuery 执行域等)。

不难得出,如果自定义组件的颗粒度太粗,组件逻辑过重,会影响节点树构建和新/旧节点树 diff 的效率,从而影响到组件内 setData 的性能。另外,如果组件内使用了 createSelectorQuery 来查找节点,过于庞大的节点树结构也会影响查找效率。

我们来看一个场景,京喜首页的 “京东秒杀” 模块涉及到一个倒计时特性,是通过 setInterval 每秒调用 setData 来更新表盘时间。我们通过把倒计时抽离出一个基础组件,可以有效降低频繁 setData 时的性能影响。

京东秒杀

适当的组件化,既可以减小数据更新时的影响范围,又能支持复用,何乐而不为?诚然,并非组件颗粒度越细越好,组件数量和小程序代码包大小是正相关的。尤其是对于使用编译型框架(如 Taro)的项目,每个组件编译后都会产生额外的运行时代码和环境 polyfill,so,为了代码包空间,请保持理智...

事件总线,替代组件间数据绑定的通信方式

WXML 数据绑定是小程序中父组件向子组件传递动态数据的较为常见的方式,如下面例程所示:Component A 组件中的变量 ab 通过组件属性传递给 Component B 组件。在此过程中,不可避免地需要经历一次 Component A 组件的 setData 调用方可完成任务,这就会产生线程间的通信。“合情合理”,但,如果传递给子组件的数据只有一部分是与视图渲染有关呢?

<!-- Component A -->
<component-b prop-a="{{a}}" prop-b="{{b}}" />
// Component B
Component({
  properties: {
    propA: String,
    propB: String,
  },
  methods: {
    onLoad: function() {
      this.data.propA
      this.data.propB
    }
  }
})

推荐一种特定场景下非常便捷的做法:通过事件总线(EventBus),也就是发布/订阅模式,来完成由父向子的数据传递。其构成非常简单(例程只提供关键代码...):

  • 一个全局的事件调度中心

    class EventBus {
      constructor() {
        this.events = {}
      }
    
      on(key, cb) { this.events[key].push(cb) }
    
      trigger(key, args) { 
        this.events[key].forEach(function (cb) {
          cb.call(this, ...args)
        })
      }
      
      remove() {}
    }
    
    const event = new EventBus()
  • 事件订阅者

    // 子组件
    Component({
      created() {
        event.on('data-ready', (data) => { this.setData({ data }) })
      }
    })
  • 事件发布者

    // Parent
    Component({
      ready() {
        event.trigger('data-ready', data)
      }
    })

子组件被创建时事先监听数据下发事件,当父组件获取到数据后触发事件把数据传递给子组件,这整个过程都是在小程序的逻辑层里同步执行,比数据绑定的方式速度更快。

但并非所有场景都适合这种做法。像京喜首页这种具有 “数据单向传递”“展示型交互” 特性、且 一级子组件数量庞大 的场景,使用事件总线的效益将会非常高;但若是频繁 “双向数据流“ 的场景,用这种方式会导致事件交错难以维护。

题外话,Taro 框架在处理父子组件间数据传递时使用的是观察者模式,通过 Object.defineProperty 绑定父子组件关系,当父组件数据发生变化时,会递归通知所有后代组件检查并更新数据。这个通知的过程会同步触发数据 diff 和一些校验逻辑,每个组件跑一遍大概需要 5 ~ 10 ms 的时间。所以,如果组件量级比较大,整个流程下来时间损耗还是不小的,我们依旧可以尝试事件总线的方案。

组件层面的 diff

我们可能会遇到这样的需求,多个组件之间位置不固定,支持随时随地灵活配置,京喜首页也存在类似的诉求。

京喜首页主体可被划分为若干个业务组件(如搜索框、导航栏、商品轮播等),这些业务组件的顺序是不固定的,今天是搜索框在最顶部,明天有可能变成导航栏在顶部了(夸张了...)。我们不可能针对多种顺序可能性提供多套实现,这就需要用到小程序的自定义模板 <template>

实现一个支持调度所有业务组件的模板,根据后台下发的模块数组按序循环渲染模板,如下面例程所示。

<!-- index.wxml -->
<template name="render-component">
  <search-bar wx:if="{{compId === 'SearchBar'}}" floor-id="{{index}}" />
  <nav-bar wx:if="{{compId === 'NavBar'}}" floor-id="{{index}}" />
  <banner wx:if="{{compId === 'Banner'}}" floor-id="{{index}}" />
  <icon-nav wx:if="{{compId === 'IconNav'}}" floor-id="{{index}}" />
</template>

<view
  class="component-wrapper"
  wx:for="{{comps}}"
  wx:for-item="comp"
>
  <template is="render-component" data="{{...comp}}"/>
</view>
// search-bar.js
Component({
  properties: {
    floorId: Number,
  },
  created() {
    event.on('data-ready', (comps) => {
      const data = comps[this.data.floorId] // 根据楼层位置取数据
    })
  }
})

貌似非常轻松地完成需求,但值得思考的是:如果组件顺序调整了,所有组件的生命周期会发生什么变化?

假设,上一次渲染的组件顺序是 ['search-bar','nav-bar','banner', 'icon-nav'],现在需要把 nav-bar 组件去掉,调整为 ['search-bar','banner', 'icon-nav']。经实验得出,当某个组件节点发生变化时,其前面的组件不受影响,其后面的组件都会被销毁重新挂载。

原理很简单,每个组件都有各自隔离的节点树(ShadowTree),页面 body 也是一个节点树。在调整组件顺序时,小程序框架会遍历比较新/旧节点树的差异,于是发现新节点树的 nav-bar 组件节点不见了,就认为该(树)分支下从 nav-bar 节点起发生了变化,往后节点都需要重渲染。

但实际上,这里的组件顺序是没有变化的,丢失的组件按道理不应该影响到其他组件的正常渲染。所以,我们在 setData 前先进行了新旧组件列表 diff:如果 newList 里面的组件是 oldList 的子集,且相对顺序没有发生变化,则所有组件不重新挂载。除此之外,我们还要在接口数据的相应位置填充上空数据,把该组件隐藏掉,done。

通过组件 diff 的手段,可以有效降低视图层的渲染压力,如果有类似场景的朋友,也可以参考这种方案。

内存占用过高?

想必没有什么会比小程序 Crash 更影响用户体验了。

当小程序占用系统资源过高,就有可能会被系统销毁或被微信客户端主动回收。应对这种尴尬场景,除了提示用户提升硬件性能之外(譬如来京东商城买新手机),还可以通过一系列的优化手段降低小程序的内存损耗。

内存不足弹窗提示

内存预警

小程序提供了监听内存不足告警事件的 API:wx.onMemoryWarning,旨在让开发者收到告警时及时释放内存资源避免小程序 Crash。然而对于小程序开发者来说,内存资源目前是无法直接触碰的,最多就是调用 wx.reLaunch 清理所有页面栈,重载当前页面,来降低内存负荷(此方案过于粗暴,别冲动,想想就好...)。

不过内存告警的信息收集倒是有意义的,我们可以把内存告警信息(包括页面路径、客户端版本、终端手机型号等)上报到日志系统,分析出哪些页面 Crash 率比较高,从而针对性地做优化,降低页面复杂度等等。

回收后台页面计时器

根据双线程模型,小程序每一个页面都会独立一个 webview 线程,但逻辑层是单线程的,也就是所有的 webview 线程共享一个 JS 线程。以至于当页面切换到后台态时,仍然有可能抢占到逻辑层的资源,譬如没有销毁的 setIntervalsetTimeout 定时器:

// Page A
Page({
  onLoad() {
    let i = 0
    setInterval(() => { i++ }, 100)
  }
})
即使如小程序的 <swiper> 组件,在页面进入后台态时依然是会持续轮播的。

正确的做法是,在页面 onHide 的时候手动把定时器清理掉,有必要时再在 onShow 阶段恢复定时器。坦白讲,区区一个定时器回调函数的执行,对于系统的影响应该是微不足道的,但不容忽视的是回调函数里的代码逻辑,譬如在定时器回调里持续 setData 大量数据,这就非常难受了...

避免频发事件中的重度内存操作

我们经常会遇到这样的需求:广告曝光、图片懒加载、导航栏吸顶等等,这些都需要我们在页面滚动事件触发时实时监听元素位置或更新视图。在了解小程序的双线程模型之后不难发现,页面滚动时 onPageScroll 被频发触发,会使逻辑层和视图层发生持续通信,若这时候再 “火上浇油” 调用 setData 传输大量数据,会导致内存使用率快速上升,使页面卡顿甚至 “假死”。所以,针对频发事件的监听,我们最好遵循以下原则:

  • onPageScroll 事件回调使用节流;
  • 避免 CPU 密集型操作,譬如复杂的计算;
  • 避免调用 setData,或减小 setData 的数据量;
  • 尽量使用 IntersectionObserver 来替代 SelectorQuery,前者对性能影响更小;

大图、长列表优化

小程序官方文档 描述,大图片和长列表图片在 iOS 中会引起 WKWebView 的回收,导致小程序 Crash。

对于大图片资源(譬如满屏的 gif 图)来说,我们只能尽可能对图片进行降质或裁剪,当然不使用是最好的。

对于长列表,譬如瀑布流,这里提供一种思路:我们可以利用 IntersectionObserver 监听长列表内组件与视窗之间的相交状态,当组件距离视窗大于某个临界点时,销毁该组件释放内存空间,并用等尺寸的骨架图占坑;当距离小于临界点时,再取缓存数据重新加载该组件。

然而无可避免地,当用户快速滚动长列表时,被销毁的组件可能来不及加载完,视觉上就会出现短暂的白屏。我们可以适当地调整销毁阈值,或者优化骨架图的样式来尽可能提升体验感。

小程序官方提供了一个 长列表组件,可以通过 npm 包的方式引入,有兴趣的可以尝试。

总结

结合上述的种种方法论,京喜小程序首页进行全方位升级改造之后给出了答卷:

1. Audits 审计工具的性能得分 86

2. 优化后的首屏渲染完成时间(FMP):

优化后的首屏渲染时间

3. 优化前后的测速数据对比:

优化前后的测速数据对比

然而,业务迭代在持续推进,多样化的用户场景徒增不减,性能优化将成为我们日常开发中挥之不去的原则和主题。本文以微信小程序开发中与性能相关的问题为出发点,基于小程序的底层框架原理,探究小程序性能体验提升的各种可能性,希望能为各位小程序开发者带来参考价值。

参考


欢迎关注凹凸实验室博客:aotu.io

或者关注凹凸实验室公众号(AOTULabs),不定时推送文章:

欢迎关注凹凸实验室公众号

查看原文

赞 26 收藏 18 评论 1

菠萝油王子 赞了文章 · 3月9日

面向面试编程,面向掘金面试

一九年已末,二零年将至。

我使用 curljq 一行简单的命令爬取了掘金的面试集合榜单,有兴趣的同学可以看看爬取过程: 使用 jq 与 sed 制作掘金面试文章排行榜,可以提高你使用命令行的乐趣

关于前端,后端,移动端的面试,这里统统都有,希望可以在面试的过程中帮助到你。另外我也有一个仓库 日问 来记录前后端以及 devops 一些有意思的问题,欢迎交流

<!--more-->

前端

后端

Android/IOS


我是山月,一个喜欢跑步与爬山的程序员,我会定期分享全栈文章在个人公众号中。如果你对全栈面试,前端工程化,graphql,devops,个人服务器运维以及微服务感兴趣的话,可以关注我

如果你对全栈面试,前端工程化,graphql,devops,个人服务器运维以及微服务感兴趣的话,可以关注我

查看原文

赞 12 收藏 9 评论 0

认证与成就

  • 获得 207 次点赞
  • 获得 12 枚徽章 获得 0 枚金徽章, 获得 1 枚银徽章, 获得 11 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2016-09-27
个人主页被 981 人浏览