hookehuyr

refactor(offline-booking): 重构离线预约缓存轮询逻辑

- 移除未使用的 Vue 组合式 API 导入
- 拆分轮询控制逻辑为独立函数,提高可维护性
- 优化网络监听器注册流程,避免重复执行
- 使用引用计数管理轮询生命周期
- 添加详细注释说明核心逻辑
......@@ -74,6 +74,51 @@
pnpm build:weapp
```
## 离线预约缓存轮询说明
### 目标
在「已授权 + 网络可用」时,定时刷新离线预约记录缓存(本地 storage:`OFFLINE_BOOKING_DATA`),用于弱网/离线兜底页面展示。
### 相关文件
* 离线缓存刷新:`src/composables/useOfflineBookingCache.js`
* 核心方法:`refresh_offline_booking_cache({ force })`
* 轮询与网络监听:`src/composables/useOfflineBookingCachePolling.js`
* 核心方法:`enable_offline_booking_cache_polling(options)` / `disable_offline_booking_cache_polling()`
* 启动入口:`src/app.js`
* 在授权成功后调用 `enable_offline_booking_cache_polling()`
### 启动条件
轮询启动需要同时满足:
1. `ref_count > 0`:表示当前确实有人需要轮询(应用级启用会占用 1 个引用)
2. `network_usable === true`:网络类型可用(由 `src/utils/network.js` 判定)
### 运行流程
1. `enable_offline_booking_cache_polling(options)`
* 缓存最后一次 `options`(用于网络恢复时重启轮询)
* 增加 `ref_count`
* 注册 `Taro.onNetworkStatusChange`(仅注册一次)
* 如果当前网络可用,则启动定时器,默认每 `60000ms` 刷新一次
2. 定时器 tick:
* 调用 `refresh_offline_booking_cache({ force: !!options?.force })`
* 内部会再次校验授权与网络,避免无效请求
3. 网络变更监听:
* 变为弱网/无网:立即停止轮询定时器
* 恢复为可用网络:若 `ref_count > 0`,用最后一次 `options` 重新启动轮询
4. `disable_offline_booking_cache_polling()`
* 释放一次 `ref_count`
*`ref_count === 0` 时:停止轮询并注销网络监听器
### options 说明
* `interval_ms`:轮询间隔(毫秒),默认 `60000`
* `immediate`:是否立即刷新一次,默认 `true`
* `force`:透传给 `refresh_offline_booking_cache`,用于强制刷新(默认 `false`
## 优化建议 (TODO)
* [x] 小程序授权流程有问题 - 已处理
......
import { ref, onMounted, onUnmounted } from 'vue'
import Taro from '@tarojs/taro'
import { refresh_offline_booking_cache } from '@/composables/useOfflineBookingCache'
import { get_network_type, is_usable_network } from '@/utils/network'
......@@ -31,9 +30,9 @@ const polling_state = {
}
/**
* @description: 归一化选项
* @description: 缓存最后一次 options(用于网络恢复时重启轮询)
* @param {Object} options 选项
* @return {Object} 归一化后的选项
* @return {Object} 最后一次选项
*/
const normalize_options = (options) => {
......@@ -48,11 +47,13 @@ const normalize_options = (options) => {
*/
const run_refresh_once = async (options) => {
if (polling_state.in_flight) return
if (polling_state.in_flight) return // 核心防重复——如果正在刷新,直接返回
// 标记为“正在刷新”
polling_state.in_flight = true
try {
await refresh_offline_booking_cache({ force: !!options?.force })
} finally {
// 刷新完成后,标记为“刷新完成”
polling_state.in_flight = false
}
}
......@@ -79,48 +80,53 @@ const should_run_polling = () => {
}
/**
* @description: 确保轮询已启动
* @param {Object} options 选项
*/
const ensure_polling_started = (options) => {
start_offline_booking_cache_polling(options)
}
/**
* @description: 确保网络监听器已注册
* @return {Promise} 网络监听器Promise
*/
const ensure_network_listener = async () => {
/**
* 代码优先通过两个条件判断避免重复执行监听器逻辑
* 1. 有已注册的监听器直接返回
* 2. 有未完成的注册 Promise 则直接返回
*/
if (polling_state.has_network_listener) {
await update_network_usable()
return
}
if (polling_state.network_listener_promise) {
await polling_state.network_listener_promise
return
}
// 立即执行异步的监听器注册流程(标记状态→更新网络可用性→定义回调→注册监听)
polling_state.network_listener_promise = (async () => {
// 标记已注册网络监听器
polling_state.has_network_listener = true
// 初始化时更新网络可用性
await update_network_usable()
// 网络状态变化监听器, 网络状态变化时的处理逻辑,此时只是定义,不会立即执行
polling_state.network_listener = (res) => {
const is_connected = res?.isConnected !== false
const type = res?.networkType || 'unknown'
polling_state.network_usable = is_connected && is_usable_network(type)
// 网络不可用时,停止轮询
if (!polling_state.network_usable) {
stop_offline_booking_cache_polling()
return
}
if (should_run_polling()) {
ensure_polling_started(polling_state.last_options || {})
start_offline_booking_cache_polling(polling_state.last_options || {})
}
}
try {
// 注册网络状态变化监听器
Taro.onNetworkStatusChange(polling_state.network_listener)
} catch (e) {
polling_state.has_network_listener = false
......@@ -131,8 +137,10 @@ const ensure_network_listener = async () => {
})()
try {
// 等待网络监听器初始化完成, Taro.onNetworkStatusChange
await polling_state.network_listener_promise
} finally {
// 等待注册流程完成后,强制清空 Promise 缓存(finally 块),保证下次执行逻辑时状态干净。
polling_state.network_listener_promise = null
}
}
......@@ -161,20 +169,23 @@ const teardown_network_listener = () => {
* @param {Object} options 选项
* @param {Number} options.interval_ms 轮询间隔,单位毫秒
* @param {Boolean} options.immediate 是否立即刷新一次
* @param {Boolean} options.force 是否强制刷新(透传给 refresh_offline_booking_cache)
*/
export const start_offline_booking_cache_polling = (options) => {
const start_offline_booking_cache_polling = (options) => {
normalize_options(options)
if (!should_run_polling()) return
if (!should_run_polling()) return // 不满足轮询条件直接返回
const interval_ms = Number(options?.interval_ms || 60000)
if (polling_state.running) return
if (polling_state.running) return // 核心防重复——如果已经在轮询,直接返回
polling_state.running = true
polling_state.running = true // 标记为“正在轮询”
// 立即刷新一次,确保轮询开始时数据是最新的
if (options?.immediate !== false) {
run_refresh_once(options)
}
// 启动轮询定时器,按照指定间隔执行刷新操作
polling_state.timer_id = setInterval(() => {
run_refresh_once(options)
}, interval_ms)
......@@ -184,7 +195,7 @@ export const start_offline_booking_cache_polling = (options) => {
* @description: 停止离线预约缓存轮询
*/
export const stop_offline_booking_cache_polling = () => {
const stop_offline_booking_cache_polling = () => {
if (polling_state.timer_id) {
clearInterval(polling_state.timer_id)
polling_state.timer_id = null
......@@ -192,70 +203,64 @@ export const stop_offline_booking_cache_polling = () => {
polling_state.running = false
}
export const enable_offline_booking_cache_polling = (options) => {
/**
* 引用计数的核心作用
* 这两个函数实现了轮询功能的 “引用计数式资源管理”,本质是追踪有多少 “使用者 / 场景” 依赖这个轮询功能,从而决定是否启动 / 维持 / 停止轮询、注册 / 注销网络监听器,
* 核心目的是:
* - 避免轮询被重复启动、错误停止
* - 防止无使用者时仍占用资源(定时器、网络监听器)
* - 保证多场景共用轮询时的逻辑一致性
*/
/**
* @description: 增加轮询引用计数
* 核心动作:将全局的 ref_count 加 1,代表 “又多了一个场景需要使用轮询功能”。
* @param {Object} options 选项
*/
const acquire_polling_ref = (options) => {
normalize_options(options)
if (polling_state.app_enabled) {
ensure_network_listener().then(() => ensure_polling_started(polling_state.last_options || {}))
return
}
polling_state.app_enabled = true
polling_state.ref_count += 1
ensure_network_listener().then(() => ensure_polling_started(polling_state.last_options || {}))
// 增加引用计数后,确保网络监听器已注册并初始化完成,然后启动轮询
ensure_network_listener().then(() => start_offline_booking_cache_polling(polling_state.last_options || {}))
}
export const disable_offline_booking_cache_polling = () => {
if (!polling_state.app_enabled) return
polling_state.app_enabled = false
/**
* @description: 减少轮询引用计数
* 核心动作:将 ref_count 减 1(且保证不会为负数),代表 “有一个场景不再需要轮询功能”。
*/
const release_polling_ref = () => {
polling_state.ref_count = Math.max(0, polling_state.ref_count - 1)
if (polling_state.ref_count === 0) {
// 引用计数降为0时,停止轮询并注销网络监听器
stop_offline_booking_cache_polling()
teardown_network_listener()
}
}
/**
* @description: 用于管理离线预约缓存轮询的组合式函数
* @description: 启用离线预约缓存轮询
* @param {Object} options 选项
* @param {Boolean} options.enabled 是否启用轮询
* @param {Boolean} options.auto 是否自动启动轮询
* @param {Number} options.interval_ms 轮询间隔,单位毫秒
* @param {Boolean} options.immediate 是否立即刷新一次
*/
export const use_offline_booking_cache_polling = (options) => {
const is_running = ref(false)
const enabled = options?.enabled !== false
const start = () => {
if (!enabled) return
polling_state.ref_count += 1
ensure_network_listener().then(() => {
ensure_polling_started(options)
})
is_running.value = true
}
const stop = () => {
if (!is_running.value) return
polling_state.ref_count = Math.max(0, polling_state.ref_count - 1)
if (polling_state.ref_count === 0) {
stop_offline_booking_cache_polling()
teardown_network_listener()
}
is_running.value = false
export const enable_offline_booking_cache_polling = (options) => {
normalize_options(options)
// 核心防重复——如果已经启用,直接返回
if (polling_state.app_enabled) {
ensure_network_listener().then(() => start_offline_booking_cache_polling(polling_state.last_options || {}))
return
}
polling_state.app_enabled = true
acquire_polling_ref(polling_state.last_options || {})
}
onMounted(() => {
if (options?.auto !== false) start()
})
onUnmounted(() => {
stop()
})
/**
* @description: 禁用离线预约缓存轮询
*/
return {
is_running,
start,
stop,
}
export const disable_offline_booking_cache_polling = () => {
if (!polling_state.app_enabled) return
polling_state.app_enabled = false
release_polling_ref()
}
......