使用 Next.js App Router 常犯的 10 个错误

本文为稀土掘金技术社区首发签约文章,30天内禁止转载,30天后未获授权禁止转载,侵权必究!

前言

最近我看了 Vercel 的产品 VP Lee Robinson 的 Youtube 视频,作为一名前端开发工程师,他分享了很多 Next.js 相关的课程视频,其中就有一条他总结的《Next.js App Router 常犯的 10 个错误》的视频,我觉得非常有意义,所以记录下了这 10 个常犯错误的内容,从我自己的角度为大家介绍下这 10 个错误。

现在就让我们开始吧!顺便看看你有没有中招。

项目准备

使用官方脚手架,创建一个 Next.js 项目:

bash 复制代码
npx create-next-app@latest

运行效果如下:

错误 1:服务端组件调用路由处理程序

新建 app/mistake1/page.js,代码如下:

javascript 复制代码
export default async function Page() {
  const data = await (await fetch('http://localhost:3000/api/hello')).json()
  return (
    <ul>{data?.data.map(({title}, index) => {
      return <li key={index}>{title}</li>
    })}</ul>
  )
}

新建 app/api/hello/route.js,代码如下:

javascript 复制代码
export async function GET() {
  const res = await fetch('https://jsonplaceholder.typicode.com/posts')
  const data = await res.json()
  
  return Response.json({ data })
}

这就是传统的应用实现方式,前后端分离,在前端页面调用后端的接口实现前后端的交互。但会有 2 个问题:

  1. API 地址硬编码,比如现在获取的是 localhost:3000,上线的时候还要设置线上地址
  2. 其实没有必要创建一个多余的 GET 路由处理程序,服务端组件直接运行在服务端,可以直接在服务端组件中获取后端资源

我们可以直接修改 app/mistake1/page.js,代码如下:

jsx 复制代码
export default async function Page() {
  const data = await (await fetch('https://jsonplaceholder.typicode.com/posts')).json()
  return (
    <ul>{data.map(({title}, index) => {
      return <li key={index}>{title}</li>
    })}</ul>
  )
}

效果是一样的:

错误 2:路由处理程序的静态处理

新建 app/api/time/route.js,代码如下:

javascript 复制代码
export async function GET() {
  console.log('GET /api/time')
  return Response.json({ data: new Date().toLocaleTimeString() })
}

在开发模式下,每次刷新时间都会改变:

现在我们部署生产版本,运行 npm run build && npm run start

你会发现,无论怎么刷新,时间都不会改变。这就是被缓存了,又或者说,被静态处理了。

这里哪怕我们不获取时间,而是改为 fetch 外部的接口,亦或者调用后端资源进行处理,都有可能会被静态处理。

这是 Next.js 的自动行为,因为 Next.js 认为并不是每次 GET 请求都要重新计算,所以干脆处理成静态数据提升性能。

路由处理程序的行为其实和页面的行为是一致的,如果你希望更改这种行为,那么添加一些动态化的操作即可将其转为动态处理。就比如使用 cookies()、headers() 函数:

javascript 复制代码
export async function GET(request) {
  const token = request.cookies.get('token')
  return Response.json({ data: new Date().toLocaleTimeString() })
}

这是因为 cookies、headers 这种数据,只能在每次具体请求的时候才能知道,所以 Next.js 会按照正常的 API 进行处理。

当你添加其他的 HTTP 方法比如 POST 方法的时候也会将其转为动态处理:

javascript 复制代码
export async function GET() {
  console.log('GET /api/time')
  return Response.json({ data: new Date().toLocaleTimeString() })
}

export async function POST() {
  console.log('POST /api/time')
  return Response.json({ data: new Date().toLocaleTimeString() })
}

这是因为 POST 请求往往用于改变数据,GET 请求用于获取数据。如果写了 POST 请求,表示数据会发生变化,此时不适合缓存。

所以**简单的来说就是,当你在路由处理程序中只写了一个 GET 请求又没有任何动态化的操作时,有可能会在生产环境的时候转为静态处理。**注意这个行为即可。

具体还有哪些行为会导致动态处理,可以参考《路由篇 | 路由处理程序》

错误 3:路由处理程序与客户端组件

如果说,服务端组件调用路由处理程序是一种"错误",你可能以为,那就只能在客户端组件中调用路由处理程序。

这还是一种错误。

新建 app/mistakes3/page.js,代码如下:

javascript 复制代码
'use client'

import { useState } from 'react';
export default function Page() {

  const [list, setList] = useState([]);

  return (
    <>
      <ul>
        {list.map(({ title, id }) => {
          return <li key={id}>{title}</li>
        })}
      </ul>
      <button onClick={async () => {
        const data = await (await fetch('http://localhost:3000/api/hello')).json()
        setList(data.data)
      }}>添加数据</button>
    </>
  )
}

这里我们将页面整个声明为客户端组件,调用了路由处理程序。依然是有 2 个问题:

  1. API 地址硬编码,比如现在获取的是 localhost:3000,上线的时候还要设置线上地址
  2. 其实没有必要创建一个多余的 GET 路由处理程序,客户端组件也可以直接调用后端资源,这就是 Server Actions

所以这段代码可以直接改为:

javascript 复制代码
'use client'

import { useState } from 'react';
import { fetchPosts } from './actions';
export default function Page() {

  const [list, setList] = useState([]);

  return (
    <>
      <ul>
        {list.map(({ title, id }) => {
          return <li key={id}>{title}</li>
        })}
      </ul>
      <button onClick={async () => {
        const data = await fetchPosts()
        setList(data)
      }}>添加数据</button>
    </>
  )
}

新建 app/mistakes3/actions.js,代码如下:

javascript 复制代码
"use server"

export async function fetchPosts(value) {
  const data = await (await fetch('https://jsonplaceholder.typicode.com/posts')).json()
  return data
}

效果是一样的:

简单总结就是:当你开发的路由处理程序只是给自己的前台界面调用的时候,那就没有必要写这个路由处理程序。如果是服务端组件,直接调用。如果是客户端组件,使用 Server Actions。

错误 4:Suspense 组件的正确位置

新建 app/mistake4/page.js,代码如下:

javascript 复制代码
const sleep = ms => new Promise(r => setTimeout(r, ms));

async function Posts() {
  const data = await (await fetch('https://jsonplaceholder.typicode.com/posts')).json()
  await sleep(2000)
  return (
    <ul>{data?.map(({title}, index) => {
      return <li key={index}>{title}</li>
    })}</ul>
  )
}
export default async function Page() {
  return (
    <>
      <h1>Articles List</h1>
      <Posts />
    </>
  )
}

交互效果如下:

因为我们特地添加了 sleep 函数,所以在地址栏输入地址后,2s 后页面才开始加载处理。

为了提升这个体验,Next.js 会推荐使用 Suspense 组件。但尴尬的是,如果你对 Suspense 不熟,很可能会写成这样:

javascript 复制代码
import { Suspense } from "react";
const sleep = ms => new Promise(r => setTimeout(r, ms));
async function Posts() {
  const data = await (await fetch('https://jsonplaceholder.typicode.com/posts')).json()
  await sleep(2000)
  return (
    <Suspense fallback={'loading'}>
      <ul>{data?.map(({title}, index) => {
      return <li key={index}>{title}</li>
      })}</ul>
    </Suspense>
  )
}
export default async function Page() {
  return (
    <>
      <h1>Articles List</h1>
      <Posts />
    </>
  )
}

不知道你写 Suspense 的时候遇到过这个错误不?我还真的这样写错过......其实应该这样写:

javascript 复制代码
import { Suspense } from "react";
const sleep = ms => new Promise(r => setTimeout(r, ms));
async function Posts() {
  const data = await (await fetch('https://jsonplaceholder.typicode.com/posts')).json()
  await sleep(2000)
  return (
      <ul>{data?.map(({title}, index) => {
      return <li key={index}>{title}</li>
      })}</ul>
  )
}
export default async function Page() {
  return (
    <>
      <h1>Articles List</h1>
      <Suspense fallback={'loading'}>
        <Posts />
      </Suspense>
    </>
  )
}

新的交互效果如下:

页面立刻就加载进来,然后 2s 后数据出现。

错误 5:处理传入的请求

第五个错误是关于如何处理传入的请求。Next.js 提供了一些内置的 API 帮助你获取信息。

比如如果你要获取 cookies 信息,你可以使用 next/headers:

javascript 复制代码
import { cookies } from 'next/headers'
 
export default function Page() {
  const cookieStore = cookies()
  const theme = cookieStore.get('theme')
  return '...'
}

