进展和成果
- 12.31 / 1.1 预期完成 Learn Next.js 的 10-16 部分,实际完成 10-13 部分。
- NextJs-Dashboard 完成了 Search 和分页、动态路由匹配、错误处理(catch-all、partial error handling)
- 离完成估计差 1 天。
- 本次归档合并上一次归档,上一次归档一些细节忽略,用🤐表示。省流用🌮表示。
技术难题和解决方案
-
tailwind 是 css-in-js 的代表之一,clsx 可以帮助 tailwind 条件切换组件的状态:
<span className={clsx( 'inline-flex items-center rounded-full px-2 py-1 text-sm', { -> 'bg-gray-100 text-gray-500': status === 'pending', -> 'bg-green-500 text-white': status === 'paid', } > // Another example {links.map((link) => { const LinkIcon = link.icon; return ( <Link key={link.name} href={link.href} + className={clsx( + 'flex h-[48px] grow items-center justify-center gap-2 rounded-md bg-gray-50 p-3 text-sm font-medium hover:bg-sky-100 hover:text-blue-600 md:flex-none md:justify-start md:p-2 md:px-3', + { + 'bg-sky-100 text-blue-600': pathname === link.href, + },// 左:没条件判断 vs 右:有条件判断 )} > <LinkIcon className="w-6" /> <p className="hidden md:block">{link.name}</p> </Link> ); })}
一些目前用到的可能常见的 attribute 有:
flex
- 这个类应用了 CSS 的 flexbox 布局,使得子元素以灵活的方式排列。h-screen
- 设置元素高度为视口的高度。flex-col
- 子元素垂直排列。md:flex-row
- 在中等屏幕尺寸(如平板电脑)以上时,子元素水平排列。md:overflow-hidden
- 在中等屏幕尺寸以上时,隐藏超出父元素的内容。w-full
- 元素宽度设置为 100%。flex-none
- 元素不参与 flexbox 布局的伸缩。md:w-64
- 在中等屏幕尺寸以上时,元素宽度设置为固定的尺寸(这里是 64 个单位)。flex-grow
- 元素会占据剩余空间。p-6
- 在所有方向上应用内边距(padding)。md:overflow-y-auto
- 在中等屏幕尺寸以上时,如果内容超出元素高度,会显示垂直滚动条。md:p-12
- 在中等屏幕尺寸以上时,增加内边距。
🤐字体和图片优化部分详见「NextJs-CSS」文档。Link 见「NextJs-Route」文档。
-
写了 Search 组件的解析。🌮在 Page 组件中,可以通过 props 获取当前的 searchParams,也可以获取 params。🎫如何区分俩个参数?。searchParams 可能为空所以
const query = searchParams?.query || '';
,以及 searchParams 的参数默认为 string,遇到整型需要 Number 强转const currentPage = Number(searchParams?.page) || 1;
。客户端路由操作导致了依赖于这些 URL 参数的Page
组件响应这些变化并重新渲染。Search 的 onchange 触发下面的函数:
const handleSearch = useDebouncedCallback((term)=>{ // console.log(term); const params = new URLSearchParams(searchParams); params.set('page', '1'); if (term) { params.set('query', term); } else { params.delete('query'); } replace(`${pathname}?${params.toString()}`); },300);
新的 Search input 会导致 Page 重新加载宏任务,具体表现是 console log 重新打印:
import Search from '@/app/ui/search'; import Table from '@/app/ui/invoices/table'; import {CreateInvoice} from '@/app/ui/invoices/buttons'; import {lusitana} from '@/app/ui/fonts'; import {InvoicesTableSkeleton} from '@/app/ui/skeletons'; import {Suspense} from 'react'; import { fetchInvoicesPages } from '@/app/lib/data'; import Pagination from "@/app/ui/invoices/pagination"; export default async function Page({searchParams}: { searchParams?: { query?: string; page?: string; }; }) { const query = searchParams?.query || ''; const currentPage = Number(searchParams?.page) || 1; console.log(query+"#"); console.log(currentPage+"###"); const totalPages = await fetchInvoicesPages(query); return ( <div className="w-full"> <div className="flex w-full items-center justify-between"> <h1 className={`${lusitana.className} text-2xl`}>Invoices</h1> </div> <div className="mt-4 flex items-center justify-between gap-2 md:mt-8"> <Search placeholder="Search invoices..."/> <CreateInvoice/> </div> <Suspense key={query + currentPage} fallback={<InvoicesTableSkeleton/>}> <Table query={query} currentPage={currentPage}/> </Suspense> <div className="mt-5 flex w-full justify-center"> <Pagination totalPages={totalPages} /> </div> </div> ); }
# 1### del# // del 是新输入的 1###
这种客户端路由操作导致了依赖于这些 URL 参数的
Page
组件响应这些变化并重新渲染。实际还有 input debounce,避免 onchange 按每个字母输入进行查询。用了
use-debounce
组件。使用方式:将原本 handleSearch 函数用 useDebouncedCallback 包裹,在用户输入停止 300ms 触发搜索。const handleSearch = useDebouncedCallback((term) => { console.log(`Searching... ${term}`); const params = new URLSearchParams(searchParams); if (term) { params.set('query', term); } else { params.delete('query'); } replace(`${pathname}?${params.toString()}`); }, 300);
-
use server 和 use client 的不同组件使用:
deleteInvoice.bind(null, id) 在 server 端使用,由于 deleteInvoice 调用包含 sql 语句的 action 函数在 action.ts 中被 use server 覆盖,其 deleteInvoice.bind 对应的函数也不能在 client 侧使用。
useSearchParams
这是一个客户端钩子,用于在组件中获取当前 URL 查询参数。在服务器端渲染过程中,可能没有 URL 查询参数可供访问,因此调用这个钩子会导致错误。这是为了避免在服务器端渲染时访问客户端相关的信息,因为服务器端渲染是在请求阶段执行的,而客户端渲染则发生在浏览器中。sql 只有在 data.ts 和 action.ts 使用到。都是在 server 端,所以依靠的是 server 端的 .env 配置与数据库通信。
-
Dynamic Route Segments 就是 id 是动态的。/page/3/xxx,id 是 3。
首先页面组件是 UpdateInvoice,传入了一个 id。
<UpdateInvoice id={invoice.id} />
UpdateInvoice 为一个 Link,指向:
href={`/dashboard/invoices/${id}/edit`}
在 edit 的 page 的 params 中:
export default async function Page({ params }: { params: { id: string } }) { const id = params.id;
In addition to
searchParams
, page components also accept a prop calledparams
which you can use to access theid
. Update your<Page>
component to receive the prop -
保证数据写到后端前,数据格式对齐。可以使用 Zod 组件:
const FormSchema = z.object({ id: z.string(), customerId: z.string(), amount: z.coerce.number(),//强转后会验证You can then pass your rawFormData to CreateInvoice to validate the types: status: z.enum(['pending', 'paid']), date: z.string(), }); const CreateInvoice = FormSchema.omit({id: true, date: true}); export async function createInvoice(formData: FormData) { const { customerId, amount, status } = CreateInvoice.parse({ customerId: formData.get('customerId'), amount: formData.get('amount'), status: formData.get('status'), }); // ...
未解决的问题/测试和质量保证
-
🎫: 提到了用 cache。Server Actions are also deeply integrated with Next.js caching. When a form is submitted through a Server Action, not only can you use the action to mutate data, but you can also revalidate the associated cache using APIs like
revalidatePath
andrevalidateTag
.🎫: 使用的方式就是在 action(这里是 createInvoices)的末尾加上
revalidatePath('/dashboard/invoices')
,这里也是对 NextJs 路由的巧用,我猜测 cache 以文件路由的方式管理,便于复用和清除/重新请求。🎫:
<main>
合理推测是 layout 后的主要部分。🎫: 还需补充 client 和 server 组件使用差异。可以在掘金上搜搜。Next.js Server Actions... 5 awesome things you can do - YouTube
🎫: error.ts细节待看。