场景
我们希望用户把一个产物目录拖进页面就能直接查看,不需要启动服务器。目录里有 settings.json、lod/lod-meta.json,以及成百上千个分块文件和体素碰撞数据。
官方 supersplat-viewer 在加载时并不认为自己在处理本地文件。它会用 new URL(chunkRelPath, contentUrl) 拼出每个分块的地址,然后用 fetch、XMLHttpRequest 或 <img> 去请求。对于普通的 Blob URL,这些相对子路径无法解析。
思路:虚拟协议 + 四层拦截
我们注册一个虚拟协议 pkg://<rootId>/<相对路径>,把拖入的目录登记为一个根,并在查看器的生命周期内拦截四类请求入口:
window.fetchXMLHttpRequest.openHTMLImageElement.srcHTMLMediaElement.src
拦截后,请求的 pkg:// 地址会被映射回内存中对应的 File 或 Blob。官方代码发出的仍然是普通的 URL 请求,因此无需理解这个虚拟协议。
为什么是四层
只拦截 fetch 不够:官方的分块图片通过 <img> 加载,体素碰撞数据则可能通过 XHR 读取。每一层都需要单独覆盖,否则会有一部分资源仍然指向无效地址。把四层放在同一个桥模块里统一管理,便于在生命周期结束时全部还原。
不 Fork 的边界
这个方案的前提是官方包以黑盒 npm 依赖的形式引入。我们只使用它公开的入口:创建查看器的函数、查看器句柄、样式文件,以及 settings 的校验与迁移函数。
为了防止升级时悄悄破坏这些边界,项目中有一个 API 面审计脚本:它扫描源码实际用到的官方符号,生成一份清单。升级官方版本时,只需更新依赖,然后对照类型声明检查清单即可。维护成本基本集中在这份清单上,而不是散落在源码的修改里。
少数官方不支持的内容(如 GLB 网格、骨骼动画角色与程序化几何体)通过查看器暴露的应用对象挂入场景根,这同样属于公开接口的使用,而不是修改内部实现。
副作用与注意事项
- 生命周期要严格:拦截会改写全局对象,离开页面或卸载查看器时必须还原。否则同一页面中的其他代码会收到意外的请求结果。
- 加载揭示特效可关闭:查看器的加载揭示效果默认开启,可以通过
?noreveal参数关闭,便于调试。
小结
当官方组件不假设你的运行环境时,最稳妥的做法通常不是 fork,而是在它依赖的边界上做一层适配。这一层越薄、越有明确的入口与出口,升级成本就越低。