【山竹记账前端-react】14.无限加载列表


大纲链接 §

[toc]


1. 无限加载的实现思路 ⇧

  • 访问 /items 页面,请求 /items?page=1
  • 每次加载10项数据,返回 { resources: [], pager: {count: 10, per_page: 10, page: 1, }, }
  • 点击加载更多时,在原先数据后再插入10项数据 ?page=2
  • 页码乘以每页项目数,和总条目数对比,判断是否有更多数据加载
  • 使用 hasMore = (page - 1) * per_page + resourses.length < count 计算 是否还有更多数据
  • 报错和特殊情况情况
    • 加载第一页:resources 返回空数组,则显示暂无数据;
    • 请求第一页时,出错了,添加错误提示
    • 请求第二页时,出错了,添加错误提示…
  • 显示正在加载状态,改变“加载更多”的文案为“正在加载”;成功后变为“没有更多”或者“加载更多”

2. useSWRInfinite ⇧

@/modules/items/ItemsSummary.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
import type { Item, Resources } from "@/global"
import { request } from "@/lib/request/request"
import { accountItems } from "@/mock/items"
import { ItemsList } from "@/modules/items/ItemsList"
import { cn } from "cn"
import { useState } from "react"
import useSWRInfinite from 'swr/infinite'

interface Props {
  startDate: string
  endDate: string
}

export const ItemsSummary: React.FC<Props> = ({
  startDate,
  endDate
}) => {
  const getKey = (pageIndex: number = 0) => {
    return `/api/v1/items?page=${pageIndex + 1}`                    // SWR key
  }
  const { data, error, /* isLoading, isValidating, mutate, size, setSize */ } = useSWRInfinite(
    getKey,
    async (path) => (await request.get<Resources<Item[]>>(path)).data,
  )

  const [items,] = useState<Item[]>(accountItems)

  console.log('ItemsSummary_______________________')
  console.log('%c⧭', 'color: #733d00', data);
  console.log('%c%s', 'color: #e50000', error);
  console.log('_______________________ItemsSummary')

  return (
    <div className="flex flex-col h-[calc(100vh-112px)] scrollbar-hide relative">
      {startDate}-{endDate}

      <ul className={cn(
        'bg-[var(--total-bg)] text-[var(--total-text)]',
        'grow-0 shrink-0 flex justify-between items-center',
        'm-1em py-0 px-1em rounded-[var(--button-radius)]',
        'children:(p-[1em] flex flex-col text-center)',
        )}>{/* ... */}</ul>

      <ItemsList items={items}/>
    </div>
  )
}
  • 使用 useSWRInfinite 的数据模型,每次获取一页数据,向 data 中添加一份当前页的数据
    • data = [{resources: [], pager: {}}, {resources: [], pager: {}}, {resources, pager}, ...]
    • data 会保留第一页以及后几页请求过的所有数据
    • 只需要遍历这个data就可以把每一页的数据都合并展示出来
  • pageIndex 说明下标是从 0 开始的;否则应该是 page
  • 仍使用 /api/v1/items 模拟数据接口
  • fetcher 函数中参数路径 path 时动态获取的,根据每次页码改而不同变

原先的 @/mock/mock.ts 数据暂时为空,之后对它做一些高级的改造

 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
import type { Item, Resource, Resources, ResponseData, User } from '@/global.d.ts'
import type { MockMethod } from 'vite-plugin-mock'

export default [
  // ...
  {
    url: '/api/v1/items',
    method: 'get',
    timeout: 888,
    response: (): ResponseData<Partial<Resources<Item>>> => {
      return {
        code: 0,
        data: {
          resources: [
            /*
            {
              id: 1,
              user_id: 1,
              amount: 100,
              kind: 'incomes',
              tag_ids: [],
              happened_at: '2027-10-07T00:00:00.000Z',
              created_at: '2027-10-07T00:00:00.000Z',
              updated_at: '2027-10-07T00:00:00.000Z',
            },
             */
          ],
          pager: {
            page: 1,
            per_page: 25,
            count: 0,
          },
        },
      }
    },
  },
] as MockMethod[]

