在博客实例中添加评论功能需要对前后端进行修改。在数据库中,我们将创建一个新的 comments 表,这个表中将存储每篇博客文章的评论。前端我们将增加显示评论和添加新评论的功能。

修改后的项目结构

my-blog/
├── src/
│   ├── server.ts
│   ├── api.ts
│   ├── db.ts
│   ├── models.ts
│   ├── types.ts
│   └── components/
│       ├── PostEditor.tsx
│       ├── PostList.tsx
│       ├── CommentList.tsx
│       ├── CommentForm.tsx
│       └── App.tsx
├── public/
│   ├── index.html
│   └── styles.css
├── Dockerfile
├── docker-compose.yml
├── tsconfig.json
├── package.json
├── bunfig.toml
└── vite.config.ts

1. 数据库配置

修改 src/models.ts 以添加 comments 表:

// src/models.ts
import { defineTable, bigint, text } from 'drizzle-orm';
 
export const postTable = defineTable({
  name: 'posts',
  columns: {
    id: bigint({ primaryKey: true, autoIncrement: true }),
    title: text(),
    content: text(),
  },
});
 
export const commentTable = defineTable({
  name: 'comments',
  columns: {
    id: bigint({ primaryKey: true, autoIncrement: true }),
    postId: bigint(),
    content: text(),
  },
});

2. API 逻辑

修改 src/api.ts 以添加 CRUD 操作:

// src/api.ts
import { postTable, commentTable } from './models';
import db from './db';
import { Post, Comment } from './types';
 
// Retrieve a post by ID
export async function getPost(id: number): Promise<Post | undefined> {
  const results = await db.select(postTable)
    .where(postTable.id.eq(id))
    .execute();
  
  return results.length ? { id: results[0].id, title: results[0].title, content: results[0].content } : undefined;
}
 
// Retrieve all posts
export async function getAllPosts(): Promise<Post[]> {
  const results = await db.select(postTable).execute();
  return results.map(post => ({ id: post.id, title: post.title, content: post.content }));
}
 
// Create a new post
export async function createPost(title: string, content: string): Promise<Post> {
  const result = await db.insert(postTable).values({ title, content }).execute();
  const id = result.insertId;
  return { id, title, content };
}
 
// Update an existing post
export async function updatePost(id: number, title: string, content: string): Promise<Post | undefined> {
  const targetPost = await getPost(id);
  
  if (targetPost) {
    await db.update(postTable)
      .set({ title, content })
      .where(postTable.id.eq(id))
      .execute();
 
    return { id, title, content };
  }
 
  return undefined;
}
 
// Retrieve comments for a specific post
export async function getComments(postId: number): Promise<Comment[]> {
  const results = await db.select(commentTable)
    .where(commentTable.postId.eq(postId))
    .execute();
  
  return results.map(comment => ({ id: comment.id, postId: comment.postId, content: comment.content }));
}
 
// Create a new comment
export async function createComment(postId: number, content: string): Promise<Comment> {
  const result = await db.insert(commentTable).values({ postId, content }).execute();
  const id = result.insertId;
  return { id, postId, content };
}

3. 服务器端配置

修改 src/server.ts 以添加新路由:

// src/server.ts
import { Elysia } from 'elysia';
import { getPost, getAllPosts, createPost, updatePost, getComments, createComment } from './api';
import { Post, Comment } from './types';
 
const app = new Elysia();
 
app.get('/', async (req, res) => {
  res.type('text/html');
  res.sendFile('public/index.html');
});
 
app.get('/posts/:id', async (req, res) => {
  const id = parseInt(req.params.id, 10);
  const post = await getPost(id);
  if (post) {
    res.json(post);
  } else {
    res.status(404).send("Post not found");
  }
});
 
app.get('/posts', async (req, res) => {
  const posts = await getAllPosts();
  res.json(posts);
});
 
app.post('/posts', async (req, res) => {
  const { title, content } = await req.json() as Post;
  const newPost = await createPost(title, content);
  res.json(newPost);
});
 
app.put('/posts/:id', async (req, res) => {
  const id = parseInt(req.params.id, 10);
  const { title, content } = await req.json() as Post;
  const updatedPost = await updatePost(id, title, content);
  if (updatedPost) {
    res.json(updatedPost);
  } else {
    res.status(404).send("Post not found");
  }
});
 
app.get('/posts/:id/comments', async (req, res) => {
  const postId = parseInt(req.params.id, 10);
  const comments = await getComments(postId);
  res.json(comments);
});
 
app.post('/posts/:id/comments', async (req, res) => {
  const postId = parseInt(req.params.id, 10);
  const { content } = await req.json() as Comment;
  const newComment = await createComment(postId, content);
  res.json(newComment);
});
 
app.listen(3000, () => {
  console.log('Server is running on http://localhost:3000');
});

4. 客户端配置

创建 src/components/CommentForm.tsx

