Skip to content

Latest commit

 

History

History
952 lines (757 loc) · 24.6 KB

README-zh_TW.md

File metadata and controls

952 lines (757 loc) · 24.6 KB

English | 中文

vue-condition-watcher

CircleCI vue3 vue3 npm npm bundle size npm

介紹

vue-condition-watcher 是 Vue 組合 API,以 conditions 為核心,可用在請求資料情境,還能簡單地使用 conditions 參數來自動獲取資料

Node.js 需大於或等於 12.0.0 版本

功能

✔ 每當 conditions 變動,會自動獲取數據
✔ 送出請求前會自動過濾掉 null undefined [] ''
✔ 重新整理網頁會自動依照 URL 的 query string 初始化 conditions,且會自動對應型別 ( string, number, array, date )
✔ 每當 conditions 變動,會自動同步 URL query string,並且讓上一頁下一頁都可以正常運作
✔ 避免 race condition,確保請求先進先出,也可以避免重複請求
✔ 在更新 data 前,可做到依賴請求 ( Dependent Request )
✔ 輕鬆處理分頁的需求,簡單客製自己的分頁邏輯
✔ 當網頁重新聚焦或是網絡斷線恢復自動重新請求資料
✔ 支援輪詢,可動態調整輪詢週期
✔ 緩存機制讓資料可以更快呈現,不用再等待 loading 動畫
✔ 不需要等待回傳結果,可手動改變 data 讓使用者體驗更好
✔ 支援 TypeScript
✔ 支援 Vue 2 & 3,感謝 vue-demi

Navigation

Demo

👉 (推薦) 這邊下載 Vue3 版本範例 (使用 Vite)

cd examples/vue3
yarn 
yarn serve

👉 這邊下載 Vue2 @vue/composition-api 版本範例

cd examples/vue2
yarn 
yarn serve

👉 線上 Demo

入門

安裝

在你的專案執行 yarn

yarn add vue-condition-watcher

或是使用 NPM

npm install vue-condition-watcher

CDN

https://unpkg.com/vue-condition-watcher/dist/index.js

快速開始

這是一個使用 vue-nextvue-router-next 的簡單範例。

首先建立一個 fetcher function, 你可以用原生的 fetch 或是 Axios 這類的套件。接著 import useConditionWatcher 並開始使用它。

createApp({
  template: `
    <div class="filter">
      <input v-model="conditions.name">
      <button @click="execute">Refetch</button>
    </div>
    <div class="container">
      {{ !loading ? data : 'Loading...' }}
    </div>
    <div v-if="error">{{ error }}</div>
  `,
  setup() {
    const fetcher = params => axios.get('/user/', {params})
    const router = useRouter()

    const { conditions, data, loading, error } = useConditionWatcher(
      {
        fetcher,
        conditions: {
          name: ''
        },
        history: {
          sync: router
        }
      }
    )
    return { conditions, data, loading, error }
  },
})
.use(router)
.mount(document.createElement('div'))

您可以使用 dataerrorloading 的值來確定請求的當前狀態。

conditions.name 值改變,將會觸發 生命週期 重新發送請求.

你可以在 config.history 設定 sync 為 sync: router。 這將會同步 conditions 的變化到 URL 的 query string。

基礎用法

const { conditions, data, error, loading, execute, resetConditions, onConditionsChange } = useConditionWatcher(config)

Configs

  • fetcher: (⚠️ 必要) 請求資料的 promise function。
  • conditions: (⚠️ 必要) conditions 預設值。
  • defaultParams: 每次請求預設會帶上的參數,不可修改。
  • initialData: data 預設回傳 null,如果想定義初始的資料可以使用這個參數設定。
  • immediate: 如果不想一開始自動請求資料,可以將此參數設定為 false,直到 conditions 改變或是執行 execute 才會執行請求。
  • manual: 改為手動執行 execute 以觸發請求,就算 conditions 改變也不會自動請求。
  • history: 基於 vue-router (v3 & v4),啟用同步 conditions 到 URL 的 Query String。當網頁重新整理後會同步 Query String 至 conditions
  • pollingInterval: 啟用輪詢,以毫秒為單位可以是 number 或是 ref(number)
  • pollingWhenHidden: 每當離開聚焦畫面繼續輪詢,預設是關閉的
  • pollingWhenOffline: 每當網路斷線繼續輪詢,預設是關閉的
  • revalidateOnFocus: 重新聚焦畫面後,重新請求一次,預設是關閉的
  • cacheProvider: vue-condition-watch 背後會緩存資料,可傳入此參數自訂 cacheProvider
  • beforeFetch: 你可以在請求前最後修改 conditions,也可以在此階段終止請求。
  • afterFetch: 你可以在 data 更新前調整 data 的結果
  • onFetchError: 當請求發生錯誤觸發,可以在dataerror 更新前調整 error& data

Return Values

  • conditions:
    Type: reactive
    reactive 型態的物件 (基於 config 的 conditions),是 vue-conditions-watcher主要核心,每當 conditions 改變都會觸發生命週期
  • data:
    Type: 👁‍🗨 readonly & ⚠️ ref
    Default Value: undefined
    config.fetcher 的回傳結果
  • error:
    Type: 👁‍🗨 readonly & ⚠️ ref
    Default Value: undefined
    config.fetcher 錯誤返回結果
  • isFetching:
    Type: 👁‍🗨 readonly & ⚠️ ref
    Default Value: false
    請求正在處理中的狀態
  • loading: 當 !data.value & !error.value 就會是 true
  • execute: 基於目前的 conditionsdefaultParams 再次觸發請求。
  • mutate: 可以使用此方法修改 data
    🔒 ( data預設是唯獨不可修改的 )
  • resetConditions: 重置 conditions 回初始值
  • onConditionsChange: 在 conditions 發生變化時觸發,回傳新值以及舊值
  • onFetchSuccess: 請求成功觸發,回傳原始的請求結果
  • onFetchError: 請求失敗觸發,回傳原始的請求失敗結果
  • onFetchFinally: 請求結束時觸發

執行請求

conditions 是響應式的, 每當 conditions 變化將自動觸發請求

const { conditions } = useConditionWatcher({
  fetcher,
  conditions: {
    page: 0
  },
  defaultParams: {
    opt_expand: 'date'
  }
})

conditions.page = 1 // fetch data with payload { page: 1, opt_expand: 'date' }

conditions.page = 2 // fetch data with payload { page: 2, opt_expand: 'date' }

如果有需要你可以執行 execute 這個 function 再次發送請求

const { conditions, execute: refetch } = useConditionWatcher({
  fetcher,
  conditions: {
    page: 0
  },
   defaultParams: {
    opt_expand: 'date'
  }
})

refetch() // fetch data with payload { page: 0, opt_expand: 'date' }

一次完整更新 conditions只會觸發一次請求

const { conditions, resetConditions } = useConditionWatcher({
  fetcher,
  immediate: false,
  conditions: {
    page: 0,
    name: '',
    date: []
  },
})

// 初始化 conditions 將會觸發 `onConditionsChange` 事件
resetConditions({
  name: 'runkids',
  date: ['2022-01-01', '2022-01-02']
})

// 重置 conditions
function reset () {
  // 直接用 `resetConditions` function 來重置初始值.
  resetConditions()
}

阻止預請求

vue-conditions-watcher 會在一開始先請求一次,如果不想這樣做可以設定 immediatefalse,將不會一開始就發送請求直到你呼叫 execute function 或是改變 conditions

const { execute } = useConditionWatcher({
  fetcher,
  conditions,
  immediate: false,
})

execute()

手動觸發請求

vue-condition-watcher 會自動觸發請求. 但是你可以設定 manualtrue 來關閉這個功能。接著可以使用 execute() 在你想要的時機觸發請求。

const { execute } = useConditionWatcher({
  fetcher,
  conditions,
  manual: true,
})

execute()

攔截請求

beforeFetch 可以讓你在請求之前再次修改 conditions

  • 第一個參數回傳一個深拷貝的 conditions,你可以任意的修改它且不會影響原本 conditions,你可以在這邊調整要給後端的 API 格式。
  • 第二個參數回傳一個 function,執行它將會終止這次請求。這在某些情況會很有用的。
  • beforeFetch 可以處理同步與非同步行為。
  • 必須返回修改後的 conditions
useConditionWatcher({
  fetcher,
  conditions: {
    date: ['2022/01/01', '2022/01/02']
  },
  initialData: [],
  async beforeFetch(conditions, cancel) {
    // 請求之前先檢查 token
    await checkToken ()

    // conditions 是一個深拷貝 `config.conditions` 的物件
    const {date, ...baseConditions} = conditions
    const [after, before] = date
    baseConditions.created_at_after = after
    baseConditions.created_at_before = before

    // 返回修改後的 `conditions`
    return baseConditions
  }
})

afterFetch 可以在更新 data 前攔截請求,這時候的 loading 狀態還是 true

  • 你可以在這邊做依賴請求 🎭,或是處理其他同步與非同步行為
  • 可以在這邊最後修改 data,返回的值將會是 data 的值
const { data } = useConditionWatcher({
  fetcher,
  conditions,
  async afterFetch(response) {
    //response.data = {id: 1, name: 'runkids'}
    if(response.data === null) {
      return []
    }
    // 依賴其他請求
    // `loading` 還是 `true` 直到 `onFetchFinally`
    const finalResponse = await otherAPIById(response.data.id)

    return finalResponse // [{message: 'Hello', sender: 'runkids'}]
  }
})

console.log(data) //[{message: 'Hello', sender: 'runkids'}]

onFetchError 可以攔截錯誤,可以在 dataerror 更新前調整 error & data,這時候的 loading 狀態還是 true

  • onFetchError 可以處理同步與非同步行為。
  • 最後返回格式必須為
{
  data: ... ,
  error: ...
}
const { data, error } = useConditionWatcher({
  fetcher,
  conditions,
  async onFetchError({data, error}) {
    if(error.code === 401) {
      await doSomething()
    }

    return {
      data: [],
      error: 'Error Message'
    }
  }
})

console.log(data) //[]
console.log(error) //'Error Message'

變異資料

在一些情況下, mutations data 是提升用戶體驗的好方法,因為不需要等待 API 回傳結果。

使用 mutate function, 你可以修改 data。 當 onFetchSuccess 觸發時會再改變 data

有兩種方式使用 mutate function:

  • 第一種:完整修改 data.
mutate(newData)
  • 第二種:使用 callback function,會接受一個深拷貝的 data 資料,修改完後再返回結果
const finalData = mutate((draft) => {
  draft[0].name = 'runkids'
  return draft
})

console.log(finalData[0]name === data.value[0].name) //true

🏄‍♂️ 範例:依據目前的資料來修改部分資料

POST API 會返回更新後的結果,我們不需要重新執行 execute 更新結果。我們可以用 mutate 的第二種方式來修改部分改動。

const { conditions, data, mutate } = useConditionWatcher({
  fetcher: api.userInfo,
  conditions,
  initialData: []
})

async function updateUserName (userId, newName, rowIndex = 0) {
  console.log(data.value) //before: [{ id: 1, name: 'runkids' }, { id: 2, name: 'vuejs' }]

  const response = await api.updateUer(userId, newName)

  // 🚫 `data.value[0] = response.data`
  // 沒作用! 因為 `data` 是唯讀不可修改的.

  // Easy to use function will receive deep clone data, and return updated data.
  mutate(draft => {
    draft[rowIndex] = response.data
    return draft
  })

  console.log(data.value) //after: [{ id: 1, name: 'mutate name' }, { id: 2, name: 'vuejs' }]
}

Conditions 改變事件

onConditionsChange 可以幫助你處理 conditions 的變化。會回傳新值和舊值

const { conditions, onConditionsChange } = useConditionWatcher({
  fetcher,
  conditions: {
    page: 0
  },
})

conditions.page = 1

onConditionsChange((conditions, preConditions)=> {
  console.log(conditions) // { page: 1}
  console.log(preConditions) // { page: 0}
})

請求事件

onFetchResponse, onFetchErroronFetchFinally 會在請求期間觸發。

const { onFetchResponse, onFetchError, onFetchFinally } = useConditionWatcher(config)

onFetchResponse((response) => {
  console.log(response)
})

onFetchError((error) => {
  console.error(error)
})

onFetchFinally(() => {
  //todo
})

輪詢

你可以透過設定 pollingInterval 啟用輪詢功能(當為 0 時會關閉此功能)

useConditionWatcher({
  fetcher,
  conditions,
  pollingInterval: 1000
})

你還可以使用 ref 動態響應輪詢週期。

const pollingInterval = ref(0)

useConditionWatcher({
  fetcher,
  conditions,
  pollingInterval: pollingInterval
})

function startPolling () {
  pollingInterval.value = 1000
}

onMounted(startPolling)

vue-condition-watcher 預設會在你離開畫面聚焦或是網路斷線時停用輪詢,直到畫面重新聚焦或是網路連線上了才會啟用輪詢。

你可以透過設定關閉預設行為:

  • pollingWhenHidden=true 離開聚焦後繼續輪詢
  • pollingWhenOffline=true 網路斷線還是會繼續輪詢

你也可以啟用聚焦畫面後重打請求,確保資料是最新狀態。

  • revalidateOnFocus=true
useConditionWatcher({
  fetcher,
  conditions,
  pollingInterval: 1000,
  pollingWhenHidden: true, // pollingWhenHidden default is false
  pollingWhenOffline: true, // pollingWhenOffline default is false
  revalidateOnFocus: true // revalidateOnFocus default is false
})

緩存

vue-condition-watcher 預設會在當前組件緩存你的第一次數據。接著後面的請求會先使用緩存數據,背後默默請求新資料,等待最新回傳結果並比對緩存資料是否相同,達到類似預加載的效果。

你也可以設定 cacheProvider 全局共用或是緩存資料在 localStorage,搭配輪詢可以達到分頁同步資料的效果。

Global Based
// App.vue
<script lang="ts">
const cache = new Map()

export default {
  name: 'App',
  provide: {
    cacheProvider: () => cache
  }
}

//Other.vue
useConditionWatcher({
  fetcher,
  conditions,
  cacheProvider: inject('cacheProvider')
})
</script>
function localStorageProvider() {
  const map = new Map(JSON.parse(localStorage.getItem('your-cache-key') || '[]'))
  window.addEventListener('beforeunload', () => {
    const appCache = JSON.stringify(Array.from(map.entries()))
    localStorage.setItem('your-cache-key', appCache)
  })
  return map
}

useConditionWatcher({
  fetcher,
  conditions,
  cacheProvider: localStorageProvider
})

History 模式

你可以設定 config.history 啟用 History 模式,是基於 vue-router 的,支援 v3 和 v4 版本

const router = useRouter()

useConditionWatcher({
  fetcher,
  conditions,
  history: {
    sync: router
  }
})

你還可以設定 history.ignore 排除 conditions 部分的 key&value 不要同步到 URL query string.

const router = useRouter()

useConditionWatcher({
  fetcher,
  conditions: {
    users: ['runkids', 'hello']
    limit: 20,
    offset: 0
  },
  history: {
    sync: router,
    ignore: ['limit']
  }
})

// the query string will be ?offset=0&users=runkids,hello

History mode 會轉換 conditions預設值的對應型別到 query string 而且會過濾掉 undefined, null, '', [] 這些類型的值.

conditions: {
  users: ['runkids', 'hello']
  company: ''
  limit: 20,
  offset: 0
}
// the query string will be ?offset=0&limit=20&users=runkids,hello

每當你重新整理網頁還會自動同步 query string 到 conditions

URL query string: ?offset=0&limit=10&users=runkids,hello&company=vue

conditions 將變成

{
  users: ['runkids', 'hello']
  company: 'vue'
  limit: 10,
  offset: 0
}

使用 navigation 可以 push 或是 replace 當前的位置. 預設值為 'push'

useConditionWatcher({
  fetcher,
  conditions: {
    limit: 20,
    offset: 0
  },
  history: {
    sync: router,
    navigation: 'replace'
  }
})

生命週期

  • onConditionsChange

    conditions 變更時觸發,會返回新舊值。

    onConditionsChange((cond, preCond)=> {
      console.log(cond)
      console.log(preCond)
    })
  • beforeFetch

    可以讓你在請求之前再次修改 conditions,也可以在這個階段終止請求。

    const { conditions } = useConditionWatcher({
      fetcher,
      conditions,
      beforeFetch
    })
    
    async function beforeFetch(cond, cancel){
      if(!cond.token) {
        // stop fetch
        cancel()
        // will fire onConditionsChange again
        conditions.token = await fetchToken()
      }
      return cond
    })
  • afterFetch & onFetchSuccess

    afterFetch 會在 onFetchSuccess 前觸發
    afterFetch 可以在data 更新前修改 data

    Type Modify data before update Dependent request
    afterFetch config ⭕️ ⭕️
    onFetchSuccess event
      <template> 
        {{ data?.detail }} <!-- 'xxx' -->
      </template>
    const { data, onFetchSuccess } = useConditionWatcher({
     fetcher,
     conditions,
     async afterFetch(response){
       //response = { id: 1 }
       const detail = await fetchDataById(response.id)
       return detail // { id: 1, detail: 'xxx' }
     })
    })
    
    onFetchSuccess((response)=> {
     console.log(response) // { id: 1, detail: 'xxx' }
    })
  • onFetchError(config) & onFetchError(event)

    config.onFetchError 會在 event.onFetchError 前觸發
    config.onFetchError 可以攔截錯誤,可以在 dataerror 更新前調整 error & data

    Type Modify data before update Modify error before update
    onFetchError config ⭕️ ⭕️
    onFetchError event
    const { onFetchError } = useConditionWatcher({
     fetcher,
     conditions,
     onFetchError(ctx){
       return {
         data: [],
         error: 'Error message.'
       }
     })
    })
    
    onFetchError((error)=> {
     console.log(error) // origin error data
    })
  • onFetchFinally

    請求結束時觸發

    onFetchFinally(async ()=> {
      //do something
    })

重複使用

建立 vue-condition-watcher 的可重用的 hook 非常容易。

function useUserExpensesHistory (id) {
  const { conditions, data, error, loading } = useConditionWatcher({
    fetcher: params => api.user(id, { params }),
    defaultParams: {
      opt_expand: 'amount,place'
    },
    conditions: {
      daterange: []
    }
    immediate: false,
    initialData: [],
    beforeFetch(cond, cancel) {
      if(!id) {
        cancel()
      }
      const { daterange, ...baseCond } = cond
      if(daterange.length) {
        [baseCond.created_at_after, baseCond.created_at_before] = [
          daterange[0],
          daterange[1]
        ]
      }
      return baseCond
    }
  })

  return {
    histories: data,
    isFetching: loading,
    isError: error,
    daterange: conditions.daterange
  }
}

接著在 components 使用:

<script setup>
  const { 
    daterange, 
    histories, 
    isFetching, 
    isError 
  } = useUserExpensesHistory(route.params.id)

  onMounted(() => {
    //start first time data fetching after initial date range
    daterange = [new Date(), new Date()]
  })
</script>
<template>
  <el-date-picker
    v-model="daterange"
    :disabled="isFetching"
    type="daterange"
  />
  <div v-for="history in histories" :key="history.id">
    {{ `${history.created_at}: ${history.amount}` }}
  </div>
</template>

恭喜你! 🥳 你已經學會再次包裝 vue-condition-watcher.

現在我們來用 vue-condition-watcher 做分頁的處理.

分頁處理

這個範例適用 Django the limit and offset functions 和 Element UI.

建立 usePagination

function usePagination () {
  let cancelFlag = false // check this to cancel fetch

  const { startLoading, stopLoading } = useLoading()
  
  const { conditions, data, execute, resetConditions, onConditionsChange, onFetchFinally } = useConditionWatcher(
    {
      fetcher: api.list,
      conditions: {
        daterange: [],
        limit: 20,
        offset: 0
      }
      immediate: true,
      initialData: [],
      history: {
        sync: 'router',
        // You can ignore the key of URL query string, prevent users from entering unreasonable numbers by themselves.
        // The URL will look like ?offset=0 not show `limit`
        ignore: ['limit'] 
      },
      beforeFetch
    }, 
  )

  // use on pagination component
  const currentPage = computed({
    get: () => conditions.offset / conditions.limit + 1,
    set: (page) => {
      conditions.offset = (page - 1) * conditions.limit
    }
  })

  // onConditionsChange -> beforeFetch -> onFetchFinally
  onConditionsChange((newCond, oldCond) => {
    // When conditions changed, reset offset to 0 and then will fire beforeEach again.
    if (newCond.offset !== 0 && newCond.offset === oldCond.offset) {
      cancelFlag = true
      conditions.offset = 0
    }
  })

  async function beforeFetch(cond, cancel) {
    if (cancelFlag) {
      // cancel fetch when cancelFlag be true
      cancel()
      cancelFlag = false // reset cancelFlag 
      return cond
    }
    // start loading
    await nextTick()
    startLoading()
    const { daterange, ...baseCond } = cond
    if(daterange.length) {
      [baseCond.created_at_after, baseCond.created_at_before] = [
        daterange[0],
        daterange[1]
      ]
    }
    return baseCond
  }

  onFetchFinally(async () => {
    await nextTick()
    // stop loading
    stopLoading()
    window.scrollTo(0, 0)
  })

  return {
    data,
    conditions,
    currentPage,
    resetConditions,
    refetch: execute
  }
}

接著在 components 使用:

<script setup>
  const { data, conditions, currentPage, resetConditions, refetch } = usePagination()
</script>
<template>
  <el-button @click="refetch">Refetch Data</el-button>
  <el-button @click="resetConditions">Reset Offset</el-button>

  <el-date-picker
    v-model="conditions.daterange"
    type="daterange"
  />

  <div v-for="info in data" :key="info.id">
    {{ info }}
  </div>

  <el-pagination
    v-model:currentPage="currentPage"
    v-model:page-size="conditions.limit"
    :total="data.length"
  />
</template>

當 daterange or limit 改變時, 會將 offset 設置為 0,接著才會重新觸發請求。

TDOD List

  • Error Retry
  • Nuxt SSR SSG Support

Thanks

This project is heavily inspired by the following awesome projects.

📄 License

MIT License © 2020-PRESENT Runkids