特色
- 要求 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.tsx
和page.tsx
-
layout.tsx
一般要烘托children
-
page.tsx
的默许导出一般是一个 React 组件 -
loading.tsx
默许导出的组件用于展现加载中 -
not-found.tsx
展现 404 -
error.tsx
展现过错 -
global-error
展现全局过错 -
route.ts
用作 API -
template.tsx
跟layout.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
目录不会对应任何路由 - 以
@
命名的目录会被作为签字插槽- 假设
layout.tsx
烘托children
、children2
和children3
,那么 -
children
会对应page.tsx
的默许导出 -
children2
会对应@children2/page.tsx
的默许导出 -
children3
会对应@children3/page.tsx
的默许导出
- 假设
Pages 路由映射
-
pages/index.js
文件映射到/
路由,需求定义默许导出-
pages/blog/index.js
文件映射到/blog
路由 -
pages/blog.js
文件也匹配到/blog
,可是不能继续嵌套了 - 即
xxx/index.js
和xxx.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
中定义
- metadata: Metadata 目标
- 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>
并行路由
运用 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;
}
代码安排
国际化
依据言语跳转路由:
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
长处:
- 尺寸优化:运用现代图画格式(如WebP和AVIF),为每个设备主动供给正确尺寸的图画。
- 视觉稳定性:当图片正在加载时主动防止布局移动。
- 更快的页面加载:运用本地浏览器惰性加载和可选的模糊占位符,仅在图画进入视口时加载图画。
- 灵活性:按需调整图画大小,即使是存储在远程服务器上的图画。
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/dynamic
和 React.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