3. 优化Mock,改造Mock数据 ⇧

实现 Mock 数据 模块化、自动生成假数据

重构 @/mock/mock.ts 模块化 ⇧

拆分为 @/mock/me.mock.ts 和 @/mock/items.mock.ts 将其他相关数据剪切过去

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import { Resource, ResponseData, User } from "@/global"
import { MockMethod } from "vite-plugin-mock"

export const MeMock: MockMethod[] = [
  {
    url: '/api/v1/me',
    method: 'get',
    timeout: 888,
    response: () => {
      return {
        code: 0,
        data: {
          resource: {
            id: 1,
            name: 'frank',
            email: 'frank@frank.com',
            updated_at: '2027-01-01T00:00:00.000Z',
            created_at: '2027-01-01T00:00:00.000Z',
          },
        },
      } satisfies ResponseData<Partial<Resource<User>>>
    },
  },
]
 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
import { Item, Resources, ResponseData } from "@/global"
import { MockMethod } from "vite-plugin-mock"

export const itemsMock: MockMethod[] = [
  {
    url: '/api/v1/items',
    method: 'get',
    timeout: 888,
    response: (): ResponseData<Partial<Resources<Item>>> => {
      return {
        code: 0,
        data: {
          resources: [
            /*
            {
              id: 1,
              user_id: 1,
              amount: 100,
              kind: 'incomes',
              tag_ids: [],
              happened_at: '2027-10-07T00:00:00.000Z',
              created_at: '2027-10-07T00:00:00.000Z',
              updated_at: '2027-10-07T00:00:00.000Z',
            },
             */
          ],
          pager: {
            page: 1,
            per_page: 25,
            count: 0,
          },
        },
      }
    },
  },
]

@/mock/mock.ts

1
2
3
4
5
6
7
8
import { itemsMock } from '@/mock/items.mock'
import { MeMock } from '@/mock/me.mock'
import type { MockMethod } from 'vite-plugin-mock'

export default [
  ...MeMock,
  ...itemsMock,
] satisfies MockMethod[]

此时遇到 vite-plugin-mock 插件bug,改用 vite-plugin-mock-dev-server

改用 vite-plugin-mock-dev-server ⇧

vite-plugin-mock 插件停止维护了,需要改用积极维护的且功能更多的 vite-plugin-mock-dev-server

  • 安装 pnpm add -D vite-plugin-mock-dev-server
  • 卸载 pnpm remove vite-plugin-mock

     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
    
    import { fileURLToPath, URL } from 'node:url'
    import { defineConfig, UserConfigExport, ConfigEnv } from 'vite'
    import react from '@vitejs/plugin-react'
    import UnoCSS from 'unocss/vite'
    import jsxScoped from '@10coding/vite-plugin-jsx-scoped'
    import { consoleBanner } from "vite-plugin-console-banner"
    import { svgsprites } from './src/plugins/svgsprites.ts'
    import { noOptimizeIconList } from './src/plugins/noOptimizeIconList.ts'
    import { mockDevServerPlugin } from 'vite-plugin-mock-dev-server'
    
    export default defineConfig(({ command }: ConfigEnv) => ({
    define: {
    __IS_DEV__: command === 'serve',
    },
    plugins: [
    UnoCSS(),
    jsxScoped({ scopedIdAttributeName: 'scopedid' }),
    mockDevServerPlugin({
      dir: './src/mock',
      enabled: command === 'serve',
      reload: true,
      awaitInitialLoad: command === 'serve',
      exclude: [
        '**/*.d.{js,ts,cjs,mjs,json,json5}',
      ],
    }),
    react(),
    consoleBanner({
      text: "xmasuhai.xyz",
      font: "big", // 大小写不敏感,也可用 "Big"、"ANSI Shadow"
      color: "#476EE3",
      badges: [
        { label: "徐帅的博客", url: "https://xmasuhai.xyz" },
        { label: "徐帅${year}简历", url: "https://xmasuhai.xyz/my-latest-resume/" },
      ],
    }),
    svgsprites({
      noOptimizeList: noOptimizeIconList,
    }),
    ],
    server: {
    host: true,
    proxy: {
      '^/api': { target: '//192.168.0.104', },
    },
    },
    resolve: {
    // 开启 Vite 8 的原生 tsconfig 路径解析支持
    tsconfigPaths: true,
    alias: {
      '@': fileURLToPath(new URL('./src', import.meta.url)),
    }
    }
    })) satisfies UserConfigExport
    
  • 原来的 mock.ts 文件可以整个删除

  • 现在可以使用天然分模块的 @/mock/me.mock.ts 和 @/mock/items.mock.ts 模拟数据文件

