IT评测·应用市场-qidao123.com

标题: 【实战教程】用 Next.js 和 shadcn-ui 打造当代博客平台 [打印本页]

作者: 络腮胡菲菲    时间: 2024-9-3 06:26
标题: 【实战教程】用 Next.js 和 shadcn-ui 打造当代博客平台

你是否梦想过拥有一个独特、当代化的个人博客平台?今天,我们将一起动手,使用 Next.js 和 shadcn-ui 来创建一个功能丰富、外观精美的博客系统。无论你是刚接触 Web 开发,照旧经验丰富的步伐员,这个教程都将带你step by step地构建一个完备的博客平台,让你的笔墨创作之旅以后与众差异!

  
1. 引言:为什么选择 Next.js 和 shadcn-ui 构建博客平台?

在开始动手之前,让我们先了解为什么 Next.js 和 shadcn-ui 是构建当代博客平台的绝佳选择。

1.1 Next.js 在博客开发中的上风

Next.js 作为一个强大的 React 框架,为博客开发提供了许多上风:

这些特性使 Next.js 成为构建高性能、SEO 友好的博客平台的抱负选择。
1.2 shadcn-ui 简介及其特点

shadcn-ui 是一个当代化的 UI 组件库,它具有以下特点:

使用 shadcn-ui,我们可以快速构建出美观、功能丰富的用户界面,而无需从零开始设计每个组件。
2. 项目设置和初始化

让我们开始动手创建我们的博客平台!

2.1 创建 Next.js 项目

首先,打开终端,运行以下命令创建一个新的 Next.js 项目:
  1. npx create-next-app@latest my-blog-platform
  2. cd my-blog-platform
复制代码
在安装过程中,选择以下选项:
  1. ✔ Would you like to use TypeScript? … Yes
  2. ✔ Would you like to use ESLint? … Yes
  3. ✔ Would you like to use Tailwind CSS? … Yes
  4. ✔ Would you like to use `src/` directory? … Yes
  5. ✔ Would you like to use App Router? (recommended) … Yes
  6. ✔ Would you like to customize the default import alias? … No
复制代码
2.2 集成 shadcn-ui

接下来,我们将集成 shadcn-ui 到我们的项目中。运行以下命令:
  1. npx shadcn@latest init
复制代码
按照提示进行配置,大多数情况下可以选择默认选项。
2.3 设置项目结构

为了保持项目结构清楚,让我们创建一些必要的目录:
  1. mkdir -p src/{components,lib,styles,types}
复制代码
现在我们的项目结构应该如下所示:
  1. my-blog-platform/
  2. ├── src/
  3. │   ├── app/
  4. │   ├── components/
  5. │   ├── lib/
  6. │   ├── styles/
  7. │   └── types/
  8. ├── public/
  9. ├── .eslintrc.json
  10. ├── next.config.js
  11. ├── package.json
  12. └── tsconfig.json
复制代码
3. 设计和实现博客的核心功能

现在我们已经搭建好了根本框架,让我们开始实现博客的核心功能。

3.1 创建博客首页

3.1.1 设计结构组件

首先,我们需要创建一个基础结构组件。在 src/components 目录下创建 Layout.tsx 文件:
  1. // src/components/Layout.tsx
  2. import React from 'react'
  3. import Link from 'next/link'
  4. interface LayoutProps {
  5.   children: React.ReactNode
  6. }
  7. export default function Layout({ children }: LayoutProps) {
  8.   return (
  9.     <div className="min-h-screen bg-background font-sans antialiased">
  10.       <header className="border-b">
  11.         <nav className="container mx-auto px-4 py-6">
  12.           <Link href="/" className="text-2xl font-bold">
  13.             My Blog
  14.           </Link>
  15.         </nav>
  16.       </header>
  17.       <main className="container mx-auto px-4 py-8">{children}</main>
  18.       <footer className="border-t">
  19.         <div className="container mx-auto px-4 py-6 text-center">
  20.           © 2024 My Blog. All rights reserved.
  21.         </div>
  22.       </footer>
  23.     </div>
  24.   )
  25. }
