從 UX 與 DX 來談一談 React SWR

自從 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>
  )
}
複製代碼

同時,值得一提的是,在多個窗口或者選項卡中,咱們也能夠配置間隔刷新來進行多窗口同步,不過這須要更多的網絡資源。

快速導航(cache-then-network )

在開發 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 一下。 博客地址

參考文檔

SWR

精讀《Hooks 取數 - swr 源碼》

相關文章
相關標籤/搜索