首页 >web前端 >js教程 >使用 React 和 esbuild 实现 SSR 的简单方法

使用 React 和 esbuild 实现 SSR 的简单方法

Barbara Streisand
Barbara Streisand原创
2024-12-30 20:56:14657浏览

本博客探讨了如何利用 React 的最新功能,使用最少的工具和框架创建轻量级、灵活的 React 应用程序。虽然像 Next.js 和 Remix 这样的框架非常适合许多用例,但本指南适合那些有兴趣在保留类似功能的同时拥有完全控制权的人。

随着 React 19 的发布及其支持 SSR 的新功能,我决定尝试使用最少的工具创建一个支持 SSR 的 React 应用程序,但首先让我们探索 Next.js 提供的我们需要考虑的基本功能:

Feature Next.js
SSR (Server-Side Rendering) Built-in with minimal setup.
SSG (Static Site Generation) Built-in with getStaticProps.
Routing File-based routing.
Code Splitting Automatic.
Image Optimization Built-in with next/image.
Performance Optimizations Automatic (e.g., prefetching, critical CSS).
SEO Built-in tools like next/head.

基于这些功能,我们将创建一个最小的设置;这是分步指南:

目录

  • 设置
  • 配置 esbuild
  • Express 应用程序和路由
  • 反应应用程序
  • 奔跑
  • 后续步骤
  • 参考文献

注意:您可以在我的仓库中找到本教程的内容 https://github.com/willyelm/react-app

设置

作为设置的先决条件,我们需要安装 Node.js 和以下软件包:

  • react:基于组件的交互式 UI 库。
  • react-dom:使用 React 功能来水合 SSR 内容。
  • react-router-dom:使用 React 处理路由。
  • express:用于静态和 REST API 的 Node.js 简单服务器。
  • esbuild:转译 TypeScript 并捆绑 JS、CSS、SVG 和其他文件。
  • typescript:将打字功能添加到我们的源代码中。

我们的设置将使用 Express 处理路由,以提供静态文件、公共文件和 REST API。然后使用react-router-dom处理所有请求。一旦加载到浏览器中,我们的客户端捆绑包将水合我们的预渲染内容并使我们的组件具有交互性。这是这个想法的图表表示:

A Simple Approach to SSR with React and esbuild

该图说明了 Express 应用程序如何通过在服务器上预渲染 React 组件并返回 HTML 来处理请求。在客户端,React 水合这些预渲染组件以实现交互性。

记住这个图,让我们创建一个文件夹结构:

react-app/             # This will be our workspace directory.
  - public/            
  - scripts/           
    - build.js         # Bundle our server and client scripts.
    - config.js        # esbuild config to bundle.
    - dev.js           # Bundle on watch mode and run server.
  - src/
    - App/             # Our components will be here.
      - App.tsx        # The main application with browser routing.
      - Home.tsx.      # Our default page component.
      - NotFound.tsx   # Fallback page for unmatched routes.
    - index.tsx        # Hydrate our pre-rendered client app.
    - main.tsx         # Server app with SSR components.
    - style.css        # Initial stylesheet.   
  package.json
  tsconfig.json

让我们添加依赖项并设置 package.json:

{
  "name": "react-app",
  "type": "module",
  "devDependencies": {
    "@types/express": "^5.0.0",
    "@types/node": "^22.10.2",
    "@types/react": "^19.0.2",
    "@types/react-dom": "^19.0.2",
    "esbuild": "^0.24.2",
    "typescript": "^5.7.2"
  },
  "dependencies": {
    "express": "^4.21.2",
    "react": "^19.0.0",
    "react-dom": "^19.0.0",
    "react-router-dom": "^7.1.0"
  },
  "scripts": {
    "build": "node scripts/build",
    "dev": "node scripts/dev",
    "start": "node dist/main.js"
  }
}

注意:“type”:“module”属性是允许node.js运行ESM脚本所必需的。