复制代码

3.1.2 实现文章列表展示

接下来,我们将创建一个文章列表组件。新建 src/components/PostList.tsx 文件:
  1. // src/components/PostList.tsx
  2. import React from 'react'
  3. import Link from 'next/link'
  4. import { Card, CardContent, CardHeader, CardTitle } from './ui/card'
  5. interface Post {
  6.   id: string
  7.   title: string
  8.   excerpt: string
  9. }
  10. interface PostListProps {
  11.   posts: Post[]
  12. }
  13. export default function PostList({ posts }: PostListProps) {
  14.   return (
  15.     <div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
  16.       {posts.map((post) => (
  17.         <Card key={post.id}>
  18.           <CardHeader>
  19.             <CardTitle>
  20.               <Link href={`/post/${post.id}`} className="hover:underline">
  21.                 {post.title}
  22.               </Link>
  23.             </CardTitle>
  24.           </CardHeader>
  25.           <CardContent>
  26.             <p className="text-muted-foreground">{post.excerpt}</p>
  27.           </CardContent>
  28.         </Card>
  29.       ))}
  30.     </div>
  31.   )
  32. }
复制代码
增长依赖
  1. npx shadcn@latest add card
复制代码
现在,我们可以更新首页来使用这些组件。编辑 src/app/page.tsx:
  1. // src/app/page.tsx
  2. import Layout from '@/components/Layout'
  3. import PostList from '@/components/PostList'
  4. const dummyPosts = [
  5.   { id: '1', title: 'First Post', excerpt: 'This is the first post' },
  6.   { id: '2', title: 'Second Post', excerpt: 'This is the second post' },
  7.   { id: '3', title: 'Third Post', excerpt: 'This is the third post' },
  8. ]
  9. export default function Home() {
  10.   return (
  11.     <Layout>
  12.       <h1 className="text-3xl font-bold mb-6">Latest Posts</h1>
  13.       <PostList posts={dummyPosts} />
  14.     </Layout>
  15.   )
  16. }
复制代码
3.2 开发文章详情页

3.2.1 使用动态路由

Next.js 提供了强大的动态路由功能。让我们创建文章详情页。新建 src/app/post/[id]/page.tsx 文件:
  1. // src/app/post/[id]/page.tsx
  2. import Layout from '@/components/Layout'
  3. interface PostPageProps {
  4.   params: { id: string }
  5. }
  6. export default function PostPage({ params }: PostPageProps) {
  7.   return (
  8.     <Layout>
  9.       <h1 className="text-3xl font-bold mb-6">Post {params.id}</h1>
  10.       <p>This is the content of post {params.id}</p>
  11.     </Layout>
  12.   )
  13. }
复制代码
3.2.2 实现 Markdown 渲染

大多数博客使用 Markdown 格式。让我们添加 Markdown 渲染功能。首先,安装必要的包:
  1. npm install react-markdown
复制代码
然后,创建一个 Markdown 渲染组件。新建 src/components/MarkdownRenderer.tsx 文件:
  1. // src/components/MarkdownRenderer.tsx
  2. import React from 'react'
  3. import ReactMarkdown from 'react-markdown'
  4. interface MarkdownRendererProps {
  5.   content: string
  6. }
  7. export default function MarkdownRenderer({ content }: MarkdownRendererProps) {
  8.   return <ReactMarkdown>{content}</ReactMarkdown>
  9. }
复制代码
更新文章详情页以使用 Markdown 渲染:
  1. // src/app/post/[id]/page.tsx
  2. import Layout from '@/components/Layout'
  3. import MarkdownRenderer from '@/components/MarkdownRenderer'
  4. interface PostPageProps {
  5.   params: { id: string }
  6. }
  7. const dummyPost = {
  8.   id: '1',
  9.   title: 'First Post',
  10.   content: '# Hello\n\nThis is the content of the first post.',
  11. }
  12. export default function PostPage({ params }: PostPageProps) {
  13.   return (
  14.     <Layout>
  15.       <h1 className="text-3xl font-bold mb-6">{dummyPost.title}</h1>
  16.       <MarkdownRenderer content={dummyPost.content} />
  17.     </Layout>
  18.   )
  19. }
