- 前端
- 通信
【免费下载链接】decimen-optical-transfer
本文以
docs/user/install-and-offline.md为骨架,结合 package.json、vite.config.ts 与 build/ 目录下的构建插件源码,系统讲解 Decimen Optical Transfer 的三种交付形态(托管站点、单文件 HTML、Demo 模式)的安装、离线能力与限制,以及开发服务器为何强制 HTTPS。读完你可以根据"是否需要服务器、是否必须离线、部署在什么设备上"三个维度,为实际场景选对交付形态,并能在不联网环境下完成安装与传输。
Decimen Optical Transfer 是一个用"屏幕 + 摄像头"在两台设备间传输文件的开源项目:一端把文件编码为无限滚动的动画二维码流,另一端用摄像头拍摄并重建文件,设备之间没有任何网络路径(详见 README.md)。正因为"没有网络"是它的核心卖点,安装与离线就成了用户最关心的部分。同一个源码可以构建出三种形态,每种形态的离线能力、依赖条件和适用场景各不相同。
三种形态一览:同一源码,三种交付
项目所有构建产物都来自同一份源码(三个页面 + 共享模块),每次 release 都会附带三种形态的构建产物。官方文档用一张表概括了三者的关键差异:
| 形态 | 是什么 | 需要服务器? | 离线能力 |
|---|---|---|---|
| Hosted site(托管站点) | 三个页面加一个 Service Worker,在线托管于 decimen.app | 需要,任意静态托管均可 | 首次访问之后即可离线 |
decimen-sender.html | 单个文件,约 55 KB | 不需要 | 始终离线 |
decimen-receiver.html | 单个文件,约 1.3 MB | 视情况而定(见下文"唯一的例外") | 始终离线 |
三种形态分别由npm run build(托管站点)、npm run build:standalone(两个单文件)产出,npm run build:all则一次性构建全部产物。下面逐一展开。
Hosted site:PWA 安装与离线
托管站点形态是三个页面加一个 Service Worker,即一个标准 PWA(Progressive Web App)。它的离线逻辑很简单:站点预缓存了一切资源,包括解码器 wasm——只要完整加载过一次,之后断网也能正常工作。这一点对接收端尤其关键,因为接收端需要解码 wasm(见下文),而vite.config.ts中 PWA 的globPatterns: ["**/*.{js,css,html,wasm,png,svg}"]明确把 wasm 纳入了预缓存范围。
值得一提的细节是:从任意页面加载都会缓存整个应用。即使你通过别人分享的链接直接落到/receive/,也会把整个应用(含发送页资源)全部缓存下来。
把它"安装"到主屏以获得全屏应用体验:
- Android— 在 Chrome 菜单里选择Install app(安装应用)。该项目提供了真实的 manifest 和规范图标(见
public/目录下的icon-192.png、icon-512.png、icon-maskable-512.png),Android 要求 192 与 512 两种尺寸且尺寸信息真实,才会认为应用"可安装"。 - iOS— 分享菜单 →Add to Home Screen(添加到主屏幕)。
文档特别强调:这是手机上最推荐的形态。因为托管站点保留了真实的https://origin,而"摄像头 API 只存在于安全上下文"(详见后文"为什么开发服务器强制 HTTPS"一节),这正是摄像头所要求的条件。
Service Worker 的两个工程细节(源码佐证)
vite.config.ts的 PWA 配置中有两处值得注意的实现:
clientsClaim: true:配合registerType: "autoUpdate",新版本 Service Worker 会立即接管已打开的页面。源码注释解释了原因:没有clientsClaim时,已打开的标签页仍绑定旧 Worker,会出现"硬刷新能看到新内容、普通刷新又变回旧内容"的诡异现象。maximumFileSizeToCacheInBytes: 4 * 1024 * 1024:workbox 默认单文件缓存上限是 2 MiB,而仓库里的演示图片success-2mb.png恰好是 2 MiB,处于边界上;显式抬到 4 MiB 既消除边界问题又留出余量。runtimeCaching中的received-media缓存:接收端收到的媒体会写入 Cache API(对应receive/main.ts中的servableMediaUrl),配合rangeRequests: true支持 Range 请求,因为 iOS Safari 对blob:URL 的视频/音频播放不可靠,但 WebKit 的媒体加载器接受带 Range 的 HTTP 响应。
另外,build/root-pwa-head.ts这个插件解决了两个 PWA 常见坑:一是 manifest 与sw.js位于站点根目录,而从/send/、/receive/子路径引用会 404,插件会按页面深度自动改写为../前缀的引用;二是它注入自定义的 Service Worker 注册脚本,处理SKIP_WAITING握手与controllerchange后的自动刷新,保证新版本 Worker 不会一直停在"waiting"状态。
Standalone files:两个零外部依赖的单文件
npm run build:standalone产出两个内部没有任何外部内容的页面——没有<script src>、没有样式表、没有fetch。这意味着它们不依赖任何 CDN、字体、样式资源,适合"把文件邮件发出去"或"拷进 U 盘"这种纯离线分发场景。
体积差异的根源在于:接收端把 940 KB 的解码 wasm 以data:URI 内嵌进了页面,因此单文件达 1.3 MB;发送端没有 wasm,所以只有约 55 KB。这个内嵌由 build/inline-codec-wasm.ts 完成:Vite 的?inline对.wasm无效(Rollup 会把二进制当 JavaScript 解析),所以该插件读取vendor/decimen-codec/decimen_codec.wasm并转成 base64 的data:application/wasm;base64,...字符串,通过虚拟模块注入。
单文件形态的构建管线(源码佐证)
vite.config.ts中standalone-send与standalone-receive两个 mode 走同一套构建逻辑,产物重命名后落到dist-standalone/。关键插件链包括:
useInlineVariants(build/use-inline-variants.ts):把worker-factory、wasm-url、support三个模块在解析期替换为对应的.inline.ts变体(如 receive/worker-factory.inline.ts 用?worker&inline把 Worker 打包成 base64 blob URL——因为从file://页面加载 module worker 会被 opaque origin 阻止)。之所以用解析期替换而非运行时分支,是因为 inline 形态都有模块级副作用,Rollup 会把死分支也保留下来。viteSingleFile(vite-plugin-singlefile):把所有 JS/CSS 内联进 HTML。standaloneCsp(build/standalone-csp.ts):给单文件页面注入default-src 'none'的严格 CSP,让"没有网络"从意图变成可执行——任何依赖项改动导致偷偷外联时,浏览器会直接拒绝而不是在气密隔离的机器上静默外联。文档还解释了'wasm-unsafe-eval'是必需的(否则WebAssembly.instantiate被 CSP 直接封死,解码器永远无法启动),以及connect-src data: blob:的原因(Emscripten 会 fetch data: URI 的 wasm,解码 Worker 是 blob: URL)。emitAs(build/emit-as.ts):把send/index.html、receive/index.html重命名为易记的decimen-sender.html、decimen-receiver.html。
接收端的唯一例外:file://下的 opaque origin
这是整篇文档里最重要的一个限制,务必记住:
从
file://打开接收页时,页面获得的是opaque origin。桌面 Chrome 和 Firefox 一般会弹出摄像头授权并正常工作;但iOS Safari 和 Android Chrome 不会把摄像头给一个本地文件。
由于接收端通常是手机,官方建议:通过任意 http(s) 服务来提供这个文件,或者改用托管站点的离线模式。而发送端没有这个问题——它在file://下任何平台都能工作(发送端不需要摄像头,只涉及渲染二维码)。
顺带一提:单文件接收页的 CSP 里img-src data: blob:、media-src blob: mediastream:、worker-src blob:等条目,正是为了支撑这种"零外联"运行而配置的。
Demo 模式:无人值守的演示机
npm run demo # sender locked to the two bundled imagesDemo 模式把发送端锁定到两个内置演示图片上:没有文件选择器、没有文本框——适合"一台发送机摆在人群前无人值守"的展台场景。从源码看(send/main.ts),DEMO = import.meta.env.VITE_DEMO === "1",demo、diagnostics、benchmark 三个模式共用这套开关逻辑,demo 模式下send/main.ts会跳过文件选择与文本输入流程。
文档给出了一个非常重要的安全定位:这是VITE_DEMO=1的 dev server,不是加固的 kiosk——任何有键盘的人都能打开 devtools。它只是"演示模式",不是"防篡改模式";如果确实需要无人值守且防止被操作,需要额外的加固手段。
注意:Demo 模式是 dev server 形态(
vite --mode demo),不是独立交付物;离线场景请用上面的单文件发送页。
为什么开发服务器强制 HTTPS
这是新手最容易踩的坑,官方文档解释得非常清楚:
接收端使用getUserMedia(摄像头 API),而浏览器在非安全来源上会直接移除这个 API——手机通过纯 http 访问你的 dev server 时,根本没有摄像头,彻底不可用(localhost是例外,但手机不是 localhost)。
因此 dev server 默认启用 HTTPS:
- 开发服务器自带自签名证书(由
@vitejs/plugin-basic-ssl插件在vite.config.ts中提供)。 - 手机首次访问时需手动信任证书:iOS 上是Show Details → visit this website,其他平台是Advanced → Proceed。
- 点过一次之后,页面即处于安全上下文,摄像头即可正常工作。
从源码看,vite.config.ts中 dev server 与 preview 都设置了host: true,以便局域网内的手机能访问开发服务器或npm run serve预览的生产构建。结合前文:托管站点形态天然是https://真实 origin,所以手机端最省事;单文件接收页则要么走 http(s) 服务、要么用托管站点离线模式,才能拿到摄像头。
实操:把三种形态跑起来
在仓库根目录执行:
npm install # 安装依赖 npm run dev # HTTPS 开发服务器(带 HMR) npm run serve # 构建后预览生产包 npm run demo # Demo 模式:只能发送内置的演示负载 npm run build # 托管站点 → dist/ npm run build:standalone # 两个自包含单文件 → dist-standalone/ npm run build:all # 全部构建离线分发的典型路径是:npm run build:standalone后,把dist-standalone/decimen-sender.html与decimen-receiver.html拷到 U 盘、邮件附件或内网 http 服务上,即可在没有公网的环境完成两端部署;其中发送端可以直接双击从file://打开,接收端建议经 http(s) 提供(见上文 opaque origin 限制)。
小结:按场景选形态
| 你的场景 | 推荐形态 | 原因 |
|---|---|---|
| 日常手机互传、想装到主屏 | 托管站点(PWA) | 真实https://origin,摄像头可用,首次访问后离线 |
| 发送端在气密/离线环境 | decimen-sender.html | 55 KB 单文件,file://随处可用 |
| 接收端在气密/离线环境 | decimen-receiver.html经 http(s) 提供,或托管站点离线模式 | file://在移动端无摄像头权限 |
| 展台、演示机无人值守 | npm run demo | 锁定内置演示内容,但非加固 kiosk |
继续深入可参考:架构文档、构建与发布文档、平台怪癖文档(其中详细记录了getUserMedia相关的 iOS/Android/Safari 细节)。
- 前端
- 通信
【免费下载链接】decimen-optical-transfer
相关推荐
Aptos 执行器(Executor)深入解析:从区块执行到状态提交的完整技术指南
Aptos 执行器(Executor)深入解析:从区块执行到状态提交的完整技术指南 导读 Aptos 是一条 Layer 1 区块链,其核心架构是一个 复制状态
前端通信TerminusDB高级功能解析:WOQL Datalog与目标搜索
TerminusDB高级功能解析:WOQL Datalog与目标搜索 TerminusDB是一个创新的分布式数据库,以其独特的协作模型和强大的查询能力在数据管理
数据库知识图谱后端如何快速切换游戏DLSS版本:DLSS Swapper让升级降级回退一步到位
如何快速切换游戏DLSS版本:DLSS Swapper让升级降级回退一步到位 游戏更新后画面开始闪烁、帧数莫名下滑,你折腾完驱动又调遍游戏内设置,最后才发现是它
桌面应用
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考