什么是 Next.js Next.js 是一个用于构建全栈 Web 应用程序的 React 框架。您可以使用 React Components 来构建用户界面,并使用 Next.js 来实现附加功能和优化。
安装项目 请确保你的操作系统上安装了 Node.js 18.17 或更高版本。
我们采用官网上的自动安装,这样会方便很多
1 2 3 4 5 6 7 8 9 10 npx create-next-app @latest
这样就得到了一个 Next.js 的项目
安装其他的插件 1 2 pnpm add zod pnpm add bcrypt
项目简介 这里提醒一下 next.js 是默认在服务端渲染的框架,对于需要在浏览器中渲染的内容,当然这也就说明你需要浏览器的 api 或者对应的 react hooks。那么你需要在文件的最顶部添加 use client
。记住 use server
使用在函数里面的,而不是 tsx 的样式组件中,表明该函数在服务端使用。
app 由于我们采用的 APP 路由模式,在 Next.js 中文件既是路由。也就是说这是约定熟成的规则。
1 2 app\\layout.tsx app\\page.tsx
除了这 2 个必要的 tsx 文件,还有其他几个常用的文件,剩余的可以查询官方文档 。
layout
.js
.jsx
.tsx
Layout
这里是布局页面内容
page
.js
.jsx
.tsx
Page
对应路由页面展示的内容
loading
.js
.jsx
.tsx
Loading UI
页面加载中的样式页面
not-found
.js
.jsx
.tsx
Not found UI
404 页面内容
error
.js
.jsx
.tsx
Error UI
当页面上出现了错误的时候可以在此页面中处理
global-error
.js
.jsx
.tsx
Global error UI
route
.js
.ts
API endpoint
你可以在这里编写接口用来处理 get post 这样的接口内容
template
.js
.jsx
.tsx
Re-rendered layout
default
.js
.jsx
.tsx
Parallel route fallback page
这里提供我遇到的文件写法案例。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 import TitleHeader from "@/app/ui/mainLayout/title-header" ;import SideNav from "@/app/ui/mainLayout/side-nav" ;import RightContainer from "@/app/ui/mainLayout/right-container" ;export default function Layout ({ children, }: Readonly <{ children: React.ReactNode; }> ) { return ( <main className ="flex h-screen" > <SideNav > </SideNav > <RightContainer > <TitleHeader > </TitleHeader > <div className ="w-full px-8" > {children}</div > </RightContainer > </main > ); }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 export default function Page ({ searchParams, params, }: { searchParams?: { query?: string ; page?: string ; }; params?: { id: number ; }; } ) { const query = searchParams?.query || "" ; const currentPage = Number (searchParams?.page || 1 ); return <> {query} </> ; }
1 2 3 4 5 6 7 8 9 10 11 12 export default function Loading ( ) { return <> 加载中...</> ; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 "use client" ;import ConversationFallback from "@/components/shared/conversation/ConversationFallback" ;import { useRouter } from "next/navigation" ;import { useEffect } from "react" ;export default function Error ({ error }: { error: Error } ) { const router = useRouter (); useEffect (() => { router.push ("/conversations" ); }, [error, router]); return <ConversationFallback /> ; }
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 export async function POST ( ) { try { return new Response (JSON .stringify ({ code : 200 , msg : "传输成功" }), { headers : { "Content-Type" : "application/json" , }, }); } catch (error) { return new Response (JSON .stringify ({ code : 500 , msg : "服务器内部错误" }), { status : 500 , headers : { "Content-Type" : "application/json" , }, }); } } const response = await fetch ("/dashboard/api" , { method : "POST" });import { type NextRequest } from "next/server" ;import { prisma } from "@/app/lib/prisma" ;export async function GET (request : NextRequest ) { const searchParams = request.nextUrl .searchParams ; const coserId = searchParams.get ("coserId" ); const query = searchParams.get ("query" ); const limit = parseInt (searchParams.get ("limit" ) || "10" , 10 ); const offset = parseInt (searchParams.get ("offset" ) || "0" , 10 ); let results = []; return new Response ( JSON .stringify ({ results : results, ok : true , }), { headers : { "Content-Type" : "application/json" }, } ); } let res = await fetch ( `/dashboard/cosers/filter?query=${filterText} &offset=0&limit=20&coserId=${coserId} ` , { signal, } );
1 2 3 4 5 export default function NotFound ( ) { return <main > 404咯</main > ; }
public 公共资源文件夹,这儿通常存放图片或一些公共文件。例如public\\next.svg
在组件上使用的地址就是/next.svg
。
next.config.mjs next.js 的配置文件,这里的具体情况可以查看官网文档 。我这里提供我用的一个案例,就是重定向。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 /** @type {import('next' ).NextConfig} */ const nextConfig = { async redirects() { return [ { source : "/" , destination : "/conversations" , permanent : true }, ] } }; export default nextConfig;
数据库连接(可选) 说明以及安装 在连接数据库之前,我们需要了解数据库和 orm 之间的关系,orm 是一种数据关系映射的工具,它用作编写纯 SQL 或使用其他数据库访问工具。这样的话,我们不用关心多种数据库的连接,改变数据库版本之后要修改代码等等问题。
1 pnpm install prisma --save-dev
1 2 npx prisma // 获取命令 npx prisma init // 初始化prisma
初始化之后会得到 prisma 的文件夹(如果你初始化失败,八成是你的网络连不上国外的网络)。
新建.env
文件,这个是本地的环境变量设置
1 2 3 4 DATABASE_URL= "file:./dev.db" DATABASE_URL= "postgresql://USER:PASSWORD@HOST:PORT/DATABASE?schema=public" DATABASE_URL= "mysql://USER:PASSWORD@HOST:PORT/DATABASE"
prisma 配置 上面新建的环境变量文件,那么就需要 prisma 来连接数据库了。
打开prisma\\schema.prisma
文件
1 2 3 4 5 6 7 8 9 10 generator client { provider = "prisma-client-js" } datasource db { provider = "postgresql" url = env("DATABASE_URL" ) }
创建数据库 添加如下的模型到prisma\\schema.prisma
文件中
1 2 3 4 5 6 model User { id Int @id @default(autoincrement()) email String @unique name String? }
之后做如下操作,同步本地的表结构到数据库中。
1 npx prisma migrate dev --name init
当然你可能会修改数据库的表结构什么,为了方便可以在package.json
把启动命令配置一下。更新表结构至线上数据库中使用npx prisma db push
如果是一个已经有数据的项目,就不能使用这个命令了,转而使用 prisma migrate
迁移。本文先不涉及。
1 2 3 4 5 6 7 8 9 10 11 { ..... "scripts" : { "dev" : "npm run prisma:generate && next dev" , "build" : "npm run prisma:generate && next build" , "start" : "next start" , "lint" : "next lint" , "prisma:generate" : "prisma generate" , } , ....... }
Next.js 客户端上使用 安装客户端
然后在 lib/prisma.ts
中写入
1 2 import { PrismaClient } from "@prisma/client" ; export const prisma = new PrismaClient();
之后你可以在任意地方引入然后编写数据库操作语句。例如
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 import { prisma } from "@/app/lib/prisma" ; export async function fetchCosplayPagesByCoserId({ coserId, itemsPrePage = ITEMS_PER_PAGE, }: { coserId: number | string; itemsPrePage?: number; }) { try { const count = await prisma.posts.count({ where : { coser_id: Number(coserId), status: { not: 2, }, }, }); const totalPages = Math.ceil(count / itemsPrePage); return totalPages; } catch (error) { console.error("数据库错误" , error); throw new Error(`获取指定Coser的作品数量错误${error} `); } }
你也可以使用npx prisma --help
查看更多的语法
Next-auth(可选) 这是 Next.js 出品的用户验证插件,主要是完成像用户登录、会话管理、校验流程。具体查看官网
首选正常我们在页面中登录。
1 2 3 4 5 6 7 8 9 10 11 import { authenticate } from "@/app/lib/actions" ;export default function Page ( ) { return ( <form action ={authenticate} > <input type ="email" name ="email" placeholder ="Email" required /> <input type ="password" name ="password" placeholder ="Password" required /> <button type ="submit" > Login</button > </form > ); }
我们在app/lib/actions.tsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 "use server" ;import { signIn } from "@/auth" ;import { authorror } from "next-auth" ;export async function authenticate ( prevState : string | undefined , formData : FormData ) { try { await signIn ("credentials" , formData); } catch (error) { if (error instanceof authorror) { switch (error.type ) { case "CredentialsSignin" : return "Invalid credentials." ; default : return "Something went wrong." ; } } throw error; } }
在/auth.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 38 39 40 41 42 43 44 45 46 47 import NextAuth from "next-auth" ;import { authConfig } from "./auth.config" ;import Credentials from "next-auth/providers/credentials" ;import { z } from "zod" ;import bcrypt from "bcrypt" ;import { prisma } from "./app/lib/prisma" ;async function getUser (email : string ) { try { const user = await prisma.users .findUnique ({ where : { email : email, }, }); return user; } catch (error) { console .error ("Failed to fetch user:" , error); throw new Error ("Failed to fetch user." ); } } export const { auth, signIn, signOut } = NextAuth ({ ...authConfig, providers : [ Credentials ({ async authorize (credentials ) { const parsedCredentials = z .object ({ email : z.string ().email (), password : z.string ().min (6 ) }) .safeParse (credentials); if (parsedCredentials.success ) { const { email, password } = parsedCredentials.data ; const user = await getUser (email); if (!user) return null ; const passwordsMatch = await bcrypt.compare (password, user.password ); if (passwordsMatch) { const nextAuthUser = { ...user, id : user.id .toString () }; return nextAuthUser; } } console .log ("Invalid credentials" ); return null ; }, }), ], });
配置需要登录才能访问的路由。在auth.config.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 import type { NextAuthConfig } from "next-auth" ;export const authConfig = { pages : { signIn : "/login" , }, callbacks : { async redirect ({ url, baseUrl } ) { return `${baseUrl} /dashboard` ; }, async authorized ({ auth, request: { nextUrl } } ) { const isLoggedIn = !!auth?.user ; const isOnDashboard = nextUrl.pathname .startsWith ("/dashboard" ); if (!isLoggedIn && isOnDashboard) { return false ; } return true ; }, }, providers : [], } satisfies NextAuthConfig ;
然后用中间件保护路由 /middleware.ts
1 2 3 4 5 6 7 8 9 import NextAuth from "next-auth" ;import { authConfig } from "./auth.config" ;export default NextAuth (authConfig).auth ;export const config = { matcher : ["/((?!api|_next/static|_next/image|.*\\\\.png$).*)" ], };
这样就完成了用户的登录验证问题。
Fetch(可选) 这个主要是为了将 Next.js 纯作为一个 SSR 项目使用,只做前端页面开发。
值得注意的是,Next.js 毕竟是混合开发,也就是混合客户端和服务端的开发。所以你的接口请求虽然说是请求第三方,但是还是得考虑这个请求是发生在服务端还是客户端上。
对 fetch 做一个封装,让它能够自动对头部做参数添加。以及对于 401 的错误处理。
由于要考虑服务端所以,服务端上的 token 处理就采用 cookie 的方式。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 "use server" ;import { cookies } from "next/headers" ;export async function setCookies (name : string , data : any ) { cookies ().set (name, data); } export async function getCookies (name : string ) { return cookies ().get (name)?.value ; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 export const isBrowser = typeof window !== "undefined" ;
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 import { redirect } from "next/navigation" ;import { isBrowser } from "@/lib/utils" ;import { getCookies } from "./setCookie" ;type HttpMethod = "GET" | "POST" | "PUT" | "DELETE" ;const backPreUrl = process.env .NODE_ENV === "development" ? "<http://localhost:3000>" : process.env .NEXT_PUBLIC_BACK_PRE_URL ; const urlPreTag = process.env .NODE_ENV === "development" ? process.env .NEXT_PUBLIC_BACK_PRE_TAG : "" ; interface FetchOptions extends RequestInit { headers ?: Record <string , string >; } interface ApiResponse <T = any > { data ?: T; error ?: string ; status ?: number ; } const apiClient = <T,>(method : HttpMethod ) => { return async ( url : string , data ?: any , options : FetchOptions = {} ): Promise <ApiResponse <T>> => { const controller = new AbortController (); const { signal } = controller; let token = "" ; let defaultLocale = "" ; token = (await getCookies ("NEXT_TOKEN" )) || "" ; defaultLocale = (await getCookies ("NEXT_LOCAL" )) || "" ; const config : FetchOptions = { method, signal, headers : { "Content-Type" : "application/json" , Authorization : `Bearer ${token} ` , ...options.headers , }, ...options, }; if (method !== "GET" && data) { config.body = JSON .stringify (data); } const response = await fetch (`${backPreUrl} ${urlPreTag} ${url} ` , config); if (response.status === 401 ) { if (isBrowser) { location.href = `/login` ; } else { redirect (`/login` ); } } const result = await response.json (); if (!response.ok ) { return { error : result.message || "Request failed" , status : response.status , }; } return result; }; }; export const getRequest = apiClient ("GET" );export const postRequest = apiClient ("POST" );export const putRequest = apiClient ("PUT" );export const deleteRequest = apiClient ("DELETE" );
使用案例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 "use client" ;import { getRequest } from "@/lib/customFetch" ;import { useCallback, useEffect, useState } from "react" ;export default function UserName ( ) { const [data, setData] = useState<{ data ?: any ; error ?: string ; status ?: number ; }>(); const loaderProfile = useCallback (async () => { const result = await getRequest (`/auth/profile` ); if (!result.error ) { setData (result); } }, []); useEffect (() => { loaderProfile (); }, [loaderProfile]); return <> userName组件 {data?.data.name}</> ; }
1 2 3 4 5 6 7 8 9 10 11 12 import { getRequest } from "@/lib/customFetch" ;export default async function UserName ( ) { const result = await getRequest (`/auth/profile` ); if (!result.error ) { console .log (result); } const data = result.data as { name : string }; return <> userName组件 {data?.name || "为获取到数据"}</> ; }
设置开发代理 next.config.mjs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 const isProd = ["production" ].includes (process.env .NODE_ENV );const rewrites = ( ) => { if (!isProd) { return [ { source : "/api/:slug*" , destination : "<http://localhost:7000/api/:slug*>" , }, ]; } else { return []; } }; const nextConfig = { rewrites : rewrites, }; export default nextConfig;
添加状态管理插件 本项目状态管理没有选择传统的 redux 而是选择了比较轻巧的 zsutand 该状态管理对于一般的项目已经足够用了
然后在store/user.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 import { create } from 'zustand' ;import { createJSONStorage, persist } from 'zustand/middleware' ;import { defaultLocale, locales } from '@/static/locales' ;interface SettingState { defaultLocale : string ; locales : string []; setDefaultLocale : (newVal : string ) => void ; } const useSettingStore = create<SettingState >()( persist ( (set, get ) => ({ defaultLocale : get ()?.defaultLocale ? get ()?.defaultLocale : defaultLocale, locales : locales, setDefaultLocale : (newVal ) => set ((state : any ) => ({ defaultLocale : state.defaultLocale = newVal, })), }), { name : 'setting' , storage : createJSONStorage (() => sessionStorage ), ), ); export default useSettingStore;
使用案例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 import useSettingStore from "@/store/useSettingStore" ;export default function CHangeLanguage ( ) { const options = [ { label : "EN" , value : "en" }, { label : "中" , value : "zh" }, ]; const setDefaultLocale = useSettingStore ((state ) => state.setDefaultLocale ); const defaultLocale = useSettingStore ((state ) => state.defaultLocale ); const [value, setValue] = useState (defaultLocale); const handleChange = ({ target: { value } }: RadioChangeEvent ) => { setValue (value); setDefaultLocale (value); }; return ( <> <Group options ={options} onChange ={onLanguageChange} value ={value} key ={value} > </Group > </> ); }
国际化
next-i18next : 一款流行的 Next.js
国际化插件,它提供了丰富的功能,包括多语言路由、服务器 端渲染和静态生成的支持,以及简单的翻译文件管理。
next-intl : 用于 Next.js
的国际化插件,它提供了基于 React Intl 的国际化解决方案,支持多语言文本和格式化。
next-translate : 这个插件为 Next.js
提供了简单的国际化解决方案,支持静态生成和服务器端渲染,并且易于配置和使用。
使用 next-intl
插件,主要是网上看了对比的文章,这款插件从扩展和使用灵活性上都非常不错。
官方文档 中提供了 2 种方式,一种基于路由地址,一种是不基于路由地址。我这里就选择路由地址。
同时这里我改用自定义的路径,以便项目更加好管理。并这里使用静态渲染,因为我喜欢在文件中使用异步函数。非静态渲染不能使用异步函数也就是它不在服务端渲染的。
在i18n/messages/en.json
中写入下面的内容
1 2 3 4 5 { "HomePage" : { "title" : "Hello world" } }
同理在i18n\\messages\\zh.json
写入
1 2 3 4 5 { "HomePage" : { "title" : "你好" } }
现在,设置插件,该插件创建别名以向服务器组件提供 i18n 配置(在下一步中指定)。 next.config.mjs
1 2 3 4 5 6 7 8 import createNextIntlPlugin from "next-intl/plugin" ;const withNextIntl = createNextIntlPlugin ("./i18n/i18n.ts" );const nextConfig = {};export default withNextIntl (nextConfig);
next.config.js
1 2 3 4 5 6 7 8 const createNextIntlPlugin = require ("next-intl/plugin" );const withNextIntl = createNextIntlPlugin ("./i18n/i18n.ts" );const nextConfig = {};module .exports = withNextIntl (nextConfig);
在i18n\\i18n.ts
中使用next-intl
创建一个请求范围的配置对象,可用于根据用户的区域设置提供消息和其他选项,以便在服务器组件中使用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 import { notFound } from "next/navigation" ;import { getRequestConfig } from "next-intl/server" ;const locales = ["en" , "zh" ];export default getRequestConfig (async ({ locale }) => { if (!locales.includes (locale as any )) notFound (); return { messages : (await import (`./messages/${locale} .json` )).default , }; });
middleware.ts
中间件匹配请求的区域设置并相应地处理重定向和重写。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 import createMiddleware from "next-intl/middleware" ;export default createMiddleware ({ locales : ["en" , "zh" ], defaultLocale : "zh" , }); export const config = { matcher : ["/" , "/(zh|en)/:path*" ], };
app\\[locale]\\layout.tsx
这里需要调整一下自定义生成的文件,将 app 下面的约定文件例如 layout.tsx 移入到[locale]文件夹下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 import { NextIntlClientProvider } from "next-intl" ;import { getMessages } from "next-intl/server" ;export default async function LocaleLayout ({ children, params: { locale }, }: { children: React.ReactNode; params: { locale: string }; } ) { const messages = await getMessages (); return ( <html lang ={locale} > <body > <NextIntlClientProvider messages ={messages} > {children} </NextIntlClientProvider > </body > </html > ); }
app/[locale]/page.tsx
在页面组件或其他任何地方使用翻译!
1 2 3 4 5 6 7 8 9 import { useTranslations } from "next-intl" ; export default function HomePage ( ) { const t = useTranslations ("HomePage" ); return <div > {t("title")}</div > ; }
到此国际化,axios 等设置就基本完成了,下面我使用一个 next.js 的 UI 框架继续完善这个项目