干货|爱奇艺PC Web新框架实践
作为爱奇艺主站PC Web前端框架,qiyiV2已使用了将近6年之久。基于seajs与jquery的他给主站带来了模块化与面向对象开发的模式,很好地解决了代码分块与复用的问题。但是qiyiV2框架很大程度上专注于业务,可扩展性并不好。随着项目复杂度与代码行数的增长,qiyiV2里添加了越来越多的兼容代码,变得越来越臃肿。另一方面,qiyiV2还需要去兼容IE6-8这样古老的浏览器,致使高版本浏览器特性无法充分发挥。基于种种原因,我们在Vue2.0的基础上,定制出了一套与自身业务场景相适应的框架 ——Uniqy。
1. 特性
作为一款十分轻量面向用户站点的前端框架,Uniqy 具有以下特点:
基于vue2.0
Vue2.0是目前最为流行的前端框架之一,易学易用是其鲜明的标志。同时不俗的性能表现也是我们选择其作为框架基础的重要原因。Uniqy在Vue2.0基础上,整合了一些流行的技术(如PJAX、组件懒加载等),使之适用于开发类似爱奇艺这样的主站组件繁多、功能复杂、迭代频次高的业务。另外为了方便开发,我们借鉴了lodash与ramda等函数式类库,另编写了一套工具类方法,分别发布为 core(框架核心)与 Qi(工具库方法)两个包,方便各个团队依据自身业务需要进行定制化安装。
利用slot插槽
对爱奇艺PC Web主站来说,良好的SEO能够帮助其获得更多的搜索流量,因而页面上一些非常重要的内容仍然需要依靠服务端进行渲染,由于另外开发一套基于Node的SSR后台成本较高,而乐趣(基于java和velocity模板引擎)平台作为渲染系统已经十分成熟且运行稳定,在充分试验后,我们决定在Uniqy中使用服务端同步与客户端浏览器异步二次渲染相结合的方式,结合Vue2.0提供的 slot插槽机制,很好地解决模板、数据复用的问题为SEO提供了一套完备的方案。
使用PJAX组件
由于爱奇艺PC Web主站拥有许多页面,而各个页面的跳转都是通过新开页(或当前页)刷新的方式。我们知道刷新是一种十分耗时的操作,往往新页面从刷新到加载完毕用户需要等待5s以上,为了提升用户体验,我们在Uniqy框架中引入了PJAX组件技术,大大降低了用户在当前页刷新式跳转的等待时间,同时这种技术也解决了一部分组件模板复用的问题。
支持装饰器与类风格声明
主站业务迭代十分迅速,我们在快速开发需求的同时也需要顾及到日后代码的易维护程度。在借鉴了Angular2+的类风格组件以及官方vue-class-component提供的组件装饰器后,我们决定向 Uniqy中添加这些特性,通过封装多种装饰器,结合类风格声明,进行组件、指令、Store等的定义,使得代码更为语义化、精炼和易维护。
2. uniqy 实践
根据我们对爱奇艺PC Web站点用户浏览器版本数据的统计,高版本浏览器(IE10及以上,chrome,360极速模式等)占据绝大多数,低版本浏览器(IE9及以下)数量仍然占据一定比例,所以对于低版本浏览器用户,我们不能完全放弃。为此,我们在CDN层面进行UA判定,对高低两类版本的浏览器在建立页面请求时响应相应高低版本的页面模板。这里有一张渲染架构图可以展示这个过程:
对于低版本浏览器运行的是qiyiV2的项目代码,而对于高版本浏览器运行的是Uniqy项目代码。
2.1 同步与异步二次渲染相结合
使用高版本框架最大的优势就是能够快速迭代,但采用纯异步渲染的模式十分不利于SEO,而爱奇艺PC Web主站十分依赖搜索引擎导流,做好SEO是不可或缺的。另外,单页应用有一个比较明显的问题,就是启动白屏(如果你的模板中只有孤零零的一个仅仅用于挂载应用的DIV的话)。基于这些,我们决定使用同步与异步二次渲染相结合的方式。也就是说在服务端按需直出页面,而在客户端浏览器上使用框架进行二次的组件渲染,完成最终的视图构建。这里有一张图可以描述主站 PC Web 页面渲染的基本模式:
一些比较重要的组件,可以优先在服务端渲染好,而一些搜索权重并不是很高的组件,我们可以使用异步渲染的方式。所以,当你使用高版本浏览器访问爱奇艺大首页、播放页等页面时,看到的第一屏内容是由服务端渲染出的,而等页面 js 完全下载好开始执行时,包括第一屏的组件会被二次编译,绑定 js 中定义好的方法、指令或过滤器等,与异步组件一并渲染好插回原始DOM树中。
可以明显看到第一屏下半部分是空白的,因为非首屏部分采用的是异步渲染,这些组件只有在滚动至视口区域时才会被编译,然后被插回到DOM树中,完成视图更新。
同步与异步二次渲染相结合的做法解决了两个问题:
启动白屏
页面无法被爬虫抓取
同时,这种模式也使得我们的首屏等待时间大大降低:
益于同步与异步二次渲染相结合的方式,大首页首屏时间可以稳定在1100ms左右,这是一个相当快的速度,对于用户而言,基本上没有明显的延迟就能获取到第一屏内容。当然,由于部分组件模板会置于服务端处进行渲染,这会涉及到复用的问题,因而我们还需要引入另外一种技术来解决它。
2.2 引入PJAX技术
PJAX是一种通过请求一段html片段来替换当前页面模板,配合HTML5提供的history.pushState API修改location.href链接从而达到无刷新跳转页面的技术。这项技术由来已久,而我们通过与 vue-router相整合,创造性地运用在了主站上。也许有人会问,直接用vue-router不就够了?
前面说到,PC Web主站属于多页应用,而且对SEO有强需求,所以页面上部分组件是通过服务端(乐趣)渲染的。而vue-router属于单页应用路由,也就是得保证组件模板已经打包进入业务 js 中,才能顺利渲染出页面,这也就意味着每个组件都需要维护乐趣和本地两套模板,十分不便。另一方面,改造渲染系统为Node SSR的成本太高,因而我们引入了PJAX技术,在路由跳转前,请求到页面,从中抽取出组件模板字符串,进行本地编译和二次渲染,最后再插回当前DOM树中,完成视图更新。
具体而言,需要利用到vue-router 的beforeRouteEnter 和beforeRouteUpdate两个钩子方法以及vue中的beforeCreate钩子。beforeRouteEnter和beforeRouteUpdate分别在跳转至新页面和当前页面刷新时调用,在这个时机我们可以通过异步请求拉取线上模板,然后在本地通过Vue.compile编译,获得一个包含渲染函数的对象,最后在beforeCreate钩子调用时替换组件里$options中的render和staticRenderFns 属性。
简而言之如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | export default { beforeCreate() { // 判断是新路由跳转还是同路由更新 // 若为新路由跳转,需要抽取 页面模板部分替换掉 this.$options. template // 若为同路由更新,需要对页面模板部分进行 Vue.compile // 然后替换掉this.$options.render 和 this.$options. staticRen derFns }, beforeRouteEnter(to, from, next) { // 新路由跳转时进入PJAX过程, 否则放行 if (from.name || (from.path && from.path != "/")) { // PJAX 过程 } else { next(); } }, beforeRouteUpdate(to, from, next) { // 同路由更新时直接进入PJAX过程 } }; |
新开页面时
前页刷新时我们看到模板上有一段这样的代码:
1 2 3 | <!--@<template slot-scope="props">@--> ... <!--@</template>@--> |
这是一段特殊的注释标记,而进入PJAX过程后,这段代码的注释标记会被去除,也就是说最终被编译的组件模板为:
1 2 3 | <template slot-scope="props"> ... </template> |
熟悉 slot 插槽的开发者应该知道,template 元素的内容将成为待编译组件的模板,我们在路由 beforeRouteEnter 的时机将此模板进行编译(使用 Vue.compile 方法),然后在组件 beforeCreate 时替换掉 vm.$options 中的 render 和 staticRenderFns 属性,从而完成同步与异步组件模板复用。
结合 PJAX 和 vue-router,自然也能够在当前页进行无刷新跳转:
首次进入页面时,会拉取 common、vendor 以及对应业务 js 和 css 文件。
而在页面点击链接触发无刷新跳转时,会去异步获取目标页面的 html 模板,此时不必再去拉取 common、vendor等js,只需再下载对应页面的业务js并执行,然后本地对目标页面模板进行编译渲染,替换掉当前的视图,从而完成无刷新跳转。
2.3 装饰器和类风格
在基于 qiyiV2 框架开发业务的时代,数据与 DOM 的相互映射需要靠我们手动去完成,先回顾一段基于 qiyiV2 框架中 action 代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | /** |
在开发qiyiV2项目时,我们需要编写大量操作DOM的代码,而在引入高版本框架后,框架会帮助我们建立数据与视图的相互映射,显然不必再重复这些工作。
另一方面,js在这些年有了巨大的发展,随着es6、es7等版本发布,各种新特性与API的引入,无疑提高了开发者的生产效率。
webpack的诞生,很好地解决代码分割与加载的难点,Vue2.0则很好地将数据与视图进行双向绑定。基于 高版本框架+es6+webpack的开发方式,原先面向DOM的编程方式已经转变为组件化编程,通过数据驱动视图,开发人员能够专注于业务逻辑开发,进一步降低了出错可能。
为了让我们的代码更为简练,Uniqy在Vue2.0基础上,还引入了装饰器(Decorators)语法。虽然目前装饰器还处于 stage-2 阶段,但实际上在业界已经被广泛使用,例如Angular2+
,vue-class-component
等。Uniqy引入装饰器则是为了提供一些预定方法的封装,配合类风格的声明,进一步使代码变得精炼和语义化。
应用的声明:
1 2 3 4 5 6 7 8 9 10 | import { Application } from "@uniqy/core"; @Application({ el: "#app", template: "<div>{{msg}}</div>"; }) class App { msg = "Hello world~"; } |
组件的声明:
1 2 3 4 5 6 7 8 9 10 | import { Component } from "@uniqy/core"; @Component({ name: "hello-world", global: true, // 表示全局组件 template:"<h1>{{msg}}</h1>" }) class HelloWorld { msg = "I am a component!"; } |
Vuex Store 的声明:
1 2 3 4 5 6 7 8 9 10 | import { Store } from "@uniqy/core"; @Store() class HomeScore { scoreDatas = {}; zhuijuDatas = {}; } const homeScore = new HomeScore(); export { homeScore }; |
软件工程里一直推崇一种理念:Write less code(编写更少的代码)更少的代码意味更低的出错概率。这也是Uniqy框架引入装饰器的重要原因,通过装饰器模式,我们能够以一种十分优雅和精炼的方式封装和使用中间方法,尽可能地减少重复代码的编写大大提升了代码的健壮性与可读性,也方便日常维护。
2.4 组件懒加载
主站页面功能十分繁杂,同一个页面里往往会有上百个组件,而有一些组件我们并不需要在第一屏或第二屏的时候就编译出来,如果能减少这些组件的渲染,页面的性能会大大提升。这也是我们优化的一个重要方向。
我们知道vue中属性为v-if="false"
的组件一般都不会立即编译和渲染,会等待v-if="true"
的时候才进行,所以我们可以利用这一点,将尚未处于可见区域的组件v-if
设置为false
,待进入可见区域后再更改为 true
。
我们在项目中安装了vue-lazyload
模块,然后使用lazy-component标签包裹组件。
们在爱奇艺VIP会员频道页试点了这一技术,页面的初始加载时间大大降低:
3. 总结
我们大致总结下在使用高版本框架进行业务开发时的实践:
同步与异步二次渲染相结合
引入 PJAX 技术
推广使用装饰器语法和类风格声明
组件懒加载
……
我们在 Vue2.0 的基础上,结合自身业务特性,封装了一套 Uniqy 框架,引入了 PJAX 技术以及装饰器语法。由于对 SEO 有强需求,我们实际开发中广泛使用了同步与异步二次渲染相结合的方式,另外对组件采取了懒加载技术,进一步降低了首屏组件渲染耗时……
4. 后记
对于工程而言,永远不只有一种解决方案,只有更适合的方案。对于每个开发人员而言,代码是僵硬的,而人是活的。相互借鉴,融会贯通不失为另一种创新。在不断的实践、试错、探索中,往往一条坦途大道就此而成,而这求知的过程也正是软件技术的魅力所在,不是吗?
End
也许你还想看:
干货|文本舆情挖掘的技术探索和实践