> ## Content Index
> Fetch the complete content index at: https://wayne-wu.com/llms.txt
> Use this file to discover other available public pages before exploring further.

# TopWidgets⁺ 创作平台：技术架构与实现
- URL: https://wayne-wu.com/topwidgets-visual-editor/
- Published: 2024-04-30T04:33:18.000Z
- Updated: 2026-08-24T05:40:31.000Z
- Description: TopWidgets⁺ 创作平台围绕可视化组件编辑、数据源与动作流、本地项目归档和社区发布，构建从设计资产到 iOS 小组件运行时的完整链路。
- Author: Wayne Wu
- Tags: #project

[TopWidgets⁺ 创作平台](https://x.xiaozujian.com/?ref=wayne-wu.com) 是一套运行在浏览器中的可视化小组件创作工具，完成的作品可以由 [Top Widgets⁺ Icons & Themes](https://apps.apple.com/us/app/top-widgets-icons-themes/id6446477593?ref=wayne-wu.com) 安装到 iPhone 与 iPad。

它的工程重点并不只是 React 页面，也不只是一个画布组件，而是如何在浏览器中组织编辑器内核、项目系统与移动端交付链路，并把浏览器真正用成一个创作运行时：直接访问用户授权的目录，在浏览器私有文件系统中持续备份，用底层文件句柄阻止多页面同时编辑，通过 Paint Worklet 扩展 CSS 绘制能力，用 Service Worker 管理远程资源，借助 WebRTC 把编辑结果实时发送到设备，再把归档、图像与曲线计算放到 Worker 和 WebAssembly 中执行。

本文先说明整体技术架构和作品的数据流，再重点拆解这些 Web 技术在真实产品中分别解决了什么问题，以及实现时需要处理哪些边界。

## 整体技术架构：四层协作

TopWidgets⁺ 创作平台的技术架构可以分成四层。它们不是四个独立应用，而是通过稳定数据模型连接的不同职责边界：

```text
交互与呈现层
画布 / 图层 / 属性面板 / 时间轴 / 预览
              ↓ Action
编辑器内核
文档树 / 布局树 / 历史记录 / 数据源 / 设置 / 动作流
              ↓ 已提交的项目状态
浏览器基础设施
OPFS / File System Access / IndexedDB / Worker / Worklet / Service Worker / WebRTC / WASM
              ↓ 归档、校验与转换
交付层
移动端视图描述 / 素材 / 本地化 / 权限 / 刷新策略 / 交互动作
              ↓
TopWidgets⁺ App 与系统小组件

```

**交互与呈现层**负责把指针、键盘和面板输入转化为明确操作。画布不是项目的事实来源，DOM 也不承担持久化职责；它只展示布局结果，并把拖动、缩放、选择和属性变更发送给编辑器内核。

**编辑器内核**保存可序列化的文档树。节点、父子关系和根节点分别索引，便于插入、移动、组合和解组。布局树是由文档、设备环境、用户设置和变量解析出的派生结构，负责绝对坐标、包围盒、可见性与命中测试。文档树维护删除和内容变化的失效集合，因此一次局部修改不必重建全部布局。

编辑操作统一进入 Action 流。真正改变作品的 Action 会产生新状态并进入撤销历史，选区、焦点和预览环境等临时变化不会污染项目版本。连续拖动共享同一个手势 ID，过程中的高频更新最终合并成一次可撤销操作；多素材插入等复合操作则通过批处理统一提交。

**浏览器基础设施层**负责文件、并发、缓存、实时通信、后台计算和绘制扩展。它不是编辑器之外的一组附加工具，而是项目系统得以成立的运行基础：OPFS 保存临时资源与恢复点，IndexedDB 提供结构化索引，File System Access API 连接用户工作区，Worker 隔离文件计算，Service Worker 接管特定资源请求，WebRTC 建立浏览器与设备之间的调试通道，Paint Worklet 扩展 CSS 绘制，WebAssembly 承担算法密集计算。

**交付层**把富含编辑信息的项目转换为移动端真正需要的资源。转换过程遍历可见节点，收集仍被引用的图片、变量、用户设置、动作、本地化内容与系统能力，并按组件尺寸生成运行时视图、权限和刷新策略。编辑器辅助信息在这一层被裁剪，App 接收的是经过校验的声明式产物，而不是浏览器中的 React 状态或 DOM。

这种分层的关键价值是：编辑器可以增加辅助能力和调试信息，项目文件仍保持可恢复，移动端协议也不必跟随每一次界面重构变化。

## 一份作品如何在架构中流动

创建或修改节点时，交互层先产生 Action，编辑器内核据此更新文档并记录历史；布局层只重新解析失效节点，再把新的几何结果交给画布。数据源、用户设置、变量和预览环境也在解析阶段参与计算，因此同一份文档能够预览不同时间、设备状态和配置组合。

当已提交版本变化时，自动保存管线克隆当前项目，过滤未使用变量，沿节点、设置与动作引用重新收集素材，再把归档任务发送给 Web Worker。生成的恢复点写入 OPFS，并在条件允许时同步到用户工作区。这个过程与主线程的下一次编辑并行，不要求画布等待文件处理完成。

再次打开项目时，加载顺序正好相反：先获取项目互斥锁，再检查工作区句柄和权限，同步浏览器与本地备份，从新到旧校验归档，恢复文档、设置、变量和素材，最后创建编辑器与布局状态。任何一步失败都有明确边界，不会直接用一个可能损坏的“最新文件”覆盖全部恢复机会。

发布时，转换器从项目模型生成移动端视图描述。数据依赖会变成运行时属性，用户可配置内容会进入设置清单，点击行为会变成动作序列，节点使用的系统能力会汇总为权限与最低版本要求。浏览器负责创作、分析和打包，App 负责设备数据、系统授权与最终渲染，两端通过产物协议解耦。

在这条数据流中，下面的 Web 技术并非孤立的 API 展示，而是分别承担并发控制、可靠保存、渲染扩展、资源缓存和计算隔离。

## 用 OPFS 文件句柄实现跨页面互斥锁

浏览器编辑器有一个很容易被忽略的数据一致性问题：用户可能在两个标签页中打开同一个项目。两个页面各自拥有内存状态，也都可能触发自动保存。如果没有互斥机制，后写入的页面会覆盖先写入的结果，而且保存文件本身没有足够的信息判断哪一份状态才是正确的。

平台没有把这个问题交给后端，也没有只使用页面内的布尔变量，而是利用 OPFS（Origin Private File System）和 `FileSystemSyncAccessHandle` 实现同源页面之间的项目锁。

锁逻辑运行在专用 Web Worker 中。Worker 首先通过 `navigator.storage.getDirectory()` 取得当前站点的 OPFS 根目录，再为项目创建一个锁文件，并尝试取得同步访问句柄：

```ts
const root = await navigator.storage.getDirectory()
const lockDirectory = await root.getDirectoryHandle('.lock', { create: true })
const lockFile = await lockDirectory.getFileHandle(projectId, { create: true })
const accessHandle = await lockFile.createSyncAccessHandle()

```

同步访问句柄在打开期间独占对应文件。当另一个标签页尝试为同一项目创建句柄时，浏览器会拒绝请求；实现中把 `NoModificationAllowedError` 转换为“项目已在其他页面打开”的业务结果。编辑结束后调用 `close()` 释放句柄，Worker 终止时句柄也会随执行环境退出而释放。

主线程与锁 Worker 之间使用带 `callbackId` 的消息协议：每次 `lock` 或 `unlock` 都生成唯一 ID，主线程用 Map 保存 Promise 的 `resolve` 与 `reject`，Worker 完成操作后带回同一个 ID。这样既保持了异步 API 的调用体验，也满足同步文件访问句柄只能在 Worker 中使用的限制。

这套锁有清晰的作用域：它协调的是同一浏览器配置、同一源下的页面，不是分布式锁，也不能阻止其他设备编辑云端副本。但对于“同一台电脑误开两个编辑页”这一最常见的冲突来源，它不需要服务器租约、心跳或超时回收，故障面更小。

## OPFS 与 File System Access API 组成双层持久化

互斥锁只是 OPFS 的一种用途。创作过程中的临时资源与自动备份也保存在 OPFS 中，而用户主动选择的工作区则通过 File System Access API 读写。

两种文件系统解决的是不同问题：

- OPFS 由浏览器管理，读写路径固定，不需要每次保存都弹出选择器，适合高频自动保存、临时素材和恢复点。
- 用户工作区来自 `showDirectoryPicker()`，文件真实存在于用户可见的本地目录中，适合长期保存、迁移和外部备份。

用户首次选择目录后，平台会把 `FileSystemDirectoryHandle` 存入 IndexedDB。再次打开时并不假设旧权限仍然有效，而是依次执行 `queryPermission()`、必要时 `requestPermission()`，并尝试创建一个探测文件，区分“目录已移动”“句柄失效”和“权限被拒绝”等状态。

编辑中的素材会写入 OPFS 临时目录；项目状态则被归档为可移植文件。自动保存监听已提交版本的变化，以 1 秒防抖、5 秒最大等待时间合并连续操作。每次保存都会先清理未被引用的变量和素材，再生成恢复文件，优先写入 OPFS；满足账户和工作区条件时，同时写入本地目录。

恢复过程不是简单读取最后一个文件。平台会先对齐 OPFS 与本地两边的备份，再按时间从新到旧尝试校验和解包。最新文件损坏时会继续回退到更早的恢复点，所有备份都不可用时才读取工作区中的主项目文件。两侧分别只保留有限数量的版本，避免自动保存无限占用存储空间。

这里的关键不是“多存一份”，而是把浏览器私有、高频、低打扰的存储与用户可见、可迁移的文件系统组合起来：OPFS 保证创作连续性，File System Access API 保证数据所有权和可携带性。

## CSS Paint Worklet：把组件绘制能力接入 CSS

项目中被称作“CSS Worker”的部分，更准确地说是 CSS Painting API 的 Paint Worklet。它不是普通 Web Worker，也不负责业务计算，而是让页面注册自定义的 CSS 图像绘制函数。

平台实现了四类绘制原语：线性渐变、边框、圆角遮罩和圆环。入口先检测 `CSS.paintWorklet.addModule` 是否存在，再以独立模块注册各个 Worklet；不支持该能力的环境不会执行注册逻辑。

每个 Worklet 都由两部分组成：

1. `inputProperties` 声明它依赖哪些 CSS 自定义属性。
2. `paint(ctx, size, properties)` 读取这些属性，并使用受限的 Canvas 2D 上下文绘制结果。

例如边框绘制会读取宽度、端点样式、拐角样式、虚线数组、虚线相位和四角半径。它不是只调用一次 `strokeRect`，而是自己构造圆角路径；遇到椭圆圆角时分别处理横向和纵向半径，并把半径限制在容器宽高的一半以内。实线和虚线使用不同的裁切方式，避免笔触落在视图边界外。

圆角与边框 Worklet 的输出可以作为 `mask-image: paint(...)` 使用，元素自身的背景决定最终颜色。这样，颜色、渐变、圆角和虚线仍然由 CSS 属性驱动，绘制细节则由 Worklet 完成。编辑器改变一个自定义属性后，浏览器能够按照 `inputProperties` 重新触发绘制，不需要 React 重新生成 Canvas 位图或替换图片 URL。

这项技术尤其适合设计工具：同一个视觉原语既要响应尺寸变化，又要支持连续参数调整，还要保持 DOM 元素原有的布局、变换与合成能力。相比在主线程维护一批 `<canvas>`，Paint Worklet 更贴近 CSS 渲染链路，也减少了组件层的绘制状态。

它的边界同样明确。Paint Worklet 可用的 API 比普通 Canvas 更受限，浏览器支持情况也需要单独判断。因此实现采用能力检测，并把它当作增强层；如果一项视觉效果是基础可用性的组成部分，还需要准备普通 CSS 或预渲染资源作为降级路径。

## Service Worker：为创作资源建立可控缓存层

创作平台会反复访问快照、归档包、图标、预设和其他体积较大的远程资源。完全依赖 HTTP 缓存很难统一控制命中范围和版本清理，缓存所有 GET 请求又会带来接口数据过期、鉴权响应落盘等风险。

项目中的 Service Worker 因此采用“明确白名单 + Cache First”，而不是笼统的离线优先：

- 只有快照、归档、资源包、图标等已知路径会进入 Cache Storage。
- 命中白名单时先查询缓存，未命中才访问网络。
- 网络响应只有在 2xx 状态下才会克隆并写入缓存。
- 安装阶段调用 `skipWaiting()`，激活阶段清理旧版本缓存并执行 `clients.claim()`。
- 开发环境不注册 Service Worker，避免调试时被历史缓存干扰。

这套策略的价值在于缩小缓存语义：Service Worker 不尝试把整个应用伪装成完全离线产品，而是专门消除创作资源的重复下载。缓存版本号变化时，激活流程会删除旧 key，资源策略可以随应用版本一起升级。

当前实现中的缓存写入是异步旁路操作。它不会阻塞网络响应返回，因此首屏资源更快交给页面；但如果未来要提供“关闭页面后也必须完成写入”的强离线保证，就应把写缓存任务纳入 `waitUntil()` 生命周期，并补充容量、淘汰和配额异常处理。Service Worker 的价值不只是“能缓存”，而是可以精确决定哪些请求值得被浏览器长期接管。

## WebRTC：把浏览器中的作品实时送到设备

浏览器里的预览无法完全替代真机。字体、系统数据、权限、刷新行为以及最终渲染都可能受到设备环境影响，如果每次调整都要先上传、发布、再到 App 中安装，创作反馈会非常缓慢。

平台因此使用 WebRTC 建立浏览器与 TopWidgets⁺ App 之间的实时调试通道。这里传输的主要内容不是摄像头和麦克风，而是组件包、配置、图标、壁纸、桌面布局和设备信息。创作者修改项目后，浏览器会重新打包当前尺寸的作品，并在防抖后直接发送到已连接设备；App 收到后安装调试版本，形成接近热更新的真机预览体验。

连接建立分成信令层和数据层：

1. 浏览器创建连接 ID，并通过 WebSocket 向信令服务注册。
2. App 被唤起后使用同一个 ID 接入，双方经 WebSocket 交换 SDP offer、answer 与 ICE candidate。
3. `RTCPeerConnection` 进入 `connected` 后，WebSocket 完成使命并关闭。
4. 后续项目和设备数据全部通过 `RTCDataChannel` 点对点传输。

这种设计让服务器只参与“帮助双方找到彼此”，不长期中转体积较大的创作资源。连接状态、ICE 收集、协商失败与断线都被独立监听；用户退出调试或组件卸载时，系统会停止 transceiver、receiver track 和全部 DataChannel，再关闭 PeerConnection，避免残留连接继续占用资源。

### 多 DataChannel 与二进制分片

组件包和壁纸可能远大于普通控制消息。发送端会同时创建 20 条 `ordered: false` 的 DataChannel，并按照每条通道当前的 `bufferedAmount` 排序。消息编码成 ArrayBuffer 后，以 100KB 为一片；每个分片都携带消息 ID、总片数和序号，再轮流分配给已经打开的通道。

接收端不依赖分片到达顺序，而是按消息 ID 暂存，在收齐后依据序号排序并重新拼接。10 秒内没有继续更新的不完整消息会被清理，防止断线或异常传输留下不断增长的内存缓存。多通道调度与显式分片共同解决了大二进制消息的排队和重组问题。

应用层协议还在每条完整消息前加入类型、时间戳和唯一 ID。类型区分“正在同步”“安装组件”“设备已就绪”“图标”“壁纸”“桌面布局”等数据；时间戳用于拒绝晚到的旧状态；唯一 ID 则关联接收确认。对于需要明确同步状态的消息，发送方先发出开始信号，接收方完成重组与解码后回传 `received`，发送方再结束同步状态。

这套 WebRTC 链路也是整体架构中浏览器与 App 的动态边界：发布产物协议解决最终交付，RTCDataChannel 解决创作过程中的即时反馈。两者使用相近的组件资源，但生命周期不同——前者追求稳定、可审查和可分发，后者追求低等待时间、双向状态交换和连接失败后的可恢复性。

## Web Worker：把文件归档移出主线程

项目文件包含文档、设置、变量和大量二进制素材。归档时需要排序文件、读取 ArrayBuffer、拼接内容、生成索引、计算校验值，并根据版本执行加密或签名；恢复时还要识别旧格式、校验完整性、解密和拆分文件。如果这些步骤与拖动、缩放、属性输入共用主线程，编辑器很容易出现长任务和掉帧。

因此归档系统使用常驻 Web Worker，支持归档、解包、校验、提取校验值和识别资源类型等命令。它与 OPFS 锁采用相同的 `callbackId` 消息协议，让主线程以 Promise 调用 Worker。版本判断也位于 Worker 内部：读取文件头后选择对应的解析器，旧项目的兼容成本不会进入 UI 组件。

Worker 的意义不只是“异步”。普通 `async` 函数仍然在主线程执行 JavaScript，密集的字节数组处理照样会阻塞渲染；Worker 才提供独立执行上下文。对于这种计算量与项目体积相关、又不依赖 DOM 的任务，它是比拆分 `setTimeout` 更可靠的隔离方式。

## IndexedDB：保存可查询状态，而不是堆放所有文件

文件系统适合二进制项目与素材，但编辑器还需要快速查询项目列表、更新时间、预设、模板、快照和用户选项。平台用 IndexedDB 保存这类结构化状态，并通过索引支持按项目 ID、创建时间、修改时间和资源类型查找。

数据库按用途拆分对象仓库，包括项目元数据、创作快照、预设、模板和选项。数据库升级由显式 migration 推进；当其他页面阻塞升级时，连接会主动关闭并清空单例，异常终止则进入统一恢复流程。

一个值得注意的细节是，File System Access API 返回的目录句柄也可以作为结构化克隆值保存在 IndexedDB 中。平台借此记住用户选择过的工作区，但每次使用前仍重新检查权限。也就是说，IndexedDB 保存的是“如何重新找到目录”，不是绕过浏览器授权的永久通行证。

这种分工避免了两种极端：既没有把大型二进制内容全塞入数据库，也没有为了展示一个项目列表就遍历整个文件目录。OPFS、用户工作区和 IndexedDB 分别承担临时文件、可迁移文件与可查询状态。

## OffscreenCanvas 与 ImageBitmap：浏览器内的图像处理管线

小组件创作涉及壁纸裁切、图标合成、区域平均色、文字对比色和预览图生成。平台使用 `createImageBitmap()` 解码 Blob，再用 `OffscreenCanvas` 完成缩放、裁切与像素读取。

以壁纸颜色计算为例，图像会先按目标设备画面执行 cover 缩放，再从指定区域读取 `ImageData`。算法计算平均亮度，选择黑色或白色前景；也可以计算平均 RGB/HSL，为系统文字和滤镜生成更协调的颜色。图标与预览生成则把多张 ImageBitmap 合成到离屏画布，最后输出新的位图资源。

`ImageBitmap` 让解码后的图像成为适合绘制的对象，`OffscreenCanvas` 则让处理逻辑不依赖可见 DOM Canvas。即使部分调用目前仍由主线程发起，这种 API 选择也把图像算法与页面节点解耦，为进一步移入 Worker 保留了空间。

## WebAssembly：处理密码学与复杂曲线计算

平台包含两类 Rust 编译的 WebAssembly 模块。

第一类用于项目归档的加密、解密、SHA-256、签名验证和请求签名。归档格式自身包含版本、资源类型、加密版本、文件索引偏移、签名和校验值；JavaScript 负责组织文件和协议结构，计算敏感或字节密集的部分交给 WebAssembly。

第二类用于关键帧编辑中的贝塞尔曲线。它负责曲线路径、控制点、交点、裁切和连续路径等几何运算，并被时间函数编辑器、运动路径面板和画布手势共同复用。应用会在渲染编辑器之前初始化曲线模块，避免用户开始操作后才遇到异步加载空窗。

这里没有为了技术标签把全部逻辑搬进 WebAssembly。DOM、状态管理和文件组织仍由 TypeScript 完成，Rust 模块只承接算法密集、数据边界清晰的部分。这样的切分比“把整个编辑器编译成 WASM”更容易调试，也保留了 Web UI 的开发效率。

## Observer 与浏览器调度 API：让界面按需工作

大型编辑器还依赖一组不显眼但很关键的 Web API：

- `ResizeObserver` 监听画布、弹层、视频和列表容器尺寸，让测量与组件真实几何变化同步，而不是只监听窗口 resize。
- `IntersectionObserver` 用于列表分页、缩略图延迟加载和预览可见性判断，元素进入视口后才请求或播放资源。
- `MutationObserver` 监听媒体节点属性变化，在资源地址切换后重置加载与播放状态。
- `requestAnimationFrame` 把播放进度、布局后的测量和视觉状态更新对齐到浏览器绘制帧。
- `requestIdleCallback` 把低优先级统计工作推迟到主线程空闲阶段，避免与编辑手势争抢响应时间。

这些 API 共同体现了 Web 编辑器的性能策略：不靠一个全局循环不断轮询，而是让浏览器在“尺寸变了”“元素可见了”“DOM 变了”或“下一帧到了”时通知应用。它们单独看都不复杂，组合起来却能显著减少无效计算和隐藏区域的资源消耗。

## 兼容性不是附注，而是实现的一部分

这套方案大量使用现代 Web 能力，因此不能把兼容性留到上线前再处理。实现中已经可以看到几类边界意识：Paint Worklet 在注册前做能力检测；Service Worker 只在生产环境启用；目录句柄每次恢复后重新检查权限；归档器识别多个文件版本；OPFS 锁把浏览器异常转换成明确的业务状态。

还需要继续坚持同一原则：每项能力都应明确“不可用时是否允许降级”。互斥锁和项目恢复属于数据安全能力，缺失时应阻止进入编辑或切换到可靠替代方案；绘制特效可以使用 CSS 或静态资源降级；资源缓存不可用时则回到普通网络请求。区分这些级别，比简单列一张浏览器支持表更重要。

## 结语

TopWidgets⁺ 创作平台最有代表性的工程价值，在于它没有把浏览器仅仅当作界面容器。OPFS 同时承担自动备份和跨页面互斥，File System Access API 把作品交还给用户，Paint Worklet 扩展 CSS 绘制，Service Worker 控制资源缓存，WebRTC 连接浏览器与真机调试，Web Worker 隔离文件计算，IndexedDB 保存可查询状态，OffscreenCanvas 与 WebAssembly 完成图像和曲线处理。

这些能力最终服务于同一个目标：让一个复杂、长时间运行、需要保护创作成果的专业工具，可以真正建立在 Web 平台之上。页面只是表层，浏览器提供的文件系统、后台执行、渲染扩展和本地数据库，才构成了这套创作环境的技术底座。