Next.js 作弊表

3,960 阅读8分钟

特点

  • 要求 Node.js 16.8+
  • 支持两种路由:Pages Router 和 App Router,官方推荐后者
  • 支持 JS、JSX、TS、TSX,官方推荐 TS 和 TSX
  • 支持 Server Components

缺点

很多其他功能既不支持,也不提供参考文档。适合轻应用,或者动手能力强的开发者。

初始化

npx create-next-app@版本号 目录名
cd 目录名
npm run dev # 启动开发环境的 server
npm run build # 编译
npm run start # 启动生产环境 server
npm run lint # eslint 检查

App 目录路由映射

  • app 目录映射到 / 路由 ,一般至少包含 layout.tsxpage.tsx
    • layout.tsx 一般要渲染 children
    • page.tsx 的默认导出一般是一个 React 组件
    • loading.tsx 默认导出的组件用于展示加载中
    • not-found.tsx 展示 404
    • error.tsx 展示错误
    • global-error 展示全局错误
    • route.ts 用作 API
    • template.tsxlayout.tsx 类似,但是它不会跨页面,渲染时一般需要指定 key 为 routeParam
    • default.tsx 用于渲染没有匹配到的路由
  • app/folder 目录映射到 /folder 路由,folder 目录的结构跟 app 类似
  • [] 命名的目录表示动态路由
    • [folder] 表示动态路由片段
    • [...folder] 表示捕获所有片段
    • [[...folder]] 表示可选的捕获所有片段
  • () 命名的目录会被路由忽略,即 app/(xxx)/about 目录会映射到 app/about 路由
    • (.) 表示当前路径,用得不多,如 app/(.)feed 目录会映射到 app/feed 路由
    • (..) 表示父目录,如 app/feed/(..)photo 目录会映射到 app/photo 路由
    • (...) 表示根目录,如 app/feed/(...)photo 目录会映射到 /photo 路由
  • _ 命名的目录表示禁用路由,即 app/_xxx 目录不会对应任何路由
  • @ 命名的目录会被当做具名插槽
    1. 假设 layout.tsx 渲染 childrenchildren2children3,那么
    2. children 会对应 page.tsx 的默认导出
    3. children2 会对应 @children2/page.tsx 的默认导出
    4. children3 会对应 @children3/page.tsx 的默认导出

Pages 路由映射

  • pages/index.js 文件映射到 / 路由,需要定义默认导出
    • pages/blog/index.js 文件映射到 /blog 路由
    • pages/blog.js 文件也匹配到 /blog,但是不能继续嵌套了
    • xxx/index.jsxxx.js 功能相同,但前者可以有子目录,后者不行
  • pages/blog/first-post.js → /blog/first-post
    • pages/posts/[id].js/posts/1 or /posts/2
    • pages/shop/[...slug].js/shop/a or /shop/a/b or /shop/a/b/c or ...
    • pages/shop/[[...slug]].js 比上面的文件多匹配一个 /shop
  • pages/_app.js 用于自定义当前页面,对应的路由依然是 /,其默认导出的组件需要渲染 children
    • _app.js 的默认导出其实就扮演着 layout 的功能,不过你需要自行在 components/layout.js 中定义 layout 组件。
    • 如果你需要 layout 支持多个插槽,那么你需要自己实现
    • 如果你需要一个页面支持多个 layout,那么你需要自己实现(虽然文档有例子)

总得来看,App 路由映射比 Pages 路由映射的功能更强大,设计更合理。

Server Components

借鉴了 PHP 和 Rails。

所有组件默认都是 Server Component,除非你在文件顶部使用 "use client" 指令将其指定为客户端组件。一般只需要在入口处指定即可,不需要递归地使用指令。

顾名思义,Server 组件在 Server 上渲染,Client 组件在客户端渲染。但是 Client 组件会在服务器上进行预渲染,然后在客户端上进行水合。

Server 组件不支持 Context。如果你需要在多个 Server 组件之间共享数据,你应该直接用 JS 自带的功能(而不是 React 的功能),比如将数据存储在一个变量或对象的属性里。

Client Components

当你

  • 使用了 onClick、onChange
  • 使用了 useState、useReducer、useEffect
  • 使用了 BOM API 或 DOM API
  • 使用了 Class 组件
  • 间接使用了上面的东西

那么你就应该使用 Client Components。

如果你使用了第三方组件,那么你无法到组件文件的顶部添加 "use client",此时你只能再套一个文件,在文件里使用 "use client" 并导出第三方组件。

在 Client 组件中渲染 Server 组件

如果你想在 Client 组件中渲染 Server 组件,那么你只能通过 children 或其他属性来渲染,不能直接 import 并渲染。

而且由于 Server 组件要在 JSON 序列化之后传给 Client 组件,所以 JSON 不支持的值都不能直接传递。

修改 <head>

你可以在 layout.js 或者 page.js 中定义

  1. metadata: Metadata 对象
  2. generateMetadata 函数

来改变 <head> 中的属性,如:

import { Metadata } from 'next';
 
export const metadata: Metadata = {
  title: 'Next.js',
};
 
export default function Page() {
  return '...';
}

使用 <Link> 导航

<ul> 
  {posts.map((post) => (
    <li key={post.id}>
      <Link
        className={pathname.startsWith(`/blog/${post.slug}`) ? 'blue' : 'black'}
        href={`/blog/${post.slug}#id`}
        scroll={false}
      >{post.title}</Link>
    </li>
  ))} 
</ul>

其中 scroll 属性可以用来控制跳转之后是否滚动到页面顶部,如果 scroll={false} 那么就会滚动到 href 属性中的 #id。

使用 useRouter() 导航

'use client';
 
import { useRouter } from 'next/navigation';
 
export default function Page() {
  const router = useRouter();
 
  return (
    <button type="button" onClick={() => router.push('/dashboard')}>
      Dashboard
    </button>
  );
}

使用 loading.tsx

export default function Loading() {
  // You can add any UI inside Loading, including a Skeleton.
  return <LoadingSkeleton />;
}

可以看作

<Layout>
  <Suspense fallback={<Loading /> }>
    <Page />
  </Suspense>
</Layout>

使用 error.tsx

'use client'; // Error components must be Client Components
 
import { useEffect } from 'react';
 
export default function Error({
  error,
  reset,
}: {
  error: Error;
  reset: () => void;
}) {
  useEffect(() => {
    // Log the error to an error reporting service
    console.error(error);
  }, [error]);
 
  return (
    <div>
      <h2>Something went wrong!</h2>
      <button
        onClick={
          // Attempt to recover by trying to re-render the segment
          () => reset()
        }
      >
        Try again
      </button>
    </div>
  );
}

可以看作是

<Layout>
<ErrorBoundary fallback={ <Error />} >
  <Page />
</ErrorBoundary>
</Layout>

并行路由

image.png

image.png

使用 Modal

import { Modal } from 'components/modal';
 
export default function Login() {
  return (
    <Modal>
      <h1>Login</h1>
      {/* ... */}
    </Modal>
  );
}

使用 route.ts

// app/products/api/route.ts
import { NextResponse } from 'next/server';
 
export async function GET(request: Request) {
  const { searchParams } = new URL(request.url);
  const id = searchParams.get('id');
  const res = await fetch(`https://data.mongodb-api.com/product/${id}`, {
    headers: {
      'Content-Type': 'application/json',
      'API-Key': process.env.DATA_API_KEY,
    },
  });
  const product = await res.json();
 
  return NextResponse.json({ product });
}
// app/items/route.ts
import { NextResponse } from 'next/server';
 
export async function POST() {
  const res = await fetch('https://data.mongodb-api.com/...', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'API-Key': process.env.DATA_API_KEY,
    },
    body: JSON.stringify({ time: new Date().toISOString() }),
  });
 
  const data = await res.json();
 
  return NextResponse.json(data);
}

使用 cookies()

import { cookies } from 'next/headers';
 
export async function GET(request: Request) {
  const cookieStore = cookies();
  const token = cookieStore.get('token');
 
  return new Response('Hello, Next.js!', {
    status: 200,
    headers: { 'Set-Cookie': `token=${token}` },
  });
}

使用 headers()

import { headers } from 'next/headers';
 
export async function GET(request: Request) {
  const headersList = headers();
  const referer = headersList.get('referer');
 
  return new Response('Hello, Next.js!', {
    status: 200,
    headers: { referer: referer },
  });
}

