WebAssembly 游戏开发技术专题全层级22 / 31 已发布

Wasm 包管理生态:WAPM、warg 与去中心化模块注册表的未来

· 0 分钟阅读
Wasm 包管理生态:WAPM、warg 与去中心化模块注册表的未来(WAPM, warg & the Future of Decentralized Module Registries) — WebAssembly 游戏开发技术专题

Wasm 包管理生态:WAPM、warg 与去中心化模块注册表的未来(WAPM, warg & the Future of Decentralized Module Registries)

为什么包管理是 Wasm 游戏生态的最后一公里

凌晨一点半,你把写了三个月的物理引擎编译成 wasm,1.2MB,Brotli 压完 380KB,跑起来比之前的 JS 实现快三倍多。你打开文档准备写「如何使用」。

第一行就卡住了。

用户怎么拿到这个文件?

发邮件?挂个 GitHub Release 让他手动下载?还是让他 clone 你的仓库、装 Rust 工具链、自己编译一遍?最后一个选项听起来荒谬,但 2019 年前后,前端开发者想用 WebAssembly,基本就是这么干的——读 README,照着命令行抄,中间少一个依赖耽误一小时。

很多人把包管理当成程序员的洁癖,觉得「不就是装一下」。不是。包管理是生态的血管。它决定了别人用你的东西是花三分钟还是三小时,决定了你的模块被十个人使用还是十万人使用。

游戏圈有句老话:做完不等于能卖。你把 Demo 打磨到 90 分,真正的胜负手都在后面那些脏活上——发行、渠道、SKU、退款、区域定价、平台审核。Wasm 生态现在正卡在这个位置:技术早就够用了,脏活没人愿意干。

这一篇我们聊 Wasm 的包管理:WAPM 为什么没做起来,warg 提了一个多漂亮的技术答案又为什么转向,Component Model 会把「包」这个玩意儿重新定义成什么样,以及——对你这种做游戏的人,这些破事到底跟你有什么关系。

先说结论,省得你往下翻:Wasm 专属注册表赢不了,未来也不会有一个「Wasm 的 npm」。包会长期依赖 npm、crates.io、OCI 镜像仓库。warg 那套内容寻址加签名的技术判断是对的,但它出现得太早、包装得太重、而且是在一个没有网络效应的地方从零修高速公路。

第一层:一个 .wasm 文件,离「包」还差五样东西

先搞清楚 wasm 文件本身是什么。它就是一段字节码,外加两张名单:一张是「我需要外面给我什么」,一张是「我能给外面提供什么」。就这样。

没有版本号。没有作者。没有依赖声明。没有「我提供的这个入口接受两个浮点数、返回一个向量」这种语义。没有签名,没有许可证,没有废弃说明。

所以一个裸 wasm 文件离「包」还差五样东西:

  • 身份:这堆字节是谁发布的?同名但不同来源怎么办?
  • 版本:新版本和旧版本能不能互换?升级会不会导致崩溃?
  • 依赖:除了这个文件我还需要别的吗?需要哪个版本?
  • 接口语义:我该怎么调用它,参数代表什么,返回值是什么单位?光有入口名字不够用。
  • 信任:这个文件在传输路上有没有被改过?

这五样东西全都可以靠文档和口头约定勉强应付,但迟早会出问题。区别只在于问题发生在你的机器上,还是发生在用户机器上。后者会让你半夜收到一封措辞礼貌但态度很不客气的邮件。

市面上有三种承载形态,各有各的活法:

承载形态典型代表能解决什么解决不了什么游戏团队的实际体验
裸文件 + CDN自己挂链接、GitHub Release最快上线,零依赖版本、签名、依赖全靠人手工记录三人以内的小项目,够用
语言包管理器npm、crates.io、NuGet版本、依赖、镜像、企业私服全现成接口语义缺失;跨语言使用者拿不到装完能用,但 wasm 和胶水版本容易对不上
Wasm 原生注册表WAPM、warg语言中立、内容寻址、签名网络效应难建,工具链要重学概念很美,实际包很少

我们先说最原始的那种。

