好的,我们将 React 应用迁移到 Vite 构建工具下进行开发。Vite 是一个快速且现代的构建工具,特别适合用来构建 React 应用。在此基础上,我们还会集成 Elysia、HTMX、UnoCSS 及 TypeScript。

项目结构

  1. 服务器端

    • server.ts: Elysia 服务器和 API。
    • models.ts: Drizzle ORM 数据库表定义。
    • api.ts: 文本文档的 CRUD 操作。
  2. 客户端

    • App.tsx: React 应用的入口。
    • TextEditor.tsx: 文本编辑器组件。
    • index.html: HTMX 和 UnoCSS 配置。

1. 初始化新项目并安装依赖

创建项目目录并初始化 Bun 项目

mkdir my-vite-text-editor
cd my-vite-text-editor
bun init

安装依赖

bun add elysia htmx react react-dom @types/react @types/react-dom unocss drizzle-orm pg
bun add -d typescript vite @vitejs/plugin-react

2. 配置 TypeScript

创建 tsconfig.json

bun add --dev typescript
bun x tsc --init

编辑生成的 tsconfig.json 文件:

{
  "compilerOptions": {
    "target": "ESNext",
    "module": "ESNext",
    "jsx": "react-jsx",
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  },
  "include": ["src"]
}

3. 配置 Vite

创建 Vite 配置文件 vite.config.ts

import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
 
export default defineConfig({
  plugins: [react()],
});

4. 服务器端配置

创建 src/db.ts 进行数据库连接配置:

// src/db.ts
import { drizzle, NodePgClient } from 'drizzle-orm';
import { Client } from 'pg';
import { config } from 'dotenv';
 
config(); // Load .env file
 
const client = new Client({
  connectionString: process.env.DATABASE_URL,
});
 
const db = drizzle(new NodePgClient(client));
 
export default db;

创建 src/models.ts 进行数据库表定义:

// src/models.ts
import { defineTable, bigint, text } from 'drizzle-orm';
 
export const textDocumentTable = defineTable({
  name: 'text_documents',
  columns: {
    id: bigint({ primaryKey: true, autoIncrement: true }),
    content: text(),
  },
});

创建 src/api.ts 进行 CRUD 操作:

// src/api.ts
import { textDocumentTable } from './models';
import db from './db';
import { TextDocument } from './types';
 
// Retrieve a document by ID
export async function getDocument(id: number): Promise<TextDocument | undefined> {
  const results = await db.select(textDocumentTable)
    .where(textDocumentTable.id.eq(id))
    .execute();
  
  return results.length ? { id: results[0].id, content: results[0].content } : undefined;
}
 
// Create a new document
export async function createDocument(content: string): Promise<TextDocument> {
  const result = await db.insert(textDocumentTable).values({ content }).execute();
  const id = result.insertId;
  return { id, content };
}
 
// Update an existing document
export async function updateDocument(id: number, content: string): Promise<TextDocument | undefined> {
  const targetDocument = await getDocument(id);
  
  if (targetDocument) {
    await db.update(textDocumentTable)
      .set({ content })
      .where(textDocumentTable.id.eq(id))
      .execute();
 
    return { id, content };
  }
 
  return undefined;
}

创建 src/server.ts 配置 Elysia 服务器:

// src/server.ts
import { Elysia } from 'elysia';
import { getDocument, createDocument, updateDocument } from './api';
import { TextDocument } from './types';
 
const app = new Elysia();
 
app.get('/', async (req, res) => {
  res.type('text/html');
  res.sendFile('public/index.html');
});
 
app.get('/documents/:id', async (req, res) => {
  const id = parseInt(req.params.id, 10);
  const document = await getDocument(id);
  if (document) {
    res.json(document);
  } else {
    res.status(404).send("Document not found");
  }
});
 
app.post('/documents', async (req, res) => {
  const { content } = await req.json() as TextDocument;
  const newDocument = await createDocument(content);
  res.json(newDocument);
});
 
