不错的想法!结合 Elysia、HTMX、UnoCSS 以及 React,我们可以构建一个功能强大但简洁的文本编辑器。在这个项目中,我们将使用 TypeScript 启用高阶函数来增强代码的复用性和可读性。Bun 是一个新的 JavaScript 运行时,可以通过其高性能进行快速开发和运行。

项目结构

my-text-editor/
├── src/
│   ├── server.ts
│   ├── api.ts
│   └── types.ts
├── public/
│   ├── index.html
│   └── styles.css
├── src/
│   ├── components/
│   │   └── TextEditor.tsx
│   └── App.tsx
├── tsconfig.json
├── package.json
├── bunfig.toml
└── vite.config.js

1. 初始化项目

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

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

安装依赖

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

配置 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"]
}

2. 创建服务器端和 API

src 目录下创建 server.tsapi.tstypes.ts

types.ts 用于定义数据类型:

export interface TextDocument {
  id: number;
  content: string;
}

api.ts 用于处理文本文档的 CRUD 操作:

import { TextDocument } from './types';
 
let documents: TextDocument[] = [];
let currentId = 1;
 
export async function getDocument(id: number): Promise<TextDocument | undefined> {
  return documents.find(doc => doc.id === id);
}
 
export async function createDocument(content: string): Promise<TextDocument> {
  const newDocument: TextDocument = { id: currentId++, content };
  documents.push(newDocument);
  return newDocument;
}
 
export async function updateDocument(id: number, content: string): Promise<TextDocument | undefined> {
  const document = documents.find(doc => doc.id === id);
  if (document) {
    document.content = content;
    return document;
  }
  return undefined;
}

server.ts 用于创建 Elysia 服务器:

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');
});

3. 创建前端应用

Vite 配置

创建 vite.config.js

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

React 组件

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;

创建应用入口

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'));

4. HTMX 和 UnoCSS 配置

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;
}

5. 启动服务器

运行服务器:

bun run src/server.ts

启动 Vite 开发服务器:

bun x vite

打开浏览器并访问 http://localhost:3000 ,你将看到一个文本编辑器,可以检索和保存文本文档。

总结

通过这个示例项目,我们展示了如何结合 Elysia、HTMX、UnoCSS 和 React 来创建一个简单但功能强大的文本编辑器。关键点如下:

  1. Elysia:设置路由和 API 端点,处理文本文档的 CRUD 操作。
  2. HTMX:在前端 HTML 中使用 HTMX 进行数据交互。
  3. UnoCSS:使用原子化样式表进行样式管理。
  4. React:使用 React 创建组件和处理用户交互。

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