
Instatic 持久化键位完全指南从 localStorage 到 user_preferences 的数据存放规范【免费下载链接】InstaticThe open-source alternative to Webflow, Framer and WordPress. Agentic self-hosted visual CMS outputting clean static pages. Users, roles, plugins, content, database, its all there.项目地址: https://gitcode.com/GitHub_Trending/in/Instatic本指南以 Instatic 仓库的 docs/reference/persistence-keys.md 为骨架系统梳理 Admin 控制台写入的每一个localStorage/sessionStorage键、唯一的会话 Cookie以及服务端跨设备同步的user_preferences偏好表。读完你将掌握所有客户端键位的命名约定与来源文件、服务端偏好接口的请求语义、parseJsonWithFallback与 TypeBox 校验的读写模式以及新增一个持久化偏好客户端或服务端的完整实操路径。一页回答数据存在哪持久化全景Instatic 将管理端状态划分为三层各自有不同的生命周期与同步范围持久化层典型用途生命周期跨设备同步localStorage编辑器偏好、布局、剪贴板、UI 视图模式等持久直到用户清理或显式删除否同设备跨标签页通过storage事件同步sessionStorage跨页面跳转的在途状态如 Spotlight 待执行动作会话级刷新跳转后仍存活否CookieHttpOnly管理端会话令牌会话 / 服务端控制否随请求携带user_preferences表仪表盘布局、模块插入器收藏等用户偏好服务端持久化是三条核心约定原文档 TL;DR 的完整版所有客户端持久化键统一前缀instatic-Spotlight 专属键用spotlight:且与站点/模块的 CSS 类名互不冲突所有服务端用户偏好存放在user_preferences表中以(user_id, key)为主键所有读取都经过parseJsonWithFallback(...)——数据损坏时静默回退到默认值而不是让编辑器白屏。该辅助函数定义在 src/core/utils/jsonValidate.ts。命名约定公式instatic-feature[-vversion]。当存储结构发生不兼容变更时递增-vN后缀使旧形状自动失效schema 上的additionalProperties: true保证读取端始终宽容详见下文版本化小节。客户端持久化键位清单localStorage以下为 Admin 应用当前写入的全部localStorage键每项附 Owner语义所有者与 source-of-truth 文件方便逆向定位这个状态到底存在哪Key所有者权威来源文件instatic-editor-prefs全部编辑器偏好自动保存、hover 预览、Admin 主题、UI 字号、密度、layers 选项src/admin/pages/site/preferences/editorPreferences.ts →EDITOR_PREFS_KEYinstatic-editor-layout-v2各工作区site / content / data / media侧边栏宽度 开合状态 浮动面板位置src/admin/state/workspaceLayoutStorage.ts →EDITOR_LAYOUT_STORAGE_KEYinstatic-clipboard-v1编辑器剪贴板图层子树复制 / 剪切 / 粘贴src/admin/pages/site/store/clipboard/clipboardStorage.ts →CLIPBOARD_STORAGE_KEYinstatic-class-usageClassPicker 自动补全中最近使用的类src/admin/pages/site/preferences/classUsage.ts→CLASS_USAGE_STORAGE_KEYinstatic-data-grid-primary-widths-v1Data 工作区网格中各表主列的宽度src/admin/pages/data/components/DataGrid/usePrimaryColumnWidth.tsinstatic-media-page-view-mode共享 Media 工作区与媒体库的视图模式grid / listsrc/admin/pages/media/utils/viewMode.tsinstatic-media-explorer-view-mode站点工作区 Media Explorer 面板的视图模式src/admin/pages/site/panels/MediaExplorerPanel/mediaExplorerUtils.ts→VIEW_MODE_STORAGE_KEYinstatic-module-inserter-v1模块插入器视图模式与最近插入记录src/admin/pages/site/module-picker/moduleInserterPrefs.tsinstatic-onboarding-dismissed仪表盘引导面板已关闭 / 展开按设备记录src/admin/pages/dashboard/hooks/useOnboardingState.tsspotlight:recent-commandsSpotlight 最近执行命令 id去重、上限 8 条src/admin/spotlight/recentStore.tsspotlight:telemetry:v1本地 Spotlight 遥测命令使用频率src/admin/spotlight/telemetry.ts几个值得展开的键位instatic-editor-prefs是所有键中最重量级的一个。它不是手写 schema而是由声明式的PREFERENCE_CATALOG见src/admin/pages/site/preferences/catalog.ts自动派生每个 catalog 条目贡献一个可选字段到EditorPrefsSchema同时贡献一条默认值到DEFAULT_EDITOR_PREFS。新增一个编辑器偏好只需在 catalog 中加一条记录editorPreferences.ts 无需改动——Settings UI 会自动渲染对应开关。实现上它还维护了一个原始字符串比对缓存每次读取都先localStorage.getItem并比较 raw 字符串命中缓存直接返回已解析对象免去JSON.parse TypeBox 校验单次解析约 0.5ms 的代价被摊平localStorage.clear()、测试注入或跨标签页storage事件都会导致 raw 串变化从而触发重新解析。跨标签页同步通过监听原生storage事件实现先失效缓存、再通知订阅者保证回调中读到的一定是新值。React 侧推荐使用useEditorPreference(id)/useEditorSelectPreference(id)钩子组件只订阅单个偏好依赖追踪保持简单。instatic-editor-layout-v2按工作区命名空间存储site/content/data/media各自记忆自己的侧栏宽度与开合状态浮动面板位置panelPositions与尺寸panelSizes放在顶层——每个FloatingPanelId唯一属于一个工作区不存在跨工作区冲突。注意Plugins、Users、Account 等页面走AdminPageLayout不参与该布局持久化。instatic-clipboard-v1是键名 v1、载荷版本 2的特例CLIPBOARD_STORAGE_KEY instatic-clipboard-v1但CLIPBOARD_VERSION 2载荷结构为{ version: 2, rootNodeIds, nodes, classes, copiedAt }支持多根节点复制。旧 v1 载荷单rootNodeId在读取时故意不支持safeParseJson会静默丢弃——剪贴板数据是可丢弃的不值得迁移。任何读取失败JSON 缺失、schema 不匹配、版本不支持一律视为无剪贴板绝不向 UI 抛错写入是 best-effort配额超限或隐私模式下静默吞掉异常内存中的 slice 状态仍可正常使用当前会话。spotlight:recent-commands保存最近执行的命令 idMAX_RECENT 8条写入前去重并置顶读取用parseJsonWithFallback(raw, RecentSchema, [])兜底schema 上限maxItems: 20任何失败含隐私模式localStorage抛异常都回退为空数组。sessionStorage跨页面跳转的在途状态Key所有者权威来源文件instatic-spotlight-pending-actionSpotlight 命令等待的跨页面重载动作如 step-up 认证后恢复执行src/admin/spotlight/pendingAction.ts与localStorage不同sessionStorage只用于在途的跨跳转状态用户触发一个需要二次认证step-up的 Spotlight 命令页面跳转到认证流程回来后必须恢复执行原动作——这个待恢复标记就放在 session 级存储中页面正常重载后依然存在但关闭标签页即丢弃。原文档的禁止模式明确警告不要用 sessionStorage 存放需要跨页面重载存活的常规状态那是 localStorage 的职责。会话 Cookie唯一允许存放秘密的地方Cookie所有者权威来源文件instatic_admin_sessionAdmin 会话令牌原始值查找时先哈希server/auth/tokens.ts→SESSION_COOKIE_NAME属性组合HttpOnly、Secure生产环境走 TLS 时、SameSiteLax、Path/admin。客户端 JavaScript永远无法直接读取它——这正是 Instatic 的安全边界设计token 只经 Cookie 自动携带前端代码层不存在任何可窃取会话令牌的读取路径。对应地原文档禁止模式明确不要在 localStorage 存放 secretstoken、密码。服务端用户偏好user_preferences 表客户端偏好按设备隔离而需要跨设备同步的用户偏好存放在服务端user_preferences表中每行对应一个(user_id, key)Key所有者权威来源文件dashboard-layout仪表盘 widget 的位置 / 尺寸含 onboarding 面板状态、底部 Block 库面板高度libraryHeightsrc/admin/pages/dashboard/hooks/useDashboardLayout.tsmodule-inserter模块插入器收藏notch favorites有序{ kind, id }引用kind 为module/savedLayout/component上限 12 条src/admin/pages/site/module-picker/useModuleInserterPreference.ts表结构两份迁移文件完全镜像Postgres 版本server/db/migrations-pg.tscreate table if not exists user_preferences ( user_id text not null references users(id) on delete cascade, key text not null, value_json jsonb not null, updated_at timestamptz not null default now(), primary key (user_id, key) );SQLite 版本server/db/migrations-sqlite.ts与 PG 保持镜像。三个设计要点迁移注释中写明key列接受任意字符串加键不需要迁移——服务端 handler 的键白名单USER_PREFERENCE_KEYS才是真正的强制边界value_json对 DB 层不透明——形状校验完全由服务端 per-key TypeBox schema 在 HTTP 边界读写两条路径完成on delete cascade——管理员删除用户时其偏好随用户记录原子删除。value_json列后缀_json触发 SQLite 适配器写入时自动 stringify、读取时自动 parse因此仓库层server/repositories/userPreferences.ts直接传递普通 JS 对象序列化由方言适配器透明处理。HTTP 接口与白名单校验GET /admin/api/cms/me/preferences/:key → { value } | { value: null }未设置时 PUT /admin/api/cms/me/preferences/:key → 保存 value DELETE /admin/api/cms/me/preferences/:key → 重置Handler 为 server/handlers/cms/userPreferences.ts能力边界任何已认证用户只能管理自己的偏好。语义细节源码注释明确未设置返回200 { value: null }而非 404——这些偏好全部可选客户端本就回退默认值404 只会变成看起来像真实失败的控制台噪音null是唯一的未设置信号每个偏好的值 schema 都是对象绝不可能是 null:key必须命中白名单USER_PREFERENCE_KEYS定义于 src/core/persistence/userPreferences.ts未知键返回 400——插件或第三方代码无法借该接口在用户记录中抢占任意键插件有自己的存储面cms.storagePUT 采用两层校验外层{ value: Type.Unknown() }信封校验内层 value 再按 per-key schema 校验规避 TypeScript 泛型推断问题同时让信封畸形 → 400与value 畸形 → 400 具体 TypeBox 错误路径边界干净GET 时对存储值重新校验——DB 行被手工改动或 schema 重命名导致的版本错位会以 500 暴露而不是静默把垃圾数据发给客户端DELETE 无论行是否存在都返回 204仓库层返回rowCount 0布尔值handler 不区分两者都视为已回到默认。服务端 schema 单一来源是 src/core/persistence/userPreferences.tsUSER_PREFERENCE_SCHEMAS同时供客户端类型推导与服务端校验使用getUserPreference(dashboard-layout)能推断出DashboardLayoutPreference | null的精确类型全程无as Foo强转。客户端的getUserPreference/setUserPreference辅助函数封装了/admin/api/cms/me/preferences基路径下的 GET / PUT 往返。读取模式safeParseJson 与 parseJsonWithFallback标准读取模式原文档核心代码含实现细节扩充import { safeParseJson, parseJsonWithFallback } from core/utils/jsonValidate // 硬失败损坏视为错误 const result safeParseJson(localStorage.getItem(instatic-...) ?? , Schema) if (!result.ok) throw result.error // 软失败典型场景损坏回退默认值 const value parseJsonWithFallback( localStorage.getItem(instatic-...) ?? , Schema, DEFAULTS, )底层实现src/core/utils/jsonValidate.ts说明了两者的差异safeParseJson返回可辨识联合{ ok: true; value } | { ok: false; error }内部把不是合法 JSON与是 JSON 但形状不符统一为失败——调用方无需区分两者都意味着丢弃并回退默认或返回 400parseJsonWithFallback是便捷包装raw为null/undefined/ 空串时直接返回默认值否则走safeParseJson失败即回退另有parseJsonResponse用于解析 HTTP 响应体——那是畸形响应是真实错误的场景失败直接抛出让上层错误边界接管。parseJsonWithFallback是默认选择用户不应该因为 localStorage 被截断就看到一个坏掉的编辑器。TypeBox 的 schema 验证细节可参考 docs/reference/typebox-patterns.md。写入模式TypeBox schema additionalPropertiesimport { Type } from core/utils/typeboxHelpers const Schema Type.Object({ view: Type.Union([Type.Literal(grid), Type.Literal(list)]), }, { additionalProperties: true }) const next { view: grid as const } localStorage.setItem(instatic-..., JSON.stringify(next))schema上的additionalProperties: true让旧客户端能读取新数据未知键在往返读-改-写时被原样保留。这一点在某个功能发布了新键而旧代码标签页仍在运行的窗口期至关重要——新键不会因为旧代码的重写而丢失。这也是为什么编辑器偏好的 schema 从 catalog 派生时每个条目都声明为Type.Optional(...)缺失字段可接受旧快照不会让新读取器崩溃。版本化约定何时、如何 bump-vN当存储形状发生不兼容变更时递增后缀instatic-editor-layout-v2 → instatic-editor-layout-v3旧键留在 localStorage 中供未升级用户继续使用新键从零开始。不要做数据迁移——让旧数据随用户代理的 GC 自然回收即可。选用标准原文档的精确表述additionalProperties: true覆盖常见场景新增一个可选字段只有字段形状变化对象变数组、枚举值被移除时才使用-vNbump。以剪贴板为实例v2 载荷把单根rootNodeId改为rootNodeIds数组属于形状级变更因此读取端对 v1 载荷故意不兼容safeParseJson直接丢弃——剪贴板是可丢弃数据不值得为它写迁移逻辑。Cookbook完整实操新增一个客户端持久化偏好// src/admin/pages/site/preferences/myFeature.ts import { Type, type Static } from core/utils/typeboxHelpers import { parseJsonWithFallback } from core/utils/jsonValidate const KEY instatic-my-feature-v1 const Schema Type.Object({ enabled: Type.Boolean(), threshold: Type.Number(), }, { additionalProperties: true }) type Prefs Statictypeof Schema const DEFAULTS: Prefs { enabled: true, threshold: 5 } export function readMyFeaturePrefs(): Prefs { return parseJsonWithFallback(localStorage.getItem(KEY) ?? , Schema, DEFAULTS) } export function writeMyFeaturePrefs(prefs: Prefs): void { localStorage.setItem(KEY, JSON.stringify(prefs)) }如果该功能是编辑器全局的优先加入editorPreferences.ts的PREFERENCE_CATALOG——Settings UI 会自动渲染开关还能免费获得跨标签页同步、缓存与useEditorPreference钩子。新增一个服务端持久化偏好// server/repositories/userPreferences.ts扩展 const MY_FEATURE_KEY my-feature export async function getMyFeature(db, userId): PromiseMyFeaturePrefs { const row await getUserPreferenceRow(db, userId, MY_FEATURE_KEY) return row ? parseValue(MyFeatureSchema, row) : MY_FEATURE_DEFAULTS }完整流程还包含在 src/core/persistence/userPreferences.ts 的USER_PREFERENCE_KEYS白名单加键名、在USER_PREFERENCE_SCHEMAS加对应 schema服务端 handler 靠它做读写两侧校验再写一个客户端 hook 请求GET /me/preferences/my-feature。更完整的偏好目录模式见 docs/features/editor-preferences.md。测试时清空某个键localStorage.removeItem(instatic-...) // 下次读取自动回退默认值端到端测试的规范重置是清空所有instatic-与spotlight:键for (const key of Object.keys(localStorage)) { if (key.startsWith(instatic-) || key.startsWith(spotlight:)) { localStorage.removeItem(key) } }禁止模式一览禁止模式正确做法键名不带instatic-前缀一律加instatic-前缀Spotlight 专属用spotlight:JSON.parse(localStorage.getItem(instatic-...) ?? {})parseJsonWithFallback(raw, Schema, DEFAULTS)手动静默捕获JSON.parse错误交给辅助函数处理在 localStorage 存 secretstoken、密码secrets 只存在于 CookieHttpOnly用 setTimeout 轮询做跨标签页广播原生storage事件跨标签页或 CustomEvent同标签页参考editorPreferences.ts在 localStorage 存大 blob1MB用 IndexedDB罕见——绝大多数 CMS 状态在服务端用 sessionStorage 存放需要跨页面重载存活的状态用 localStoragesession 只用于在途的跨跳转状态原地改 schema 而不 bump 键名形状不兼容变更时 bump-vN相关文档docs/features/editor-preferences.md —— 规范化的偏好目录PREFERENCE_CATALOGdocs/features/dashboard.md —— 仪表盘布局持久化dashboard-layoutdocs/features/spotlight.md —— Spotlight 最近命令 遥测spotlight:*键docs/reference/typebox-patterns.md ——parseJsonWithFallback、safeParseJson与 TypeBox 模式权威来源文件精选src/admin/pages/site/preferences/editorPreferences.ts ——EDITOR_PREFS_KEYsrc/admin/state/workspaceLayoutStorage.ts ——EDITOR_LAYOUT_STORAGE_KEYsrc/admin/pages/site/store/clipboard/clipboardStorage.ts ——CLIPBOARD_STORAGE_KEYsrc/admin/spotlight/recentStore.ts —— Spotlight 最近命令src/core/utils/jsonValidate.ts ——safeParseJson/parseJsonWithFallback实现src/core/persistence/userPreferences.ts —— 键白名单 per-key schema 单一来源server/repositories/userPreferences.ts ——user_preferences行 CRUDserver/handlers/cms/userPreferences.ts ——/admin/api/cms/me/preferences/:key【免费下载链接】InstaticThe open-source alternative to Webflow, Framer and WordPress. Agentic self-hosted visual CMS outputting clean static pages. Users, roles, plugins, content, database, its all there.项目地址: https://gitcode.com/GitHub_Trending/in/Instatic创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考