vue condition watcher
made with
Vuejs
vue-condition-watcher是一个Vue Compose API组件,在进行后端请求时,可以监控condition变化,自动刷新请求。它可以方便处理条件且更好的维护核心逻辑,像是自动响应列表页面的筛选条件等情境。
特色:
- ✔ 每当 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
- 安装
- 快速开始
- Configs
- Return Values
- 执行请求
- 阻止预请求
- 手动触发请求
- 拦截请求
- 变异资料
- Conditions 改变事件
- 请求事件
- 轮询
- 缓存
- History 模式
- 生命週期
- 分页处理
- Changelog
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-next
和 vue-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'))
您可以使用 data
、error
和 loading
的值来确定请求的当前状态。
当 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
: 当请求发生错误触发,可以在data
和error
更新前调整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
: 基于目前的conditions
和defaultParams
再次触发请求。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
会在一开始先请求一次,如果不想这样做可以设定 immediate
为 false
,将不会一开始就发送请求直到你呼叫 execute
function 或是改变 conditions
const { execute } = useConditionWatcher({
fetcher,
conditions,
immediate: false,
})
execute()
手动触发请求
vue-condition-watcher
会自动触发请求. 但是你可以设定 manual
为 true
来关闭这个功能。接著可以使用 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
可以拦截错误,可以在 data
和 error
更新前调整 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((currentData) => {
currentData[0].name = 'runkids'
return currentData
})
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(currentData => {
currentData[rowIndex] = response.data
return currentData
})
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
, onFetchError
和 onFetchFinally
会在请求期间触发。
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>
LocalStorage Based
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
}
生命週期
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
可以拦截错误,可以在data
和error
更新前调整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
- [ ] Cache
- [ ] Prefetching
- [ ] Automatic Revalidation
- [ ] Error Retry
- [ ] Nuxt SSR SSG Support
Thanks
This project is heavily inspired by the following awesome projects.
📄 License
MIT License © 2020-PRESENT Runkids
相关项目