好的, 我们可以使用 Bun、Elysia 和 Next. Js 重写之前的 Todo 项目。这个组合将提供一个高性能的全栈解决方案。以下是实现的步骤和代码:
- 首先, 创建一个新的 Next. Js 项目:
bun create next-app todo-elysia
cd todo-elysia- 安装必要的依赖:
bun add elysia @elysiajs/html marked- 创建 Elysia 服务器文件
server.ts:
import { Elysia, t } from 'elysia';
import { html } from '@elysiajs/html';
import { marked } from 'marked';
let todos = [
{ id: 1, text: "Learn **Elysia**", completed: false },
{ id: 2, text: "Build a _Todo_ app", completed: false },
];
const app = new Elysia()
.use(html())
.get('/', () => 'Hello Elysia')
.get('/api/todos', () => todos)
.post('/api/todos', ({ body }) => {
const newTodo = {
id: todos.length + 1,
text: body.text,
completed: false
};
todos.push(newTodo);
return newTodo;
}, {
body: t.Object({
text: t.String()
})
})
.put('/api/todos/:id', ({ params, body }) => {
const todo = todos.find(t => t.id === parseInt(params.id));
if (todo) {
todo.text = body.text;
todo.completed = body.completed;
}
return todo;
}, {
params: t.Object({
id: t.String()
}),
body: t.Object({
text: t.String(),
completed: t.Boolean()
})
})
.delete('/api/todos/:id', ({ params }) => {
todos = todos.filter(t => t.id !== parseInt(params.id));
return { success: true };
}, {
params: t.Object({
id: t.String()
})
})
.get('/api/todos/render/:id', ({ params }) => {
const todo = todos.find(t => t.id === parseInt(params.id));
if (todo) {
return html(`<div>${marked(todo.text)}</div>`);
}
return 'Todo not found';
}, {
params: t.Object({
id: t.String()
})
})
.listen(3000);
console.log(`🦊 Elysia is running at ${app.server?.hostname}:${app.server?.port}`);- 更新
package.json的 scripts:
"scripts": {
"dev": "bun run server.ts & next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
}- 创建
pages/index.tsx:
import { useState, useEffect } from 'react';
import { marked } from 'marked';
interface Todo {
id: number;
text: string;
completed: boolean;
}
export default function Home() {
const [todos, setTodos] = useState<Todo[]>([]);
const [newTodo, setNewTodo] = useState('');
const [editingId, setEditingId] = useState<number | null>(null);
const [editingText, setEditingText] = useState('');
useEffect(() => {
fetchTodos();
}, []);
const fetchTodos = async () => {
const response = await fetch('/api/todos');
const data = await response.json();
setTodos(data);
};
const addTodo = async (e: React.FormEvent) => {
e.preventDefault();
if (newTodo.trim()) {
Const response = await fetch ('/api/todos', {
Method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text: newTodo })
});
const data = await response.json();
setTodos([...todos, data]);
setNewTodo('');
}
};
const updateTodo = async (id: number, text: string, completed: boolean) => {
const response = await fetch(`/api/todos/${id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text, completed })
});
const updatedTodo = await response.json();
setTodos(todos.map(todo => todo.id === id ? updatedTodo : todo));
};
const deleteTodo = async (id: number) => {
await fetch(`/api/todos/${id}`, { method: 'DELETE' });
setTodos(todos.filter(todo => todo.id !== id));
};
const startEditing = (todo: Todo) => {
setEditingId(todo.id);
setEditingText(todo.text);
};
const saveEdit = async () => {
if (editingId !== null) {
await updateTodo(editingId, editingText, todos.find(t => t.id === editingId)?.completed || false);
setEditingId(null);
setEditingText('');
}
};
const renderTodoText = async (id: number) => {
const response = await fetch(`/api/todos/render/${id}`);
const html = await response.text();
return { __html: html };
};
return (
<div className="p-4 max-w-2xl mx-auto">
<h1 className="text-3xl font-bold mb-4">Todo App with Markdown</h1>
<form onSubmit={addTodo} className="mb-4">
<input
value={newTodo}
onChange={(e) => setNewTodo(e.target.value)}
placeholder="Add a new todo"
className="border p-2 mr-2 w-full mb-2"
/>
<button type="submit" className="bg-blue-500 text-white p-2 rounded hover:bg-blue-600">Add</button>
</form>
<ul>
{todos.map(todo => (
<li key={todo.id} className="mb-4 bg-gray-100 p-4 rounded">
{editingId === todo.id ? (
<div>
<textarea
value={editingText}
onChange={(e) => setEditingText(e.target.value)}
className="w-full p-2 border rounded mb-2"
/>
<button onClick={saveEdit} className="bg-green-500 text-white p-2 rounded mr-2">Save</button>
<button onClick={() => setEditingId(null)} className="bg-gray-500 text-white p-2 rounded">Cancel</button>
</div>
) : (
<div>
<div className="flex items-center mb-2">
<input
type="checkbox"
checked={todo.completed}
onChange={(e) => updateTodo(todo.id, todo.text, e.target.checked)}
className="mr-2"
/>
<span className={todo.completed ? 'line-through' : ''}>{todo.text}</span>
<button onClick={() => startEditing(todo)} className="ml-auto text-blue-500 mr-2">Edit</button>
<button onClick={() => deleteTodo(todo.id)} className="text-red-500">Remove</button>
</div>
<div dangerouslySetInnerHTML={renderTodoText(todo.id)} className="markdown-content text-sm text-gray-600" />
</div>
)}
</li>
))}
</ul>
</div>
);
}- 添加一些基本样式, 创建
styles/globals. Css:
@import 'tailwindcss/base';
@import 'tailwindcss/components';
@import 'tailwindcss/utilities';
.markdown-content {
@apply prose prose-sm;
}- 更新
pages/_app. Tsx:
import '../styles/globals.css'
import type { AppProps } from 'next/app'
function MyApp({ Component, pageProps }: AppProps) {
return <Component {...pageProps} />
}
export default MyApp- 配置 Tailwind CSS, 创建
tailwind.config.js:
module.exports = {
content: [
"./pages/**/*.{js,ts,jsx,tsx}",
"./components/**/*.{js,ts,jsx,tsx}",
],
theme: {
extend: {},
},
plugins: [
require('@tailwindcss/typography'),
],
}- 安装 Tailwind CSS 和插件:
bun add -d tailwindcss postcss autoprefixer @tailwindcss/typography这个实现使用了 Bun 作为运行时, Elysia 作为后端 API 服务器, Next. Js 作为前端框架。它保留了之前的 Todo 功能, 包括 Markdown 支持和编辑功能。
要运行这个项目, 使用以下命令:
bun run dev这将同时启动 Elysia 服务器和 Next. Js 开发服务器。你可以在浏览器中访问 http://localhost:3000 来查看和使用这个 Todo 应用。
这个设置提供了一个强大的全栈解决方案, 结合了 Bun 的高性能, Elysia 的轻量级 API 服务, 以及 Next. Js 的强大前端功能。它适合构建现代的、高性能的 web 应用。