vue condition watcher
made with Vuejs

vue condition watcher

这是一个Vue Compose API组件。

相关问答
暂无相关问题
查看全部
简介及使用教程

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

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((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, 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>
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 可以拦截错误,可以在 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

  • [ ] 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

作者

相关项目

这是一个面向开发人员的简单进程管理器。
这是一个使用Vue组件编写歌曲的工具。
这是一个经过Podcast优化的、基于HTML5的音频播放器。
这是一个媒体管理器。
这是一个自动报告未捕获的JS异常的Vue插件。