所用技术栈:Next.js
, Tailwind CSS
, TypeScript
, DaisyUI
使用 anchor 标签这种导航方式会重新加载重复网页文件的,而不是替换需要的内容:
export default function Home() {
return (
<main>
<h1>Hello World</h1>
<a href='/users'>Users</a>
</main>
)
}
因此使用next中的Link
组件:
<Link href='/users'>Users</Link>
-
用户端可交互,暴露密钥给用户,标准React App的工作方式
-
服务端资源占用小,搜索引擎bot可以查看页面并建立索引, 在服务器上保留API等敏感数据,失去了交互性
因此现实中默认使用服务端组件(Next中就是),必须使才使用客户端组件。
比如在一个商城应用中,应该把导航、侧边栏、页码页脚等组件放在服务端,对于需要交互的商品组件,可以只把需要点击的微小组件(如“AddToCart”)发送到客户端,
- Client:
useState()
,useEffect()
ReactQuery
Large bundles, No SEO(Search Engine Optimization), Less secure, Extra roundtrip to server
- Server:
Typescript Magic:
interface User {
id: number;
name: string;
}
const UsersPage = async () => {
const res = await fetch('https://jsonplaceholder.typicode.com/users');
const users: User[] = await res.json();
return (
<>
<h1>Users</h1>
<ul>
{users.map(users => <li key={users.id}>{users.name}</li>)}
</ul>
</>
)
}
(-> slower)
Memory -> File System -> Networks
For this reason, Next.js
has a built in data cache( in fetch
):
const res = await fetch(
'https://jsonplaceholder.typicode.com/users',
{cache: 'no-store'});
// or
{next: {revalidate: 10}} // 10s
对于
<p>{new Date().toLocaleTimeString()}</p>
npm run build
使用cache: 'no-store'
后:
scoped to a single component / page, preventing clashing and overwriting
新建ProductCard.module.css
,使用:
import styles from './ProductCard.module.css'
<div className={styles.card}>
Nextjs 使用 postcss
来生成唯一类名。
<div className='p-5 my-5 bg-sky-400 text-white text-xl hover:bg-sky-600'>
好神奇,删除这个组件就是删除了,不需要再去找对应的css文件。
类似boostrap。
- Installation
npm i -D daisyui@latest
- Then add daisyUI to the
tailwind.config.ts
files:
module.exports = {
//...
plugins: [require("daisyui")],
}
button className='btn btn-primary' onClick={() => console.log('clicked')}>Add to cart</button>
tailwind.config.ts
:
plugins: [require("daisyui")],
daisyui: {
themes: ["winter"],
},
layout.tsx
:
<html lang="en" data-theme="winter">
-
page.tsx
:可公开访问的页面文件 -
layout.tsx
:定义页面通用布局 -
loading.tsx
:显示加载的UI ^bbaedd -
route.tsx
:创建API -
not-found.tsx
:显示常规错误 -
error.tsx
:自定义错误页面
动态路由就是带参数的路由:
- 在
[id]
文件夹的page.tsx
中:
interface Props {
parmas: { id: number }
}
// 这里是直接解构出参数变量
const UserDetailPage = ({ parmas: {id} }: Props) => {
return (
<div>UserDetailPage {id}</div>
)
}
- 更多参数的路由
interface Props {
params: { id: number; photoId: number }
}
const PhotoPage = ({params: {id, photoId}}: Props) => {
return (
<div>PhotoPage {id} {photoId}</div>
)
}
文件名:[[...slug]]
使所有路径segments的捕获变为可选项,即还可以匹配所在主路径本身:
interface Props {
params: {slug: string[]}
}
const ProductPage = ({ params: { slug }}: Props) => {
return (
<div>ProductPage {slug}</div>
)
}
page.tsx
中将路由字符传入组件中:
interface Props {
searchParams: { sortOrder: string };
}
const UsersPage = async ({ searchParams: { sortOrder }}: Props) => {
return (
<>
<h1>Users</h1>
<UserTable sortOrder={ sortOrder }/>
</>
)
}
UserTable.tsx
中:
import { sort } from 'fast-sort';
interface Props {
sortOrder: string;
}
const UserTable = async ({ sortOrder }: Props) => {
const res = await fetch(
'https://jsonplaceholder.typicode.com/users',
{cache: 'no-store'});
const users: User[] = await res.json();
const sortedUsers = sort(users).asc(
sortOrder == 'email'
? user => user.email
: user => user.name
);
return (...
<Link href="/users?sortOrder=name">Name</Link>
</th>
<th>
<Link href="/users?sortOrder=email">Email</Link>
...
{sortedUsers.map(users => <tr key={users.id}>
<td>{users.name}</td>
<td>{users.email}</td></tr>)}...
}
使用layout来创建在多个页面中共享的UI
- 在新建的admin文件夹中
layout.tsx
,可以定义这个文件夹中page.tsx
的布局:
import React, { ReactNode } from 'react'
interface Props {
children: ReactNode;
}
const AdminLayout = ({ children }: Props) => {
return (
<div className='flex'>
<aside className='bg-slate-200 p-5 mr-5'>Admin Sidebar</aside>
<div>{children}</div>
</div>
)
}
export default AdminLayout
- app文件夹中
NavBar.tsx
:
import Link from 'next/link'
import React from 'react'
const NavBar = () => {
return (
<div className='flex bg-slate-200 p-5'>
<Link href="/" className='mr-5'>Next.js</Link>
<Link href="/users">Users</Link>
</div>
)
}
export default NavBar
layout.tsx
:
return (
<html lang="en" data-theme="winter">
<body className={inter.className}>
<NavBar />
<main className='p-5 '>
{children}
</main>
</body>
</html>
in global.css
:
@layer base {
h1 {
@apply font-bold text-2xl mb-3
}
}
- 只下载目标页面内容
- 预获取viewport内链接的页面
- 将页面缓存在客户端中
注意这里,默认的import路径可能不是这个:
'use client'
import { useRouter } from 'next/navigation'
import React from 'react'
const NewUserPage = () => {
const router = useRouter()
return (
<button className='btn' onClick={() => router.push('/users')}>Create</button>
)
}
通过流式传输(streaming),客户端初始接受的html文件后续生命周期中会接受loading后的内容,不影响SEO:
<Suspense fallback={<p>Loading...</p>}>
<UserTable sortOrder={ sortOrder }/>
</Suspense>
多页面loading:
- 在全局
layout.css
中:
<Suspense fallback={<p>Loading...</p>}>
{children}
</Suspense>
- 通过loading files(
loading.tsx
):
const Loading = () => {
return (
<span className="loading loading-spinner loading-md"></span>
)
}
例子中是DaisyUI的组件。
在想要实现的文件目录下编辑not-found.tsx
,如:
import { notFound } from 'next/navigation'
const UserDetailPage = ({ params: {id} }: Props) => {
if (id > 10) notFound()
生产环境下,报错的页面可以自定义error.tsx
:
'use client'
import React from 'react'
interface Props {
error: Error;
reset: () => void;
}
const ErrorPage = ({ error, reset }: Props) => {
console.log('error', error)
return (
<>
<div>An Unexpected Error has Occured</div>
<button className='btn' onClick={() => reset()}>Retry</button>
</>
)
}
global-error.tsx
可以捕获全局layout中的错误。
Route Handler in app/api/users/route.tsx
:
import { NextRequest, NextResponse } from "next/server";
export function GET(request: NextRequest) {
return NextResponse.json([
{ id: 1, name: 'Mosh'},
{ id: 2, name: 'John'},
]);
}
- Get a single object
interface Props {
params: { id: number }
}
export function GET(
request: NextRequest,
{ params }: Props) {}
// or
export function GET(
request: NextRequest,
{ params }: { params: {id: number}}) {
}
in api/users/[id]
:
import { NextRequest, NextResponse } from "next/server";
export function GET(
request: NextRequest,
{ params }: { params: {id: number}}) {
if ( params.id > 10)
return NextResponse.json({ error: 'User Not Found!'}, { status: 404 })
return NextResponse.json({ id: 1, name: 'mosh'})
}
- Creating Object
import { NextRequest, NextResponse } from "next/server";
export function GET(request: NextRequest) {
return NextResponse.json([
{ id: 1, name: 'Mosh'},
{ id: 2, name: 'John'},
]);
}
export async function POST(request: NextRequest) {
const body = await request.json();
// if invalid, return 400
if (!body.name)
return NextResponse.json({ error: 'Name is required'}, { status: 400 })
return NextResponse.json({ id: 1, name: body.name }, { status: 201});
}
- Update an Object
// PUT for replacing object, PATCH for updating 1 or more properties
export async function PUT(
request: NextRequest,
{params}: {params: {id: number}}) {
// Validate the request body
const body = await request.json();
if (!body.name)
return NextResponse.json({ error: 'Name is required'}, { status: 400 });
if (params.id > 10)
return NextResponse.json({ error: 'User Not Found!'}, { status: 404 });
return NextResponse.json({ id: 1, name: body.name });
}
- Delete an Object
export function DELETE(
request: NextRequest,
{params}: {params: {id: number}}
) {
if (params.id > 10)
return NextResponse.json({ error: 'User Not Found!'}, { status: 404 });
return NextResponse.json({}) ;
}
对于复杂的 object,if-else 显然不再方便使用,最好使用 validation library,如zod
api/users/schema.ts
:
import { z } from 'zod'
const schema = z.object({
name: z.string().min(3),
// email: z.string().email(),
// age: z.number()
})
export default schema
api/users/[id]/route.tsx
:
export async function POST(request: NextRequest) {
const body = await request.json();
const validation = schema.safeParse(body);
// if invalid, return 400
if (!validation.success)
return NextResponse.json(validation.error.errors, { status: 400 })
return NextResponse.json({ id: 1, name: body.name }, { status: 201});
}