动态

详情 返回 返回

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 片段:

picture 1

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 元素,然後在下方引入我們剛剛編譯的腳本,然後就可以在客户端看到我們想要的結果:

picture 2

可以看到事件可以正常觸發了。

此處還有個注意點,在 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 中,可自行取閲。

user avatar Leesz 头像 alibabawenyujishu 头像 nihaojob 头像 kobe_fans_zxc 头像 aqiongbei 头像 leexiaohui1997 头像 longlong688 头像 inslog 头像 xiaoxxuejishu 头像 solvep 头像 dunizb 头像 febobo 头像
点赞 141 用户, 点赞了这篇动态!
点赞

Add a new 评论

Some HTML is okay.