红红红 發表於 2024-5-20 12:05:00

Next.js + Mongodb CURD

<h1 id="环境">环境</h1>
<ul>
<li>Next.js 14</li>
<li>React 18</li>
<li>Mongodb</li>
</ul>
<h1 id="前言">前言</h1>
<p>花了两周时间学习了Next.js, 自己做了个demo,尝试了下服务器端渲染,客户端渲染,给人的感觉就是又像回到了asp.net MVC时代, 需要在页面初次加载时显示的数据可以使用ViewModel来解决,需要在页面上有交互、异步刷新的业务可以使用ajax来解决。<br>
最主要的是整理了使用Next.js 项目结构,一些文件、目录应该怎么放。</p>
<h1 id="项目结构目录结构">项目结构(目录结构)</h1>
<p><img src="https://img2024.cnblogs.com/blog/115511/202405/115511-20240520120558005-1412132936.png" alt="" loading="lazy"></p>
<ol>
<li>需要区分服务器端渲染和客户端渲染的页面,Next.js 14版本推荐服务器端渲染页面是放在app目录下,按照目录约定方式配置路由。 比如这里app/addTopic, app/editTopic就对应两个路由地址</li>
</ol>
<p>http://localhost:3000/addTopic<br>
http://localhost:300/editTopic.<br>
如果需要带路由参数,是把文件夹名称使用中括号包装起来</p>
<ol start="2">
<li>需要通过客户端渲染的页面建议创建Views or Pages目录,主要是和服务器渲染组件区分开,并且使用'use client' 指令描述</li>
<li>API Endpoint - Next.js 可以创建服务器端接口, 就像asp.net MVC里创建Controller/Action 可以在Razor 视图里异步调用。默认放在app/api目录下,如上图,根据约定就会暴露出如下几个api endpoint</li>
</ol>
<p>http://localhost:3000/api/topics<br>
http://localhost:3000/api/topics/{id}<br>
然后具体的http协议可以在代码里指定,可以是POST OR GET OR PUT.后面会贴上参考代码。这一点我个人发现如果是复杂一些的路由,可能会在api目录先出现嵌套很多的目录结构,比如说URL上包含很多查询参数,按照Restful URL设计的话, 此时项目结构可能就有点凌乱,体验不是很好。</p>
<h1 id="api-endpoint-代码片段">API Endpoint 代码片段</h1>
<h2 id="apptopicsroutets">app/topics/route.ts</h2>
<pre><code class="language-typescript">/**
* 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 });
}
</code></pre>
<h2 id="apptopicsidroutets">app/topics//route.ts</h2>
<pre><code class="language-typescript">import connectMongoDb from "@/libs/mongodb";
import Topic from "@/models/topic";
import { NextResponse } from "next/server";

/**
* http get
* http://localhost:3000/api/topics/
*/
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/
*/
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 });
}
</code></pre>
<h2 id="使用postman测试接口-在mongodb里查看数据">使用postman测试接口, 在mongodb里查看数据</h2>
<p>插入一条数据<br>
<img src="https://cdn.nlark.com/yuque/0/2024/png/359374/1716173049463-2a3c2f07-b8f7-4742-bf8b-d13ae03b9829.png#averageHue=%23222222&amp;clientId=u20d7ed55-2933-4&amp;from=ui&amp;id=ue6e6e52f&amp;originHeight=632&amp;originWidth=1327&amp;originalType=binary&amp;ratio=1.5&amp;rotation=0&amp;showTitle=false&amp;size=50883&amp;status=done&amp;style=none&amp;taskId=u6bb9ba83-9e01-4e4f-aa42-a7a23b7dd2d&amp;title=" alt="QQ截图20240520104332.png" loading="lazy"><br>
获取所有数据<img src="https://cdn.nlark.com/yuque/0/2024/png/359374/1716173049471-c6e7d0e5-0dbc-40cf-b6a8-b7b0aaed06a2.png#averageHue=%23232323&amp;clientId=u20d7ed55-2933-4&amp;from=ui&amp;id=ubae3b76e&amp;originHeight=414&amp;originWidth=798&amp;originalType=binary&amp;ratio=1.5&amp;rotation=0&amp;showTitle=false&amp;size=29107&amp;status=done&amp;style=none&amp;taskId=ue889fc80-b747-456a-aae8-4e1e0187226&amp;title=" alt="QQ截图20240520104346.png" loading="lazy"><br>
修改数据<br>
<img src="https://cdn.nlark.com/yuque/0/2024/png/359374/1716173131486-56055b14-b88f-499e-b9a6-63bab0a0ad57.png#averageHue=%23252423&amp;clientId=u20d7ed55-2933-4&amp;from=ui&amp;id=u23178f9a&amp;originHeight=457&amp;originWidth=1041&amp;originalType=binary&amp;ratio=1.5&amp;rotation=0&amp;showTitle=false&amp;size=56709&amp;status=done&amp;style=none&amp;taskId=uf38491ec-3141-46b2-a8d7-cc030d92577&amp;title=" alt="QQ截图20240520104505.png" loading="lazy"></p>
<h1 id="列表页面">列表页面</h1>
<pre><code class="language-typescript">const getTopics = async () =&gt; {
    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 (
      &lt;&gt;
            {topics.map((t: Topic) =&gt; (
                &lt;div
                  key={t._id}
                  className="p-4 border border-slate-300 my-3 flex justify-between gap-5 items-start"&gt;
                  &lt;div&gt;
                        &lt;h2 className="font-bold text-2xl"&gt;{t.title}&lt;/h2&gt;
                        &lt;div&gt;{t.description}&lt;/div&gt;
                  &lt;/div&gt;
                  &lt;div className="flex gap-2"&gt;
                        &lt;RemoveBtn id={t._id} /&gt;
                        &lt;Link href={`/editTopic/${t._id}`}&gt;
                            &lt;HiPencilAlt size={24} /&gt;
                        &lt;/Link&gt;
                  &lt;/div&gt;
                &lt;/div&gt;
            ))}
      &lt;/&gt;
    )
}
</code></pre>
<p>列表页面使用服务器端渲染, 页面初始化时就去调用接口加载数据,这里会发现【删除】按钮就单独封装出来,需要客户端交互就需要使用客户端组件,也就是普通的React组件。</p>
<h1 id="删除组件">删除组件</h1>
<p>删除组件顶部需要使用'use client'指令</p>
<pre><code class="language-typescript">'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 () =&gt; {
      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 (
      &lt;button onClick={removeTopic} className="text-red-400"&gt;
            &lt;HiOutlineTrash size={24} /&gt;
      &lt;/button&gt;
    )
}

</code></pre>
<h1 id="连接mongodb">连接Mongodb</h1>
<h2 id="connect">connect</h2>
<pre><code class="language-typescript">import mongoose from "mongoose";

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

export default connectMongoDb;
</code></pre>
<h2 id="schema">Schema</h2>
<pre><code class="language-typescript">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;
</code></pre>
<h1 id="参考">参考</h1>
<p>https://gitee.com/garfieldzf/next-topiclist-app</p>


</div>
<div id="MySignature" role="contentinfo">
    <table>
<tr>
   <td id="tdSign" >博客地址:</td><td>http://www.cnblogs.com/sword-successful/</td

>
</tr>
<tr>
   <td>博客版权:</td><td>本文以学习、研究和分享为主,欢迎转载,但必须在文章页面明显位置给出原文连接。<br/>如果文中有不妥或者错误的地方还望高手的你指出,以免误人子弟。如果觉得本文对你有所帮助不如【推荐】一下!如果你有更好的建议,不如留言一起讨论,共同进步!<br/>再次感谢您耐心的读完本篇文章。</td>
</tr>
</table><br><br>
来源:https://www.cnblogs.com/sword-successful/p/18201623
頁: [1]
查看完整版本: Next.js + Mongodb CURD