复制代码
3.3 添加批评功能

3.3.1 设计批评组件

让我们创建一个批评组件。新建 src/components/Comments.tsx 文件:
  1. // src/components/Comments.tsx
  2. import React from 'react'
  3. import { Card, CardContent, CardHeader, CardTitle } from './ui/card'
  4. interface Comment {
  5.   id: string
  6.   author: string
  7.   content: string
  8.   createdAt: string
  9. }
  10. interface CommentsProps {
  11.   comments: Comment[]
  12. }
  13. export default function Comments({ comments }: CommentsProps) {
  14.   return (
  15.     <div className="mt-8">
  16.       <h2 className="text-2xl font-bold mb-4">Comments</h2>
  17.       {comments.map((comment) => (
  18.         <Card key={comment.id} className="mb-4">
  19.           <CardHeader>
  20.             <CardTitle>{comment.author}</CardTitle>
  21.             <p className="text-sm text-muted-foreground">
  22.               {new Date(comment.createdAt).toLocaleDateString()}
  23.             </p>
  24.           </CardHeader>
  25.           <CardContent>
  26.             <p>{comment.content}</p>
  27.           </CardContent>
  28.         </Card>
  29.       ))}
  30.     </div>
  31.   )
  32. }
复制代码
3.3.2 实现批评提交和展示

现在,让我们在文章详情页中添加批评功能。更新 src/app/post/[id]/page.tsx:
  1. // src/app/post/[id]/page.tsx
  2. "use client"
  3. import Layout from '@/components/Layout'
  4. import MarkdownRenderer from '@/components/MarkdownRenderer'
  5. import Comments from '@/components/Comments'
  6. import { Button } from '@/components/ui/button'
  7. import { Textarea } from '@/components/ui/textarea'
  8. import { useState } from 'react'
  9. interface PostPageProps {
  10.   params: { id: string }
  11. }
  12. const dummyPost = {
  13.   id: '1',
  14.   title: 'First Post',
  15.   content: '# Hello\n\nThis is the content of the first post.',
  16. }
  17. const dummyComments = [
  18.   { id: '1', author: 'Alice', content: 'Great post!', createdAt: '2023-09-01T12:00:00Z' },
  19.   { id: '2', author: 'Bob', content: 'Thanks for sharing.', createdAt: '2023-09-02T10:30:00Z' },
  20. ]
  21. export default function PostPage({ params }: PostPageProps) {
  22.   const [comments, setComments] = useState(dummyComments)
  23.   const [newComment, setNewComment] = useState('')
  24.   const handleSubmitComment = (e: React.FormEvent) => {
  25.     e.preventDefault()
  26.     if (newComment.trim()) {
  27.       const comment = {
  28.         id: String(comments.length + 1),
  29.         author: 'Anonymous', // We'll update this when we add authentication
  30.         content: newComment.trim(),
  31.         createdAt: new Date().toISOString(),
  32.       }
  33.       setComments([...comments, comment])
  34.       setNewComment('')
  35.     }
  36.   }
  37.   return (
  38.     <Layout>
  39.       <h1 className="text-3xl font-bold mb-6">{dummyPost.title}</h1>
  40.       <MarkdownRenderer content={dummyPost.content} />
  41.       <Comments comments={comments} />
  42.       <form onSubmit={handleSubmitComment} className="mt-8">
  43.         <Textarea
  44.           value={newComment}
  45.           onChange={(e) => setNewComment(e.target.value)}
  46.           placeholder="Write a comment..."
  47.           className="mb-4"
  48.         />
  49.         <Button type="submit">Submit Comment</Button>
  50.       </form>
  51.     </Layout>
  52.   )
  53. }
复制代码
  1. 这段代码添加了评论列表和评论提交表单。现在,用户可以查看现有评论并添加新评论。
复制代码
4. 使用 Next.js API Routes 构建后端

