天天看点

前端工程师的自我修养-SSR在可视化大屏搭建中的实践

前言

​ 这篇关于SSR的文章计划好久了,但是一直没着落。一方面,网上已经有很多相关的文章,还写的挺不错;另一方,之前对SSR也一直处于理论阶段,没有实操,担心写出来误导群众。从去年10月份开始,一直在做大屏搭建相关的工作,在这过程中积累了一些SSR的使用心得,这里拿出来和大家分享一下,与君共勉。

背景介绍

话不多说,直接上图

前端工程师的自我修养-SSR在可视化大屏搭建中的实践

这张图就是我去年下半年一直在做的事儿。通过可视化的方式搭建出一张数据大屏。目的有两个:

  1. 组件复用
  2. 快速部署
  3. 节约成本,毕竟某云的可视化解决方案也不便宜

在之前的文章中我同事分享过一篇可视化搭建数据大屏系统BIG的文章,这里不再详细介绍,有兴趣的可以转场去看看。这张图展现的是如何通过可视化操作将一个个组件组合在一起,行成一面具有信息价值的大屏。今天要讲的是这张图背后,关于服务端渲染(SSR)在搭建场景中的应用。接下来会从下面几个方面来讲:

  1. SSR在搭建可视化页面中扮演的角色
  2. 如何让SSR覆盖多场景
  3. 如何在服务端渲染时何实现组件加载
  4. SSR在搭建系统中有什么不一样

系统使用的技术栈是Vue+Nodejs+Mysql,下面的讲解也是基于使用的技术栈。

##SSR在搭建可视化页面中扮演的角色

每种技术都有他的特定使用场景,在使用前一定要搞明白一个问题,那就是我们的业务到底是否需要它。SSR目前被人熟知的使用场景有两个:

  1. 页面对SEO有强烈诉求
  2. 追求极致的内容到达

那么搭建系统是否满足这两种场景呢?答案是肯定的。搭建系统主要负责营销活动和门户大厅页的搭建,这些页面对SEO和更快的内容到达时间都有诉求。所以在搭建系统中引入SSR方案,没毛病。那么SSR在整个搭建系统中扮演者什么角色呢?这有点废话的意思,当然是渲染咯,不然怎么叫服务端渲染。

前端工程师的自我修养-SSR在可视化大屏搭建中的实践

服务端渲染在搭建系统中扮演下面两种角色:

  1. 动态直出
  2. 静态直出

动态直出

动态直出就是当页面请求到达时服务端时,服务端实时获取页面所需数据和页面模板,动态的将页面渲染出来。最后返回给客户端,大致架构如下:

前端工程师的自我修养-SSR在可视化大屏搭建中的实践

这样的好处是可以获取实时数据进行渲染。缺点在高并发的场景下需要考虑负载、健壮性、降级等一系列问题,因为在这个时候流量全部打到了Node服务上。

静态直出

相比较于动态直出,静态直出就要简单许多。就是在可视化界面上搭建好页面后,将页面数据保存到数据库,在发布页面的时候,将对应的数据和页面模板通过SSR生成静态页面保存起来。当页面请求到达服务端的时候不再走服务端渲染,直接通过静态服务返回已经生成好的静态页面。这样这样的好处就是可以大大降低服务端的压力和维护成本。缺点就是页面内容相对固定,为什么说是相对固定呢?后面再讲。

上面这两种方案,在我们的搭建系统中都有用到。动态直出用在了搭建系统的编辑和预览场景下。静态直出用在了发布场景下。下面具体讲讲为什么要这么用。

编辑和预览

编辑页:

前端工程师的自我修养-SSR在可视化大屏搭建中的实践

预览页:

前端工程师的自我修养-SSR在可视化大屏搭建中的实践

首先说一下编辑和预览为什么用到了SSR方案。从实现的角度的来说,做成CSR也没毛病。但是为什么要考虑服务端渲染呢?两个原因

  1. 页面渲染效率

    通过CSR的方式,需要先获取需要编辑页面数据,然后根据数据按需异步加载用到的组件,这个过程是需要时间的,页面使用的组件越多,编辑页面渲染完成的时间就越长,体验就越差。那是不是可以考虑把所有组件都同步加载进来,进行全局注册呢?答案是肯定不行,因为组件库中的组件每天在不断的增长,一方面不利于组件的更新和扩展,另一方面全部打包进来bundle得多大呀。

  2. 可以尽可能保证编辑页面和最终发布页面的代码逻辑一致

    两者都是通过SSR渲染,所以在代码层面两者可以最大程度做到一致。

