乐趣区

关于react.js:React-SSR-写个-Demo-一学就会

明天写个小 Demo 来从头实现一下 reactSSR,帮忙了解 SSR 是如何实现的,有什么细节。

什么是 SSR

SSRServer Side Rendering 服务端渲染,是指将网页内容在服务器端中生成并发送到浏览器的技术。相比于客户端渲染(CSR),SSR 个别用于以下场景:

  1. SEO(搜索引擎优化):因为局部搜索引擎对 CSR 内容反对不佳,所以 SSR 能够晋升网站在搜索引擎后果中的排名。
  2. 首屏加载速度:因为 SSR 能够在服务器端生成残缺的 HTML 页面,用户关上网页时可能更快地看到内容,不会看到长时间的白屏,能够晋升用户体验。
  3. 暗藏某些数据:因为 CSR 须要从服务器将数据下载下来进行动静渲染,所以一些数据很容易被别人获取,而 SSR 因为数据到渲染的过程在服务端实现,所以能够用来暗藏一些不想让别人轻易取得的数据。

如何实现

简略的 SSR 其实实现很简略,只须要在服务端导入要渲染的组件,而后调用 react-dom/server 包中提供的 renderToString 办法将该组件的渲染内容输入为字符串后返回客户端即可。

Server 端的组件

上面写一个简略的例子:

服务端代码:

import express from 'express';
import React from 'react';
import {renderToString} from 'react-dom/server';

import App from '../ui/App';

const app = express();

app.get('/', (_: unknown, res: express.Response) => {res.send(renderToString(<App />));
});

app.listen(4000, () => {console.log('Listening on port 4000');
});

此处要留神 服务端须要反对 jsx 语法的解析,我这里间接应用 esno 执行 ts 代码,在 tsconfig.json 中配置 jsx 即可。

其实看到这里就能明确为什么在 SSR 的页面上应用 windowlocalstorage 等浏览器 API 须要放到 useEffect 里了,因为 该页面的组件都会被 server 端读取解析,而 server 端并没有这些 API

而后看下 App 组件的代码:

import React, {useCallback} from 'react';

export default () => {const log = useCallback(() => {console.log('Hello world');
    }, []);

    return (
        <div>
            <p>react ssr demo</p>
            <button onClick={log}>Click me</button>
        </div>
    );
};

启动服务器后 server 端就会应用 renderToString<App /> 渲染成 html 字符串,而后通过 send 返回给前端,上面就是服务端返回的 html 内容:

<div>
    <p>react ssr demo</p>
    <button>Click me</button>
</div>

关上浏览器拜访该地址即可看到服务端返回了该 html 片段:

hydrate 复活组件

如果你跟着下面的操作很快就会发现问题:为什么点按钮没法操作了?

其实起因很简略,因为咱们只拿到了一个 html 并没有任何的 js,事件绑定等天然是无奈实现的,要复活组件的交互咱们还须要很重要的一步 – hydrate 也就是常说的水合。

hydrate 即通过 react 将对应的组件从新渲染到 SSR 渲染的动态内容上,相似于 render 差别点在于 render 会疏忽 root 元素中现有的 domhydrate 则会复用并会进行内容匹配查看。

Hydration failed because the initial UI does not match what was rendered on the server.

如果遇到上述谬误即示意在客户端执行 hydrate 时服务端返回的初始的 domhydrate 接管到的须要进行渲染的 dom 不匹配。

说了这么多咱们再来看下代码如何编写,首先要进行 hydrate 咱们须要客户端的代码来执行:

import React from 'react';
import {hydrateRoot} from 'react-dom/client';

import App from './App';

hydrateRoot(document.getElementById('root')!, <App />);

而后将该代码进行编译打包,我这里就间接应用 webpack 进行打包:

const path = require('path');

module.exports = {
    entry: './ui/index.tsx',
    output: {path: path.resolve(__dirname, 'static'),
        filename: 'bundle.js'
    },
    resolve: {extensions: ['.ts', '.tsx', '.js', '.jsx']
    },
    module: {
        rules: [
            {test: /\.(t|j)sx?$/,
                exclude: /node_modules/,
                use: {
                    loader: 'babel-loader',
                    options: {presets: ['@babel/preset-react', '@babel/preset-typescript']
                    }
                }
            }
        ]
    }
};

打包实现后生成一个 bundle.js 即可在客户端应用它来进行 hydrate

而后咱们再批改下 server 端的代码:

app.get('/', (_: unknown, res: express.Response) => {
    res.send(
        `
<div id="root">${renderToString(<App />)}</div>
<script src="/bundle.js"></script>
`
    );
});

app.use(express.static('static'));

咱们在动态内容的外层套上 root 元素,而后在下方引入咱们刚刚编译的脚本,而后就能够在客户端看到咱们想要的后果:

能够看到事件能够失常触发了。

此处还有个留神点,在 server 端要留神将动态字符串包裹在 root 元素中不要增加换行空格等,不然 reacthydrate 时依旧会因为内容不匹配而提醒 Hydration failed(仅在 hydrateRoot 时呈现,如果应用 hydrate 不会报错,不过 18 中 hydrate 曾经被弃用。)

动态数据

此时有些同学可能发现一些问题:后面的内容所渲染的内容都是动态的,如果要针对用户渲染出不同的内容比方用户信息等如何是好?

其实很简略,只须要在服务端将对应的信息作为 props 进行渲染即可,咱们上面应用 userName 模仿一下:

app.get('/', (_: unknown, res: express.Response) => {const userName = ['张三', '李四', '王五', '赵六'][(Math.random() * 4) | 0];
    res.send(
        `
<div id="root">${renderToString(<App userName={userName} />)}</div>
<script src="/bundle.js"></script>
`
    );
});

可是客户端要如何与服务端匹配呢?此处有两种解决方案:

  1. 客户端获取对应的信息并在信息获取实现后再进行 hydrate 操作。
  2. 服务端将获取到的信息放在页面中。

能够看出计划 1 会带来显著的延时,所以个别会采纳计划 2,实现个别能够应用全局变量或特定标签来实现:

app.get('/', (_: unknown, res: express.Response) => {const userName = ['张三', '李四', '王五', '赵六'][(Math.random() * 4) | 0];
    res.send(
        `
<div id="root">${renderToString(<App userName={userName} />)}</div>
<script>
window.__initialState = {userName: '${userName}' };
</script>
<script src="/bundle.js"></script>
`
    );
});
import React from 'react';
import {hydrateRoot} from 'react-dom/client';

import App from './App';

hydrateRoot(document.getElementById('root')!, <App {...window.__initialState} />);

总结

  1. React 中的 SSR 能够通过 renderToString 来实现,然而只能输入动态内容,要让页面反对交互须要搭配 hydrate 应用。
  2. 实现 SSR 时服务端须要反对 jsx 语法的解析,因为服务端也须要读取组件。
  3. hydrate 会查看服务端与客户端的内容是否匹配。
  4. 要实现动态数据须要在客户端与服务端之间做好如何应用初始 props 的约定。

最初

本文的 demo 代码搁置在 React SSR Demo 中,可自行取阅。

退出移动版