![50e9bf2f915b341f7a334677c6026ea6.png](https://img-blog.csdnimg.cn/img_convert/50e9bf2f915b341f7a334677c6026ea6.png)
背景
我们的搭建系统从积木盒子到奇美拉,PC和H5页面都是静态的,发布时将页面静态化发布到cdn,浏览器访问时,从cdn获取页面进行异步渲染。由于页面是静态和异步的,对于首屏和SEO有诉求的场景无法很好的支持;另外对于像组件AB、动态区块等动态能力也没有办法实现。基于上诉原因,同构渲染是我们搭建系统所需要的。
技术方案
出于性能、稳定性和可扩展性的考虑,奇美拉的同构渲染服务分为3个部分:搭建(页面来源)-> 预处理 -> 同构渲染服务,下面是整体架构图,接下来将详细介绍每个部分。
![7c22fecb9ee4f19acb1e4c3cf42b84d6.png](https://img-blog.csdnimg.cn/img_convert/7c22fecb9ee4f19acb1e4c3cf42b84d6.png)
搭建(页面来源)
搭建就是将组件拼凑成页面,类似搭积木,输出是描述页面结构的schema,供预处理消费。由于页面是通过schema表示的,所以可以方便的对接其他平台。那么schema里面有什么呢?
组件
第一个肯定是组件,组件分为”布局“和”组件“,”布局“是用来控制”组件“放在什么地方的,比如下面的”布局“含有三个位置,可以放不同的”组件“。
![96728c4fb9c46980fdfc52ec98cb73d4.png](https://img-blog.csdnimg.cn/img_convert/96728c4fb9c46980fdfc52ec98cb73d4.png)
数据
一般情况下仅有组件是无法渲染的,还需要数据,除非是静态组件,为此schema还需要包括数据获取方式的描述。虽然奇美拉和积木盒子通过jdata能够很好的获取数据,但是一些业务无法走jdata,所以需要一种通用的方式,在服务器端HSF就是一种很好的方式。为此获取数据的方式分为jdata和HSF两种,如下:
jdata(通过value就可以获取数据):
{
"type": "jdata",
"value": 123456
}
HSF(value是用来描述调用HSF的数据):
{
"type": "hsf",
"value": {
"appName": "widget-router-hsf",
"id": "com.alibaba.widgetpt.service.WidgetService:2.0.0",
"group": "HSF",
"method": "getJsonComponent",
"args": [
"adPcWidget:adPcWidget",
"execute"
],
"argsTypes": [
"java.lang.String",
"java.lang.String"
]
}
}
除了jdata和HSF,上面的格式能够容易的扩展到其他方式。
组件组件的其他描述
为了能够支持一张页面同时存在同构动态组件(服务器端渲染,数据会不断变化,比如千人千面的组件)、同构静态组件(服务器端渲染,数据不会变化,比如热区组件)、异步组件(在浏览器端获取数据并渲染的组件),schema需要有字段区分它们;同时为了支持不同技术体系的组件,比如react和jquery,也需要有字段区分;此外schema中还有一些组件其他描述,比如spm(c位)。
页面渲染描述
上面已经了解了渲染组件所需要的内容,将组件拼装成页面,还需要一些页面级别的描述。比如承载页面的模板,我们成为原型prototype;页面的访问地址;页面的SPM ab位;下线时间和下线地址;页面的头部和尾部等。
schema
下面是一份简单的页面schema:
{
"attributes": {
"page-url": "https://cms.1688.com/ssr/ssr/ssr/hhd2n7pi.html", // 访问地址
"page-spm": "a2632k.12847700", // spm ab位置
"offline-time": null, // 下线时间
"offline-url": "", // 下线地址
"page-prototype": "1688/pc/fusion.html", // 页面原型
"page-header": "", // 页面头部
"page-footer": "", // 页面尾部
},
"children": [
{
...
"attributes": {
"component-name": "@alife/ocms-layout-1688-wap-layout-common", // 包名
"component-version": "1.0.4", // 版本
"component-spm": "jxolqgpw", // spm c
"component-type": "layout", // 表示布局类型
"component-tech": "pure", // 技术体系,pure表示jquery组件
"component-runat": "server" // 表示同构静态组件
},
"children": [
{
"attributes": {
...
"component-name": "@alife/ocms-fusion-1688-pc-brand-pc-gather",
"component-spm": "jyh2ezk1",
"component-version": "1.0.17",
"component-type": "component",
"component-tech": "fusion",
"component-runat": "web",
"component-isomorphic": true,
// 描述数据获取的方式
"component-isomorphic-data": "{"type":"hsf","value":{"appName":"widget-router-hsf","id":"com.alibaba.widgetpt.service.WidgetService:2.0.0","group":"HSF","method":"getJsonComponent","args":["adPcWidget:adPcWidget","execute"],"argsTypes":["java.lang.String","java.lang.String"]}}"
}
}
]
}
]
}
预处理
出于性能和稳定性的考虑,我们没有把描述页面的schema直接交给同构渲染服务,而且进行一次预处理,主要包括资源分析和处理、预渲染、资源预取和兜底页面几个部分。
![0dca1707f34c870e93f050e91866eefd.png](https://img-blog.csdnimg.cn/img_convert/0dca1707f34c870e93f050e91866eefd.png)
资源分析处理
由于我们的组件都在cdn上,schema只给出了组件的包名和版本,组件渲染还需要其依赖,为此要将每个组件的依赖、js列表和css列表都分析出来。由于同构渲染服务是通过积木盒子的渲染引擎croco来实现的,它的渲染是根据描述页面的DSL来实现的,所以需要将描述页面schema转化成DSL,DSL中包含预先渲染好的同构静态组件。下面是一个croco DSL的例子:
<div page-id="4286" data-cms="chimera" class="ocms-container">
<div component-uuid="4" component-name="@alife/ocms-layout-1688-wap-layout-common"
component-version="1.0.4" component-type="layout" component-tech="pure" data-spm="jxolqgpw"
component-async="false" component-stage="render">
<div class="ocms-layout-1688-wap-layout-common-1-0-4">
<div>
<div name="main" class="croco slot">
<component slot="main" component-uuid="5" component-name="@alife/ocms-fusion-1688-pc-brand-pc-gather"
component-version="1.0.17" component-type="component"
component-tech="fusion" component-smart="false" component-isomorphic="true"
component-isomorphic-data="{"type":"hsf","hsf":{"appName":"widget-router-hsf","id":"com.alibaba.widgetpt.service.WidgetService:2.0.0","group":"HSF","method":"getJsonComponent","args":["adPcWidget:adPcWidget","execute"],"argsTypes":["java.lang.String","java.lang.String"]}}"
data-spm="jyh2ezk1" component-async="true">
<div style=";overflow:hidden; width:auto;height:150px" class="ocms-loading"></div>
</component>
</div>
</div>
</div>
</div>
</div>
预渲染
根据上文,一个页面可能具有同构静态组件,它在服务器端渲染的结果是不会变,因为组件版本和数据都不会变,所以没必要每次访问页面时都渲染一次,提前渲染好是一种不错的选择,这样访问时只需要将渲染好的html拼接到页面里就好了。
另外搭建系统中页面的头部和尾部是通过esi实现的,几乎很少变动,所以也可以提前处理好。
资源预取
拼接页面需要模板,也就是原型prototype,它在cdn上,可以提前预取。资源分析出的css也可以提前预取,在拼接页面的时候打到html,可以避免reflow和repaint。
兜底页面
出于稳定性的考虑,预处理的时候会cdn发布一张浏览器可以直接跑的页面,同构动态组件会在浏览器端进行异步渲染,这样即使同构渲染服务挂掉了,也可以通过兜底页面展示内容。
输出
预处理的结果会保存在oss上供同构渲染服务使用,具体结构如下:
![b914bcfd443a09f2b4fbb7b0873adf8c.png](https://img-blog.csdnimg.cn/img_convert/b914bcfd443a09f2b4fbb7b0873adf8c.png)
同构渲染服务
当用户访问时,同构渲染服务会返回渲染好的html,从下到上分为3层:纯渲染服务 -> 拼接服务 -> 上层服务,它们是基于Fass实现的,架构图如下:
![fd89d70fe6280986565eadec6c31216b.png](https://img-blog.csdnimg.cn/img_convert/fd89d70fe6280986565eadec6c31216b.png)
纯渲染服务
纯渲染服务只做渲染,输入croco的DSL、组件列表和数据列表,就能得到渲染好的html。由于只做渲染,所以该服务可以开放出去给其他团队使用,比如和我们密切合作的广告团队。
拼接服务
获取渲染好的html,接下来就是拼接页面了,核心就是将预取的头尾、js列表、css和渲染好的html填充到原型prototype中,原型prototype本质上是一个art-template。
上层服务
实际上拼接服务返回的内容就可以直接返回给用户了,出于性能、稳定性和扩展性的考虑,我们抽象出了这一层。该服务目前做了兜底和缓存。如果下层拼接服务或者纯渲染服务挂掉了,会将预处理的兜底页面返回给用户;一些业务没有实时性的要求,可以为页面配置不同的缓存策略。除了兜底和缓存,这层还可以扩展其他功能,比如页面AB、页面下线等。
最近看了一下CDN团队开发的边缘Serverless计算环境EdgeRoutine,感觉是非常适合做这些事情的,后期可能会将该服务迁移到EdgeRoutine上。
性能优化
上面我们已经提及了一些优化,主要在预处理阶段,比如同构静态组件的预渲染,原型prototype的预取、css的预取等。此外我们在同构渲染服务那层也做了一定优化,主要在于组件上。
- 因为组件资源在cdn上,如果每次渲染都从cdn上拉取,会非常耗时;另外组件从文件变成类也会消耗大量的CPU计算。为此我们对组件进行了3级缓存:内存、本地文件和oss,具体如下:
![d39aaacd94425d9410baec3532c26dd5.png](https://img-blog.csdnimg.cn/img_convert/d39aaacd94425d9410baec3532c26dd5.png)
2. 第一次访问页面时,由于各级缓存都没有,所以还会走cdn,为此我们做了组件预热。在diamond中配置重要页面的组件,服务器在重启或修改diamond时会将组件提前加载并缓存起来。
3. 如果一个页面有多个组件,所以组件和数据都是批量并发请求的。
兜底
处于稳定性的考虑,我们做了一定兜底处理,分为组件兜底和页面兜底。
组件兜底
组件渲染需要模块和数据,两者都获取成功时才会在服务器端进行同构渲染;如果有一个获取失败,组件将会到前端进行异步渲染。
![c071b841540d0e5038c97bcbe8d95743.png](https://img-blog.csdnimg.cn/img_convert/c071b841540d0e5038c97bcbe8d95743.png)
页面兜底
页面渲染时,如果出现错误或超时,会返回预处理阶段生成的兜底页面。
![0f3a56057664bbc14cb75c90a53347ee.png](https://img-blog.csdnimg.cn/img_convert/0f3a56057664bbc14cb75c90a53347ee.png)
案例展示
ok,原理讲了这么多,让我们看一下效果吧。广告pc品专组件,在接入同构渲染服务之前,首屏时间大概需要1.52s,接入同构渲染后首屏变成了0.3s,降低了1.22s。查看地址:链接
图中红框框柱的部分是广告pc品专组件:
![10cc1b868a4d6ae5daa3b75abbf28059.png](https://img-blog.csdnimg.cn/img_convert/10cc1b868a4d6ae5daa3b75abbf28059.png)
接入奇美拉同构渲染服务的结果:
![3296fa6ca6d3ed85d90da43de139b90d.png](https://img-blog.csdnimg.cn/img_convert/3296fa6ca6d3ed85d90da43de139b90d.png)