Thanks to visit codestin.com
Credit goes to github.com

Skip to content

后端渲染、客户端渲染(CSR)、同构应用(SSR) #60

@amandakelake

Description

@amandakelake

CSR: client-side-render
SSR: server-side-render,服务端渲染,这里理解为同构应用

一、后端渲染、客户端渲染(CSR)、同构应用(SSR)

  • 后端渲染:服务器直接生成HTML文档并返回给浏览器,但页面交互能力有限。适用于任何后端语言:PHP、Java、Python、GO等。
  • 客户端渲染:页面初始加载的HTML文档中无内容,需要下载执行JS文件,由浏览器动态生成页面,并通过JS进行页面交互事件与状态管理。
  • 同构:isomorphic/universal,基于react、vue框架,客户端渲染和服务器端渲染的结合,在服务器端执行一次,用于实现服务器端渲染(首屏直出),在客户端再执行一次,用于接管页面交互,核心解决SEO和首屏渲染慢的问题。

二、CSR和SSR的区别

ef47902a-09cb-48c4-99ad-c40f6467b708
eb5a7f2f-5044-4cf1-9f7b-76f6193b74ab

  • 1、客户端渲染的TTFP(Time To First Page)时间比较长,一般起码需要3个HTTP请求周期:
    加载HTML文档 -> 加载JS文件 -> API请求数据 -> 根据数据渲染页面
    也就是初始化页面会出现白屏,性能上通过Node直出, 将传统的三次串行http请求简化成一次http请求,降低首屏渲染时间

  • 2、单页应用的SEO能力几乎为零
    SPA首次加载的HTML文档没有内容,而目前大多数搜索引擎主要识别的内容还是 HTML,对 JavaScript 文件内容的识别都还比较弱,所以如果公司对SEO有需求(或者将来需要),那么SPA就不太适合了

CSR和SSR有几个共同点

  • 都需要下载React的
  • 都需要经历虚拟DOM构建过程
  • 都需要(给页面元素)绑定事件来增强页面的可交互性

不过对于使用SSR方式渲染出的HTML页面来说,用户是可以在这些操作(指的是下载React、构建虚拟DOM、绑定事件)完成之前就能看到页面。
再反观使用CSR方式渲染出的HTML页面,你必须等到上面的这些操作(指的是下载React、构建虚拟DOM、绑定事件)都完成,virtual-dom转换成(浏览器)页面上的真实dom之后,用户才能看到页面。
5626694f-2558-4716-9623-3ee0f3bb6ce1

SSR的缺点?

  • 1、理论上,SSR(包括传统的服务端渲染)最大的瓶颈就是服务端的性能
    如果用户规模大,SPA本身就是一个大型分布式系统,充分利用用户的设备去运行JS的运算,SSR则是把这些工作包揽到自己的服务器上。所以对于需要大量计算(图表特别多)而且用户量巨大的页面,并不太适合,但SSR非常适合于大部分的内容展示页面

  • 2、项目复杂度增加,需要前端团队有较高的技术素养
    为了同构要处处兼容 Node.js 不同的执行环境,不能有浏览器相关的原生代码在服务端执行,前端代码使用的 window 在 node 环境是不存在的,所以要 mock window,其中最重要的是 cookie,userAgent,location

三、SSR是如何实现的:Virtual DOM

SSR 的工程中,React 代码会在客户端和服务器端各执行一次

JS代码同时可以在浏览器和Node服务器上执行,但如果react项目里有直接操作DOM的代码,那就无法在Node环境下执行了,因为Node环境中没有DOM的概念

React和Vue等MVVM框架中都引入了虚拟DOM(Virtual DOM)的概念,本质上是真实DOM的JS对象映射,前端er操作的是普通的JS对象,并不是直接操作DOM,所以在SSR中

  • Node服务器环境:Virtual DOM -> 字符串
  • 浏览器环境:Virtual DOM -> 直接操作真实DOM
    d4d49485-e3d8-4a79-8f81-ef71e51e231d

四、SSR的难点

1、路由代码的差异

服务器端需要通过请求路径,找到路由组件,而在客户端需通过浏览器中的网址,找到路由组件,是完全不同的两套机制,所以这部分代码无法公用

// 客户端路由
const App = () => {
  return (
    <Provider store={store}>
		// 自动从浏览器地址中,匹配对应的路由组件
      <BrowserRouter>
        <div>
          <Route path='/' component={Home}>
          </div>
      </BrowserRouter>
    </Provider>
  )
}
// ReactDom.render直接把组件映射成真实DOM
ReactDom.render(<App/>, document.querySelector('#root'))
// 服务端路由
const App = () => {
  return 
    <Provider store={store}>
		// 根据传入的路由规则去匹配对应的组件
      <StaticRouter location={req.path} context={context}>
        <div>
          <Route path='/' component={Home}>
        </div>
      </StaticRouter>
    </Provider>
}

// ReactDom.renderToString把组件转化成字符串
Return ReactDom.renderToString(<App/>)

2、打包代码的差异

虽然客户端和服务端共用组件代码,但由于入口路由代码不一致,所以客户端和服务端的入口代码是不一样的,所以打包的时候,webpack的打包机制有不同

3、异步数据的获取以及状态管理的不同

服务端渲染 + SPA 共存的模式看起来非常棒,但也是有缺点的:
第一,要同时保证两种模式共存的情况和两种模式独立的情况下都能够表现一致,开发和测试比较繁琐;
第二,多页应用(不用 Router 或刷新页面)的情况下,每一页都拥有完全独立的 window, document,但是共存模式下用 Router 切换到 SPA 后,由于单页面应用的特点,他们将会共用 window 和没有变化的 DOM,这个需要在开发每个页面每一个模块的时候特别注意对一些数据的销毁和事件的解绑,避免页面被切换掉之后旧逻辑依然生效,这个可能会引发很多意想不到的问题,而且难以排查。

如果是使用redux,要注意store不能是单例模式,因为store是所有页面需要公用的,每个用户访问的时候,生成store的函数需要重新执行,为每个用户提供一个独立的 Store

要避免出现这些问题,就需要很好的、统一的开发规范和踩坑意识,如果项目比较大比较复杂,开发成本就会比采用单个模式大得多。

4、生命周期的调用时机

React 组件的生命周期函数有的会在浏览器调用,有的会在服务端调用,有的则两端都会调用,例如在 componentWillMount 和 render 函数中不小心用了 window 或 document,在服务端渲染的时候就会报错,这些问题在两种环境公用代码的情况下是不可避免的,特别是有些不报错的代码,开发时察觉不到,却很有可能导致内存泄漏。

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions