Bun JSX 支持 #
概述 #
Bun 原生支持 JSX 语法,无需配置 Babel 或其他转译工具。Bun 可以直接运行 .jsx 和 .tsx 文件,支持 React、Preact 等框架。
快速开始 #
直接运行 JSX #
tsx
// app.tsx
function Greeting({ name }: { name: string }) {
return <h1>Hello, {name}!</h1>;
}
console.log(<Greeting name="Bun" />);
bash
# 直接运行
bun run app.tsx
React 项目 #
bash
# 创建 React 项目
bun create react my-app
# 或手动安装
bun add react react-dom
bun add -d @types/react @types/react-dom
tsx
// src/index.tsx
import { createRoot } from "react-dom/client";
function App() {
return (
<div>
<h1>Hello, Bun + React!</h1>
</div>
);
}
const root = createRoot(document.getElementById("root")!);
root.render(<App />);
JSX 配置 #
tsconfig.json 配置 #
json
{
"compilerOptions": {
"jsx": "react-jsx",
"jsxImportSource": "react"
}
}
JSX 转换模式 #
| 模式 | 说明 |
|---|---|
react |
使用 React.createElement |
react-jsx |
使用 _jsx 函数(React 17+) |
react-jsxdev |
开发模式的 react-jsx |
preserve |
保留 JSX 不转换 |
自动导入 #
tsx
// 使用 react-jsx 模式,无需手动导入 React
// 旧方式
import React from "react";
function App() {
return <div>Hello</div>;
}
// 新方式(react-jsx)
function App() {
return <div>Hello</div>; // 自动导入 jsx 函数
}
React 支持 #
函数组件 #
tsx
// components/Button.tsx
interface ButtonProps {
children: React.ReactNode;
onClick?: () => void;
variant?: "primary" | "secondary";
}
export function Button({
children,
onClick,
variant = "primary"
}: ButtonProps) {
const styles = {
primary: "bg-blue-500 text-white",
secondary: "bg-gray-200 text-gray-800",
};
return (
<button
onClick={onClick}
className={`px-4 py-2 rounded ${styles[variant]}`}
>
{children}
</button>
);
}
Hooks #
tsx
// hooks/useCounter.ts
import { useState, useCallback } from "react";
export function useCounter(initialValue: number = 0) {
const [count, setCount] = useState(initialValue);
const increment = useCallback(() => {
setCount((c) => c + 1);
}, []);
const decrement = useCallback(() => {
setCount((c) => c - 1);
}, []);
const reset = useCallback(() => {
setCount(initialValue);
}, [initialValue]);
return { count, increment, decrement, reset };
}
tsx
// components/Counter.tsx
import { useCounter } from "../hooks/useCounter";
export function Counter() {
const { count, increment, decrement, reset } = useCounter(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={increment}>+</button>
<button onClick={decrement}>-</button>
<button onClick={reset}>Reset</button>
</div>
);
}
服务端渲染 #
tsx
// server.tsx
import { renderToString } from "react-dom/server";
function App({ name }: { name: string }) {
return (
<html>
<head>
<title>Bun SSR</title>
</head>
<body>
<h1>Hello, {name}!</h1>
</body>
</html>
);
}
Bun.serve({
port: 3000,
fetch() {
const html = renderToString(<App name="Bun" />);
return new Response(`<!DOCTYPE html>${html}`, {
headers: { "Content-Type": "text/html" },
});
},
});
console.log("Server running at http://localhost:3000");
Preact 支持 #
安装配置 #
bash
# 安装 Preact
bun add preact
json
// tsconfig.json
{
"compilerOptions": {
"jsx": "react-jsx",
"jsxImportSource": "preact"
}
}
Preact 组件 #
tsx
// app.tsx
import { useState } from "preact/hooks";
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}
function App() {
return (
<main>
<h1>Preact + Bun</h1>
<Counter />
</main>
);
}
export default App;
Preact 服务端渲染 #
tsx
// server.tsx
import { render } from "preact-render-to-string";
import App from "./app";
Bun.serve({
port: 3000,
fetch() {
const html = render(<App />);
return new Response(`<!DOCTYPE html>${html}`, {
headers: { "Content-Type": "text/html" },
});
},
});
自定义 JSX 运行时 #
创建自定义运行时 #
typescript
// jsx-runtime/index.ts
export function jsx(
tag: string | Function,
props: Record<string, any> | null,
...children: any[]
) {
if (typeof tag === "function") {
return tag({ ...props, children });
}
const element = {
tag,
props: props || {},
children: children.flat(),
};
return element;
}
export const jsxs = jsx;
export const Fragment = ({ children }: { children: any[] }) => children;
配置使用 #
json
// tsconfig.json
{
"compilerOptions": {
"jsx": "react-jsx",
"jsxImportSource": "./jsx-runtime"
}
}
使用自定义运行时 #
tsx
// app.tsx
function Card({ title, children }: { title: string; children: any }) {
return (
<div className="card">
<h2>{title}</h2>
<div className="content">{children}</div>
</div>
);
}
const element = (
<Card title="Hello">
<p>This is the content</p>
</Card>
);
console.log(element);
JSX 类型定义 #
React 类型 #
typescript
// 安装类型
bun add -d @types/react
// 使用
import type { ReactNode, FC, ComponentProps } from "react";
type ButtonProps = {
children: ReactNode;
onClick?: () => void;
};
const Button: FC<ButtonProps> = ({ children, onClick }) => {
return <button onClick={onClick}>{children}</button>;
};
自定义类型 #
typescript
// types/jsx.d.ts
declare global {
namespace JSX {
interface IntrinsicElements {
"my-element": {
customProp?: string;
children?: React.ReactNode;
};
}
interface ElementChildrenAttribute {
children: {};
}
}
}
高级用法 #
条件渲染 #
tsx
function UserGreeting({ user }: { user?: { name: string } }) {
return (
<div>
{user ? (
<h1>Welcome, {user.name}!</h1>
) : (
<h1>Please log in</h1>
)}
</div>
);
}
列表渲染 #
tsx
interface Item {
id: string;
name: string;
}
function ItemList({ items }: { items: Item[] }) {
return (
<ul>
{items.map((item) => (
<li key={item.id}>{item.name}</li>
))}
</ul>
);
}
表单处理 #
tsx
function ContactForm() {
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
const form = e.target as HTMLFormElement;
const formData = new FormData(form);
console.log(Object.fromEntries(formData));
};
return (
<form onSubmit={handleSubmit}>
<input name="name" type="text" placeholder="Name" />
<input name="email" type="email" placeholder="Email" />
<button type="submit">Submit</button>
</form>
);
}
样式处理 #
tsx
// 内联样式
function StyledComponent() {
const style = {
color: "blue",
fontSize: "16px",
padding: "10px",
};
return <div style={style}>Styled content</div>;
}
// CSS 类
function ClassComponent() {
return <div className="container active">Content</div>;
}
// CSS Modules(需要打包器支持)
import styles from "./Button.module.css";
function Button() {
return <button className={styles.button}>Click</button>;
}
服务端渲染(SSR) #
基础 SSR #
tsx
// components/App.tsx
export function App({ data }: { data: any }) {
return (
<html>
<head>
<meta charSet="utf-8" />
<title>Bun SSR</title>
</head>
<body>
<div id="root">
<h1>{data.title}</h1>
<p>{data.content}</p>
</div>
<script src="/client.js" />
</body>
</html>
);
}
tsx
// server.tsx
import { renderToString } from "react-dom/server";
import { App } from "./components/App";
Bun.serve({
port: 3000,
async fetch(req) {
const url = new URL(req.url);
if (url.pathname === "/client.js") {
const file = Bun.file("./dist/client.js");
return new Response(file);
}
const data = {
title: "Hello SSR",
content: "This is server-side rendered content",
};
const html = renderToString(<App data={data} />);
return new Response(`<!DOCTYPE html>${html}`, {
headers: { "Content-Type": "text/html" },
});
},
});
流式 SSR #
tsx
// streaming-ssr.tsx
import { renderToPipeableStream } from "react-dom/server";
import { App } from "./App";
Bun.serve({
port: 3000,
fetch(req) {
const { pipe } = renderToPipeableStream(<App />, {
onShellReady() {
// 流式响应
},
});
// 使用 ReadableStream
const stream = new ReadableStream({
start(controller) {
pipe({
write: (chunk: string) => controller.enqueue(chunk),
end: () => controller.close(),
});
},
});
return new Response(stream, {
headers: { "Content-Type": "text/html" },
});
},
});
静态站点生成(SSG) #
tsx
// build.ts
import { renderToString } from "react-dom/server";
import { App } from "./App";
import fs from "fs";
const pages = [
{ path: "/", title: "Home" },
{ path: "/about", title: "About" },
{ path: "/contact", title: "Contact" },
];
async function build() {
for (const page of pages) {
const html = renderToString(<App title={page.title} />);
const outputPath = `./dist${page.path}/index.html`;
fs.mkdirSync(`./dist${page.path}`, { recursive: true });
fs.writeFileSync(outputPath, `<!DOCTYPE html>${html}`);
console.log(`Generated: ${outputPath}`);
}
}
build();
框架集成 #
Next.js 风格路由 #
tsx
// router.tsx
import { renderToString } from "react-dom/server";
const pages = import.meta.glob("./pages/**/*.tsx");
async function renderPage(pathname: string) {
const pagePath = `./pages${pathname}.tsx`;
const pagePathIndex = `./pages${pathname}/index.tsx`;
const importer = pages[pagePath] || pages[pagePathIndex];
if (!importer) {
return null;
}
const { default: Page } = await importer();
return renderToString(<Page />);
}
Bun.serve({
port: 3000,
async fetch(req) {
const url = new URL(req.url);
const html = await renderPage(url.pathname);
if (!html) {
return new Response("Not Found", { status: 404 });
}
return new Response(`<!DOCTYPE html>${html}`, {
headers: { "Content-Type": "text/html" },
});
},
});
最佳实践 #
组件组织 #
text
src/
├── components/
│ ├── ui/
│ │ ├── Button.tsx
│ │ ├── Input.tsx
│ │ └── index.ts
│ ├── layout/
│ │ ├── Header.tsx
│ │ ├── Footer.tsx
│ │ └── index.ts
│ └── features/
│ ├── UserCard.tsx
│ └── index.ts
├── hooks/
│ ├── useUser.ts
│ └── index.ts
├── pages/
│ ├── Home.tsx
│ └── About.tsx
└── App.tsx
类型安全 #
tsx
// 严格类型定义
interface ButtonProps {
variant: "primary" | "secondary" | "danger";
size: "sm" | "md" | "lg";
children: React.ReactNode;
onClick?: () => void;
disabled?: boolean;
}
export function Button({
variant,
size,
children,
onClick,
disabled = false,
}: ButtonProps) {
return (
<button
className={`btn btn-${variant} btn-${size}`}
onClick={onClick}
disabled={disabled}
>
{children}
</button>
);
}
下一步 #
现在你已经了解了 Bun 的 JSX 支持,接下来学习 HTTP 服务 深入了解 Bun 的 HTTP API。
最后更新:2026-03-29