所以编辑和预览场景下选择了SSR方案去实现。那为什么要选择动态直出,而不是静态直出呢?编辑页和预览页都是临时生成的一个页面,从实现上讲,动态直出和静态直出都没问题,但是关键是"临时"二字,"临时"代表这个页面的存在时间比较短,编辑是在搭建页面才用到,预览也是临时看一下页面的整体效果,动态渲染一次性输出完事儿,无需存档。当然也不存在渲染压力,就内部使用一下能有多大压力,毛毛雨啦。

发布场景

懒得画图了,直接盗用一张公司鲁班之父的之前画的

前端工程师的自我修养-SSR在可视化大屏搭建中的实践

发布后的页面为什么是通过SSR渲染成静态页面,而不是动态渲染一把梭?一句话就是"没必要"。静态直出已经能满足我们的当前的业务需求,所以没必要去搞动态渲染。具体点来讲:

  1. 我们现目前阶段对千人千面的需求还不是那么迫切,一张静态页面完事儿。在系统的前期首要任务是解决搭建能力的有无问题。后期可能也会提供动态直出的能力。
  2. 降低了架构的复杂度,线上直接走静态服务,减轻了服务端压力。

如何让SSR覆盖多场景

一个搭建系统的开发成本非常高的,如果只服务与一种场景,那就太浪费资源了。所以搭建平台需要具备横向扩展的能力,从PC、H5、大屏再到native、小程序等等场景,搭建都可以大展前脚。

目前公司的搭建系统可以覆盖PC、H5、数据大屏,后续可能还会接入小程序,native….。那么服务渲染要如何才能支持这么多种类的页面的渲染呢?

答案就是模板,我们把每一种页面类型抽离成一个公共的模板,行成一个模板库。服务端在渲染的时动态读取页面数据,然后结合页面类型对应的页面模板,最后渲染成最终页面。

前端工程师的自我修养-SSR在可视化大屏搭建中的实践

是不是很简单"简单"。

在服务端渲染时如何实现组件注册

实际上这是一个伪标题,因为在我们的搭建系统中,组件内容并不是在服务端渲染出来的,而是在浏览器端。

前端工程师的自我修养-SSR在可视化大屏搭建中的实践

组件加载

首先,在渲染服务的同级目录一个建一个放组件的文件夹,然后将组件按照固定的目录层级和命名规范从组件库中下载到文件夹中。

前端工程师的自我修养-SSR在可视化大屏搭建中的实践

然后,在服务端渲染的时候,获取页面用到的组件列表,然后根据组件列表中的组件信息通过Nodejs的fs模块将服务器本地组件文件夹中对应的组件读取出来保存在对象中。

async loadSsrComponent(comp, options = {}) {
     //读取组件js
    const { name, version } = comp;
    const compAbsPath = path.join(ssr.component, `${name}/${version}`);
    const compName = formatCompName(name);
    const compVersionExist = await fs.exists(compAbsPath);
    const comInstancePath = path.join(compAbsPath, 'lib/index.umd.min.js');
    const component = await fs.readFile(comInstancePath, 'utf8').catch(error => {
      this.logger.error('获取组件异常', error);
    });
		//读取schema
    const schemaPath = path.join(compAbsPath, 'lib/schema.json');
    //读取组件样式文件
    const comStylePath = path.join(compAbsPath, 'lib/index.css');
    let schema = {};
    let style = '';
    const { noSchema = false } = options;
    const schemPromise = noSchema
      ? Promise.resolve()
      : fs.exists(schemaPath).then(async () => {
        // schema存在
        schema = await this.ctx.service.util.readJsonFile(schemaPath);
        schema.info = comp;
      }).catch(() => {
        schema = { errMsg: compName };
      });
    await Promise.all([
      schemPromise,
      // 样式存在
      fs.exists(comStylePath).then(async () => {
        style = await fs.readFile(comStylePath, 'utf8');
      })
    ]);

    return {
      component,
      schema,
      style,
    };
  }
           

在这个对象中包含了组件代码,样式,以及schema数据。

最后,将这些数据插入模板中,这里插入有两种方式:

  1. 将css和js分别上传到oss,然后在页面模板中插入各个资源对应的CDN地址

    这种方式用在发布后的页面中,可以减少页面体积,页面内容可以更快到达。

  2. 将css代码和js直接插入到页面模板中

    这一种用在编辑页和预览页,主要是编辑页和预览页可与接受页面体积大点。毕竟OSS存储也是要钱的,编辑页和预览页变动频繁,这两个页面都只是临时存在的,所以他们的静态文件也没有必要存在OSS上。

组件注册

在Vue项目中,要使用一个组件,需要全局或者局部注册组件。在搭建系统中是如何实现组件的注册的呢?

服务端渲染只需要将组件代码和组件样式插入到页面中,然后返回给浏览器端,浏览器在收到页面后,会解析组件的js脚本和css样式。关键点就在这里的组件js,写组件的时候可以让组件具备主动注册到全局的Vue对象中的能力

import Component from "./index.vue";
// 定义 install 方法,接收 Vue 作为参数。如果使用 use 注册插件,则所有的组件都将被注册
const install = function(Vue) {
  // 判断是否安装
  if (install.installed) return;
  Vue.component(Component.name, Component);
};
// 判断是否是直接引入文件
if (typeof window !== "undefined" && window.Vue) {
  install(window.Vue);
}
Component.install = install;
export default Component;
           

组件js被加载的时候当检测到全局对象中存在Vue对象时就将组件注册上去,这样不就完事了吗。

前端工程师的自我修养-SSR在可视化大屏搭建中的实践

组件使用

服务端渲染的时候只是将页面需要的组件列表插入到页面中,具体的组件渲染还是在浏览器端动态去渲染的。

SSR在搭建系统中有什么不一样

在搭建系统中引入SSR方案的实现过程中发现和自己的那套SSR理论知识有很多出入的地方:

  1. 在搭建系统SSR部分并没有考虑路由。搭建通常产出的是一个页面,并不是一个单页应用,所以不存在页面内路由跳转的情况。
  2. 数据预取和状态和想象的不一样

    预取:在官方教程中数据的获取是在每个页面中提供一个获取数据的方法,通过获取路由匹配到对应的页面实例,然后调用实例中的方法实现数据预取。但是搭建的页面没有路由,所以这种数据预取就行不通了。

    状态:在Vue项目中,通常需要一个全局的Store来存取全局状态,达到组件之间数据共享的目的。在搭建中,各个组件通常是相互独立,不需要进行数据共享,这个时候也就不用Vuex做全局状态管理。

    在搭建系统中,需要预取的数据主要是页面的结构数据和组件中的内容数据。页面结构数据可以在服务端获取后直接将数据插入到html模板中。组件内容数据可以不在服务端获取,组件在客户端mounted的时候再异步的去加载数据。这也就是为什么我在上面说静态直出的方式页面是内容"相对"固定,因为页面的内容最终还是客户端异步获取的。当然这样做有两个缺点:

    1. 没法做接口聚合,可能会出现重复请求
  3. 对SEO不够友好,因为页面并没有在服务端完全渲染出来,内容最终还是在客户端加载出来的。
  4. 页面并没有完全在服务端渲染,只是组件代码和组件列表数据插入到了页面中,具体的页面内容还是在浏览器端渲染完成的。当然你也可以选择在服务端渲染的时候将全部内容渲染出来,只是我们选择了这种方案,条条大路通罗马。
  5. 没有了服务端渲染降级。正常情况下一个具备SSR的web应用需要具备降级的能力。流量短时间内暴涨,服务器快吃不消的情况下,为了保证应用正常访问,我们需要牺牲一点用户体验将SSR降成CSR。但是因为我们在线上是使用的静态直出的方式,用户访问的时候其实已经是走的静态服务了,所以也就没考虑降级的措施,如果你们的应用采用的是动态直出的方式,那就必须考虑降级方案。

也算是对SSR的活学活用吧。

总结

Vue、React、Angular都有各自的SSR解决方案,说明SSR并不是为"秀"而生,可能只是缺乏使用场景。希望通过分享我们的部分使用场景,对感兴趣的你有点帮助。

前端工程师的自我修养-SSR在可视化大屏搭建中的实践

继续阅读