@/mock/me.mock.ts

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import { Resource, ResponseData, User } from "@/global"
import { defineMock } from "vite-plugin-mock-dev-server"

export default defineMock({
  url: '/api/v1/me',
  method: 'get',
  status: 200,
  delay: 1800,
  body(): ResponseData<Resource<User>> {
    return {
      code: 0,
      data: {
        resource: {
          id: 1,
          name: 'frank',
          email: 'frank@frank.com',
          updated_at: '2027-01-01T00:00:00.000Z',
          created_at: '2027-01-01T00:00:00.000Z',
        },
      },
    }
  },
})

@/mock/items.mock.ts

 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
import { Item, Resources, ResponseData } from "@/global"
import type { ResponseParams } from "@/mock/mockType"
import { defineMock } from 'vite-plugin-mock-dev-server'

let id = 0
const createId = () => {
  id += 1
  return id
}

const createMockItem = (attrs?: Partial<Item>): Item => ({
  id: createId(),
  user_id: 1,
  amount: 100,
  kind: 'incomes',
  tag_ids: [],
  happened_at: '2027-10-07T00:00:00.000Z',
  created_at: '2027-10-07T00:00:00.000Z',
  updated_at: '2027-10-07T00:00:00.000Z',
  ...attrs,
})

const createMockItemList = (n: number, attrs?: Partial<Item>): Item[] =>
  Array.from({ length: n })
    .map(() => createMockItem(attrs))

export const createItemsMockResponse = (
  {
    perPage = 10,
    page = 1,
    count = 10,
  },
  attrs?: Partial<Item>,
) => {
  return {
    code: 0,
    data: {
      resources: createMockItemList(perPage, attrs),
      pager: {
        page,
        per_page: perPage,
        count,
      },
    },
  }
}

export default defineMock({
  url: '/api/v1/items',
  method: 'get',
  body({ query }: ResponseParams): ResponseData<Partial<Resources<Item>>> {
    return createItemsMockResponse({
      count: 100,
      perPage: 10,
      page: parseInt(query.page) ?? 1,
    })
  },
})
  • 使用方式不变

    1
    2
    3
    4
    5
    6
    
    const getKey = (pageIndex: number = 0) => `/api/v1/items?page=${pageIndex + 1}`
    
    const { data, error, /* isLoading, isValidating, mutate, size, setSize */ } = useSWRInfinite(
    getKey,
    async (path) => (await request.get<Resources<Item[]>>(path)).data,
    )
    

4. 请求所有分页 ⇧

``

1


5. 没有更多数据 ⇧

``

1


6. 展示error ⇧

``

1


7. 展示loading ⇧

``

1


8. 为什么SWR有多余请求 ⇧

``

1


·未完待续·

参考文章

相关文章


  • 作者: Joel
  • 文章链接:
  • 版权声明
  • 非自由转载-非商用-非衍生-保持署名