Next.js 的 API Routes 功能允许我们直接在 Next.js 应用中创建 API 端点。让我们为我们的博客平台创建一些根本的 API。
4.1 创建文章 API

4.1.1 获取文章列表

创建 src/app/api/posts/route.ts 文件:
  1. // src/app/api/posts/route.ts
  2. import { NextResponse } from 'next/server'
  3. const posts = [
  4.   { id: '1', title: 'First Post', excerpt: 'This is the first post' },
  5.   { id: '2', title: 'Second Post', excerpt: 'This is the second post' },
  6.   { id: '3', title: 'Third Post', excerpt: 'This is the third post' },
  7. ]
  8. export async function GET() {
  9.   return NextResponse.json(posts)
  10. }
复制代码
4.1.2 获取单篇文章详情

创建 src/app/api/posts/[id]/route.ts 文件:
  1. // src/app/api/posts/[id]/route.ts
  2. import { NextResponse } from 'next/server'
  3. const posts = {
  4.   '1': { id: '1', title: 'First Post', content: '# Hello\n\nThis is the content of the first post.' },
  5.   '2': { id: '2', title: 'Second Post', content: '# Greetings\n\nThis is the content of the second post.' },
  6.   '3': { id: '3', title: 'Third Post', content: '# Welcome\n\nThis is the content of the third post.' },
  7. }
  8. export async function GET(request: Request, { params }: { params: { id: string } }) {
  9.   const post = posts[params.id]
  10.   if (post) {
  11.     return NextResponse.json(post)
  12.   } else {
  13.     return NextResponse.json({ error: 'Post not found' }, { status: 404 })
  14.   }
  15. }
复制代码
4.2 实现批评 API

4.2.1 提交新批评

创建 src/app/api/posts/[id]/comments/route.ts 文件:
  1. // src/app/api/posts/[id]/comments/route.ts
  2. import { NextResponse } from 'next/server'
  3. let comments: { [key: string]: any[] } = {
  4.   '1': [
  5.     { id: '1', author: 'Alice', content: 'Great post!', createdAt: '2023-09-01T12:00:00Z' },
  6.     { id: '2', author: 'Bob', content: 'Thanks for sharing.', createdAt: '2023-09-02T10:30:00Z' },
  7.   ],
  8. }
  9. export async function GET(request: Request, { params }: { params: { id: string } }) {
  10.   const postComments = comments[params.id] || []
  11.   return NextResponse.json(postComments)
  12. }
  13. export async function POST(request: Request, { params }: { params: { id: string } }) {
  14.   const { author, content } = await request.json()
  15.   const newComment = {
  16.     id: String(Date.now()),
  17.     author,
  18.     content,
  19.     createdAt: new Date().toISOString(),
  20.   }
  21.   
  22.   if (!comments[params.id]) {
  23.     comments[params.id] = []
  24.   }
  25.   comments[params.id].push(newComment)
  26.   
  27.   return NextResponse.json(newComment, { status: 201 })
  28. }
复制代码
现在我们有了根本的 API 端点,可以获取文章列表、单篇文章详情,以及提交和获取批评。
5. 集成数据库


为了使我们的博客平台更加动态和可扩展,我们需要集成一个数据库。在这个例子中,我们将使用 MongoDB,因为它易于设置和使用。
5.1 选择和设置 MongoDB

首先,我们需要安装必要的依赖:
  1. npm install mongodb
复制代码
然后,创建一个 MongoDB Atlas 账户并设置一个新的集群。获取连接字符串后,将其添加到项目的环境变量中。创建一个 .env.local 文件:
  1. MONGODB_URI=your_mongodb_connection_string_here
复制代码
5.2 创建数据模型