由于我们将使用 Typescript,因此我们将配置一个 tsconfig.json 文件:

{
  "compilerOptions": {
    "esModuleInterop": true,
    "verbatimModuleSyntax": true,
    "noEmit": true,
    "resolveJsonModule": true,
    "skipLibCheck": true,
    "strict": true,
    "lib": ["DOM", "DOM.Iterable", "ES2022"],
    "target": "ES2022",
    "module": "ES2022",
    "moduleResolution": "bundler",
    "jsx": "react-jsx",
    "baseUrl": ".",
    "paths": {
      "src": [
        "./src/"
      ]
    }
  },
  "include": [
    "src"
  ],
  "exclude": [
    "node_modules"
  ]
}

配置 esbuild

为什么要使用 esbuild?与其他工具相比,esbuild 保持了最小化,它非常快(迄今为止最快的捆绑器)并且默认支持 typescript 和 esm。

在此设置中,我们将使用 esbuild 创建我们的开发和构建脚本,并转译客户端和服务器包。在本节中,我们将在工作区的脚本文件夹中工作。

scripts/config.js:此文件将包含客户端和服务器包的基本配置,该配置将为我们的脚本共享。

import path from 'node:path';
// Working dir
const workspace = process.cwd();
// Server bundle configuration
export const serverConfig = {
  bundle: true,
  platform: 'node', 
  format: 'esm',        // Support esm packages
  packages: 'external', // Omit node packages from our node bundle
  logLevel: 'error',
  sourcemap: 'external',
  entryPoints: {
    main: path.join(workspace, 'src', 'main.tsx') // Express app
  },
  tsconfig: path.join(workspace, 'tsconfig.json'),
  outdir: path.join(workspace, 'dist')
};

// Client bundle configuration
export const clientConfig = {
  bundle: true,
  platform: 'browser',
  format: 'esm',
  sourcemap: 'external',
  logLevel: 'error',
  tsconfig: path.join(workspace, 'tsconfig.json'),
  entryPoints: {
    index: path.join(workspace, 'src', 'index.tsx'), // Client react app
    style: path.join(workspace, 'src', 'style.css')  // Stylesheet
  },
  outdir: path.join(workspace, 'dist', 'static'),    // Served as /static by express
};

scripts/dev.js:此脚本捆绑了客户端和服务器应用程序,并在监视模式下运行主服务器脚本。

react-app/             # This will be our workspace directory.
  - public/            
  - scripts/           
    - build.js         # Bundle our server and client scripts.
    - config.js        # esbuild config to bundle.
    - dev.js           # Bundle on watch mode and run server.
  - src/
    - App/             # Our components will be here.
      - App.tsx        # The main application with browser routing.
      - Home.tsx.      # Our default page component.
      - NotFound.tsx   # Fallback page for unmatched routes.
    - index.tsx        # Hydrate our pre-rendered client app.
    - main.tsx         # Server app with SSR components.
    - style.css        # Initial stylesheet.   
  package.json
  tsconfig.json

使用此脚本,我们应该能够按照工作区中 package.json 中的配置运行 npm run dev。

scripts/build.js:与 dev 类似,但我们只需要启用 minify。

{
  "name": "react-app",
  "type": "module",
  "devDependencies": {
    "@types/express": "^5.0.0",
    "@types/node": "^22.10.2",
    "@types/react": "^19.0.2",
    "@types/react-dom": "^19.0.2",
    "esbuild": "^0.24.2",
    "typescript": "^5.7.2"
  },
  "dependencies": {
    "express": "^4.21.2",
    "react": "^19.0.0",
    "react-dom": "^19.0.0",
    "react-router-dom": "^7.1.0"
  },
  "scripts": {
    "build": "node scripts/build",
    "dev": "node scripts/dev",
    "start": "node dist/main.js"
  }
}

此脚本将通过运行 npm run build 并使用 npm start 运行应用程序来生成准备用于生产的 dist 包。