如果你要获取 headers 信息,你可以使用 next/headers:

javascript 复制代码
import { headers } from 'next/headers'
 
export default function Page() {
  const headersList = headers()
  const referer = headersList.get('referer')
 
  return <div>Referer: {referer}</div>
}

如果你要获取搜索参数,直接提供了函数参数:

javascript 复制代码
export default function Page({ params, searchParams }) {
  return <h1>My Page</h1>
}

为了演示这些 API 的效果,新建 /app/mistake5/[id]/page.js,代码如下:

javascript 复制代码
import { cookies, headers } from 'next/headers'
export default function Page({ params, searchParams }) {
  const cookieStore = cookies()
  const headersList = headers()
  return (
    <>
      <h1>My Page</h1>
      <h2>params</h2>
      <div>{JSON.stringify(params, null, 2)}</div>
      <h2>searchParams</h2>
      <div>{JSON.stringify(searchParams, null, 2)}</div>
      <h2>cookies</h2>
      <div>{JSON.stringify(cookieStore, null, 2)}</div>
      <h2>headers</h2>
      <div>{JSON.stringify(headersList, null, 2)}</div>
    </>
  )
}

效果如下:

错误 6:使用 Context Providers

在 Next.js 中怎么是用 Context 呢?因为使用 Context 需要先声明为客户端组件,如果你一不小心,可能会让整个页面都转为客户端组件,进而失去服务端渲染的优势。

新建 app/mistake6/page.js,代码如下:

javascript 复制代码
'use client'
 
import { createContext, useContext } from 'react'
import dayjs from "dayjs";

export const ThemeContext = createContext('light')

function Button() {
  var now = dayjs().format('DD/MM/YYYY')
  const theme = useContext(ThemeContext);
  return <button>{ now } { theme }</button>;
}

export default function Page() {
  return (
    <ThemeContext.Provider value="dark">
      <Button />
    </ThemeContext.Provider>
  )
}

这样做,就将整个页面都声明为了客户端组件,查看页面 bundle,将 dayjs 也打包到了客户端 bundle 中。

Next.js 推荐的做法是放在根布局中

修改 app/layout.js,代码如下:

javascript 复制代码
import ThemeProvider from './provider'
 
export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        <ThemeProvider>{children}</ThemeProvider>
      </body>
    </html>
  )
}

新建 app/provider.js,代码如下:

javascript 复制代码
'use client'
 
import { createContext } from 'react'
 
export const ThemeContext = createContext({})
 
export default function ThemeProvider({ children }) {
  return <ThemeContext.Provider value="dark">{children}</ThemeContext.Provider>
}

这样 app/layout就还是服务端组件。当你在具体的页面中使用 Context 的时候:

新建 app/correct6/page.js,代码如下:

javascript 复制代码
import dayjs from "dayjs";
import Button from './button';
function Time() {
  var now = dayjs().format('DD/MM/YYYY')
  return <div>{ now }</div>;
}
export default function Page() {
  return (
    <>
      <Time />
      <Button />
    </>
  )
}

新建 app/correct6/button.js,代码如下:

javascript 复制代码
'use client'

import { useContext } from 'react';
import { ThemeContext } from "../provider"
function Button() {
  const theme = useContext(ThemeContext);
  return <button>{ theme }</button>;
}

export default Button

效果如下:

因为 dayjs 用在了服务端组件,所以不会打包到客户端 bundle 中

错误 7:不必要的 "use client"

在 Next.js 中,我们使用 "use client"声明为客户端组件,那么问题来了,如果父组件已经声明为客户端组件,子组件还需要再次声明吗?

答案是不用。其实 "use client" 声明的是客户端组件与服务端组件的边界,正常导入的情况下,客户端组件下的所有组件都会是客户端组件,也就意味着所有代码都会打包到客户端 bundle 中。

错误 8:当客户端组件与服务端组件一起使用

除非你使用 props 的形式将服务端组件传入客户端组件,比如这种:

javascript 复制代码
import ClientComponent from './client-component'
import ServerComponent from './server-component'
 
export default function Page() {
  return (
    <ClientComponent>
      <ServerComponent />
    </ClientComponent>
  )
}

客户端组件代码为:

javascript 复制代码
'use client'
 
import { useState } from 'react'
 
export default function ClientComponent({ children }) {
  const [count, setCount] = useState(0)
 
  return (
    <>
      <button onClick={() => setCount(count + 1)}>{count}</button>
      {children}
    </>
  )
}