让我们创建一些根本的数据模型。在 src/lib 目录下创建 db.ts 文件:
  1. // src/lib/db.ts
  2. import { MongoClient } from 'mongodb'
  3. if (!process.env.MONGODB_URI) {
  4.   throw new Error('Invalid/Missing environment variable: "MONGODB_URI"')
  5. }
  6. const uri = process.env.MONGODB_URI
  7. const options = {}
  8. let client
  9. let clientPromise: Promise<MongoClient>
  10. if (process.env.NODE_ENV === 'development') {
  11.   // 在开发模式下,使用全局变量,以便在 HMR(热模块替换)导致的模块重新加载之间保留该值。
  12.   if (!(global as any)._mongoClientPromise) {
  13.     client = new MongoClient(uri, options)
  14.     ;(global as any)._mongoClientPromise = client.connect()
  15.   }
  16.   clientPromise = (global as any)._mongoClientPromise
  17. } else {
  18.   // In production mode, it's best to not use a global variable.
  19.   client = new MongoClient(uri, options)
  20.   clientPromise = client.connect()
  21. }
  22. export default clientPromise
复制代码
5.3 连接数据库并实现 CRUD 操作

现在,让我们更新我们的 API 路由以使用 MongoDB。首先,更新文章 API:
  1. // src/app/api/posts/route.ts
  2. import { NextResponse } from 'next/server'
  3. import clientPromise from '@/lib/db'
  4. export async function GET() {
  5.   try {
  6.     const client = await clientPromise
  7.     const db = client.db('blog')
  8.     const posts = await db.collection('posts').find({}).toArray()
  9.     return NextResponse.json(posts)
  10.   } catch (e) {
  11.     console.error(e)
  12.     return NextResponse.json({ error: 'Failed to fetch posts' }, { status: 500 })
  13.   }
  14. }
复制代码
同样,更新单篇文章 API:
  1. // src/app/api/posts/[id]/route.ts
  2. import { NextResponse } from 'next/server'
  3. import clientPromise from '@/lib/db'
  4. import { ObjectId } from 'mongodb'
  5. export async function GET(request: Request, { params }: { params: { id: string } }) {
  6.   try {
  7.     const client = await clientPromise
  8.     const db = client.db('blog')
  9.     const post = await db.collection('posts').findOne({ _id: new ObjectId(params.id) })
  10.    
  11.     if (post) {
  12.       return NextResponse.json(post)
  13.     } else {
  14.       return NextResponse.json({ error: 'Post not found' }, { status: 404 })
  15.     }
  16.   } catch (e) {
  17.     console.error(e)
  18.     return NextResponse.json({ error: 'Failed to fetch post' }, { status: 500 })
  19.   }
  20. }
复制代码
最后,更新批评 API:
  1. // src/app/api/posts/[id]/comments/route.ts
  2. import { NextResponse } from 'next/server'
  3. import clientPromise from '@/lib/db'
  4. import { ObjectId } from 'mongodb'
  5. export async function GET(request: Request, { params }: { params: { id: string } }) {
  6.   try {
  7.     const client = await clientPromise
  8.     const db = client.db('blog')
  9.     const comments = await db.collection('comments').find({ postId: new ObjectId(params.id) }).toArray()
  10.     return NextResponse.json(comments)
  11.   } catch (e) {
  12.     console.error(e)
  13.     return NextResponse.json({ error: 'Failed to fetch comments' }, { status: 500 })
  14.   }
  15. }
  16. export async function POST(request: Request, { params }: { params: { id: string } }) {
  17.   try {
  18.     const { author, content } = await request.json()
  19.     const client = await clientPromise
  20.     const db = client.db('blog')
  21.     const newComment = {
  22.       postId: new ObjectId(params.id),
  23.       author,
  24.       content,
  25.       createdAt: new Date().toISOString(),
  26.     }
  27.     const result = await db.collection('comments').insertOne(newComment)
  28.     return NextResponse.json({ ...newComment, _id: result.insertedId }, { status: 201 })
  29.   } catch (e) {
  30.     console.error(e)
  31.     return NextResponse.json({ error: 'Failed to add comment' }, { status: 500 })
  32.   }
  33. }
复制代码
这些更新将使我们的博客平台使用 MongoDB 数据库来存储和检索数据。
6. 实现用户认证

为了让用户能够发表批评和管理自己的文章,我们需要实现用户认证。我们将使用 NextAuth.js,它是一个灵活的认证办理方案,专为 Next.js 设计。
6.1 设置 NextAuth.js

