Nextjs学习笔记

23 阅读11分钟

1. 简介

  • 构建全栈 Web 应用程序的 React 框架
  • 使用react来构建用户界面
特性
  • 路由:基于文件系统的路由器,构建在服务器组件之上,支持布局、嵌套路由、加载状态、错误处理
  • 渲染:使用客户端和服务器进行客户端和服务器渲染。通过 Next.js 在服务器上进行静态和动态渲染进一步优化。在 Edge 和 Node.js 运行时进行流式传输。
  • 数据获取:在服务器组件中使用 async/await 简化数据获取,并扩展了 fetch API 以实现请求缓存、数据缓存和重新验证。
  • 样式 : 支持你熟悉的样式方法,包括 CSS Modules、Tailwind CSS 和 CSS-in-JS
  • 优化 : 图像、字体和脚本优化,以改善应用程序的核心 Web 指标和用户体验。
  • TypeScript : 改进了对 TypeScript 的支持,具有更好的类型检查和更高效的编译,以及自定义 TypeScript 插件和类型检查器。
两种不同的路由:APP Router和Pages Router

2. 安装

  • nodejs 18.8 或者更高版本
  • 自动安装:npx create-next-app@latest
  • 手动安装:npm install next@latest react@latest react-dom@latest
创建app目录结构

Next.js 使用文件系统路由,这意味着应用程序中的路由由你的文件结构决定。

创建一个 app 文件夹。然后,在 app 内创建一个 layout.tsx 文件。这个文件是根布局。它是必需的,并且必须包含 <html> 和 <body> 标签。

app/layout.tsx

TypeScript

TypeScriptJavaScript

export default function RootLayout({  children,}:{  children: React.ReactNode}) { 
    return(    
        <html lang="en">
            <body>{children}</body>
        </html>  
    )}

创建一个带有初始内容的主页 app/page.tsx

app/page.tsx

export default function Page() {
  return <h1>Hello, Next.js!</h1>
}

当用户访问你的应用程序根目录(/)时,layout.tsx 和 page.tsx 都将被渲染。

创建public文件夹(可选)
  • 在项目根目录创建一个 public 文件夹,用于存储静态资源,如图片、字体等。public 中的文件可以从基础 URL(/)开始被你的代码引用。
    • 例如,public/profile.png 可以被引用为 /profile.png

3. 项目结构

  • 顶级文件夹 app : APP Router pages : pages Router public : 要提供的静态资产 src : 可选的应用程序源文件夹
  • 文件夹中,只有page.js 和 route.js 返回的内容才会发送到客户端,比如 /dashboard 对应的是文件夹dashboard/page.js 。 api/route.js 客户端可以访问
  • 私有文件夹 : 这表示该文件夹是私有实现细节,不应被路由系统考虑,从而将该文件夹及其所有子文件夹排除在路由之外。 比如 : _components文件夹放置组件
  • 路由组:(文件夹名) 用括号括起来的文件夹,不会被包含在URL路径中

project-organization-route-groups.png

4. 布局和页面

  • app下直接创建一个page.js 对应的路由是 "/"

  • 创建布局:

    • 根布局:app下创建一个layout.js
    export default function DashboardLayout({
      children,
    }: {
      children: React.ReactNode
    }) {
      return (
        <html lang="en">
          <body>
            {/* Layout UI */}
            {/* Place children where you want to render a page or nested layout */}
            <main>{children}</main>
          </body>
        </html>
      )
    }
    
    • 上面是根布局,因为在app目录根部,根布局是必须的,并且需要包含html、body标签
  • 创建嵌套路由

    • 嵌套路由是由多个 URL 段组成的路由。例如,/blog/[slug] 路由由三个段组成:
      • /(根段)
      • blog(段)
      • [slug](叶段)

image.png - [slug]这种将文件夹名称用中括号括起来,会创建一个特殊的动态路由段,用于从数据生成多个页面,比如产品详情 /product/123. 123是产品id

  • 嵌套布局:还可以给文件夹下添加新的布局文件,比如blog下添加layout.js

