本博客探讨了如何利用 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. |
基于这些功能,我们将创建一个最小的设置;这是分步指南:
注意:您可以在我的仓库中找到本教程的内容 https://github.com/willyelm/react-app
作为设置的先决条件,我们需要安装 Node.js 和以下软件包:
我们的设置将使用 Express 处理路由,以提供静态文件、公共文件和 REST API。然后使用react-router-dom处理所有请求。一旦加载到浏览器中,我们的客户端捆绑包将水合我们的预渲染内容并使我们的组件具有交互性。这是这个想法的图表表示:
该图说明了 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 保持了最小化,它非常快(迄今为止最快的捆绑器)并且默认支持 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 静态和中间件方法来提供静态文件、处理服务器路由以及使用 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 元标记:
您还可以在我的仓库 willyelm/react-app 中找到本教程的源代码
该项目利用了react@19、react-router-dom@7等的最新功能来配置SSR。
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…
在本指南中,我们使用 esbuild 和 Express 构建了一个带有 SSR 的 React 应用程序,重点关注最小且灵活的设置。使用 React 19、React Router DOM 7 和 esbuild 等现代工具,我们实现了快速高效的工作流程,同时避免了大型框架的开销。
我们可以增强此实现,包括以下内容:
感谢您的阅读并祝您编码愉快。
以上是使用 React 和 esbuild 实现 SSR 的简单方法的详细内容。更多信息请关注PHP中文网其他相关文章!