京东架构师:前端工程化在京东首页实践

2016年3月,互联网技术联盟(ITA1024)推出前端技术专题月,由联盟成员企业推荐国内一流技术专家联手打造。通过每周的线上万人课堂和每月的线下ITA1024互联网技术大会,针对前端开发,前端框架,性能调优,复合型前端技术等热门话题展开深入的分享和交流。

 本期分享嘉宾:刘威(京东资深前端架构开发工程师)

本期特邀主持:赵晓强(百度高级前端工程师)

 如下是3.28刘威在ITA1024前端精英群分享实录。

京东首页前端架构设计与实现

面临挑战

前端页面静态化

前端页面整体架构

前端页面加载策略

前端基础架构

前端工具和系统

前端灾备策略

前端性能优化

前端工程化在电商首页的实践

命令行工具

前端模块

前端组件

前端开发流程

前端文档

实际应用

京东首页前端架构设计与实现

1、面临挑战

  • 页面DOM元素剧增:单个楼层Tab标签由5个到9个

  • 页面整体高度翻倍:算上头尾,共计14个楼层,高度也由4820px到9862px

  • 页面图片量增加:80%的位置变为图片展示

  • 首屏加载时间要有保证:加载时间相比原来不能增加

  • 首页独特的影响力:页面不能空白,不能有报错

  • 大流量高并发,对稳定性要求极高

  • 对接业务方很多,临时需求、紧急需求较多

 

2、前端页面静态化

众所周知,一般网站首页栏目会有很多,不同栏目的数据库查询方式也不同,而首页流量巨大,如果按照一般的动态网站每次用户来时查询后台数据库取数据的话,开销巨大,从而导致首页访问速度降低,于是要考虑做静态化,我们具体的架构如下:

 

接入层:CDN—>HAProxy—>nginx

应用层:PHP Redis

存储层:Mysql

首先,用户访问首页实际是请求CDN上拼接好的静态页面,往下会到达Nginx动态缓存,然后到达Nginx,Nginx再把请求转发给PHP进行处理,并通过HTTP头来精确控制缓存;接着到了PHP应用层,使用了Redis作为缓存,并用MySQL存储数据;定时2分钟循环任务生成静态文件,从源服务器同布分发至各地CDN结点。这样用户访问时,会访问到离当前用户最近CDN结点机器上拼装好的静态文件。

3、前端页面整体架构

  • 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成一个文件请求加载合算。

4、前端页面加载策略

常见的前端页面加载策略一般为:后端从数据库读取出数据,拼装好必要的页面元素,图片做lazyload(懒加载),用户通过浏览器加载页面上的所有HTML元素。

 基于首屏加载时间最短的原则,我们的页面加载采用:楼层异步加载和本地缓存方式,具体如下:

    •   把页面按楼层进行拆分,把首屏做为页面框架主体,每个楼层的数据,单独做成数据接口,异步加载,页面仅保留一个楼层一个DIV标签,比如如下服饰楼层:
  • <divclass="w floor lazy-fn" data-title="服饰"id="lazy-clothes" data-path="floor1-floor_index.js"data-time="01d15d664a61ff8f11cf6321f5b7a503"></div>
    其中floor1-floor_index.js是楼层数据文件 ,包含楼层数据和信赖的脚本文件,其样式已放在header里预加载。
    

      

  • 给每个楼层设置默认高度,到达这个区域时请求当前楼层数据文件,同时对楼层数据文件进行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,减少后台服务器压力,节约成本。请求数据时延时查看成功与否,同时二次请求保证高可用性。楼层之间数据和脚本完全解耦,有利于后期维护,可随意定时上线和下线运营楼层。

5、前端基础架构

如下图:

6、前端工具和系统

主要是以JDF命令行工具为核心进行开发,工具和系统的具体使用步骤如下:

  • 在代码共享平台新建项目工程

  • 使用JDFinit进行项目构建,生成标准化的文件目录和工程文件

  • 使用JDFbuild进行模块编译

  • 使用JDFoutput进行输出

  • 联调时使用JDF upload把编译好的静态文件上传至测试服务器

  • 在页面元素上增加前端统计埋点,检测元素的点击量

  • 联系后端把项目中页面头部和尾部代码放在头尾系统里,以备后续头尾业务变更

  • 上线。上线前通过git提交代码至代码库,上线时在Jone统一工作平台使用线上JDF进行打包;通过Deploy系统把静态文件发布至CDN; 上线后清除静态文件CDN缓存,同时检查线上是否正常;上线后在听云上监控页面性能,定期生成性能报表邮件。

7、前端灾备策略

灾备是为了保持线上业务在极端很差网络环境下的高可用性,具体做法如下:

  • 本地缓存:异步接口数据优先使用本地localStorage中的缓存数据

  • 二次请求:接口数据本地无localStorage缓存数据,重新再次发出ajax请求

  • 双层接口:部分接口和域名会上线至CDN,业务调用时优先使用CDN接口,如果一定时间内未请求到数据,会用源站接口再次请求

  • 垫底数据:源站接口万一无法访问,使用预设好的垫底备份数据

  • 接口下线:必要时候,下线非核心业务接口和非核心功能

 8、前端性能优化

好的页面性能可以提高页面加载速度,从而增加用户体验,主要如下:

  • 尽可能减少首屏元素数量:用异步加载和本地缓存加载数据

  • CDN加载慢时候,减少页面空白概率:页面CSS样式文件内联在页面上

  • 减少页面请求数量:CSS/JS combo,CSS sprite

  • 减少静态文件体积:CSS/JS/Images压缩

  • DNS预解析:头部增加比如<link rel="dns-prefetch" href="//d.jd.com"/>

  • 减小Cookie体积等等

 

前端工程化在电商首页的实践

JDF京东前端开发集成解决方案,核心就是解决前端工程化的问题,包括命令行工具、前端模块、前端开发流程、前端组件、前端文档。

Github地址:https://github.com/putaoshu/jdf/

 

1、命令行工具

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插件

 

2、前端模块定义

前端模块即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用如下代码片断

{%widgetname="ui-product-list"%}

很明显html不支持{%%}语法,此时会用到jdf的编译命令"jdf  build",核心是把ui-product-list.json中的json数据打到ui-product-list.vm模板上,最后输出静态html片断;同时.scss编译成.css,并且在header头增加样式引用:

<linktype="text/css" rel="stylesheet" href="/widget/ui-product-list/ui-product-list.css"source="widget"/>

页面尾部增加js引用:

<scripttype="text/javascript" src="/widget/ui-product-list/ui-product-list.js"source="widget"></script>

输出的时候使用jdf output会把多个wiget中js/css引用变成combo的形式如:

??/widget/ui-product-list/ui-product-list.css,/widget/header/header.css/widget/footer/footer.css

这样就算页面引用N多个模块,也只会发出一个请求

3、前端模块使用方法

  • 模块安装:(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命令行工具进行发布,下载

4、前端组件

前端组件主要由UI交互组件和Unit业务组件构成,经过积累,已有以下部分公共组件可供全站使用,如下:

前端开发流程

  • 使用JDFinit进行项目构建,生成标准化的文件目录和工程

  • 引用JDFwidget -install模块云中可以直接使用的已有模块,同时对页面按业务进行拆分,一个同学负责一个或者多个模块,独立开发和调试

  • 使用JDFbuild进行模块编译

  • 使用JDFoutput进行输出

  • 联调时使用JDF upload把编译好的静态文件上传至测试服务器

  • 上线前通过git提交代码至代码库,上线时使用线上JDF进行打包

6、前端文档

 

    • 前端文档-规范

 

文档请参考: https://github.com/putaoshu/jdf/tree/master/doc

  • 前端文档-组件API

生成工具请参考: https://github.com/putaoshu/jdd

  • 前端文档-命令行工具

文档请参考: https://github.com/putaoshu/jdf/tree/master/doc

7、实际应用

本地新建项目,调用线上模块云相关widget,本地使用JDF工具进行编译,联调,输出,上线时使用线上JDF进行打包,发布至CDN,最后清静态文件缓存。

 

————————————Q&A————————————

问:公共模块云是自己搭建的,还是基于私有的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,前端在本地修改测试后,可以一键发送至联调机器

 

posted @ 2016-04-07 11:31  小兵传奇`  阅读(2352)  评论(0编辑  收藏  举报