跳到内容

版本 15

从 14 升级到 15

要更新到 Next.js 版本 15,您可以使用 upgrade codemod

终端
npx @next/codemod@canary upgrade latest

如果您更喜欢手动操作,请确保您正在安装最新的 Next 和 React 版本

终端
npm i next@latest react@latest react-dom@latest eslint-config-next@latest

须知

  • 如果您看到对等依赖项警告,您可能需要将 reactreact-dom 更新到建议的版本,或者您可以使用 --force--legacy-peer-deps 标志来忽略警告。一旦 Next.js 15 和 React 19 都稳定下来,这将不再必要。

React 19

  • 现在 reactreact-dom 的最低版本为 19。
  • useFormState 已被 useActionState 替换。useFormState 钩子在 React 19 中仍然可用,但它已被弃用,并将在未来的版本中删除。建议使用 useActionState,它包含额外的属性,例如直接读取 pending 状态。了解更多
  • useFormStatus 现在包含额外的键,如 datamethodaction。如果您未使用 React 19,则只有 pending 键可用。了解更多
  • React 19 升级指南 中阅读更多信息。

须知:如果您正在使用 TypeScript,请确保您还将 @types/react@types/react-dom 升级到最新版本。

异步请求 API(破坏性更改)

以前依赖于运行时信息的同步动态 API 现在是异步的

为了减轻迁移负担,codemod 可用于自动化该过程,并且可以临时同步访问 API。

cookies

import { cookies } from 'next/headers'
 
// Before
const cookieStore = cookies()
const token = cookieStore.get('token')
 
// After
const cookieStore = await cookies()
const token = cookieStore.get('token')

临时同步用法

app/page.tsx
import { cookies, type UnsafeUnwrappedCookies } from 'next/headers'
 
// Before
const cookieStore = cookies()
const token = cookieStore.get('token')
 
// After
const cookieStore = cookies() as unknown as UnsafeUnwrappedCookies
// will log a warning in dev
const token = cookieStore.get('token')

headers

import { headers } from 'next/headers'
 
// Before
const headersList = headers()
const userAgent = headersList.get('user-agent')
 
// After
const headersList = await headers()
const userAgent = headersList.get('user-agent')

临时同步用法

app/page.tsx
import { headers, type UnsafeUnwrappedHeaders } from 'next/headers'
 
// Before
const headersList = headers()
const userAgent = headersList.get('user-agent')
 
// After
const headersList = headers() as unknown as UnsafeUnwrappedHeaders
// will log a warning in dev
const userAgent = headersList.get('user-agent')

draftMode

import { draftMode } from 'next/headers'
 
// Before
const { isEnabled } = draftMode()
 
// After
const { isEnabled } = await draftMode()

临时同步用法

app/page.tsx
import { draftMode, type UnsafeUnwrappedDraftMode } from 'next/headers'
 
// Before
const { isEnabled } = draftMode()
 
// After
// will log a warning in dev
const { isEnabled } = draftMode() as unknown as UnsafeUnwrappedDraftMode

params & searchParams

异步布局

app/layout.tsx
// Before
type Params = { slug: string }
 
export function generateMetadata({ params }: { params: Params }) {
  const { slug } = params
}
 
export default async function Layout({
  children,
  params,
}: {
  children: React.ReactNode
  params: Params
}) {
  const { slug } = params
}
 
// After
type Params = Promise<{ slug: string }>
 
export async function generateMetadata({ params }: { params: Params }) {
  const { slug } = await params
}
 
export default async function Layout({
  children,
  params,
}: {
  children: React.ReactNode
  params: Params
}) {
  const { slug } = await params
}

同步布局

app/layout.tsx
// Before
type Params = { slug: string }
 
export default function Layout({
  children,
  params,
}: {
  children: React.ReactNode
  params: Params
}) {
  const { slug } = params
}
 
// After
import { use } from 'react'
 
type Params = Promise<{ slug: string }>
 
export default function Layout(props: {
  children: React.ReactNode
  params: Params
}) {
  const params = use(props.params)
  const slug = params.slug
}

异步页面

app/page.tsx
// Before
type Params = { slug: string }
type SearchParams = { [key: string]: string | string[] | undefined }
 
export function generateMetadata({
  params,
  searchParams,
}: {
  params: Params
  searchParams: SearchParams
}) {
  const { slug } = params
  const { query } = searchParams
}
 
export default async function Page({
  params,
  searchParams,
}: {
  params: Params
  searchParams: SearchParams
}) {
  const { slug } = params
  const { query } = searchParams
}
 
// After
type Params = Promise<{ slug: string }>
type SearchParams = Promise<{ [key: string]: string | string[] | undefined }>
 
export async function generateMetadata(props: {
  params: Params
  searchParams: SearchParams
}) {
  const params = await props.params
  const searchParams = await props.searchParams
  const slug = params.slug
  const query = searchParams.query
}
 
export default async function Page(props: {
  params: Params
  searchParams: SearchParams
}) {
  const params = await props.params
  const searchParams = await props.searchParams
  const slug = params.slug
  const query = searchParams.query
}

同步页面

'use client'
 
// Before
type Params = { slug: string }
type SearchParams = { [key: string]: string | string[] | undefined }
 
