next教程(四)

动态渲染

对那些需要实时更新的页面采用动态渲染,在data.tsx里写入:

1
import { unstable_noStore as noStore } from 'next/cache';

导入unstable_noStore这个函数,然后在fetchRevenue ,fetchLatestInvoices,fetchCardData,fetchFilteredInvoices,fetchInvoiceById,fetchFilteredCustomers这几个函数里调用。

流传输

动态渲染带来的问题就是,应用会等待最慢的那个函数获取数据。然后一次性显示。流传输就是让它分解为块,一块一块的传输。
整个页面流传输,需要创建一个loading.tsx文件,比如创建一个app/dashboard/loading.tsx,写入:

1
2
3
4
5
import DashboardSkeleton from '@/app/ui/skeletons';

export default function Loading() {
return <DashboardSkeleton />;
}

在dashboard目录内创建这个文件将会影响所有页面,这时候可以使用路由分组,就是新建一个名字带括号的目录。比如新建
app\dashboard\(overview)\目录,把loading.tsx,page.tsx放到这个目录里。

组件流传输:参考react的suspend组件用法。

发票页面添加搜索

app/dashboard/invoices/page.tsx文件中写入:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import Pagination from '@/app/ui/invoices/pagination';
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';

export default async function Page({
searchParams,
}: {
searchParams?: {
query?: string;
page?: string;
};
}) {

const query = searchParams?.query || '';
const currentPage = Number(searchParams?.page) || 1;

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>
);
}

app/ui/search.tsx文件中写入:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
'use client';

import {MagnifyingGlassIcon} from '@heroicons/react/24/outline';
import {useSearchParams,usePathname, useRouter} from 'next/navigation';
import { useDebouncedCallback } from 'use-debounce';


export default function Search({placeholder}: { placeholder: string }) {
const searchParams = useSearchParams();
const pathname = usePathname();
const { replace } = useRouter();

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);

return (
<div className="relative flex flex-1 flex-shrink-0">
<label htmlFor="search" className="sr-only">
搜索
</label>
<input
className="peer block w-full rounded-md border border-gray-200 py-[9px] pl-10 text-sm outline-2 placeholder:text-gray-500"
placeholder={placeholder}
onChange={(e) => {
handleSearch(e.target.value);
}}
defaultValue={searchParams.get('query')?.toString()}
/>
<MagnifyingGlassIcon
className="absolute left-3 top-1/2 h-[18px] w-[18px] -translate-y-1/2 text-gray-500 peer-focus:text-gray-900"/>
</div>
);
}

添加分页

app/ui/invoices/pagination.tsx文件中写入:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
'use client';

import {ArrowLeftIcon, ArrowRightIcon} from '@heroicons/react/24/outline';
import clsx from 'clsx';
import Link from 'next/link';
import {generatePagination} from '@/app/lib/utils';
import {usePathname, useSearchParams} from 'next/navigation';


export default function Pagination({totalPages}: { totalPages: number }) {

const pathname = usePathname();
const searchParams = useSearchParams();
const currentPage = Number(searchParams.get('page')) || 1;

const allPages = generatePagination(currentPage, totalPages);

const createPageURL = (pageNumber: number | string) => {
const params = new URLSearchParams(searchParams);
params.set('page', pageNumber.toString());
return `${pathname}?${params.toString()}`;
};


return (
<>
<div className="inline-flex">
<PaginationArrow
direction="left"
href={createPageURL(currentPage - 1)}
isDisabled={currentPage <= 1}
/>

<div className="flex -space-x-px">
{allPages.map((page, index) => {
let position: 'first' | 'last' | 'single' | 'middle' | undefined;

if (index === 0) position = 'first';
if (index === allPages.length - 1) position = 'last';
if (allPages.length === 1) position = 'single';
if (page === '...') position = 'middle';

return (
<PaginationNumber
key={page}
href={createPageURL(page)}
page={page}
position={position}
isActive={currentPage === page}
/>
);
})}
</div>

<PaginationArrow
direction="right"
href={createPageURL(currentPage + 1)}
isDisabled={currentPage >= totalPages}
/>
</div>
</>
);
}

function PaginationNumber({
page,
href,
isActive,
position,
}: {
page: number | string;
href: string;
position?: 'first' | 'last' | 'middle' | 'single';
isActive: boolean;
}) {
const className = clsx(
'flex h-10 w-10 items-center justify-center text-sm border',
{
'rounded-l-md': position === 'first' || position === 'single',
'rounded-r-md': position === 'last' || position === 'single',
'z-10 bg-blue-600 border-blue-600 text-white': isActive,
'hover:bg-gray-100': !isActive && position !== 'middle',
'text-gray-300': position === 'middle',
},
);

return isActive || position === 'middle' ? (
<div className={className}>{page}</div>
) : (
<Link href={href} className={className}>
{page}
</Link>
);
}

function PaginationArrow({
href,
direction,
isDisabled,
}: {
href: string;
direction: 'left' | 'right';
isDisabled?: boolean;
}) {
const className = clsx(
'flex h-10 w-10 items-center justify-center rounded-md border',
{
'pointer-events-none text-gray-300': isDisabled,
'hover:bg-gray-100': !isDisabled,
'mr-2 md:mr-4': direction === 'left',
'ml-2 md:ml-4': direction === 'right',
},
);

const icon =
direction === 'left' ? (
<ArrowLeftIcon className="w-4"/>
) : (
<ArrowRightIcon className="w-4"/>
);

return isDisabled ? (
<div className={className}>{icon}</div>
) : (
<Link className={className} href={href}>
{icon}
</Link>
);
}

然后取消page.tsx文件中的pagination组件注释。在app/ui/search.tsx文件中,handleSearch函数里写入:

1
params.set('page', '1');

最后效果:
image host