Skip to content

渲染组件

服务端渲染组件

React 服务端渲染(Server-Side Rendering,简称 SSR)是一种将 React 组件在服务器端渲染成 HTML 字符串的技术,然后将这些 HTML 发送到客户端,客户端的浏览器接收到完整的 HTML 文档后可以直接进行渲染。这种方式可以改善首屏加载时间、SEO 效果等。

要在 React 中实现 SSR,通常需要以下几个步骤:

1. 创建一个 Express 服务器

首先,你需要创建一个 Node.js 服务器来处理请求。Express 是一个流行的 Node.js 框架,非常适合用来构建这种服务器。

javascript
// server/index.js
const express = require("express");
const server = express();
app.listen(8080, () => {
  console.log("server start on 8080");
});

2. 渲染 React 组件

使用ReactDOMServer.renderToString()方法将 React 组件转换为 HTML 字符串。

首先,确保安装了react, react-dom/server等相关依赖。

javascript
// server/index.js
import React from "react";
import { renderToString } from "react-dom/server";
import Home from "./Home"; // 假设这是你的主组件

server.get("*", (req, res) => {
  const content = renderToString(<Home />);

  const html = `
    <html>
      <head></head>
      <body>
        <div id="root">${content}</div>
      </body>
    </html>
  `;
  res.send(html);
});
jsx
// src/pages/Home/index.jsx
import React from "react";

export default function () {
  return (
    <>
      <h1>Hello World</h1>
      <h3>第一个React SSR Demo</h3>
    </>
  );
}

3. 添加 webpack 配置

安装相关依赖

bash
# webpack相关依赖
yarn add webpack webpack-cli -D
# babel 相关依赖
yarn add @babel/core @babel/preset-react babel-loader -D
# 忽略 node_modules 中的文件
yarn add webpack-node-externals -D

webpack.config.js 配置

js
// webpack.config.js
const path = require("path");
const nodeExternals = require("webpack-node-externals");

module.exports = {
  mode: "development", // 开发环境推荐 development,生产环境推荐 production
  devtool: "source-map", // 开发环境推荐
  entry: "./server/index.js", // 服务端入口文件
  output: {
    filename: "server.js", // 打包后文件名称
  },
  watch: true, // 开启监听文件修改, 默认为 false
  target: "node", // 指定打包环境为 node
  resolve: {
    alias: {
      "@": path.resolve(__dirname, "./src"), // 配置别名
    },
    extensions: [".js", ".jsx"], // 指定需要编译的文件后缀
  },
  externals: [nodeExternals()], // 排除 node_modules 文件
  module: {
    rules: [
      {
        test: /\.jsx?/, // 指定编译的文件
        exclude: /node_modules/,
        use: {
          loader: "babel-loader",
          options: {
            presets: ["@babel/preset-react"],
          },
        },
      },
    ],
  },
};

4. 运行服务器

运行打包命令

bash
yarn build

运行服务器

bash
node dist/server.js

5. package.json 命令优化

安装相关依赖

bash
#
yarn add nodemon -G
# npm-run-all
yarn add npm-run-all -D
  • nodemon:监听文件变化,自动重启服务
  • npm-run-all:同时运行多个命令
json
{
  "scripts": {
    "dev:build": "webpack",
    "dev:start": "nodemon --watch dist --exec node dist/server",
    "dev": "npm-run-all --parallel dev:*"
  }
}
  • "nodemon --watch dist --exec node dist/server": 监听 dist 文件夹,当文件变化时,重启服务
  • "npm-run-all --parallel dev:*": 同时运行 dev:builddev:start 两个命令

6. 服务端文件优化

render.js

js
// server/render.js
import React from "react";
import App from "./App";
import ReactDom from "react-dom/server";

export default (req, res) => {
  const componentHTML = ReactDom.renderToString(<App />);
  const html = `
  <!DOCTYPE html>
  <html lang="en">
  <head>
    <meta charset="UTF-8"/>
    <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
    <title>SSR</title>
  </head>
  <body>
    <div id="root">
      ${componentHTML}
    </div>
  </body>
  </html>
  `;
  res.send(html);
};

App.jsx

jsx
// server/App.jsx
import React from "react";
import Home from "@/pages/Home";

export default () => {
  return <Home />;
};

server/index.js

js
// server/index.js
import express from "express";
import render from "./render";

const app = express();

app.use(express.static("./public"));

app.get("*", render);

app.listen(8080, () => {
  console.log("server start on 8080");
});

客户端渲染组件

在 React 的服务端渲染(SSR, Server-Side Rendering)应用中,客户端渲染组件的过程通常被称为“激活”(Hydration)。这个过程是让服务端已经渲染好的 HTML 页面变得可交互。以下是如何实现客户端渲染或激活服务端渲染的 React 组件的基本步骤:

1. 确保服务端正确渲染

首先确保你的服务端代码能够正确地将 React 组件渲染为 HTML 字符串,并将其发送到客户端。这通常通过ReactDOMServer.renderToString()来完成。

2. 客户端激活(Hydration)

在客户端,使用ReactDOM.hydrate()方法来激活服务端渲染的 HTML。与ReactDOM.render()不同,hydrate()假设 DOM 内容已经由服务端渲染好了,它只是在此基础上添加事件监听器和其他交互功能。

javascript
// client/index.js
import React from "react";
import ReactDOM from "react-dom";
import App from "./App"; // 假设这是你的主组件

// 在挂载点上进行Hydration,仅注入js 代码
ReactDOM.hydrate(<App />, document.getElementById("root"));

这里有几个关键点需要注意:

  • 一致性:确保服务端渲染的内容和服务端传送到客户端的 JavaScript 代码生成的内容是一致的。如果不一致,可能会导致不正确的事件绑定或其他问题。
  • 性能考虑:虽然hydrate()方法比render()更快,因为它不需要重新创建 DOM 节点,但在大型应用中仍然需要关注性能。例如,可以通过懒加载非关键路径下的组件来优化首屏加载时间。
jsx
// ./client/App.js
import React from "react";
import Home from "@/pages/Home";

export default () => {
  return <Home />;
};

3. webpack.client.js

js
// ./webpack.client.js
const path = require("path");

module.exports = {
  mode: "development",
  devtool: "source-map",
  entry: "./client/index.js",
  output: {
    filename: "js/bundle.js",
    path: path.resolve(__dirname, "./public"),
  },
  watch: true,
  target: "node",
  resolve: {
    alias: {
      "@": path.resolve(__dirname, "./src"),
    },
    extensions: [".js", ".jsx"],
  },
  module: {
    rules: [
      {
        test: /\.jsx?/,
        exclude: /node_modules/,
        use: {
          loader: "babel-loader",
          options: {
            presets: ["@babel/preset-react"],
          },
        },
      },
    ],
  },
};
  • 输出目录:客户端渲染的静态资源将输出到 public 目录下,这样可以确保静态资源在部署时能够正确地访问。

4. 动态响应组件

jsx
import React, { useState } from "react";

export default function () {
  const [count, setCount] = useState(0);
  const handleClick = (e) => {
    // console.log(e);
    setCount(count + 1);
  };
  return (
    <>
      <h1>Hello World</h1>
      <h3>第一个React SSR Demo</h3>
      <h3>点击操作:{count}</h3>
      <button onClick={handleClick}>点击</button>
    </>
  );
}

5. package.json 调整

json
{
  "scripts": {
    "dev:build:server": "webpack --config webpack.server.js",
    "dev:build:client": "webpack --config webpack.client.js",
    "dev:start": "nodemon --watch dist --exec node dist/server",
    "dev": "npm-run-all --parallel dev:**"
  }
}
  • npm-run-all --parallel dev:**: 这个命令会并行执行 dev:build:serverdev:build:client 两个任务,\*\*表示匹配所有以dev: 开头的任务。

webpack 配置优化

webpack.base.js

js
// webpack/webpack.base.js
const path = require("path");

module.exports = {
  mode: "development",
  devtool: "source-map",
  watch: true,
  target: "node",
  resolve: {
    alias: {
      "@": path.resolve(__dirname, "../src"),
    },
    extensions: [".js", ".jsx"],
  },
  module: {
    rules: [
      {
        test: /\.jsx?/,
        exclude: /node_modules/,
        use: {
          loader: "babel-loader",
          options: {
            presets: ["@babel/preset-react"],
          },
        },
      },
    ],
  },
};

webpack.client.js

js
// webpack/webpack.client.js
const { merge } = require("webpack-merge");
const baseConfig = require("./webpack.base");
const path = require("path");
const { CleanWebpackPlugin } = require("clean-webpack-plugin");

module.exports = merge(baseConfig, {
  entry: "./client/index.js",
  output: {
    filename: "js/bundle.[hash:5].js",
    path: path.resolve(__dirname, "../public"),
  },
  plugins: [
    new CleanWebpackPlugin({
      cleanOnceBeforeBuildPatterns: ["**/*", "!favicon.ico"],
    }),
  ],
});
  • clean-webpack-plugin: 用于清除构建目录中的文件。

webpack.server.js

js
// webpack/webpack.server.js
const nodeExternals = require("webpack-node-externals");
const { merge } = require("webpack-merge");
const baseConfig = require("./webpack.base");

module.exports = merge(baseConfig, {
  entry: "./server/index.js",
  output: {
    filename: "server.js",
  },
  externals: [nodeExternals()],
});

动态获取打包后 js 文件

js
// server/utils.js
import fs from "fs";
export function getFileName(dirPath = "./public/js") {
  return fs
    .readdirSync(dirPath)
    .filter((file) => file.endsWith(".js"))
    .map((file) => {
      return `<script src="./js/${file}"></script>`;
    });
}

render.js

js
// server/render.js
import React from "react";
import App from "./App";
import ReactDom from "react-dom/server";
import { getFileName } from "./utils";

export default (req, res) => {
  const componentHTML = ReactDom.renderToString(<App />);
  const html = `
  <!DOCTYPE html>
  <html lang="en">
  <head>
    <meta charset="UTF-8"/>
    <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
    <title>SSR</title>
  </head>
  <body>
    <div id="root">${componentHTML}</div>
    ${getFileName()}
    </body>
  </html>
  `;
  res.send(html);
};

总结

SSR 的主要目标之一是提升用户体验和 SEO 表现,因此确保服务端渲染的内容尽可能完整,同时合理利用客户端资源来增强交互性和应用的功能性。

Released under the MIT License.