首先,安装 NextAuth.js:
  1. npm install next-auth
复制代码
然后,创建一个 NextAuth.js 配置文件。在 src/app 目录下创建 api/auth/[...nextauth]/route.ts 文件:
  1. // src/app/api/auth/[...nextauth]/route.ts
  2. import NextAuth from "next-auth"
  3. import GithubProvider from "next-auth/providers/github"
  4. const handler = NextAuth({
  5.   providers: [
  6.     GithubProvider({
  7.       clientId: process.env.GITHUB_ID as string,
  8.       clientSecret: process.env.GITHUB_SECRET as string,
  9.     }),
  10.   ],
  11. })
  12. export { handler as GET, handler as POST }
复制代码
确保在 .env.local 文件中添加 GitHub OAuth 应用的凭证:
  1. GITHUB_ID=your_github_client_id
  2. GITHUB_SECRET=your_github_client_secret
  3. NEXTAUTH_SECRET=your_nextauth_secret
复制代码
6.2 创建登录和注册页面

创建一个简朴的登录页面。在 src/app/login/page.tsx 中:
  1. // src/app/login/page.tsx
  2. 'use client'
  3. import { signIn } from 'next-auth/react'
  4. import { Button } from '@/components/ui/button'
  5. export default function LoginPage() {
  6.   return (
  7.     <div className="flex items-center justify-center min-h-screen">
  8.       <Button onClick={() => signIn('github')}>Sign in with GitHub</Button>
  9.     </div>
  10.   )
  11. }
复制代码
6.3 实现受掩护的路由和操作

为了掩护某些路由或操作,我们可以创建一个高阶组件。在 src/components 目录下创建 ProtectedRoute.tsx:
  1. // src/components/ProtectedRoute.tsx
  2. 'use client'
  3. import { useSession } from 'next-auth/react'
  4. import { useRouter } from 'next/navigation'
  5. import { useEffect } from 'react'
  6. export default function ProtectedRoute({ children }: { children: React.ReactNode }) {
  7.   const { data: session, status } = useSession()
  8.   const router = useRouter()
  9.   useEffect(() => {
  10.     if (status === 'loading') return // Do nothing while loading
  11.     if (!session) router.push('/login')
  12.   }, [session, status])
  13.   if (status === 'loading') {
  14.     return <div>Loading...</div>
  15.   }
  16.   return session ? <>{children}</> : null
  17. }
复制代码
现在,你可以在需要用户登录的页面中使用这个组件。比方,在创建新文章的页面:
  1. // src/app/new-post/page.tsx
  2. import ProtectedRoute from '@/components/ProtectedRoute'
  3. import NewPostForm from '@/components/NewPostForm'
  4. export default function NewPostPage() {
  5.   return (
  6.     <ProtectedRoute>
  7.       <NewPostForm />
  8.     </ProtectedRoute>
  9.   )
  10. }
复制代码
这样,只有登录的用户才能访问创建新文章的页面。
7. 优化用户界面和用户体验


7.1 响应式设计

我们的博客平台已经使用了 Tailwind CSS,这使得创建响应式设计变得简朴。确保在构建组件时使用 Tailwind 的响应式类,比方 md:, lg: 等。
7.2 添加加载状态和错误处置惩罚

