【山竹记账前端-react】8.数据请求与缓存失效之 SWR


大纲链接 §

[toc]


0.实现首页 ⇧

实现首页,需要分以下几种情况

  • 如果没有登录:可选显示首页,重定向到登录
  • 如果登录但没有记账:展示首页
  • 如果登录且记过账:记账列表页面

如何知道用户是否登录

  • 用 JS 读取 cookie/jwt ❌️
  • 用 JS 发请求问服务器用户是否登录 ✅️

首页发送验证登录请求,展示分三种情况

  • 例如 axios.get('/api/currentUser')
    • 加载中 return <div>loading</div>
    • 加载出错 return <div>error</div>
    • 加载成功内容 return <div>Home</div>

可自行封装通用请求逻辑,示例伪代码

  • const {loading, error, data} = useAjax('get', '/api/v1/curentUser')

使用成熟的请求库 swr.js

1. swr原理介绍 ⇧

用于数据请求的 React Hooks 库

  • stale-while-revalidate 在重新验证期间过期
    • 一种由HTTP RFC5861推广的HTTP缓存失效策略
    • 这种策略首先从缓存中返回数据(过期的),同时发送fetch请求(重新验证),最后得到最新数据。
    • 使用SWR,组件将会不断地、自动获得最新数据流。 UI 也会一直保持快速响应。

swr

缓存失效问题:

  • 用户页面一直开着,正好超过了缓存有效时间,导致缓存失效不更新
  • 用户的另一台设备更新了数据,原来设备的缓存即失效
  • 前端数据只是后端服务器的数据库的缓存
    • 前端数据某一时刻一定会失效
    • 前端状态管理的本质:缓存管理,即存在缓存失效的问题

缓存失效问题

需要正确稍慢的结果

  • 而不是过期有可能错误但稍快的结果

自动更新的时机

  • 与用户交互部分的数据才会去更新
    • 用户请求路由跳转
    • 用户请求分页

数据获取流派

swr-折衷策略

简单示例

1
2
3
const {data, error} = useSwr('me', (/* key, */) => {
  return axios.get('/api/me')
})
  • me 是数据唯一标识符,通常是 API URL
  • 回调是返回数据的异步函数,可以使用 Axios 或原生 fetch
  • 负责当 me 过期时重新发送请求
  • 返回:data 和 error

支持重复请求去除

  • 浏览器默认去重:请求路径一直,浏览器取消上一次请求
  • swr 支持一定时间内,合并重复请求
  • 使用 Axios 作为 fetcher

2. swr基本使用 ⇧

安装

1
pnpm add axios@1.20.0 swr@2.5.1

在 @/pages/Home.tsx 中使用

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
import axios from 'axios'
import useSWR from 'swr'

function fetcher(urlPath: string) {
  return axios.get<string>(urlPath)
}

export const Home: React.FC = () => {
  const { data, error } = useSWR(`/`, fetcher)
  if (error) { return <div>Error, failed to load</div> }
  if (!data) { return <div>Loading...</div> }

  window.console.log('useSWR_______________________')
  window.console.log('error', error)
  window.console.log('data?.data', data?.data)
  window.console.log('_______________________useSWR')

  return (
    <div text-6xl>{data?.data}</div>
  )
}

使用 https://apifox.com/apihub/ 中的 https://echo.apifox.cn/ HTTP 类接口,获取免费的测试接口数据

  • 测试错误 https://echo.apifox.com/status/404

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    
    import axios from 'axios'
    import useSWR from 'swr'
    
    function fetcher(urlPath: string) {
    return axios.get<string>(urlPath)
    }
    
    export const Home: React.FC = () => {
    const { data, error } = useSWR(`https://echo.apifox.com/status/404`, fetcher)
    if (error) { return <div>Error, failed to load</div> }
    if (!data) { return <div>Loading...</div> }
    
    window.console.log('useSWR_______________________')
    window.console.log('error', error)
    window.console.log('data?.data', data?.data)
    window.console.log('_______________________useSWR')
    
    return (
    <div text-6xl>{data?.data}</div>
    )
    }
    
  • 测试成功 https://echo.apifox.com/get?name=frank&age=18

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    
    import axios from 'axios'
    import useSWR from 'swr'
    
    function fetcher(urlPath: string) {
    return axios.get<{ args: { name: string, age: number } }>(urlPath)
    }
    
    export const Home: React.FC = () => {
    const { data, error } = useSWR(`https://echo.apifox.com/get?name=frank&age=18`, fetcher)
    if (error) { return <div text-6xl>Error, failed to load</div> }
    if (!data) { return <div text-6xl>Loading...</div> }
    
    window.console.log('useSWR_______________________')
    window.console.log('error', error)
    window.console.log('data?.data', data?.data)
    window.console.log('_______________________useSWR')
    
    return (
    <div text-6xl>{data?.data.args.name + data?.data.args.age}</div>
    )
    }
    
  • 调慢网速 slow 4G 可以看到 loading 状态;可以删除整个元素,然后刷新页面查看

  • 响应数据类型为 { args: { name: string, age: number } },在 axios.get<> 定义


swr@1.x+ 不提供 isLoading 的原因 ⇧

  • Best practice for isLoading. What different with isValidating
  • swr 认为只有第一次加载数据中才是 loading,之后的请求都只是验证刷新数据,即 isValidating
  • 即 loading 只发生一次,当没有数据且没有错误的时候,loading = !error && !data
  • isValidating 发生一次以上,当已经有了数据或者错误时, swr 会重新调用 fetcher 去更新

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    
    import axios from 'axios'
    import useSWR from 'swr'
    
    function fetcher(urlPath: string) {
    return axios.get<{ args: { name: string, age: number } }>(urlPath)
    }
    
    export const Home: React.FC = () => {
    const { data, error, isValidating } = useSWR(`https://echo.apifox.com/get?name=frank&age=18`, fetcher)
    if (error) { return <div text-6xl>Error, failed to load</div> }
    if (!data) { return <div text-6xl>Loading...</div> }
    if (isValidating) { return <div>正在获取最新数据...</div> }
    
    return (
    <div text-6xl>{data?.data.args.name + data?.data.args.age}</div>
    )
    }
    
  • 初学者学习的方法:将问题使用英文描述,发到github的Discussions

