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

364次阅读  |  发布于2年以前

SSR最佳实践

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

网页的前世今生

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

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

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

图 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 页面结构图

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

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

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

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

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

SSR运用的最佳实践

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

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

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

这种实现方式主要是结合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可以配置一种样式, 但是底层代码是一套),使得元素多样化,在视觉上给用户体验带来很大提升.

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

以上就是展示效果, 借住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 这个库

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

目前, 集团内部正在使用 魔方 一步一步去替代这种方式, 魔方 只需要运营同学拖拖拽拽, 就能生成一个活动页, 简单高效

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

其实这种优化页面的方法并不是说只适用于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高阶组件, 元素到达可见范围内, 延迟加载组件;

看看效果图:

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

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

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

怎样利用缓存

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

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

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

SSR的不足

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

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

总结

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

参考资料

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

Copyright© 2013-2020

All Rights Reserved 京ICP备2023019179号-8