为了提升用户体验,我们应该添加加载状态和错误处置惩罚。比方,在文章列表页面:
  1. // src/app/page.tsx
  2. 'use client'
  3. import { useState, useEffect } from 'react'
  4. import Layout from '@/components/Layout'
  5. import PostList from '@/components/PostList'
  6. import { Spinner } from '@/components/ui/spinner'
  7. import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'
  8. export default function Home() {
  9.   const [posts, setPosts] = useState([])
  10.   const [isLoading, setIsLoading] = useState(true)
  11.   const [error, setError] = useState(null)
  12.   useEffect(() => {
  13.     async function fetchPosts() {
  14.       try {
  15.         const response = await fetch('/api/posts')
  16.         if (!response.ok) {
  17.           throw new Error('Failed to fetch posts')
  18.         }
  19.         const data = await response.json()
  20.         setPosts(data)
  21.       } catch (err) {
  22.         setError(err.message)
  23.       } finally {
  24.         setIsLoading(false)
  25.       }
  26.     }
  27.     fetchPosts()
  28.   }, [])
  29.   return (
  30.     <Layout>
  31.       <h1 className="text-3xl font-bold mb-6">Latest Posts</h1>
  32.       {isLoading ? (
  33.         <div className="flex justify-center">
  34.           <Spinner size="lg" />
  35.         </div>
  36.       ) : error ? (
  37.         <Alert variant="destructive">
  38.           <AlertTitle>Error</AlertTitle>
  39.           <AlertDescription>{error}</AlertDescription>
  40.         </Alert>
  41.       ) : (
  42.         <PostList posts={posts} />
  43.       )}
  44.     </Layout>
  45.   )
  46. }
复制代码
7.3 实现无限滚动加载

为了提升大量文章的加载体验,我们可以实现无限滚动。首先,我们需要更新我们的 API 以支持分页:
  1. // src/app/api/posts/route.ts
  2. import { NextResponse } from 'next/server'
  3. import clientPromise from '@/lib/db'
  4. export async function GET(request: Request) {
  5.   const { searchParams } = new URL(request.url)
  6.   const page = parseInt(searchParams.get('page') || '1', 10)
  7.   const limit = parseInt(searchParams.get('limit') || '10', 10)
  8.   try {
  9.     const client = await clientPromise
  10.     const db = client.db('blog')
  11.     const posts = await db.collection('posts')
  12.       .find({})
  13.       .sort({ createdAt: -1 })
  14.       .skip((page - 1) * limit)
  15.       .limit(limit)
  16.       .toArray()
  17.     const total = await db.collection('posts').countDocuments()
  18.     return NextResponse.json({
  19.       posts,
  20.       currentPage: page,
  21.       totalPages: Math.ceil(total / limit)
  22.     })
  23.   } catch (e) {
  24.     console.error(e)
  25.     return NextResponse.json({ error: 'Failed to fetch posts' }, { status: 500 })
  26.   }
  27. }
复制代码
然后,我们可以在前端实现无限滚动。我们将使用 react-intersection-observer 库来检测滚动到底部的时机:
  1. npm install react-intersection-observer
复制代码
更新 src/app/page.tsx:
  1. 'use client'
  2. import { useState, useEffect } from 'react'
  3. import { useInView } from 'react-intersection-observer'
  4. import Layout from '@/components/Layout'
  5. import PostList from '@/components/PostList'
  6. import { Spinner } from '@/components/ui/spinner'
  7. import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'
  8. export default function Home() {
  9.   const [posts, setPosts] = useState([])
  10.   const [page, setPage] = useState(1)
  11.   const [isLoading, setIsLoading] = useState(true)
  12.   const [error, setError] = useState(null)
  13.   const [hasMore, setHasMore] = useState(true)
  14.   const { ref, inView } = useInView({
  15.     threshold: 0,
  16.   })
  17.   useEffect(() => {
  18.     if (inView && hasMore) {
  19.       loadMorePosts()
  20.     }
  21.   }, [inView, hasMore])
  22.   async function loadMorePosts() {
  23.     setIsLoading(true)
  24.     try {
  25.       const response = await fetch(`/api/posts?page=${page}&limit=10`)
  26.       if (!response.ok) {
  27.         throw new Error('Failed to fetch posts')
  28.       }
  29.       const data = await response.json()
  30.       setPosts((prevPosts) => [...prevPosts, ...data.posts])
  31.       setPage((prevPage) => prevPage + 1)
  32.       setHasMore(data.currentPage < data.totalPages)
  33.     } catch (err) {
  34.       setError(err.message)
  35.     } finally {
  36.       setIsLoading(false)
  37.     }
  38.   }
  39.   return (
  40.     <Layout>
  41.       <h1 className="text-3xl font-bold mb-6">Latest Posts</h1>
  42.       {posts.length > 0 && <PostList posts={posts} />}
  43.       {error && (
  44.         <Alert variant="destructive">
  45.           <AlertTitle>Error</AlertTitle>
  46.           <AlertDescription>{error}</AlertDescription>
  47.         </Alert>
  48.       )}
  49.       {isLoading && (
  50.         <div className="flex justify-center mt-4">
  51.           <Spinner size="lg" />
  52.         </div>
  53.       )}
  54.       <div ref={ref} style={{ height: '10px' }} />
  55.     </Layout>
  56.   )
  57. }