export default function BlogLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return <section>{children}</section>
}
  • app/blog/layout.js 接受children image.png
  • 文件之间的链接
    • 可以使用 <Link href>跳转
    • 也可以使用 useRouter 钩子

5. 优化图片和字体

  • 优化图片 : Next.js 的 <Image> 组件扩展了原始img标签元素,从 nnext/image中导入。提供以下功能

    • 尺寸优化:  自动为每个设备提供正确大小的图片,使用现代图片格式如 WebP。

    • 视觉稳定性:  在图片加载时自动防止布局偏移

    • 更快的页面加载:  使用浏览器原生懒加载功能,仅在图片进入视口时加载,可选模糊占位符。

    • 资源灵活性:  按需调整图片大小,甚至包括存储在远程服务器上的图片。

    • src 如果是本地图片 "/"开头直接能找到 和app同级的public下。Next.js 将根据导入的文件自动确定图片的固有 width 和 height。这些值用于确定图片比例并防止图片加载时的累积布局偏移

    • src 如果是远程图片 需要手动指定宽和高。还要安全地允许来自远程服务器的图片,你需要在 next.config.js 中定义支持的 URL 模式列表。尽可能具体,以防止恶意使用。

  • 优化字体

    • next/font 模块自动优化你的字体并移除外部网络请求,以提高隐私性和性能。

6. CSS

  • CSS Modules
    • blog/style.module.css 要使用css modules 创建一个扩展名为 .module.css的文件并将其导入app下的任何文件夹中。CSS Modules 通过生成唯一的类名来局部作用域化 CSS。这允许你在不同文件中使用相同的类名而不用担心命名冲突。
  • 全局CSS : app/global.css 并在根布局中导入

7. 获取数据

  1. 使用 fetch API
  2. 使用 ORM 或数据库
  • client Components
  • 流式传输 :
    • why :在 Server Components 中使用 async/await 时,Next.js 将选择动态渲染。这意味着数据将在服务器上为每个用户请求获取和渲染。如果有任何慢速数据请求,整个路由将被阻止渲染。

    • 为了改善初始加载时间和用户体验,你可以使用流式传输将页面的 HTML 分解成更小的块,并从服务器到客户端逐步发送这些块。

    • 有两种方式可以在你的应用程序中实现流式传输:

      1. 使用 loading.js 文件
      export default function Loading() {
        // 在这里定义 Loading UI
        return <div>Loading...</div>
      }
      
      • 在导航时,用户将立即看到布局和加载状态,同时页面正在渲染。一旦渲染完成,新内容将自动替换。

      1. 使用 React 的 <Suspense> 组件
        • 允许更加精细地控制页面的哪些部分进行流式传输 。。例如,你可以立即显示 <Suspense> 边界外的任何页面内容,并在边界内流式传输博客文章列表。
import { Suspense } from 'react'
import BlogList from '@/components/BlogList'
import BlogListSkeleton from '@/components/BlogListSkeleton'
 
export default function BlogPage() {
  return (
    <div>
      {/* 这些内容将立即发送到客户端 */}
      <header>
        <h1>欢迎访问博客</h1>
        <p>阅读下面的最新文章。</p>
      </header>
      <main>
        {/* 任何包裹在 <Suspense> 边界中的内容都将被流式传输 */}
        <Suspense fallback={<BlogListSkeleton />}>
          <BlogList />
        </Suspense>
      </main>
    </div>
  )
}

