24 年归档-1-NextJsDashboard小节1

进展和成果

  • 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 有:

    1. flex - 这个类应用了 CSS 的 flexbox 布局,使得子元素以灵活的方式排列。
    2. h-screen - 设置元素高度为视口的高度。
    3. flex-col - 子元素垂直排列。
    4. md:flex-row - 在中等屏幕尺寸(如平板电脑)以上时,子元素水平排列。
    5. md:overflow-hidden - 在中等屏幕尺寸以上时,隐藏超出父元素的内容。
    6. w-full - 元素宽度设置为 100%。
    7. flex-none - 元素不参与 flexbox 布局的伸缩。
    8. md:w-64 - 在中等屏幕尺寸以上时,元素宽度设置为固定的尺寸(这里是 64 个单位)。
    9. flex-grow - 元素会占据剩余空间。
    10. p-6 - 在所有方向上应用内边距(padding)。
    11. md:overflow-y-auto - 在中等屏幕尺寸以上时,如果内容超出元素高度,会显示垂直滚动条。
    12. 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 called params which you can use to access the id. 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 and revalidateTag.

    🎫: 使用的方式就是在 action(这里是 createInvoices)的末尾加上 revalidatePath('/dashboard/invoices'),这里也是对 NextJs 路由的巧用,我猜测 cache 以文件路由的方式管理,便于复用和清除/重新请求。

    🎫: <main> 合理推测是 layout 后的主要部分。

    🎫: 还需补充 client 和 server 组件使用差异。可以在掘金上搜搜。Next.js Server Actions... 5 awesome things you can do - YouTube

    🎫: error.ts细节待看。