大模型
dev.to ·
@qnbs
同一套 React/Vite 产品横跨 PWA 与 Tauri:不假装两者完全相同
文章以 WorldScript Studio 为例,说明同一 React/Vite 代码库可同时部署为 GitHub Pages 站点、已安装 PWA、边缘托管应用和 Tauri 桌面应用,但这些运行面并非可互换。作者主张共享领域语义与互操作契约,通过显式适配器暴露平台能力,而非用统一抽象掩盖差异。文中给出各部署面的能力与限制对比:GitHub Pages 无法注入任意 HTTP 安全响应头,只能用 meta CSP 作为唯一执行点;Vercel/Cloudflare Pages 可设置真实响应头并运行同源边缘中继(如 api/claude-proxy.ts);PWA 存储仍受浏览器源限制;Tauri 桌面端提供文件系统持久化和原生 HTTP,但桌面项目文件目前未做静态加密。代码引用来自 commit 2d9157c0、release v1.28.8。
AI推荐理由 这是一篇有具体代码引用(commit 2d9157c0、release v1.28.8)和明确部署对比表的工程实践文章,讨论同一 React/Vite 代码库在 GitHub Pages、PWA、边缘托管和 Tauri 桌面端之间的能力与安全边界差异,并给出“共享领域语义、用显式适配器暴露平台能力”的可操作原则。其价值在于对跨端架构、CSP 头限制、存储与文件系统权限边界的实际经验总结,对做 AI 应用/桌面端/离线产品的开发者有参考意义。局限是案例来自单一项目 WorldScript Studio,属经验分享而非可复现的基准或官方规范,且未提供性能或安全性的量化验证。
62
正文
One React/Vite codebase can power a GitHub Pages site, an installed PWA, an edge-hosted deployment, and a Tauri desktop app.
That does not make those surfaces interchangeable.
This distinction matters whenever an application handles user projects, offline behavior, AI providers, filesystem access, or security policy. Shared components are valuable. Shared assumptions can be dangerous.
WorldScript Studio is a useful case study because its React/Vite application is intentionally available across browser/PWA and Tauri desktop environments. The product shares domain semantics, but it does not pretend that a browser tab and a native shell have the same authority boundaries. Code references are from the repository at commit 2d9157c0 (2026-09-28), release v1.28.8; simplified excerpts are labeled.
Share the product model, not every implementation detail
A cross-platform product needs a stable answer to questions such as:
What is a project?
What data belongs in it?
Which state is authoritative?
What does export mean?
What should happen when an AI provider is unavailable?
Those answers should be shared.
The mechanisms underneath them should not be forced to look identical.
For a PWA, the browser provides IndexedDB, Cache Storage, service workers, Web APIs, WebGPU, and origin-scoped storage. A desktop shell can provide application-data filesystem access, native networking, OS integration, and platform packaging.
Trying to hide every difference behind a single universal abstraction often creates a worse outcome: browser APIs leak into desktop code, native assumptions leak into web code, and the product accumulates several accidental definitions of persistence or network policy.
A better rule is:
Share domain semantics and interoperability contracts. Expose platform capabilities through explicit adapters.
The host changes the security boundary
The same static build can be deployed to different hosts, but hosting changes what the application can guarantee.
Surface
Important capability
Important limitation
GitHub Pages
Static public deployment
Cannot inject arbitrary HTTP security headers
Vercel / Cloudflare Pages
Response headers and edge-function capabilities
Still browser-origin storage for web project data
Installed PWA
Cached shell and browser-native installation
Storage remains origin- and browser-specific
Tauri desktop
Filesystem persistence and native HTTP
Desktop project files are not currently app-encrypted at rest
GitHub Pages is a good example of why "deployed from the same source" is not enough.
WorldScript uses a meta Content Security Policy there because GitHub Pages cannot add the corresponding response headers — the project's deployment documentation calls that meta tag the sole enforcement point on that host. Vercel and Cloudflare Pages can set real response headers that mirror the meta CSP, and both can run a same-origin edge relay for supported functionality (a Claude proxy lives at api/claude-proxy.ts for Vercel and functions/api/claude-proxy.ts for Cloudflare, sharing one core module). Those are materially different deployment guarantees, even when users see the same React interface.
The right documentation does not flatten that distinction. It names it.
Storage is an authority decision, not a convenience API
The PWA's live project path uses browser storage. The desktop path uses filesystem-backed stores under application data.
Both are local. They are not the same.
Browser persistence is governed by the browser's origin, quota, eviction behavior, and storage APIs. Desktop persistence is governed by filesystem access, native process boundaries, and the application's own read/write rules.
That difference becomes especially important for security language. Browser/PWA protected IndexedDB data can use the application's passphrase-based encryption lifecycle when configured. The filesystem-backed desktop project store currently does not receive that same at-rest encryption.
A UI toggle with the same name is not enough to make the protection equivalent. The actual persistence path decides what is protected.
Native networking changes what "local server" means
A browser connecting to localhost is still subject to browser rules such as CORS and Private Network Access. A Tauri desktop application can use an admitted native HTTP capability for local or cloud endpoints.
That does not mean desktop networking is automatically safer. It means its policy must be defined and enforced differently — and "narrowly admitted" is meant literally here. The desktop shell's HTTP capability is an explicit allowlist, not an open pipe:
// src-tauri/capabilities/default.json (excerpt)
{
"identifier": "http:default",
"allow": [
"http://localhost:*/*",
"http://127.0.0.1:*/*",
"https://generativelanguage.googleapis.com/*",
"https://api.openai.com/*"
// …remaining provider hosts, nothing else
]
}
Enter fullscreen mode
Exit fullscreen mode
At runtime, the fetch adapter picks its implementation by environment: in the Tauri runtime it dynamically loads the native HTTP plugin; everywhere else it uses the browser's fetch. For WorldScript, that desktop-native path supports local inference-server workflows such as Ollama-compatible endpoints without requiring the WebView to bypass browser-origin rules. The browser/PWA path should not silently probe local ports or pretend that the same route will work without user-managed server configuration.
The general lesson is simple:
Browser security restrictions are product constraints, not annoyances to work around.
Native capabilities should be narrowly admitted, not made globally available.
UI copy must say when a feature is desktop-only or depends on local server configuration.
PWA caching must not leak into desktop behavior
A service worker is a powerful browser feature, but it is not a universal application runtime.
WorldScript's service worker caches its web shell and handles offline fallbacks on web surfaces. In Tauri, the registration code takes the opposite path — it actively tears the browser mechanism down:
// register-sw.ts (excerpt — called only from the Tauri branch)
async function teardownServiceWorkerInTauri(): Promise {
if (!('serviceWorker' in navigator)) return;
const registrations = await navigator.serviceWorker.getRegistrations();
await Promise.all(registrations.map((reg) => reg.unregister()));
const keys = await caches.keys();
await Promise.all(
keys.filter(isWorldScriptOwnedCacheName).map((k) => caches.delete(k)),
);
}
Enter fullscreen mode
Exit fullscreen mode
It unregisters any existing service worker and purges the app's own caches. That prevents a stale browser cache from serving an offline fallback over the real bundled desktop application.
This is a subtle example of platform adaptation done well: the feature is not merely disabled because it is inconvenient. It is disabled because its browser lifecycle would be the wrong authority for a native bundle.
A cross-platform checklist
Before claiming that a web and desktop product are "the same app," ask:
Where does each platform persist the authoritative project?
Which platform controls headers, CSP delivery, and network policy?
Is offline behavior provided by a cached shell, a native bundle, or both?
Which integrations need browser permissions, CORS configuration, or native capabilities?
Does each storage path have the same encryption and recovery properties?
Are platform-specific boundaries visible in the UI and documentation?
A shared codebase is an implementation advantage. It is not a reason to erase the differences users and maintainers need to understand.
The goal is parity where the product contract is shared—and honesty where the platform changes the contract.
Source note: WorldScript Studio is open source (github.com/qnbs/WorldScript-Studio). Code references correspond to main at 2d9157c0 (2026-09-28); release anchor v1.28.8. Key files: register-sw.ts, index.html, vercel.json, docs/DEPLOYMENT.md, src-tauri/capabilities/default.json, services/ai/fetchAdapter.ts, api/claude-proxy.ts, functions/api/claude-proxy.ts. Part of the "Engineering WorldScript Studio" series.
AI disclosure: AI tools helped with repository research, structure, and editing of this article. I reviewed the technical claims against the referenced source files before publication.