使用 redirect()

import { redirect } from 'next/navigation';
 
export async function GET(request: Request) {
  redirect('https://nextjs.org/');
}

获取 params

export async function GET(
  request: Request,
  {
    params,
  }: {
    params: { slug: string };
  },
) {
  const slug = params.slug; // 'a', 'b', or 'c'
}

使用流 Straming

// https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream#convert_async_iterator_to_stream
function iteratorToStream(iterator: any) {
  return new ReadableStream({
    async pull(controller) {
      const { value, done } = await iterator.next();
 
      if (done) {
        controller.close();
      } else {
        controller.enqueue(value);
      }
    },
  });
}
 
function sleep(time: number) {
  return new Promise((resolve) => {
    setTimeout(resolve, time);
  });
}
 
const encoder = new TextEncoder();
 
async function* makeIterator() {
  yield encoder.encode('<p>One</p>');
  await sleep(200);
  yield encoder.encode('<p>Two</p>');
  await sleep(200);
  yield encoder.encode('<p>Three</p>');
}
 
export async function GET() {
  const iterator = makeIterator();
  const stream = iteratorToStream(iterator);
 
  return new Response(stream);
}

获取请求体

import { NextResponse } from 'next/server';
 
export async function POST(request: Request) {
  const res = await request.json();
  return NextResponse.json({ res });
}

CORS

export async function GET(request: Request) {
  return new Response('Hello, Next.js!', {
    status: 200,
    headers: {
      'Access-Control-Allow-Origin': '*',
      'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
      'Access-Control-Allow-Headers': 'Content-Type, Authorization',
    },
  });
}

配置路由片段

// /app/items/route.ts
export const dynamic = 'auto';
export const dynamicParams = true;
export const revalidate = false;
export const fetchCache = 'auto';
export const runtime = 'nodejs';
export const preferredRegion = 'auto';

更多说明:nextjs.org/docs/app/ap…

中间件

import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
 
export function middleware(request: NextRequest) {
  // Clone the request headers and set a new header `x-hello-from-middleware1`
  const requestHeaders = new Headers(request.headers);
  requestHeaders.set('x-hello-from-middleware1', 'hello');
 
  // You can also set request headers in NextResponse.rewrite
  const response = NextResponse.next({
    request: {
      // New request headers
      headers: requestHeaders,
    },
  });
 
  // Set a new response header `x-hello-from-middleware2`
  response.headers.set('x-hello-from-middleware2', 'hello');
  return response;
}

代码组织

image.png

image.png

image.png

image.png

国际化

根据语言跳转路由:

import { NextResponse } from 'next/server'
 
let locales = ['en-US', 'nl-NL', 'nl']
 
// Get the preferred locale, similar to above or using a library
function getLocale(request) { ... }
 
export function middleware(request) {
  // Check if there is any supported locale in the pathname
  const pathname = request.nextUrl.pathname
  const pathnameIsMissingLocale = locales.every(
    (locale) => !pathname.startsWith(`/${locale}/`) && pathname !== `/${locale}`
  )
 
  // Redirect if there is no locale
  if (pathnameIsMissingLocale) {
    const locale = getLocale(request)
 
    // e.g. incoming request is /products
    // The new URL is now /en-US/products
    return NextResponse.redirect(
      new URL(`/${locale}/${pathname}`, request.url)
    )
  }
}
 
export const config = {
  matcher: [
    // Skip all internal paths (_next)
    '/((?!_next).*)',
    // Optional: only run on root (/) URL
    // '/'
  ],
}
// app/[lang]/page.js
// You now have access to the current locale
// e.g. /en-US/products -> `lang` is "en-US"
export default async function Page({ params: { lang } }) {
  return ...
}

本地化

// dictionaries/en.json
{
  "products": {
    "cart": "Add to Cart"
  }
}
// dictionaries/zh.json
{
  "products": {
    "cart": "添加到购物车"
  }
}
// app/[lang]/dictionaries.js
import 'server-only';
 
const dictionaries = {
  en: () => import('./dictionaries/en.json').then((module) => module.default),
  nl: () => import('./dictionaries/nl.json').then((module) => module.default),
};
 