现在我们已经将 esbuild 配置为捆绑我们的节点和客户端应用程序,让我们开始创建一个 Express 服务器并实现 React SSR。

Express 应用程序和路由

这是一个简单的应用程序,使用 Express 静态和中间件方法来提供静态文件、处理服务器路由以及使用 React-router-dom 进行路由。

src/main.tsx:这是主要的 Node.js 应用程序,用于初始化服务器、使用 Express 处理路由并实现 React SSR。

{
  "compilerOptions": {
    "esModuleInterop": true,
    "verbatimModuleSyntax": true,
    "noEmit": true,
    "resolveJsonModule": true,
    "skipLibCheck": true,
    "strict": true,
    "lib": ["DOM", "DOM.Iterable", "ES2022"],
    "target": "ES2022",
    "module": "ES2022",
    "moduleResolution": "bundler",
    "jsx": "react-jsx",
    "baseUrl": ".",
    "paths": {
      "src": [
        "./src/"
      ]
    }
  },
  "include": [
    "src"
  ],
  "exclude": [
    "node_modules"
  ]
}

反应应用程序

React应用程序将使用react-router-dom处理路由,我们的应用程序将包含一个主页和一个NotFound页面来测试水合作用,我们将在主页上添加一个计数器按钮并利用React 19,我们将更新元标签标题和描述。

src/App/Home.tsx:一个非常小的 FunctionComponent。

import path from 'node:path';
// Working dir
const workspace = process.cwd();
// Server bundle configuration
export const serverConfig = {
  bundle: true,
  platform: 'node', 
  format: 'esm',        // Support esm packages
  packages: 'external', // Omit node packages from our node bundle
  logLevel: 'error',
  sourcemap: 'external',
  entryPoints: {
    main: path.join(workspace, 'src', 'main.tsx') // Express app
  },
  tsconfig: path.join(workspace, 'tsconfig.json'),
  outdir: path.join(workspace, 'dist')
};

// Client bundle configuration
export const clientConfig = {
  bundle: true,
  platform: 'browser',
  format: 'esm',
  sourcemap: 'external',
  logLevel: 'error',
  tsconfig: path.join(workspace, 'tsconfig.json'),
  entryPoints: {
    index: path.join(workspace, 'src', 'index.tsx'), // Client react app
    style: path.join(workspace, 'src', 'style.css')  // Stylesheet
  },
  outdir: path.join(workspace, 'dist', 'static'),    // Served as /static by express
};

src/App/NotFound.tsx:未找到页面时的默认 FunctionComponent。

import { spawn } from 'node:child_process';
import path from 'node:path';
import { context } from 'esbuild';
import { serverConfig, clientConfig } from './config.js';
// Working dir
const workspace = process.cwd();
// Dev process
async function dev() {
  // Build server in watch mode
  const serverContext = await context(serverConfig);
  serverContext.watch();
  // Build client in watch mode
  const clientContext = await context(clientConfig);
  clientContext.watch();
  // Run server
  const childProcess = spawn('node', [
    '--watch',
    path.join(workspace, 'dist', 'main.js')
  ], {
    stdio: 'inherit'
  });
  // Kill child process on program interruption
  process.on('SIGINT', () => {
    if (childProcess) {
      childProcess.kill();
    }
    process.exit(0);
  });
}
// Start the dev process
dev();

src/App/App.tsx:使用react-router-dom设置我们的应用程序。

import { build } from 'esbuild';
import { clientConfig, serverConfig } from './config.js';
// build process
async function bundle() {
  // Build server
  await build({
    ...serverConfig,
    minify: true
  });
  // Build client
  await build({
    ...clientConfig,
    minify: true
  });
}
// Start the build process
bundle();

跑步

最后,配置了 esbuild 脚本,设置了 Express 服务器,并实现了 React SSR。我们可以运行我们的服务器:

import path from 'node:path';
import express, { type Request, type Response } from 'express';
import { renderToPipeableStream } from 'react-dom/server';
import { StaticRouter } from 'react-router';
import { App } from './App/App';

