实现Mini React SSR

14 天前(已编辑)
/ ,
4

实现Mini React SSR

CSR

实现SSR之前先实现一遍CSR渲染过程

react-csr
|- app.js             // 测试组件
|- client.js          // react生成出root节点
|- index.html         // 入口文件
|- package.json       // npm init 自动生成
|- webpack.client.js  // 客户端打包配置

安装依赖

npm install webpack webpack-cli babel-loader @babel/preset-env @babel/preset-react react react-dom

index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Tiny React SSR</title>
</head>
<body>
    <div id="root">
    </div>
    <script src="./index.js" />
</body>
</html>

先用webpack打包React代码成index.js

client.js

import React from "react";
import {createRoot} from 'react-dom/client'
import App from './app'

const root = createRoot(document.getElementById('root'))
root.render(<App />);

app.js

import { useState } from "react";

export default function App(){
    const [count,setCount] = useState(0);
    return (
        <div>
            <h1>Counters {count} times</h1>
            <button onClick={() => setCount(count + 1)}>
                Click me
            </button>
        </div>
    )
}

由于Javascript不能直接识别React的JSX格式,所以需要webpack和babel将JSX代码转译成Javascript代码,新建webpack.client.js

const path = require('path');

module.exports = {
    mode: 'development',
    entry: './client.js',
    output: {
        filename: 'index.js',
        path: path.resolve(__dirname)
    },
    module: {
        rules: [
            {
                test: /\.(js|jsx)$/,
                exclude: /node_modules/,
                use: {
                    loader: 'babel-loader',
                    options: {
                        presets: [
                            '@babel/preset-env', 
                            ['@babel/preset-react', { "runtime": "automatic" }]
                        ]
                    }
                }
            }
        ]
    }
};

Webpack.client.js是为了区分接下来的csr和ssr起的名字,不会像webpack.config.js一样被自动读取,所以我们需要在运行webpack时候手动指定一下该文件

修改package.json

"scripts": {
    "start": "webpack --config webpack.client.js",
    "test": "echo \"Error: no test specified\" && exit 1"
  },

运行npm start

image-20241021220744178

Support/typora-user-images/image-20241021220744178.png

此时根目录会生成index.js,打开index.html

image-20241021220918623

Support/typora-user-images/image-20241021220918623.png

SSR

与CSR相比,SSR多了服务端,所以需要用到Express

react-csr
|- public
|- build
|- pages
     |- index,js
|- server.js                  
|- package.json       
|- webpack.server.js
|- webpack.client.js

安装依赖

npm install webpack webpack-cli babel-loader @babel/preset-env @babel/preset-react react react-dom express

Server.js

const express = require('express')
const app = express()

app.get('/',(req,res) => res.send(`
<html>
    <head>
        <title>Tiny React SSR</title>
    </head>
    <body>
        <div id='root'>
            Counter 0 times
        </div>
    </body>
</html>    
    `))

app.listen(3000,() => console.log('listening on port 3000'))

node server.js

Support/typora-user-images/image-20241021222243391.png

Support/typora-user-images/image-20241021222243391.png

express 启动成功

开始实现SSR,修改server.js

import express from 'express'
import React from 'react'
import {renderToString} from 'react-dom/server'
import App from './pages/index'

const app = express()
const content = renderToString(<App />)

app.get('/',(req,res) => res.send(`
<html>
    <head>
        <title>Tiny React SSR</title>
    </head>
    <body>
        <div id='root'>
            ${content}
        </div>
        <script src='/client.bundle.js'></script>
    </body>
</html>    
    `))

app.listen(3000,() => console.log('listening on port 3000'))

pages/index.js

import React,{useState} from "react";

export default function App(){
    const [count,setCount] = useState(0)

    return (
        <div>
            <h1>Counters {count}</h1>
            <button onClick={() => setCount(count + 1)}>
                Click me
            </button>
        </div>
    )
}

