在浏览器中用 Vite 与 tursodatabase/sync-wasm 实现 Turso 数据库的离线优先双向同步【免费下载链接】tursoA SQL database in Rust: SQLite-compatible, now also speaking Postgres (experimental). The LLVM of databases.项目地址: https://gitcode.com/GitHub_Trending/tu/turso本文以本仓库中的最小示例 examples/javascript/sync-wasm-vite 为骨架完整讲解如何在浏览器端借助 Vite 使用tursodatabase/sync-wasm通过pull()/push()在本地数据库与 Turso Cloud 之间做双向同步并正确处理 SharedArrayBuffer 所必需的 COOP/COEP 跨域隔离头。读完本文你可以独立搭建一个可在浏览器离线写入、联网后自动同步的 Web 应用本文示例即一个“留言簿”Guestbook 应用同时理解该同步包在仓库源码层面的工作原理与全部可配置参数。一、示例项目定位浏览器里的双向同步数据库tursodatabase/sync-wasm是在普通数据库之上扩展了**双向同步bidirectional synchronization**能力的浏览器端包本地一个文件数据库远端一个 Turso Cloud 数据库二者可以通过两个核心方法保持一致pull()从远端拉取并应用变更到本地数据库确保本地状态反映服务器端的最新更新push()把本地变更上传回远端数据库适合本地做了 insert/update 后想把变更复制到云端时使用。二者配合即可实现offline-first离线优先的工作方式先在本地数据库上随意增删改之后再与 Turso Cloud 同步同时也能接收其他客户端产生的更新。这个示例位于 examples/javascript/sync-wasm-vite是一个“最小化minimal”的 Vite 浏览器示例对应仓库中的 bindings/javascript/sync/packages/wasm 包tursodatabase/sync-wasm。示例本身是一个经典的 Guestbook 留言簿任何访客在页面上输入留言数据先写入本地再通过 push 同步到云端其他客户端则通过周期性的 pull 拉取到新留言。二、示例项目结构一览examples/javascript/sync-wasm-vite/ ├── README.md # 本文所依据的使用说明 ├── index.html # Guestbook 页面与全部同步逻辑单文件 ├── package.json # 依赖与 dev/build/serve 脚本 ├── server.mjs # 生产构建的静态文件服务器设置 COOP/COEP 头 ├── vercel.json # Vercel 部署时的跨域隔离头配置 └── vite.config.ts # Vite dev server 的响应头配置值得注意的一点是仓库采用 monorepo 结构package.json 中tursodatabase/sync-wasm依赖直接以相对路径指向本地包目录../../../bindings/javascript/sync/packages/wasm而不是 npm registry 上的版本scripts 中定义了devvite、buildvite build、servenode server.mjs三个命令vite版本为^7.3.5。三、准备工作获取 Turso Cloud 凭据运行示例前需要为云端数据库创建访问令牌并获取数据库 URL。README 给出的命令如下npm install export VITE_TURSO_AUTH_TOKEN$(turso db tokens create db-name) # 为 Turso Cloud 中的数据库创建 auth token export VITE_TURSO_DATABASE_URL$(turso db show db-name --url) # 获取 Turso Cloud 中数据库的 URL npm run dev # 或者构建资源并用简单的 node.js 服务器托管 npm run build npm run serveVITE_TURSO_AUTH_TOKEN数据库的认证令牌通过turso db tokens create db-name创建VITE_TURSO_DATABASE_URL数据库连接 URL形如libsql://db-org.turso.io通过turso db show db-name --url获取。这两个变量之所以以VITE_前缀命名是因为 Vite 会把import.meta.env中以该前缀开头的环境变量注入到客户端代码中详见下文代码解析中对import.meta.env.VITE_TURSO_AUTH_TOKEN的使用。⚠️ 关于浏览器中令牌的安全警告README 特别强调在浏览器里使用VITE_TURSO_AUTH_TOKEN时该令牌会被打包进客户端代码任何加载你网站的人都能看到它。因此不要把这个令牌当作秘密对待只能把它用于可以安全暴露的数据库或角色例如只读、demo 实例。这一点从示例代码也能印证——令牌直接出现在import.meta.env.VITE_TURSO_AUTH_TOKEN中并随 bundle 分发本质上是一个公开凭据。四、核心代码逐段解析示例的所有逻辑都集中在 index.html 的script typemodule中全流程可以拆成四个部分。4.1 建立连接connect()import { connect } from tursodatabase/sync-wasm/vite; let db await connect({ path: sync-guestbook.db, // 本地数据库文件路径 authToken: import.meta.env.VITE_TURSO_AUTH_TOKEN, // Turso Cloud 认证令牌 url: import.meta.env.VITE_TURSO_DATABASE_URL, // Turso Cloud 数据库 URL longPollTimeoutMs: 5000, // pull 操作的可选长轮询间隔 });注意示例导入的是tursodatabase/sync-wasm/vite子路径。从 package.json 的 exports 声明可以看到该包针对不同打包器提供了多个入口./vitedevelopment 环境走dist/promise-vite-dev-hack.js否则走dist/promise-default.js、./bundle、./turbopack等。connect()接受的核心参数对应 types.ts 中DatabaseOpts类型参数类型说明pathstring本地存放同步数据库文件的路径注意同步数据库会以该前缀写出多个文件如xxx.db-info、xxx.db-wal等urlstring | (() string | null)远端数据库 URL如libsql://db-org.turso.io省略时创建纯本地数据库。也可以传入一个返回 URL 或 null 的函数实现“延迟启用同步”authTokenstring | (() Promisestring)认证令牌可以是静态字符串也可以是每次请求提供短期凭据的函数longPollTimeoutMsnumberpull 操作的长轮询超时时间设置后服务器会保持连接打开直到数据库出现新变更或超时示例设为 5000ms即 pull 循环中最多挂起 5 秒clientNamestring任意客户端名称库会追加唯一后缀保证 clientId 唯一remoteEncryptionEncryptionOpts云端数据库已加密时的远程加密参数如aes256gcm、chacha20poly1305等 ciphertransformTransform每次变更发送到远端前的回调可用于实现复杂冲突解决策略tracingerror | warn | info | debug | trace内部日志级别experimentalExperimentalFeature[]本地数据库要启用的实验特性如views、index_method、vacuumremoteWritesExperimentalboolean实验特性写语句直接在远端执行每次写后自动 pull 保证“写后读”一致pushOperationsThresholdnumber单个 push HTTP 批次最多打包的 CDC 操作数上限在事务边界拆分pullBytesThresholdnumber引导bootstrap阶段按字节拆分/pull-updates请求的提示logicalMvccPullboolean增量 pull 的协议覆盖开关true 强制 MVCC 逻辑日志流false 强制页流默认自动探测fetchtypeof fetch同步引擎所有 HTTP 请求的 fetch 替代实现可用于重试、超时、日志埋点、测试 mockpartialSyncExperimentalobject实验特性部分同步支持prefix启动时加载前 N 字节与query按 SQL 语句加载涉及的页两种 bootstrap 策略4.2 建表与查询exec()/prepare()/run()/all()// exec(...) 一次执行多条 SQL 语句 await db.exec( CREATE TABLE IF NOT EXISTS guestbook ( comment TEXT, created_at DEFAULT (unixepoch()) ); CREATE INDEX IF NOT EXISTS guestbook_idx ON guestbook (created_at); ); // 使用预编译语句参数用占位符 ? 绑定 const insert db.prepare(INSERT INTO guestbook(comment) VALUES (?)); await insert.run([text]); // run(...) 适用于只需执行到完成的语句 // all(...) / get(...) 分别获取全部行或单行 const select db.prepare(SELECT comment, created_at FROM guestbook ORDER BY created_at DESC LIMIT ?); const rows await select.all([5]);这套 API 与仓库中tursodatabase/database-wasm的用法一致参见 bindings/javascript/sync/packages/wasm/README.md 中的 in-memory / file-based / transaction 示例同步包只是在其上叠加了同步能力。4.3 同步循环pull()/push()/stats()示例中的同步采用两个独立的递归定时循环async function pull() { try { // 从远端拉取新数据 await db.pull(); await refresh(); } catch (e) { console.error(pull error, e); } finally { setTimeout(pull, 0); // 拉取完成后立即进入下一轮 } } async function push() { try { // 如果本地有新的待推送操作 if ((await db.stats()).operations 0) { // 把新数据推送到远端 await db.push(); } } catch (e) { console.error(push error, e); } finally { setTimeout(push, 10); // 每 10ms 轮询一次 } } pull(); push();这里的stats().operations是示例判断“是否有本地变更需要推送”的依据。在底层 promise-default.ts 中stats()会调用同步引擎返回DatabaseStats其字段定义在 types.ts包括cdcOperations尚未发送到远端的本地变更数量mainWalSize/revertWalSize主 WAL 与回滚 WAL 文件大小字节lastPullUnixTime/lastPushUnixTime最近一次成功 pull / push 的时间戳revision本地已拉取远端变更的不透明修订标识可作 etag但必须当作不透明字符串networkSentBytes/networkReceivedBytes累计网络收发字节数。pull()在引擎层面的实现promise-default.ts先执行engine.wait()等待并获取远端变更若变更为空则返回false否则执行engine.apply(changes)将变更应用到本地并返回true若设置了longPollTimeoutMs服务器会保持连接直到出现新变更或超时。push()同文件 L202-L207则直接调用engine.push()把本地 CDC 变更集上送远端。4.4 渲染与交互async function refresh() { const select db.prepare(SELECT comment, created_at FROM guestbook ORDER BY created_at DESC LIMIT ?); const rows await select.all([5]); // 渲染到 ul idcommentscreated_at 为 unix 秒乘以 1000 后格式化 ... } document.getElementById(form).addEventListener(submit, (e) { e.preventDefault(); addComment(); // prepare run 插入留言后调用 refresh() });页面即写即读addComment()写入本地后立刻refresh()展示无需等待云端确认——这正是 offline-first 体验的直接体现。五、关键前提COOP / COEP 跨域隔离头README 明确指出因为tursodatabase/database-wasm同步包的基础依赖SharedArrayBuffer开发环境和生产环境都需要设置跨域隔离响应头Cross-Origin-Opener-Policy: same-originCross-Origin-Embedder-Policy: require-corp浏览器只有在页面满足这两种跨域隔离条件时才会开放 SharedArrayBuffer 能力。下面给出三种场景的配置。5.1 Vite dev servervite.config.tsimport { defineConfig } from vite export default defineConfig({ server: { headers: { Cross-Origin-Opener-Policy: same-origin, Cross-Origin-Embedder-Policy: require-corp, } } })5.2 静态生产服务器server.mjsnpm run build后npm run serve会用该脚本托管dist/目录脚本基于 Node 原生http模块在每次响应前设置同样的两个头// COOP / COEP headers necessary for shared WASM memory res.setHeader(Cross-Origin-Opener-Policy, same-origin); res.setHeader(Cross-Origin-Embedder-Policy, require-corp);同时脚本为.html/.js设置了正确的 MIME 类型默认监听0.0.0.0:8080。5.3 Vercel 部署vercel.json如果部署到 Vercel需要添加vercel.json保证 COOP/COEP 头对/(.*)全部路径生效{ headers: [ { source: /(.*), headers: [ { key: Cross-Origin-Opener-Policy, value: same-origin }, { key: Cross-Origin-Embedder-Policy, value: require-corp } ] } ] }如果缺失这些头浏览器会因无法使用 SharedArrayBuffer 而无法运行同步引擎页面会在控制台报出相关的跨域隔离错误。六、源码视角浏览器端同步引擎是如何工作的从 promise-default.ts 的实现可以进一步理解示例背后发生的事1. 浏览器端的本地存储 IO。包内默认的BrowserIO直接用localStorage读写数据库文件L6-L19read()从localStorage.getItem(path)取数据并 UTF-8 编码为字节write()把字节解码后setItem回 localStorage对于纯内存数据库则使用Map实现的memoryIO()。这也是为什么示例中path只是一个逻辑文件名——它最终落在浏览器存储里。2. Web Worker 中的 WASM 引擎。index-default.ts 会启动一个名为turso-database-sync的 module Worker把sync.wasm32-wasi.wasm交给主线程初始化并导出connect、Database、SyncEngine等符号。同步、查询逻辑运行在 WASM 中避免阻塞主线程 UI。3. 连接时初始化引擎。构造Database时L91-L105会创建SyncEngine传入path、clientName、longPollTimeoutMs、pushOperationsThreshold、pullBytesThreshold等选项并组装Authorization: Bearer token请求头若配置了remoteEncryption还会附加x-turso-encryption-key/x-turso-encryption-cipher头。4. pull/push 的完整语义。pull()返回布尔值表示“是否拉取到了新变更”push()把本地 CDC 变更批量上送二者都通过run()把操作调度给同步引擎并用SyncEngineGuards保证并发安全。示例中“push 前先查stats().operations”正是为了避免无变更时的空推送。更底层的同步协议、CDC 与 WAL 实现可继续阅读仓库的 sync/engine 目录与 bindings/javascript/sync/srcRust 侧以及 docs/javascript-api-reference.md 中的完整 API 参考。七、常见问题与注意事项小结令牌暴露不可避免浏览器场景下 auth token 必然进入 bundle务必只用于只读或 demo 级数据库绝不可当作机密跨域隔离头三处都要配Vite devvite.config.ts、生产静态服务器server.mjs、托管平台Vercel 用vercel.json遗漏任何一处都会导致 SharedArrayBuffer 不可用离线优先的前提是本地持久化浏览器端数据库文件默认落在 localStorage源码中BrowserIO理解这一点有助于预判数据容量与清理行为pull 循环与长轮询示例中longPollTimeoutMs: 5000让 pull 在无新变更时挂起至多 5 秒配合setTimeout(pull, 0)形成持续监听同时避免空转同步 API 是数据库能力之上的扩展exec/prepare/run/all等 SQL 接口与普通tursodatabase/database-wasm一致pull/push/stats才是同步层新增的入口。总而言之sync-wasm-vite这个最小示例用不到百行代码完整演示了“浏览器内建库 云端同步 离线优先”的整套模式本地 Guestbook 写入即时可见push()将变更上送 Turso Cloudpull()把其他客户端的留言拉取回来而 COOP/COEP 头则是让这一切在浏览器里跑通的必要前提。你可以以此为模板把任意一个 Vite 前端应用改造成具备离线写入与云端同步能力的形态。【免费下载链接】tursoA SQL database in Rust: SQLite-compatible, now also speaking Postgres (experimental). The LLVM of databases.项目地址: https://gitcode.com/GitHub_Trending/tu/turso创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考