refactor(offline-booking): 重构离线预约缓存轮询逻辑
- 移除未使用的 Vue 组合式 API 导入 - 拆分轮询控制逻辑为独立函数,提高可维护性 - 优化网络监听器注册流程,避免重复执行 - 使用引用计数管理轮询生命周期 - 添加详细注释说明核心逻辑
Showing
2 changed files
with
117 additions
and
67 deletions
| ... | @@ -74,6 +74,51 @@ | ... | @@ -74,6 +74,51 @@ |
| 74 | pnpm build:weapp | 74 | pnpm build:weapp |
| 75 | ``` | 75 | ``` |
| 76 | 76 | ||
| 77 | +## 离线预约缓存轮询说明 | ||
| 78 | + | ||
| 79 | +### 目标 | ||
| 80 | + | ||
| 81 | +在「已授权 + 网络可用」时,定时刷新离线预约记录缓存(本地 storage:`OFFLINE_BOOKING_DATA`),用于弱网/离线兜底页面展示。 | ||
| 82 | + | ||
| 83 | +### 相关文件 | ||
| 84 | + | ||
| 85 | +* 离线缓存刷新:`src/composables/useOfflineBookingCache.js` | ||
| 86 | + * 核心方法:`refresh_offline_booking_cache({ force })` | ||
| 87 | +* 轮询与网络监听:`src/composables/useOfflineBookingCachePolling.js` | ||
| 88 | + * 核心方法:`enable_offline_booking_cache_polling(options)` / `disable_offline_booking_cache_polling()` | ||
| 89 | +* 启动入口:`src/app.js` | ||
| 90 | + * 在授权成功后调用 `enable_offline_booking_cache_polling()` | ||
| 91 | + | ||
| 92 | +### 启动条件 | ||
| 93 | + | ||
| 94 | +轮询启动需要同时满足: | ||
| 95 | + | ||
| 96 | +1. `ref_count > 0`:表示当前确实有人需要轮询(应用级启用会占用 1 个引用) | ||
| 97 | +2. `network_usable === true`:网络类型可用(由 `src/utils/network.js` 判定) | ||
| 98 | + | ||
| 99 | +### 运行流程 | ||
| 100 | + | ||
| 101 | +1. `enable_offline_booking_cache_polling(options)`: | ||
| 102 | + * 缓存最后一次 `options`(用于网络恢复时重启轮询) | ||
| 103 | + * 增加 `ref_count` | ||
| 104 | + * 注册 `Taro.onNetworkStatusChange`(仅注册一次) | ||
| 105 | + * 如果当前网络可用,则启动定时器,默认每 `60000ms` 刷新一次 | ||
| 106 | +2. 定时器 tick: | ||
| 107 | + * 调用 `refresh_offline_booking_cache({ force: !!options?.force })` | ||
| 108 | + * 内部会再次校验授权与网络,避免无效请求 | ||
| 109 | +3. 网络变更监听: | ||
| 110 | + * 变为弱网/无网:立即停止轮询定时器 | ||
| 111 | + * 恢复为可用网络:若 `ref_count > 0`,用最后一次 `options` 重新启动轮询 | ||
| 112 | +4. `disable_offline_booking_cache_polling()`: | ||
| 113 | + * 释放一次 `ref_count` | ||
| 114 | + * 当 `ref_count === 0` 时:停止轮询并注销网络监听器 | ||
| 115 | + | ||
| 116 | +### options 说明 | ||
| 117 | + | ||
| 118 | +* `interval_ms`:轮询间隔(毫秒),默认 `60000` | ||
| 119 | +* `immediate`:是否立即刷新一次,默认 `true` | ||
| 120 | +* `force`:透传给 `refresh_offline_booking_cache`,用于强制刷新(默认 `false`) | ||
| 121 | + | ||
| 77 | ## 优化建议 (TODO) | 122 | ## 优化建议 (TODO) |
| 78 | 123 | ||
| 79 | * [x] 小程序授权流程有问题 - 已处理 | 124 | * [x] 小程序授权流程有问题 - 已处理 | ... | ... |
| 1 | -import { ref, onMounted, onUnmounted } from 'vue' | ||
| 2 | import Taro from '@tarojs/taro' | 1 | import Taro from '@tarojs/taro' |
| 3 | import { refresh_offline_booking_cache } from '@/composables/useOfflineBookingCache' | 2 | import { refresh_offline_booking_cache } from '@/composables/useOfflineBookingCache' |
| 4 | import { get_network_type, is_usable_network } from '@/utils/network' | 3 | import { get_network_type, is_usable_network } from '@/utils/network' |
| ... | @@ -31,9 +30,9 @@ const polling_state = { | ... | @@ -31,9 +30,9 @@ const polling_state = { |
| 31 | } | 30 | } |
| 32 | 31 | ||
| 33 | /** | 32 | /** |
| 34 | - * @description: 归一化选项 | 33 | + * @description: 缓存最后一次 options(用于网络恢复时重启轮询) |
| 35 | * @param {Object} options 选项 | 34 | * @param {Object} options 选项 |
| 36 | - * @return {Object} 归一化后的选项 | 35 | + * @return {Object} 最后一次选项 |
| 37 | */ | 36 | */ |
| 38 | 37 | ||
| 39 | const normalize_options = (options) => { | 38 | const normalize_options = (options) => { |
| ... | @@ -48,11 +47,13 @@ const normalize_options = (options) => { | ... | @@ -48,11 +47,13 @@ const normalize_options = (options) => { |
| 48 | */ | 47 | */ |
| 49 | 48 | ||
| 50 | const run_refresh_once = async (options) => { | 49 | const run_refresh_once = async (options) => { |
| 51 | - if (polling_state.in_flight) return | 50 | + if (polling_state.in_flight) return // 核心防重复——如果正在刷新,直接返回 |
| 51 | + // 标记为“正在刷新” | ||
| 52 | polling_state.in_flight = true | 52 | polling_state.in_flight = true |
| 53 | try { | 53 | try { |
| 54 | await refresh_offline_booking_cache({ force: !!options?.force }) | 54 | await refresh_offline_booking_cache({ force: !!options?.force }) |
| 55 | } finally { | 55 | } finally { |
| 56 | + // 刷新完成后,标记为“刷新完成” | ||
| 56 | polling_state.in_flight = false | 57 | polling_state.in_flight = false |
| 57 | } | 58 | } |
| 58 | } | 59 | } |
| ... | @@ -79,48 +80,53 @@ const should_run_polling = () => { | ... | @@ -79,48 +80,53 @@ const should_run_polling = () => { |
| 79 | } | 80 | } |
| 80 | 81 | ||
| 81 | /** | 82 | /** |
| 82 | - * @description: 确保轮询已启动 | ||
| 83 | - * @param {Object} options 选项 | ||
| 84 | - */ | ||
| 85 | - | ||
| 86 | -const ensure_polling_started = (options) => { | ||
| 87 | - start_offline_booking_cache_polling(options) | ||
| 88 | -} | ||
| 89 | - | ||
| 90 | -/** | ||
| 91 | * @description: 确保网络监听器已注册 | 83 | * @description: 确保网络监听器已注册 |
| 84 | + * @return {Promise} 网络监听器Promise | ||
| 92 | */ | 85 | */ |
| 93 | 86 | ||
| 94 | const ensure_network_listener = async () => { | 87 | const ensure_network_listener = async () => { |
| 88 | + /** | ||
| 89 | + * 代码优先通过两个条件判断避免重复执行监听器逻辑 | ||
| 90 | + * 1. 有已注册的监听器直接返回 | ||
| 91 | + * 2. 有未完成的注册 Promise 则直接返回 | ||
| 92 | + */ | ||
| 93 | + | ||
| 95 | if (polling_state.has_network_listener) { | 94 | if (polling_state.has_network_listener) { |
| 96 | await update_network_usable() | 95 | await update_network_usable() |
| 97 | return | 96 | return |
| 98 | } | 97 | } |
| 98 | + | ||
| 99 | if (polling_state.network_listener_promise) { | 99 | if (polling_state.network_listener_promise) { |
| 100 | await polling_state.network_listener_promise | 100 | await polling_state.network_listener_promise |
| 101 | return | 101 | return |
| 102 | } | 102 | } |
| 103 | 103 | ||
| 104 | + // 立即执行异步的监听器注册流程(标记状态→更新网络可用性→定义回调→注册监听) | ||
| 104 | polling_state.network_listener_promise = (async () => { | 105 | polling_state.network_listener_promise = (async () => { |
| 106 | + // 标记已注册网络监听器 | ||
| 105 | polling_state.has_network_listener = true | 107 | polling_state.has_network_listener = true |
| 108 | + // 初始化时更新网络可用性 | ||
| 106 | await update_network_usable() | 109 | await update_network_usable() |
| 107 | 110 | ||
| 111 | + // 网络状态变化监听器, 网络状态变化时的处理逻辑,此时只是定义,不会立即执行 | ||
| 108 | polling_state.network_listener = (res) => { | 112 | polling_state.network_listener = (res) => { |
| 109 | const is_connected = res?.isConnected !== false | 113 | const is_connected = res?.isConnected !== false |
| 110 | const type = res?.networkType || 'unknown' | 114 | const type = res?.networkType || 'unknown' |
| 111 | polling_state.network_usable = is_connected && is_usable_network(type) | 115 | polling_state.network_usable = is_connected && is_usable_network(type) |
| 112 | 116 | ||
| 117 | + // 网络不可用时,停止轮询 | ||
| 113 | if (!polling_state.network_usable) { | 118 | if (!polling_state.network_usable) { |
| 114 | stop_offline_booking_cache_polling() | 119 | stop_offline_booking_cache_polling() |
| 115 | return | 120 | return |
| 116 | } | 121 | } |
| 117 | 122 | ||
| 118 | if (should_run_polling()) { | 123 | if (should_run_polling()) { |
| 119 | - ensure_polling_started(polling_state.last_options || {}) | 124 | + start_offline_booking_cache_polling(polling_state.last_options || {}) |
| 120 | } | 125 | } |
| 121 | } | 126 | } |
| 122 | 127 | ||
| 123 | try { | 128 | try { |
| 129 | + // 注册网络状态变化监听器 | ||
| 124 | Taro.onNetworkStatusChange(polling_state.network_listener) | 130 | Taro.onNetworkStatusChange(polling_state.network_listener) |
| 125 | } catch (e) { | 131 | } catch (e) { |
| 126 | polling_state.has_network_listener = false | 132 | polling_state.has_network_listener = false |
| ... | @@ -131,8 +137,10 @@ const ensure_network_listener = async () => { | ... | @@ -131,8 +137,10 @@ const ensure_network_listener = async () => { |
| 131 | })() | 137 | })() |
| 132 | 138 | ||
| 133 | try { | 139 | try { |
| 140 | + // 等待网络监听器初始化完成, Taro.onNetworkStatusChange | ||
| 134 | await polling_state.network_listener_promise | 141 | await polling_state.network_listener_promise |
| 135 | } finally { | 142 | } finally { |
| 143 | + // 等待注册流程完成后,强制清空 Promise 缓存(finally 块),保证下次执行逻辑时状态干净。 | ||
| 136 | polling_state.network_listener_promise = null | 144 | polling_state.network_listener_promise = null |
| 137 | } | 145 | } |
| 138 | } | 146 | } |
| ... | @@ -161,20 +169,23 @@ const teardown_network_listener = () => { | ... | @@ -161,20 +169,23 @@ const teardown_network_listener = () => { |
| 161 | * @param {Object} options 选项 | 169 | * @param {Object} options 选项 |
| 162 | * @param {Number} options.interval_ms 轮询间隔,单位毫秒 | 170 | * @param {Number} options.interval_ms 轮询间隔,单位毫秒 |
| 163 | * @param {Boolean} options.immediate 是否立即刷新一次 | 171 | * @param {Boolean} options.immediate 是否立即刷新一次 |
| 172 | + * @param {Boolean} options.force 是否强制刷新(透传给 refresh_offline_booking_cache) | ||
| 164 | */ | 173 | */ |
| 165 | 174 | ||
| 166 | -export const start_offline_booking_cache_polling = (options) => { | 175 | +const start_offline_booking_cache_polling = (options) => { |
| 167 | normalize_options(options) | 176 | normalize_options(options) |
| 168 | - if (!should_run_polling()) return | 177 | + if (!should_run_polling()) return // 不满足轮询条件直接返回 |
| 169 | const interval_ms = Number(options?.interval_ms || 60000) | 178 | const interval_ms = Number(options?.interval_ms || 60000) |
| 170 | - if (polling_state.running) return | 179 | + if (polling_state.running) return // 核心防重复——如果已经在轮询,直接返回 |
| 171 | 180 | ||
| 172 | - polling_state.running = true | 181 | + polling_state.running = true // 标记为“正在轮询” |
| 173 | 182 | ||
| 183 | + // 立即刷新一次,确保轮询开始时数据是最新的 | ||
| 174 | if (options?.immediate !== false) { | 184 | if (options?.immediate !== false) { |
| 175 | run_refresh_once(options) | 185 | run_refresh_once(options) |
| 176 | } | 186 | } |
| 177 | 187 | ||
| 188 | + // 启动轮询定时器,按照指定间隔执行刷新操作 | ||
| 178 | polling_state.timer_id = setInterval(() => { | 189 | polling_state.timer_id = setInterval(() => { |
| 179 | run_refresh_once(options) | 190 | run_refresh_once(options) |
| 180 | }, interval_ms) | 191 | }, interval_ms) |
| ... | @@ -184,7 +195,7 @@ export const start_offline_booking_cache_polling = (options) => { | ... | @@ -184,7 +195,7 @@ export const start_offline_booking_cache_polling = (options) => { |
| 184 | * @description: 停止离线预约缓存轮询 | 195 | * @description: 停止离线预约缓存轮询 |
| 185 | */ | 196 | */ |
| 186 | 197 | ||
| 187 | -export const stop_offline_booking_cache_polling = () => { | 198 | +const stop_offline_booking_cache_polling = () => { |
| 188 | if (polling_state.timer_id) { | 199 | if (polling_state.timer_id) { |
| 189 | clearInterval(polling_state.timer_id) | 200 | clearInterval(polling_state.timer_id) |
| 190 | polling_state.timer_id = null | 201 | polling_state.timer_id = null |
| ... | @@ -192,70 +203,64 @@ export const stop_offline_booking_cache_polling = () => { | ... | @@ -192,70 +203,64 @@ export const stop_offline_booking_cache_polling = () => { |
| 192 | polling_state.running = false | 203 | polling_state.running = false |
| 193 | } | 204 | } |
| 194 | 205 | ||
| 195 | -export const enable_offline_booking_cache_polling = (options) => { | 206 | +/** |
| 207 | + * 引用计数的核心作用 | ||
| 208 | + * 这两个函数实现了轮询功能的 “引用计数式资源管理”,本质是追踪有多少 “使用者 / 场景” 依赖这个轮询功能,从而决定是否启动 / 维持 / 停止轮询、注册 / 注销网络监听器, | ||
| 209 | + * 核心目的是: | ||
| 210 | + * - 避免轮询被重复启动、错误停止 | ||
| 211 | + * - 防止无使用者时仍占用资源(定时器、网络监听器) | ||
| 212 | + * - 保证多场景共用轮询时的逻辑一致性 | ||
| 213 | + */ | ||
| 214 | + | ||
| 215 | +/** | ||
| 216 | + * @description: 增加轮询引用计数 | ||
| 217 | + * 核心动作:将全局的 ref_count 加 1,代表 “又多了一个场景需要使用轮询功能”。 | ||
| 218 | + * @param {Object} options 选项 | ||
| 219 | + */ | ||
| 220 | + | ||
| 221 | +const acquire_polling_ref = (options) => { | ||
| 196 | normalize_options(options) | 222 | normalize_options(options) |
| 197 | - if (polling_state.app_enabled) { | ||
| 198 | - ensure_network_listener().then(() => ensure_polling_started(polling_state.last_options || {})) | ||
| 199 | - return | ||
| 200 | - } | ||
| 201 | - polling_state.app_enabled = true | ||
| 202 | polling_state.ref_count += 1 | 223 | polling_state.ref_count += 1 |
| 203 | - ensure_network_listener().then(() => ensure_polling_started(polling_state.last_options || {})) | 224 | + // 增加引用计数后,确保网络监听器已注册并初始化完成,然后启动轮询 |
| 225 | + ensure_network_listener().then(() => start_offline_booking_cache_polling(polling_state.last_options || {})) | ||
| 204 | } | 226 | } |
| 205 | 227 | ||
| 206 | -export const disable_offline_booking_cache_polling = () => { | 228 | +/** |
| 207 | - if (!polling_state.app_enabled) return | 229 | + * @description: 减少轮询引用计数 |
| 208 | - polling_state.app_enabled = false | 230 | + * 核心动作:将 ref_count 减 1(且保证不会为负数),代表 “有一个场景不再需要轮询功能”。 |
| 231 | + */ | ||
| 232 | + | ||
| 233 | +const release_polling_ref = () => { | ||
| 209 | polling_state.ref_count = Math.max(0, polling_state.ref_count - 1) | 234 | polling_state.ref_count = Math.max(0, polling_state.ref_count - 1) |
| 210 | if (polling_state.ref_count === 0) { | 235 | if (polling_state.ref_count === 0) { |
| 236 | + // 引用计数降为0时,停止轮询并注销网络监听器 | ||
| 211 | stop_offline_booking_cache_polling() | 237 | stop_offline_booking_cache_polling() |
| 212 | teardown_network_listener() | 238 | teardown_network_listener() |
| 213 | } | 239 | } |
| 214 | } | 240 | } |
| 215 | 241 | ||
| 216 | /** | 242 | /** |
| 217 | - * @description: 用于管理离线预约缓存轮询的组合式函数 | 243 | + * @description: 启用离线预约缓存轮询 |
| 218 | * @param {Object} options 选项 | 244 | * @param {Object} options 选项 |
| 219 | - * @param {Boolean} options.enabled 是否启用轮询 | ||
| 220 | - * @param {Boolean} options.auto 是否自动启动轮询 | ||
| 221 | - * @param {Number} options.interval_ms 轮询间隔,单位毫秒 | ||
| 222 | - * @param {Boolean} options.immediate 是否立即刷新一次 | ||
| 223 | */ | 245 | */ |
| 224 | 246 | ||
| 225 | -export const use_offline_booking_cache_polling = (options) => { | 247 | +export const enable_offline_booking_cache_polling = (options) => { |
| 226 | - const is_running = ref(false) | 248 | + normalize_options(options) |
| 227 | - const enabled = options?.enabled !== false | 249 | + // 核心防重复——如果已经启用,直接返回 |
| 228 | - | 250 | + if (polling_state.app_enabled) { |
| 229 | - const start = () => { | 251 | + ensure_network_listener().then(() => start_offline_booking_cache_polling(polling_state.last_options || {})) |
| 230 | - if (!enabled) return | 252 | + return |
| 231 | - polling_state.ref_count += 1 | ||
| 232 | - ensure_network_listener().then(() => { | ||
| 233 | - ensure_polling_started(options) | ||
| 234 | - }) | ||
| 235 | - is_running.value = true | ||
| 236 | - } | ||
| 237 | - | ||
| 238 | - const stop = () => { | ||
| 239 | - if (!is_running.value) return | ||
| 240 | - polling_state.ref_count = Math.max(0, polling_state.ref_count - 1) | ||
| 241 | - if (polling_state.ref_count === 0) { | ||
| 242 | - stop_offline_booking_cache_polling() | ||
| 243 | - teardown_network_listener() | ||
| 244 | - } | ||
| 245 | - is_running.value = false | ||
| 246 | } | 253 | } |
| 254 | + polling_state.app_enabled = true | ||
| 255 | + acquire_polling_ref(polling_state.last_options || {}) | ||
| 256 | +} | ||
| 247 | 257 | ||
| 248 | - onMounted(() => { | 258 | +/** |
| 249 | - if (options?.auto !== false) start() | 259 | + * @description: 禁用离线预约缓存轮询 |
| 250 | - }) | 260 | + */ |
| 251 | - | ||
| 252 | - onUnmounted(() => { | ||
| 253 | - stop() | ||
| 254 | - }) | ||
| 255 | 261 | ||
| 256 | - return { | 262 | +export const disable_offline_booking_cache_polling = () => { |
| 257 | - is_running, | 263 | + if (!polling_state.app_enabled) return |
| 258 | - start, | 264 | + polling_state.app_enabled = false |
| 259 | - stop, | 265 | + release_polling_ref() |
| 260 | - } | ||
| 261 | } | 266 | } | ... | ... |
-
Please register or login to post a comment