Loading

Next.js + Mongodb CURD

环境

  • Next.js 14
  • React 18
  • Mongodb

前言

花了两周时间学习了Next.js, 自己做了个demo,尝试了下服务器端渲染,客户端渲染,给人的感觉就是又像回到了asp.net MVC时代, 需要在页面初次加载时显示的数据可以使用ViewModel来解决,需要在页面上有交互、异步刷新的业务可以使用ajax来解决。
最主要的是整理了使用Next.js 项目结构,一些文件、目录应该怎么放。

项目结构(目录结构)

  1. 需要区分服务器端渲染和客户端渲染的页面,Next.js 14版本推荐服务器端渲染页面是放在app目录下,按照目录约定方式配置路由。 比如这里app/addTopic, app/editTopic就对应两个路由地址

http://localhost:3000/addTopic
http://localhost:300/editTopic.
如果需要带路由参数,是把文件夹名称使用中括号包装起来

  1. 需要通过客户端渲染的页面建议创建Views or Pages目录,主要是和服务器渲染组件区分开,并且使用'use client' 指令描述
  2. API Endpoint - Next.js 可以创建服务器端接口, 就像asp.net MVC里创建Controller/Action 可以在Razor 视图里异步调用。 默认放在app/api目录下,如上图,根据约定就会暴露出如下几个api endpoint

http://localhost:3000/api/topics
http://localhost:3000/api/topics/{id}
然后具体的http协议可以在代码里指定,可以是POST OR GET OR PUT. 后面会贴上参考代码。 这一点我个人发现如果是复杂一些的路由,可能会在api目录先出现嵌套很多的目录结构,比如说URL上包含很多查询参数,按照Restful URL设计的话, 此时项目结构可能就有点凌乱,体验不是很好。

API Endpoint 代码片段

app/topics/route.ts

/**
 * POST
 * http://localhost:3000/api/topics
 * **/
export async function POST(request: any) {
  const { title, description } = await request.json();
  await connectMongoDb();

  await Topic.create({ title, description });
  return NextResponse.json({ message: "Topic Created" }, { status: 201 });
}

/**
 * GET
 * http://localhost:3000/api/topics
 * **/
export async function GET() {
  await connectMongoDb();
  const topics = await Topic.find();
  return NextResponse.json(topics);
}

/**
 * DELETE
 * http://localhost:3000/api/topics?id=123
 * **/
export async function DELETE(request: any) {
  const id = request.nextUrl.searchParams.get("id");
  await connectMongoDb();
  await Topic.findByIdAndDelete(id);
  return NextResponse.json({ message: "Topic Deleted" }, { status: 200 });
}

app/topics/[id]/route.ts

import connectMongoDb from "@/libs/mongodb";
import Topic from "@/models/topic";
import { NextResponse } from "next/server";

/**
 * http get
 * http://localhost:3000/api/topics/[id]
 */
export async function GET(request: any, { params }: any) {
    const { id } = params;
    await connectMongoDb();
    const topic = await Topic.findOne({ _id: id });
    return NextResponse.json({ topic }, { status: 200 });
}

/**
 * http put
 * http://localhost:3000/api/topics/[id]
 */
export async function PUT(request: any, { params }: any) {
    const { id } = params;
    //从request body 中解析参数newTitle, newDescription 给title, description 赋值
    const { newTitle: title, newDescription: description } = await request.json();
    await connectMongoDb();
    await Topic.findByIdAndUpdate(id, { title, description });
    return NextResponse.json({ message: "Topic Updated" }, { status: 200 });
}

使用postman测试接口, 在mongodb里查看数据

[POST]插入一条数据
QQ截图20240520104332.png
[GET]获取所有数据QQ截图20240520104346.png
[PUT]修改数据
QQ截图20240520104505.png

列表页面

const getTopics = async () => {
    const res = await fetch('http://localhost:3000/api/topics', {
        method: 'GET',
        cache: 'no-cache'//不使用缓存
    });

    return res.json();
}

export default async function TopicList() {
    const topics = await getTopics();

    return (
        <>
            {topics.map((t: Topic) => (
                <div
                    key={t._id}
                    className="p-4 border border-slate-300 my-3 flex justify-between gap-5 items-start">
                    <div>
                        <h2 className="font-bold text-2xl">{t.title}</h2>
                        <div>{t.description}</div>
                    </div>
                    <div className="flex gap-2">
                        <RemoveBtn id={t._id} />
                        <Link href={`/editTopic/${t._id}`}>
                            <HiPencilAlt size={24} />
                        </Link>
                    </div>
                </div>
            ))}
        </>
    )
}

列表页面使用服务器端渲染, 页面初始化时就去调用接口加载数据,这里会发现【删除】按钮就单独封装出来,需要客户端交互就需要使用客户端组件,也就是普通的React组件。

删除组件

删除组件顶部需要使用'use client'指令

'use client'

import { HiOutlineTrash } from "react-icons/hi";
import { useRouter } from "next/navigation";

export default function RemoveBtn({ id }: any) {
    const router = useRouter();
    const removeTopic = async () => {
        const confirmed = confirm("Are you sure?");
        if (confirmed) {
            const res = await fetch(`http://localhost:3000/api/topics?id=${id}`, {
                method: "DELETE"
            })

            if (res.ok) {
                router.refresh();
            }
        }
    }

    return (
        <button onClick={removeTopic} className="text-red-400">
            <HiOutlineTrash size={24} />
        </button>
    )
}

连接Mongodb

connect

import mongoose from "mongoose";

const connectMongoDb = async () => {
    try {
        await mongoose.connect(process.env.MONGODB_URI!);
        console.log("Connected to MongoDB");
    } catch (error) {
        console.log(error);
    }
}

export default connectMongoDb;

Schema

import mongoose, { Schema } from "mongoose";

const topicSchema = new Schema(
    {
        title: { type: String, required: true },
        description: { type: String, required: true }
    },
    {
        timestamps: true
    }
)

const Topic = mongoose.models.Topic || mongoose.model('Topic', topicSchema);

export default Topic;

参考

https://gitee.com/garfieldzf/next-topiclist-app

posted @ 2024-05-20 12:05  歪头儿在北京  阅读(14)  评论(0编辑  收藏  举报