4

能让你纵享丝滑的SSR技术,转转这样实践

 3 years ago
source link: https://zhuanlan.zhihu.com/p/367081228
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

能让你纵享丝滑的SSR技术,转转这样实践

关注微信公众号:大转转FE。 一个有趣的前端团队~
v2-2f6bad790ee186625d3a4cdc90122aec_720w.jpg

SSR最佳实践

秒开率对于用户的留存率有直接的影响,数据表明, 网页加载时间过长会直接导致用户流失.转转集团作为一家电商公司, 对于H5页面的秒开率有着更加严格的需求, 在主要的卖场侧页面(手机频道页、3c频道页、活动页)等重要流量入口我们都采用了SSR(服务端渲染)技术来构建页面,今天就带大家了解一下我们摸索出来的一些最佳实践.

网页的前世今生

在早期的web应用中,实际上我们都是用的服务端渲染技术, 像jsp、asp、php等各种后台模板生成的页面,前端都是拿到整张页面,不用自己去拼接DOM.后来随着前后端分离开发模式,衍生出了最主要的两种渲染方式CSR以及SSR.

  • CSR : 客户端渲染,整个渲染流程是: 浏览器请求url --> 服务器返回index.html(空body、白屏) --> 再次请求bundle.js、路由分析 --> 浏览器渲染 bundle.js体积越大, 就会导致白屏的时间越长,给用户的体验就越差(当然,这可以借助打包构建工具来优化这部分)
    交互流程图如下
v2-ff3f99935860ece3d1be34a9b748df9e_720w.jpg

图1-1客户端渲染流程图

  • SSR : 服务端渲染, 服务端渲染分为两个步骤:
  • 阶段一: 浏览器请求url --> 服务器路由分析、执行渲染 --> 服务器返回index.html(实时渲染的内容,字符串) --> 浏览器渲染 (此时是一个静态页面, 不可交互, 依赖于服务端的能力, 这就是快的原因)
  • 阶段二:浏览器请求bundle.js --> 服务器返回bundle.js --> 浏览器路由分析、生成虚拟DOM --> 比较DOM变化、绑定事件 --> 二次渲染 (用户可交互)

我们来看看整个交互流程图 :

v2-297db7b36ce19c0e8f56ffb86b529424_720w.jpg

图 1-2 服务端渲染交互流程图

SSR构建逻辑

理解了两种渲染模式的异同,我们来看看SSR整个构建逻辑(主要以Vue-SSR为例)

我们以官网的图片为例:

图 2-1 SSR构建逻辑

从图中我们可以知道 :

在整个构建过程中, 我们有两个入口, 一个是server-entry.js, 执行server端的逻辑, 一个是client.js, 执行client端的逻辑, 然后通过会将webpack打包分成两个Bundle: 服务端bundle; 客户端bundle. Node.js会处理服务端bundle用于SSR, 客户端bundle会在用户请求时和已经由SSR渲染出的页面一起返回给用户, 然后在浏览器执行”注水”(hydrate), 接管Vue接下来的业务逻辑.

理解了整个构建逻辑,接下来我们来看看我们是怎么运用SSR来服务我们的项目的.

SSR构建项目的背景

卖场侧业务首页组成大同小异: 主要分首屏和第二屏, 首屏有多个模块组成, 第二屏是商品Feed流,便于读者理解, 我们抽象出了页面结构图:

图 2-2 页面结构图

(欢迎大家下载转转app体验)

而且这些页面都有一个共同的特点:

  • 只用于展示, 和用户的状态不强绑定(不需要用户登录)
  • 页面状态稳定,内容不会经常变更
  • 都是重要流量的第一个入口(首页)

由于对于秒开率有着极高的要求,又承载了主要流量入口,结合以上页面特点,所以我们使用了SSR来提升用户体验.

经过一系列的探索和探究, 我们最终使用Nuxt.js来作为我们的技术选型.

这里提下为啥使用Nuxt.js作为我们的技术选型, 主要原因有以下几点:

  1. 集团内部C端业务是以Vue技术栈为主,B端技术栈是以React为主, 所以不考虑React服务端渲染技术栈;
  2. Nuxt.js是开箱即用的服务端渲染框架,不用开发人员自己去搭建Vue+ Vue-server-renderer + vuex来集成服务端渲染框架, 接入成本比较低.

SSR运用的最佳实践

