渲染组件
服务端渲染组件
React 服务端渲染(Server-Side Rendering,简称 SSR)是一种将 React 组件在服务器端渲染成 HTML 字符串的技术,然后将这些 HTML 发送到客户端,客户端的浏览器接收到完整的 HTML 文档后可以直接进行渲染。这种方式可以改善首屏加载时间、SEO 效果等。
要在 React 中实现 SSR,通常需要以下几个步骤:
1. 创建一个 Express 服务器
首先,你需要创建一个 Node.js 服务器来处理请求。Express 是一个流行的 Node.js 框架,非常适合用来构建这种服务器。
// 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等相关依赖。
// 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);
});// src/pages/Home/index.jsx
import React from "react";
export default function () {
return (
<>
<h1>Hello World</h1>
<h3>第一个React SSR Demo</h3>
</>
);
}3. 添加 webpack 配置
安装相关依赖
# 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 -Dwebpack.config.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. 运行服务器
运行打包命令
yarn build运行服务器
node dist/server.js5. package.json 命令优化
安装相关依赖
#
yarn add nodemon -G
# npm-run-all
yarn add npm-run-all -Dnodemon:监听文件变化,自动重启服务npm-run-all:同时运行多个命令
{
"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:build和dev:start两个命令
6. 服务端文件优化
render.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
// server/App.jsx
import React from "react";
import Home from "@/pages/Home";
export default () => {
return <Home />;
};server/index.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 内容已经由服务端渲染好了,它只是在此基础上添加事件监听器和其他交互功能。
// 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 节点,但在大型应用中仍然需要关注性能。例如,可以通过懒加载非关键路径下的组件来优化首屏加载时间。
// ./client/App.js
import React from "react";
import Home from "@/pages/Home";
export default () => {
return <Home />;
};3. webpack.client.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. 动态响应组件
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 调整
{
"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:server和dev:build:client两个任务,\*\*表示匹配所有以dev:开头的任务。
webpack 配置优化
webpack.base.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
// 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
// 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 文件
// 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
// 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 表现,因此确保服务端渲染的内容尽可能完整,同时合理利用客户端资源来增强交互性和应用的功能性。