最近,我完成了一个新的网站项目,是用来收录 Github Profile 和 Readme Components。一开始,我并没有计划将其发展成一个全栈应用,包括点赞和收藏在内的一些功能都是基于本地存储的。然而,随着网站发布后的逐渐迭代,我感觉增加全栈支持可能是一个必要的方向。不仅有助于项目之后的扩展,还能提升用户体验。于是,我在工作之余大概做了一周多的时间将项目转变为一个完整的全栈网站。下面是我的项目链接。
完成网站的全栈版本后,我就想着写一篇文章记录一下,本篇的所有代码你都可以在此项目中找到。我接下来会逐步分享关于登录认证、Prisma 与 PostgreSQL 的使用等内容,最终带你了解一个全栈网站的建站过程。
技术栈
- 全栈框架 Next.js
- 数据库工具 Prisma
- 数据库 PostgreSQL
- 身份认证 NextAuth
- 部署平台 Vercel
初始化项目
首先在终端运行 npx create-next-app@latest
命令,然后按照提示进行配置
What is your project named? my-app
Would you like to use TypeScript? No / Yes
Would you like to use ESLint? No / Yes
Would you like to use Tailwind CSS? No / Yes
Would you like to use `src/` directory? No / Yes
Would you like to use App Router? (recommended) No / Yes
Would you like to customize the default import alias (@/*)? No / Yes
What import alias would you like configured? @/*
运行成功后,进入项目目录并执行 npm run dev
确保启动成功后就可以开始搭建基础环境
- 首先将项目上传至 GitHub,创建仓库并链接远程地址,然后推送代码
- 进入 vercel 官网 https://vercel.com/,登录并选择 GitHub 进行绑定。上传项目时,在 Configure Project 保持默认设置,直接点击 Deploy。环境变量可以在需要的时候进行调整。静等自动部署完成。
- 部署成功后,创建数据库。首先,在 Storage tab 中选择 PostgreSQL,输入名称和选择区域后创建。创建后数据库就会与你的项目链接在一起。
本地代码拉取环境变量:
- 运行
npm i -g vercel@latest
安装 Verlcel CLI - 运行
verlcel link
链接你的 vercel 项目 - 运行
vercel env pull .env
拉取最新的环境变量
- 运行
连接数据库
运行 npm install prisma --save-dev
安装 Prisma CLI,并创建一个 Prisma
文件夹,在其中添加schema.prisma
文件,然后添加几个模型,内容如下:
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("POSTGRES_PRISMA_URL") // uses connection pooling
directUrl = env("POSTGRES_URL_NON_POOLING") // uses a direct connection
}
model Post {
id String @default(cuid()) @id
title String
content String?
published Boolean @default(false)
author User? @relation(fields: [authorId], references: [id])
authorId String?
}
model User {
id String @default(cuid()) @id
name String?
email String? @unique
createdAt DateTime @default(now()) @map(name: "created_at")
updatedAt DateTime @updatedAt @map(name: "updated_at")
posts Post[]
@@map(name: "users")
}
Prisma 是通过 model 来定义表结构,比如上面就是建了一个 Post 表和一个 User 表,然后这两个表通过 User 中的 posts 和 Post 中的 author 建立了一个一对多的表关系。
然后运行 npx prisma db push
命令把定义的数据模型同步到远程数据库中。然后你应该能看见下面的提示。说明表已经创建成功
Your database is now in sync with your schema.
然后运行 npx prisma studio
在 localhost:5555
中就可以看到你的表结构,当然现在都是空数据。
获取数据库信息到视图中
获取数据库数据到视图中,首先需要安装 @prisma/client
, 运行命令 npm install @prisma/client
然后运行 npx prisma generate
,注意之后每次修改 Prisma Schema 文件时,都需要运行这个命令,它会生成与表结构对应的 TypeScript 或 JavaScript 代码,用于执行数据库查询、插入、更新和删除等操作的函数,以及相关的类型定义。
接下来,在项目中创建一个 prisma.ts 文件,通常可以放在 lib 文件夹下。在该文件中添加以下代码:
import { PrismaClient } from '@prisma/client';
let prisma: PrismaClient;
if (process.env.NODE_ENV === 'production') {
prisma = new PrismaClient();
} else {
if (!global.prisma) {
global.prisma = new PrismaClient();
}
prisma = global.prisma;
}
export default prisma;
这段代码用于配置并导出一个 Prisma 实例。在生产环境中,每次请求都会创建一个新的 PrismaClient 实例,而在开发环境中,它会重复使用全局的实例以防止数据连接数耗尽。这样的设置可以有效提高性能并减少资源占用。
然后 PrismaClient 中有增删改查四类基本 API,具体可以看CRUD
const user = await prisma.user.create({
data: {
email: '[email protected]',
name: 'Elsa Prisma',
},
})
const user = await prisma.user.findUnique({
where: {
email: '[email protected]',
},
})
const updateUser = await prisma.user.update({
where: {
email: '[email protected]',
},
data: {
name: 'Elsa',
},
})
const deleteUser = await prisma.user.delete({
where: {
email: '[email protected]',
},
})
比如你想查询一条 Post 数据
export const getServerSideProps: GetServerSideProps = async ({ params }) => {
const post = await prisma.post.findUnique({
where: {
id: String(params?.id),
},
include: {
author: {
select: { name: true },
},
},
});
return {
props: post,
};
};
NextAuth 认证登录
首先安装两个依赖 next-auth
, @next-auth/prisma-adapter
npm install next-auth @next-auth/prisma-adapter
然后修改用户相关的模型
model User {
id String @id @default(uuid())
name String
email String? @unique
emailVerified DateTime? @map("email_verified")
image String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
posts Post[]
@@map("users")
}
model Account {
id String @id @default(cuid())
userId String @map("user_id")
type String?
provider String
providerAccountId String @map("provider_account_id")
token_type String?
refresh_token String? @db.Text
access_token String? @db.Text
expires_at Int?
scope String?
id_token String? @db.Text
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@unique([provider, providerAccountId])
@@map("accounts")
}
model Session {
id String @id @default(cuid())
userId String? @map("user_id")
sessionToken String @unique @map("session_token") @db.Text
accessToken String? @map("access_token") @db.Text
expires DateTime
user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@map("sessions")
}
model VerificationRequest {
id String @id @default(cuid())
identifier String
token String @unique
expires DateTime
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([identifier, token])
}
然后,以 GitHub 为例,你需要创建一个 OAuth App。按照以下步骤进行:
- 登录 GitHub 账户,然后点击 Settings。
- 在 Settings 页面底部找到 Developer Settings,切换到 OAuth Apps。
- 点击 "Register a new application",填写名称和域名。在本地开发环境下,Homepage URL 可以填写
http://localhost:3000
,Authorization callback URL 可以填写http://localhost:3000/api/auth/callback/github
。
创建成功后,复制生成的 Client ID 和 Client Secret,并将它们添加到你项目中的 .env
文件中:
GITHUB_CLIENT_ID=
GITHUB_CLIENT_SECRET=
SECRET= // 这个是生产环境必须的,内容可自定义
这样,你的项目就可以通过这些凭证进行 GitHub OAuth 认证了。另外要确保 .env
文件不要泄漏出去,现在就可以把它添加到 .gitignore
然后需要在你的根组件加入以下代码
import { SessionProvider } from 'next-auth/react';
const Home = () => {
return (
...
);
};
export default Home;
然后,创建一个特定的路由 api/auth/[...nextauth].ts
,供 NextAuth 使用,并添加以下代码
// api/auth/[...nextauth].ts
import { NextAuth, NextAuthOptions } from 'next-auth';
import { PrismaAdapter } from '@next-auth/prisma-adapter';
import GitHubProvider from 'next-auth/providers/github';
import prisma from '@/lib/prisma';
const authOptions: NextAuthOptions = {
providers: [
GitHubProvider({
clientId: process.env.GITHUB_CLIENT_ID as string,
clientSecret: process.env.GITHUB_CLIENT_SECRET as string,
httpOptions: {
timeout: 10000, // 等待响应时间,因为本地环境经常登录超时,所以改了这个配置
}
})
],
adapter: PrismaAdapter(prisma),
secret: process.env.SECRET, // 目前生产环境是必须的
callbacks: {
// 调用 getSession 和 useSession 时会触发
// 文档可查看 https://next-auth.js.org/configuration/callbacks
async session({ session, user }) {
if (user.id && session?.user) {
session.user.userId = user.id;
}
return session;
}
}
};
const handler = NextAuth(authOptions);
export { handler as GET, handler as POST };
然后在你想点击登录的地方加入以下代码,正常情况就会进入 github 授权页面。
import { signIn } from 'next-auth/react';
const clickSignIn = () => {
signIn('github')
}
获取具体的用户会话信息可以通过以下API
- Client:
useSession
,getSession
- Server:
getServerSession
import { useSession } from 'next-auth/react';
function MyComponent() {
const { data: session } = useSession();
if (session) {
console.log('Logged in as:', session.user);
}
// ...
}
import { getSession } from 'next-auth/react';
export async function getServerSideProps(context) {
const session = await getSession(context);
if (session) {
console.log('Logged in as:', session.user);
}
return {
props: {},
};
}
import { getServerSession } from 'next-auth/react';
export async function POST(req: NextRequest) {
const session = await getServerSession();
const userId = session?.user?.userId;
// validation session
if (!userId) {
return responseFail(HTTP_CODE.NOT_LOGGED);
}
//...
}
Vercel 部署注意事项
- build 命令前需要运行
prisma generate
,所以 package.json 中的 build 命令可以改为
"scripts": {
"build": "prisma generate && next build"
}
- 由于 GitHub OAuth App 中的回调路径只支持一个,因此需要为不同环境创建多个 OAuth App。在 Vercel 上设置环境变量,将各个 OAuth App 的具体密钥添加到环境变量中。Vercel 是支持多环境变量的设置。
- 确保将
.env
文件添加到.gitignore
中,不要暴露出去
遇到的坑
- 在进行认证登录时非常容易失败,经过多次排查仍未找到具体原因,暂时将其归为网络错误,如果你在认证失败时可以尝试切换 wifi 或者切换你的 VPN 节点,当然这主要是在本地开发环境中出现的问题,生产环境还是很丝滑的。相关讨论 issue
- 在 Next.js 14 中,如果直接将
request
对象传递给getServerSession
会导致直接报错,可以尝试像我一样按照下面的写法,然后在需要使用的地方引入这个函数
import { getServerSession as originalGetServerSession } from 'next-auth/next';
export const getServerSession = async () => {
const req = {
headers: Object.fromEntries(headers() as Headers),
cookies: Object.fromEntries(
cookies()
.getAll()
.map((c) => [c.name, c.value])
)
} as any;
const res = { getHeader() {}, setCookie() {}, setHeader() {} } as any;
const session = await originalGetServerSession(req, res, authOptions);
return session;
};
- 在使用 GitHub、Google 等认证登录平台时,如果邮箱是唯一的,会导致登录失败。这是由于在模型中,邮箱被定义为唯一值
- 我最初写用户相关模型时按照官网的示例一直提示类型错误,如果你也是这样,可以尝试我上面写的那一套基础模型
总结
本篇文章只是大致总结了一个全栈网站的建站过程,其中有一些细节没有写到,但是你按照这个流程进行你的网站搭建是没有问题的。有问题欢迎讨论交流