目前我们使用SSR实现的主要能力有:

  • 首屏使用服务端渲染,第二屏使用客户端渲染,首屏模块数据可调整,即优化了性能, 又丰富了页面配置.
  • 合理化使用缓存, 进一步提升用户体验.
  • 实现css注入,达到按需换肤的效果.
  • 使用ErrorBoundary拦截错误,使得组件错误不会影响整个页面白屏.
  • 按需加载第二屏数据,只有滑动到可见范围, 才加载第二屏数据
  • 针对大促场景, 结合服务端能力, 以及各种监控,docker扩容,保证页面稳定.

接下来就和大家探索其中几种能力的主要思路:

怎样实现首屏使用服务端渲染,第二屏使用客户端渲染

这种实现方式主要是结合asyncData在服务端异步获取数据,使用vue动态组件component的特性,来调整模块的渲染顺序; mounted生命钩子只会在客户端执行, 使用仅在客户端渲染组件的特性来实现的.

示例代码:

<template>
  <!--服务端渲染,动态获取首屏模块并且加载对应模块的数据, 使用error-boundary来拦截错误-->
  <template v-for="(e, i) in structureOrder">
    <error-boundary>
      <component :info="activityState.structure[e]"
                 :is="Mutations.name2Component(e)"
                 class="anchor"
                 :id="e"
                 :key="i" :name="e" v-if="activityState.structure[e] || e === 'bar' "/>
    </error-boundary>
  </template>
  <!--客户端渲染-->
  <client-only>
    <!--滑动到可见范围加载对应的数据-->
    <div :is="listComponent" :tab="labelFilter"/>
  </client-only>
</template>

获取数据:

//服务端渲染数据
async asyncData({app, route, req}) {
  const initData = await app.$axios.$get(host, {
    params: {
      name: key, from, smark, keys: `structure,base,labelFilter,navigate,redPack,${elements}`
    }, headers
  })
  const {structureInfo, structureOrder, restStructure, anchors} = Mutations.initStructure(initData)
  return {
    structureInfo,
    restStructure,
    structureOrder, //动态返回对应模块的名称
    useVideo: Mutations.checkUseVideo(req),
    theme,
    pageFrom: route.query.from,
    isPOP,
    anchors,
    ...formInfo
  }
},
async mounted() {
    //获取客户端渲染的数据
   const res = await this.initData()
},

怎样使用ErrorBoundary捕获组件级别错误,避免整个页面白屏

关于ErrorBoundary这个捕获错误的组件,这个组件的主要功能是使得组件级的错误不会蔓延到页面级,不会造成整个页面的白屏,考虑到服务端渲染可能会发生偶发性错误,状态容易变的不可控, 所以使用这个能力还是很有必要的, 这个组件主要使用vue提供的 errorCaptured 来捕获组件级的错误, 想详细了解这个api的作用可以去看官方文档,具体的实现如下:

const errorBoundary = Vue => {
  Vue.component('ErrorBoundary', {
    data: () => ({ error: null }),
    errorCaptured(err, vm, info) {
      this.error = `${err.stack}\n\nfound in ${info} of component`
      SentryCapture(err, 1) //异常上报到sentry
      return false
    },
    render() {
      return (this.$slots.default || [null])[0] || null
    }
  })
}

// 全局注册errorBoundary
Vue.use(errorBoundary)

怎样实现css注入,实现页面换肤

这个功能的主要作用是 : 可以根据配置json文件定制化活动页面的样式, 做到"千人千面" (一个会场的key可以配置一种样式, 但是底层代码是一套),使得元素多样化,在视觉上给用户体验带来很大提升.

我们先来看看效果示意图:

v2-2b61851b7588f362eb0d843832c74271_b.jpg

以上就是展示效果, 借住CSS注入, 我们可以根据不同的json文件来定制化页面的样式, 只需要维护一套代码, 简单高效.

实现逻辑也很简单,主要是运用了Nuxt.js框架提供的head方法:

head() {
            //this.baseInfo.additionStyle是从json文件拿到的样式
      //通过css权重, 可以实现样式覆盖
      return {
        style: [
          {cssText: this.baseInfo?.additionStyle || '', type: 'text/css'}
        ],
        __dangerouslyDisableSanitizers: ['style']  // 防止对一些选择器的特殊字符进行转义
      }
    },

不仅如此, 还可以实现js注入, 感兴趣的小伙伴可以自己去了解,底层原理可以了解下 vue-meta 这个库

但是, 随着业务的不断迭代, 这种注入方式还是存在很多可优化的点:

  • 每个活动页运营同学都要维护一份json文件,里面包含冗长的css配置字段, 活动规则等等, 特别是css配置字段,就是一段冗长的css, 给运营和开发同学带来很大的不便;
  • 运营同学维护成本高,学习成本高,操作成本高;
  • 对于UI同学成本也大, 每次都需要UI同学来设计活动页面样式;