const app = express(); // Create Express App
const port = 3000; // Port to listen
const workspace = process.cwd(); // workspace
// Serve static files like js bundles and css files
app.use('/static', express.static(path.join(workspace, 'dist', 'static')));
// Server files from the /public folder
app.use(express.static(path.join(workspace, 'public')));
// Fallback to render the SSR react app
app.use((request: Request, response: Response) => {
  // React SSR rendering as a stream
  const { pipe } = renderToPipeableStream(
    <html lang="en">
      <head>
        <meta charSet="UTF-8" />
        <link rel='stylesheet' href={`/static/style.css`} />
      </head>
      <body>
        <base href="/" />
        <div>


<p>src/index.tsx: On the client side to activate our components and make them interactive we need to "hydrate".<br>
</p>
<pre class="brush:php;toolbar:false">import { hydrateRoot } from 'react-dom/client';
import { BrowserRouter } from 'react-router';
import { App } from './App/App';
// Hydrate pre-renderer #app element
hydrateRoot(
  document.getElementById('app') as HTMLElement,
  <BrowserRouter>
    <App />
  </BrowserRouter>
);

这将显示一条消息 [app] 正在端口 3000 上侦听。导航到
http://localhost:3000 来查看一下。

测试 SSR 和 SEO 元标记:

A Simple Approach to SSR with React and esbuild

您还可以在我的仓库 willyelm/react-app 中找到本教程的源代码

A Simple Approach to SSR with React and esbuild 威廉 / 反应应用程序

使用 SSR 和 esbuild 的简单 React 应用程序设置

该项目利用了react@19、react-router-dom@7等的最新功能来配置SSR。

依赖关系

  • react:基于组件的交互式 UI 库。
  • react-dom:使用 React 功能来水合 SSR 内容。
  • react-router-dom:使用 React 处理路由。
  • express:用于静态和 REST API 的 Node.js 简单服务器。
  • esbuild:转换 TypeScript 并捆绑 JS、CSS、SVG 和其他文件。
  • typescript:将打字功能添加到我们的源代码中。

项目结构

react-app/             # This will be our workspace directory.
  - public/            
  - scripts/           
    - build.js         # Bundle our server and client scripts.
    - config.js        # esbuild config to bundle.
    - dev.js           # Bundle on watch mode and run server.
  - src/
    - App/             # Our components will be here.
      - App.tsx        # The main application with browser routing.
      - Home.tsx.      # Our default page component.
      - NotFound.tsx   # Fallback page for unmatched routes.
    - index.tsx        # Hydrate our pre-rendered client app.
    - main.tsx         # Server app with SSR components.
    - style.css        # Initial stylesheet.   
  package.json
  tsconfig.json
进入全屏模式 退出全屏模式
在 GitHub 上查看

下一步

在本指南中,我们使用 esbuild 和 Express 构建了一个带有 SSR 的 React 应用程序,重点关注最小且灵活的设置。使用 React 19、React Router DOM 7 和 esbuild 等现代工具,我们实现了快速高效的工作流程,同时避免了大型框架的开销。

我们可以增强此实现,包括以下内容:

  • TailwindCSS:配置 PostCSS 和 esbuild 以支持 tailwindcss 样式。
  • Jest:使用 Jest 和 @testing-library 添加单元测试。
  • 剧作家:与剧作家一起配置端到端测试。
  • SSG:实现SSG(静态站点生成)以提高性能和服务器负载。
  • HMR:开发模式下使用esbuild支持HMR(热模块替换),提高效率。

感谢您的阅读并祝您编码愉快。

参考

  • 反应 19
  • React 路由器 DOM
  • 反应 SSR
  • Express.js
  • 打字稿
  • esbuild

以上是使用 React 和 esbuild 实现 SSR 的简单方法的详细内容。更多信息请关注PHP中文网其他相关文章!

声明:
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn