无限加载列表
改造 Mock,快速创建假数据
重构Mock
重构item.mock.ts
,详细代码见链接。
tsx
import type { MockMethod } from 'vite-plugin-mock'
let id = 0
// 自增id
const createId = () => {
id += 1
return id
}
// 创建单个数据
const create = (attrs?: Partial<Item>): Item => {
return {
id: createId(),
user_id: 1,
amount: 1000,
tag_ids: [1, 2],
happen_at: '2021-08-01T00:00:00.000Z',
created_at: '2021-08-01T00:00:00.000Z',
updated_at: '2021-08-01T00:00:00.000Z',
kind: 'expenses',
...attrs
}
}
// 创建数据列表
const createList = (n: number, attrs?: Partial<Item>): Item[] => {
return Array.from({ length: n }).map(() => create(attrs))
}
// 创建响应返回值
const createResponse = ({ count = 10, perPage = 10, page = 1 }, attrs?: Partial<Item>): Resources<Item> => {
return {
resources: createList(perPage, attrs),
pager: {
page,
per_page: perPage,
count
}
}
}
// item数据mock方法
export const itemsMock: MockMethod = {
url: '/api/v1/items',
method: 'get',
statusCode: 200,
response: ({ query }: ResponseParams): Resources<Item> => createResponse({ count: 100, perPage: 10, page: parseInt(query.page) })
}
创建me.mock.ts
和mock.ts
文件,修改test.ts
文件,改为导入meMock
和itemsMock
方法。
tsx
import type { MockMethod } from 'vite-plugin-mock'
export const meMock: MockMethod = {
url: '/api/v1/me',
method: 'get',
timeout: 10000,
response: (): Resource<User> => {
return {
resource: {
id: 1,
email: 'frank@frank.com',
updated_at: '2021-08-01T00:00:00.000Z',
created_at: '2021-08-01T00:00:00.000Z',
}
}
},
}
tsx
type ResponseParams = {
query: Record<string, string>
}
修改ItemsList.tsx
组件,使用useSWR的useSWRInfinite,接受一个getKey
方法,用于返回页面的key,在接受一个fetcher
,用于请求数据,还有一个额外的参数<font style="color:rgb(0, 0, 0);">options</font>
tsx
import useSWRInfinite from 'swr/infinite'
import { ajax } from '../../lib/ajax'
interface Props {}
const getKey = (pageIndex: number) => {
return `/api/v1/items?page=${pageIndex + 1}`
}
export const ItemsList: React.FC<Props> = () => {
const { data, error } = useSWRInfinite(
getKey,
async path => (await ajax.get<Resources<Item>>(path)).data
)
console.log(data, error)
const items: Item[] = []
return <div>
<ol >
{items.map(item =>
<li key={item.id} grid grid-cols="[auto_1fr_auto]" grid-rows-2 px-16px py-8px gap-x-12px
border-b-1 b="#EEE">
<div row-start-1 col-start-1 row-end-3 col-end-2 text-24px w-48px h-48px
bg="#D8D8D8" rounded="50%" flex justify-center items-center>
😘
</div>
<div row-start-1 col-start-2 row-end-2 col-end-3>
旅行
</div>
<div row-start-2 col-start-2 row-end-3 col-end-4 text="#999999">
2011年1月1日
</div>
<div row-start-1 col-start-3 row-end-2 col-end-4 text="#53A867">
¥999
</div>
</li>
)}
</ol>
<div p-16px>
<button j-btn>加载更多</button>
</div>
</div>
}
引入FakerJS构造假数据
安装命令pnpm install @faker-js/faker
,详细参数配置可见@faker-js/faker。
修改item.mock.ts
文件的mock数据。
tsx
import type { MockMethod } from 'vite-plugin-mock'
import { faker } from '@faker-js/faker'
let id = 0
const createId = () => {
id += 1
return id
}
const create = (attrs?: Partial<Item>): Item => {
return {
id: createId(),
user_id: 1,
amount: faker.datatype.number({ min: 99, max: 1000_00 }),
tag_ids: [1, 2],
happen_at: faker.date.past().toISOString(),
created_at: faker.date.past().toISOString(),
updated_at: faker.date.past().toISOString(),
kind: 'expenses',
...attrs
}
}
const createList = (n: number, attrs?: Partial<Item>): Item[] => {
return Array.from({ length: n }).map(() => create(attrs))
}
const createResponse = ({ count = 10, perPage = 10, page = 1 }, attrs?: Partial<Item>): Resources<Item> => {
return {
resources: createList(perPage, attrs),
pager: {
page,
per_page: perPage,
count
}
}
}
export const itemsMock: MockMethod = {
url: '/api/v1/items',
method: 'get',
statusCode: 200,
response: ({ query }: ResponseParams): Resources<Item> => createResponse({ count: 100, perPage: 10, page: parseInt(query.page) })
}
请求所有页面的数据
tsx
import type { MockMethod } from 'vite-plugin-mock'
import { faker } from '@faker-js/faker'
let id = 0
const createId = () => {
id += 1
return id
}
const create = (attrs?: Partial<Item>): Item => {
return {
id: createId(),
user_id: 1,
amount: faker.datatype.number({ min: 99, max: 1000_00 }),
tag_ids: [1, 2],
happen_at: faker.date.past().toISOString(),
created_at: faker.date.past().toISOString(),
updated_at: faker.date.past().toISOString(),
kind: 'expenses',
...attrs
}
}
const createList = (n: number, attrs?: Partial<Item>): Item[] => {
return Array.from({ length: n }).map(() => create(attrs))
}
const createResponse = ({ count = 10, perPage = 10, page = 1 }, attrs?: Partial<Item>): Resources<Item> => {
// page页上一个页码时候的页面总条数
// count 页面总条数
const sendCount = (page - 1) * perPage
// left 当前page页面所剩下的条数
const left = count - sendCount
// 创建条数的时候取每页总条数和当前page页面所剩下的条数的最小值
return {
resources: left > 0 ? createList(Math.min(left, perPage), attrs) : [],
pager: {
page,
per_page: perPage,
count
}
}
}
export const itemsMock: MockMethod = {
url: '/api/v1/items',
method: 'get',
statusCode: 200,
response: ({ query }: ResponseParams): Resources<Item> => createResponse({ count: 30, perPage: 10, page: parseInt(query.page) })
}
修改itemsList.tsx
tsx
import useSWRInfinite from 'swr/infinite'
import { ajax } from '../../lib/ajax'
interface Props {}
const getKey = (pageIndex: number, prev: Resources<Item>) => {
if (prev) {
// 当前传入pageIndex页数的时的页面总条数
const sendCount = (prev.pager.page - 1) * prev.pager.per_page + prev.resources.length
// 数据总条数
const count = prev.pager.count
// sendCount 大于 数据总条数就不继续请求了
if (sendCount >= count)
return null
}
return `/api/v1/items?page=${pageIndex + 1}`
}
export const ItemsList: React.FC<Props> = () => {
const { data, error, size, setSize } = useSWRInfinite(
getKey,
async path => (await ajax.get<Resources<Item>>(path)).data
)
const onLoadMore = () => {
setSize(size + 1)
}
if (!data) {
return <span>'还没搞定'</span>
} else {
return <>
<ol>{
data.map(({ resources }) => {
return resources.map(item =>
<li key={item.id} grid grid-cols="[auto_1fr_auto]" grid-rows-2 px-16px py-8px gap-x-12px
border-b-1 b="#EEE">
<div row-start-1 col-start-1 row-end-3 col-end-2 text-24px w-48px h-48px
bg="#D8D8D8" rounded="50%" flex justify-center items-center>
😘
</div>
<div row-start-1 col-start-2 row-end-2 col-end-3>
旅行
</div>
<div row-start-2 col-start-2 row-end-3 col-end-4 text="#999999">
2011年1月1日
</div>
<div row-start-1 col-start-3 row-end-2 col-end-4 text="#53A867">
¥{item.amount / 100}
</div>
</li>
)
})
}</ol>
<div p-16px>
<button j-btn onClick={onLoadMore}>加载更多</button>
</div>
</>
}
}
没有更多数据时,不展示加载按钮
设置一个hasMore
根据请求到的总条数和列表总条数进行对比,如果比总条数少则显示加载按钮,否则显示没有更数据内容。
tsx
import useSWRInfinite from 'swr/infinite'
import { ajax } from '../../lib/ajax'
interface Props {}
const getKey = (pageIndex: number, prev: Resources<Item>) => {
if (prev) {
const sendCount = (prev.pager.page - 1) * prev.pager.per_page + prev.resources.length
const count = prev.pager.count
if (sendCount >= count)
return null
}
return `/api/v1/items?page=${pageIndex + 1}`
}
export const ItemsList: React.FC<Props> = () => {
const { data, error, size, setSize } = useSWRInfinite(
getKey,
async path => (await ajax.get<Resources<Item>>(path)).data
)
const onLoadMore = () => {
setSize(size + 1)
}
if (!data) {
return <span>还没搞定</span>
} else {
const last = data[data.length - 1]
const { pager: { page, per_page, count } } = last
const hasMore = (page - 1) * per_page + last.resources.length < count
return <>
<ol>{
data.map(({ resources }) => {
return resources.map(item =>
<li key={item.id} grid grid-cols="[auto_1fr_auto]" grid-rows-2 px-16px py-8px gap-x-12px
border-b-1 b="#EEE">
<div row-start-1 col-start-1 row-end-3 col-end-2 text-24px w-48px h-48px
bg="#D8D8D8" rounded="50%" flex justify-center items-center>
😘
</div>
<div row-start-1 col-start-2 row-end-2 col-end-3>
旅行
</div>
<div row-start-2 col-start-2 row-end-3 col-end-4 text="#999999">
2011年1月1日
</div>
<div row-start-1 col-start-3 row-end-2 col-end-4 text="#53A867">
¥{item.amount / 100}
</div>
</li>
)
})
}</ol>
{
hasMore
? <div p-16px><button j-btn onClick={onLoadMore}>加载更多</button></div>
: <div p-16px text-center>没有更多数据了</div>
}
</>
}
}
完成加载中和加载失败的展示逻辑
设置了isLoadingInitialData
、isLoadingMore
、isLoading
这三个值。
isLoadingInitialData
为是否加载初始化数据isLoadingMore
为是否加载更多isLoading
为isLoadingInitialData
和isLoadingMore
并集。
tsx
import styled from 'styled-components'
import useSWRInfinite from 'swr/infinite'
import { ajax } from '../../lib/ajax'
interface Props {}
const Div = styled.div`
padding: 16px;
text-align: center
`
const getKey = (pageIndex: number, prev: Resources<Item>) => {
if (prev) {
const sendCount = (prev.pager.page - 1) * prev.pager.per_page + prev.resources.length
const count = prev.pager.count
if (sendCount >= count)
return null
}
return `/api/v1/items?page=${pageIndex + 1}`
}
export const ItemsList: React.FC<Props> = () => {
const { data, error, size, setSize } = useSWRInfinite(
getKey,
async path => (await ajax.get<Resources<Item>>(path)).data
)
const onLoadMore = () => {
setSize(size + 1)
}
const isLoadingInitialData = !data && !error
const isLoadingMore = data?.[size - 1] === undefined && !error
const isLoading = isLoadingInitialData || isLoadingMore
if (!data) {
return <div>
{ error && <Div>数据加载失败,请刷新页面</Div> }
{ isLoading && <Div>数据加载中...</Div>}
</div>
} else {
const last = data[data.length - 1]
const { pager: { page, per_page, count } } = last
const hasMore = (page - 1) * per_page + last.resources.length < count
return <>
<ol>{
data.map(({ resources }) => {
return resources.map(item =>
<li key={item.id} grid grid-cols="[auto_1fr_auto]" grid-rows-2 px-16px py-8px gap-x-12px
border-b-1 b="#EEE">
<div row-start-1 col-start-1 row-end-3 col-end-2 text-24px w-48px h-48px
bg="#D8D8D8" rounded="50%" flex justify-center items-center>
😘
</div>
<div row-start-1 col-start-2 row-end-2 col-end-3>
旅行
</div>
<div row-start-2 col-start-2 row-end-3 col-end-4 text="#999999">
2011年1月1日
</div>
<div row-start-1 col-start-3 row-end-2 col-end-4 text="#53A867">
¥{item.amount / 100}
</div>
</li>
)
})
}</ol>
{ error && <Div>数据加载失败,请刷新页面</Div> }
{
!hasMore
? <Div>没有更多数据了</Div>
: isLoading
? <Div>数据加载中...</Div>
: <Div><button j-btn onClick={onLoadMore}>加载更多</button></Div>
}
</>
}
}
为什么SWR会请求那么多次
为什么请求那么多次,这是SWR故意为之的,详细原因见链接。
也可以关掉它,将选项中设置revalidateFirstPage: false
。
SWR多次请求适用于更新数据的,适用的场景比如微博 Twiter这种数据更新快的。
关闭vite的自动刷新,在vite.config.ts
内配置server
的选项为hmr: false
。