特色

  • 要求 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>

并行路由

Next.js 作弊表

Next.js 作弊表

运用 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;
}

代码安排

Next.js 作弊表

Next.js 作弊表

Next.js 作弊表

Next.js 作弊表

国际化

依据言语跳转路由:

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