浏览器 → 浅析SSR架构相关

401 阅读16分钟

前置知识

前端渲染模式

CSR(Client-side Rendering)

前端几乎都是由客户端动态渲染(客户端执行JS代码、动态创建DOM结构)出来的,其中包括数据请求、视图模版、路由在内的所有逻辑都在客户端处理

  • 性能指标
    • FP(First Paint):首次绘制,第一次有像素对用户可见的时间
    • FCP(First Contentful Paint):首次内容绘制:用户请求的内容在屏幕上可见的时间点
    • TTFB(Time to First Byte):从点击链接到接收第一个字节内容之间的时间
    • TTI(Time To Interactive):可交互时间:页面可交互的时间点(如事件绑定等)
  • 主要缺陷在于随着应用程序的更新迭代,客户端需要执行的JS代码越来越多,前置的第三方类库/框架、polyfill等都会在一定程度上拖慢首屏的性能,尤其在中低端设备上尤为明显,此时就需要改变渲染模式来解决了
SSR(Server-Side Rendering)

页面逻辑(包括即时数据请求)和模版渲染工作都放在服务端完成,减少了客户端的JS代码量,流式文档解析等浏览器优化机制也可发挥其作用,在低端设备和弱网的情况下表现更好,但由于工作都累积到服务端了,因此会导致页面内容相应时间(TTFB:Time to First Byte)变慢

  • 解决TTFB变慢的方法
    • 流式SSR
      • 流式服务器渲染可以以chunk形式发送HTML,浏览器可以在接收时进行逐块渲染,这促成了快速的FP和FCP,因为HTML标签更快的到达了用户侧
      • 在React中,流在renderToNodeStream()中异步处理,相比于同步的renderToString,服务器压力相对小很多
    • 组件级缓存
    • 模板化
    • HTML缓存
    • 更换渲染模式
静态渲染(Static Rendering)

将生成HTML页面的工作放到编译时(此时页面已经是可交互的了),而不用在请求到来时动态完成,为每个URL预先单独生成HTML文件,并进一步借助CDN加速访问

  • 存在的问题
    • 需要为每个URL单独生成一份HTML文件,对于URL无法预知和存在大量不同页面的网站,静态渲染就显得相对吃力了
    • 只适用于偏静态的内容:对于动态的、个性化的内容作用不大
预渲染(Prerendering)

主要区别在于,静态渲染得到的页面已经是可交互的,无需在客户端额外执行大量的JS代码,而预渲染必须经过客户端渲染才可以真正的可交互,即在禁用JS后,静态渲染的页面几乎不受影响,而预渲染的页面将只剩下超链接之类的基本功能

Rehydration

Rehydration模式将CSR和SSR结合起来,服务端渲染出基本内容后,在客户端进行二次渲染;
在客户端上启动JS视图,复用服务器渲染的HTML DOM数和数据,利用服务端返回HTML中的JS数据,重新渲染页面的技术

拓展区块

  • TS
    • 泛型:
      • 泛型可以在保证类型安全的前提下,让函数与多种类型一起工作,从而实现复用,常用于:函数、自定义类型、接口等类型中
      • 泛型也可以用来实现可复用组件功能的重要工具之一
      • 语法:
        • 在函数名称前后添加尖括号<>,尖括号中添加类型变量,常用的如T等,只是一个变量,具体叫啥自己定
        • 类型变量指的是一种类型而不是值,相当于是一个类型容器,可以捕获用户提供的类型,具体类型在用户调用该函数时进行指定
        • 可以在函数参数和返回值处使用该类型变量,表示参数和返回值都具有相同的类型

SSR开发浅析

  • 基本流程

