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