大家好,我是京东用户体验设计部前端架构团队的刘威,网上ID是putaoshu,非常高兴 “ITA1024团队”的邀请,有这样的一个机会与大家分享下我们团队关于2015年京东PC新首页的一些前端开发实践,希望通过今天的讲解能为大家以后在大型前端项目开发和改版时提供一个思路和参考。
今天我的讲解主要分为两部分,具体如下:
京东首页前端架构设计与实现
-
面临挑战
-
前端页面静态化
-
前端页面整体架构
-
前端页面加载策略
-
前端基础架构
-
前端工具和系统
-
前端灾备策略
-
前端性能优化
前端工程化在电商首页的实践
-
命令行工具
-
前端模块
-
前端组件
-
前端开发流程
-
前端文档
-
实际应用
京东首页前端架构设计与实现
面临挑战
-
页面DOM元素剧增:单个楼层Tab标签由5个到9个
-
页面整体高度翻倍:算上头尾,共计14个楼层,高度也由4820px到9862px
-
页面图片量增加:80%的位置变为图片展示
-
首屏加载时间要有保证:加载时间相比原来不能增加
-
首页独特的影响力:页面不能空白,不能有报错
-
大流量高并发,对稳定性要求极高
-
对接业务方很多,临时需求、紧急需求较多
前端页面静态化
众所周知,一般网站首页栏目会有很多,不同栏目的数据库查询方式也不同,而首页流量巨大,如果按照一般的动态网站每次用户来时查询后台数据库取数据的话,开销巨大,从而导致首页访问速度降低,于是要考虑做静态化,我们具体的架构如下:
接入层:CDN—>HAProxy—>nginx
应用层:PHP Redis
存储层:Mysql
首先,用户访问首页实际是请求CDN上拼接好的静态页面,往下会到达Nginx动态缓存,然后到达Nginx,Nginx再把请求转发给PHP进行处理,并通过HTTP头来精确控制缓存;接着到了PHP应用层,使用了Redis作为缓存,并用MySQL存储数据;定时2分钟循环任务生成静态文件,从源服务器同布分发至各地CDN结点。这样用户访问时,会访问到离当前用户最近CDN结点机器上拼装好的静态文件。
前端页面整体架构
-
JS部分
首页页面依赖的JS库为jQuery V1.6.4版本,前期调研评估后采用这个比较低的版本,是因为首页改版上线后,头尾组件以及一大部分公共组件要推至全站使用,即还要平滑升级上千个一二级页面站点的公共头尾,而旧页面引用了非常旧的jQery版本,要想平滑升级综合评估后,1.6.4这个版本最合适;另外jQuery我们根据整体业务进行特殊修正,就比如jsonp回调函数名称后端规范对长度有要求,我们就对长度进行修正等等。
第二层是JDF公共组件,包括UI组件、Unit业务组件、Widget模块,这些组件包括event,localStorage,焦点图,动画,地区选择,对话框,下拉菜单,懒惰加载,suggestion,login,search,category,cookie等常见交互组件和业务组件;同时也有当前业务级需要的公共组件,比如楼层懒加载组件,楼层切换电梯组件等。
第三层是页面脚本,比如今日推荐楼层,猜你喜欢楼层,广告楼层,天天低价楼层等等。
-
CSS部分
改版前的公共样式base.css耦合不少公共组件样式,现在采用很轻的样式重置ui-base.css,公共组件单独按需调用自身样式,彻底解耦。另外首页所有楼层依赖的样式文件是放在首屏加载,而不是请求当前楼层时异步加载,主要为了保证楼层的高可用性和预加载,另外单一个楼层文件非常小,每次到达时候异步加载并不如放在首屏,所有楼层样式文件combo成一个文件请求加载合算。
前端页面加载策略
常见的前端页面加载策略一般为:后端从数据库读取出数据,拼装好必要的页面元素,图片做lazyload(懒加载),用户通过浏览器加载页面上的所有HTML元素。
基于首屏加载时间最短的原则,我们的页面加载采用:楼层异步加载和本地缓存方式,具体如下:
-
把页面按楼层进行拆分,把首屏做为页面框架主体,每个楼层的数据,单独做成数据接口,异步加载,页面仅保留一个楼层一个DIV标签,比如如下服饰楼层:
-
给每个楼层设置默认高度,到达这个区域时请求当前楼层数据文件,同时对楼层数据文件进行md5(即data-time),并把楼层请求后的数据文件和其data-time localStorage至本地,如果页面上的楼层data-time和本地localStorage中的data-time值一样,数据直接取localStorage,如果不一致,重新ajax请求数据,请求后同时会localStorage数据至本地;
-
第一次ajax请求数据时,会延时5秒,查看请求是否成功,如果成功则退出,如果失败:首先会取上次localStorage数据做垫底数据,但浏览器如果不支持localStorage, 基于高可用性原则考虑会进行第二次ajax请求数据,如果第二请求成功则正常渲染和localStorage楼层,如果第二次请求失败,此时极大可能说明网络存在异常,这时为了保持良好的用户体验,不能留有空白,直接隐藏当前楼层。
-
其中用户到达当前楼层时,请求楼层数据(包括楼层的html结构和当前楼层的脚本),渲染数据到楼层元素中,同时初始化楼层脚本。另外楼层有提前预加载逻辑,即用户在第一二楼层时,提前加载好第三四五楼层的数据,到达楼层即显示当前楼层数据。
-
楼层异步加载和本地缓存方式的好处:页面大约共有3288个元素,首屏大约仅900左右,楼层数据lazyload,只渲染首屏元素,大大降低首屏页面内容大小,极大的缩短页面首屏加载完成时间,当时测算首屏约1.6s即加载完,时间比旧版和竞品缩短很多。
-
数据接口优先请求走本地localStorage,减少后台服务器压力,节约成本。请求数据时延时查看成功与否,同时二次请求保证高可用性。楼层之间数据和脚本完全解耦,有利于后期维护,可随意定时上线和下线运营楼层。
前端基础架构
如下图:
前端工具和系统
主要是以JDF命令行工具为核心进行开发,工具和系统的具体使用步骤如下:
-
在代码共享平台新建项目工程
-
使用JDFinit进行项目构建,生成标准化的文件目录和工程文件
-
使用JDFbuild进行模块编译
-
使用JDFoutput进行输出
-
联调时使用JDF upload把编译好的静态文件上传至测试服务器
-
在页面元素上增加前端统计埋点,检测元素的点击量
-
联系后端把项目中页面头部和尾部代码放在头尾系统里,以备后续头尾业务变更
-
上线。上线前通过git提交代码至代码库,上线时在Jone统一工作平台使用线上JDF进行打包;通过Deploy系统把静态文件发布至CDN; 上线后清除静态文件CDN缓存,同时检查线上是否正常;上线后在听云上监控页面性能,定期生成性能报表邮件。
前端灾备策略
灾备是为了保持线上业务在极端很差网络环境下的高可用性,具体做法如下:
-
本地缓存:异步接口数据优先使用本地localStorage中的缓存数据
-
二次请求:接口数据本地无localStorage缓存数据,重新再次发出ajax请求
-
双层接口:部分接口和域名会上线至CDN,业务调用时优先使用CDN接口,如果一定时间内未请求到数据,会用源站接口再次请求
-
垫底数据:源站接口万一无法访问,使用预设好的垫底备份数据
-
接口下线:必要时候,下线非核心业务接口和非核心功能
前端性能优化
好的页面性能可以提高页面加载速度,从而增加用户体验,主要如下:
-
尽可能减少首屏元素数量:用异步加载和本地缓存加载数据
-
CDN加载慢时候,减少页面空白概率:页面CSS样式文件内联在页面上
-
减少页面请求数量:CSS/JS combo,CSS sprite
-
减少静态文件体积:CSS/JS/Images压缩
-
DNS预解析:头部增加比如<linkrel="dns-prefetch" href="//d.jd.com"/>
-
减小Cookie体积等等
前端工程化在电商首页的实践
JDF京东前端开发集成解决方案,核心就是解决前端工程化的问题,包括命令行工具、前端模块、前端开发流程、前端组件、前端文档。
Github地址:https://github.com/putaoshu/jdf/
命令行工具
JDF命令工具基于nodejs,介绍如下:
-
跨平台
完美支持windows、mac、linux三大系统
-
项目构建
生成标准化的项目文件夹
支持本地,联调,线上三种开发流程
每个项目都拥有一个单独的配置文件,按选项统一编译
-
模块开发
可快速方便的对模块进行创建,引用,预览,安装和发布
通过积累,可形成完全符合自己业务的模块云服务
-
模块编译
支持模块编译,内置模块编译引挚
支持将vm和smarty模版编译为html
支持将sass和less编译为css
支持ES6
-
项目优化
自动将页面中的js、css引用转换成combo请求格式
自动压缩优化js、css、png文件
-
项目输出
默认给所有静态资源添加CDN域名前缀或后缀戳
支持cmd规范,自动提取文件id和dependencies,压缩时保留require关键字
支持png图片压缩插件,将png24压缩为png8
自动生成css雪碧图,并更新background-position属性值
可将小图片一键生成base64编码
文件编码统一化,即无论当前文件格式是gbk,gb2312,utf8,utf8-bom,统一输出utf8
-
项目联调
一键上传文件到测试服务器,方便其他同学开发预览
-
本地服务
支持开启本地服务器,方便调试
支持本地静态文件预览,内置本地开发调试服务器,以及当前目录浏览
支持实时监听文件,文件被修改时会自动编译成css,并刷新浏览器
实时在控制台输出错误信息,方便定位代码错误
-
辅助工具
支持html/js/css文件格式化
支持html/js/css代码压缩
支持html/js/css文件lint,代码质量检查
支持chrome浏览器的LiveReload插件
前端模块定义
前端模块即widget:包括配置文件、数据源文件、模版文件、样式文件、JS文件、图片文件,如下ui-product-list为一个widget:
-
ui-product-list[文件夹]
-
component.json[配置文件]
-
ui-product-list.json[数据源文件]
-
ui-product-list.vm[模板文件或者.smarty文件]
-
ui-product-list.scss[scss文件]
-
ui-product-list.js[js文件]
-
images[图片文件]
页面中引用widget用如下代码片断
很明显html不支持{%%}语法,此时会用到jdf的编译命令"jdf build",核心是把ui-product-list.json中的json数据打到ui-product-list.vm模板上,最后输出静态html片断;同时.scss编译成.css,并且在header头增加样式引用:
页面尾部增加js引用:
输出的时候使用jdf output会把多个wiget中js/css引用变成combo的形式如:
这样就算页面引用N多个模块,也只会发出一个请求
前端模块使用方法
-
模块安装:(jdf widget -install xxx ) 解决可复用的问题,不用每次都新建,可以先在模块云上查找合适的模块,然后下载至当前项目
-
模块新建:(jdf widget -create xxx ) 在本地新建一个模块
-
发布模块:(jdf widget -publish xxx ) 解决了共享和沉淀的问题,项目结项,可以把可复用或者修改后的模块提交至模块云,审核通过就会发布至模块云上
-
模块预览:(jdf widget -preview xxx ) 用于调试单个模块,同时解决了团队协作问题,项目分成多个独立模块,各负其责
-
模块列表:(jdf widget -list) 展示模块云上所有模块的列表
搭建成本非常低,只需配置好一台FTP服务器,通过FTP服务器储存,下载,以及分配相关用户权限,版本管理依赖于widget中的componet.json中的version,使用方便,可以使用jdf命令行工具进行发布,下载
前端组件
前端组件主要由UI交互组件和Unit业务组件构成,经过积累,已有以下部分公共组件可供全站使用,如下:
前端开发流程
-
使用JDFinit进行项目构建,生成标准化的文件目录和工程
-
引用JDFwidget -install模块云中可以直接使用的已有模块,同时对页面按业务进行拆分,一个同学负责一个或者多个模块,独立开发和调试
-
使用JDFbuild进行模块编译
-
使用JDFoutput进行输出
-
联调时使用JDF upload把编译好的静态文件上传至测试服务器
-
上线前通过git提交代码至代码库,上线时使用线上JDF进行打包
前端文档
-
前端文档-规范
文档请参考: https://github.com/putaoshu/jdf/tree/master/doc
-
前端文档-组件API
生成工具请参考: https://github.com/putaoshu/jdd
-
前端文档-命令行工具
文档请参考: https://github.com/putaoshu/jdf/tree/master/doc
实际应用
本地新建项目,调用线上模块云相关widget,本地使用JDF工具进行编译,联调,输出,上线时使用线上JDF进行打包,发布至CDN,最后清静态文件缓存。
这就是我今天分享的内容,看大家有什么问题请提出,谢谢大家!
分享问答
问:公共模块云是自己搭建的,还是基于私有的npm仓库?
刘威:自己搭建的,只需在linux上配置好一台FTP服务器,通过FTP服务器储存,下载,以及分配相关用户权限;有好的模块可以发布,开发项目时候看到已有模块可以下载到当前项目中;版本管理依赖于widget中的componet.json中的version;使用方便,可以使用jdf命令行工具进行发布,下载
问:很多这种样式class=“xxx|yyy|zzz” ,是什么意思啊?
刘威:你说的应该是clstag,这是前端埋点统计用的,就是记录某个链接或者某个区域点击的次数,xxx是页面,yyy为楼层,zzz为某个元素,是当前站唯一的标识
问:jdf用什么语言写的?go?python?
刘威:Nodejs
问:刚才提到的“上线后清理本地缓存”是什么意思,是指服务器的cache么?那么用户本地的资源文件缓存是如何处理的?
刘威:是指清CDN上静态文件的缓存,我们某个项目中静态资源会有统一前缀,比如product/home/1.0.0/a.js,如果项目有更新会发product/home/1.0.1,大型项目会变更大的版本号product/home/2.0.0这样,这样到用户那里都是全新的文件。
问:关于cdn缓存,假如有文件更新你们是怎么处理?时间截、md5?
刘威:一般是给静态资源前面加统一的版本号来实现,如果特别小的变动,不想变更文件url,可以在文件上线后,通过CDN后台,手动清当前文件的CDN缓存。
问:你们的图片压缩是如何实现的,应用那些组件?
刘威:我们自己根据不同操作系统进行了封装,可以下载https://www.npmjs.com/package/jdf-png-native查看
问:模块的依赖解析也是在jdf构建时处理的么?比如a依赖b,b依赖c,在使用a时,是不是会把a b c三个js都加到尾部?
刘威:模块中的js依赖我们基于CMD规范,jdf工具会自动提取文件id和dependencies,压缩时保留require关键字,另外某个项目一般建议有一个统一的入口文件,比如init.js,如果init.js有依赖的通过seajs的combo插件完成全并依赖加载。
问:刚才资源文件是用版本号来区分,那么如果多出引用更新后是否需要多处修改版本号?
刘威:我们项目一般会把页面的公共头尾接入公共头尾系统,就是头部的静态html片断以及js/css引用,而项目会对应头尾部系统中某一个编号的头尾,前端静态资源上线后,只需要修改一处,然后系统就会推送到业务线中。
问:DNS预解析在不同浏览器的兼容性如何?会不会影响当前页面的性能?确实我们也有一些页面做了上十个DNS预解析,甚至一些暂时用不到的。
刘威:DNS预解析在不同浏览器的兼容性:Chrome: 全部 IE: 9+ Firefox: 3+ Safari 5+ ,可以说大部分高级浏览器都支持的;如果业务依赖的域名很多,建议增加上DNS预解析。
问:前后分离的情况下,SEO方面,怎么处理会比较好?
刘威:前后端分离对SEO没有什么大的关系吧,我的建议是最好前端把后端模板层接过来,比如php的smarty,java的vm,由前端来写模板,另外而jdf工具支持.smarty,.vm模板渲染,可以看一下;SPA页面的SEO我还没好的建议
问:jdf应该与glup,grunt,fis等类似的构建工具吧,jdf有什么特点?
刘威:jdf比较轻便,另外jdf依赖的npm插件一般经过会我们精心筛选,也有部分第二次封装,不像grunt的插件质量参差不齐;fis相对jdf比较重,fis基于java,php,node都有一套独立的解决方案,而jdf只需要nodejs下即可支持.smarty,.vm模板渲染和解析
问:关于页面静态化,有几个问题,1.“并通过HTTP头来精确控制缓存”,这个精确控制指的是什么?2.我是不是可以这么理解,用户请求先访问CDN,如果CDN有数据直接返回,没有数据回源到后端服务器,此时的逻辑是分级缓存策略,减少对服务端上游接口的压力,“定时2分钟循环任务生成静态文件” 这个任务是单独部署一台服务器,只从上游接口拼装数据生成静态html,然后推送同步到CDN节点? 3.静态html从源服务器同步到不同的CDN节点这个是怎么实现的? 4.如果个性化的数据比如根据用户维度下发不同的定向数据,或者是有秒杀楼层等时效性很强的楼层,这个静态化就没有意义了吧?
刘威:1.通过Cache-Control控制,每两分钟更新一次 2.对的 3.生成好静态文件碎片,通过nginx定时任务,从源服务器同步至CDN 4.对的,个性化推荐是通过ajax异步加载数据,每次都不一样。
问:如果是统一头尾更新版本号那么是针对各个资源更新还是所有资源?如果是各个资源岂不是要维护很多版本?如何自动化?如果是所有资源用户的缓存岂不是浪费了?
刘威: 恩,jdf工具可以支持所有静态资源加统一前缀版本号,另外我们有线上jdf环境,只需要在本地版本库里更新一下版本号,上线会系统自动处理,再然后这些静态资源上线后通过头尾系统手动更新版本号;当然最理想的方式是前端全部接入模板层,这样头部文件的版本号可以用全部jdf工具来自动更新了,但电商网站的后端系统复杂性暂时选择了半手动的方式,只有部分业务线进行了实现.
问:京东是如何处理线下测试smarty模版的?是前端直接在php环境下开发吗 ?
刘威:jdf工具支持.smarty解析,只要nodejs就ok,前端在本地修改测试后,可以一键发送至联调机器