react-dom/server提供了renderToString函数,可以从虚拟Dom结构生成HTML字符串(把react组件转化为普通的HTML字符串),后端通过拼接字符串生成完整的HTML代码,然后返回给前端;
也可以通过react-dom/server提供的ReactDOMServerAPI将 前端通过react-dom提供的hydrate方法,在前端进行同构,实现事件方法的绑定等功能

  • renderToString是同步和单线程的,导致会相对较慢渲染
  • renderToString与renderToStaticMarkup浅析
    • renderToStringrenderToStaticMarkup的主要作用都是将react Component转换为HTML字符串,区别在于renderToString生成的HTML中会带有额外的属性,如data-reactroot="",而renderToStaticMarkup生成的HTML中的Dom没有额外的属性,可以节省HTML字符串的大小
    • renderToString生成的HTML中的Dom上会有对应的属性,在客户端进行渲染react组件时,会根据Dom的属性是否相同从而判断是否重新渲染组件
    • 当页面是纯粹的静态页面时,最好使用renderToStaticMarkup,否则最好使用renderToString
  • 核心重点
    • 项目开发:官网的SSR开发与B端应用的CSR(客户端渲染)不同,原理上更多的数据请求等逻辑需要放在服务端完成,来保证SEO捕获到完整的站点相关信息,以达到更精确的引擎匹配和站点排行
    • 用户体验:官网对用户体验和性能上要求较高,官网的开发需要尽可能优化首屏等指标,以保证用户在不同环境下拥有很好的用户体验
    • 运营维护:官网需要有不断的更新维护,以达到数据上的指标达成,因此需要保证数据的可灵活配置性,哪怕单独搭建一个后台系统来支持相关的运营配置
    • 部署流程:需要考虑到相关的备案、相关集群的选择、用户访问的平衡性、SEO检索和排行等,可以增加足够的曝光和流量以达到上述目标
  • B端于C端
    • C端Web应用更加要求易传播性和交互稳定性,相比于客户端渲染,服务端渲染可以有效的提高搜索引擎爬取的精度,进而提高网站的易传播性
    • SSR会在服务端完成对页面数据的请求,将对应数据注入DOM一同返回,最后得到一个完整可预览的HTML
    • 交互稳定性:SSR更加的高效
      • SSR服务端渲染不需要在客户端进行数据请求,拥有了更短的首屏时间,而客户端渲染则需要在请求到数据后才可以进行数据展示
  • lint与构建在开发中的重要性
    • 一个好的项目会有很完善的lint机制和IDE提示来规范项目中的代码规范,从而降低多人维护对项目易迭代性的折损,相对于JS这种弱类型的语言,这个过程可以有效的避免「编译时不报错,运行时因为隐式类型转换等导致的预期不符的问题」
    • 构建是每个项目中必不可少的步骤,通常项目中会用到ES6及以上或是TS来提升代码的可维护性和开发效率,但是由于不同的运行设备对这种语法的支持度不同,因此就需要通过构建来保证生产环境的项目可以在不同浏览器下进行运行
  • 浏览器应用渲染页面的过程
    • 模版页面的渲染:先编写页面模版模块 → 相关数据请求 → 页面统一渲染
    • 路由匹配:通过路由系统控制不同指定模板进行展示
    • header标签修改
  • 同构相关
    • 同构就是前后端采用同一套js代码,采用不同的构建方式,如同一端js代码既可以运行在浏览器端,又可以运行在NOde端
    • 当客户端渲染和服务端渲染内容一样的时候,推荐使用hydrate代替render,当然不替换也是可以滴,就是会有警告
    • 事件注册
      • 因为HTML代码是从服务端获取的,而事件绑定是在Dom元素上的,服务端没有类似于客户端的click等事件,renderToString只可以处理HTML,不能处理事件,因此Dom的相关事件在SSR中的不原生支持的
  • 在NodeJS中运行TS文件的方法
    • 用ts-node去执行TS文件 →逻辑参考
    • 用webpack打包后,然后去执行打包后的JS文件
    • 配置后需要安装具体的依赖yarn add @babel/preset-env babel-loader ts-loader webpack webpack-merge webpack-cli @types/express --save-dev
      • @types/express当在TS文件中引用的express时,express是没有内置的.d.ts的类型定义的,需要安装对应的类型定义依赖
    • 具体运行步骤是
      • 先执行webpack.server.js文件,即将TS文件编译打包成JS文件
      • 在执行打包好的JS文件进行服务启动等事项,可以才有nodemon进行启动
    • 具体实现
    // webpack.base.js 通用webpack配置
    const path = require("path");
    
    module.exports = {
      module: {
        rules: [
          {
            test: /.js$/,
            loader: "babel-loader", // 构建JS代码
            exclude: /node_modules/,
            options: {
              presets: ["@babel/preset-env"],
            },
          },
          {
            test: /.(ts|tsx)?$/,
            use: "ts-loader", // 构建TS代码
            exclude: /node_modules/,
          },
        ],
      },
      resolve: {
        extensions: [".tsx", ".ts", ".js"],
        alias: {
          "@": path.resolve(process.cwd(), "./src"),
        },
      },
    };
    
    // webpack.server.js 服务端webpack配置
    const path = require("path");
    const { merge } = require("webpack-merge");
    const baseConfig = require("./webpack.base");
    
    module.exports = merge(baseConfig, {
      mode: "development",
      entry: "./src/server/index.tsx",
      target: "node",
      output: {
        filename: "bundle.js",
        path: path.resolve(process.cwd(), "server_build"),
      },
    });
    
    // tsconfig.json 配置用于编译TS代码的配置文件
    {
      "compilerOptions": {
        "module": "CommonJS",
        "types": ["node"], // 声明类型,使得ts-node支持对tsx的编译
        "jsx": "react-jsx", // 全局导入, 不再需要每个文件定义react
        "target": "es6",
        "lib": ["dom", "dom.iterable", "esnext"],
        "allowJs": true,
        "skipLibCheck": true,
        "esModuleInterop": true,
        "allowSyntheticDefaultImports": true,
        "strict": true,
        "forceConsistentCasingInFileNames": true,
        "moduleResolution": "node",
        "resolveJsonModule": true,
        "isolatedModules": true,
        "baseUrl": "./",
        "paths": {
          "@/*": ["./src/*"]
        }
      },
      "include": ["src/**/*"]
    }
    

