Skip to content

无限加载列表

改造 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.tsmock.ts文件,修改test.ts文件,改为导入meMockitemsMock方法。

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>
      }
    </>
  }
}

完成加载中和加载失败的展示逻辑

设置了isLoadingInitialDataisLoadingMoreisLoading这三个值。

  • isLoadingInitialData为是否加载初始化数据
  • isLoadingMore为是否加载更多
  • isLoadingisLoadingInitialDataisLoadingMore并集。
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