【第1830期】2019年京东PLUS会员前端开发之路
前言
项目经验总结。今日早读文章由京东@京东用户体验设计部投稿分享。
京东用户体验设计部-前端开发部现有前端开发人员 50 左右,主要为京东零售集团、京东健康提供 WEB 前端开发、APP RN开发,小程序开发、小游戏开发、H5开发等能力支持。
正文从这开始~~
时光如梭,白驹过隙,2019年转瞬即逝。这一年对于 PLUS 会员项目前端同学来说是坎坷和充实的,如白岩松所说,痛并快乐着。回首望去,异业合作权益的陆续接入,6.18大促和双11活动的需求扎堆,中间穿插部分机型首屏白页等问题的困扰,在一阵慌乱之后,我们逐渐稳住了阵脚。在完成日常需求的同时,基于原有框架,对项目的稳定性、加载、体验、开发效率等方面做了更多夯实。
2019年,累计支持了近90多个大小需求。主要分为四类:产品升级、异业合作、促销活动、紧急需求。在这些需求中包含了经典卡新增用户权益的需求,如健康、读书、快递券、95折商品权益。也大大扩展了和其他异业的联合,如腾讯视频、携程旅游、酷狗音乐等。此外还有研发侧发起的性能优化、用户体验优化等。
PLUS 会员项目横跨三个职场,研发和项目侧在北京(2处职场),产品在上海团队。沟通显得尤为重要,任何一个微小的问题,都可能在用户中被放大,每一次操作上线都需小心谨慎,如履薄冰。
项目功能复杂,状态繁琐,业务逻辑常常内含玄机,一个改动有可能会影响数十个状态,牵一发而动全身。整页纸都容不下一个弹窗的交互逻辑,改一处而崩全部。此外,时常并行十来个需求,每周需要数次合并功能上线,还有那不得不说的恐怖魔咒:上线频繁通宵~~~
然而,破茧成蝶,涅槃重生,哪一个不是需要痛到极致之后才能得到升华。在经历了疯狂的成长之后,PLUS 会员项目像个横冲直撞的孩子,我们决心重新梳理,改变现状,让他健康成长。本文将从性能优化,开发效率,流程优化这三个方面阐述,如图所示:
什么是PLUS会员
首先请允许我简单介绍下 PLUS,京东为向核心客户提供更优质的购物体验,特别推出京东 PLUS 会员,包含十几项权益:
购物10倍返京豆:PLUS 会员人均 1 年最高可返 220 元价值京豆,一年买几个大件电器会员费就回本了。
自营免运费:每月可获得 5 张运费券,一年就可获得 60 张运费券。另外还有全年 36 元快递券。
会员价商品:可尊享 500 万件京东商城商品会员价,以更低的价格购买商品。
上门退换货:会员享受自营商品售后服务(退货、换货、维修)免运费和免费上门取件“双免”服务。
海量生活特权:吃甜品买一赠一,看电影打 5 折,唱KTV 2 小时免单,吃个火锅还免费赠你一盘羊肉卷,还有好多好多特权,统!统!不!要!钱!
...
更多权益就不赘述了,更多请戳文末链接[1]
此外,购买京东 PLUS 会员联名卡,就拥有了两个以上的会员权益,而且价格不要你998!只要148!买不了吃亏买不了上当,嘘~ 低调,毕竟大家都是有会员身份的人~~
孩子:妈妈,我想买京东 PLUS 会员~ 妈妈:买,买个联名卡,一个顶两个,够不够?孩子:够了,谢谢妈妈,妈妈真好!
接下来就带大家看一下 PLUS 会员部分页面的庐山真面目吧:
前端架构
PLUS 会员项目 18 年开始选用 Vue 技术栈,使用了团队自行开发的 Gaea 构建工具和 NutUI 组件库,此外引入了 Carefree,SMock,Vuex,TS,PWA等。
[Gaea构建工具][2],是我们团队自主开发的一套 Vue 技术栈构建工具,基于 Node.js、Webpack 模版工程等的 Vue 技术栈的整套解决方案,包含了开发、调试、打包上线完整的工作流程。极大的提高了工作效率,目前团队所有 Vue 业务都使用该脚手架。
[NutUI组件库][3],是一套京东风格的轻量级移动端 Vue 组件库,由我们团队历时数年打磨,目前有30+京东移动端项目使用,github上得到1.5k+的star。该 Vue 组件库提供大量的可复用的 Vue 基础组件,极大的便利了 PLUS 项目的开发。
[Carefree][4],一套不依赖 wifi 热点的移动 web 真机测试一站式解决方案,是我们团队在日常开发中发现真机很依赖电脑发出热点才能进行调试的痛点,针对这一问题,旨在摆脱wifi热点束缚,让移动web真机测试自由自在而自主研发的一套解决方案。
[SMock][5],由团队自主研发,针对项目前期尚无数据的问题,分析需要 mock 的文档,输出相应的 mock 数据,并启动 node 服务,供前端开发时调试使用,提高前端开发效率,支持跨域访问。
此外,我们引入了 Vuex 做状态管理,PWA 做数据缓存和离线应用,也尝试使用 TypeScript 语言开发。
性能优化
提高页面渲染性能,改善用户体验,是团队一直重视的方向。首页已经使用了很多方法来优化性能,包括使用骨架屏减少用户等待体验、核心数据直接出以此减少请求接口时间,楼层内部滚动懒加载,使用 Webp 缩小图片大小等等。所以一开始我们犹如站在巨人的肩膀上,一动不动,生怕一不小心失足掉下去~~~
提高页面渲染性能
事情的起因,是在半年前,页面卡死白屏问题会时不时的冒出来,像个幽灵一样困扰着我们。而且是随机偶然出现的,几千万用户中总会有几部手机被幸运之神选中————让其出现白屏、卡死、气的拿手机砸墙!可是同样的手机、同样的配置、同样的 APP 版本,在我们这边却总是安然无恙。在此期间我们尝试了很多方法排查,比如~~睡觉、喝茶、聊天~~ 搞笑,怎么可能?我们回访用户咨询复现步骤,联合 WebView 研发排查原因,根据用户 pin 查询崩溃信息,调查埋点上报等等,奈何是偶现问题收效甚微,并且仅仅通过一个白屏图片,很难定位问题究竟出现在哪里?即使修改怀疑影响的代码,也无法验证是否得到修复。
于是茶不思饭不想,百思不得其解。这难道是量子力学中薛定谔的猫?事物最后变化的结果是根据人为观测而变化的?那解决后是不是可以获得下一届的诺贝尔物理学奖呢?想想突然还有点小激动呢...
第一阶段:问题初解决
好在皇天不负有心人,在切换了多种网速,模拟各种环境的尝试下,发现在页面卡死的情况下,往往会上报一个错误,就是依赖的变量没有定义导致的。
我们知道基于 Webpack 脚手架和 Vue 框架开发的项目,会打包生成一个依赖 JS 文件,一个业务 JS 文件。为了便于上线,当时采用动态生成静态资源的链接。而浏览器往往会并行下载多个外部静态资源,如果业务 JS 文件一旦先于依赖 JS 文件执行,则找不到依赖变量,结局只有一个:那就是页面卡死!
好了,真凶既然已经锁定,接下来就是如何修改了,鉴于动态生成静态资源外链有可能导致 JS 执行顺序的不确定,我们又改回成了 Webpack 打包自动注入静态资源链接的形式。然而,事情真的就这么结束了吗?
第二阶段:新的问题
果然,之后白屏像幽灵一样,依然偶现!虽然比之前少了一些客诉,但仍然会不时的零星出现几例。并且随着时间的推移,需求越来越多,每次都要并行开发很多个需求。为了并行开发代码不冲突,以及避免每次上线时客户端缓存旧文件,所以每次上线都是使用新版本号的文件。但是这样导致即使改动一点代码,也要前端、后端一起上线,在快速迭代需求的情况下,每次改动都要后端研发修改十几个静态资源的版本号,前、后端研发不堪其扰。
于是,我们决定使用公司的头尾系统(前端在头尾系统中编辑带有静态资源路径的文件,后端在 HTML 中引入系统中对应的文件),把静态资源的路径放在头尾系统中,每次上线新文件后,前端推送头尾,一键部署静态资源到各个服务器。然而好事多磨,由于维护多个页面,每个页面对应两个静态文件:一个是 JS 文件,一个是 CSS 文件,分别放在 <body>
的尾部和 <head>
之中。导致每次改动头尾文件都要涉及到十几个文件的修改(哎,好想念当年后端同学帮忙修改版本号的日子~),维护成本剧增,且随着页面的增多,这一现象有增无减;
此外,为了缓解用户在等待页面渲染中的焦急心情,早已经在返回的 HTML 中增加了骨架屏。然而遗憾的是,我们发现在 IOS 系统中骨架屏并不会出现,而是直接呈现出最终渲染的页面。
基于解决以上问题,我们没有像遇见危险就把头埋进沙子中的鸵鸟一样逃避,而是决定主动优化,去啃下这个硬骨头!
第三阶段:山穷水尽疑无路,柳暗花明又一村
常规的浏览器渲染机制,客户端从服务器请求回 HTML 之后,HTML 解析器就会从 HTML 文件的头部到尾部,一个个地遍历这些节点。当这些节点是普通节点的话,HTML 解析器就会将这些节点加入到 DOM 树中。当这些节点是 JS 代码的话,HTML 解析器就会将控制权交给 JS 解析器。如果这些节点是 CSS 代码的话,HTML 解析器就会将控制权交给 CSS 解析器。不过,当外联的 JS 代码和 CSS 代码还没从服务器传到浏览器的时候,这个时候如果 DOM 树上有可视元素的话,浏览器通常会选择在这个时候,将一些内容提前渲染到屏幕上来。但是问题就在于 IOS 系统中的 WebView 并没有将首屏直出的这部分 HTML 页面显示出来。HTML 中直出的 DOM 结构会等待外部 CSS 和 JS 加载执行后才统一进行渲染。
为了验证这一猜想,我们可以把 HTML 代码化简到最少的代码:只挂载元素中放置一个红色 div,在其外面放一个绿色 div。
<!DOCTYPE html>
<html lang="zh-cmn-Hans">
<head>
<meta content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=0,viewport-fit=cover" name="viewport">
<title>PLUS会员</title>
<link rel="stylesheet" type="text/css" href="//static.360buyimg.com/exploit/mplus/4.3.2/v4/css/index.css">
<script>document.documentElement.style.fontSize = 320 / 7.5 + "px"</script>
</head>
<body>
<div id="app">
<div style="width:100%;height: 200px;background:red;"></div>
</div>
<div style="width: 100%;height: 200px;background:green;"></div>
<script type="text/javascript" src="//static.360buyimg.com/exploit/mplus/lib/4.1/vendor.dll.js"></script>
<script type="text/javascript" src="//static.360buyimg.com/exploit/mplus/4.3.2/v4/js/index.js"></script>
</body>
</html>
然后在IOS系统中效果如下图所示:
可以看到,由于红色 div 位于挂载元素之中,直接被生成的 Vue 页面替换掉了;绿色 div 是和 Vue 页面一起渲染出来,由于完整的页面需要调用很多接口判断,所以显示的稍晚。但是无论哪一个 div 都是在 Vue 最初的页面渲染前不会显示的。
那么 IOS 端的骨架屏面临着一样的境遇,如果放在挂载元素里面,根本不会显示;如果放在挂载元素外面,就会和 Vue 渲染页面同时出现,也就失去了骨架屏的意义。
为了避免使用外部下载 CSS 和 JS,导致 IOS 系统影响首屏直出骨架屏的渲染,仍要采用待骨架屏加载完之后,再去动态引入 JS 和 CSS,由于首页骨架屏的代码量本身并不大,待其加载后再去动态引入静态资源,时间影响不大(后面有实验数据)。但是又回到了第一阶段中,动态生成静态资源导致的 JS 执行顺序问题。
所以,绕了一圈,无解,剧终~
等等,开玩笑,此可轻易言败!经过多次的尝试,我们采用了引入事件监听的方法,核心思想是待依赖文件都加载完之后,再去执行业务代码。好处是:
保证了静态资源的并行下载,不影响原有下载方式;
保证了代码执行的顺序;
在此过程中需要注意:
HTML 中不要写 ES6 语法的代码,避免因为没有 babel 的编译导致低版本浏览器不兼容的问题;
生成多个 JS 标签,先挂载到虚拟的节点对象 再统一挂载到 body 上,减少页面的重绘;
var oFragment = document.createDocumentFragment();
var indexScript1 = document.createElement('script');
var indexScript2= document.createElement('script');
oFragment.appendChild(indexScript1);
oFragment.appendChild(indexScript2);
body.appendChild(oFragment);
兼容本地开发,本地开发的时候仍然使用 Webpack 自动引入静态资源的形式,打包和线上的再用动态生成静态资源的方式:
<script type="text/javascript">
var staticVersion = 'dev';
</script>
<script type="text/javascript">
//头尾系统的版本号,重新定义 staticVersion
var staticVersion = '1.0.0';
</script>
<script type="text/javascript">
window.onload = function(){
if(staticVersion != 'dev'){
//动态生成代码
}
}
</script>
经过实验,在正常网络情况下,IOS系统中页面虽然渲染平均慢了 266ms,但是骨架屏不再等待 CSS 和 JS 的加载,提前了1000ms左右出现。在弱网情况下,页面渲染时间相差无几,骨架屏依然可以在 HTML 返回后快速渲染出来。
解决了以下问题:
优化静态资源执行顺序问题,避免了因为业务代码早于依赖文件执行而出现白屏卡死问题;
IOS 系统和安卓系统都可以在 HTML 返回后立即展示骨架屏;
减少骨架屏渲染时间,无需等待外部静态资源的下载;
如下图所示,由于动态生成静态资源路径,以往每个页面都要引入两个头尾文件(包括 JS 和 CSS 路径文件),优化之后只需要维护3个头尾文件即可,待之后版本进一步迁移之后,整个项目只需要维护1个头尾文件即可!
优化楼层懒加载
页面楼层懒加载,能够很有效的提高首页加载速度,减少不必要的请求。在 PLUS 会员首页中,底部长长的商品楼层很是符合懒加载的条件,用户并不会在进来的时候看到商品楼层。在之前的项目中,每次滑动商品楼层只请求一页的数据,减少不必要的请求和渲染。但是还有待优化空间,如果用户的手机首屏并没有显示商品楼层,我们就不需要在刚进来就加载该部分的 JS 代码。再来看这个文件的大小,线上经过压缩后的 JS 文件 11.7k,CSS 大小16.8k,加起来总共有 28.3k 了,这样看来已经不小了。
为了进一步减少首页不必要的加载,我们使用了 Vue-lazyload 插件,来实现组件的懒加载功能,并且使用预加载技术,让浏览器空闲时间先把该文件下载下来,避免用户滑动到商品楼测才去下载对应的静态资源。
基于 webpack 的 @babel/plugin-syntax-dynamic-import
和 Vue-lazyload 插件很容易实现异步加载代码的独立打包。
首先更改 Webpack 的 .babelrc 配置文件,使其支持懒加载:
"plugins": [
//新增
"@babel/plugin-syntax-dynamic-import"
]
然后在引入组件时改为动态异步加载:
{
floorFeeds: () => import(/* webpackPrefetch: true */ /* webpackChunkName: "floor-feeds" */'@/component/floor-feeds.vue'),
}
其中:webpackPrefetch 是预加载,如果首屏没有展示商品楼层,也会在浏览器空闲的时候先将该文件缓存下来,便于后续访问时,从缓存中快速获取。
webpackChunkName 定义 chunk 打包后的文件名字,便于识别。
引入 vue-lazyload 插件,并在入口文件中定义相关配置参数:
import VueLazyload from 'vue-lazyload';
const VueLazyloadOption = {
lazyComponent: true,
preLoad: 1.1,
//省略其余配置项
}
Vue.use(VueLazyload, VueLazyloadOption);
因为懒加载楼层需要首屏页面渲染之后,才能获取到上面楼层的高度,再去执行初始化计算是否触发加载,所以加上标识参数 lazyComFlag;懒加载曝光后再去加载 floor-feeds 组件,即“v-if="lazyFlag"”;
<!-- 商品feeds -->
<lazy-component
@show="init"
:key="floor.id"
v-if="floor.id == floorId.feeds[0] && lazyComFlag"
>
<floor-feedsv-if="lazyFlag"></floor-feeds>
</lazy-component>
尤其注意页面中处于该商品楼层上面的区域,如果没有固定高度的楼层,一定要保证渲染完成后,再去触发计算商品楼层的是否在可视区域内。最终懒加载组件曝光后,再去加载商品楼层。记得取消 network 面板中的 Disable cache,效果如下:
可以看到效果,页面商品楼层才去加载floor-feeds.js 文件,并且已经有了预加载,所以从缓存中获取,时间为2ms。
如上图所示,在小屏手机下,进入页面并不会加载 floor-feeds.js,只有在页面往下滑动时,才会从预加载 Prefetch 中加载该文件。
最后兼顾一下用户体验和性能,在大屏手机下的商品楼层,在首屏下面呼之欲出,此时设置懒加载功能意义不大,因为用户稍微滑动一下屏幕,就会露出商品楼层,并且渲染商品楼层也需要时间。而在一些小屏幕手机下,商品楼层距离首屏很远,商品楼层的懒加载就显得很是重要了。
引入PWA
PWA 全称 Progressive Web App,即渐进式 WEB 应用。是近两年来比较火的一个技术,常用来实现离线缓存功能,用户可以较快的访问已经访问过的页面,PLUS 会员首次使用了该技术,具体的代码就不说了,线上示例一搜一麻袋,这里聊一聊较少提及的坑吧。
尽量把 sw.js 要放在和页面 HTML 同一个目录下,否则注册 serviceWorker 无效;
最初模板代码发在了 src/template/index.html,我把 sw.js 放在了根目录,也就是和 src 一个路径下,再去配置 scope ,总是提示注册 serviceWorker 无效;
终于注册成功之后,代码也不报错,可是查看控制台中的 Network,所有的请求并没有走 serviceWorker,配置文件改了又改,缓存删了又删,Network 中的 Disable cache 也取消选择,最后才发现环境的配置问题!如下图所示:
打开浏览器的控制台,选择 Application 页签,左侧选择 Service Workers 页签,一定要注意这三个选项:
Offline:选中后关闭网络,此时可以模拟离线情况;
Update on reload:选中之后表示每次刷新页面,重新加载 sw.js 文件;
Bypass for netWork:选中之后请求只走网络,而不从缓存中加载;
我最开始的 Bypass for netWork 默认选中,所以导致页面请求一直走的是网络请求。
注意的是,我们此次只是在更新迭代较少的异常状态页面加入了 PWA 作为尝试,其他状态按照每周至少两次上线,同时并行十来个需求的节奏,每个页面对应一个 sw.js 文件的情况来说,如果每次都要后端去更新 sw.js 的文件,我感觉后端童鞋又要扛着五米长的大刀找我来玩耍了~~~
引入 TypeScript
提到 TypeScript 大家肯定不陌生,它是 JavaScript 的一个超集,主要提供了类型系统和对 ES6 的支持。它的出现让开发人员在编译阶段就能检测大部分错误,增加了代码的可读性和可维护性。但是面对上千万用户的 PLUS 会员项目,我们迟迟不敢动手,考虑到 PLUS 会员项目影响范围较广,业务逻辑复杂,且不断有新的业务需求加入,小小的变化可能会产生严重的、意想不到的后果。
本着小心求证再扩大战果的思想。我们尝试在新页面的需求中,把 TypeScript 引入进来,在这个全新的页面上进行一场 Vue + Webpack + Vuex + TypeScript 的小实验。基于安全这一原则,在不改动原有项目代码的基础上,我们小心翼翼的踏上了外界传言 vue + typescript 深坑的探雷之路:
1、安装依赖包
//安装vue的官方插件
npm i vue-class-component vue-property-decorator --save
//ts-loader typescript 必须安装,其他可选
npm i ts-loader typescript tslint tslint-loader tslint-config-standard --save-dev
如果你的项目引用了vuex,那么安装 vue-class
可以帮助你进行 store 数据的传输。
2、配置entry入口
例:修改pay-method.js 为 pay-method.ts
注意原项目引用的插件在ts文件里需要用 TS 声明,比如:
//原有:
Dialog.install(Vue);
Toast.install(Vue);
ActionSheet.install(Vue);
//改为ts断言声明
(ActionSheet as any).install(Vue);
(Dialog as any).install(Vue);
(Toast as any).install(Vue);
3、配置 webpack 文件
entry: {
index: './src/entry/index.js',
my: './src/entry/my.js',
'index-expired': './src/entry/index-expired.js',
'pay-method': './src/entry/pay-method.ts'
},
只对 pay-method 文件进行修改,其他文件慢慢改造
resolve: {
extensions: ['.js', '.vue', '.json', '.ts'],
alias: {
'@': path.resolve('src')
}
},
module rules里增加 TS 设置
{
test: /\.tsx?$/,
exclude: /node_modules/,
include: path.resolve(__dirname, "./src"),
use: [
'babel-loader',
{
loader: 'ts-loader',
options: {
appendTsSuffixTo: [/\.vue$/],
appendTsxSuffixTo: [/\.vue$/]
}
}
]
},
4、设置 tsconfig.json,这个文件可以从官网上复制一份根据项目需求修改下。
5、src中增加 vue-shim.d.ts 文件(重要!!!),文件配置内容如下
de
declare module '*.vue' {
import Vue from 'vue';
export default Vue;
}
以上内容确保vue文件会被 TS 识别出来
6、 改造vue文件
import {Component, Prop, Vue, Watch} from 'vue-property-decorator';
@Component
export default class PayMethod extends Vue{
//----data------
isFirstType: number = 0;
//-----computed----
get showSetFirstPay() {
return this.isFirstType != null && this.isFirstType != 0 ;
}
//-----Watch----
@Watch('statusForStat', {deep: true, immediate: true})
statusWatch() {}
mounted(): void {
this.initData();
}
}
经过以上步骤,在原来的 JavaScript 项目基础上进行 TypeScript 改造,之后就可以使用 TypeScript 语法进行开发了。开发过程跟 Vue 项目开发没有什么区别,但有几点是我们开发时遇到的问题,抛砖引玉:
1、背景图片路径问题
如果你的项目中有这样引入图片:
.weixin {
background: url("../img/weixin-pay.png") no-repeat center;
background-size: 0.46rem0.41rem;
}
并且在 TS 文件中是通过 import 引入的 css 文件
<style lang="scss">@import'../asset/css/pay-method.scss';</style>
那么在启动服务的时候可能会访问不到背景图片,这个问题初步判断是路径解析错误的问题。在尝试了把样式直接写到页面中或者写到 common.css 里还是路径用@来引入,都不能解决,最终在入口文件中引入 css 文件就能解决这个问题。
2、引入组件
原有项目中引入组件我们可能会是这样:
import Dialog from "@/component/dialog/index.js";
Dialog.install(Vue);
但如果引用了 TS 后,我们需要这样改写下否则将会报错:
import Dialog from '@/component/dialog/index.js';
(Dialog as any).install(Vue);
3、this 的使用
在 TS 中我们想使用 this 引入全局组件,但它所指向的上下文并不是全局,所以要使用全局的组件就要对 this 进行下定义:
(this as any).$toast.text('正在处理中,请稍后再试~');
4、全局变量的声明
在引入 TS 之后,如果在项目中想用 window
等全局变量,都需要在代码最前边声明一下
declare var wx;
declare var MPing;
declare var window;
以上是目前在开发项目时遇到的问题,以及我们现在的处理方法,可能不是最优的解决方案,欢迎留言一起讨论学习~~
从 CodeReview 中优化性能
随着迭代需求的日益增长,代码冗余度和繁琐度也愈加严重,长此以往可维护性就会变差。此外,开发人员书写代码水平、风格不一,有可能留有隐患,对此,一般在项目上线前,最好要进行 CodeReview,发现问题后便于修改,趁着安排好的测试资源检查无误后上线。。。
然而,对于 PLUS 团队成员来说,纷至沓来的需求压力以及各个需求卡死的上线日期,往往能够实现需求按时上线已经实属不易。所以只能在项目上线后,见缝插针式的抽时间进行 CodeReview,这就导致了修改后没有项目排期,没有测试资源的尴尬境地。但是为了推动从 CodeReview 中发现的问题,能够顺利上线,我们在每次 CodeReview 之后,做了以下事情:
梳理 CodeReview 后发现的问题,分析哪些问题需要进行修改和优化;
整理好每个修改点可能会导致的问题;
向项目经理、产品经理、测试等研发发送申请项目优化的排期邮件;
待排期确定后,再安排研发修改问题,避免提前修改,因为中间穿插的日常需求开发,有可能会导致后期合并代码带来的隐患;
经过测试检查无误后,再上线;
至此,我们从增强代码的健壮性、优化用户体验、提高页面渲染性能,优化代码结构、提高开发人员效率几个方面着重入手,排查了一些待优化的问题,并对这些问题进行梳理,申请项目排期和宝贵的测试资源,推动优化需求上线。
开发效率
如何提高开发工作效率?在提升性能和用户体验的同时,也是我们苦苦思索的方向。磨刀不误砍柴工,只有解决开发过程中对研发人员的痛点,才能让研发人员更加快速的进入业务的开发之中。
自动导入文件
Vue框架开发项目,组件化思想已经深入人心,但是随着项目复杂度的深入,我们发现每次新建一个组件,都要在首页进行引入文件路径,并且在 components 中添加该组件:
如果有十几个组件,这里就要 import 十几个组件,类似的还有单页面应用,每次新增路由时,也要引入大量的路由。面对这种情况,为了提高开发人员的工作效率,减少开发组件工序,我们使用自动导入文件夹中的组件的方法,避免页面引入过多的 import 和机械单调的引入步骤;
首先把所有要引入的文件按照文件夹分类,新建导出文件 myIndex.js
const files = require.context('./', false, /\.vue$/); //引入当前路径下所有的vue文件
let configRouters = {};
const modules = {};
files.keys().forEach(key => {
const handleKey = key.replace('.vue', '').replace('./', '');//提取vue文件名字
const newName = handleKey.split('-'); //将floor-coupon.vue 改为 ['floor','name','defal']
let newString = '';
newName.forEach((item, index) => {
let newStr = '';
if ( index !== 0) {
newStr = item.substring(0, 1).first.toLocaleUpperCase() + item.substring(1);
} else {
newStr = item;
}
newString += newStr;
})
configRouters[newString] = files(key).default;//将files文件和对应的配置文件 configRouters 联系起来
})
export default configRouters; //导出配置文件
在业务代码 index.vue 文件中,导入上面的入口文件:
import configRouters from '@/component/myIndex.js';
export default{
components: configRouters, //把组件配置文件赋值给 components 即可。
}
这样通过上述方式,每次新增的组件,在文件夹中直接新建即可,实现了自动引入组件的功能。通过以上优化的好处是:
减少业务代码中引入的组件数量;
优化添加组件的操作过程,只需要在相关文件夹内开发组件代码,即可自动引入该组件。
开启本地服务
工欲善其事,必先利其器。一款拥有着高效率的开发流程是每一位研发人所追求的。为了便捷快速的开发,一般在本地使用 webpack-dev-server
启动项目。然而一旦进入后续迭代,由于存在接口跨域和登陆信息限制问题,无法在本地打开页面进行开发。以往大都使用 whistle 或者 fildder 采取配置线上代理文件看调试效果,如下所示进行配置:
plus.m.jd.com/index https://localhost:8080/index.html
plus.m.jd.com/js/ https://localhost:8080/js/
plus.m.jd.com/css/ https://localhost:8080/css/
plus.m.jd.com/img/ https://localhost:8080/img/
plus.m.jd.com/vendordev.dll.js https://localhost:8080/vendordev.dll.js
因为每个页面都对应一套配置代理文件,多个页面则有不同的配置代理文件。切换配置代理和操作起来,配置过程甚是繁琐。
为了解决以上问题,如何在本地开启的环境上也能调用接口?就需要解决请求跨域和登陆信息两只拦路虎。
如何解决跨域问题?
目前解决跨域常用的方法是采用 webpack-dev-server 结合 proxy 接口代理或者使用 Nginx 均可配置跨域的代理。由于在本地配置好 webpack-dev-server 之后,所有项目成员都可以使用,一劳永逸。我们采用了配置 webpack-dev-server 中 proxy 的接口代理,在 webpack.config.js 文件中对 webpack-dev-server 配置如下:
proxy: [
{
context: ['/user','/apis'], //使用context属性,可以把多个代理到同一个target下
target: 'https://rsp.jd.com/', //把用 user 和 apis 开头的接口代理到 https://rsp.jd.com/域名下
secure: false, //默认不支持运行在https上,且使用了无效证书的后端服务器,这里设置为true,可以支持
changeOrigin: true,//如果接口跨域,需要进行这个参数配置
pathRewrite: {'^/apis': ''},//由于apis开头的路径,是人为添加方便区分哪些接口要代理的,所以这里去掉apis
headers: { //设置请求头
origin: 'https://plus.m.jd.com', //请求接口限制来源,所以要改动请求源
host: 'rsp.jd.com',//设置请求头的host
referer: 'https://plus.m.jd.com/index'//设置请求头的referer,因为后端接口会有限制
}
}
]
经过上述配置请求接口,并且设置项目中所有的接口前缀是 user 和 apis 开头,如果是本地开发才新增的前缀,就需要根据开发环境切换接口前缀,在上线环境上删除该前缀。
这样,就可以在本地开发时解决跨域问题,如果出现状态码错误,就要问一下后端是不是做了什么限制,一般修改请求头 headers 对应的信息即可。
如何解决接口传递登陆信息?
解决了跨域问题,接口还能不能访问,因为请求接口还需要携带 cookie 信息来判断用户是否登陆。我们知道用户登陆后,浏览器会把当前用户信息对应的 id 值保存到 cookie 中,之后每次请求都会携带该域名下的 cookie 来判断用户登陆状态。而本地开发的项目,是无法携带 plus 域名下的 cookie,所以本地访问的接口都会返回用户未登陆,从而无法获取相关数据。
浏览器说了:一切不给登陆 cookie 就想要数据的行为都是耍流氓!好吧,这种行为可以理解,毕竟不能空手套白狼,我们只好曲线救国。由于服务器为了防止 XSS 攻击,对 cookie 中用户信息的 id 值设置了 httpOnly 属性,避免使用 JS 获取。但这样导致我们也没有办法使用 JS 获取用户信息的 cookie,所以我们只好先在浏览器中访问线上的 plus 页面。然后在dev-tool面板上查找到用户信息的 id,然后把该值放在本地的 cookie 中。这样启动本地 webpack-dev-server 服务,在之后的请求中把该 cookie 携带过去:
通过以上两个核心步骤,开发人员可以实现在本地一键命令即可启动项目,极大的给开发人员提供了便利。如需模拟不同状态用户,只需要在js中替换对应的cookie变量即可。
自动格式化代码
世界因为多样性而变得如此精彩,但是用在代码格式上,却是一场灾难!在团队协作开发当中,相信小伙伴们大部分会遇见以下尴尬的场景:
不同的代码风格在 lint 工具检查出问题之后只能人肉修改,而通常这样的错误都是分号、tab缩进、空格、引号之类的;
当你需要在其他同事的分支基础上进行二次开发的时候,发现他的代码风格和自己的不一样,阅读和开发起来非常不习惯;
为了避免这种情况,我们使用了超高人气的自动格式化工具 Prettier,它可以自动的按配置好的规范帮你完成代码的格式化,这不仅省时省力,还能提高你的代码阅读性,简直就是团队协作开发的福音!
Prettier 的优点
它可以在编辑器中使用,也可以在命令行中使用,配置非常简单。
它既能在保存代码的时候进行格式化当前文件,也能一键格式化所有的文件;
它支持 HTML/JS/JSX/TS/JSON/CSS/SCSS/LESS/VUE 等主流文件格式;
安装使用 Prettier 的步骤如下所示:
这样每次修改代码后,保存文件的时候就会自动格式化。如果想一键把项目中所有文件都进行格式化,还可以使用命令行的方式,这里推荐在 package.json 中配置scripts命令:
"prettier:check": "prettier -l src/**/*.{js,vue,scss}",
//"prettier:check" 是检查项目src目录下的任何js、vue、scss文件是否存在格式问题;
"prettier:fix": "prettier --write src/**/*.{js,vue,scss}"
//"prettier:fix" 是一键格式化项目中src目录下所有js、vue、scss文件;
这样我们就可以通过执行命令方式,来检查代码格式和自动修复格式,减少团队不同开发人员代码风格不一样,以及单独修改代码格式的弊端,一劳永逸。
提高 Git 规范化
PLUS项目快速的迭代需求和多个需求并行开发的节奏,导致每次都要新拉分支进行开发,但是往往狂欢之后一地鸡毛,贪图一时的便利进行代码提交,往往是到最后也无法想起最初的修改目的。git版本管理系统为代码的有序和稳定做出了突出贡献,但是便利的同时也需要规范和优化,才能使得git展现出更多的价值。
规范 git 提交
所有的提交建议使用 标识:内容 的形式,说明此次提交的目的,使每次提交都有价值
标识 | 说明 |
---|---|
feat | 新功能(feature) |
fix | 修补bug |
docs | 文档(documentation) |
style | 格式(不影响代码运行的变动) |
refactor | 重构(即不是新增功能,也不是修改bug的代码变动) |
test | 增加测试 |
chore | 构建过程或辅助工具的变动 |
提交信息优化
上面是 PLUS 项目中的一段提交信息,一连几个相同的提交,像这样提交价值就会减弱。一般来说,commit message 应该清晰明了,说明本次提交的目的。对于这种情况的发生,一般有以下几种场景:
当前分支开发到了一半需要紧急切换其他分支,如果不提交,可能不允许切换分支或者出现把当前的修改带到了新的分支的情况,所有先提交一个临时的,回头再次开发;
当前分支开功能开发完了,提交一版本,一会发现有问题,可能只是一行代码,在提交一次。来来回回提交了好几次代码;
针对这两种情况:
可以使用 git stash
,去暂时保存,但不提交代码,等切换回分支的时候,再读取出来开发 git stash apply
;
针对第二种情况已经提交了,这时候可以用 git commit--amend
,撤销上一次提交到暂存区,并重新提交内容;
注意:该方法一定要在未提交到远程的commit进行操作,千万不要提交到远程之后在执行此命令。
流程优化
回想起2019年,别有一番滋味在心头~~
经历过通宵达旦的上线,眼神迷离的看见第二天前来上班的同事。也遇到过周末早上突然被电话惊醒,支持紧急需求的情况。有的时候刚刚下班,却被通知有需求要立刻评审。也有过压力大、陷入迷茫困惑、情绪波动的时候。虽说我们无怨无悔,但是一个规范化的流程,本不应该让人如此精疲力尽。工作就是一个不断遇到问题并且解决问题过程,主动推进项目的规范化,提高人效,才是正面问题的态度。所以我们主动在以下方向进行了推进:
制定开发规范
团队的人员变动和快速迭代的需求,一度让新加入的成员对 PLUS 项目不知如何下手。同时,一个合格的项目也需要制定开发规范,既可以让开发成员快速的了解进入项目,还能让老成员明确规范化开发流程。因此,我们团队主要从以下几个方面进行汇总:
梳理各个分支对应状态分布
制定项目开发流程
汇总项目调试方法
规范上线流程
汇总 PLUS 会员相关资料链接
包括但并不限于以上方向,通过制定总结项目规范,汇总开发时遇到的问题,有力的推进团队开发人员对项目的熟悉,避免了新加入的成员通过自己摸着石头过河的现状,有利于快速的融入 PLUS 项目,顺利的承担起开发工作。
优化评审机制
PLUS 业务发展快,频繁的需求评审,随时随地咚咚会议,困扰着整个 PLUS 项目团队成员。原计划安排的事情往往被迫中断,手头的工作不得不加班消化。长此以往,不但会让团队成员疲于应付,也打乱整个项目的排期进度,严重的引起蝴蝶效应,之后的需求有可能都会受到影响,因此解决这一问题迫在眉睫。
在项目复盘会议上,呼吁建立更加合理的评审机制,将需求评审会议规范化。好在各个团队的大力配合下,通过了每两周进行一次需求集体评审的制度。对于紧急需求,根据其紧急程度评估是否单独组织评审。
通过确定需求评审机制之后,除非赶上6.18大促、双11活动等节日有着密集的评审,其他时间有了很大的改善,合理的评审机制,让大家可以更好的安排手头的工作,让人头疼的需求混乱情况有了较大的缓解。
推进新技术学习
学习如逆水行舟,不进则退。在前端技术日益更新的大环境下,如果只考虑实现业务需求,团队成员不但会慢慢的进入舒适区,也会逐渐会在技术上逐渐掉队。但是每周都要支持多个并行需求,加上前端主动发起的项目优化工作,再让团队成员去学习一些新的知识,时间上往往会显得捉襟见肘。所以我们不能漫无目的的去学习新知识。必须结合 PLUS 会员项目有的放矢。学习到的新技术可以在 PLUS 项目中得到实践,无论是推进代码更加完善还是优化用户体验,都能够学以致用才行。
于是基于以上目标,我们开始有意识的去探索。由于项目使用 Vue 开发,一直在关注 Vue 3 的源码公布,其中使用的 TypeScript 技术映入我们眼帘。发现 TypeScript 有很多优点,尤其是对多人合作开发的项目,增加了代码的可读性和可维护性。无规矩不成方圆,于是我们团队制定好学习计划,每个人利用空余时间进行学习,在项目需求不紧张的时候,组织部分成员对该项技术做了技术分享和知识讨论。此外,在新页面中进行了引入 TypeScript 的尝试工作,于是才了上面对 TypeScript 的实践部分。
与视觉侧的协作
由于 PLUS 会员迭代需求很多,大部分需求都涉及到视觉稿的更新,和设计师的密切沟通是常态。在这个过程中我们发现两个比较突出的问题。
设计师使用公司的产品协作工具 [RELAY][6](类蓝湖),但是有些图片没有给到切图,或者到我们开发的时候发现有些切图没有符合研发预期,于是联系视觉设计师去要对应的切图的时候,由于大家都很忙,可能不会及时回复。问题就会阻塞,遇到严重的情况,比如重构页面时缺失很多切图,或者相关字号、颜色等信息无法获取时,就会导致项目进度卡住。无形中增加了沟通成本。
需求上线需要经过视觉设计师的样式走查,走查问题需要再修改。然而种种原因,很多次都是项目晚上上线,下午才视觉走查。导致研发没有时间改动,况且一旦改动也容易导致其他的样式问题。
经历过几次之后,我们和视觉设计师们坐下来一起探讨了如何解决这些问题。
针对问题1: 增加研发确认视觉稿的环节,视觉设计师定稿后,或者研发准备开发前,把本次需要的视觉稿先走查一遍,将过程中需要用到的切图反馈给视觉设计师,统一提供切图,此外,时尚的设计师们还贴心的补充了PS稿件,方便我们在联系不到人时,进行查漏补缺。
针对问题2: 研发侧每次发提测邮件的时候,都要抄送相关@视觉设计师,推进在提测阶段先视觉走查。
此外测试同学提前给产品告知大概什么时候需要视觉走查,产品侧提前申请视觉资源,申请走查和走查结果一律走邮件,多重提醒方案,将样式走查流程提前。
通过以上方法,最大程度的保证了研发在开发项目时拿到自己希望的视觉信息,避免了临时抱佛脚,到开发时才去沟通的问题,减少了沟通成本。此外提前通知视觉设计师进行样式走查,避免了上线前的改动,容易带来的隐患问题。
推进旧版本迁移
由于历史原因,PLUS 会员项目有三个分支,分布着不同状态的 9 个页面。第一批开发的页面时间在两年前,当时采用的一些技术和开发代码不是很规范,里面混杂着 [jdf][7] + zepto 技术栈的一些代码。在后续的开发过程中逐渐延伸出共三个开发主分支:V2、V3、V4。
V2/V3,17年上半年开发,不是基于 Webpack 构建,受 [jdf][7] 影响大,不够合理。首页计算器和我的 PLUS 计算器未复用,使用老一版接口
V4,19年上半年新版本,基于 [Gaea4][2] 修改而来的一个Vue多页面页面应用,包含诸多新特性,性能有大幅度提升
V4 新特性包括
Webpack 升级到 4.0,Vue-loader 升级到15
集成 [carefree][4] 和 [NutUI2.0][3] 按需加载插件
Babel 升级到 7.0,实现 polyfill 的按需加载
结合代码及楼层数据懒加载、Tree-shaking、Scope Hoisting、DllPlugin剔除未用到的 css 等技术手段削减代码体积
京东APP内部分 H5 链接通过 openapp 协议拉起新的 webview 打开,提升用户体验和埋点上报成功率
综合应用 webp 和 dpg 图片格式,削减图片体积,提升图片渲染速度
引入Vuex进行数据集中管理、沉浸式、骨架屏
老版本的加载速度瓶颈是串行请求:页面脚本→用户信息接口→楼层信息接口→各楼层数据→渲染。
V4版本通过后端把用户信息接口和楼层信息接口数据直接打在页面上,避免了接口的串行等待,提升了首页渲染速度:页面脚本→各楼层数据→渲染
下图 Chrome 内置的自动化测试评估工具 Audits 对 V4 新页面性能评分较高
骨架屏的引入缩短白屏等待时间,如图测试在200ms-600ms白屏期间展示骨架屏,白屏时间缩短2/3,有助于提升页面的主观加载速度,进而提升用户体验。
因此迁移到 V4 从性能和开发效率方面考虑显得尤为重要。尤其当多个并行需求过来,涉及到基于从三个主分支新拉出一系列子分支进行开发,维护多个版本的弊端一览无遗。为此我们主动发声,以提效为出发点讲道理摆事实。首先争取到产品侧的理解和支持(特别感谢)得以立项提需,后续得到项目经理及测试的资源支持。推动产品侧关注代码版本迁移的情况,在之后的需求迭代中,逐渐将老版本的代码迁移到最新的 V4 分支上。
目前为止,V2 分支上的季卡过期首页、年卡过期首页、尊享商品页面[后改为省钱攻略页]、我的PLUS页面(正式过期、试用中状态)、试用过期30天内均迁移到 V4 上。还剩下 V2 改动很少的异常首页及 V3 中的试用中及试用过期30天内代码。这一进程尚未结束,最终的目标是把所有用户状态和页面代码全部迁移到最新技术栈架构分支上。
排期规范化
随着对 PLUS 项目的重视,我们安排了单独研发来跟进所有的需求和排期,避免了大家要一边支持开发,一边时间被打断参加各种会议。所有的需求经过评审、梳理之后,确定哪些可以并行开发,哪些需要串行开发。再和项目经理配合,制定出前端团队的工作排期。这样每个人下周的工作都是确定了的,分工明确,不会被临时需求和紧急需求打乱。
此外,所有的紧急需求、新插入的需求、线上出现的问题都要再经过对接人的梳理、排查之后视需求情况而定:
紧急需求安排较为空闲研发支持;
不是特别紧急的需求安排下次排期;
对于线上问题,如果能够很快修复,一般跟随下一个上线需求一起上线;如果不能快速修复的,重新走排期。
这样,整个团队的时间有了更好的安排,不会被项目需求牵着鼻子走,每周的工作都是安排好的,每个人的工作都是明确的,可以适当的调整自己工作的节奏,避免了各种紧急需求的插入。
经过以上措施,团队成员能够快速的进入项目开发,分工明确、每个节点做什么事情也变的清晰。同时也提高了和其他团队的沟通协作,减少推诿。孟子曾曰:不以规矩,不能成方圆;不以六律,不能正五音。深以为然!
结语
2019 年双十一,PLUS 正式会员数突破 1500w,迎来新的历史时刻。一位 PLUS 会员单笔消费 46.7 万元,成为京东单笔消费最高的一单。我们在感受着团队成绩的同时,也倍感责任重大。回首 2019 年,对于 PLUS 前端团队成员来说,是不平凡的一年。在保证完成纷至沓来的迭代需求前提下,从优化用户体验、提高开发效率、推进完善制度流程等方面入手,使得 PLUS 会员项目日趋完善。
此刻,窗外寒冬中呼啸着凛冽的寒风,吹散了往日的雾霾,吹来了艳阳高照、晴空万里。悄悄的抓住 2019 的尾巴,对即将到来的 2020 年充满希翼。正如京东 PLUS 会员项目遇到的挑战一样,终究会跨过去迎来新的征程,2019再见,2020 加油!
扩展阅读
[1] 京东PLUS会员权益详情:https://plus.jd.com/right/index#item-coupon [2] Gaea构建工具:https://github.com/jdf2e/Gaea4 [3] NutUI组件库:http://nutui.jd.com/#/index [4] Carefree:http://carefree.jd.com/#/ [5] SMock:http://smock.jd.com/#/ [6] RELAY:http://relay.jd.com [7] jdf:https://github.com/jdf2e/jdf
为你推荐
在公众号后台回复关键词 京东 查看专栏文章