为什么直接导入就会被视为客户端组件,而使用 props 的形式就可以呢?

这是因为通过 props 的形式,组件还是在服务端渲染,只是将服务端渲染的结果传给客户端组件而已。

错误 9:数据更新后没有重新验证

新建 app/mistake9/page.js,代码如下:

javascript 复制代码
import { findToDos, createToDo } from './actions';

export default async function Page() {
  const todos = await findToDos();
  return (
    <>
      <form action={createToDo}>
        <input type="text" name="todo" />
        <button type="submit">Submit</button>
      </form>
      <ul>
        {todos.map((todo, i) => <li key={i}>{todo}</li>)}
      </ul>
    </>
  )
}

新建 app/mistake9/actions.js,代码如下:

javascript 复制代码
'use server'

import { revalidatePath } from "next/cache";

const data = ['阅读', '写作', '冥想']
 
export async function findToDos() {
  return data
}

export async function createToDo(formData) {
  const todo = formData.get('todo')
  data.push(todo)
  // revalidatePath("/mistake9");
  return data
}

这里我们实现了一个简易的 Server Actions 提交,交互效果如下:

我们提交了数据,接口也是返回 200 成功状态,但是数据并没有更新,刷新页面,数据才更新。

此时我们取消注释,再看下交互效果:

错误 10:在 try/catch 中 redirect

以错误 9 的代码为例,假设我们修改 actions.js 的代码为:

javascript 复制代码
'use server'

import { revalidatePath } from "next/cache";
import { redirect } from 'next/navigation'

const data = ['阅读', '写作', '冥想']
 
export async function findToDos() {
  return data
}

export async function createToDo(formData) {
  try {
    const todo = formData.get('todo')
    data.push(todo)
    revalidatePath("/mistake9");
    redirect('/')
    return data
  } catch(e) {
    return {message: 'error'}
  }
}

数据重新验证后,我们调用 redirect 希望页面重定向到 /

但这是没有效果的!因为 redirect 的内部实现是通过抛出一个固定的错误来处理的,所以如果你在 try/catch 中使用就会失效。建议是在之后或者 finally 中使用,比如:

javascript 复制代码
'use server'

import { revalidatePath } from "next/cache";
import { redirect } from 'next/navigation'

const data = ['阅读', '写作', '冥想']
 
export async function findToDos() {
  return data
}

export async function createToDo(formData) {
  try {
    const todo = formData.get('todo')
    data.push(todo)
    revalidatePath("/mistake9");
    return data
  } catch(e) {
    return {message: 'error'}
  } finally {
    redirect('/')
  }
}

这样就是有效果的,效果如下:

  1. 功能实现:Next.js 10 个常犯的错误
  2. 仓库源码:github.com/mqyqingfeng...
  3. 下载代码:git clone -b nextjs-common-mistakes git@github.com:mqyqingfeng/next-app-demo.git

总结

本篇我们总结了 Next.js 常犯的 10 个错误,提前预习这些错误,防止大家遇到这些问题的时候被卡住。

PS:学习 Next.js,欢迎入手小册《Next.js 开发指南》。基础篇、实战篇、源码篇、面试篇四大篇章带你系统掌握 Next.js!

相关推荐
阿伟来咯~37 分钟前
记录学习react的一些内容
javascript·学习·react.js
吕彬-前端42 分钟前
使用vite+react+ts+Ant Design开发后台管理项目(五)
前端·javascript·react.js
学前端的小朱1 小时前
Redux的简介及其在React中的应用
前端·javascript·react.js·redux·store
guai_guai_guai1 小时前
uniapp
前端·javascript·vue.js·uni-app
也无晴也无风雨1 小时前
在JS中, 0 == [0] 吗
开发语言·javascript
bysking2 小时前
【前端-组件】定义行分组的表格表单实现-bysking
前端·react.js
王哲晓2 小时前
第三十章 章节练习商品列表组件封装
前端·javascript·vue.js
fg_4112 小时前
无网络安装ionic和运行
前端·npm
理想不理想v2 小时前
‌Vue 3相比Vue 2的主要改进‌?
前端·javascript·vue.js·面试
酷酷的阿云2 小时前
不用ECharts!从0到1徒手撸一个Vue3柱状图
前端·javascript·vue.js