8. 更新数据

  • 创建Server Functions
    • 使用 "use server" 指令创建Server Functions 。你可以将该指令放在异步函数的顶部以将该函数标记为 Server Function,或者放在单独文件的顶部以标记该文件的所有导出。
    // app/lib/actions.ts
    export async function createPost(formData: FormData) {
      'use server'
      const title = formData.get('title')
      const content = formData.get('content')

      // 更新数据
      // 重新验证缓存
    }
 
    export async function deletePost(formData: FormData) {
      'use server'
      const id = formData.get('id')

      // 更新数据
      // 重新验证缓存
    }
  • Server Components
    • 可以通过在函数体顶部添加 "use server" 指令来在 Server Components 中内联 Server Functions:
    export default function Page() {
      // Server Action
      async function createPost(formData: FormData) {
        'use server'
        // ...
      }
      return <></>
    }
  • client components : 在client component中无法定义server function .但是,你可以通过从顶部有 "use server" 指令的文件中导入来在 Client Components 中调用它们:
    // app/action.ts
    'use server'
     export async function createPost() {}
    // app/ui/button.tsx
    'use client'
     // 在client component 中调用 server component
    import { createPost } from '@/app/actions'

    export function Button() {
      return <button formAction={createPost}>Create</button>
    }
  • 调用server functions 主要有两种方式
      1. Server 和 Client Components中的表单
      1. Client Components 中的事件处理程序
  • 表单
    • React 扩展了 元素,允许通过html中的action属性调用Server fanction.在表单中调用时,函数会自动接收 FormData 对象。你可以使用原生 FormData 方法来提取数据:
    // form.tsx
    import { createPost } from '@/app/actions'
 
    export function Form() {
      return (
        <form action={createPost}>
          <input type="text" name="title" />
          <input type="text" name="content" />
          <button type="submit">Create</button>
        </form>
      )
    }
   // app/action.ts
    'use server'
 
    export async function createPost(formData: FormData) {
      const title = formData.get('title')
      const content = formData.get('content')

      // Update data
      // Revalidate cache
    }
  • 事件处理程序

    • 可以通过onclick等事件处理程序,在client component中调用server Function
  • 示例

    1. 显示等待状态

     * 在执行server function时,可以使用react的`useActionState`钩子显示加载指令器。这个钩子返回一个pending的布尔值
    
    // app/ui/button.tsx
    import { useActionState } from 'react'
    import { createPost } from '@/app/actions'
    import { LoadingSpinner } from '@/app/ui/loading-spinner'
    
    export default Btton(){
        const [state,action,pending] = useActionState(createPost,false);// createPost是处理请求的server function
        
        return (
            <button obClick={async ()=>action()}>
                {pending?<LoadingSpinner /> : 'Create Post'}
            </button>
        )
    }
    
    
    useActionState 是一个用来专门处理异步提交表单的钩子。它可以让你处理表单提交的状态,比如 loading 状态、返回值、错误信息等。
    • 使用场景?
      • 你想处理一个表单,比如用户点击提交按钮,后台执行一个异步请求(例如发送 API)。
      • 提交后你希望能拿到返回的结果或错误,并根据这个状态更新 UI。
      • 而且你不想用 useState + useEffect 自己组合那么多状态了。
    const [state, action, isPending] = useActionState(handler, initialState)
    • 参数说明:
    参数含义
    handler一个异步函数,处理提交逻辑
    initialState初始状态(比如:{ success: '', error: '' })
    state当前返回的状态(每次调用 handler 后会更新)
    action传给 <form action={action}> 的函数
    isPending是否正在执行异步逻辑(loading 状态)

    2. 重新验证缓存

    • 执行更新后,你可以通过在 Server Function 中调用 revalidatePath 或 revalidateTag 来重新验证 Next.js 缓存并显示更新后的数据
        import {revalidatePath} from 'next/cache';
        export async function createPost(formData:FormData){
            'use server'
            // Update data 
            // ... 
            revalidatePath('/posts')
        }
    
    • revalidatePath 和 revalidateTag 是用来刷新(重新生成)页面缓存的工具。让页面数据重新加载,再一次获取最新内容。
      • 使用场景:

        • 你有个页面展示文章列表,数据来自数据库;
        • 页面被访问时,Next.js 会缓存这个页面(默认是静态生成);
        • 你在后台添加了一篇新文章;
        • 但前台页面不会自动更新!
          这时候你就需要用 revalidatePathrevalidateTag 去「手动刷新」页面缓存!
      • revalidatePath 使用方法:刷新某个路径的缓存

        import {revalidatePath} from 'next/cache'
        revalidatePath('/blog') // 刷新blog列表的数据
        

        常用于:

        • 提交表单或请求后,让某个页面立即更新
        • 你知道具体页面路径(比如 /product/123
      • revalidateTag 用法:刷新某个 tag 对应的缓存页面

          import {revalidateTag} from 'next/cache';
          revalidateTag('blog-list')
      
          // 需要配合fetch使用
          // 这个 fetch 的返回会被打上 tag
          await fetch('https://api.example.com/posts', {
            next: {
              tags: ['blog-list']
            }
          })
      
      
      适用于:
      • 多个页面用了相同的数据源
      • 想一次刷新它们的缓存

    3. 重定向 - 在执行更新后,想要重定向到另一个页面。可以使用server function 中的redirect来实现。

    import {redirect} from 'next/nevigation';
    
    export async function createPost(formData:FormData){
        // update data
        redirect('/posts')
    }

9. 错误处理

  • 分为 预期错误未捕获异常
  • 处理预期错误
    • 预期错误是那些可能在应用程序正常运行期间发生的错误,例如来自服务器端表单验证或失败的请求。这些错误应该被明确处理并返回给客户端。
    • 服务器函数 : 可以使用useActionState hook来处理服务器函数中的预期错误
    'use client'
    
    import { useActionState } from 'react'
    import { createPost } from '@/app/actions'
    
    const initialState = {
      message: '',
    }
    
    export function Form() {
      const [state, formAction, pending] = useActionState(createPost, initialState)
    
      return (
        <form action={formAction}>
          <label htmlFor="title">Title</label>
          <input type="text" id="title" name="title" required />
          <label htmlFor="content">Content</label>
          <textarea id="content" name="content" required />
          {state?.message && <p aria-live="polite">{state.message}</p>}
          <button disabled={pending}>Create Post</button>
        </form>
      )
    }
    
    
    • 服务器组件 : 在 Server Component 内部获取数据时,你可以使用响应来有条件地渲染错误消息或redirect
```js
// app/page.tsx
  import { getPostBySlug } from '@/lib/posts'

    export default async function Page({ params }: { params: { slug: string } }) {
      const { slug } = await params
      const post = getPostBySlug(slug)
      if (!post) {
        notFound()
      }
      return <div>{post.title}</div>
    }

 // app/blog/[slug]/not-found.tsx

   export default function NotFound() {
      return <div>404 - 页面未找到</div>
    }
 ```
  • 处理未捕获异常 : 意外错误,表明在应用程序正常流程中不应该发生的错误或问题。这些应该通过抛出错误来处理,然后由错误边界捕获。

    • 嵌套的错误边界
      • <ErrorBoundary fallback={<Error/>}></ErrorBoundery>
      • Nextjs 使用错误边界来处理未捕获异常,错误边界会捕获其子组件的错误,并显示一个备用UI,而不是崩溃的组件树
        // app/dashboard/error.tsx
       'use client' // 错误边界必须是客户端组件
    
        import { useEffect } from 'react'
    
        export default function Error({
          error,
          reset,
        }: {
          error: Error & { digest?: string }
          reset: () => void
        }) {
          useEffect(() => {
            // 将错误记录到错误报告服务
            console.error(error)
          }, [error])
    
          return (
            <div>
              <h2>出错了!</h2>
              <button
                onClick={
                  // 尝试通过重新渲染段来恢复
                  () => reset()
                }
              >
                重试
              </button>
            </div>
          )
        }
    
  • 错误将冒泡到最近的父错误边界。这允许通过在路由层次结构的不同级别放置 error.tsx文件来进行细粒度的错误处理 image.png

    • 全局错误
      • 虽然不太常见,但你可以使用 global-error.js 文件(位于根 app 目录中)来处理根布局中的错误,即使使用了国际化。全局错误 UI 必须定义自己的 <html> 和 <body> 标签,因为在激活时它会替换根布局或模板。 ```js // app/golobal-error.js 'use client' // 错误边界必须是客户端组件

    export default function GlobalError({ error, reset, }: { error: Error & { digest?: string } reset: () => void }) { return ( // global-error 必须包含 html 和 body 标签

    出错了!

    <button onClick={() => reset()}>重试 ) }

```

10. 添加元数据和创建 OG 图片

    import type { Metadata } from 'next'
    export const metadata: Metadata = {
      title: 'My Blog',
      description: '...',
    }
    export default function Page() {}

11.

12.

13.