点击useSWR跳转查看对应的类型声明 SWRResponse

  • 查看ts类型 IsLoadingResponse

swr@2.x + 在 SWR 2.0 的更新中,官方引入了一个全新的返回值 isLoading,用以解决之前开发者在区分“初次加载”和“后台重新验证”时遇到的痛点

  • 用于第一次展示骨架屏
  • SWR 的核心机制是先渲染本地缓存(Stale),同时在后台发起验证(Revalidate)
  • 单纯的 isLoading(传统库):关注的是“网络管道”。只要网络连接在建立、数据在传输,它就是 true。它是面向计算机和网络状态的。
  • SWR 的 isLoading:关注的是“用户感知”。它回答的是“用户眼前是不是一片空白?”。它是面向信息密度和用户心理的。

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    
    import axios from 'axios'
    import useSWR from 'swr'
    
    function fetcher(urlPath: string) {
    return axios.get<{ args: { name: string, age: number } }>(urlPath)
    }
    
    export const Home: React.FC = () => {
    const { data, error, isValidating, isLoading } = useSWR(`https://echo.apifox.com/get?name=frank&age=18`, fetcher)
    if (isLoading) { return <div text-6xl>骨架屏加载中...</div> }
    if (error) { return <div text-6xl>Error, failed to load</div> }
    if (!data) { return <div text-6xl>Loading...</div> }
    if (isValidating) { return <div text-6xl>正在获取最新数据...</div> }
    return (
    <div text-6xl>{data?.data.args.name + data?.data.args.age}</div>
    )
    }
    

参考


POST 不能缓存,swr 不支持 POST ⇧

  • 使用 mutate 来更新数据变更
  • const { data, error, isValidating, isLoading, mutate } = useSWR(https://echo.apifox.com/get?name=frank&age=18`, fetcher)`
  • 单独另外发送变更数据的 POST 请求,然后用 mutate 来更新变更的数据

     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
    
    import axios from 'axios'
    import useSWR from 'swr'
    
    function fetcher(urlPath: string) {
    return axios.get<{ args: { name: string, age: number } }>(urlPath)
    }
    
    export const Home: React.FC = () => {
    const { data, error, isValidating, isLoading, mutate } = useSWR(`https://echo.apifox.com/get?name=frank&age=18`, fetcher)
    if (isLoading) { return <div text-6xl>骨架屏加载中...</div> }
    if (error) { return <div text-6xl>Error, failed to load</div> }
    if (!data) { return <div text-6xl>Loading...</div> }
    if (isValidating) { return <div text-6xl>正在获取最新数据...</div> }
    
    const onClick = async () => {
    // 示例请求伪代码
    const data = await axios.post('xxx')
    mutate(data)
    }
    
    return (
    <div text-6xl>
      <div>{data?.data.args.name + data?.data.args.age}</div>
      <button onClick={onClick}>更新</button>
    </div>
    )
    }
    
  • 用另外的方法修改了服务器的数据,swr 需要通过 mutate 来告知数据已更新,再去更新缓存

参考


3. 制作首页 ⇧

基本结构 @/pages/Home.tsx

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
import pig from '@/assets/icons/pig.svg'

export const Home: React.FC = () => {
  return (
    <main>
      <img src={pig} alt="pig" w-128px h-130px />
      <button>开始记账</button>
      <button>+</button>
    </main>
  )
}

填充样式 @/pages/Home.tsx

  • 按钮:Add,引入图标 add.svg
    • 限制宽高 max-w="80%" max-h="80%"
    • 微调 translate-y="-3px"
  • 按钮:开始记账

     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
    
    import pig from '@/assets/icons/pig.svg'
    import add from '@/assets/icons/add.svg'
    import { cn } from 'cn'
    
    export const Home: React.FC = () => {
    return (
    <main flex flex-col justify-center items-center>
      <div flex justify-center items-center>
        <img src={pig} alt="pig" w-128px h-130px mt-20vh mb-20vh block />
      </div>
    
      <div px-16px self-stretch>
        <button
          w="100%"
          className={cn(
            'h-48px bg-[var(--button-primary-color)] b-none rounded-8px',
            'text-[var(--button-primary-text-color)]',
          )}>
          开始记账
        </button>
      </div>
    
      <button
        className={cn(
          'w-56px h-56px',
          'bg-[var(--button-primary-color)] rounded-full',
          'text-[var(--button-primary-text-color)] text-6xl',
          'p-4px text-center flex place-content-center place-items-center-safe',
          'fixed bottom-.5em right-.5em',
        )}>
        <img src={add} alt="add" max-w="80%" max-h="80%" translate-y="-3px" />
      </button>
    </main>
    )
    }
    

插播一个老油条的技巧:适配是设计师的事情,不是程序员的事情

  • 不做适配,除非设计师出一版适配的设计稿
  • 没有设计稿,前端就先不要写代码,使用简陋的画图工具画一版丑丑的设计稿,直到项目经理/老板点头开始做再开始写代码
  • 界面样式刷锅设计师,适配用什么方案好,去找设计师
  • 只要设计出几个版本,一定给你实现

·未完待续·

参考文章

相关文章


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