export const getDictionary = async (locale) => dictionaries[locale]();
// app/[lang]/page.js
import { getDictionary } from './dictionaries';
 
export default async function Page({ params: { lang } }) {
  const dict = await getDictionary(lang); // en
  return <button>{dict.products.cart}</button>; // Add to Cart
}

获取数据

async function getData() {
    const res = await fetch('<https://api.example.com/>...');
    // The return value is *not* serialized
    // You can return Date, Map, Set, etc.

    // Recommendation: handle errors
    if (!res.ok) {
    // This will activate the closest `error.js` Error Boundary
    throw new Error('Failed to fetch data');
    }

    return res.json();
    }

    export default async function Page() {
    const data = await getData();

    return <main></main>;
}

使用 fetch

fetch('https://...'); // cache: 'force-cache' is the default
fetch('https://...', { next: { revalidate: 10 } });
fetch('https://...', { cache: 'no-store' });

Server Actions

// next.config.js 
module.exports = {
  experimental: {
    serverActions: true,
  },
};
// app/add-to-cart.jsx
import { cookies } from 'next/headers';
 
// Server action defined inside a Server Component
export default function AddToCart({ productId }) {
  async function addItem(data) {
    'use server';
 
    const cartId = cookies().get('cartId')?.value;
    await saveToDb({ cartId, data });
  }
 
  return (
    <form action={addItem}>
      <button type="submit">Add to Cart</button>
    </form>
  );
}

使用 startTransition

'use client';
 
import { useTransition } from 'react';
import { addItem } from '../actions';
 
function ExampleClientComponent({ id }) {
  let [isPending, startTransition] = useTransition();
 
  return (
    <button onClick={() => startTransition(() => addItem(id))}>
      Add To Cart
    </button>
  );
}

'use server';
 
export async function addItem(id) {
  await addItemToDb(id);
  // Marks all product pages for revalidating
  revalidatePath('/product/[id]');
}

使用 Image

优点:

  1. 尺寸优化:使用现代图像格式(如WebP和AVIF),为每个设备自动提供正确尺寸的图像。
  2. 视觉稳定性:当图片正在加载时自动防止布局移动。
  3. 更快的页面加载:使用本地浏览器惰性加载和可选的模糊占位符,仅在图像进入视口时加载图像。
  4. 灵活性:按需调整图像大小,即使是存储在远程服务器上的图像。
import Image from 'next/image';
import profilePic from './me.png';
 
export default function Page() {
  return (
    <Image
      src={profilePic}
      alt="Picture of the author"
      // width={500} automatically provided
      // height={500} automatically provided
      // blurDataURL="data:..." automatically provided
      // placeholder="blur" // Optional blur-up while loading
    />
  );
}

如果 src 是一个远程图片,我们就需要手动指定宽高和 blurDataURL 了。

你也可以在 next.config.js 统一配置:

module.exports = {
  images: {
    remotePatterns: [
      {
        protocol: 'https',
        hostname: 's3.amazonaws.com',
        port: '',
        pathname: '/my-bucket/**',
      },
    ],
  },
};

使用 Font

中文网站用得不多,大家自己看 nextjs.org/docs/app/bu…

使用 Script

import Script from 'next/script';
 
export default function DashboardLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <>
      <section>{children}</section>
      <Script src="https://example.com/script.js" />
    </>
  );
}

懒加载

你可以使用 next/dynamicReact.lazy() 实现懒加载。

'use client';
 
import { useState } from 'react';
import dynamic from 'next/dynamic';
 
// Client Components:
const ComponentA = dynamic(() => import('../components/A'));
const ComponentB = dynamic(() => import('../components/B'));
const ComponentC = dynamic(() => import('../components/C'), { ssr: false });
 
export default function ClientComponentExample() {
  const [showMore, setShowMore] = useState(false);
 
  return (
    <div>
      {/* Load immediately, but in a separate client bundle */}
      <ComponentA />
 
      {/* Load on demand, only when/if the condition is met */}
      {showMore && <ComponentB />}
      <button onClick={() => setShowMore(!showMore)}>Toggle</button>
 
      {/* Load only on the client side */}
      <ComponentC />
    </div>
  );
}

参考手册

API Reference