编程导航Next话题讨论

Next

3 参与
分享

快来分享你的内容吧~

点击登录,快来和大家讨论吧~
表情
图片
话题
打卡
综合
交流
文章
问答

nextjs 项目运行时,刷新页面问题

学习面试鸭前端时,每次刷新页面都会先显示html再加载样式,这是正常的吗 ![image.png](https://pic.code-nav.cn/post_picture/1840286658952077314/LU3SrGhccuIkoF0S.webp)

用户中心上线(NextJS改造)

# 用最新的 NextJS 实现 跟着鱼皮哥的课程实现的项目,一边学习项目的规范和实践经验,一边探索用 nextjs 实现相同的功能。 项目已上线,欢迎大家体验! ## 项目 Github 仓库:[用户中心项目 - 鱼皮](https://github.com/waitlistSawana/user-center-yupi-nextjs) 上线地址:[鱼皮用户中心项目改造 - NextJs 版](https://user-center-yupi-nextjs.highlightpulse.site/) 开发记录:[开发记录.md](https://github.com/waitlistSawana/user-center-yupi-nextjs/blob/main/%E5%BC%80%E5%8F%91%E8%AE%B0%E5%BD%95.md) 主要功能 - 用户注册、登录、注销 - 用户表格查看 待完善部分 - [ ] 统一后端返回 - [ ] 定义统一错误码 - [ ] docker - [ ] nginx ## 技术栈 总体来说,使用 T3 Stack 启动了 NextJS 的项目,前后端同构,前端是 React 后端是 Nodejs。T3 stack 集成了生产级的配置,配好了 prettier 和 eslint,以及全栈需要各项开源项目。 前端: - NextJS 15 + Tailwindcss 4 + React 19,跟进最新技术框架 - Shadcn UI 定制化组件开发,学习其设计方法和理念,组件属性解构、cn 和 cva 模式 - Tankstack Query 异步数据状态管理,高效实现 React 组件数据获取、缓存和更新 - Tankstack Table 强大的 Headless UI 表格状态管理和数据操作 后端: - 项目分层 api、services、repositories - JSDoc 风格代码注释 - Drizzle ORM 数据库访问 - 阿里云 RDS 的 Serverless MySql 数据库 - JWT session 和 cookie 管理 其他 - 测试 Vitest (AI 写的),Rest Client 测试 api 端点 - Cloufalre 域名托管 - Vercel 项目部署上线 - Github 数据托管,commit 命名遵循 Angular Commit Message Guidelines # 项目结构 可以直接在 [Github 仓库](https://github.com/waitlistSawana/user-center-yupi-nextjs/tree/main/src) 中查看 <img src="https://pic.code-nav.cn/post_picture/1857029075076202498/AdbCML9myTZWEpDW.webp" alt="tmp51B.png" width="100%" /> ## 后端 app/api/v1/user controler 的接口,前端直接访问,如 example.com/api/v1/user/current 。 get 方法 和 post 方法 server/db/ 数据库相关 Drizzle ORM server/db/index.ts 创建数据库连接 server/db/schema.ts 数据库表设计 server/db/types.ts 根据 schema 自动定义类型 server/repositories/user.ts 用户数据访问层 server/services/user.ts 服务层 server/utils/error-handling.ts 用 TRPCError 包装 NextResponse server/utils/hash.ts 用 bcryptjs 密码加密函数 server/utils/session.ts 根据 NextJS 文档建议,JWT session 创建 ## 前端 app/ 页面 app/(auth)/ 用户相关页面 app/(auth)/login 登录表单,useEffect + React Form Hook + Zod + axios app/(auth)/register 注册表单,Tankstack Query(useMutation) + React Form Hook + Zod + axios app/(auth)/logout 登出页面,清除 session app/(dashboard)/layout.ts 侧边栏设计 app/(dashboard)/welcome/ 用户主页 app/(dashbaord)/user-manage 用户数据库表格,Tankstack Table components/ 组件整理 components/ui/ 导入进来的 shadcn ui 组件,可定制 components/aceternity/ 从组件库 aceternity ui 复制来的组件代码 compoents/fallback/ 用于 suspense 的骨架组件 components/user-manage tankstack table 表格的构建,columns 定义 compoents/ ** .tsx 自建的各种 React 组件 ## 其他 src/env.js 复用 t3 实现环境变量配置 src/middleware.ts 中间件,鉴权,路由保护和跳转 (ps. 之前出了挺火的漏洞233,所以必须在别处二次鉴权) test/ 测试代码 test/rest-client 后端端点连接和测试 eslint.config.js 静态检查 prettier.config.js 代码格式化

使用next.js搭建一个免费的博客网站

# 使用next.js搭建一个免费的博客网站 你好!这是我用Next.js搭建的第一个项目[blogs💡](https://blogs-sable-zeta.vercel.app),访问🪜[原作者视频地址](https://www.youtube.com/watch?v=y7JCnfbETPs) <img src="https://pic.code-nav.cn/post_picture/1623235832938774529/3igyyH2hxQwvGzDt.webp" alt="Pasted image 20241012150557.png" width="100%" /> >完成项目的过程中,我掌握了 `react`,`next.js`,`typeScript`,`tailwindcss`,[`prisma(数据库)`](https://www.prisma.io/docs/orm/more/help-and-troubleshooting/help-articles/nextjs-prisma-client-dev-practices) [`kinde(登录授权)`](https://kinde.com),[`vercel(部署)`](https://vercel.com/)的部分知识🍻 >搭建部署过程我梳理并总结成了文档,希望你能喜欢 ~ ## 一. 搭建过程 ### 1. 创建并初始化项目 ##### 1.1 创建一个`my-blogs`的文件夹,在`my-blogs`中创建next.js应用: ``` npx create-next-app@14.2.3 . ``` 或最新版本。 ``` npx create-next-app@latest . ``` 选择的一些配置。 ``` √ Would you like to use TypeScript? ... Yes √ Would you like to use ESLint? ... Yes √ Would you like to use Tailwind CSS? ... Yes √ Would you like to use `src/` directory? ... Yes √ Would you like to use App Router? (recommended) ... Yes √ Would you like to customize the default import alias (@/*)? ... No ``` ##### 1.2 初始化部分文件: ```tsx // src\app\page.tsx export default function Page() {   return (     <div>       Page     </div>   ) } ``` ```css /* src\app\globals.css */ @tailwind base; @tailwind components; @tailwind utilities; ``` ### 2. 组件化,布局,响应式容器,首页,底部,头部实现 > 对next.js项目结构不熟悉可以查看文档 [Project Structure](https://nextjs.org/docs/getting-started/project-structure)。 > 在编写tailwindcss时推荐插件:Tailwind CSS IntelliSense,Tailwind Snippets。 ##### 2.1 组件化: 将响应式容器,头部,底部独立成组件,创建目录。 ``` src\components\container.tsx src\components\header.tsx src\components\footer.tsx ``` ##### 2.2 布局: ```tsx // src\app\layout.tsx import type { Metadata } from "next"; import { Inter } from "next/font/google"; import Header from "../components/header"; import Footer from "../components/footer"; import Container from "../components/container"; import "./globals.css"; const inter = Inter({ subsets: ["latin"] }); //网页字体 export const metadata: Metadata = { //静态元数据   title: "My Posts",   description: "Generated by create next app", }; export default function RootLayout({   children, }: Readonly<{   children: React.ReactNode; }>) {   return (     <html lang="en">       <body className={`${inter.className} bg-zinc-100 text-zinc-900 `}>         <Container>           <Header/>           {children}           <Footer/>         </Container>       </body>     </html>   ); } ``` ##### 2.3 响应式容器: ```tsx // src\component\container.tsx export default function Container({ children }: Readonly<{ children: React.ReactNode }>) {     return (         <div className="max-w-[1100px] mx-auto bg-white flex flex-col min-h-screen border-l border-r">             {children}         </div>     ) } ``` ##### 2.4 首页: ```tsx // src\app\page.tsx export default function Page() {   return (     <main className="text-center pt-32 px-5">       <h1 className="text-4xl md:text-5xl font-bold mb-5">欢迎来到我的博客!</h1>       <p className="max-w-[750px] mx-auto leading-8">         Lorem ipsum dolor sit amet consectetur         adipisicing elit. Ducimus,a         natus? Dolores veritatis perferendis         non doloribus numquam praesentium         aliquid tempora qui similique,         exercitationem distinctio labore culpa         nam natus consequuntur rem!       </p>     </main>   ) } ``` ##### 2.5 底部: ```tsx // src\component\footer.tsx export default function Footer() {     return (         /* 当前Footer是在一个垂直方向的Flexbox容器中,mt-auto (margin-top:auto),可以将footer推到容器的底部。 */         <div className="mt-auto text-center text-zinc-400 py-5 px-7 border-t">             <small>&copy; 2024. All Rights Reserved.</small>         </div>     ) } ``` ##### 2.6 头部: >Image标签中src放的地址可能会出现==next-image-unconfigured-host==提示,原因在next.js官方文档可以找到[`next/image` Un-configured Host](https://nextjs.org/docs/messages/next-image-unconfigured-host),解决方法是在next.config.mjs中增加Image配置。 ```js // next.config.mjs /** @type {import('next').NextConfig} */ const nextConfig = {     images: {         remotePatterns: [             {                 protocol: 'https',                 hostname: 'bytegrad.com'             }         ]     } }; export default nextConfig; ``` ```tsx // src\component\header.tsx 'use client' //当前页面是客户端组件 import Image from "next/image" //next.js提供的image组件 import Link from "next/link"; //next.js提供的路由组件 import { usePathname } from "next/navigation"; //通过usePathname获取当前页面路由地址 const navLinks = [     {         href: '/',         label: 'Home'     },     {         href: "/posts",         label: "Posts"     },     {         href: '/create-post',         label: "Create post"     } ] export default function Header() {     const pathname = usePathname();     return (         <header className="flex justify-between items-center py-4  px-7 border-b">             <Link href='/'>                 <Image                     src="https://bytegrad.com/course-assets/youtube/example-logo.png"                     alt='logo'                     className="w-[35px] h-[35px]"                     width='35'                     height='35'                 />             </Link>             <nav>                 <ul className="flex gap-x-5 text-[14px]">                     {                         navLinks.map((link) => (                             <li key={link.href}>                                 <Link                                     className={`font-bold ${pathname === link.href ? 'text-zinc-900' : "text-zinc-400"                                         }`}                                     href={link.href}                                 >                                     {link.label}                                 </Link>                             </li>                         ))                     }                 </ul>             </nav>         </header>     ) } ``` ### 3. Posts页面,通过动态路由进入post详情,Create Post页面 ##### 3.1 创建posts页面(文档 [Pages](https://nextjs.org/docs/app/building-your-application/routing/pages)) 点击标题进入详情页面时,通过标题对应的id进入到子路由页面 ``/posts/${post.id}``,每次点击标题的id都是不固定的,所以接下来需要用到动态路由。 ```tsx // src\app\posts\page.tsx import Link from "next/link"; export default async function Posts() {     const response = await fetch('https:dummyjson.com/posts?limit=10');     const { posts } = await response.json()     return (         <main className="text-center pt-16 px-5">             <h1 className="text-4xl md:text-5xl font-bold mb-5">all posts</h1>             <ul >                 {posts.map((post: { id: number, title: string }) => (                     <li key={post.id} className="mb-3">                         <Link href={`/posts/${post.id}`} className="text-2xl font-bold">{post.title}</Link>                     </li>                 ))}             </ul>         </main>     ) } ``` ##### 3.2 通过 [动态路由](https://nextjs.org/docs/app/building-your-application/routing/dynamic-routes)进入post详情 ```tsx // src/app/posts/[id]/page.tsx export default async function Page({ params }: { params: { id: string } }) {     const response = await fetch(`https://dummyjson.com/posts/${params.id}`);     const post = await response.json();     return (         <main className="px-7 pt-24 text-center">             <h1 className="text-5xl font-semibold mb-7">{post.title}</h1>             <p className="max-w-[700px] mx-auto">{post.body}</p>         </main>     ) } ``` 这样,就可以动态获取到每个posts标题对应的内容了。此时会有一些小问题:1.id不存在时如何处理。2.详情页面渲染等待时卡顿。 优化:加入404页面,loading页面。 ```tsx // src/app/posts/[id]/page.tsx import { notFound } from "next/navigation"; export default async function Page({ params }: { params: { id: string } }) { await new Promise((resolve) => setTimeout(resolve, 1000));//不会立即完成。会等待1秒钟后调用resolve函数     const response = await fetch(`https://dummyjson.com/posts/${params.id}`);     const post = await response.json();     //id不存在,跳转到404页面     if (!post.id) {         notFound();     }     return (         <main className="px-7 pt-24 text-center">             <h1 className="text-5xl font-semibold mb-7">{post.title}</h1>             <p className="max-w-[700px] mx-auto">{post.body}</p>         </main>     ) } ``` ```tsx // src/app/posts/[id]/not-found.tsx export default function NotFound() {     return (         <div>             No post found for that id.         </div>     ) } ``` ```tsx // src/app/posts/[id]/loading.tsx export default function Loading() {     return (         <div className="text-center mt-5">             Loading...         </div>     ) } ``` 最后将Posts中标题列表独立成单独的组件。 ```tsx // src/components/posts-list.tsx import Link from "next/link"; export default async function PostsList() {     const response = await fetch('https:dummyjson.com/posts?limit=10');     const { posts } = await response.json()     return (         <ul >             {posts.map((post: { id: number, title: string }) => (                 <li key={post.id} className="mb-3">                     <Link href={`/posts/${post.id}`} className="text-2xl font-bold">{post.title}</Link>                 </li>             ))}         </ul>     ) } ``` ```tsx // src/app/posts/page.tsx import PostsList from "@/components/posts-list"; import { Suspense } from "react"; export default function Posts() {     return (         <main className="text-center pt-16 px-5">             <h1 className="text-4xl md:text-5xl font-bold mb-5">all posts</h1>             {/* 当ui尚未加载时显示loading */}             <Suspense fallback="Loading...">                 <PostsList />             </Suspense>         </main>     ) } ``` ##### 3.3 Create Post页面 ```tsx // src\app\create-post\page.tsx import Form from '@/components/form'; export default async function CreatePost() {     return (         <main className="text-center pt-16">             <h1 className='text-4xl md:text-5xl font-bold mb-5'>Create post</h1>             <Form />         </main>     ) } ``` ```tsx // src\components\form.tsx import { createPost } from "@/actions/actions"; export default function Form() {     return (         <form         action={createPost}             className="flex flex-col max-w-[400px] mx-auto space-x-2 gap-2 my-10"         >             <input                 name="title"                 type='text'                 placeholder="Title for new post"                 className="border rounded px-3 h-10"                 required             />             <textarea                 name="body"                 placeholder="Body content for new post"                 className="border rounded px-3 py-2"                 rows={6}                 required             />             <button className="h-10 bg-blue-500 px-5 rounded text-white">Submit</button>         </form>     ) } ``` ### 4. 用[prisma](https://www.prisma.io/docs/orm/more/help-and-troubleshooting/help-articles/nextjs-prisma-client-dev-practices)数据库存放数据 ##### 4.1 安装prisma 安装依赖。 ``` npm i prisma -save-dev ``` 初始化数据库后会在根目录生成一个 `prisma`文件夹和 `.env`文件。 ``` npx prisma init --datasource-provider sqlite ``` 定义一个Post模型。 ```prisma // prisma\schema.prisma // 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" } datasource db {   provider = "sqlite"   url      = env("DATABASE_URL") } //定义一个post模型 model Post {   id       Int      @id @default(autoincrement())   title    String   body     String   createAt DateTime @default(now())   updateAt DateTime @updatedAt } ``` 更新数据库,成功后prisma文件夹内会生成一个`dev.db`文件。 ``` npx prisma db push ``` 查看数据库UI,成功后进入 http://localhost:5555 ,点击post进入查看刚才创建的数据。 ``` npx prisma studio ``` ##### 4.2 使用prisma 在actions中配置请求。 ```ts // src\actions\actions.ts "use server";//服务端组件 import prisma from "@/lib/db"; export async function createPost(formData:FormData) {     console.log("formData-->","title:",formData.get("title"),"body:",formData.get("body"))     const title = formData.get("title") as string;     const body = formData.get("body") as string;     await prisma.post.create({         data:{             title,             body         }       }) } ``` ```ts // src\lib\db.ts import { PrismaClient } from '@prisma/client' const prismaClientSingleton = () => { // 创建一个新的 Prisma Client 实例,以便后续与数据库交互。   return new PrismaClient() } declare const globalThis: {   prismaGlobal: ReturnType<typeof prismaClientSingleton>; } & typeof global; const prisma = globalThis.prismaGlobal ?? prismaClientSingleton() // 确保在整个应用程序中只有一个 Prisma Client 实例,避免了多个实例同时存在导致的连接问题 export default prisma if (process.env.NODE_ENV !== 'production') globalThis.prismaGlobal = prisma // 如果当前环境不是生产环境,则将 prisma 实例赋值给 globalThis.prismaGlobal ``` 此时,在create post页面中输入内容并提交后,在 http://localhost:5555 中查看数据是否已经写入进prisma。 接下来对posts-list列表页,post详情页获取数据的方法进行改造。 ```tsx // src\components\posts-list.tsx import prisma from "@/lib/db"; import Link from "next/link"; export default async function PostsList() {     await new Promise((resolve) => setTimeout(resolve, 1000));     const posts = await prisma.post.findMany();     return (         <ul>             {posts.map((post) => (                 <li key={post.id} className="mb-3">                     <Link href={`/posts/${post.id}`} >{post.title}</Link>                 </li>             ))}         </ul>     ) } ``` ```tsx // src\app\posts\[id]\page.tsx import prisma from "@/lib/db"; import { notFound } from "next/navigation"; export default async function Page({ params }: { params: { id: string } }) {     await new Promise((resolve) => setTimeout(resolve, 1000));//不会立即完成。会等待1秒钟后调用resolve函数     const post = await prisma.post.findUnique({         where: {             id: parseInt(params.id)         }     })     if (!post) {         notFound();     }     return (         <main className="px-7 pt-24 text-center">             <h1 className="text-5xl font-semibold mb-7">{post.title}</h1>             <p className="max-w-[700px] mx-auto">{post.body}</p>         </main>     ) } ``` ##### 4.3 优化页面未及时更新 此时有一个问题需要解决:当在create post中创建数据后,切换到posts页面时,标题列表中的数据没有更新到最新创建的数据。[解决方法](https://nextjs.org/docs/app/api-reference/functions/revalidatePath):每次访问 `/posts` 路由时都刷新缓存。 ```tsx // src\sctions\actions.ts "use server";//服务端组件 import prisma from "@/lib/db"; import { revalidatePath } from "next/cache"; export async function createPost(formData:FormData) {     console.log("formData-->","title:",formData.get("title"),"body:",formData.get("body"))     const title = formData.get("title") as string;     const body = formData.get("body") as string;     await prisma.post.create({         data:{             title,             body         }       })     revalidatePath("/posts")//访问该路由的时候刷新缓存 } ``` ### 5. kinde授权登录 ##### 5.1 安装kinde和配置 如果我们希望登录的用户才能访问create post页面,可以用[kinde](https://kinde.com)进行登录授权。[视频教程 1:14:28](https://www.youtube.com/watch?v=y7JCnfbETPs) ``` 1. 点击 start for free 2. 点击 add application 3. 输入项目名 选择 Back-end web 后 save 4. Quick start 选择 Next.js 后 save 5. 点击 Existing codebase 后看到 Set callback URL, Set logout URL 点击 Set 6. 下载依赖 npm i @kinde-oss/kinde-auth-nextjs 7. 继续滚动页面到 Update environment vars 复制粘贴到根部录下.env.local中,注意:未避免泄露信息,请不要将环境变量中的信息提交;检查.gitignore文件 8. 继续滚动页面到 API endpoints 创建目录 `src\app\api\auth\[kindeAuth]\route.js` 9. 选择 Authentication 选择 Social connenctions 中 登录的方式 后 save ``` ``` npm i @kinde-oss/kinde-auth-nextjs ``` ```js // src\app\api\auth\[kindeAuth]\route.js import { handleAuth } from "@kinde-oss/kinde-auth-nextjs/server"; export const GET = handleAuth(); ``` ##### 5.2 使用kinde 需要用到 [中间件](https://docs.kinde.com/developer-tools/sdks/backend/nextjs-prev-sdk/?r=search#protect-routes-using-middleware),在每次访问 `Create post` 页面时验证是否有登录信息。 ```ts // src\middleware.ts import {withAuth} from "@kinde-oss/kinde-auth-nextjs/middleware"; import {NextRequest} from 'next/server'; export default function middleware(req:NextRequest) { // 定义一个中间件函数,接收请求对象 req   return withAuth(req, {     isReturnToCurrentPage: true // 设置选项,表示在认证后返回当前页面   }); } export const config = { // 导出配置对象   matcher: ["/create-post"]// 设置匹配路径,只有当请求路径为 "/create-post" 时才会应用该中间件 }; ``` 每次`/create-post`页面发起请求时进行验证是否登录授权。 ```tsx // src\actions\actions.ts "use server";//服务端组件 import prisma from "@/lib/db"; import { getKindeServerSession } from "@kinde-oss/kinde-auth-nextjs/server"; import { revalidatePath } from "next/cache"; import { redirect } from "next/navigation"; export async function createPost(formData:FormData) {     /* 每次发起请求时,都进行验证是否授权 */     const {isAuthenticated} = getKindeServerSession();     if(!(await isAuthenticated())){         redirect("/api/auth/login");     }     console.log("formData-->","title:",formData.get("title"),"body:",formData.get("body"))     const title = formData.get("title") as string;     const body = formData.get("body") as string;     await prisma.post.create({         data:{             title,             body         }       })     revalidatePath("/posts")//访问该路由的时候刷新缓存 } ``` 加入logout按钮。 ```tsx // src\app\create-post\page.tsx import { LogoutLink } from '@kinde-oss/kinde-auth-nextjs/components'; import Form from '@/components/form'; export default async function CreatePost() {     return (         <main className="text-center pt-16">             <h1 className='text-4xl md:text-5xl font-bold mb-5'>Create post</h1>             <Form />             <LogoutLink>logout</LogoutLink>         </main>     ) } ``` (可忽略)将环境变量统一放入一个文件夹内,将 `.env.local` 中内容复制粘贴到 `.env` 中,只保留 `.env` , 注意请在.gitignore中配置不要将.env文件提交到远程仓库。 ## 二. 部署过程 >将代码部署到 [vercel](https://vercel.com):先把代码提交到github仓库,然后在vercel中导入项目进行部署。(在首次部署的过程中会出现以下问题。 [视频教程1:56:10](https://www.youtube.com/watch?v=y7JCnfbETPs)) ##### prisma数据库问题 在package.json中 `"scripts"` 中加入。 ``` "postinstall":"prisma generate" ``` 进入vercel中刚才导入的项目,选择Storge,创建一个用于生产的数据库。 ``` - 选择 Postgres 后 Create - Accept --> Create - 进入新建的数据库 - 选择prisma 复制粘贴到本地 schema.prisma 中替换掉 datasource db 对应代码 - 选择.env.local 复制粘贴到本地 .env 中替换掉 DATABASE_URL - 终端将数据库模型同步到数据库 npx prisma db push - 再次将代码提交到github,此时vercel会自动部署刚才提交的代码。若没有自动部署也可以点进项目中选择Deployments 选择 Redeploy 重新部署,绿灯表示部署成功 ``` ##### 环境变量问题 在预览项目是,切换到post页面发现此时postlist数据为空,create post页面打不开,发现地址还是本地的,所以需要将URL地址替换成生产URL地址。 ``` - 选择 Project 复制 Domains 中 生产URL地址 - 选择 Settings 选择 Environment Variables - 选择 KINDE_SITE_URL 修改 value 替换成 https://你的生产地址 后 SAVE - 选择 KINDE_POST_LOGOUT_REDIRECT_URL 修改 value 替换成 https://你的生产地址 后 SAVE - 选择 KINDE_POST_LOGIN_REDIRECT_URL 修改 value 替换成 https://你的生产地址/create-post 后 SAVE ``` 还需要在kinde中配置退出登录后重定向地址。 ``` - 进入kinde中之前建好的项目内 - 选择 Details - 滚动至 Allowed callback URLs 加入地址 https://你的生产地址/auth/kinde_callback - 滚动至 Allowed logout redirect URLs 加入地址 https://你的生产地址 后 SAVE ``` 最后再部署一次,等到Status变绿后,此时你已经自己搭建并部署了你的博客🎉🎉🎉 如果我的文档对你有帮助,别忘了[Star⭐️~ ](https://github.com/MuscleProgrammerXiao/blogs) 我会更加积极的更新🍻

Nextjs App Router 模式新增哪些能力

<html> <head></head> <body> <div class="content ql-editor"> <p>nextjs 13.4 的发布,宣布 App Router 稳定版本的到来,</p> <p></p> <p><img src="https://pic.code-nav.cn/planet_post_image/1619930914211520514/cmztntqc.jpeg" alt="image-20231004170531857"></p> <p></p> <p>​ 上图是 next 的下载量,可以看到 <strong>遥遥领先</strong> 其他框架,vercel 公司的新一代 React 框架,为什么如此好用?下面我们看一下 13.4 版本的新功能吧!</p> <p></p> <h3>App Router</h3> <p></p> <p>​ Next.js 13.4 是第一个将新的应用程序目录和应用程序路由器功能从测试版中剔除的版本!这就代表着 App Router 正式成为稳定版,可以用于生产环境,相比较之前的<code>pages/</code> 下文件路由,<code>App Router</code>更加的自由,具体优势有:</p> <p></p> <ol> <li data-list="bullet"><span class="ql-ui"></span>更灵活的路由规则:使用 "app route" 可以更自由地定义路由,不再仅依赖于文件的相对路径。这样可以更好地组织和管理路由结构,特别是对于复杂的应用程序而言。</li> <li data-list="bullet"><span class="ql-ui"></span>更好的代码分离:在 "app route" 中,可以将路由定义和页面逻辑分离到不同的文件或模块中。这样可以提高代码的可读性和可维护性。</li> <li data-list="bullet"><span class="ql-ui"></span>更好的代码重用:"app route" 支持模块化的路由定义和组合,使得页面的复用更加便利。您可以将一些共享的路由规则封装成模块,便于在不同的页面或应用中共享和复用。</li> </ol> <p></p> <p>app route 使 Next 的开发体验变得更好,此外,Next.js 文档站点进行了重大更新,将 App Router 设为默认值,这也许象征着此版本的重要性!您现在可以在新的 App Router 和之前的 Pages Router 之间切换文档。</p> <p></p> <p><img src="https://pic.code-nav.cn/planet_post_image/1619930914211520514/11g57e2s.jpeg" alt="image-20231005165320746"></p> <p></p> <h4>App 文件下的路由</h4> <p></p> <p>新应用程序目录中的路由就像添加导出 React 函数组件的 <code>page.tsx</code> 文件一样简单。应用程序的路由由应用程序目录中文件夹的层次结构定义。例如,在 <code>/</code> 路由上创建页面就像在应用程序目录的根目录中添加 page.tsx 文件一样简单</p> <p></p> <h5>1)新的路由规范</h5> <p></p> <p>接下来,让我们展示如何使用新的应用程序路由器创建更复杂的应用程序路由:</p> <p></p> <p><img src="https://pic.code-nav.cn/planet_post_image/1619930914211520514/zwhgvvmt.jpeg" alt="image-20231007124732252"></p> <p></p> <p>正如您在上面看到的,我们能够创建一个简单的路由,例如 <code>/about</code> ,以及使用 Next.js 的动态路由创建更复杂的路由,例如 <code>/articles/article/12345</code> 。</p> <p></p> <p>新的路由规范通过为布局、页面、模板等强制使用特殊文件名,解决了之前版本中所有文件都作为路由,无法在<code>pages/</code> 下声明不作为路由的文件,使开发体验变得更好。</p> <p></p> <h5>2)获取数据方式更简洁</h5> <p></p> <p>借助新的 App Router,我们的页面可以使用 React Server 组件,这使我们能够使用熟悉的 async/await 语法来简化数据获取。</p> <p></p> <div class="ql-code-block-container"> <div class="ql-code-block"><span class="ql-token hljs-comment">// app/page.tsx</span> export <span class="ql-token hljs-keyword">default</span> async function <span class="ql-token hljs-title">Page()</span> { <span class="ql-token hljs-type">const</span> <span class="ql-token hljs-variable">res</span> <span class="ql-token hljs-operator">=</span> await <span class="ql-token hljs-title">fetch('https://api.example.com/...')</span>; <span class="ql-token hljs-type">const</span> <span class="ql-token hljs-variable">data</span> <span class="ql-token hljs-operator">=</span> res.json(); <span class="ql-token hljs-keyword">return</span> <span class="ql-token hljs-string">'...'</span>; } </div> </div> <p></p> <p>在 12 的时候,我们想要获取数据需要:</p> <p></p> <div class="ql-code-block-container"> <div class="ql-code-block"><span class="ql-token hljs-comment">// pages/example.js</span> <span class="ql-token hljs-keyword">import</span> React from <span class="ql-token hljs-string">'react'</span>; export async function <span class="ql-token hljs-title">getServerSideProps()</span> { <span class="ql-token hljs-comment">// 在此处获取服务端数据</span> <span class="ql-token hljs-type">const</span> <span class="ql-token hljs-variable">response</span> <span class="ql-token hljs-operator">=</span> await <span class="ql-token hljs-title">fetch('https://api.example.com/data')</span>; <span class="ql-token hljs-type">const</span> <span class="ql-token hljs-variable">data</span> <span class="ql-token hljs-operator">=</span> await response.json(); <span class="ql-token hljs-keyword">return</span> { props: { data <span class="ql-token hljs-comment">// 将数据作为 props 返回</span> } }; } function <span class="ql-token hljs-title">Example({ data })</span> { <span class="ql-token hljs-comment">// 在客户端使用获取到的数据进行渲染</span> <span class="ql-token hljs-keyword">return</span> ( &lt;div&gt; &lt;h1&gt;Server-side rendering example&lt;/h1&gt; &lt;p&gt;{data}&lt;/p&gt; &lt;/div&gt; ); } export <span class="ql-token hljs-keyword">default</span> Example; </div> </div> <p></p> <p>相比较更加符合之前的开发习惯!</p> <p></p> <h5>3)新的概念:服务器组件</h5> <p></p> <p>在 Next.js 13 中,<strong>文件默认是服务器组件</strong>。您可以通过在模块顶部包含 <code>'use client'</code> 指令来选择使用客户端组件。如果您还不了解客户端组件 和 服务器组件的区别,强烈推荐文章:<a href="https://www.joshwcomeau.com/react/server-components/" target="_blank">理解React服务器组件</a>,这篇文章详细解释了为什么需要引入新的概念:服务器组件,以及客户端组件和服务端组件的边界问题,开发中容易混淆的概念等等,非常值得一看</p> <p></p> <h5>4)路由段配置</h5> <p></p> <p>我们的页面还可以导出路由段,使我们能够控制页面的渲染方式。 Next.js 中的一种常见做法是静态生成页面。但是,当我们使用动态参数时,我们遇到了一个问题,Next.js 如何知道要静态生成哪些页面?我们可以利用 <code>generateStaticParams</code> 来解决这个问题。这在做 seo 中非常有用</p> <p></p> <div class="ql-code-block-container"> <div class="ql-code-block"><span class="ql-token hljs-comment">// app/articles/article/[id]/page.tsx</span> <span class="ql-token hljs-type">type</span> <span class="ql-token hljs-variable">ArticleProps</span> <span class="ql-token hljs-operator">=</span> Pick&lt;Article, <span class="ql-token hljs-string">'id'</span>&gt; export <span class="ql-token hljs-keyword">default</span> async function <span class="ql-token hljs-title">Page({ id }: ArticleProps)</span> { <span class="ql-token hljs-type">const</span> <span class="ql-token hljs-variable">article</span> <span class="ql-token hljs-operator">=</span> await(await <span class="ql-token hljs-title">fetchArticleById(id)</span>).json(); <span class="ql-token hljs-keyword">return</span> &lt;BlogArticle article={article} /&gt; } export async function <span class="ql-token hljs-title">generateStaticParams()</span>: ArticleProps[] { <span class="ql-token hljs-type">const</span> <span class="ql-token hljs-variable">articles</span> <span class="ql-token hljs-operator">=</span> await(await <span class="ql-token hljs-title">fetchArticles()</span>).json(); <span class="ql-token hljs-keyword">return</span> articles.map(article =&gt; ({ id: article.id })) } </div> </div> <p></p> <p>在上面的示例中,我们导出一个名为 <code>generateStaticParams</code> 的异步函数,该函数加载我们的所有博客文章并返回一个对象数组,该数组的属性与动态路线段的名称相匹配,在本例中为 <code>id</code> 。 Next.js 将使用它为我们的每篇博客文章静态生成页面。</p> <p></p> <h5>5)路由段配置</h5> <p></p> <p>Next.js 13 为我们提供了几个<a href="https://nextjs.org/docs/app/api-reference/file-conventions/route-segment-config" target="_blank">路由段配置</a>选项来控制 Next.js 渲染页面的方式。除了 <code>generateStaticParams</code> 之外,最常用的路线段配置是 <code>revalidate</code> 。当与静态站点生成 (SSG) 结合使用时,重新验证允许您控制页面重新生成的频率。例如我在 page.tsx | route.ts | layout.ts 中,添加如下代码:</p> <p></p> <div class="ql-code-block-container"> <div class="ql-code-block"> export <span class="ql-token hljs-type">const</span> <span class="ql-token hljs-variable">revalidate</span> <span class="ql-token hljs-operator">=</span> <span class="ql-token hljs-number">60</span> </div> </div> <p></p> <p>那么我们的当前页面就会每 60 秒重建一次页面,请注意,它比这更微妙,因此请务必阅读 Next.js 文档网站上的 ISR</p> <p></p> <p>我们还可以通过使用 dynamicParams 路由段配置来告诉 next.js 不要渲染 <code>generateStaticParams</code> 中未返回的 ID 的页面,</p> <p></p> <p>如果我们有一个使用查询参数的页面,我们可能希望在使用动态路由段请求该页面时动态服务器呈现它,那么我们可以根据我们的用例强制页面静态或动态生成</p> <p></p> <div class="ql-code-block-container"> <div class="ql-code-block"><span class="ql-token hljs-type">export</span> <span class="ql-token hljs-variable">dynamic</span> <span class="ql-token hljs-operator">=</span> <span class="ql-token hljs-string">'force-dynamic'</span> <span class="ql-token hljs-comment">// or: 'auto' | 'force-dynamic' | 'error' | 'force-static'</span> </div> </div> <p></p> <h5>6)布局和模版</h5> <p></p> <p><strong>布局</strong></p> <p></p> <p>除了页面之外,Next.js 13 还引入了<strong>布局</strong>和<strong>模板</strong>的特殊文件。在上面的示例中,您可能已经注意到,我们在文件夹层次结构的各个级别包含了多个 <code>layout.tsx</code> 文件。我们创建了一个根级 <code>layout.tsx</code>,这是 Next.js 13 中的典型做法。</p> <p></p> <p>在根布局中,我们可以创建 HTML 文档,向头部添加脚本和其他内容,提供语义结构,并将应用程序包裹在供应器中。我们不再需要使用 Next.js 早期版本中的特殊 <code>&lt;Head&gt;</code> 和 <code>&lt;Script&gt;</code> 组件。我们也可以在这里添加<strong>元数据</strong>,</p> <p></p> <p>请注意,<code>layout.tsx</code> 和<code>page.tsx</code>通常应该是服务器组件,因为我们应该让布局组件在服务器渲染,这样用户可以下载更少的 js,让用户更快看到一些内容。</p> <p></p> <div class="ql-code-block-container"> <div class="ql-code-block"><span class="ql-token hljs-comment">// app/layout.tsx</span> export <span class="ql-token hljs-keyword">default</span> function <span class="ql-token hljs-title">RootLayout({ children })</span> { <span class="ql-token hljs-keyword">return</span> ( &lt;html lang=<span class="ql-token hljs-string">"en"</span>&gt; &lt;head&gt; &lt;script&gt;<span class="ql-token hljs-comment">//...&lt;/script&gt;</span> &lt;/head&gt; &lt;body&gt; &lt;main&gt; &lt;AppProvider&gt;{children}&lt;/AppProvider&gt; &lt;/main&gt; &lt;/body&gt; &lt;/html&gt; ); } <span class="ql-token hljs-comment">// app/page.tsx</span> export <span class="ql-token hljs-keyword">default</span> function <span class="ql-token hljs-title">Page()</span> { <span class="ql-token hljs-keyword">return</span> <span class="ql-token hljs-comment">//...;</span> } </div> </div> <p></p> <p>我们还可以在应用程序目录层次结构的每个级别添加布局文件,以将更具体的布局应用于应用程序的不同部分。例如,假设我们想要围绕我们的博客文章添加一个特定的 AppWrapper。我们可以将新的 <code>layout.tsx</code> 文件添加到 <code>page.tsx</code> 文件旁边的 <code>/app/articles/article/[id]</code> 目录中。</p> <p></p> <div class="ql-code-block-container"> <div class="ql-code-block"><span class="ql-token hljs-comment">// app/articles/article/[id]/layout.tsx</span> export <span class="ql-token hljs-keyword">default</span> function <span class="ql-token hljs-title">RootLayout({ children })</span> { <span class="ql-token hljs-keyword">return</span> ( &lt;AppWrapper&gt;{children}&lt;/AppWrapper&gt; ); } </div> </div> <p></p> <p><strong>模版</strong></p> <p></p> <p>模版和布局类似,但是区别在于,模版在不同路由之间切换的时候,模版的内容是销毁和重新新建,但是布局是对应路由下 共享这个 DOM 元素,路由切换不会销毁再新建DOM,</p> <p></p> <p>更多请看:<a href="https://nextjs.org/docs/app/building-your-application/routing/pages-and-layouts#templates" target="_blank">路由之排版</a></p> <p></p> <h4>自动代码分割</h4> <p></p> <p>Next.js 13.4 让<strong>代码分割</strong>和<strong>动态加载内容</strong>变得比以往更容易。在 之前版本中,您必须使用 <code>next/dynamic</code> 。现在,整个应用程序可以通过使用 <code>Suspense</code> 选择代码分割和动态加载。事实上,您可以在客户端组件中使用简单的条件进行代码分割。查看 Next.js 博客中的以下示例,该示例根据用户是否登录返回完全不同的包:</p> <p></p> <div class="ql-code-block-container"> <div class="ql-code-block"><span class="ql-token hljs-comment">// app/layout.tsx</span> <span class="ql-token hljs-keyword">import</span> { getUser } from <span class="ql-token hljs-string">'./auth'</span>; <span class="ql-token hljs-keyword">import</span> { Dashboard, Landing } from <span class="ql-token hljs-string">'./components'</span>; export <span class="ql-token hljs-keyword">default</span> async function <span class="ql-token hljs-title">Layout()</span> { <span class="ql-token hljs-type">const</span> <span class="ql-token hljs-variable">isLoggedIn</span> <span class="ql-token hljs-operator">=</span> await <span class="ql-token hljs-title">getUser()</span>; <span class="ql-token hljs-keyword">return</span> isLoggedIn ? &lt;Dashboard /&gt; : &lt;Landing /&gt;; } </div> </div> <p></p> <h4>BETA 功能</h4> <p></p> <h5>1)服务器操作</h5> <p></p> <p>Next.js 13.4 引入了服务器操作(目前处于 alpha 版本),它为您的应用程序带来了更多的灵活性和强大功能。服务器操作允许您直接在应用程序中处理服务器端逻辑,例如获取数据或与 API 交互。此功能不仅简化了您的应用程序架构,还使您能够创建更快、更高效的应用程序。</p> <p></p> <h5>2)Turbopack</h5> <p></p> <p>随着 Next.js 13.4 的发布,Turbopack 已进入测试阶段,提供功能更完整、更稳定的体验。 Turbopack 是 Next.js 基于 Rust 的新捆绑器,旨在加速开发中的本地迭代以及很快的生产构建。由于社区在测试和报告错误方面的支持,Turbopack 的采用率不断提高,为未来更快、更高效的开发体验铺平了道路。</p> <p></p> <h5>3)草稿模式:轻松获取和渲染草稿内容</h5> <p></p> <p>Next.js 13.4 引入了<a href="https://nextjs.org/docs/app/building-your-application/configuring/draft-mode" target="_blank">草稿模式</a>,该功能允许您轻松地从无头 CMS 中获取和渲染草稿内容。与现有的预览模式 API 相比,草稿模式提供了更简化和增强的体验,新应用程序目录不支持现有预览模式 API。</p> <p></p> <p>通过草稿模式,您现在可以在发布内容之前无缝预览和迭代内容,确保为用户提供最高质量的输出。</p> <p></p> <p>什么是 CMS ?</p> <p></p> <p>答:CMS 是 Content Management System(内容管理系统)的缩写。它是一种应用程序或一组相关的应用程序,用于创建、管理、分发、发布和更新内容。CMS 提供了一个集中化的仓库,使得你可以在其中收集,管理和发布信息。</p> <p></p> <p>在前端开发的上下文中,CMS 通常会提供 API(如 RESTful 或 GraphQL API),前端应用则通过这些 API 与 CMS 进行交互,获取或修改数据。而 CMS 的内容通常包括了网页的文本、图片、视频等。</p> <p></p> <p>有许多常见的 CMS,例如 Wordpress, Drupal, Joomla 等等。近年来,所谓的 "headless CMS" 也变得越来越流行,例如 Contentful, Prismic, Strapi 等。所谓 "headless",是指在 CMS 不再负责内容的呈现,只通过 API 提供内容,因此可以更灵活地与各种前端框架和技术(例如React,Vue,Angular,或者静态网站生成器)结合使用。</p> <p></p> <h2>总结</h2> <p></p> <p>个人觉得 Next.js 是一种新的前端开发范式,通过使用 Next.js 13 ,学习到了很多新的概念,同时也觉得使用 Next.js 开发,没有之前流畅,需要学习的还有很多</p> <p>#前端# #Next.js#</p> </div> </body> </html>

下载 APP