一、背景

Vue 3.0已经出到3.0.5了,React Hooks大家也用的如日中天。整天大家都在讨论Hooks,Hooks,那么Hooks式的编程到底有什么好处?

参加本人的这篇文章。

二、使用

项目 React Vue
包名 swr vue-swr
仓库地址 github-swr github-vue-swr

二、useSWR

随着React Hooks的浪潮,各种基于Hooks 的方案越来越多,其中主要包含 状态管理、数据请求、通用功能的封装 等等。而 数据请求 是日常业务开发中最常见的需求,那么在 Hooks 模式下,我们应该如何请求数据,先来看下面的一个简单示例。

需求场景

产品需求:首页通过接口获取 github trending 项目列表,然后点击列表项可查看单个项目的信息。

程序实现:接到需求后一顿操作,无非就是在数据请求时需要显示 loading效果,数据获取完成时展示列表数据,以及考虑请求错误后的容错处理,稳健如飞的撸出了如下代码:

// 首页列表实现
const Home = () => {// 设置初始数据const [data, setData] = useState([]) // 设置初始状态const [isLoading, setIsLoading] = useState(false)// 设置初始错误值const [isError, setIsError] = useState(false)useEffect(() => {// 定义 fetchFn const fetchData = async () => {setIsLoading(true)try {const result = await fetch('api/data')setData(result)} catch(error) {setIsError(true)}setIsLoading(false)}// 调用接口fetchData()}, [])return (<div className='hero'><h1 className='title'>Trending Projects</h1>{isError && <div>Something went wrong ...</div>}<div>{isLoading ? 'loading...' :data.map(project =><p key={project}><Link href='/[user]/[repo]' as={`/${project}`}><a>{project}</a></Link></p>)}</div></div>)
}

获取项目详情实现与上面基本一样,基础代码如下:

// 项目详情实现
const project = () => {const [data, setData] = useState([]) const [isLoading, setIsLoading] = useState(false)const [isError, setIsError] = useState(false)useEffect(() => {const fetchData = async () => {setIsLoading(true)try {// 获取的 API 带了 id 参数const result = await fetch(`api/data?id=${id}`)setData(result)} catch(error) {setIsError(true)}setIsLoading(false)}fetchData()}, [id])return ()
}

如上面的例子所示,代码看上去很简洁,一个纯函数包含了数据请求时的请求状态、容错处理、数据更新,视图渲染,以及使用了React 的 useEffectuseState 两个 Hooks API,很好的满足了场景需求。

这看上去很好,但你可能存在一些疑惑,从示例代码可以看到获取项目列表和项目详情的 数据请求部分的代码 基本上是一样的,同样的代码重复写两遍,这显然是不能接受的,基于此通常的做法是对其进行一层抽象封装,实现逻辑的复用,具体如下。

简易模型

基于重复的数据请求代码,对比发现只是 API 和初始数据值的不同,其他如设置dataisLoadingisError 的逻辑都是一样,可以先将其进行一层抽象封装以便进行复用,简易模型如下:

import { useState, useEffect } from 'react'
import fetch from 'isomorphic-unfetch'/*** 对 fetch 进行封并返回 isLoading、isError、data 三个值* @param {*} url 请求的 API 地址* @param {*} initialData 初始化数据*/
function useFetch(url, initialData) {const [data, setData] = useState(initialData)const [isLoading, setIsLoading] = useState(false);const [isError, setIsError] = useState(false)useEffect(() => {const fetchData = async () => {setIsError(false)setIsLoading(true)try {const result = await fetch(url)const newData = await result.json()setData(newData)} catch(error) {setIsError(false)}setIsLoading(false)}fetchData()}, [])return [data, isLoading, isError]
}export default useFetch;

然后修改我们的业务代码如下,这时视图层只需要一行代码即可完成数据的请求,并返回了dataisLoadingisError三个值,渲染处理逻辑完全一致。

// 首页列表实现
const Home = () => {const [data, isLoading, isError] = useFetch('api/data', []);return (// render jsx)
}// 项目详情实现
const project = () => {const [data, isLoading, isError] = useFetch(`api/data?id=${id}`, []);return (// render jsx)
}

至此我们的 useFetch API形式如下,接收urlinitialData 作为参数,返回 dataisLoadingisError三个值。

 const [data, isLoading, isError] = useFetch('api/data', []);

功能迭代

上面的代码看起来应该是不错了,通过 useFetch的封装,在具体的视图中只需要调用 useFetch 传入对应的 API 地址和初始数据,即可正常工作,然而实际的业务场景并不都是如此,接下来将逐步对它进行功能迭代,满足常见的业务开发需求。

自定义请求

上面实现的useFetch是将 fetch 的实现逻辑进行了内置,且默认使用了 isomorphic-unfetch 这个库,在实际业务中,你可能习惯了使用 axios,也可能需要对fetch 的逻辑进行定制,那么现有的 useFetch显然就不能满足要求,这时我们可以考虑将fetch逻辑通过参数的形式进行传入,外层可以自定义获取数据的行为,如果不传递则默认为 undefined

import { useState, useEffect } from 'react'// 支持传入 fetcher 用于自定义请求
function useFetch(url, fetcher) {const [data, setData] = useState(undefined)const [isLoading, setIsLoading] = useState(false);const [isError, setIsError] = useState(false)useEffect(() => {const fetchData = async () => {setIsError(false)setIsLoading(true)try {// 这里直接调用外部传进来的 fetcher,并使用 url 作为参数const newData = await fetcher(url);setData(newData)} catch(error) {setIsError(false)}setIsLoading(false)}fetchData()}, [url])return [data, isLoading, isError]
}export default useFetch;

这时在组件层获取数据的方式,可自定义请求函数如下:

import fetch from 'isomorphic-unfetch'const customFetch = async (...args) => {const res = await fetch(...args)return await res.json()
}const Home = () => {// useFetch 的第二个参数可以使用自定义的 customFetchconst [data, isLoading, isError] = useFetch('api/data', customFetch);return ()
}

可以看到,useFetch 现在可以接收一个函数用于获取数据,且该函数的唯一参数为 useFetch的第一个参数 url,这意味着可以使用你喜欢的任何请求库来获取数据。

  const [data, isLoading, isError] = useFetch('api/data', customFetch);

全局配置项

我们已经可以通过自定义fetcher获取数据,但每个调用处都需要重复的去传递 fetcher,因此可以考虑将其统一配置,在调用时可以直接使用该默认配置,也可以自定义配置来覆盖,为此需要一个全局配置的方式。

在 React 中全局配置数据共享最简单的就是通过 Context方式,这里我们选择使用 useContext来实现 useFetch的全局配置功能。

import { createContext } from 'react'const useFetchConfigContext = createContext({})
useFetchConfigContext.displayName = 'useFetchConfigContext'export default useFetchConfigContext;

useFetch 改造如下:

import { useState, useEffect } from 'react'function useFetch(url, fetcher, options = {}) {const [data, setData] = useState(undefined)const [isLoading, setIsLoading] = useState(false);const [isError, setIsError] = useState(false)// 通过 useContext 获取 useFetchConfigContext 的全局配置const config = Object.assign({},useContext(useFetchConfigContext),options)let fn = fetcherif(typeof fn === 'undefined') {// 使用全局配置的 fetcherfn = config.fetcher}useEffect(() => {// ...}, [url])return [data, isLoading, isError]
}// 导出 useFetchConfig
const useFetchConfig = useFetchConfigContext.Provider;
export { useFetchConfig };export default useFetch;

现在组件层获取数据的方式如下,在全局统一配置fetcher,然后在调用 useFetch的组件中只需要传入对应的 url 即可:

  • 全局配置
import React from 'react'
import App from 'next/app'
import { useFetchConfig as UseFetchConfig } from '../libs/useFetch'
import fetch from '../libs/fetch';class MyApp extends App {render() {const { Component, pageProps } = this.propsreturn (/* 通过 useFetchConfig 配置全局的 useFetch 参数 */<UseFetchConfig value={{ fetcher: fetch }}><Component {...pageProps} /></UseFetchConfig>)}
}
  • 组件调用
const Home = () => {const [data, isLoading, isError] = useFetch('api/data');return (// jsx code)
}

至此,我们提供了一个全局配置来代替每个调用 useFetch 时的重复逻辑。

依赖请求

除了自定义请求和全局配置,实际业务中另外一类常见需求就是请求之间的依赖,如 B 依赖 A 的返回结果作为请求参数,通常的写法如下:

const { data: a } = await fetch('/api/a')
const { data: b } = await fetch(`/api/b?id=${a.id}`)

那么在 useFetch的模式下该如何处理这类需求,当 /api/a 接口未正常返回结果时 a 的值为 undefined ,在/api/b 接口中调用 a.id 就会直接抛出异常,导致页面渲染失败。

那这是否意味我们可以假设当调用接口时 url 这个参数抛出异常,也就意味着它的依赖还没有准备就绪,暂停这个数据的请求;等到依赖项准备就绪时,然后对就绪的数据发起新的一轮请求,以此来解决依赖请求的问题。

而依赖项准备就绪的时机也就是在任一请求完成时,如上面的 /api/a请求完成时 useFetch会通过 setState 触发重新渲染,同时/api/b?id=${a.id} 得到更新,只需要将该 url 作为useEffect 的依赖项即可自动监听并触发新一轮的请求。其示意图如下:

通过上面的分析, useFetch处理依赖请求的逻辑主要分为以下三步:

  • 1、约定参数 url可以是一个函数并且该函数返回一个字符串作为请求的唯一标识符;
  • 2、当调用该函数抛出异常时就意味着它的依赖还没有就绪,将暂停这个请求;
  • 3、在依赖的请求完成时,通过setState 触发重新渲染,此时url会被更新,同时通过useEffect 监听 url 是否有改变触发新一轮的请求。
const Home = () => {// A 和 B 两个并行请求,且 B 依赖 A 请求const { data: a } = useFetch('/api/a')const { data: b } = useFetch(() => `/api/b?id=${a.id}`)return ()
}const useFetch = (url, fetcher, options) => {const getKeyArgs = _key => {let keyif (typeof _key === 'function') {// 核心所在:// 当 url 抛出异常时意味着它的依赖还没有就绪则暂停请求// 也就是将 key 设置为空字符串try {key = _key()} catch (err) {key = ''}} else {// convert null to ''key = String(_key || '')}return key}useEffect(() => {const [data, setData] = useState(undefined)const key = getKeyArgs(url)const fetchData = async () => {try {const newData = await fn(key);setData(newData)} catch(error) {// }},fetchData()// 核心所在// 当 A 请求完成时通过 setData 触发 UI 重新渲染// 继而当 url 更新时触发 B 的新一轮请求}, [key])return {}
}

如 SWR 官方文档所描述,允许获取依赖于其他请求的数据,且可以确保最大程度的并行(avoiding waterfalls),其原理主要是通过约定 key 为一个函数进行 try {} 处理 ,并巧妙的结合 React 的 UI = f(data) 模型来触发请求,以此确保最大程度的并行。

SWR also allows you to fetch data that depends on other data. It ensures the maximum possible parallelism (avoiding waterfalls), as well as serial fetching when a piece of dynamic data is required for the next data fetch to happen.

二、总结

SWR还有很多功能,缓存特性这里都没说,想了解的可以参考这里。还好这都已经封装好了,不管是vue3.0还是react都有库可以使用。这也是Hooks的实际应用。

参考:
https://zhuanlan.zhihu.com/p/133819602
https://zhuanlan.zhihu.com/p/93824106?from_voters_page=true

Hooks编程扫盲(一)-- useSWR相关推荐

  1. lua 去除小数点有效数字后面的0_【物联网学习番外篇】Lua脚本编程扫盲

    在后面的物联网教程中,我们很快就会接触到Lua这个脚本语言,那么本篇内容结合后续的一些需要的开发点,针对Lua零基础的读者写一篇入门扫盲文章. 01 Lua 介 绍 Lua是一个小巧的脚本语言,其目标 ...

  2. 函数式编程扫盲 - 转载系列1

    抽象 豪不夸张地说,这是一本影响了好几代程序员的书.自从上世纪80年代MIT开始使用这本书作为教材开始,它使用Lisp语言--直到前两年才被Python取代,但是使用哪本教材不得而知,由这个侧面也可见 ...

  3. 其它React Hooks以及自定义Hooks和第三方Hooks库的使用

    - useMemo 作用:相当于vue中的计算属性.用于组件中复杂运算的优化 特点:当它所关联的声明式变量发生变化,它才会重新运算:反之,不会 语法:const result = useMemo(fn ...

  4. python全套教程-老王Python全套教程完整版

    课程目录 PYthon基础篇 基础篇1-福利课Python先入为主 基础篇2-福利课Python先入为主下 基础篇3-虚拟机安装xubuntu开发环境 基础篇4-linux基本命令以及开发环境 基础篇 ...

  5. 深圳靠谱的python培训机构排名

    各位AI er们,大家对每周至少两场的Talk视频,以及不定期.高密度的TechBeat线上活动一定不陌生啦!以推动大家共同学习的名义,海纳天下AI勇士,才有了今天社区里热火朝天的学习景象. 社区内容 ...

  6. 【物联网平台篇9】使用MQTT上传图片到OneNET

    最近这几天在准备后续的物联网STM32开发和实战演示.编写小程序等相关的内容,没时间更新公众号的文章. 其实关于物联网平台使用的系列文章,我本来打算写到上一篇就差不多可以停了.可是我在B站上一个粉丝私 ...

  7. react具名插槽与作用域插槽

    // 插槽的使用,首先插槽是在 组件标签内部组合的 JSX 内容,进行传递 // 因为 JSX 本身就是一个 类 js 对象, 插槽 直接使用 props.children 拿到了 JSX 直接渲染到 ...

  8. 【粉丝福利,限时免费】【千里之行,始于脚下】我在CSDN上的精品博文汇总,收藏起来慢慢看

    文章目录 1 写在前面 2 精品博文汇总 3 更多分享 1 写在前面 就在上周,我在CSDN的原创博文数量正式突破 100+,顺利拿下 CSDN博客专家的称号,前前后后耗时有4-5年吧,不过中间有两年 ...

  9. 鱼c工作室python课件_我见过最全的python教程鱼C工作室

    绝对属实 [1]开发基础带安装包 xa0 xa0xa0 xa0xa0xa00000安装包 xa0 xa0xa0 xa0xa0xa0000愉快的开始 xa0 xa0xa0 xa0xa0xa0001我和P ...

最新文章

  1. 全部开课!加入学习群一起进步(附点云、多传感器融合、SLAM、三维重建课程)...
  2. 继续昨日计划: 2022-2-16
  3. 分布式一致性协议paxos
  4. 【音频处理】Adobe Audition 快捷键设置
  5. python 字典取值加引号创建一个对象_Python在添加到字典时从列表项中删除单引号...
  6. “城迷”:黑白梦与精神逃离
  7. java 字符串子串_java实现字符串匹配求两个字符串的最大公共子串
  8. Maven -- 在进行war打包时用正式环境的配置覆盖开发环境的配置
  9. c语言中二维数组的结构体,怎么才能把结构体里面的二维数组打印出来?
  10. dubbo-admin管理平台搭建
  11. 【量产】波士顿动力机器狗,当警犬不错,上战场。。。
  12. 带宽叠加是什么意思?
  13. 视频教程-10分钟搞定 php+H5手机网页微信支付 在线视频教程(含源代码)-微信开发
  14. WINDOWS下输入法中英文切换
  15. JS 超大文件上传解决方案:分片断点上传(一)
  16. wannier拟合能带总是拟合不上_中科大PRL:面内磁化的本征量子反常霍尔效应:搜索规则和材料预测...
  17. java localdate_Java LocalDate now()用法及代码示例
  18. toi,atol,strtod,strtol,strtoul实现类型转换
  19. burpsuit无法抓包
  20. JAVA——反转的两种方法

热门文章

  1. 赠何艳红(帮别人名字作诗)
  2. linux - 守护进程的方式
  3. 特斯拉自动驾驶中的 OccupancyNetworks NeRFs
  4. 软件外包开发测试工具
  5. 数据仓库常见的概念术语解析
  6. i5 10500和i7 9700k哪个好
  7. win7 硬盘安装 ubuntu14.04 桌面版
  8. Sepolicy学习(一)
  9. OpenAI 总裁:GPT-4 有不完美,高阶版本正在测试!
  10. cocos lua ccui.TextField 本文输入框 editbox手机无法正常输入