裸文件加 CDN。大部分独立团队的第一版都长这样:构建产物传到对象存储,前面挂个 CDN,文档里固定写一个链接。三分钟搞定,零依赖,永远能跑。代价是版本全靠人手动记录——你更新了 wasm 但没有更新文档,用户拿着旧胶水去接新模块,报错信息是一句看不出所以然的「类型不匹配」。这种坑我踩过,不体面,但真实。

语言包管理器。这是目前 90% 的 Wasm 游戏模块真正待的地方。最典型的例子是物理引擎 Rapier。

Dimforge 这帮人用 Rust 写了整套刚体物理:碰撞检测、约束求解、关节、传感器、连续碰撞检测。他们没有做 JS 重写版,卖点就是「把 Rust 的物理核心直接提供给前端使用」。分发方式呢?在项目里装一个 npm 包,装完之后 node_modules 里是一个 wasm 文件、一层自动生成的 JS 胶水、一份类型声明。开发者写几十行代码就能造一个物理世界,加刚体、加碰撞体、丢进去掉,全都有了。

注意他们做了什么、没做什么。他们没有为 Wasm 单独建一个注册表。他们没让用户装 Rust。他们把 wasm 当成「构建产物」,跟一份 JS 包一起发到 npm 上。发布流程只有一条线:Rust 那边发版,npm 那边发版,同一条流水线跑完。

代价也很实在:包体积。Rapier 3D 的 wasm 未压缩是几 MB 量级,压完不到 1MB。还有一个兼容版本把 wasm 内联成 base64 塞进 JS 里,体积再涨三分之一,但你终于不用配 MIME 类型了。这是典型的工程妥协——用体积换「装上就能跑」。

还有个隐性代价:胶水和 wasm 必须严格使用同一版本。一旦有人从 CDN 上手动组合了两个不同版本的产物,就会出现难以排查的崩溃。

游戏行业其实早就干过一模一样的事,只是很多人没意识到。Unity 的 Package Manager 用的就是 npm 那套协议。清单文件格式跟 npm 一致,注册表走 HTTP,还支持一种叫「scoped registry」的东西——你可以自建一个仓库,在项目的清单里加一行地址,引擎就会去你那儿拉包。中间件团队卖插件、公司内部共享工具链,全是这么干的。

收益很清楚:版本锁定、全局缓存、按需下载,一整套可以继承来的基础设施。代价是连 npm 的毛病一起继承了。Unity 的依赖解析只有一个结果,两个插件依赖同一个库的不同大版本时,你只能看着其中一个被强行降级或者升级。还有个更适用于游戏场景的毛病——UPM 的包可以塞美术资源,一个带贴图和预制体的包动辄几百 MB,装一次卡三分钟。刚推出那阵子社区批评声音很大,因为改依赖得手写清单,后来补了图形界面才好点。

但方向是对的:没人愿意为「Wasm」这一层单独学一套工具。大家只想在自己熟悉的地方把包装上。

第三种就是 Wasm 原生注册表。名字很好听,现实很骨感。下一层我们专门说它。

第二层:WAPM——第一个吃螃蟹的人,也是第一个被夹的人

2019 年,Wasmer 团队搞了 WAPM。野心很大:既然 wasm 是语言无关的,那包管理也应该是语言无关的。npm 绑死 JS,crates.io 绑死 Rust,pip 绑死 Python——凭什么一个 C++ 编译出来的 wasm 和一个 Rust 编译出来的 wasm 不能放在同一个货架上?

它的设计现在看依然合理:

  • 包有命名空间,形如「作者名/包名」,早期还允许下划线开头的官方命名空间;
  • 包里可以装多个模块、命令行入口、静态资源,用一份清单文件描述;
  • 安装是全局的,装完就能在终端里直接运行,思路更像 Homebrew 而不是 npm;
  • 不关心你用什么语言写的,只关心产物是不是 wasm。

说句公道话,这套设计在 2019 年是超前的。那时候大家还在争论 wasm 能不能比得上 JS,Wasmer 已经在想「wasm 作为通用软件分发格式」了。

然后它就没有后续发展了。

我去翻过 WAPM 上的包,很多还是几年前的老版本,链接指向的仓库有的已经归档。Wasmer 公司的重心后来转到了边缘计算运行时,WAPM 更像一个仍然在线但基本没人维护的入口。

为什么?我觉得有四条,每条都值得你记下来——如果你哪天想给自己的游戏做插件市场,这四条会原样再来一遍。

第一,它和语言生态完全重叠。Rust 开发者有 cargo,C++ 开发者用 Emscripten 产出的是一堆 JS 加 wasm 的混合体,粘合逻辑在 JS 那一侧,你光把 wasm 装下来还得自己接线。AssemblyScript 的人本来就在 npm 里。也就是说,WAPM 提供的「语言中立」对绝大多数团队不是痛点,因为他们根本不需要跨语言,他们只需要在自己熟悉的领域里把事办了。

第二,注册表的第一性原理不是技术,是别人愿不愿意多按一次发布按钮。这句话我想重复一遍。一个包管理器的成败,不取决于它的格式设计得多漂亮,取决于维护者要不要为它多做一步。你做 Rust 库,把包发布到 crates.io 是本能反应;再让你去 WAPM 上注册账号、写第二份清单、跑第二次发布,你第二个月就懒得干了。人是有惰性的,生态是被惰性塑造的。

第三,清单里没有接口语义。WAPM 知道你依赖某个包的某个版本号,但它不知道你提供的入口长什么样、参数是什么类型。所以版本号升了,谁也判断不了能不能兼容,只能靠人祈祷。这是所有早期 Wasm 包管理的致命问题——它能描述「有哪些文件」,描述不了「这些文件对外承诺了什么」。

第四,命名空间是个隐患。开放注册的注册表,早期谁抢到好名字谁就是事实上的官方。一个第三方把「python」这个命名空间注册了,后面官方来了怎么办?收回?那叫抢夺。不收回?那会被人耻笑。npm 早年在这上面吃过亏,WAPM 也没躲过去。

给做插件平台的团队提个醒:你如果是给自家游戏做 Mod 平台,恭喜你,你没有网络效应问题——你的用户只有你的玩家,他们不得不用你的仓库。但如果你做的是给同行用的中间件,不要自建注册表。真的。你会花 80% 的时间在做鉴权、配额、垃圾回收、镜像同步、404 页面,然后发现根本没人来。

第三层:warg——把「不可篡改」做成包的第一属性

2022 年,字节码联盟(Bytecode Alliance)提了 warg。这次是认真在解决前面那些核心问题。

核心思路一句话:包的身份是内容的哈希,名字只是指向哈希的一个指针。

把这条想明白,后面全是推论。名字可以被抢注、可以被撤销、可以指向不同的东西;但哈希不会。同一个哈希,在任何地方、任何时间、任何人手里解开,都是同一堆字节。你没法在中间偷偷换掉一个字节——换了哈希就变了,客户端一验证就知道。

其实你早就在干这件事了,只是手动做。美术把一张贴图发给外包,回来之后你比对一下文件校验和,对得上才收货。这叫内容寻址,只是你用的是手动版。

warg 把它做成了制度:每一次发布是往一条只能追加、不能修改的日志上写一条记录,记录里有内容哈希、有发布者的签名、有上一条记录的哈希。客户端从头验证整条链,就能确认「这个包确实是这个人发的,而且中间没人动过」。

配合签名,还有几个很好的性质:镜像友好(任何 CDN 都能存,存的东西自己证明自己是真的)、可离线验证、联邦(多个注册表可以互相同步,任何一个挂了都不影响)。

这套东西对做游戏的人价值在哪?我举一个非常具体、能让人立刻感受到风险的场景。

你做了个支持玩家自制关卡的平台游戏。玩家写好关卡逻辑,编译成 wasm 传上来,你的服务器运行它。问题来了:这个模块从玩家电脑传到你的服务器,中间经过多少个环节?玩家的浏览器、他的网盘、你花钱买的 CDN、CDN 背后的对象存储。任何一个环节被篡改,运行在你的服务器上的就是别人的代码。

内容寻址加白名单能解决大部分问题:玩家第一次上传时你算哈希,之后只认这个哈希。中间任何一个 CDN 被换成了恶意版本,哈希对不上,直接拒绝。这就是为什么做 UGC 平台的团队应该关注 warg 的设计,哪怕你不用它。

顺便说一句,这条线跟本系列的第 20 篇(静态分析与安全审计)是连着的。哈希只能证明「它有没有变化」,证明不了「它有没有恶意」。如果一个模块从第一天就是恶意的,哈希再正确也还是恶意的。这两件事要一起做。

但故事到这里出现了一个转折。

2024 年,字节码联盟把 warg 的方向调整了:不再自己造一套全新的分发协议,转向复用 OCI 分发标准。也就是说,Wasm 模块被打包成 OCI artifact,放进全世界已经存在的那些镜像仓库里,用现成的工具推和拉。

原因不复杂,也不浪漫:OCI 背后已经有全世界所有的镜像仓库、CDN、鉴权体系、配额管理、垃圾回收、企业合规审计。你要的东西它几乎都有,虽然长得没那么优雅。

维度warg 的理想模型OCI 的现实方案
身份标识内容哈希 + 发布日志内容摘要 + 标签
签名日志链逐条签名Sigstore / cosign 附加签名
镜像同步联邦,多注册表互相同步任意镜像仓库直接复制
鉴权与配额需要从零建设容器生态十年积累
垃圾回收自己写仓库自带
上手成本一套新工具开发者可能已经会了

我当时看到这个消息,第一反应是有点可惜——warg 那套设计确实漂亮。第二反应是:技术上的优雅,经常输给运维上现成的方案。

这不是第一次了。历史上无数「更好的包格式」都因为这一点而失败。你造了一台更好的发动机,但全世界加油站都加另一种油。

对你最有用的结论是:如果你的游戏服务端要把 wasm 模块推到线上,直接用镜像仓库就行,不要自己搭建。打个 artifact、加个签名、推上去,Kubernetes 那套拉取、缓存、私有仓库、权限控制全都能复用。这条线在第 22 篇(Docker + WebAssembly)里展开得更细。

第四层:Component Model 把「包」从文件重定义成接口

前面三层都在聊「怎么把文件送到用户手里」。Component Model 换了个问题:为什么我们送的是文件,而不是承诺?

传统包管理分发的是实现。你装了一个库,你知道它叫这个名字、是这个版本号,但你不知道它到底承诺了什么。两个库能不能一起用,靠版本号加祈祷。所谓「依赖地狱」,本质上是「没人能机器判断兼容性」。

Component Model 的核心动作,是把接口单独拿出来定义。你先写清楚「一个物理组件应该提供哪些能力」——创建世界、加刚体、步进、查询碰撞。这份定义是独立的一等公民,有名字、有版本。然后任何人都可以实现它,编译成组件,声明「我实现了这个接口的这一版」。

这里有个特别关键的变化:接口的版本和实现的版本要分开。

接口加了一个可选参数,这是兼容的;接口把一个参数的类型从整数改成文本,这是不兼容的。而这个判断,工具能自动做,不需要人手动读变更日志。这才是版本号该有的样子。

对做游戏的人来说,这件事的意义在于「插拔」终于能落地了。

你可能听过无数次「我们的引擎是模块化的」。实际情况往往是:模块化只停留在编译期,更换物理引擎要重新编译整个工程,改一处接口要动十几个文件。Component Model 想做的,是让更换物理引擎变成更换一个组件——引擎只认接口,不认实现。网络同步层、寻路、对话系统、AI 推理,理论上都能这么插。

当然,理论到落地之间还隔着一整个工具链。目前的现实是:工具能用(wasm-tools 有组件相关的子命令,wit-bindgen 能生成各种语言的绑定),但没有任何一个主流商业游戏引擎深度接入了组件模型。Unity 没有,Unreal 没有,Godot 也没有。你在独立项目里试水是可以的,指望明年生产环境全上,不现实。

粒度分发的东西升级时怎么判断兼容适合谁
文件一个 wasm 字节流没法判断,靠人读文档单个小游戏、内部工具
模块带名字和版本的 wasm 模块看版本号,靠自觉引擎插件、中间件
组件实现了明确接口的组件接口可机械校验,工具能报错多人协作、要长期维护的平台

这张表其实是在告诉你一件事:你分发的东西越接近「承诺」,你的用户就越省心,但你自己要付出的设计成本就越高。大多数独立团队停在第二行就够了。只有当你真的要做平台、要让第三方给你写模块、要维护五年以上,第三行才值得投入。

参数化工具:一张表决定你的模块该走哪条路

下面这张表是我自己经历过问题之后整理的。不要把它当成标准答案,把它当成一面镜子——对照自己的项目,看看落在哪一档。

参数它决定什么建议区间 / 判断线
需要分发的 wasm 模块数量用不用得上注册表1–3 个:裸文件就行;4–20 个:用语言包管理器;20 个以上且跨语言:才值得考虑自建
外部使用者规模要对版本管理投入多少10 人以下:能跑就行;10–100 人:版本号加变更日志;100 人以上:签名加废弃策略
更新频率要不要内容寻址月更以下无所谓;周更以上必须内容寻址,不然 CDN 缓存会让你反复排查
是否有第三方或玩家提交信任模型复杂度没有:信任自己即可;有:内容寻址 + 白名单 + 沙盒,三件套缺一不可
单模块体积加载策略与压缩方案500KB 以下:直链;500KB–3MB:Brotli 加长缓存;3MB 以上:拆模块、按需加载
合规与审计要求选哪条分发链涉及支付、未成年人、平台审核:直接上 OCI 加签名,成本最低
团队已有基础设施迁移代价已有镜像仓库就选 OCI,已有前端体系就选 npm,千万别另造一套

三个关键阈值的解释:

一是 模块数量 20。低于这个数,你根本不需要注册表,一个目录加一份说明文档就够了。超过这个数,人脑记不住版本关系,才需要工具接管。

二是 周更。只要你的更新频率到周级,CDN 缓存就会开始影响你。这时候内容寻址不只是「优雅」,更是「保命」——因为你可以给每次构建一个唯一地址,缓存永远不会拿错版本。

三是 有没有第三方提交。这是唯一一条会让你的架构从「简单」跳到「复杂」的分界线。没有第三方,信任模型是「我信我自己」;有了第三方,你得做沙盒、白名单、哈希校验、撤销机制、审计日志。第 20 篇整篇都在讲这个。

初级路径:今天下午就能做完的三件事

如果你现在只有一个 wasm 模块要给别人用,别想架构,做这三件:

  • 把胶水和 wasm 绑定为同一版本。不要提供「随便拼接」的可能,包里带一份明确的版本对应表,或者干脆内联。
  • 加完整性校验。网页里引用脚本时有完整性属性,写上哈希,浏览器会帮你验证。一行配置的事,能阻止 CDN 中间被改包的情况。
  • 写清楚「这个包不负责什么」。文档里最值钱的往往不是「怎么用」,是「什么情况下不要用」。省下来的支持时间比你想象的多。

进阶路径:要做插件平台的人,顺序得反过来

如果你是要做 UGC 平台、要给第三方开接口,顺序弄反会带来严重后果。正确的顺序是:

先定接口,再定仓库,最后才定工具。接口是唯一不能随意改动的东西,仓库可以搬,工具可以换,接口一旦被第三方依赖,你就会被固定住。

具体说:先用接口定义语言把能力描述清楚,冻结第一版;然后选一个内容寻址的存储方案(OCI 仓库最快,现成的鉴权、配额、垃圾回收全有);最后才是给开发者写发布工具。工具是最不值钱的一环,随时可以重写,不要在工具上花三个月。

还有一条经验:在仓库这一层加一层简单的抽象。今天你可能用 npm,明天可能因为企业合规被迫搬到 OCI,如果所有代码都直接依赖某一个仓库的接口,迁移成本会让你难以承受。留一层适配,不丢人。

常见坑:四个我见过真人掉进去的

坑一:把 wasm 当静态资源扔 CDN,然后版本对不上。胶水是上一版,wasm 是这一版,报错信息是一句「内存访问越界」。你会花两天排查一个不存在的内存问题。记住:wasm 和胶水是一个整体的两半,永远一起发布、一起缓存、一起失效。

坑二:以为版本号对 wasm 有效。版本号是给人看的约定,不是机器能验证的东西。你改了内存里的数据布局,从「结构体里多加一个字段」这种角度看,版本号只升了个小版本,但实际上所有旧的调用方全部会崩溃。在接口能被机器校验之前,版本号永远只能当作约定。

坑三:以为哈希等于安全。哈希证明的是「没变」,不是「没毒」。一个从第一天就写着恶意逻辑的模块,哈希完美正确。内容寻址解决的是传输链路的信任,解决不了发布者的信任。这两件事必须分开建模,别混着谈。

坑四:自建注册表的幻觉。「我们就发几个包,搭个简单的服务器就行」。然后你会依次需要:账号体系、命名空间冲突处理、版本撤销、垃圾回收、镜像同步、断网降级、迁移方案、废弃公告页。这些东西加起来的工作量,比你的游戏本体还大。而且做完之后你会发现,用户还是习惯从 npm 装。

FAQ

Q:我用 Rust 加 wasm-pack,还需要关心 WAPM 或者 warg 吗?

不用,至少现在不用。你的产物大概率应该发到 npm 或者你们公司自己的镜像仓库。wasm-pack 默认产出的就是一个 npm 包结构,这是经过市场验证的路径。warg 的设计思想值得读——尤其是内容寻址和签名那套——但你不需要为了用而用。

Q:玩家做的 Mod,用 npm 分发靠谱吗?

技术上能跑,运营上不靠谱。npm 的账号体系和你的游戏账号体系是两套东西,你没法做「这个 Mod 属于这个玩家」的绑定,也没法在你自己的后台做审核和下架。更现实的做法是你自己做一个上传入口,后端用对象存储加内容哈希做寻址,前端只需要拿到一个哈希列表。听起来像自建注册表?不完全一样——你不需要「注册」,你只需要「存」和「查」。

Q:warg 是不是已经死了?现在该关注什么?

与其说死了,不如说它换了条路走。它的核心主张——内容寻址、签名日志、联邦镜像——正在被 OCI 生态吸收。所以你现在该关注的是:怎么把 wasm 模块打成 OCI artifact,怎么在流水线里加签名,怎么在拉取端做校验。这套东西的工具链成熟度比 warg 高一个数量级,而且你公司的运维人员多半已经会了。

Q:Component Model 现在投入,会不会太早?

分情况。如果你在做的是一个要运行五年的插件平台,现在就该把接口设计按组件的思路来做,哪怕暂时用不上工具链——因为接口设计欠下的技术债最难偿还。如果你的项目明年就要上线,别碰,用最原始的方式把东西发出去,用户不关心你的包是模块还是组件。

Q:为什么 Wasm 的包管理折腾了这么多年,还是这个样子?

因为包管理从来不是技术问题。它是一个「大家都去哪儿」的问题。技术方案可以在一周内做出来,网络效应要五年。Wasm 生态现在最缺的不是更好的注册表,是足够多的、非用不可的模块——让开发者觉得「我不用这个仓库就少了关键能力」。

所以我想留一个反直觉的判断给你:Wasm 包管理的未来,可能根本不会长成「包管理」的样子。它更可能是你游戏引擎里的一行配置,指向一个你从来没听说过的镜像仓库,后台自动完成版本解析和签名校验,你全程看不到这些细节。就像今天没人会专门打开一个「字体管理器」去安装字体,字体就那么在系统里了。

真正的问题不是「我们该用哪个注册表」,而是「我们什么时候才能不再需要谈论注册表」。你觉得还要几年?

文章标签
WebAssemblyWasmEmscriptenWASIwasm-bindgenComponent ModelSharedArrayBufferAtomicsWasm 多线程Wasm SIMDwasm-packRust Wasm
更多专题全部专题
觉得有价值?点赞或收藏支持内容持续产出。
← 返回专题:WebAssembly 游戏开发技术专题