Next.js 全栈开发
https://nextjs.org 是一个服务端渲染框架
创建项目
1 | npx create-next-app@latest . # 在当前空文件夹中创建 |
安装插件ES7+ React/Redux/React-Native snippets
在tsx
中键入rfce
快速生成代码片。
在app下任意创建一个文件夹就是一个页面的路由,比如app/demo
。在文件夹中可以创建page.tsx
和layout.tsx
分别为页面和模版,如果没有创建模版页面也能通过路由访问,也可以创建模版将页面作为children
嵌入其中。
page.tsx
是固定的名字,每一个路由下都要有它且名字不能改变。
创建一个demo/list/page.tsx
可以看到demo模版依然作为顶层模版。list
下也可以创建一个layout.tsx
。
对layout
页面一个简单的示例: 1
2
3
4
5
6
7
8
9
10
11
12
13import React from 'react'
function DemoLayout({children}: any) {
return (
<div className='demo'>
<h2>这是demo页面的模版...</h2>
<hr />
{children}
</div>
)
}
export default DemoLayout
如果要创建动态路由,创建app/demo/list/[id]/page.tsx
就可以,访问http://localhost:3000/demo/list/1
就出来了。
获取params
的方式:
1 | import React from 'react' |
如果只需要一个id
params就可以用ts显示地申明出来:
1
2
3
4
5function ListDetailPage({params}: { params: {id: string} }) {
return (
<div>ListDetailPage -- {params.id}</div>
)
}
分组
在app/demo
下创建:
(admin)/layout.tsx
(admin)/goods/page.tsx
(admin)/us/page.tsx
这相当于是将这几个页面分组在一起,(admin)
并不会体现在路由当中,仍然可以通过/demo/us
来访问我们的页面。但(admin)/layout.tsx
却会作用在我们所有的分组页面当中。
比如我们的layout.tsx
: 1
2
3
4
5
6
7
8
9
10
11import React, { ReactNode } from 'react'
function AdminLayout({children}: {children: ReactNode}) {
return (
<div className='demo-admin p-8 bg-rose-400 text-white'>
<h4>这是一个admin页面中的内容</h4>
{children}</div>
)
}
export default AdminLayout
可以看到玫瑰色的layout现在分组中的页面
metadata
如需更改页面的 metadata,在对应页面的page.tsx
添加:
1
2
3
4
5
6
7import { Metadata } from 'next'
export const metadata: Metadata = {
title: "这是一个列表页",
description: "这是一个用nextJs输出的列表页",
keywords: "next.js,react"
}
title会发生变化,常用于SEO优化。
要定义一个动态的metadata,比如根据list/[id]
来动态改变页面的title可以这样做:
1 | import React from "react"; |
关于searchParams
在代码中的接收方法,修改[id]/page.tsx
中这一部分:
1
2
3function ListDetailPage({ params, searchParams }: Props) {
return <div>ListDetailPage -- {params.id}, Query -- {searchParams.name}</div>;
}
接着可以通过http://localhost:3000/demo/list/2342asdf?name=老刘
发现页面中包含文本:
1 | 这是demo页面的模版... |
需要说明的是,组件中接受到的searchParamsparams, searchParams
和generateMetadata()
中接受到的是一样的,都是路由的信息。
编写 API
服务器端渲染
创建文件夹app/api
用于存放我们的后端
api,建立一个api/goods
文件夹,并创建goods/route.ts
。
一定要是
route.ts
不能是其他名字,如page.tsx
一个原理。
1 | import { NextResponse } from "next/server" // nestJs 请求对象 |
直接用http://localhost:3000/api/goods
就能访问我们的api了。
同样也可以使用动态路由,比如需要访问http://localhost:3000/api/goods/123
。
在api/goods/[id]
下创建route.ts
:
1
2
3
4
5
6
7
8import { NextResponse } from "next/server" // nestJs 请求对象
export const GET = () => {
return NextResponse.json({ // 返回一个json格式
success: true,
errorMessage: '获取单条记录',
data: {}
})
如果要获取params: 1
2
3
4
5
6
7
8
9
10
11import { NextResponse } from "next/server"; // nestJs 请求对象
// req 是前端传过来的请求,第二个参数是一个上下文,我们可以解析上下文props获得其中的params参数,比如goods/1231中的1231就是一个params
export const GET = (req: NextResponse, { params }: any) => {
return NextResponse.json({
// 返回一个json格式
success: true,
errorMessage: "获取单条记录:" + params.id,
data: {},
});
};
关键在于:
export const GET = (req: NextResponse, { params }: any) => {}
。
客户端调用接口
如果直接在客户端组件中使用useEffect
会报错。
我们可以在list
下创建一个_components/List.tsx
来单独封装我们的列表渲染组件,组件中代码:
1 | "use client" |
一般来说我们将客户端组件可以单独封装并放在
_components
下供服务端组件调用。
为了有数据可以用,可以修改api/goods/route.ts
:
1 | import { NextResponse } from "next/server" // nestJs 请求对象 |
这样就完成了接口的调用了。其中这里的useState
是React语法,可以在React文档查看具体的用法。
数据库 Prisma
安装
1 | npm install prisma --save-dev # 安装 |
初始化完成后在根目录多了prisma
文件夹,可以在其中定义我们的数据库模型。
定义Goods
表的模型: 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema
generator client {
provider = "prisma-client-js"
output = "../src/generated/prisma"
}
datasource db {
provider = "sqlite"
url = env("DATABASE_URL")
}
model Goods {
// id 类型 设置为主键 设置为unique 默认值
id String @id @unique @default(uuid())
name String
desc String @default("")
content String @default("")
// 用 @map 起别名
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
// 表的别名
@@map("products")
}
完成后生成一下数据表: 1
npx prisma db push
prisma/dev.db
就是我们的数据库了。
对于数据库的连接在 prisma
的官方文档中有想尽的说明,我们只需要在文档中搜索:next.js
就能找到。
实则不然,非常难找,但我还是找到了,见文档:
1
2
3
4
5
6
7
8
9// lib/prisma.ts
// 使用相对路径导入生成在 `src/generated/prisma` 的 Prisma Client
import { PrismaClient } from "./generated/prisma";
const globalForPrisma = global as unknown as { prisma: PrismaClient };
export const prisma = globalForPrisma.prisma || new PrismaClient();
if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = prisma;
将这段代码粘贴到src/db.ts
中,官网给的示例似乎有点问题,这里是用Capilot生成的能跑的连接代码。
要查询数据和增加数据,修改api/goods/route.ts
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30import { NextRequest, NextResponse } from "next/server" // nestJs 请求对象
import { prisma } from "@/db"
export const GET = async () => {
// 查询数据,根据创建时间倒序排列
const data = await prisma.goods.findMany({
orderBy: {
createdAt: 'desc'
}
})
return NextResponse.json({ // 返回一个json格式
success: true,
errorMessage: '获取数据成功',
data: data
})
}
// 添加 GOODS
export const POST = async (req: NextRequest) => {
const data = await req.json() // 获取请求体中传递的json数据
// data是请求创建的json格式{ name: '商品名' }
await prisma.goods.create({ // 利用prisma.create插入一个新的数据项
data,
})
return NextResponse.json({ // 返回一个json格式
success: true,
errorMessage: '创建成功',
data: {}
})
}
完事儿后我们就写好了可以创建商品的API了,打开PostMan添加一个POST请求:http://localhost:3000/api/goods
,写入请求体(格式为raw):
1 | { |
请求成功: 1
2
3
4
5{
"success": true,
"errorMessage": "创建成功",
"data": {}
}
antd组件库与后台
安装antd组件库:
1 | npm i antd |
要创建一个管理后台,我们可以先创建一个app/admin
用于后台页面,接下来继续创建:
_components/AntContainer.tsx
app/admin/login/page.tsx
app/admin/layout.tsx
在layout.tsx
和page.tsx
中用rfce
创建基本的代码框架。
使用antd开发,只需要在其官网复制粘贴就好:https://ant.design/docs/react/introduce-cn。
由于 antd 的更新,现在已经可以直接在 Next.js 很方便地使用。见文档:https://ant.design/docs/react/use-with-next-cn
比如创建一个按钮组件_components/Button.tsx
:
1 | 'use client' |
完成后记得为layout.tsx
添加children
,类型申明为any
即可。然后在page.tsx
中引入Button将能被看到:
1 | import React from 'react' |
页面上会出现一个蓝色的小按钮。
一个简单的后台页面
我们来加一个登陆页面,修改admin/login.tsx
:
1 | 'use client' |
完成后会出现表单,但样式似乎有点奇怪,这是因为 Tailwindcss 和 Ant
产生了一些冲突,我们可以修改 Tailwindcss
的配置tailwind.config.ts
来解决。
这里发现我似乎没有原教程中这个问题,省略处理过程。
我们修改一下上述页面,让它具有获取表单内容的功能:
1 | "use client"; |
这里为Form.Item
添加了name
属性,并通过onFinish={(v) => {}} >
来打印获取表单数据。
中间件做登陆判断
我们创建一个app/admin/dashboard
文件夹,并创建:
admin/dashboard/_components/PageContainer.tsx
admin/dashboard/page.tsx
这样就有了看版页面。
看板组件dashbaord/_components/PageContainer.tsx
中:
1 | 'use client' |
我们需要一个中间件来判断是否登陆,检测到未登陆则重定向到login.tsx
页。
创建一个文件:src/middleware.ts
:
1 | import { NextResponse } from "next/server"; |
这时我们会被强制拉到login
页面了。
登陆接口
创建app/api/admin/login/route.ts
用于登陆接口:
1
2
3
4
5
6
7
8
9
10
11
12import { NextRequest, NextResponse } from "next/server";
export const POST = (req: NextRequest) => {
return NextResponse.json({
success: true,
errorMessage: '登陆成功'
}, {
headers: {
'Set-Cookie': 'admin-token=123;Path=/'
}
})
}
安装Vscode插件:Thunder Client
便于直接测试接口。
安装完成后打开 Thunder Client 面板,new一个新的 POST
接口,直接发送一个localhost:3000/api/admin/login
请求,可以看到成功发送了。
1 | { |
并且可以在Response Headers中看到 set-cookie
被设置为:admin-token=123;Path=/
。
完成后我们在客户端加上发送登陆请求的代码:
只需要修改关键部分:
1 | import { useRouter } from "next/navigation"; |
1 | <Form labelCol={{span: 3}} onFinish={async (v) => { |
完成后就可以登陆跳转到看板了。
接下来我们来做一个后台面板,修改admin下的文件结构,将layout.tsx和dashboard单独合并在一起。
修改admin/(admin-layout)/layout.tsx
中的代码,将ant中的带有侧边导航的布局面板加入其中:
1 | import React from 'react'; |
并对login/page.tsx
页面做适当的修改(主要是样式的调整):
1 | "use client"; |
到这里项目代码开始有点复杂了,不太方便记录在笔记中。从下面开始只介绍一些新的概念。
About this Post
This post is written by Sy, licensed under CC BY-NC 4.0.