不使用框架的服务器端渲染和服务器端数据提取的演示 - 展示 React 驱动的框架实际做什么。
在这篇文章中,我将解释如何在 React 中启用服务器端渲染和服务器端数据提取......无需使用框架!
虽然这篇文章中的代码并不是我所说的“可用于生产”,但它应该有助于解释 React 的两个内置方法hydrateRoot
和renderToString
,这两个方法都是在 React 中启用服务器端渲染所必需的。
如果您有兴趣快速浏览,可以在以下 GitHub 存储库中查看本文中使用的所有代码。
-
https://github.com/PaulieScanlon/simple-react-ssr-vite-express
两个小警告:
-
我不会介绍如何部署 React SSR 应用程序。
-
我将要解释的大部分内容都可以在 Vite 文档中找到:服务器端渲染[https://vitejs.dev/guide/ssr.html]。
设置并安装依赖项
您需要做的第一件事是初始化一个新的 npm 包。(-y 标志会跳过问卷调查并在创建 package.json 时使用 npm 默认值)
npm init -y
现在您可以安装依赖项。
npm install react react-dom express
最后,安装开发依赖项。
npm install vite @vitejs/plugin-react -D
将脚本添加到 package.json
您需要添加五个脚本。一个用于开发,其余四个用于创建生产版本,还有一个服务脚本,以便您可以在浏览器中预览生产版本。
// package.json "scripts": { "dev": "node server-dev.js", "build:client": "vite build --outDir dist/client", "build:server": "vite build --ssr src/entry-server.jsx --outDir dist/server", "build": "npm run build:client && npm run build:server", "serve": "node server-prod.js", ... },
-
dev。该脚本启动 Vite 开发服务器。
-
build:client。此脚本捆绑了 index.html 和 entry-client.jsx。
-
build:server。此脚本捆绑了 entry-server.jsx。
-
build。此脚本运行上述两个“build:”脚本。
-
服务:此脚本运行 server-prod.js(我稍后会解释这是什么)>
将 type:module 添加到 package.json
Vite 的开发服务器使用原生 ES 模块,因此您需要将“type” : “module” 添加到您的 package.json。如果不这样做,您可能会看到与以下内容相关的错误:无法在模块外使用 import 语句。
// package.json { "name": "...", "type": "module", "scripts": { ... }, }
创建 src 文件
index.html
在项目的根目录下创建一个名为 index.html 的文件。它充当应用程序的“模板”。此文件中有两点需要注意。
-
”app” 的 div id 是 React 在调用 hydrateRoot 时使用的目标 DOM 节点。
-
<!–outlet–> 的注释被服务器用 React 的 renderToString 函数的结果替换。
//index.html <html lang='en'> <head> <meta charset='UTF-8' /> <meta name='viewport' content='width=device-width, initial-scale=1.0' /> <title>Simple React SSR Vite Express</title> </head> <body> <div id='app'><!--outlet--></div> <script type='module' src='/src/entry-client.jsx'></script> </body> </html>
app.jsx
在项目根目录创建一个 src 目录,然后创建一个名为 app.jsx 的文件。
这是一个简单的函数组件,它返回一些要由浏览器呈现的基本 HTML。该组件使用导出默认语法。
// src/app.jsx import { useState } from 'react'; const App = () => { const [count, setCount] = useState(0); return ( <main> <h1>App</h1> <p>Lorem Ipsum</p> <div> <div>{count}</div> <button onClick={() => setCount(count + 1)}>Count</button> </div> </main> ); }; export default App
entry-client.jsx
在 src 目录中创建一个名为 entry-client.jsx 的文件。此文件负责显示 id 为 app 的 div 中的 <App /> 组件。
您可以在此处的 React 文档中阅读有关 hydrateRoot 的更多信息:hydrateRoot。
//src/entry-client.jsx import { hydrateRoot } from 'react-dom/client'; import App from './app'; hydrateRoot(document.getElementById('app'), <App />);
entry-server.jsx
在 src 目录中创建一个名为 entry-server.jsx 的文件。此文件负责将 <App /> 组件“转换”为适合在浏览器中使用的纯 HTML 字符串。
你可以在 React 文档中阅读有关 renderToString 的更多信息:renderToString。此文件导出一个名为 render 的命名函数。
//src/entry-server.jsx import { renderToString } from 'react-dom/server'; import App from './app'; export const render = () => { return renderToString(<App />); };
Vite 配置
在项目根目录创建一个名为 vite.config.js 的文件并添加以下代码片段。
//vite.config.js import { defineConfig } from 'vite'; import react from '@vitejs/plugin-react'; export default defineConfig({ plugins: [react()], });
创建开发服务器
在项目根目录中创建一个名为 server-dev.js 的文件。这是运行 npm run dev 时启动的服务器。
//server-dev.js import fs from 'fs'; import express from 'express'; import { createServer } from 'vite'; const app = express(); const vite = await createServer({ server: { middlewareMode: true, }, appType: 'custom', }); app.use(vite.middlewares); app.use('*', async (req, res) => { const url = req.originalUrl; try { const template = await vite.transformIndexHtml(url, fs.readFileSync('index.html', 'utf-8')); const { render } = await vite.ssrLoadModule('/src/entry-server.jsx'); const html = template.replace(`<!--outlet-->`, render); res.status(200).set({ 'Content-Type': 'text/html' }).end(html); } catch (error) { res.status(500).end(error); } }); app.listen(4173, () => { console.log('http://localhost:4173.'); });
应用程序=快递()
正如您所期望的,这是一个 Express 的实例,通常将其定义为名为 app 的 const。
创建服务器
这将创建一个 Vite 开发服务器,需要额外的配置以便 Vite 知道将控制权移交给 express。
应用程序.使用(vite.中间件)
这可确保任何表达请求都能传回 Vite 开发服务器。
应用程序.使用('*')
express 应用程序处理所有传入的请求,每个请求的 url 都可以从 req 对象中提取,并且在 Vite 转换 index.html 时需要它。
模板
模板是页面的起点。它由服务器上的 render 函数使用 app.jsx 中的 HTML 填充,然后使用 app.jsx 中的相同 HTML 在浏览器中再次填充(或水化)。
{ 使成为 }
如上所述,该函数负责将 React 代码“转换”为纯 HTML 字符串。
html
这就是所有内容整合在一起的地方。使用 .replace,您可以定位 index.html 并将其替换为渲染函数的返回值。
.结束(html)
使用标准的 .end() Express 方法您可以返回状态 200,设置内容类型,然后传入 HTML 以在浏览器中显示。
这些是在服务器上渲染 React 的基本原理;但由于 Vite 是一个开发工具,您必须进行一些更改才能创建一个部署后可以运行的 Express 服务器。
创建生产服务器
在项目根目录中创建一个名为 server-prod.js 的文件。这是运行 npm run serve 时启动的服务器。生产服务器与开发服务器非常相似,但也有一些明显的区别。
//server-prod.js import fs from 'fs'; import path from 'path'; import { fileURLToPath } from 'url'; import express from 'express'; const app = express(); app.use(express.static(path.resolve(path.dirname(fileURLToPath(import.meta.url)), 'dist/client'), { index: false })); app.use('*', async (_, res) => { try { const template = fs.readFileSync('./dist/client/index.html', 'utf-8'); const { render } = await import('./dist/server/entry-server.js'); const html = template.replace(`<!--outlet-->`, render); res.status(200).set({ 'Content-Type': 'text/html' }).end(html); } catch (error) { res.status(500).end(error); } }); app.listen(5173, () => { console.log('http://localhost:5173.'); });
没有 Vite
Vite 仅在开发模式下使用。当您运行 npm run build 时,Vite 会在后台使用 Rollup 编译所有必需的文件并将其输出到 npm run build 目录。因此,在生产环境中无需使用 Vite 的 createServer。
express.static
express.static() 函数是内置中间件,可用于提供 React 在浏览器中运行所需的静态文件(HTML、.js)。
模板和 { render }
这些与开发服务器大致相同,但 Vite 特定方法(transformIndexHtml 和 ssrLoadModule)已被删除。路径现在指向 ./dist 目录,而不是 src 文件。
这就是这篇文章的第一部分,但还缺少一部分……
服务器端数据获取
具有服务器端渲染功能的 React 应用还可以利用服务器端日期获取功能。这样做有两个好处。
-
服务器端请求可用于与数据库建立安全连接(例如)。
-
即使在禁用 JavaScript 时或在发生水合之前,来自服务器端请求的数据仍将显示在浏览器中。
为了使其正常工作,需要进行相当多的更改,我会解释每个更改是什么;但是,如果您希望在 GitHub 上看到它们,我已经在以下链接上准备了一个包含所有更改的拉取请求。
package.json
添加一个名为“build:function”的新脚本,并将其指向新的 function.js 文件(您将在下一步创建该文件)。然后修改构建脚本以包含 && npm run build:function。
//package.json "scripts": { ... + "build:function": "vite build --ssr src/function.js --outDir dist/function", - "build": "npm run build:client && npm run build:server", + "build": "npm run build:client && npm run build:server && npm run build:function", ... }
function.js
在 src 目录中创建一个名为 function.js 的文件。此文件包含一个名为 getServerData 的异步函数,该函数将从服务器调用。
//src/function.js export const getServerData = async () => { const response = await fetch('https://dummyjson.com/products/1'); const data = await response.json(); return data; };
serve-dev.js
此处的更改涉及导入新的 function.js 文件、调用 getServerData 异步函数,然后将数据传回渲染函数。还需要创建一个脚本元素,该元素将使用新获取的服务器数据填充 window.__data__。将数据添加到窗口将允许 React 在刷新页面时访问与服务器相同的数据。
//server-dev.js app.use('*', async (req, res) => { const url = req.originalUrl; try { const template = await vite.transformIndexHtml(url, fs.readFileSync('index.html', 'utf-8')); const { render } = await vite.ssrLoadModule('/src/entry-server.jsx'); + const { getServerData } = await vite.ssrLoadModule('/src/function.js'); + const data = await getServerData(); + const script = `<script>window.__data__=${JSON.stringify(data)}</script>`; - const html = template.replace(`<!--ssr-outlet-->`, render); + const html = template.replace(`<!--outlet-->`, `${render(data)} ${script}`); res.status(200).set({ 'Content-Type': 'text/html' }).end(html); } catch (error) { res.status(500).end(error); } });
serve-prod.js
这里的更改与对开发服务器所做的更改几乎相同,除了可以找到 function.js 的路径之外。
app.use('*', async (_, res) => { try { const template = fs.readFileSync('./dist/client/index.html', 'utf-8'); const { render } = await import('./dist/server/entry-server.js'); + const { getServerData } = await import('./dist/function/function.js'); + const data = await getServerData(); + const script = `<script>window.__data__=${JSON.stringify(data)}</script>`; - const html = template.replace(`<!--outlet-->`, render); + const html = template.replace(`<!--outlet-->`, `${render(data)} ${script}`); res.status(200).set({ 'Content-Type': 'text/html' }).end(html); } catch (error) { res.status(500).end(error); } });
entry-client.jsx
此处的更改是定义一个名为 data 的新变量,然后将其设置为等于 window.__data__ 的值。现在 data 变量包含服务器端请求的数据,可以通过名为 data 的 prop 将其传递给 <App /> 组件。
//entry-client.jsx import { hydrateRoot } from 'react-dom/client'; import App from './app'; + let data; + if (typeof window !== 'undefined') { + data = window.__data__; + } - hydrateRoot(document.getElementById('app'), <App />); + hydrateRoot(document.getElementById('app'), <App data={data} />);
entry-server.jsx
entry-server.jsx 的情况类似;但这一次,您无需从窗口对象中获取数据,而是可以在使用 data 参数调用渲染函数时访问从服务器传递的数据。然后可以使用相同的方法通过名为 data 的 prop 将数据传递给 <App /> 组件。
//entry-server.jsx import { renderToString } from 'react-dom/server'; import App from './app'; - export const render = () => { + export const render = (data) => { - return renderToString(<App />); + return renderToString(<App data={data} />); };
app.jsx
最后要做的更改是解构新的数据道具并将其返回到 HTML <pre> 元素中,以便它在页面上可见。
//src/app.jsx import { useState } from 'react'; - const App = () => { + const App = ({ data }) => { const [count, setCount] = useState(0); return ( <main> ... + <pre>{JSON.stringify(data, null, 2)}</pre> </main> ); }; export default App;
总结
就这样,您无需使用框架即可实现服务器端渲染和服务器端数据提取!
我从 2017 年左右开始使用 React,但从未真正理解过 hydrateRoot 或 renderToString,但通过这个示例项目,我现在对 React 的实际工作原理有了更好的理解,并且更加欣赏 React 驱动的框架的实际作用。
Vite 也给我留下了深刻的印象——从文档到开发者体验,一切都是一流的。另外,如果您错过了,Remix 刚刚宣布了他们的新 Vite 插件;如果 Remix 团队正在使用 Vite,这是一个很好的迹象,表明 Vite 的每一部分都和它看起来一样好!
网友评论文明上网理性发言 已有0人参与
发表评论: