React SSR同构渲染方案是什么?

一、背景

目前主流的前端架构分为SSRCSRSSG,比较适合首屏直出的方案除了CSR都还不错,因为服务端会直接返回路由对应的html + css,浏览器直接解析DOM即可,而水合的作用是什么?服务端首次返回的是静态页面,页面需要”动“起来的话,则需要水合,即将页面所需的JS引入并加载、给DOM绑定交互等等,核心的API即ReactDOM.hydrateRoot

二、同构渲染

这样服务端渲染一次、客户端渲染一次的流程也称为同构渲染,一般情况是通过Nodejs实现文件服务,基于组件文件通过React Server API——renderToString,就像这样:

import { renderToString } from 'react-dom/server';
import App from './App';console.log(renderToString(<App/>));

而在客户端接收到服务端渲染所生成的html string后,基于客户端水合API——react DOM API——hydrateRoot进行组件与静态标签的关联,就像这样:

import React from 'react';
import { hydrateRoot } from 'react-dom/client';
import './index.css';
import App from './App';hydrateRoot(document.getElementById('root'), <App/>);

三、手写一个SSR

基于同构渲染的秘密,我们解开了,那手写一个基础的SSR服务其实很简单,我们可以把整个加载链路拆解成三步:

  1. 起一个本地express服务,用于按路由返回html
  2. 前端构建时约定式路由/pages,解析每个路由下的根组件;生成html模板;
  3. webpack构建工程代码,产出bundle.js
  4. 前端项目中引入bundle.js,进行同构渲染水合逻辑;

整体的项目工程如下:

├── .next    // 生成文件服务html、构建结果
├── src
│ ├── pages // 页面
│ │ ├── a.jsx // 页面A
│ │ ├── b.jsx // 页面B
│ │ ├── c.jsx // 页面C
│ ├── client.js // 水合
├── generateHtml.js // 路由转换html能力
├── server.js // 文件服务
├── teamplate.html // html基础模板
├── webpack.config.js
└── package.json

3.1、文件服务搭建

我们起一个简单的工程项目,并安装基础依赖。

mkdir ssr-demo
cd ssr-demo
npm init -y
npm i express react react-dom webpack webpack-cli fs-extra

express服务代码如下:

const express = require("express");
const path = require("path");// 定义根目录
const rootDir = path.resolve(__dirname, "./");
const outputDir = path.join(rootDir, ".next");const app = express();
const PORT = process.env.PORT || 3000;// 提供 .next 目录的静态文件服务
app.use(express.static(outputDir));// 启动服务器
app.listen(PORT, () => {console.log(`Server is running on http://localhost:${PORT}`);
});

代码中启用了静态文件服务,在路由解析服务实现后,每次项目构建阶段都会在.nest路由生成所有页面的html,用于在服务端直接返回。

3.2、路由解析服务

接下来我们实现服务端核心部分,将所有路由的组件解析成html模板,在此之前需要有一个基础的html模板,用于动态插入组件部分的标签,html模板如下:

<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8" /><title>My App</title></head><body data-component="{{componentName}}">{{content}}<script src="/client.bundle.js" defer></script></body>
</html>

其中content是实际渲染的组件标签、componentName用于在后续水合阶段定位组件、script用于执行水合逻辑。

遍历生成所有html文件的代码如下:

// src/generate.js
require("@babel/register")({presets: ["@babel/preset-env", "@babel/preset-react"],
});const fs = require("fs-extra"); // 使用 fs-extra 方便处理文件
const path = require("path");
const React = require("react");
const ReactDOMServer = require("react-dom/server");// 定义根目录
const rootDir = path.resolve(__dirname, "./");
const pagesDir = path.join(rootDir, "src/pages");
const outputDir = path.join(rootDir, ".next");// 生成 HTML 文件
const generateHtmlFiles = () => {return new Promise((resolve, reject) => {fs.readdir(pagesDir, (err, files) => {if (err) {reject("Error reading pages directory");return;}const promises = files.map((file) => {const filePath = path.join(pagesDir, file);const fileExt = path.extname(file);// 只处理以 .jsx 结尾的文件if (fileExt === ".jsx") {const pageName = path.basename(file, fileExt);const PageComponent = require(filePath).default; // 导入组件const renderedContent = ReactDOMServer.renderToString(React.createElement(PageComponent));const templatePath = path.resolve(__dirname, "template.html"); // Load HTML templatereturn new Promise((resolve, reject) => {fs.readFile(templatePath, "utf8", (err, template) => {if (err) {console.error("Error loading template:", err);reject(err);return;}const html = template.replace("{{content}}", renderedContent).replaceAll("{{componentName}}", pageName); // 注入组件名称// 构造输出路径,放在以页面名字为名的文件夹中const outputDirForPage = path.join(outputDir, pageName);const outputFilePath = path.join(outputDirForPage, "index.html");fs.ensureDirSync(outputDirForPage); // 确保页面目录存在fs.outputFileSync(outputFilePath, html, "utf8");console.log(`Generated HTML for ${pageName}: ${outputFilePath}`);resolve();});});}return Promise.resolve(); // 对于不是 .jsx 文件的情况});Promise.all(promises).then(() => resolve("HTML generation completed!")).catch(reject); // 处理生成过程中的异常});});
};// 直接调用生成函数并输出结果
generateHtmlFiles().then((message) => {console.log(message);process.exit(0); // 结束进程}).catch((err) => {console.error(err);process.exit(1); // 发生错误,结束进程});

3.3、webpack打包前端工程

这里比较好理解,整个前端客户端代码也需要打包,包括水合的代码,我们基于webpack构建,初始化一个基础的打包配置:

const path = require("path");module.exports = {entry: path.resolve(__dirname, 'src', 'client.js'),output: {path: path.resolve(__dirname, ".next"), // 输出到 .next 目录filename: "client.bundle.js", // 根据入口名称生成文件名publicPath: "/", // 公开路径},module: {rules: [{test: /\.(js|jsx)$/,exclude: /node_modules/,use: {loader: "babel-loader",options: {presets: ["@babel/preset-env", "@babel/preset-react"],},},},],},resolve: {extensions: [".js", ".jsx"],},mode: "production", // 可以根据需要设置为 'development'
};

配置完webpack之后,我们前三步的流程可以串起来了,这个效果和webpack dev server比较类似,我们基于流程顺序,配置对应的package.json scripts

  "scripts": {"build": "webpack --config webpack.config.js","generate": "node generateHtml.js","server": "node server.js","start": "npm run build && npm run generate && npm run server"},
  1. build负责构建前端工程;
  2. generate负责生成所有页面的html;
  3. server负责创建最终的文件服务;

3.4、渲染的终点——水合

这段代码运行时,服务端已经返回html文件,此时是同构渲染的终点,通过hydrateRoot API将服务端的标签在浏览器水合,让页面组件动起来即可。

实现代码:

// // src/client.js
import React from "react";
import ReactDOM from "react-dom/client";console.log(React);
const hydrateComponent = (componentName) => {// 动态 import 组件import(`./pages/${componentName}.jsx`).then(({ default: Component }) => {const domContainer = document.getElementById(componentName);if (domContainer) {ReactDOM.hydrateRoot(<Component />, domContainer);} else {console.error(`No container found for component ${componentName}`);}}).catch((error) => {console.error("Error loading component:", error);});
};// 从 HTML 中提取组件名称,然后进行水合
document.addEventListener("DOMContentLoaded", () => {const componentName = document.body.getAttribute("data-component");hydrateComponent(componentName);
});

至此整个demo就完成了。

大致的效果如下:

在这里插入图片描述

结尾

本文希望对于原本不是很了解SSR、SSG方案的同学可以快速的理解与传统单页应用的区别,并且可以基于这个demo,了解到服务端渲染、webpack dev server的工作原理。

对于实现同构渲染的方案有建议的同学,欢迎评论探讨。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/89067.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【1/2, 2/3, 3/5, 5/8, 8/13, ...写一个函数,计算以下数列的前10项之和,在主函数中调用该函数并输出结果。】2022-5-19

缘由这个c程序怎么编写呀-编程语言-CSDN问答 int fm 2, fz 1, zz 0, x 0; double jg 0;while (x < 10)cout << x << ":" << fz << "/" << fm << "\n",jg 1.0*fz / fm, zz fz, fz fm, fm zz;cou…

基于51单片机的简易售货机系统

目录 具体实现功能 设计介绍 资料内容 全部内容 资料获取 具体实现功能 具体功能&#xff1a; &#xff08;1&#xff09;售货机上电后显示商品信息&#xff0c;具有5种商品&#xff0c;可以按键选择商品&#xff0c;选择后进入付款界面&#xff1b; &#xff08;2&#…

线性回归原理推导与应用(九):逻辑回归多分类问题的原理与推导

普通的逻辑回归只能针对二分类问题&#xff0c;也就是分类结果为是和不是&#xff0c;好和不好等问题&#xff0c;而实际应用中还会有多分类的问题&#xff0c;例如金融行业信用评分中需要将用户分为好&#xff0c;中&#xff0c;差三类&#xff0c;对企业信用评级要划分为低风…

JPA将大数据量的Excel文件导入到数据库中

在日常的数据处理中&#xff0c;经常碰到Excel的数据&#xff0c;需要将Excel的数据导入到数据库中。 Excel数据的预处理 将sheet分拆为不同的文件 如果一个文件中有太多的sheet&#xff0c;在处理的时候&#xff0c;对计算机的内存要求很高&#xff0c;为了避免内存崩溃。预…

异步爬虫---

代码结构分析 这是一个同步新闻爬虫程序&#xff0c;主要包含以下几个部分&#xff1a; 们把爬虫设计为一个类&#xff0c;类在初始化时&#xff0c;连接数据库&#xff0c;初始化logger&#xff0c;创建网址池&#xff0c;加载hubs并设置到网址池。 爬虫开始运行的入口就是r…

04 dnsmasq 的环境搭建

前言 这里介绍一下 dnsmasq 的调试环境的搭建 这是一种比较常见的开远的 dns 服务器 git 仓库在 git://thekelleys.org.uk/dnsmasq.git dnsmasq 的编译 首先是 clone 代码, git clone git://thekelleys.org.uk/dnsmasq.git 然后是开始编译 dnsmasq 最顶层的目录结构如下…

visual studio2019+vcpkg管理第三方库

下载 vcpkg官方教程 git clone https://github.com/microsoft/vcpkg.git cd vcpkg && bootstrap-vcpkg.bat注意&#xff1a;git clone一直失败&#xff0c;我直接在github仓库下载的zip&#xff0c;一样可以 设置环境变量 E:\software\vcpkg 安装你想要的库 比如…

logger2js - JavaScript日志与调试工具库

logger2js - JavaScript日志与调试工具库 logger2js是一个功能强大的前端JavaScript日志与调试工具库&#xff0c;提供了丰富的日志输出、性能测试和代码调试功能。该库支持配置化引入&#xff0c;包含5种皮肤风格和丰富的API接口&#xff0c;如 a l e r t 增强方法、 alert增…

影像组学5:Radiomics Score的计算

Rad-score&#xff08;全称 Radiomics score&#xff0c;影像组学评分&#xff09;是通过数学模型将影像组学提取的多个特征整合为一个综合性指标&#xff0c;从而简化临床分析与决策。 前文已介绍影像组学的病灶分割、特征提取及筛选流程&#xff0c;本节将重点阐述 Rad-scor…

性能测试——搭建Prometheus+Grafana平台(超详细版)

一、搭建influxdb prometheus grafana Jmeter监控平台 1、目的&#xff1a;对性能测试的结果进行持久化存储。 2、每个组件介绍 Jmeter&#xff1a;性能测试工具&#xff0c;可以收集到服务器的性能测试指标&#xff1a;统计TPS、响应时间、线程数、错误率等信息。influxd…

微服务--nacos+feign

微服务使用到了我们的多模块开发&#xff0c;父级工程可以在modules管理子模块 子模块中也会定义父模块 1. Nacos注册中心 Nacos已成为Java微服务生态的事实标准组件&#xff0c;在2023年中国Java开发者调研中占比达62%。其优势在于将服务发现与配置管理统一&#xff0c;显著降…

基于Python的二手房源信息爬取与分析的设计和实现,7000字论文编写

摘要 本文设计并实现了一个基于 Python 的二手房源信息爬取与分析系统。该系统通过网络爬虫技术自动从房地产网站获取二手房源信息&#xff0c;经过数据清洗、存储后进行多维度分析&#xff0c;并通过可视化界面展示分析结果。系统采用模块化设计&#xff0c;包括爬虫模块、数…

力扣HOT100之栈:739. 每日温度

这道题是单调栈的一个经典应用&#xff0c;这里我们使用单调递减的栈&#xff08;从栈底到栈顶单调递减&#xff09;来实现&#xff0c;首先我们创建一个与temperatures大小一致的全0数组result&#xff0c;然后我们通过一个for循环&#xff0c;通过下标访问的方式遍历所有元素…

vue3 报错Missing semicolon

快速定位问题&#xff1a; 一、在git中对比改动&#xff0c;实在不行重置。 二、找分号或逗号 三、误碰键盘&#xff0c;多了空格or一些字母&#xff0c;删除即可。如下

在pyCharm中创建新的conda环境

在conda中创建pychars环境 打开 CMD 或 PowerShell 或 Anaconda Prompt 输入以下命令&#xff1a; conda create -n pychars python3.10你可以把 3.10 换成你需要的 Python 版本&#xff0c;如 3.9、3.11 等。 创建完成后激活环境&#xff1a; conda activate pychars在 Py…

如何确定某个路由器的路由表?(计算机网络)

以下题为例 题目说要路由表关键是目的网络地址和下一跳地址 那么我们第一步先确定目的网络地址。 一共有四个网络&#xff0c;即有四个目的网络地址&#xff1a;15.0.0.0 20.0.0.0 30.0.0.0 40.0.0.0 下一跳地址就是去往目地网络的下一个ip地址 。 我们这里是要的…

ubuntu 系统 多条命令通过 bash 脚本执行

ubuntu 系统 多条命令通过 bash 脚本执行。 1、新建sh脚本。 vim run.sh 2、示例命令&#xff0c;写入run.sh文件内&#xff0c;具体命令如下: #!/bin/bash# 切换到指定目录 cd /work_space/build/bin# 执行程序 ./demo 3、给sh脚本权限。 chmod x run.sh 4、执行sh脚本…

《拆解问题的技术》笔记

思维导图 拆解问题的技术 拆解职场难题 拆解项目难题 拆解简报企划难题 拆解学习难题 拆解人生难题

langChain构建ChatRobot(1)—基础对话

摘要&#xff1a;本文介绍利用langChain核心组件Models里的Chat Models构建基本的Chatbot&#xff0c;能实现简单的问答。 文章目录 概述Chat Model1. message对象1.1 消息类型介绍&#xff1a;1.2 使用场景&#xff1a; 2. 利用ChatModel构建简单的 Chatbot2.1 实现基本的问答…

行为模式-迭代器模式

定义&#xff1a; Provide a way to access the elements of an aggregate object sequentially without exposing its underlying representation.&#xff08;它提供一种方法访问一个容器对象中各个元素&#xff0c;而又不需暴露该 对象的内部细节。&#xff09; 迭代器模式通…