app.put('/documents/:id', async (req, res) => {
  const id = parseInt(req.params.id, 10);
  const { content } = await req.json() as TextDocument;
  const updatedDocument = await updateDocument(id, content);
  if (updatedDocument) {
    res.json(updatedDocument);
  } else {
    res.status(404).send("Document not found");
  }
});
 
app.listen(3000, () => {
  console.log('Server is running on http://localhost:3000');
});

5. 客户端配置

创建 React 组件 src/components/TextEditor.tsx

// src/components/TextEditor.tsx
import React, { useState, useEffect } from 'react';
import { TextDocument } from '../types';
 
interface TextEditorProps {
  documentId: number;
}
 
const TextEditor: React.FC<TextEditorProps> = ({ documentId }) => {
  const [content, setContent] = useState('');
 
  useEffect(() => {
    const fetchData = async () => {
      const response = await fetch(`/documents/${documentId}`);
      const document: TextDocument = await response.json();
      setContent(document.content);
    };
    fetchData();
  }, [documentId]);
 
  const handleSave = async () => {
    await fetch(`/documents/${documentId}`, {
      method: 'PUT',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ content }),
    });
  };
 
  return (
    <div className="p-4 border">
      <textarea
        className="w-full h-64 p-2 border"
        value={content}
        onChange={(e) => setContent(e.target.value)}
      />
      <button onClick={handleSave} className="mt-2 p-2 bg-blue-500 text-white">
        Save
      </button>
    </div>
  );
};
 
export default TextEditor;

创建 React 应用入口 src/App.tsx

// src/App.tsx
import React from 'react';
import ReactDOM from 'react-dom';
import TextEditor from './components/TextEditor';
 
const App: React.FC = () => {
  return (
    <div className="app">
      <h1 className="text-2xl font-bold">Text Editor</h1>
      <TextEditor documentId={1} />
    </div>
  );
};
 
ReactDOM.render(<App />, document.getElementById('root'));

6. 配置静态页面和样式

创建 public/index.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Text Editor</title>
  <script type="module" src="/src/App.tsx"></script>
  <link rel="stylesheet" href="https://unpkg.com/@unocss/reset@latest/antfu.css">
  <link rel="stylesheet" href="styles.css">
</head>
<body class="p-4">
  <div id="root"></div>
</body>
</html>

创建 public/styles. Css

/* Source: UnoCSS or similar atomic CSS library */
.text-2xl {
  font-size: 1.5rem;
}
 
.font-bold {
  font-weight: 700;
}
 
.border {
  border: 1px solid #ddd;
}
 
.p-4 {
  padding: 1rem;
}
 
.mt-2 {
  margin-top: .5rem;
}
 
.p-2 {
  padding: .5rem;
}
 
.bg-blue-500 {
  background-color: #3b82f6;
}
 
.text-white {
  color: #fff;
}

7. 配置 .env 进行环境变量管理

创建 .env 文件并添加数据库连接字符串:

DATABASE_URL=postgres://username:password@localhost:5432/mydatabase

8. 初始化数据库

确保 PostgreSQL 数据库正在运行,执行数据库初始化脚本来创建表:

CREATE TABLE IF NOT EXISTS text_documents (
  id SERIAL PRIMARY KEY,
  content TEXT NOT NULL
);

通过 pgAdmin 或 DBeaver 执行以上 SQL 脚本。

9. 启动项目

首先,确保环境变量已加载:

source .env

运行 Elysia 服务器端:

bun run src/server.ts

启动 Vite 开发服务器:

bun x vite

访问 http://localhost:3000 ,将可访问到一个集成了 Elysia、HTMX、UnoCSS、TypeScript、React 和 Drizzle ORM 的文本编辑器应用程序。

总结

通过本文,我们详细说明了如何重新使用 Vite 进行配置和构建 React 应用。并使用 Elysia 创建 API 服务器,结合 HTMX 和 UnoCSS 构建前端样式,以及使用 Drizzle ORM 管理 PostgreSQL 数据库从而实现了一个简洁高效的文本编辑器应用。

希望这个示例能够帮助你更好地理解如何结合这些技术创建一个功能丰富的应用程序。如果有任何问题或需要进一步的帮助,请随时提问!