Next.js + primsa + NextAuth + Postgresql。
什么是Next.js Next.js 是一个用于构建全栈网络应用程序的 React 框架。你可以使用 React Components 构建用户界面,使用 Next.js 实现附加功能和优化。Next.js 抽象并自动配置 React 所需的工具,如捆绑、编译等。这样,开发者就可以专注于构建应用程序,而不必花时间进行配置。无论是个人开发者还是大型团队的一员,Next.js 都能帮助您构建交互式、动态和快速的 React 应用程序。
Next.js的主要功能
功能
描述
路由
基于文件系统的路由器构建于服务器组件之上,支持布局、嵌套路由、加载状态、错误处理等。
渲染
通过客户端和服务器组件进行客户端和服务器端渲染。利用 Next.js 进一步优化服务器上的静态和动态渲染。
数据获取
利用服务器组件中的 async/await 简化数据获取,并利用扩展的获取 API 实现请求备忘录化、数据缓存和重新验证。
样式设计
支持首选的样式设计方法,包括 CSS 模块、Tailwind CSS 和 CSS-in-JS
优化
图像、字体和脚本优化,以改善应用程序的核心网络性能和用户体验。
TypeScript
通过更好的类型检查和更高效的编译,以及自定义 TypeScript 插件和类型检查器,改进了对 TypeScript 的支持。
创建Next.js项目 1 2 3 4 5 6 7 8 9 $ 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
当使用create-next-app命令创建一个新的Next.js项目时,你将会被询问一系列选项,这些选项将决定项目的初始配置。以下是每个选项的含义:
项目名称:这是项目的名称,可以输入任何你喜欢的名称。
是否使用TypeScript:这个选项决定是否在项目中使用TypeScript作为主要的编程语言。如果你选择”Yes”,那么项目将会被初始化为TypeScript项目。选择使用TypeScript意味着你希望在项目中使用静态类型检查,以提高代码的可靠性和可维护性。TypeScript还可以帮助你在开发过程中捕获潜在的错误,并提供更好的代码编辑体验。此外,TypeScript还可以提供更好的代码自动补全和文档提示,以及更好的重构支持。因此,你对TypeScript可能有以下期望和需求:
静态类型检查:希望通过类型检查来捕获潜在的错误,提高代码的可靠性。
更好的编辑体验:希望在编辑器中获得更好的代码自动补全、文档提示和重构支持。
可维护性:希望通过类型定义和接口来提高代码的可读性和可维护性。
生态系统支持:希望利用TypeScript丰富的生态系统和社区支持,以便更好地开发和维护项目。
是否使用ESLint:ESLint是一个用于代码规范和错误检查的工具。选择”Yes”将会在项目中集成ESLint。ESLint可以根据预定义的规则集或自定义规则来检查代码,以确保代码的一致性、可读性和安全性。它还可以集成到开发工具和持续集成流程中,提供实时的代码检查和反馈。ESLint的主要作用是帮助开发团队维护一致的代码风格,减少潜在的错误和漏洞,并提高代码质量和可维护性。使用ESLint有以下优点:
自动检测潜在漏洞:ESLint可以自动检测潜在的漏洞和安全问题,帮助你在开发过程中及早发现并修复这些问题,从而提高代码的安全性。
可定制的规则:ESLint允许你根据项目的特定需求定制规则,这意味着你可以针对项目的特定情况设置规则,从而更好地关注对项目最相关的问题。
与其他工具的集成:ESLint可以轻松集成到其他工具和流程中,例如编辑器、构建工具和持续集成/持续部署(CI/CD)流程中,使其成为确保代码安全性的便捷选择。
是否使用Tailwind CSS:Tailwind CSS是一个实用的CSS框架,选择”Yes”将会在项目中集成Tailwind CSS。使用Tailwind CSS的优点包括:
快速开发:Tailwind CSS提供了大量的预定义样式类,可以快速构建页面和组件,节省了开发时间。
可定制性:Tailwind CSS允许开发人员根据项目需求自定义样式,而不需要编写自定义的CSS代码,使得样式更易于维护和管理。
一致性:通过使用Tailwind CSS的预定义样式类,可以确保项目中的样式具有一致的外观和行为,减少了样式的不一致性。
响应式设计:Tailwind CSS提供了丰富的响应式设计类,可以轻松地创建适应不同屏幕尺寸的布局和样式。
生态系统和社区支持:Tailwind CSS拥有庞大的生态系统和活跃的社区,提供了大量的插件、工具和资源,为开发人员提供了丰富的支持和帮助。
是否使用src/
目录:这个选项决定是否在项目中使用src/
目录来存放源代码文件。
是否使用App Router(推荐):App Router是Next.js的路由系统,选择”Yes”将会在项目中使用App Router。Next.js 的 App Router 是 Next.js 框架中的核心功能之一,它提供了一种用于处理路由的简单且强大的方式。下面是 App Router 的几个优点:
服务端渲染(Server-side Rendering):App Router 支持在服务器上进行页面渲染,这意味着用户在访问页面时可以立即看到内容,而不需要等待 JavaScript 的加载和执行。这提供了更好的性能和用户体验,并对搜索引擎优化(SEO)也有积极影响。
动态路由:App Router 允许您使用动态路由构建灵活的页面结构。您可以在路由中使用参数,从而根据参数的不同加载不同的内容。例如,您可以创建一个动态路由来显示特定用户的个人资料页面。
文件系统路由:Next.js 的 App Router 可以根据文件系统的结构自动创建路由。这意味着您可以在 pages 目录中创建一个文件,并将其自动映射到对应的 URL 路径上,无需手动配置路由。这种简单而直观的路由配置方式使得开发过程更加高效。
客户端导航:App Router 还提供了客户端导航功能,可以在不刷新整个页面的情况下进行导航。这种无需重新加载整个页面的导航方式使得用户体验更加流畅,并且能够在应用程序中实现各种交互效果。
生命周期钩子:App Router 提供了一些生命周期钩子函数,您可以在路由切换前后执行特定的操作。例如,您可以在路由切换前加载数据,或者在路由切换后进行一些清理操作。这使得处理页面之间的状态转换和数据加载变得更加灵活和可控。
是否自定义默认导入别名(@/):这个选项决定是否自定义默认的导入别名。如果你选择”Yes”,你可以输入你想要的导入别名。在 Next.js 中自定义默认导入别名(如 @ 或 )具有以下几个优点:
简化导入路径:通过设置默认导入别名,可以将导入路径简化为更短、更易读的形式。例如,可以将长路径 import { Component } from '../../components'
简化为 import { Component } from '@/components'
。这样一来,代码中的导入语句更加清晰和易于理解。
提高可维护性:使用默认导入别名可以减少代码中的硬编码路径,并使代码更具可维护性。如果项目中的文件结构发生变化,只需要更新别名配置,而不需要在整个代码库中修改导入路径。
简化重构:当需要对项目进行重构或重新组织时,使用默认导入别名可以让这个过程更加简单和安全。您可以通过修改别名配置来实现文件移动或重命名,而无需修改所有相关的导入语句。
提高开发效率:默认导入别名可以减少编码时的重复劳动。您可以快速且一致地引用项目中的不同模块,而无需记住复杂的相对路径。这样可以节省时间和精力,提高开发效率。
跨平台兼容性:默认导入别名是一种通用的技术,在很多前端工具和框架中都可以使用。这意味着,如果您在将来决定切换到不同的工具或框架,您可以轻松地迁移别名配置,而无需修改大量的导入语句。
Next.js的典型目录结构 以使用Next.js 14 + next-auth + prisma + edgeStore
构建博客网站为例。
root
app
(auth)
(site)
about
access
blog/[id]
contact
create
posts
userposts
globals.css
layout.tsx
page.tsx
actions
blogAction.ts
getCurrentUser.ts
api
[post]/[id]
edgestore/[…edgestore]
components
shared
Footer.tsx
Navbar.tsx
Posts.tsx
CreateForm.tsx
BlogCard.tsx
DeletePost.tsx
ui
Button.tsx
Form.tsx
Input.tsx
Map.tsx
Overlay.tsx
Route.tsx
SingleImageDropZone.tsx
Tag.tsx
constants
context
hooks
lib
prisma
public
assets
about.jpg
post1.jpg
post2.jpg
types
postTypes.ts
userTypes.ts
utils
.env
.gitignore
next.config.js
package-lock.json
package.json
postcss.config.js
tailwind.config.js
tsconfig.json
关于primsa 首先,确保已经在项目中安装了Prisma CLI,并且已经初始化了Prisma项目。可以使用以下命令来安装Prisma CLI并初始化项目:1 2 npm install @prisma/cli --save-dev npx prisma init
在Prisma项目初始化完成后,你需要编辑Prisma的schema文件(通常是prisma/schema.prisma,代码在后面),定义你的数据库模型和连接信息。
定义好Prisma的schema后,你可以使用Prisma CLI来生成Prisma Client。运行以下命令来生成Prisma Client:
生成Prisma Client后,你可以在Next.js项目中的API路由或页面组件中使用Prisma Client来连接和操作数据库。以下是一个简单的示例:1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 import prisma from '../lib/prisma' ;export default function Home ({ users } ) { return ( <div> <h1>用户列表</h1> <ul> {users.map((user) => ( <li key={user.id}>{user.name}</ li> ))} </ul> </ div> ); } export async function getServerSideProps ( ) { const users = await prisma.user.findMany(); return { props: { users }, }; }
当数据库的结构发生变化时,你需要更新Prisma的schema文件以反映这些变化,并生成新的Prisma Client。以下是操作步骤:
更新Prisma Schema:根据数据库结构的修改,你需要更新Prisma的schema文件(通常是prisma/schema.prisma),修改或添加相应的数据模型、关系和字段定义。
生成Prisma Client:运行以下命令来生成新的Prisma Client,以便它能够反映数据库结构的变化
数据迁移:如果数据库结构的变化需要进行迁移操作,你可以使用Prisma Migrate来创建和应用数据库迁移。运行以下命令来创建一个新的迁移:1 npx prisma migrate dev --name your-migration-name
实现email注册/登录的方法 .env文件 1 2 3 4 5 6 7 8 9 10 DATABASE_URL =postgresql://admin:password6@服务.器主.机.地址:5432 /blogEMAIL_SERVER =smtp://1234566 @qq.com:password@smtp.qq.com:587 EMAIL_FROM =1234567 @qq.comEDGE_STORE_ACCESS_KEY =xxxxxxxEDGE_STORE_SECRET_KEY =xxxxxxx
schema.prisma文件 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 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 datasource db { provider = "postgresql" url = env("DATABASE_URL" ) } generator client { provider = "prisma-client-js" } model Account { id String @id @default (cuid()) userId String type String provider String providerAccountId String refresh_token String ? access_token String ? expires_at Int? token_type String ? scope String ? id_token String ? session_state String ? user User @relation(fields: [userId], references : [id], onDelete : Cascade) @@unique([provider, providerAccountId]) } model Session { id String @id @default (cuid()) sessionToken String @unique userId String expires DateTime user User @relation(fields: [userId], references : [id], onDelete : Cascade) } model User { id String @id @default (cuid()) name String ? email String ? @unique emailVerified DateTime? image String ? accounts Account[] sessions Session[] Blog Blog[] } model VerificationToken { id String @id @default (cuid()) identifier String token String @unique expires DateTime @@unique([identifier, token]) } model Blog { id String @id @default (cuid()) createdAt DateTime @default (now()) title String description String img String ? category String userEmail String featured Boolean @default (false ) topPost Boolean @default (false ) user User @relation(fields: [userEmail], references : [email]) }
app/(auth)/api/auth/[…nextauth]/route.ts内容 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 import NextAuth, { AuthOptions } from "next-auth" ;import EmailProvider from "next-auth/providers/email" ;import { PrismaAdapter } from "@auth/prisma-adapter" ;import client from "@/lib/prismadb" ;export const authOptions: AuthOptions = ({ adapter: PrismaAdapter(client), providers: [ EmailProvider({ server: process.env.EMAIL_SERVER as string , from : process.env.EMAIL_FROM as string , }), ], debug: process.env.NODE_ENV === "development" , secret: process.env.NEXTAUTH_SECRET }) const handler = NextAuth(authOptions)export { handler as GET, handler as POST}
context/AuthContext.tsx内容 1 2 3 4 5 6 7 8 9 10 11 12 13 "use client"; import { SessionProvider } from "next-auth/react"; export interface AuthContextProps { children: React.ReactNode; } export default function AuthContext({ children, }: AuthContextProps) { return <SessionProvider>{children}</SessionProvider>; }
(site)/layout.tsx内容 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 30 31 32 33 34 35 36 37 38 39 import type { Metadata } from 'next' import { Roboto } from 'next/font/google' import './globals.css' import Navbar from '@/components/shared/Navbar' import Footer from '@/components/shared/Footer' import AuthContext from '@/context/AuthContext' import getCurrentUser from '../actions/getCurrentUser' import { EdgeStoreProvider } from '@/lib/edgestore' const roboto = Roboto({ subsets: ['latin'], weight: ["100","400","700","900"] }) export const metadata: Metadata = { title: 'ExploreX', description: 'Travel Blog', } export default async function RootLayout({ children, }: { children: React.ReactNode }) { const user = await getCurrentUser() return ( <html lang="en"> <AuthContext> <EdgeStoreProvider> <body className={`${roboto.className} overflow-x-hidden bg-light`}> <Navbar user={user}/> {children} <Footer/> </body> </EdgeStoreProvider> </AuthContext> </html> ) }
后端 types内定义的两个类型文件 postTypte.ts 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import { User } from "@prisma/client" ;export interface PostTypes { id: string ; title: string ; img: string | null ; desc: string ; featured: boolean ; topPost: boolean ; category: string ; authorImage: string ; authorName: string ; publishDate: string ; createdAt: string | Date user: User; }
userTypes.ts 1 2 3 4 5 6 7 export type userTypes = { id: string ; name: string | null ; email: string | null ; emailVerified: Date | string | null ; image: string | null ; } | null ;
Prismadb.ts内容 1 2 3 4 5 6 7 8 9 10 11 import { PrismaClient } from "@prisma/client" ;declare global { var prisma: PrismaClient | undefined } const client = globalThis.prisma || new PrismaClient()if (process.env.NODE_ENV !== "production" ) globalThis.prisma = client export default client
页面访问后端 userposts/page.tsx 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 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 import client from "@/lib/prismadb"; import getCurrentUser from "@/app/actions/getCurrentUser"; import BlogCard from "@/components/shared/BlogCard"; import DeletePosts from "@/components/shared/DeletePosts"; const page = async () => { const user = await getCurrentUser(); const posts = await client.blog.findMany({ where: { userEmail: user?.email ?? undefined }, orderBy: { createdAt: "desc" }, include: { user: true } }) return ( <div className="w-full flex justify-center items-center"> {!user ? ( <h1 className="text-3xl font-extrabold">Sign in to view your posts.</h1> ) : (<div className="max-w-[90%] mx-auto"> <div className="w-full text-center mb-10"> <h1 className="text-3xl font-extrablold text-tertiary"> Hello { user?.name } </h1> <span className="text-lg"> You have published {posts.length} posts. </span> </div> <div className="grid md:grid-cols-2 grid-cols-1 justify-center items-center gap-10"> {posts.map((post) => ( <div key={post.id} className="relative"> <BlogCard post={post as any}/> <DeletePosts post={post as any} /> </div> ))} </div> </div>)} </div> ) } export default page;
Post数据 app/(site)/create/page.tsx 1 2 3 4 5 6 7 8 9 10 11 import getCurrentUser from "@/app/actions/getCurrentUser"; import CreateForm from "@/components/shared/CreateForm"; const page = async () => { const user = await getCurrentUser(); return ( <CreateForm user={user} /> ) } export default page
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 30 31 32 33 34 35 "use client" import { useRef, FormEvent } from "react" import { ReactNode } from "react" interface FormProps { children: ReactNode action: (formData: FormData) => Promise<void | boolean> className?: string onSubmit?: (e: FormEvent<HTMLFormElement>) => void } const Form = ({ children, action, className, onSubmit }: FormProps) => { const ref = useRef<HTMLFormElement>(null) return ( <form className={className} onSubmit={onSubmit} ref={ref} action={async (formData) => { await action(formData) ref.current?.reset() }} > {children} </form> ) } export default Form
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 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 "use client" import Form from "../ui/Form"; import Input from "../ui/Input"; import { useEdgeStore } from "@/lib/edgestore"; import { useState, useEffect } from "react"; import Button from "../ui/Button"; import { SingleImageDropzone } from "../ui/SingleImageDropZone"; import { userTypes } from "@/types/userTypes"; import { createPost } from "@/app/actions/blogActions"; const CreateForm = ({user}: {user:userTypes}) => { const [file, setFile] = useState<File>(); const {edgestore} = useEdgeStore(); const [imagePath, setImagePath] = useState(""); const uploadImageHandler =async () => { if (file) { const res = await edgestore.publicFiles?.upload({ file, }); setImagePath(res.url); } } useEffect(() => { if (file) { uploadImageHandler(); } }, [file]); return ( <div className="mt-8 mx-auto w-full max-w-3xl px-4"> <div className="bg-white py-8 shadow rounded-lg px-10"> <h1 className="text-center text-2xl font-extrabold mb-10"> Create a Post </h1> {!user ? ( <h2 className="text-center text-xl font-extrabold uppercase"> Please Sign or Log in to create a post! </h2> ): ( <> <SingleImageDropzone onChange={(file) => { setFile(file) }} value={file} width={200} height={200} /> <Form action={createPost} className="flex flex-col gap-5 mt-5" onSubmit={() => setFile(undefined)} > <Input type="hidden" name="image" value={imagePath} /> <Input name="title" type="text" placeholder="Enter Title" /> <textarea name="description" rows={10} placeholder="Write Here..." className="text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-primary sm:text-sm sm:leading-6 border w-full border-gray-200 p-2 rounded-md py-1.5" > </textarea> <select required name="category" className="text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-primary sm:text-sm sm:leading-6 border w-full border-gray-200 p-2 rounded-md py-1.5" > <option value="" disabled selected> Choose Tag </option> <option value="Adventure"> Adventure </option> <option value="Culture"> Culture </option> <option value="Journey"> Journey </option> <option value="Wanderlust"> Wanderlust </option> </select> <Input name="email" type="hidden" value={user.email || ""} /> <Button type="submit" text="Create" aria="create blog" /> </Form> </> ) } </div> </div> ) } export default CreateForm
app/actions/blogActions.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 30 31 32 33 34 35 36 37 "use server" import client from "@/lib/prismadb" ;import { revalidatePath } from "next/cache" ;export async function createPost (formData: FormData ) { const title = formData.get("title" ) as string ; const desc = formData.get("description" ) as string ; const cat = formData.get("category" ) as string ; const userEmail = formData.get("email" ) as string ; const image = formData.get("image" ) as string ; await client.blog.create({ data: { img: image, title: title, description: desc, category: cat, userEmail: userEmail } }); revalidatePath("/create" ); } export async function deletePost (formData: FormData ) { const id = formData.get("postId" ) as string ; console .log('the id is:' + id); await client.blog.delete({ where: { id: id } }); revalidatePath("/userposts" ); }
tailwind.config.ts 自定义配色 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 import type { Config } from 'tailwindcss' const config: Config = { content: [ './pages/**/*.{js,ts,jsx,tsx,mdx}' , './components/**/*.{js,ts,jsx,tsx,mdx}' , './app/**/*.{js,ts,jsx,tsx,mdx}' , ], theme: { extend: { colors: { primary: "#eb5e28" , secondary: "#252422" , tertiary: '#403d39' , light: "#fffcf2" , } }, }, plugins: [], } export default config
几种文件类型的区别 JavaScript (JS)、TypeScript (TS) 和 TypeScript with JSX (TSX) 文件的异同如下:
JavaScript (JS) 文件:
JS文件是纯粹的JavaScript代码文件,其中包含的代码是标准的JavaScript语法。
JS文件通常以.js为扩展名,例如:example.js。
JS文件中的代码不会经过类型检查,因此在开发过程中需要更加小心地处理类型相关的问题。
TypeScript (TS) 文件:
TS文件包含的是使用TypeScript语言编写的代码,它是JavaScript的超集,提供了静态类型检查和其他高级特性。
TS文件通常以.ts为扩展名,例如:example.ts。
TS文件中的代码会经过类型检查,可以在开发过程中提供更好的类型安全和代码提示。
TypeScript with JSX (TSX) 文件:
TSX文件是TypeScript的一种变体,用于编写包含JSX语法的代码,通常用于React应用程序的组件开发。
TSX文件通常以.tsx为扩展名,例如:example.tsx。
TSX文件中的代码除了经过类型检查外,还可以包含JSX语法,用于编写React组件。
总的来说,JS文件是标准的JavaScript代码文件,TS文件是使用TypeScript语言编写的代码文件,而TSX文件是用于编写包含JSX语法的TypeScript代码文件,它们在语法和类型检查方面有所不同,适用于不同的开发场景和需求。