此时运行node server.js看到会报错

image-20241021222732062

Support/typora-user-images/image-20241021222732062.png
  1. 使用了import语法,这是ES规范,而非Node.js的CommonJS规范
  2. 我们使用了React的JSX语法,JavaScript并不认识,需要用babel转译

新建webpack.server.js

const path = require('path')

module.exports = {
    mode: 'development',
    target: 'node',
    entry: './server.js',
    output: {
        filename: 'server.bundle.js',
        path: path.resolve(__dirname,'build')
    },
    module: {
        rules: [
            {
                test: /\.(js|jsx)$/,
                exclude: /node_modules/,
                use: {
                    loader: 'babel-loader',
                    options: {
                        presets: ['@babel/preset-env','@babel/preset-react']
                    }
                }
            }
        ]
    }
}

修改package.json

"scripts": {
   "start": "webpack --config webpack.server.js && node ./build/server.bundle.js",
}
image-20241021222956023

Support/typora-user-images/image-20241021222956023.png

此时成功渲染,但是只用renderToString将React组件转为了HTML字符串,并没有任何的Hydrate(水合)事件绑定,只有静态的HTML

绑定事件

既然服务端只能渲染HTML,客户端能渲染事件,那就结合一下,再实现一遍CSR,让页面插入一个打包后的bundle,挂载到想同的节点,让客户端将一模一样的内容重新渲染一遍,并绑定上事件(重复渲染后面解决)

修改server.js

import express from 'express'
import React from 'react'
import {renderToString} from 'react-dom/server'
import App from './pages/index'

const app = express()
app.use(express.static('public'))
const content = renderToString(<App />)

app.get('/',(req,res) => res.send(`
<html>
    <head>
        <title>Tiny React SSR</title>
    </head>
    <body>
        <div id='root'>
            ${content}
        </div>
        <script src='/client.bundle.js'></script>
    </body>
</html>    
    `))

app.listen(3000,() => console.log('listening on port 3000'))

打包后的客户端代码被命名为client.bundle.js,并放入public目录

新建client.js

import React from 'react'
import { hydrateRoot } from 'react-dom/client'
import App from './pages/index'

const root = createRoot(document.getElementById('root'));

root.render(<App />)

新建webpack.client.js

const path = require('path');

module.exports = {
    mode: 'development',
    entry: './client.js',
    output: {
        filename: 'index.js',
        path: path.resolve(__dirname)
    },
    module: {
        rules: [
            {
                test: /\.(js|jsx)$/,
                exclude: /node_modules/,
                use: {
                    loader: 'babel-loader',
                    options: {
                        presets: [
                            '@babel/preset-env', 
                            ['@babel/preset-react', { "runtime": "automatic" }]
                        ]
                    }
                }
            }
        ]
    }
};

现在的流程是:

先打包客户端的JS,将引用pages/index.js 核心React代码的client.js打包到public下的client.bundle.js

然后将同样引用pages/index.js核心React代码的server.js打包到build下的server.bundle.j中,然后node开启server.bundle.js

服务端会先渲染一遍组件代码,然后输出到HTML中,引用client.bundle.js,然后用js再渲染一遍,并绑定上事件

修改package.json

"scripts": {
    "start": "webpack --config webpack.client.js && webpack --config webpack.server.js && node ./build/server.bundle.js",
  },

此时已经能交互,但是有个重复渲染问题需要解决

HydrateRoot

React官方提供了HydratRoot API,允许你在先前react-dom/server生成的HTML DOM节点中展示React组件,简单来说就是服用服务器渲染出来的DOM节点

修改client.js

import React from 'react'
import { hydrateRoot } from 'react-dom/client'
import App from './pages/index'

const root = hydrateRoot(document.getElementById('root'));

root.render(<App />)

此时既实现了SSR又不会重复渲染

使用社交账号登录

  • Loading...
  • Loading...
  • Loading...
  • Loading...
  • Loading...