回想17年的京东首页改版从上线到现在竟然已经过去了四个多月。这四个多月除了不曾中断的日常维护需求对首页孜孜不倦的优化工作更多的是那些与拖延症抗争的日夜:是今天写还是等好好休憩回味后再动手?很明显在这几十上百个日夜里我基本都选择了第三个选项:不折腾了先休息吧。现在想起来关于那一个月白加黑五加二加班生活的印象已经渐渐模糊。到现在依然能清晰记着的大概是最为深刻的记忆了。
16版的京东首页在性能、体验、灾备策略等各方面都做到了极致。站在如此高大的巨人肩上除了满满的自信我们心里更怕扑街。毫无疑问我们在接到改版需求的那一刻立马就敲定了新首页的技术选型:妥妥的jQuery + SeaJS!
但很快我们就发现这样一点都不酷。jQuery是2006年的框架了SeaJS停止维护也已经四年。这些项目的诞生都是为了解决当时业界的一些痛点:比如jQuery最开始是为了方便程序员在页面中操作DOM绑定事件等;SeaJS则是为了在浏览器中实现CMD规范的模块开发和加载。但在各种VirtualDOM框架横飞的现在程序员已经很少会直接操作DOM元素而模块的开发和加载也有早已有了别的方案。
就在这时Nerv项目的作者提出了建议:“不然用Nerv来一发?”我记得当时他脸上洋溢着淳朴的笑Nerv也仅仅是部门内部的一个小项目。我们回想了首页这个业务技术栈已经好几年未曾更新过开发流程也不够理想。如果再不做出改变明年的这个时候我们依然会面对一堆陈年老代码头疼不已。抱着试一试的心态我们接受了他的提议。没想到这个决定让首页从此摆脱了落后的技术架构而Nerv现在也已经成长为GitHub上3k+ Star的热门项目。
Q: 为什么不使用React/Preact/Vue?
A: 这三者都是前端圈子中相当流行的项目。React有完善的体系和浓厚的社区氛围Preact有着羞涩的体积Vue则使用了先进的html模板和数据绑定机制。但是上边这三者都 无法兼容IE8。我们在经过相关数据的论证后发现IE8的用户还是有一定的价值这才最终激发了我们团队内部自己造轮子的想法。当然在造轮子的过程中我们也不忘向上面这些优秀框架的看齐。最终Nerv在完美兼容React语法的同时具有着出众的性能表现在Gzip后也只占用9Kb的体积。
整体架构
在这次的项目中我们基于上一年久经考验的前端体系进行了升级:
Athena前端工程化工具:团队自研的前端工程化工具。除了自动化编译、代码处理、依赖分析、文件压缩等常规需求2.0版本还支持 基于npm的依赖管理更加先进的引入、导出机制还有 最新的es语言特性。
Athena管理平台:新增了 针对Nerv的项目模板另外还有针对H5项目的特色模板可选。
Athena基础库与组件库:新增了基于 jQuery + SeaJS的组件重构全新升级的Nerv组件。
Athena模拟接口:除了已有的mock接口数据的能力还支持 接口文档生成便于沉淀项目接口信息。
Athena兜底接口:可以定时抓取线上接口的数据 生成兜底数据还支持 接口数据校验评估接口健康度。
Athena前端监控:我们部署了一系列的监控服务对页面上的素材以及页面的完整功能进行监控。一旦图片尺寸/体积超限某些特定的操作出现异常或者接口成功率降低等异常情况就会触发告警推送开发者可以 实时收到告警信息。
Athena可视化报表:Athena可视化报表平台上对上报的数据都有 直观的展示。
Athena前端工程化工具:团队自研的前端工程化工具。除了自动化编译、代码处理、依赖分析、文件压缩等常规需求2.0版本还支持 基于npm的依赖管理更加先进的引入、导出机制还有 最新的es语言特性。
Athena管理平台:新增了 针对Nerv的项目模板另外还有针对H5项目的特色模板可选。
Athena基础库与组件库:新增了基于 jQuery + SeaJS的组件重构全新升级的Nerv组件。
Athena模拟接口:除了已有的mock接口数据的能力还支持 接口文档生成便于沉淀项目接口信息。
Athena兜底接口:可以定时抓取线上接口的数据 生成兜底数据还支持 接口数据校验评估接口健康度。
Athena前端监控:我们部署了一系列的监控服务对页面上的素材以及页面的完整功能进行监控。一旦图片尺寸/体积超限某些特定的操作出现异常或者接口成功率降低等异常情况就会触发告警推送开发者可以 实时收到告警信息。
Athena可视化报表:Athena可视化报表平台上对上报的数据都有 直观的展示。
前后端协作
我们依然是采用了 前后端分离的协作模式由后端给出json格式的数据前端拉取json数据进行渲染。对于大部分的组件来说都会在constructor中做好组件的初始化工作在componentDidMount的生命周期中拉取数据写入组件的state再通过render函数进行渲染。
代码规范约束
有一千个读者就会有一千个哈姆雷特。
上面这句名言深刻地体现在了16版首页的代码仓库中。同一个组件如果是基于 jQuery + SeaJS的模式一千个程序猿就会有一千种写法。结果在同一个项目中代码风格不尽相同代码质量良莠不齐多人协作也会无从下手。
何以解忧?唯有统一代码风格了。通过 ESLint + Husky我们对每次代码提交都做了代码风格检查对是否使用prefer const的变量声明、代码缩进该使用Tab还是空格等等的规则都做了约束。一开始定下规范的时候团队成员或多或少都会有些不习惯。但通过偷偷在代码里下毒Athena的生成的项目模板中添加对应的规则潜移默化地团队成员们也都开始接受、习惯这些约束。
禁用变量重声明等规则在一定程度上保证了代码质量;而统一的代码样式风格则使得项目的多人协作更加便利。
Q: 保证代码质量促进多人协作的终极好处是什么?
A: 由于项目代码风格统一通俗易懂容易上手我们首页的开发团队终于开始 有妹纸加入了!一群雄性程序猿敲代码能敲出什么火花啊…
对性能优化的探索
首屏直出
直出可能是加快首屏加载最行之有效的办法了。它在减少页面加载时间、首屏请求数等方面的好处自然不必再提结合jQuery也可以很方便地在直出的DOM上进行更多的操作。
Nerv框架对于它内部的组件、DOM有着良好的操作性但是 对于体系外的DOM节点却是天生的 操作无力。举个例子比如在页面文件中我们直出一个轮播图:
使用Nerv为这段HTML添加轮播逻辑成为了非常艰难的操作。终极的解决方案应该是使用 SSR(Server Side Render)的方案搭建Nerv-server中间层来将组件直出。但现在革命尚未成功首屏直出尚且依赖后端的研发同学首页上线又迫在眉睫。被逼急的我们最终选择了比较trick的方式来过渡这个问题:在组件初始化的时候先通过DOM操作获取渲染所需的数据再将DOM替换成Nerv渲染后的内容。
代码分割
在生产环境中随着代码体积增大浏览器解压Gzip、执行等操作也会需要更多的开销。在SeaJS的时代我们尚且会通过SeaJS.use或者require.async异步加载模块代码避免一次性加载过多内容。但webpack的默认行为却会将整个页面的代码打包为一个单独的文件这明显不是最佳的实践。对此webpack给出的解决方案是动态引入(Dynamic Imports)。我们可以通过如下的代码来使用这个便利的特性:
与此同时webpack会将使用了动态引入的组件从主bundle文件中抽离出来这就 减小了主bundle文件的体积。
对于我们的具体需求而言需要做动态引入的一般是Nerv的组件。对于组件的动态引入业界已经有非常好的实现方案 react-loadable。举个栗子通过下面的代码我们可以在页面中使用来实现对组件MyComponent的动态引入并且具有 加载超时、错误、加载中等不同状态的展示:
再进一步我们希望对于屏幕外的组件仅仅是在它进入用户视野后再开始加载这也就是我们常说的滚动懒加载。这可以结合业界已有的懒加载组件react-lazyload来实现。针对上面的在下面的例子中只有进入用户屏幕后MyComponent才会开始加载:
上面的例子为lazyload的组件设置了200px的占位高度。并且设定了占位元素的类名方便设定样式。
代码延后加载
在给首页全面升级技术栈的时候我们忽略了一个问题:页面上还引用着少量来自兄弟团队的SeaJS模块我们升级了技术栈是可以但是强迫兄弟团队也一起去掉SeaJS重构一遍代码这就有点不合理了。我们也不能仅仅为了这部分模块就把SeaJS给打包进代码里面这也是不科学的。
上面讲到的 动态引入功能帮我们很好地解决了这个问题。我们在代码中单独抽离了一个legacy模块其中包含了SeaJS、SeaJS-combo等老模块并做了导出。这部分代码在首屏中并不直接引入而是在需要执行的时候通过上面的 动态引入功能单独请求下来使用:
打包性能优化
webpack默认会对整个项目引用到的文件进行编译、打包其中还包括了Nervjs、es5-polyfill等基础的依赖库。这些文件从加入项目开始基本都不会再有任何更改;然而在每次构建新版本时webpack打包的这些基础库都会与上一版本有一些细微的区别这会导致用户浏览器中对应的代码缓存失效。为此我们考虑将这些基础库分开打包。
针对这种需求webpack官方建议使用DLL插件来优化。DLL是Dynamic Link Library的简称是windows系统中对于应用程序依赖的函数库的称呼。对于webpack我们需要使用一个单独的webpack配置去生成DLL:
接下来在我们的项目的webpack配置中引用DllReferencdPlugin传入上面生成的json文件:
这样就完成了动态链接库的生成和引用。除了最开始的一次编译后续开发中如果基础库没有变动DLL就再也不需要重新编译这也就解决了上面的代码变动的问题。
体验优化探索
兼容IE8
兼容旧版本IE浏览器一直是前端开发人员心中永远的痛。过去我们使用jQuery去统一不同浏览器的DOM操作和绑定事件通过jQuery元素实例的map、each等类数组函数批量做JavaScript动画等等。
但是在使用Nerv之后从体系外直接操作DOM就显得很不优雅;更推荐的写法是通过组件的ref属性来访问原生DOM。而map、each等函数IE9+的浏览器也已经在Array.prototype下有了相应的实现。如果我们在代码中直接引入jQuery这肯定是不科学的这将使页面的脚本体积提高许多同时还引入了很多我们根本用不上的多余功能。
面对这种情况我们做了一个仅针对ie8的轻量级的兼容库es5-polyfill。它包括这些实现:Object的扩展函数、ES5对Array.prototype的扩充、标准的addEventListener和removeEventListener等。在入口文件顶部使用require('es5-polyfill');引入es5-polyfill后只需3分钟你就会甘我一样爱上这款框架可以在代码中愉快地使用上面说到的那些IE8不支持的API了。
但是通过上面的CMD方式引入不就意味着对于IE9+的用户都引入了这些代码吗?这并不符合我们“随用随取避免浪费”的原则。我们更推荐的做法是在webpack中为配置多个entry再使用HTMLWebpackPlugin在HTML模板中为es5-polyfill输出一段针对IE8的条件注释。具体实现可以参考nerv-webpack-boilerplate。
SVG Sprite
在页面中使用SVG可以有效提升小图标在高清屏中的体验。类似于图片SpriteSVG也可以通过Sprite来减少页面的请求(参考文章:拥抱Web设计新趋势:SVG Sprites实践应用)。
举个栗子我们在Nerv中声明svgSprite组件用以存放页面中用到的svg小图标:
接下来我们可以在页面中 动态引入上面的svgSprite组件就可以了:
在页面中挂载后我们即可使用形如的代码去引用相应的图标了。
数据大屏
除了用户我们同样也关注运营人员的体验。如果可以将运营数据都以直观的图表展示这对于运营同学、产品同学都是十分幸福的事。这次的首页改版我们与数据方合作为首页配套开发了数据大屏项目SEE用于运营数据的实时滚动展示。
SEE基于Nerv+Redux开发使用ECharts进行数据的可视化展示。除了线上数据SEE还有专门针对开发人员需求的性能版大屏实时展示开发人员关心的页面onload时间接口成功率js报错数等指标。我们也希望未来SEE可以在更多的业务中用起来。
页面可用性保障和监控
我们做了许多优化工作来提升页面在性能、体验上的优良表现。但如果页面出现了JS逻辑错误或者展示有问题前面的优化工作就都前功尽弃了。所以在保证项目进度的基础上我们又做了一系列的工作来保证首页的安全与稳定。
统一上线
同一份代码经过不同版本的开发工具进行编译、压缩生成的文件可能会天差地别这种情况在多人协作中是相当致命的。比如:开发人员A的代码使用了新版本开发工具的API而无辜的开发人员B对此毫不知情使用了老版本的开发工具进行编译和发布…说多了都是泪又是一场人间悲剧。
为了消除差异我们希望不同开发人员的开发环境保持严格统一但这其实是难以保证的:除了开发工具版本不同有时候windows下macOS下甚至是Linux下的表现也是不一样的。
为了解决这个问题我们将编译的工作挪到了服务器端。开发人员在本地进行开发、自测联调通过后提交到代码仓库中确认上线后上线平台拉取项目的代码使用服务器端的工具链进行编译、压缩、发布等工作。
此外上线平台还提供上线代码diff功能可以将待上线的文件与线上的版本进行diff待开发人员确认完才能继续上线操作。
接入上线平台后开发人员再也不必担心开发环境的差异影响了编译结果也不会误操作将其他同事开发中的分支带上线。就算是出现了线上bug开发人员也可以轻松地通过上线平台记录的git commitId进行 精确快速的回滚有效保障了页面的可用性。
自动化测试
我们注意到每次上线迭代在经过编译工具的压缩、组合后都有可能会对代码中其他部分的代码造成影响。如果在测试时只验证了当前迭代的功能点负面seo深度理解搜索引擎,为企业实现百都有可能会对代码中其他部分的代码造成影响。如果在测试时只验证了当前迭代的功能点疏漏了原先其他功能点的验证就有可能引起一些意想不到的BUG。传统的DOM元素监控并无法满足我们的需求因为有的bug出现的时机是在一连串特定的操作后。所以我们认为我们造轮子的时候又到了。我们需要在Athena监控体系中增加一套针对页面中各个功能点的自动进行验证测试的系统。
这个系统基于selenium webdriver搭建。在后台中开发者 针对CSS选择器配置一系列的动作链Actionchain包括点击、hover、输入文字、拖拽等操作再通过对指定CSS选择器的HTML属性、样式、值等因素指定一个预期结果。后台服务会定时打开页面执行预设的操作如果与预期结果有出入就会触发告警。
这种单服务器运行的e2e测试容易碰到一些偶然网络波动的影响而导致乱告警。事实上我们刚开始跑这套服务的时候我们经常收到告警但事实上页面展示并没有任何问题。针对这种偶发情况我们在验证的过程中加入了失败重试的机制。只有在连续3次测试状态都为fail的情况才会触发告警。经过优化后监控的准确性有了质的提升。
素材监控
在生产环境中除了程序BUG数据运营的一些不规范操作也有可能影响到用户的体验。举例来说如果页面中的图片体积过于庞大会导致页面的加载时间变长用户等待的时间会更久;如果页面中图片尺寸不合规会导致 图片展示不正常出现拉伸/压缩等现象页面就会给人很山寨的感觉了。
针对这些素材异常情况我们部署了针对性的监控服务。开发者 针对特定的CSS选择器配置图片的标准体积以及尺寸。监控服务定时开启headless浏览器抓取页面中的图片并判断是否符合规则不符合就触发告警。通过这样的手段我们可以第一时间知道页面上出现的超限素材通知运营的同学修改。
实时告警
作为Athena前端体系的一环我们接入了Athena系统用于收集首页各种性能以及用户环境相关的数据。在这套系统中我们可以获取到用户的 屏幕分辨率占比浏览器占比同时还有 页面加载时间、接口成功率等性能数据。
基于这些数据我们在发现问题时可以进行有针对性的优化比如调整特定接口的等待时间或者是调整特定请求的重试策略。但是并没有谁会一整天对这些数据的仪表盘盯着看;等我们发现问题可能已经是上线后第二天在工位上吃着早餐喝着牛奶的时候了。
解决信息滞后的问题加强消息触达是关键。我们强化了Athena监控平台的功能:除了平台上仪表盘直观的数据展示还支持配置告警规则。在绑定告警接收人的微信号后平台就可以通过部门公众号实时推送告警信息真正做到24小时监控360度无盲点触达。
更长远的探索
“如何做得更好?”这是一个永不过时的问题。以前我们觉得在页面使用CMD的模块加载体系非常酷所以后来会在项目中使用SeaJS;去年我们渴求一次架构升级所以我们今年用上了Nerv。今年我们又会渴望什么呢?
前后端同构
作为提升性能的一个捷径代码同构、服务器端渲染是目前看来的终极解决方案。我们计划搭建中间层同样使用Nerv来渲染从而减少首屏的代码逻辑这将对页面的加载速度有大幅度的优化。
引入强类型校验
除了更快我们也希望能够更稳。作为弱类型语言JavaScript有着强大的灵活性数据类型的相互转换十分便利;但也由于各种不严谨的类型转换代码中存在着大量不可预测的分支走向容易出现undefined的报错调用不存在的API等等。
强类型语言TypeScript从13年诞生到现在已经十分成熟了。在类型推断、静态验证上TypeScript明显会更胜一筹;而在减少了多余的类型转换之后TypeScript的性能表现也比常规JavaScript更强。我们希望未来可以将TypeScript在项目中用起来这对于提升页面可靠性和性能是很有意义的。
总结
这篇文章从 整体开发架构与模式性能、体验优化的探索页面可用性的保障等方面对京东首页的开发过程做了 简单的介绍。之所以说简单是因为短短的篇幅完全无法说完我们在开发期间的故事和感悟:许多问题的解决并不像上面讲的那样水到渠成;除此之外更是有一大堆深夜加班撸串的故事没有地方讲。
最后献上个照片这是项目上线成功之后在公司拍的通宵证明。虽然现在会觉得这拍得真……丑但是项目成功上线的喜悦之情我相信屏幕前的你也一样可以感受到。