diff --git a/src/stores/appSettings.ts b/src/stores/appSettings.ts index 74328c9..f139045 100644 --- a/src/stores/appSettings.ts +++ b/src/stores/appSettings.ts @@ -81,20 +81,41 @@ async function remove(key: string): Promise { * 这样 `loadAll` 异步填充缓存后能正确刷新组件视图(customRef 的 trigger 不会被 * reactive cache 的异步 mutation 自动唤起,故改用 watch 显式同步)。 * + * 值类型契约(重要,误用将静默不落库): + * - 适用值类型(基本类型 boolean/number/string)及整体替换语义。 + * - 对象/数组值必须以「整体替换」方式写入(ref.value = newObj), + * 严禁深 mutate(r.value.x = 1 / r.value.push(...))。 + * 原因:落库守卫用 JSON.stringify 比较新旧值,深 mutate 后 ref 引用未变、 + * 但值已与缓存同引用,若直接改 cache 引用又会破坏响应式;故约定对象/数组 + * 一律整体替换赋值,使守卫捕获到值变化并触发 set 落库。 + * * @example * const theme = useSetting('df-theme', 'dark') * // 模板里 —— 改动自动落库 + * + * @example 对象/数组须整体替换 + * const layout = useSetting('df-layout', { sidebar: true }) + * // ✅ 正确:整体替换 + * layout.value = { sidebar: false } + * // ❌ 错误:深 mutate 不会落库 + * // layout.value.sidebar = false */ function useSetting(key: string, defaultValue: T): Ref { const r = ref(get(key, defaultValue)) as Ref + // 同引用浅比较恒真时,退化为 JSON.stringify 深比较 —— 这样对象/数组即便 + // 同引用(整体替换前 ref 与 cache 指向同一对象),只要内容变化也能触发回写 + // 与落库。注:仍依赖「整体替换」语义,深 mutate 不在覆盖范围内(见函数文档)。 + const changed = (a: unknown, b: unknown): boolean => + Object.is(a, b) ? false : JSON.stringify(a) !== JSON.stringify(b) + // 缓存 → ref:loadAll/set/remove 改 cache[key] 时同步到 ref watch( () => cache[key], (v) => { // 写回值与当前 ref 不同时才更新,避免组件 set 后被 watch 回写造成抖动 const next = v === undefined ? defaultValue : (v as T) - if (!Object.is(next, r.value)) r.value = next + if (changed(next, r.value)) r.value = next }, { deep: true }, ) @@ -104,7 +125,7 @@ function useSetting(key: string, defaultValue: T): Ref { r, (v) => { // 仅当与缓存当前值不同时才写,避免 watch 回写触发循环 - if (!Object.is(v, cache[key])) void set(key, v) + if (changed(v, cache[key])) void set(key, v) }, { deep: true }, )