复制代码
8. 性能优化


8.1 实现静态生成(SSG)和增量静态再生(ISR)

Next.js 提供了强大的静态生成和增量静态再生功能。对于博客文章这种不经常更新的内容,我们可以使用这些功能来提高性能。
更新 src/app/post/[id]/page.tsx:
  1. import { Suspense } from 'react'
  2. import { notFound } from 'next/navigation'
  3. import Layout from '@/components/Layout'
  4. import MarkdownRenderer from '@/components/MarkdownRenderer'
  5. import Comments from '@/components/Comments'
  6. import clientPromise from '@/lib/db'
  7. import { ObjectId } from 'mongodb'
  8. async function getPost(id: string) {
  9.   const client = await clientPromise
  10.   const db = client.db('blog')
  11.   const post = await db.collection('posts').findOne({ _id: new ObjectId(id) })
  12.   if (!post) {
  13.     notFound()
  14.   }
  15.   return post
  16. }
  17. export async function generateStaticParams() {
  18.   const client = await clientPromise
  19.   const db = client.db('blog')
  20.   const posts = await db.collection('posts').find({}, { projection: { _id: 1 } }).toArray()
  21.   
  22.   return posts.map((post) => ({
  23.     id: post._id.toString(),
  24.   }))
  25. }
  26. export default async function PostPage({ params }: { params: { id: string } }) {
  27.   const post = await getPost(params.id)
  28.   return (
  29.     <Layout>
  30.       <h1 className="text-3xl font-bold mb-6">{post.title}</h1>
  31.       <MarkdownRenderer content={post.content} />
  32.       <Suspense fallback={<div>Loading comments...</div>}>
  33.         <Comments postId={params.id} />
  34.       </Suspense>
  35.     </Layout>
  36.   )
  37. }
  38. export const revalidate = 3600 // Revalidate every hour
复制代码
8.2 图片优化

Next.js 提供了内置的图像优化组件。确保在整个应用中使用 next/image 组件:
  1. import Image from 'next/image'
  2. // In your component
  3. <Image src="/path/to/image.jpg" alt="Description" width={500} height={300} />
复制代码
8.3 代码分割和懒加载

Next.js 默认进行代码分割,但我们可以通过动态导入进一步优化:
  1. import dynamic from 'next/dynamic'
  2. const DynamicComponent = dynamic(() => import('@/components/HeavyComponent'), {
  3.   loading: () => <p>Loading...</p>,
  4. })
复制代码
9. 部署博客平台


9.1 准备生产环境配置

确保全部环境变量都已精确设置。创建一个 .env.production 文件来存储生产环境特定的变量。
9.2 选择符合的部署平台

对于 Next.js 应用,Vercel 是一个很好的选择,因为它是由 Next.js 的创建者开发的。
9.3 部署过程和注意事项

确保在部署之前运行构建命令并修复任何警告或错误:
  1. npm run build
复制代码

10. 总结与下一步

10.1 回顾学到的核心概念

在这个项目中,我们学习了:

10.2 扩展功能的想法


10.3 持续学习的资源保举


通过这个项目,你已经掌握了使用 Next.js 和 shadcn-ui 构建当代博客平台的核心技能。继承探索和实践,你将能够构建更加复杂和功能丰富的 Web 应用。记住,学习是一个持续的过程,保持好奇心和实践精神,你将在 Web 开发范畴取得更大的进步!

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。




欢迎光临 IT评测·应用市场-qidao123.com (https://dis.qidao123.com/) Powered by Discuz! X3.4