名称: Bun React SSR 描述: 当使用 Bun 构建服务器端渲染的 React 应用时使用,包括流式 SSR、hydration、renderToString 或无需框架的自定义 SSR。 版本: 1.0.0
Bun React SSR
使用 Bun 构建自定义服务器端渲染的 React 应用。
快速开始
# 初始化项目
mkdir my-ssr-app && cd my-ssr-app
bun init
# 安装依赖
bun add react react-dom
bun add -D @types/react @types/react-dom
基本 SSR 设置
服务器入口
// src/server.tsx
import { renderToString } from "react-dom/server";
import App from "./App";
Bun.serve({
port: 3000,
async fetch(req) {
const url = new URL(req.url);
// 提供静态文件
if (url.pathname.startsWith("/static/")) {
const file = Bun.file(`./public${url.pathname}`);
if (await file.exists()) {
return new Response(file);
}
}
// 渲染 React 应用
const html = renderToString(<App url={url.pathname} />);
return new Response(
`<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>React SSR</title>
</head>
<body>
<div id="root">${html}</div>
<script src="/static/client.js"></script>
</body>
</html>`,
{
headers: { "Content-Type": "text/html" },
}
);
},
});
console.log("服务器运行在 http://localhost:3000");
客户端入口
// src/client.tsx
import { hydrateRoot } from "react-dom/client";
import App from "./App";
hydrateRoot(
document.getElementById("root")!,
<App url={window.location.pathname} />
);
React 应用
// src/App.tsx
interface AppProps {
url: string;
}
export default function App({ url }: AppProps) {
return (
<div>
<h1>使用 Bun 的 React SSR</h1>
<p>当前路径: {url}</p>
<button onClick={() => alert("Hydrated!")}>点击我</button>
</div>
);
}
构建客户端包
// build.ts
await Bun.build({
entrypoints: ["./src/client.tsx"],
outdir: "./public/static",
target: "browser",
minify: true,
splitting: true,
});
# 构建客户端
bun run build.ts
# 启动服务器
bun run src/server.tsx
流式 SSR
// src/server-streaming.tsx
import { renderToReadableStream } from "react-dom/server";
import App from "./App";
Bun.serve({
port: 3000,
async fetch(req) {
const url = new URL(req.url);
const stream = await renderToReadableStream(
<App url={url.pathname} />,
{
bootstrapScripts: ["/static/client.js"],
onError(error) {
console.error(error);
},
}
);
// 等待 shell 准备就绪(Suspense 边界)
await stream.allReady;
return new Response(stream, {
headers: { "Content-Type": "text/html" },
});
},
});
使用 Suspense
// src/App.tsx
import { Suspense } from "react";
function SlowComponent() {
// 这将是一个数据获取组件
return <div>已加载!</div>;
}
export default function App({ url }: { url: string }) {
return (
<html>
<head>
<title>流式 SSR</title>
</head>
<body>
<div id="root">
<h1>快速外壳</h1>
<Suspense fallback={<div>加载中...</div>}>
<SlowComponent />
</Suspense>
</div>
</body>
</html>
);
}
数据获取
服务器端数据
// src/server.tsx
import { renderToString } from "react-dom/server";
import { Database } from "bun:sqlite";
import App from "./App";
const db = new Database("data.sqlite");
Bun.serve({
async fetch(req) {
const url = new URL(req.url);
// 在服务器端获取数据
const users = db.query("SELECT * FROM users").all();
const html = renderToString(
<App url={url.pathname} initialData={{ users }} />
);
return new Response(
`<!DOCTYPE html>
<html>
<head><title>SSR</title></head>
<body>
<div id="root">${html}</div>
<script>
window.__INITIAL_DATA__ = ${JSON.stringify({ users })};
</script>
<script src="/static/client.js"></script>
</body>
</html>`,
{ headers: { "Content-Type": "text/html" } }
);
},
});
客户端 Hydration
// src/client.tsx
import { hydrateRoot } from "react-dom/client";
import App from "./App";
const initialData = (window as any).__INITIAL_DATA__;
hydrateRoot(
document.getElementById("root")!,
<App url={window.location.pathname} initialData={initialData} />
);
路由
简单路由
// src/Router.tsx
import { useState, useEffect } from "react";
interface Route {
path: string;
component: React.ComponentType;
}
interface RouterProps {
routes: Route[];
initialPath: string;
}
export function Router({ routes, initialPath }: RouterProps) {
const [path, setPath] = useState(initialPath);
useEffect(() => {
const handlePopState = () => setPath(window.location.pathname);
window.addEventListener("popstate", handlePopState);
return () => window.removeEventListener("popstate", handlePopState);
}, []);
const route = routes.find((r) => r.path === path);
const Component = route?.component || NotFound;
return <Component />;
}
export function Link({ href, children }: { href: string; children: React.ReactNode }) {
const handleClick = (e: React.MouseEvent) => {
e.preventDefault();
window.history.pushState({}, "", href);
window.dispatchEvent(new PopStateEvent("popstate"));
};
return <a href={href} onClick={handleClick}>{children}</a>;
}
function NotFound() {
return <h1>404 - 未找到</h1>;
}
CSS 处理
内联样式
const html = renderToString(<App />);
return new Response(
`<!DOCTYPE html>
<html>
<head>
<style>${await Bun.file("./src/styles.css").text()}</style>
</head>
<body>
<div id="root">${html}</div>
</body>
</html>`,
{ headers: { "Content-Type": "text/html" } }
);
外部样式表
// 构建 CSS
await Bun.build({
entrypoints: ["./src/styles.css"],
outdir: "./public/static",
});
// 在 HTML 中链接
`<link rel="stylesheet" href="/static/styles.css">`
开发设置
热重载开发
// dev.ts
import { watch } from "fs";
const srcDir = "./src";
let serverProcess: Subprocess | null = null;
async function startServer() {
serverProcess?.kill();
serverProcess = Bun.spawn(["bun", "run", "src/server.tsx"], {
stdout: "inherit",
stderr: "inherit",
});
}
// 监视变化
watch(srcDir, { recursive: true }, async (event, filename) => {
console.log(`检测到变化: ${filename}`);
await startServer();
});
await startServer();
console.log("开发服务器正在监视...");
生产构建
// build-prod.ts
// 构建客户端
await Bun.build({
entrypoints: ["./src/client.tsx"],
outdir: "./dist/public/static",
target: "browser",
minify: true,
splitting: true,
sourcemap: "external",
});
// 构建服务器
await Bun.build({
entrypoints: ["./src/server.tsx"],
outdir: "./dist",
target: "bun",
minify: true,
});
console.log("构建完成!");
常见错误
| 错误 | 原因 | 修复 |
|---|---|---|
Hydration 不匹配 |
服务器/客户端 HTML 不同 | 检查初始状态 |
document 未定义 |
SSR 访问 DOM | 使用 typeof window 防护 |
不能使用钩子 |
钩子用在组件外部 | 检查组件结构 |
无样式内容闪烁 |
CSS 未加载 | 内联关键 CSS |
性能提示
- 使用流式 SSR 以更快 TTFB
- 内联关键 CSS 在首屏之上
- 代码分割 客户端包
- 缓存渲染的 HTML 对于静态页面
- 使用 Suspense 用于渐进加载
何时加载参考
加载 references/streaming-patterns.md 当:
- 复杂的 Suspense 边界
- 选择性 hydration
- 渐进增强
加载 references/caching.md 当:
- HTML 缓存策略
- CDN 集成
- 边缘渲染