51工具盒子

依楼听风雨
笑看云卷云舒,淡观潮起潮落

如何使用 Vite 和 Express 构建服务器端 React 应用

不使用框架的服务器端渲染和服务器端数据提取的演示 - 展示 React驱动的框架实际做什么。

如何使用 Vite 和 Express 构建服务器端 React 应用

在这篇文章中,我将解释如何在 React 中启用服务器端渲染和服务器端数据提取......无需使用框架!

虽然这篇文章中的代码并不是我所说的"可用于生产",但它应该有助于解释 React 的两个内置方法hydrateRootrenderToString,这两个方法都是在 React 中启用服务器端渲染所必需的。

如果您有兴趣快速浏览,可以在以下 GitHub 存储库中查看本文中使用的所有代码。

  • https://github.com/PaulieScanlon/simple-react-ssr-vite-express

两个小警告:

  1. 我不会介绍如何部署 React SSR 应用程序。

  2. 我将要解释的大部分内容都可以在 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",
  ...
},
  1. dev。该脚本启动 Vite 开发服务器。

  2. build:client。此脚本捆绑了 index.html 和 entry-client.jsx。

  3. build:server。此脚本捆绑了 entry-server.jsx。

  4. build。此脚本运行上述两个"build:"脚本。

  5. 服务:此脚本运行 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 的文件。它充当应用程序的"模板"。此文件中有两点需要注意。

  1. "app" 的 div id 是 React 在调用 hydrateRoot 时使用的目标 DOM 节点。

  2. <!--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 应用还可以利用服务器端日期获取功能。这样做有两个好处。

  1. 服务器端请求可用于与数据库建立安全连接(例如)。

  2. 即使在禁用 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)
未经允许不得转载:工具盒子 » 如何使用 Vite 和 Express 构建服务器端 React 应用