深入分析官网开发的相关技术

服务端渲染 → 静态页面相关

  • 基本步骤
    • 基础react/vue组件定义
    • 将模板转换成HTML标签传递给服务端
      • 通过react-dom中的renderToString方法将模板元素转换成HTML字符串返回
      • const content = renderToString(<Home />)
      • 底层和客户端模板编译一致,都是根据AST来转换成真实的DOM的过程
    • 客户端通过请求拿到在服务端转换成HTML的字符串后进行渲染
      • 此时客户端只是将HTML字符串渲染到界面上了,但是是不具备任何交互效果的,原因是renderToString方法只是渲染页面,事件的相关绑定是无法在服务端进行的
    • 事件绑定的解决方案 → 同构
      • 同构:是服务端渲染的核心概念,不仅在事件绑定页面渲染上,后续的路由、数据请求都涉及到同构的概念
      • 同构指同一套React代码在服务端渲染一遍,然后在客户端再执行一遍,服务端负责静态DOM的拼接,客户端负责事件的绑定
      • 事件绑定最终方案
        • 利用reactDom.hydrateRoot方法了,这个API在已经提供了服务端静态渲染节点的情况下使用,只会对模板中的事件进行处理
        • 例如:
          // src/client/index.tsx
          import { hydrateRoot } from "react-dom/client";
          import Home from "@/pages/Home";
          hydrateRoot(document.getElementById("root") as Document | Element, <Home />);
          // 服务端在将HTML字符串拼接整理后返回给前端,此时也需要给定容器的ID供`hydrateRoot`方法初始化绑定事件
          
      • 根据上述方案进行webpack配置
      // webpack.base.js
      const path = require("path");
      const { merge } = require("webpack-merge");
      const baseConfig = require("./webpack.base");
      
      module.exports = merge(baseConfig, {
        mode: "development",
        entry: "./src/client/index.tsx",
        output: {
          filename: "index.js",
          path: path.resolve(process.cwd(), "client_build"),
        },
      });
      
      
      // webpack.client.js
      const path = require("path");
      const { merge } = require("webpack-merge");
      const baseConfig = require("./webpack.base");
      
      module.exports = merge(baseConfig, {
        mode: "development",
        entry: "./src/client/index.tsx",
        output: {
          filename: "index.js",
          path: path.resolve(process.cwd(), "client_build"),
        },
      });
      
      
      • 服务端相关逻辑
      // const express = require("express");
      // const childProcess = require("child_process");
      import express from 'express'
      import childProcess from 'child_process';
      import { renderToString } from 'react-dom/server'
      // import Home from '@/pages/Home'
      import router from '@/router';
      import { Route, Routes } from 'react-router-dom';
      import { StaticRouter } from 'react-router-dom/server'
      import path from 'path'
      import { Helmet } from 'react-helmet';
      
      // StaticRouter是无状态的路由,服务端是不同于客户端的,在客户端中,浏览器历史记录会改变状态,同时将屏幕更新,但是服务端是不能改动到应用庄涛的,因此采用StaticRouter无状态路由
      const app = express();
      // const content = renderToString(<Home />)
      app.use(express.static(path.resolve(process.cwd(), "client_build")));
      app.get("*", (req, res) => {
        const content = renderToString(
          <StaticRouter location={req.path}>
            <Routes>
              {
                router?.map((item,index) => {
                  return <Route {...item} key={index}></Route>
                })
              }
            </Routes>
          </StaticRouter>
        )
        const helmet = Helmet.renderStatic();
        res.send(`
          <html
            <head>
              ${helmet.title.toString()}
              ${helmet.meta.toString()}
            </head>
            <body>
              <div id="root">${content}</div>
              <script src="/index.js"></script>
            </body>
          </html>
          `);
      });
      
      app.listen(3000, () => {
        console.log("ssr server listen on http://127.0.0.1:3000");
      });
      
      childProcess.exec("start http://127.0.0.1:3000");
      
      • 路由配置
        • <></><Fragment></Fragment>标签是一样的,都是一个聚合子元素的标签,不增加真实的DOM节点
        • 通过同构在客户端和服务端的入口都加上对应的路由配置实现多页面组件配置
        • 依赖于react-router-dom,其路由系统分为客户端路由和服务端路由
          • 客户端路由:通过react内置的路由跳转,利用的是BrowserRouter
          • 服务端路由:通过新开页进行匹配,利用的是StaticRouter,StaticRouter是无状态路由,因为服务端不同于客户端,客户端中会有其他方式改变路由的状态,同时使得界面更新,而服务端是无法改变应用状态的,因此在服务端中采用无状态路由进行实现
          // src/router.tsx
          import Home from "@/pages/Home"
          import Demo from "./pages/demo"
          
          interface IRouter {
              path: string;
              element: JSX.Element
          }
          
          const router:Array<IRouter> = [
              {
                  path: "/",
                  element: <Home />
              },
              {
                  path: "/demo",
                  element: <Demo />
              }
          ]
          
          export default router
          
          
          // src/server/index.tsx
          
          import router from '@/router';
          import { Route, Routes } from 'react-router-dom';
          import { StaticRouter } from 'react-router-dom/server'
          // ......
          const content = renderToString(
              <StaticRouter location={req.path}>
                <Routes>
                  {
                    router?.map((item,index) => {
                      return <Route {...item} key={index}></Route>
                    })
                  }
                </Routes>
              </StaticRouter>
            )
          // .....
          
          // src/client/index.tsx
          import { hydrateRoot } from "react-dom/client";
          import { BrowserRouter, Route, Router, Routes } from "react-router-dom";
          import router from "@/router";
          
          const Client = (): JSX.Element => {
            return (
              <BrowserRouter>
                <Routes>
                  {router?.map((item, index) => {
                    return <Route {...item} key={index} />;
                  })}
                </Routes>
              </BrowserRouter>
            );
          };
          
          hydrateRoot(document.querySelector("#root") as Document | Element, <Client />);
          
          // import Home from '@/pages/Home'
          
          // hydrateRoot(document.getElementById("root") as Document | Element, <Home />);
          
          
        • 路由的另一种实现方式
          • 基础路由配置
          // src/routes.js
          import React from "react";
          import { Route } from "react-router-dom";
          import Home from "./containers/Home";
          import News from "./containers/News";
          
          export default (
            <>
              <Route path="/" exact component={Home} />
              <Route path="/news" component={News} />
            </>
          );
          
          • 客户端配置
            • 客户端比较简单,就是将路由组件作为子组件传递即可,与基础路由配置一致,可以实现复用,
          import React from "react";
          import { hydrate } from "react-dom";
          import { BrowserRouter } from "react-router-dom";
          import routes from "../routes";
          
          hydrate(<BrowserRouter>{routes}</BrowserRouter>, window.root);
          
          • 服务端配置
            • 服务端路由还是使用无状态路由,即静态路由:staticRouter
            • staticRouter需要传递两个参数,一个是contextlocation
              • context主要是用来给组件传递数据信息,这个属性可以在客户端和服务端互相传递参数,如CSS样式的参数
              • location主要是用来接收请求的路径信息,如pathnamesearchhash
          // src/server.js
          import routes from "../routes";
          
          app.get("*", (req, res) => {
            let context = {};
            let domContent = renderToString(
              <StaticRouter context={context} location={req.path}>
                {routes}
              </StaticRouter>
            );
            // html 的内容不改变
            res.send(html);
          });
          
      • header配置更改
        • 有些需求是需要更改header配置进行不同页面的多媒体适配、利于SEO等目的
        • 可以利用react-helmet进行实现,同样也是利用到了同构技术
        • 客户端同构
        // src/pages/Home/index.tsx
        import { useNavigate } from "react-router-dom";
        import { Helmet } from "react-helmet";
        import { Fragment } from "react";
        const Home = () => {
          const navigate = useNavigate();
        
          return (
            <Fragment>
              <Helmet>
                <title>简易的服务端渲染 - Home</title>
                <meta name="服务端渲染 Home " content="服务端渲染"></meta>
              </Helmet>
              <div>
                <h1>hello-ssr</h1>
                <button
                  onClick={(): void => {
                    alert("hello-ssr");
                  }}
                >
                  alert
                </button>
                <a href="http://127.0.0.1:3000/demo">a标签路由跳转</a>
                <span
                  onClick={(): void => {
                    navigate("/demo");
                  }}
                >
                  react路由跳转
                </span>
              </div>
            </Fragment>
          );
        };
        
        export default Home;
        
        // src/server/index.tsx
        
        
        // const express = require("express");
        // const childProcess = require("child_process");
        import express from 'express'
        import childProcess from 'child_process';
        import { renderToString } from 'react-dom/server'
        // import Home from '@/pages/Home'
        import router from '@/router';
        import { Route, Routes } from 'react-router-dom';
        import { StaticRouter } from 'react-router-dom/server'
        import path from 'path'
        import { Helmet } from 'react-helmet';
        
        // StaticRouter是无状态的路由,服务端是不同于客户端的,在客户端中,浏览器历史记录会改变状态,同时将屏幕更新,但是服务端是不能改动到应用庄涛的,因此采用StaticRouter无状态路由
        const app = express();
        // const content = renderToString(<Home />)
        app.use(express.static(path.resolve(process.cwd(), "client_build")));
        app.get("*", (req, res) => {
          const content = renderToString(
            <StaticRouter location={req.path}>
              <Routes>
                {
                  router?.map((item,index) => {
                    return <Route {...item} key={index}></Route>
                  })
                }
              </Routes>
            </StaticRouter>
          )
          const helmet = Helmet.renderStatic();
          res.send(`
            <html
              <head>
                ${helmet.title.toString()}
                ${helmet.meta.toString()}
              </head>
              <body>
                <div id="root">${content}</div>
                <script src="/index.js"></script>
              </body>
            </html>
            `);
        });
        
        app.listen(3000, () => {
          console.log("ssr server listen on http://127.0.0.1:3000");
        });
        
        childProcess.exec("start http://127.0.0.1:3000");
        
        
      • 在SSR中支持对接口数据的请求和注入

        通过建立一个全局的store,将对应的初始化方法透传给服务端进行统一请求,最后再通过脱水和注水的操作,使得客户端初始化可以和服务端保持相同的store,从而实现SSR支持接口数据的请求和注入

        • body-parser库的应用
          • axios无法直接读取请求的body,因此需要body-parser对请求进行解析
          // src/server/index.tsx
          // ...
          const bodyParser = require("body-parser");
          // const content = renderToString(<Home />)
          app.use(express.static(path.resolve(process.cwd(), "client_build")));
          
          app.use(bodyParser.json());
          app.use(bodyParser.urlencoded({ extended: true }));
          
          //...
          
          • 通过上述的方式实现的渲染,数据在服务端是没有注入到拼接的HTML中的,即还是走的客户端渲染,因此需要通过同构的方式实现数据在服务端注入到HTML中,实现服务端渲染
        • 更新store的三种方式
          • 同步:包括客户端和服务端的统一更新
          • 客户端异步:即客户端发起请求,异步获取数据,然后修改store的值
          • 服务端异步:
        • 全局store实现对客户端和服务端「数据注入」的同构
          • 需要依赖于redux进行实现:@reduxjs/toolkit redux-thunk react-redux
          • 在定义好redux全局数据管理后,就可以通过react-redux库的ProvideAPI实现数据的注入了;客户端和服务端的组件中都需要进行Provide数据注入
          • redux搭建流程
            • 构建底层的redux维护逻辑,包括接口数据的请求与数据处理、兜底默认数据的配置维护
              // src/pages/demo/store/demoReducer.ts
              import { createSlice, createAsyncThunk } from "@reduxjs/toolkit"
              import axios from "axios"
              
              const getDemoData = createAsyncThunk(
                  "demo/getData",
                  async (initData:string) => {
                      const res = await axios.post("http://127.0.0.1:3000/api/getDemoData",{
                          content: initData
                      })
              
                      return res?.data?.data?.content
                  }
              )
              
              const  demoReducer = createSlice({
              
                  // 是这个reducer的空间,后续取store的时候会根据这个进行区分
                  name: "demo",
              
                  // 可以理解为原来的state
                  // initialState: {content: "默认"},
                  initialState: 
                      // content: "默认"
                      typeof window !== 'undefined' ?
                          (window as any)?.context?.state?.demo:
                          {
                              content: "默认值"
                          }
                  ,
              
                  // 存放同步的reducers 不需要请求参数
                  reducers: {},
              
                  // 异步reducer 包含三个状态:pending、fulfilled、rejected 对应到请求的三个状态
                  extraReducers(build) {
                      build
                          .addCase(getDemoData.pending,(state,action) => {
                              state.content = "pending"
                          })
                          .addCase(getDemoData.fulfilled,(state,action) => {
                              state.content = action.payload
                          })
                          .addCase(getDemoData.rejected,(state,action) => {
                              state.content = "rejected"
                          })
                  }
              })
              
              export { demoReducer,getDemoData }
              
            • 构建客户端和服务端redux逻辑
              // src/store/index.ts
              import { configureStore } from "@reduxjs/toolkit";
              import thunk from "redux-thunk";
              import { demoReducer } from "@/pages/demo/store";
              const clientStore = configureStore({
                reducer: { demo: demoReducer.reducer },
                middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(thunk),
              });
              
              const serverStore = configureStore({
                reducer: { demo: demoReducer.reducer },
                middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(thunk),
              });
              
              export { clientStore, serverStore };
              
            • 最后将创建好的store分别注入到客户端和服务端即可(有路由时需要注入到路由外层进行全组件页面数据注入)
            • 在进行上述操作后,依然存在客户端和服务端store不同步的问题,服务端在请求完成填充store之后,客户端JS又执行了一遍store,提取了默认值,导致数据不同步,此时就需要使用脱水注水逻辑了
            • 脱水与注水:即数据流的相关操作
              • 客户端脱水处理:即底层的redux维护逻辑中的逻辑处理
                • 客户端在进行数据获取时先判断是否有服务端的数据注水操作,然后再进行相关处理,也需要有对应的兜底方案,即相应的默认数据进行兜底,在服务端数据处理有问题后还可以降级为客户端渲染进行兜底
              • 服务端的注水处理:即在返回给客户端的HTML中进行数据的透传注入
                // ...
                
                const helmet = Helmet.renderStatic();
                res.send(`
                  <html
                    <head>
                      ${helmet.title.toString()}
                      ${helmet.meta.toString()}
                    </head>
                    <body>
                      <div id="root">${content}</div>
                      <script>
                        window.context = {
                          state: ${JSON.stringify(serverStore.getState())}
                        }
                      </script>
                      <script src="/index.js"></script>
                    </body>
                  </html>
                `);
                
                // ...
                
      • CSS在SSR中的应用
        • CSS只适用于客户端的配置中,服务端是不支持CSS代码的,因此在服务端中需要单独使用对应的库实现服务端对CSS代码的支持,如isomorphic-style-loader,在客户端的webpack配置中就不需要进行单独处理了
      • SSR与Serverless
        • 随着Serverless的生态不断完善,Serverless的模式可以为SSR带来更多的便捷性,主要有以下几点
          • 借助于函数即服务的能力,不需要再去搭建传统的Node应用,一个函数就可以是一个服务,开发者可以更纯粹的关注于业务逻辑
          • FaaS以函数为单位的形式以及弹性机制,为SSR应用带来了天然的隔离性和动态修复能力,可以更好的避免页面间的交叉污染,或一些边界的异常场景对应用带来的影响
          • 另外还有无需运维、按需执行、弹性伸缩这些特性,大大,降低了SSR应用对开发者的门槛
          • 因此借助Serverless带来的想象空间,以及Rax在工程和SSR渲染引擎上所做的工作,我们是完全可以做到媲美目前CSR模式的开发体验的
            • Rax的server端渲染引擎,采用了静态模版+动态组件的混合渲染模式,渲染性能是react的6倍