目前, 集团内部正在使用 魔方 一步一步去替代这种方式, 魔方 只需要运营同学拖拖拽拽, 就能生成一个活动页, 简单高效, 想要了解魔方的同学, 可以继续关注我们的公众号

怎样实现组件滑动到可见范围,才加载数据

其实这种优化页面的方法并不是说只适用于SSR, 其他非SSR页面也可以使用这种方式来优化;

看看我们的实现方式 :

function asyncComponent({componentFactory, loading = 'div', loadingData = 'loading', errorComponent, rootMargin = '0px',retry= 2}) {
  let resolveComponent;
  return () => ({
    component: new Promise(resolve => resolveComponent = resolve),
    loading: {
      mounted() {
        const observer = new IntersectionObserver(([entries]) => {
          if (!entries.isIntersecting) return;
          observer.unobserve(this.$el);

          let p = Promise.reject();
          for (let i = 0; i < retry; i++) {
            p = p.catch(componentFactory);
          }
          p.then(resolveComponent).catch(e => console.error(e));
        }, {
          root: null,
          rootMargin,
          threshold: [0]
        });
        observer.observe(this.$el);
      },
      render(h) {
        return h(loading, loadingData);
      },
    },
    error: errorComponent,
    delay: 200
  });
}

export default {
  install: (Vue, option) => {
    Vue.prototype.$loadComponent = componentFactory => {
      return asyncComponent(Object.assign(option, {
        componentFactory
      }))
    }
  }
}

实现原理主要是使用vue高阶组件, 元素到达可见范围内, 延迟加载组件;

看看效果图:

v2-be2d49ea146d0f97fcaf86b7e68a06b2_b.jpg

我们可以看到,只有到底部商品Feed流出现在可视范围,才去请求对应的接口

针对大促场景怎样保证页面稳定

所谓大促场景,是指像 6.18, 双11,这种场景下, 面对大流量, 如何保证页面稳定? SSR是CPU密集型任务, 意味着很耗费服务器资源,集团目前主要采取的策略是:

  • 对接口进行压测, 模拟高并发场景下页面性能, 接口响应速度;
  • 集团内部实现了一套监控系统, 可以实时监控CPU,内存的消耗情况;
  • 需要有服务器扩容方案, 比如接入docker, 可以实现服务器实时扩容

怎样利用缓存

请大家移步集团一位前端大佬写的公众号文章: Nuxt实现的SSR页面性能优化的进一步探索与实践

最终,看看我们最终的实现效果:

可以看到, 首屏渲染时间在594ms, 秒开率在百分之87左右;

SSR的不足

ssr的使用过程并不是一帆风顺的, 在使用的过程中, 也总结几点不足之处:

  • 对于开发人员的要求更高, 要学习其他的额外知识,例如: Linux , node相关知识, 需要具备一定的后端思维;
  • 服务端渲染接口抓包不方便, 我们在客户端抓取不到服务端的接口请求, 不过对于使用Mac电脑开发的同学,可以使用 proxychains-ng 来抓取服务端请求的接口
  • 冗长的配置环境过程, 每次开发联调需要配置后端host
  • 对于服务器资源有要求,并发量越大, 资源消耗的越多
  • 服务端渲染可能会发生偶发性错误, 需要有一套降级方案

至于如何取舍, 看各位同学的项目需求,以及运用场景;

总结

SSR的使用有利有弊, 我们应该结合自己的业务特性去制定合适的方案, 它的优点就是快, 有利于SEO, 缺点也很明显, 比较耗费服务器资源, 对于亿级流量的超巨app来说, 理论上是不太合适的, 集团内部也有自己的一套方案来优化客户端渲染, 使得用户体验尽量向SSR靠齐.每一种技术的运用只有实践了才知道利弊,才能产生碰撞. 本文只是简单的带大家了解一条业务线上对SSR的运用, 所阐述的方面也只是冰山一角, 希望给广大开发者带来一定的启发, 前人栽树, 后人乘凉, 感谢转转FE前辈们留下的宝贵财富.

最后

最后,感谢您的阅读,有任何问题,欢迎评论区留言讨论,互相学习~ 本月我们将推出更多实践相关文章及有奖留言活动,也欢迎扫描下方二维码关注我们的微信公众号及时获取 ↓

v2-18f5a5b5edde344633c8c6344e8ced17_720w.jpg

nuxt官网: https://www.baidu.com/link?url=xy0d8KPUgTmiVoGge6g-FgdeqjJSTjxdpT0tpxZzBG_&wd=&eqid=db50cacf00052e5e000000066081587d

Vue SSR指南: https://ssr.vuejs.org/zh/guide/


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK