react ssr 搭建

1,106 阅读6分钟

csr(客户端渲染) & ssr(服务端渲染) & nsr & esr(边缘渲染)

  • csr:

    页面内容的渲染来自于服务端返回的js脚本和ajax请求的数据的执行。csr的出现实现了前后端架构分离。csr的本质就是拿到html后请求打包工具打包生成的js脚本,然后执行该脚本在渲染。缺点:不利于seo和首次渲染时间长

  • Ssr:

    在服务端完成页面模板、数据预取、填充,并且在服务端就可以将完整的 HTML 内容返回给浏览器。ssr本质上就是把原来csr的初始化数据这个操作让服务端执行了,然后调用renderToString函数生成html文本,再执行服务生成一个初始的静态页面。缺点: 会增加项目整体复杂度,对服务器压力大

  • Nsr:

    这是一种在 hybrid 中特有的渲染技术。简单说就是通过 Native 渲染生成 HTML 数据,并且缓存在客户端。这样一来,对于一个 hybrid WebView 的用户访问,会优先从离线包中加载离线页面模板,再通过前端 Ajax/或客户端能力请求数据,最终完成页面完整的展示

    这样做的好处显而易见:我们将服务器的渲染工作放在了一个个独立的移动设备中,并借助离线存储技术,实现了页面的预加载,同时又不会增加额外的服务器压力。

  • Esr

    边缘渲染则更加激进。ESR 其实借助了最近几年较火的“边缘计算”能力。

    边缘计算: 是指在靠近物或数据源头的一侧,采用网络、计算、存储、应用核心能力为一体的开放平台,就近提供最近端服务。其应用程序在边缘侧发起,产生更快的网络服务响应,满足行业在实时业务、应用智能、安全与隐私保护等方面的基本需求。边缘计算处于物理实体和工业连接之间,或处于物理实体的顶端。而云端计算,仍然可以访问边缘计算的历史数据。

    ESR 渲染利用了 CDN 能力。ESR会在 CDN 上缓存页面的静态部分,这样在用户访问页面时,可以快速返回给用户静态内容,同时在 CDN 节点上也发起动态部分内容请求,在动态内容获取之后,利用流的方式,继续返回给用户

手动搭建一个ssr框架

同构

同构:一套react代码,在服务端执行一次,在客户端也执行一次,reactDom.renderToString将jsx转为html文本的时候,不会处理jsx上面的attrs的事件属性。所以需要在客户端执行ReactDom.hydrate,把事件和属性生效

具体的例子请看我写的demo:

那么怎么同构呢? 首先服务端运行react生成一遍html, 然后在html中嵌入打包后的index.js, 把index.js放入静态文件目录,这样就可以通过请求获取到index.js. 这里的index.js内容是reactDom.hydrate重新执行渲染操作。

  1. 服务端渲染jsx->html,使用ReactDom.renderToString生成

  2. 客户端在运行jsx->html,使用ReactDom.hydrate进行客户端的再次渲染

这里客户端的代码写在client,用webpack.client.js打包,打包到src/public目录。 服务端的代码写在server,用webpack.server.js打包,打包到build目录。 流程:服务端运行react渲染出html->发送html到浏览器->浏览器加载js文件->js中的react代码重新执行->js中的react代码接管页面操作。

路由的处理

客户端使用BrowserRouter, 服务端使用StaticRouter。 客户端执行react-router的时候,是可以知道浏览器当前的url的。 然而服务端只是返回的StaticRouter生成的html文本。StaticRouter并不知道当前浏览器访问的路径 所以需要node app应用将自己的context.request.path传入到StaticRouter,再渲染出对应的html文本。

hydrate(注水) & 脱水 & 全局context store

同构项目中如何引入context store

主要代码如下:

import {
    Store as GlobalStore,
    useReduxHook as useGlobalReduxHook,
    initStore,
} from "../store/global";

function GlobalProvider({ children }) {
    const [state, dispatch] = useGlobalReduxHook();
    return (
        <GlobalStore.Provider value={{ state, dispatch }}>
            {children}
        </GlobalStore.Provider>
    );
}

服务端renderToString这个函数不会执行组件的生命周期函数。所以客户端的生命周期运行生成的数据,服务端拿不到。那么怎么解决异步数据服务器渲染呢?

目标:服务端根据用户访问的路径不同,加载不同的异步数据。

方法: 改写路由给组件添加静态方法loadData, 调用loadData方法, 将请求的数据更新到 Context store. 这样就可以拿到数据在renderString渲染了

主要代码如下:

// 组件异步请求数据
const Home = (props) => {
  const { dispatch: globalDispatch, state: globalState }
	  = useContext(GlobalStore);
  const { weatherMessage } = globalState;
  return <div />
}
Home.loadData = (Store) => {
    return serverAxios
        .get("/api/homedata")
        .then((response) => {
            Store.weatherMessage = response.data.weatherMessage;
        })
        .catch((err) => {
            console.log("error", err);
        });
};

// 路由执行组件的异步处理函数loadData
import { matchRoutes } from "react-router-config";
router.get("/(.*)", async (context, next) => {
    // 根据路由的路径,来往store里面加数据
    const promises = [];
    const matchedRoutes = matchRoutes(Routes, context.request.path);
    // 让matchRoutes里面的所有组件对应的loadData方法执行一次
    // 使得数据提前加载好在渲染。
    matchedRoutes.forEach((item) => {
        if (item.route.loadData) {
            const promise = new Promise((resolve, reject) => {
                return item.route
                    .loadData(initStore)
                    .then(resolve())
                    .catch(resolve());
            });
            promises.push(promise);
        }
    });
    Promise.all(promises).then(() => {
        const ctx = {
            css: [],
        };
        const html = render(context.request, Routes, ctx);
        if (ctx.notFound) {
            context.response.status = 404;
        }
        // staticRouter的Redirect组件被调用
        // ctx增加了action,url属性
        if (ctx.action === "REPLACE") {
            context.response.redirect(301, ctx.url);
        }
        context.response.body = html;
    });
    await next();
});

虽然服务端渲染执行loadData拿到了数据,但是客户端渲染的时候,初始store并没有数据,那么怎么保证组件在客户端和服务端的初始返回渲染文本一致呢?这就需要注水与出水操作。

注水:将服务端渲染的数据放到script中的window.context 出水:客户端的store初始数据从window.context里面拿

另外客户端在执行渲染的时候,由于history.go, link等方法都是不会通过本地的BrowserRouter根据路由的变化返回响应的组件,这个时候组件的渲染不是由服务器执行loadData返回的,客户端出水操作拿到的初始store的是空值。所以需要客户端在生命周期函数中也执行数据的请求在渲染。

404页面的编写和301重定向

多级路由下,没有配置的路径显示404组件 服务端的staticRouter的context对象添加一个NotFound属性作为标识

react-router的Redirect组件调用的时候,服务端的staticRouter的context对象会增加一个action属性。

if (ctx.action === "REPLACE") {
   context.response.redirect(301, ctx.url);
}

样式的处理

服务端不能用style-loader, css-loader. 因为在nodeJs中window是不存在的。 那么在组件使用css需要怎么解决呢,这里使用isomorphic-style-loader插件

注意:

  1. webpack配置isomorphic-style-loader的时候,必须使用CommonJs模块语法,必须写上esModule:false
  2. isomorphic-style-loader这个插件不会将css文案写入到html中。它只会生成className. 所以需要自己手动引入
 use: ['isomorphic-style-loader', {loader: 'css-loader', options: {esModule: false}}]

样式的处理主要代码如下:

import styles from './style.css'
// props.staticContext 是有staticRouter的context属性注入的
if(props.staticContext) {
   props.staticContext.css.push(styles._getCss())
}

seo

使用react-helmet插件, 会在head头部注入: 和

主要代码如下:

// 组件中写入:
<Helment>
  <title />
  <meta name="description" content="" />
</Helment>

// 渲染时
const helmet = Helmet.renderStatic()
${helmet.title.toString()}
${helmet.meta.toString()}

react ssr 搭建代码

下面是实现react ssr完成搭建的代码

  1. scss的使用
  2. 关于context中notFound,action属性,打印一下context