// src/components/CommentForm.tsx
import React, { useState } from 'react';
import { Comment } from '../types';
 
interface CommentFormProps {
  postId: number;
  onCommentAdd: (comment: Comment) => void;
}
 
const CommentForm: React.FC<CommentFormProps> = ({ postId, onCommentAdd }) => {
  const [content, setContent] = useState('');
 
  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    const response = await fetch(`/posts/${postId}/comments`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ content }),
    });
    const comment: Comment = await response.json();
    onCommentAdd(comment);
    setContent('');
  };
 
  return (
    <form onSubmit={handleSubmit} className="mt-2">
      <textarea
        className="w-full p-2 border"
        value={content}
        onChange={(e) => setContent(e.target.value)}
        placeholder="Add a comment"
      />
      <button type="submit" className="mt-2 p-2 bg-blue-500 text-white">
        Submit
      </button>
    </form>
  );
};
 
export default CommentForm;

创建 src/components/CommentList.tsx

// src/components/CommentList.tsx
import React, { useEffect, useState } from 'react';
import { Comment } from '../types';
 
interface CommentListProps {
  postId: number;
}
 
const CommentList: React.FC<CommentListProps> = ({ postId }) => {
  const [comments, setComments] = useState<Comment[]>([]);
 
  useEffect(() => {
    const fetchData = async () => {
      const response = await fetch(`/posts/${postId}/comments`);
      const comments: Comment[] = await response.json();
      setComments(comments);
    };
    fetchData();
  }, [postId]);
 
  const handleCommentAdd = (comment: Comment) => {
    setComments([...comments, comment]);
  };
 
  return (
    <div className="mt-4">
      <h3 className="text-xl font-semibold">Comments</h3>
      <CommentForm postId={postId} onCommentAdd={handleCommentAdd} />
      <ul className="mt-2">
        {comments.map((comment) => (
          <li key={comment.id} className="p-2 border mb-2">
            {comment.content}
          </li>
        ))}
      </ul>
    </div>
  );
};
 
export default CommentList;

修改 src/components/PostList.tsx

// src/components/PostList.tsx
import React, { useEffect, useState } from 'react';
import { Post } from '../types';
import PostEditor from './PostEditor';
import CommentList from './CommentList';
 
const PostList: React.FC = () => {
  const [posts, setPosts] = useState<Post[]>([]);
  const [editingPostId, setEditingPostId] = useState<number | null>(null);
 
  useEffect(() => {
    const fetchData = async () => {
      const response = await fetch('/posts');
      const posts: Post[] = await response.json();
      setPosts(posts);
    };
    fetchData();
  }, []);
 
  const handleSave = (post: Post) => {
    setPosts((prevPosts) =>
      prevPosts.map((p) => (p.id === post.id ? post : p))
    );
    setEditingPostId(null);
  };
 
  return (
    <div>
      <h1 className="text-2xl font-bold">Blog Posts</h1>
      <button onClick={() => setEditingPostId(null)} className="mt-2 p-2 bg-green-500 text-white">
        Create New Post
      </button>
      {editingPostId === null && <PostEditor onSave={handleSave} />}
      <ul className="mt-4">
        {posts.map((post) => (
          <li key={post.id} className="p-4 border mb-2">
            <h2 className="text-xl font-semibold">{post.title}</h2>
            <p>{post.content}</p>
            <button onClick={() => setEditingPostId(post.id)} className="mt-2 p-2 bg-yellow-500 text-white">
              Edit
            </button>
            <CommentList postId={post.id} />
          </li>
        ))}
      </ul>
    </div>
  );
};
 
export default PostList;

5. 配置静态页面和样式

配置文件不需要更改,继续使用之前的 public/index.htmlpublic/styles.css

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

配置文件不需要更改,继续使用之前的 .env

7. 初始化数据库

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

CREATE TABLE IF NOT EXISTS posts (
  id SERIAL PRIMARY KEY,
  title TEXT NOT NULL,
  content TEXT NOT NULL
);
 
CREATE TABLE IF NOT EXISTS comments (
  id SERIAL PRIMARY KEY,
  postId INTEGER NOT NULL REFERENCES posts(id) ON DELETE CASCADE,
  content TEXT NOT NULL
);

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

8. Docker 配置

配置文件不需要更改,继续使用之前的 Dockerfiledocker-compose.yml

9. 启动项目

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

source .env

运行 Docker Compose:

docker-compose up --build

访问 http://localhost:3000 ,将可访问到一个博客实例,其中你可以创建、编辑、查看博客文章,并为每篇文章添加评论。

总结

通过本文,我们详细说明了如何在现有的博客实例中添加评论功能。以下是已完成的主要任务:

  1. 服务器端配置:添加评论的 API 路由和 CRUD 操作逻辑。
  2. 客户端配置:使用 React 实现评论列表和添加评论的组件。
  3. 数据库配置:增加 comments 表,管理评论数据。

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