export default function Page({
  params,
  searchParams,
}: {
  params: Params
  searchParams: SearchParams
}) {
  const { slug } = params
  const { query } = searchParams
}
 
// After
import { use } from 'react'
 
type Params = Promise<{ slug: string }>
type SearchParams = Promise<{ [key: string]: string | string[] | undefined }>
 
export default function Page(props: {
  params: Params
  searchParams: SearchParams
}) {
  const params = use(props.params)
  const searchParams = use(props.searchParams)
  const slug = params.slug
  const query = searchParams.query
}
// Before
export default function Page({ params, searchParams }) {
  const { slug } = params
  const { query } = searchParams
}
 
// After
import { use } from "react"
 
export default function Page(props) {
  const params = use(props.params)
  const searchParams = use(props.searchParams)
  const slug = params.slug
  const query = searchParams.query
}
 

路由处理程序

app/api/route.ts
// Before
type Params = { slug: string }
 
export async function GET(request: Request, segmentData: { params: Params }) {
  const params = segmentData.params
  const slug = params.slug
}
 
// After
type Params = Promise<{ slug: string }>
 
export async function GET(request: Request, segmentData: { params: Params }) {
  const params = await segmentData.params
  const slug = params.slug
}
app/api/route.js
// Before
export async function GET(request, segmentData) {
  const params = segmentData.params
  const slug = params.slug
}
 
// After
export async function GET(request, segmentData) {
  const params = await segmentData.params
  const slug = params.slug
}

runtime 配置(破坏性更改)

runtime 段配置 以前除了 edge 之外还支持 experimental-edge 值。这两种配置都指同一件事,为了简化选项,如果使用 experimental-edge,我们现在会报错。要解决此问题,请将您的 runtime 配置更新为 edgecodemod 可用于自动执行此操作。

fetch 请求

fetch 请求 默认不再缓存。

要选择将特定的 fetch 请求加入缓存,您可以传递 cache: 'force-cache' 选项。

app/layout.js
export default async function RootLayout() {
  const a = await fetch('https://...') // Not Cached
  const b = await fetch('https://...', { cache: 'force-cache' }) // Cached
 
  // ...
}

要选择将布局或页面中的所有 fetch 请求加入缓存,您可以使用 export const fetchCache = 'default-cache' 段配置选项。如果单个 fetch 请求指定了 cache 选项,则将使用该选项。

app/layout.js
// Since this is the root layout, all fetch requests in the app
// that don't set their own cache option will be cached.
export const fetchCache = 'default-cache'
 
export default async function RootLayout() {
  const a = await fetch('https://...') // Cached
  const b = await fetch('https://...', { cache: 'no-store' }) // Not cached
 
  // ...
}

路由处理程序

路由处理程序 中的 GET 函数默认不再缓存。要选择将 GET 方法加入缓存,您可以使用路由配置选项,例如在您的路由处理程序文件中使用 export const dynamic = 'force-static'

app/api/route.js
export const dynamic = 'force-static'
 
export async function GET() {}

客户端路由器缓存

当通过 <Link>useRouter 在页面之间导航时,页面 段不再从客户端路由器缓存中重用。但是,它们仍然在浏览器后退和前进导航以及共享布局期间重用。

要选择将页面段加入缓存,您可以使用 staleTimes 配置选项

next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
  experimental: {
    staleTimes: {
      dynamic: 30,
      static: 180,
    },
  },
}
 
module.exports = nextConfig

布局加载状态仍然在导航时缓存和重用。

next/font

@next/font 包已被删除,取而代之的是内置的 next/fontcodemod 可用于安全且自动地重命名您的导入。

app/layout.js
// Before
import { Inter } from '@next/font/google'
 
// After
import { Inter } from 'next/font/google'

bundlePagesRouterDependencies

experimental.bundlePagesExternals 现在已稳定,并重命名为 bundlePagesRouterDependencies

next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
  // Before
  experimental: {
    bundlePagesExternals: true,
  },
 
  // After
  bundlePagesRouterDependencies: true,
}
 
module.exports = nextConfig

serverExternalPackages

experimental.serverComponentsExternalPackages 现在已稳定,并重命名为 serverExternalPackages

next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
  // Before
  experimental: {
    serverComponentsExternalPackages: ['package-name'],
  },
 
  // After
  serverExternalPackages: ['package-name'],
}
 
module.exports = nextConfig

Speed Insights

Next.js 15 中移除了 Speed Insights 的自动检测。

要继续使用 Speed Insights,请按照 Vercel Speed Insights 快速入门 指南进行操作。

NextRequest 地理位置

NextRequest 上的 geoip 属性已被移除,因为这些值由您的托管服务提供商提供。codemod 可用于自动化此迁移。

如果您正在使用 Vercel,您也可以使用 @vercel/functions 中的 geolocationipAddress 函数来代替

middleware.ts
import { geolocation } from '@vercel/functions'
import type { NextRequest } from 'next/server'
 
export function middleware(request: NextRequest) {
  const { city } = geolocation(request)
 
  // ...
}
middleware.ts
import { ipAddress } from '@vercel/functions'
import type { NextRequest } from 'next/server'
 
export function middleware(request: NextRequest) {
  const ip = ipAddress(request)
 
  // ...
}