自从 9102 年初 react 推出了 Hook 以后,我就开始在私人项目中先行了。不得不说的是,react Hook 的确足够“跨时代”。大量的文章研读以及伴随着项目中组件的改造,对Hook 的优势,缺点,以及自己的机制也有必定的了解。前端
若是你是 Hook 初学者,建议先阅读 https://usehooks.com/ 以及 Dan Abramov 的我的博客。vue
伴随着 Hook 时代的带来,react 社区也是到来了无 Hook 不欢的时代。 如火如荼的封装。包括 axios 以及 immer 等库都未能“幸免”,被 Hook 包裹了一层而变成了 axios-hooks 以及 use-immer。可是却始终没有一个杀手级应用。react
在我今天阅读 精读《Hooks 取数 - swr 源码》 时候,了解到这个 12天就拿到4000+ star 的杀手级应用 swr。既然大牛已经从 swr 源码来展开。那我就从 UX(用户体验) 以及 DX(开发者体验) 来聊一聊。ios
做为一个开发者而言,始终面临着一个问题,究竟当前数据是否应该放入 状态管理库或者仅仅只在组件中使用?就我的开发而言,始终秉承着一个思想,若是一个数据,不会被两个及以上的不能直接通讯的祖先组件使用。那么它就不配使用状态管理,用完便直接抛弃。不要由于多写一些代码而放弃简单性。git
用 SWR 最基础的功能以下所示:github
import useSWR from 'swr'
function Profile () {
const { data, error } = useSWR('/api/user', fetch)
// 保镖模式(the Bouncer Pattern), 后面处理正确业务逻辑
if (error) return <div>failed to load</div>
// 没有错误,且没有数据 只有多是正常业务流程中的等待取数据
if (!data) return <div>loading...</div>
// 没有错误有数据,进行渲染
return <div>hello {data.name}!</div>
}
复制代码
因此我以为如上代码十分切合 DX 的设计与思想。在取数据以前,取数据是一个 UI 展现,发生错误也是一个 UI 展现。仅仅 4 行代码,就囊括了 error, loading 以及正常业务的全部 UI 切换。在 数据没有获取以前,data 与 error 都是 undefined。进行 loading。在数据获取以后, data 与 error 二者必居其一。web
若是你写过 Go 语言,必定对这种代码不陌生。算法
val, err := myFunction( args... );
if err != nil {
// handle error
return
}
// success
复制代码
因为 Go 中有大量此类代码的处理,因此在 Go2 中有新的草案提出,这里就不进行深刻讨论。不过对于任何语言和业务而言,错误处理设计都是很是重要的。这种代码在须要大量书写的 Go 语言中,是一种负担。可是,对于当前 SWR 而言,反而并不繁琐,具备更加清晰的状态切换。数据库
对于用户而言,并不关心咱们如何取得数据,可是对于开发者来讲,形形色色的需求使得自定义配置不会是可选的,而是必需的。大到 vue, react 的平台(Weex, react native ) 适配。小到咱们提供给他人的基础功能模块,都是须要对他人负责的。编程
例如,若是须要提供功能代码给别人用,一般就会这样写。
const DEFAULT_CONFIG = {
// 基础配置
// ...
}
// 利用 Object.assign 后面配置来覆盖
const config = Object.assign({}, DEFAULT_CONFIG, config)
复制代码
而在 SWR 中,在其中追加了全局配置:
config = Object.assign(
{},
// 默认配置
defaultConfig,
// 全局配置
useContext(SWRConfigContext),
// 当前组件配置
config
)
复制代码
这里咱们来介绍一下 fetcher 函数,接受传入的 key 值,返回一个 promise 或者数据。中间也能够结合各类库来进行数据处理。
import fetch from 'unfetch'
const fetcher = url => fetch(url).then(r => r.json())
function App () {
const { data } = useSWR('/api/data', fetcher)
// ...
}
复制代码
import { request } from 'graphql-request'
const API = 'https://api.graph.cool/simple/v1/movies'
const fetcher = query => request(API, query)
function App () {
const { data, error } = useSWR(
`{ Movie(title: "Inception") { releaseDate actors { name } } }`,
fetcher
)
// ...
}
复制代码
当时看到这里以前,我一度不能理解 useSWR 函数第一个参数叫 key 的缘由。当使用 GraphQL时候,我终于知道,我仍是 So young So simple。毕竟对 GraphQL 缺少实战经验,因此每每会对不熟悉的技术产生遗漏。固然了,若是你参考过其余关于 api 的缓存的开源代码必定能够当即想到,缓存工做必定围绕着当前的 key 值。
若是你并不须要特殊处理,直接略过 fetcher 这个参数便可,就像基础功能版。当看到这里时,基本上咱们能够判断在实际使用过程当中,即便遇到了没法预料的业务情景,咱们也能够经过咱们的代码来解决掉问题。
在使用 SWR 以后,若是咱们在当前应用打开多个窗口或者选项卡。从新聚焦当前页面时候,无需手动或者在代码中从新刷新。SWR 会自动取得数据而后基于 React diff 进行渲染。
基于 DX 而言,这帮咱们解决了一个痛点。在不少状况下,用户或基于两个数据页面的比对。或者 To C 应用,咱们须要打开多个窗口或选项卡。而窗口或者 tab 切换时候,是否可以基于业务进行处理是值得思考的。
如下代码是判断当前文档是否可见,代码风格依然是保镖模式(the Bouncer Pattern)。
export default function isDocumentVisible(): boolean {
if (
typeof document !== 'undefined' &&
typeof document.visibilityState !== 'undefined'
) {
return document.visibilityState !== 'hidden'
}
// always assume it's visible
return true
}
复制代码
固然了,咱们能够经过配置来决定是否使用该功能。
// revalidateOnFocus = true:窗口聚焦时自动从新验证
const { data } = useSWR('dynamic-6', () => value++, {
revalidateOnFocus: false
})
// 或者全局配置
function App () {
return (
<SWRConfig
value={{
refreshInterval: 3000,
fetcher: (...args) => fetch(...args).then(res => res.json()),
revalidateOnFocus: false
}}
>
<Dashboard />
</SWRConfig>
)
}
复制代码
同时,值得一提的是,在多个窗口或者选项卡中,咱们也能够配置间隔刷新来进行多窗口同步,不过这须要更多的网络资源。
在开发 web 应用程序时,性能都是必不可少的话题。 而事实上,缓存必定是提高web应用程序最有效有效方法之一,尤为是用户受限于网速的状况下。提高系统的响应能力,下降网络的消耗。固然,内容越接近于用户,则缓存的速度就会越快,缓存的有效性则会越高。 以前,我曾经写过 前端 api 请求缓存方案。
可是若是使用 SWR,咱们若是在系统内部进行导航或者按下后退按钮,咱们直接会取得缓存版本数据。而后系统为了一致性,呈现了数据以后,会继续请求服务端,从新拉去数据。看到这里,我不由要说一句,这很 ServiceWorker。相似于 cache-then-network 机制。
若是想要仔细研究 ServiceWorker 来帮助开发离线应用程序,能够学习 The offline cookbook 以及 workbox 文档。
若是一个语言(库)不能给你带来思想上的扩展,那么就不要学习它。SWR 在获取数据方面的确有他特殊之处。一方面是条件获取。
// 条件获取
const { data } = useSWR(shouldFetch ? '/api/data' : null, fetcher)
// 条件获取得到 fetcher
const { data } = useSWR(() => shouldFetch ? '/api/data' : null, fetcher)
复制代码
若是当前 shouldFetch 是 falsy,那么若是 useSWR 则不会进行请求。那么依赖获取则更加有趣。SWR 为了性能而确保了最大的并行性。按照代码解析以下
import useSWR from 'swr'
function MyProjects () {
const { data: user } = useSWR('/api/user')
const { data: projects } = useSWR(
() => '/api/projects?uid=' + user.id
)
if (!projects) return 'loading...'
return 'You have ' + projects.length + ' projects'
}
复制代码
若是按照平时书写代码的逻辑,若是后一个请求依赖前一个请求的响应,是须要promise 或者 async 与 await。可是在当前 SWR 框架中,却仅仅只须要把顺序写好。
因为 SWR 不是一个与编译结合的依赖库,因此不要想像的太过复杂,仅仅只是由于错误重试。当执行到 user.id 时候,由于 user 并非一个对象,因此在当前请求以前会发生错误。而后再继续重试请求。等到第一次请求 user 取到以后,项目才会真正的向后端进行请求。
请求时机以 2 的指数性增加,代码以下:
const count = Math.min(opts.retryCount || 0, 8)
const timeout =
~~((Math.random() + 0.5) * (1 << count)) * config.errorRetryInterval
setTimeout(revalidate, timeout, opts)
复制代码
上述也是带有随机性质的 截断指数退避算法,当使用这种策略时候,客户端不断增长重试的延迟时间,而不是固定的延时。这样的话会更加符合现实世界的逻辑。固然咱们也是能够控制重试的。
useSWR(key, fetcher, {
onErrorRetry: (error, key, option, revalidate, { retryCount }) => {
if (retryCount >= 10) return
if (error.status === 404) return
// retry after 5 seconds
setTimeout(() => revalidate({ retryCount: retryCount + 1 }), 5000)
}
})
复制代码
这种决策很是有趣,相似于所有的请求都是 promise.all 。我我的虽然承认这种模式,可是在极端状况下,会出现前置依赖仅仅延迟一点,后置请求延迟一轮的状况。即便在不那么极端的状况中,也有必定的时间损耗。
若是在能够商议的状况下,将多个取数 api 结合为一个多参数的 api 也不失为一种可行的解决方案。是否采用 SWR 依赖取数,这取决于项目是否可以接受这种时间损耗。
使用 mutate,您能够经过编程方式更新本地数据,同时从新验证并最终用最新数据替换它。
import useSWR, { mutate } from 'swr'
function Profile () {
const { data } = useSWR('/api/user', fetcher)
return (
<div> <h1>My name is {data.name}.</h1> <button onClick={async () => { const newName = data.name.toUpperCase() // 请求更新名称 await requestUpdateUsername(newName) // 先更新名称,后从新拉去数据验证 mutate('/api/user', { ...data, name: newName }) }}>Uppercase my name!</button> </div>
)
}
// requestUpdateUsername 返回 200 无需验证。填写 new User
// 不过该方案仅仅只能修改无乐观锁的数据
mutate('/api/user', newUser, false)
// promise 返回更新的 user。直接更新
mutate('/api/user', requestUpdateUsername(newUser))
// 也能够返回 id 与乐观锁
const modifiedUser = requestUpdateUsername(newUser).then(res => {
return Object.assign({}, newUser, res)
})
// promise 返回更新的 user。直接更新
mutate('/api/user', modifiedUser)
复制代码
而是为当前的取数服务提供了修改的功能,使得 SWR 不仅仅是一个单纯的取数框架。如此以来,修改列表,编辑页面便都实现。( 在没有仔细看该功能的状况下,我一度觉得该功能相似 Meteor (Meteor 是一个实时框架, 在客户端也自带数据库,查询与更新都是先针对客户端数据库,后面再交由服务端来容许与拒绝,也就是失败补偿)可是后面却发现,该功能并非我预想的)。
对比自身书写的 Hook 方法,不得不说的是,SWR 的确够硬核,做者虽然只解决了取数这一方面,可是无不彰显出做者的代码和业务的设计功底。在这个仅仅只有 4kb 的小库中,真正深度运用了 Hook,同时也给与了用户很大的便利。同时,我也相信该库必定对任何想要深刻学习 Hook 的人有所帮助。
若是你以为这篇文章不错,但愿能够给与我一些鼓励,在个人 github 博客下帮忙 star 一下。 博客地址