React服务端渲染SSR总结

426 阅读5分钟

一、背景介绍

⻚⾯打开过程

服务端渲染SSR是什么

1.客户端渲染CSR

现代化的前端项目,大部分都是单页应用程序,也就是我们说的 SPA ,整个应用只有一个页面,通过组件的方式,展示不同的页面内容,所有的数据通过请求服务器获取后,在进行客户端的拼装和展示;这就是目前前端框架的默认渲染逻辑,我们称为:客户端渲染方案( Client Side Render 简称: CSR );

加载渲染过程如下: HTML/CSS 代码 --> 加载 JavaScript 代码 --> 执行 JavaScript 代码 --> 渲染页面数据

SPA 应用的客户端渲染方式,最大的问题有两个方面:

1:白屏时间过长,用户体验不好;

2:HTML 中无内容,SEO 不友好;

2.服务端渲染SSR

SSR 的全称是Server Side Rendering,对应的中文名称是:服务端渲染,也就是将渲染的工作放在服务端进行。

加载渲染过程如下: HTML + CSS + JS + Data -> 渲染后的 HTML

3.客户端渲染 vs 服务端渲染

总结:服务端渲染 (SSR) 的核⼼是减少请求

为什么要用服务端渲染

1.加快首屏渲染,减少白屏时间

与传统的web项目直接获取服务端渲染好的HTML不同,单页面应用使用JavaScript在脚本客户端生成HTML来呈现内容,用户需要等待JS解析执行完成后才能看到页面,这就使得白屏加载时间变长,影响用户体验。

2.SEO 友好

对于单页面应用,当搜索引擎的爬虫爬取网站HTMl文件时,通常情况下单页面应用中没有任何的内容,仅有<div id="root"> </div>这么一句话,从而影响排名。

因此,业界借鉴传统的服务端渲染的方案,提出在服务端执行前端框架(React/Vue/Angular)代码生成HTML,然后将渲染好的HTML直接返回给客户端。

二、如何做

传统多页面实现

1.工程化打包

目录结构

在React中实现SSR主要有两种形式

1.手动搭建一个 SSR 框架(React + SSR + express构建demo)

目录结构

实现思路

服务端

  • 使⽤ react-dom/server 的 renderToString ⽅法将React 组件渲染成字符串
  • 服务端路由返回对应的模板

客户端

  • 打包出针对服务端的组件

首选写一下server端 renderToString 将组件转为字符串 renderMarkup方法 模板包装成html

if (typeof window === 'undefined') {
  global.window = {};
}


const fs = require('fs');
const path = require('path');
const express = require('express');
const { renderToString } = require('react-dom/server');
const SSR = require('../dist/search-server');
const template = fs.readFileSync(path.join(__dirname, '../dist/search.html'), 'utf-8');
const data = require('./data.json');


const server = (port) => {
  const app = express();
  app.use(express.static('dist'));
  app.get('/search', (req, res) => {
      const html = renderMarkup(renderToString(SSR));
      res.status(200).send(html);
  });

  app.listen(port, () => {
      console.log('Server is running on port:' + port);
  });
};


server(process.env.PORT || 3000);

const renderMarkup = (str) => {
  const dataStr = JSON.stringify(data);
  return template.replace('<!--HTML_PLACEHOLDER-->', str)
      .replace('<!--INITIAL_DATA_PLACEHOLDER-->', `<script>window.__initial_data=${dataStr}</script>`);
}

设置占位符

<!DOCTYPE html>
<html lang="en">
<head>
    ${ require('raw-loader!./meta.html')}
    <title>Document</title>
    <script>${ require('raw-loader!babel-loader!../../node_modules/lib-flexible/flexible.js')}</script>
</head>
<body>
    <div id="root"><!--HTML_PLACEHOLDER--></div>
    <script type="text/javascript" src="https://11.url.cn/now/lib/16.2.0/react.min.js"></script>
    <script type="text/javascript" src="https://11.url.cn/now/lib/16.2.0/react-dom.min.js"></script>
    <!--INITIAL_DATA_PLACEHOLDER-->
</body>
</html>

webpack ssr 打包存在的问题

浏览器的全局变量 (Node.js 中没有 document, window)

·组件适配:将不兼容的组件根据打包环境进⾏适配

·请求适配:将 fetch 或者 ajax 发送请求的写法改成 isomorphic-fetch 或者 axios

样式问题 (Node.js ⽆法解析 css)

·⽅案⼀:服务端打包通过 ignore-loader 忽略掉 CSS 的解析

·⽅案⼆:将 style-loader 替换成 isomorphic-style-loader

如何解决样式不显示的问题?

使⽤打包出来的浏览器端 html 为模板

设置占位符,动态插⼊组件

⾸屏数据如何处理?

服务端获取数据

替换占位符

整体react服务端渲染原理并不复杂,具体如下:

node server 接收客户端请求,得到当前的请求url 路径,然后在已有的路由表内查找到对应的组件,拿到需要请求的数据,将数据作为 props、context或者store 形式传入组件

然后基于 react 内置的服务端渲染方法 renderToString()把组件渲染为 html字符串在把最终的 html进行输出前需要将数据注入到浏览器端

浏览器开始进行渲染和节点对比,然后执行完成组件内事件绑定和一些交互,浏览器重用了服务端输出的 html 节点,整个流程结束

2.使用成熟的SSR 框架Next.JS(React + Next + Redux + Koa构建demo)

目录结构

实现流程
  1. 项目框架搭建;
  2. 项目目录重构;
  3. 完善框架-增加css、less解析
  4. 完善框架-增加UI组件antd
  5. _document.js加载公共样式
  6. 子组件覆盖Head中的title
  7. 头部和底部公共布局
  8. 项目集成Redux
  • 安装react中redux相关的包
  • 在reducer文件夹创建testReducer
  • store文件夹新建index.js
  • 改造_app.js文件利用next-redux-wrapper的withRedux初始化store
  • 改造layout.js index.js home.js文件
  • store自动保存到localStorage
  1. featch实现数据请求
  • 安装isomorphic-fetch
  • _app.js 全局导入
  • 封装fetch
打包部署

对于线上项目,如果直接通过 node app 来启动,如果报错了可能直接停止导致整个服务崩溃,一般监控 node 有几种方案。

  • supervisor: 一般用作开发环境的使用。
  • forever: 管理多个站点,一般每个站点的访问量不大的情况,不需要监控。
  • PM2: 网站的访问量比较大,需要完整的监控页面。

PM2配置文件

module.exports = {
  apps: [
    {
      name: 'mi',
      script: './server.js', // start script
      cwd: './', // current workspace
      watch: [
        // watch directorys and restart when they change
        '.next'
      ],
      ignore_watch: [
        // ignore watch
        'node_modules',
        'logs',
        'static'
      ],
      instances: 2,  // start 2 instances
      node_args: '--harmony',
      env: {
        NODE_ENV: 'production',
        PORT: 5999
      },
      out_file: './logs/out.log', // normal log
      error_file: './logs/err.log', // error log
      merge_logs: true,
      log_date_format: 'YYYY-MM-DD HH:mm Z' // date format
    }
  ]
};

启动

pm2 start pm2.config.js --env production