总结

SSR静态页面渲染

  • 模版页面渲染
    • renderToString()生成纯静态页面字符串
    • hydrateRoot()处理模版页面的事件绑定
  • 路由匹配
    • BrowserRouter处理客户端路由
    • staticRouter处理服务端无状态路由
  • header修改
    • 客户端helmet配置 → react-helmet
    • 服务端helmet.renderStatic()获取渲染的header注入返回HTML字符串

SSR支持数据请求

  • express无法直接读取请求的body,需要使用body-parse对请求进行解析
  • @redux/toolkit是redux最新提供的工具包,可用于状态的统一管理,提供了很多hook能力,代码也相对简单 redux-toolkit官网:
  • redux-thunk是一个redux中间件,提供了dispatch和getState与异步方法交互能力
  • npm-run-all可以一步执行对应的所有配置化的script指令,如npm-run-all --parallel start:**可以执行script中所有以start:XXX以start开头的命令,简化开发过程中的步骤

常规渲染方案

  • 普通页面加载
    • 请求域名、服务端返回HTML资源
    • 浏览器加载HTML片段,识别到有css/javascript资源时,获取资源并加载
  • 单页面应用渲染流程(除上述流程外)
    • 加载并初始化前端框架、路由库
    • 根据当前页面路由配置,命中对应的页面组件并进行渲染
    • 页面组件如果有依赖的资源,则发起请求获取数据后,再进行渲染
  • 框架自带的SSR能力
    • 许多开源框架提供了SSR能力,以vue为例,vue提供了vue-server-render服务端能力,可以实现在浏览器请求服务端时,服务端可以完成动态拼接HTML的能力,将拼接好的HTML直接返回给浏览器,浏览器再完成渲染页面 服务端渲染的正确方式是:查找和构建组件缓存方案、内存消耗管理、应用记忆化技术的相互结合和升华迭代

相关配置

  • 将ESM转成CommonJS运行起来
    require("ignore-styles"); //用于Babel编译过程中忽略样式文件的导入
    require("@babel/register")({
      ignore: [/(node_modules)/],
      presets: ["@babel/preset-env", "@babel/preset-react"],
    });
    
    require("./server");
    
    • @babel/register:该依赖会将 node 后续运行时所需要 require 进来的扩展名为 .es6、.es、.jsx、 .mjs 和 .js 的文件将由 Babel 自动转换

推荐文献

服务端渲染相关原理
图解 SSR 等 6 种前端渲染模式