WASI 0.3 + 组件模型:WebAssembly 终于迎来自己的「Docker 时刻」——一次把 Wasm 云原生化讲透(2026 深度实战)
一句流传很广的话:Docker 联合创始人 Solomon Hykes 说过,如果 2008 年就有 WebAssembly,他们根本不需要发明 Docker。可十几年过去,Wasm 在浏览器里所向披靡,在云端却始终「叫好不叫座」。转折点就在 2026:Wasm 3.0 把 64 位内存和垃圾回收送进标准,WASI 0.3 又把异步(async)彻底内化进组件模型(Component Model)。本文用一套能跑的代码,把「为什么 Wasm 现在才真正具备替代容器的潜力」讲透。
一、背景:被耽误了十年的「Docker 时刻」
2015 年前后,容器用一张「轻量、隔离、可移植」的船票把整个云原生行业送上了快车道。但它的底层始终没摆脱操作系统:每个容器再怎么轻,也是跑在一个裁剪过的 Linux 用户态里,共享宿主内核,启动要 fork 进程、要挂载 namespace、要初始化 libc。冷启动再快也快不过「创建进程」这个物理事实。
WebAssembly 从一开始就走了一条完全不同的路。它不是「一个更小的 Linux」,而是一套可验证的字节码 + 结构化控制流 + capability 式系统调用。字节码在加载时会被线性扫描验证,控制流只能是线性跳转和结构化块(没有任意 jmp),内存是边界清晰的线性内存(linear memory),宿主调用靠显式的 import/export 表。这套设计天然适合「不可信代码的安全多租户执行」——换句话说,它生来就是为云原生准备的。
那为什么 Wasm 在云端一直没火?答案很现实:Wasm 1.0(2019)只解决了「计算」,没解决「怎么和系统打交道」。
- 浏览器里的 Wasm 靠 JS 互操作访问 DOM、fetch、Storage,链路清晰;
- 但到了服务端,Wasm 模块需要读文件、开 socket、读环境变量、打日志——这些在 Wasm 1.0 里完全没有标准。
于是社区先用 wasi_snapshot_preview1(后来被称为 WASI Preview 1)顶上:它把 POSIX 式的一坨函数(fd_read、path_open、proc_exit……)用 C ABI 暴露给 Wasm。能用,但问题很大——它是单模块、单世界、无类型、无组合能力的;你没法把一个 Rust 写的函数和一个 Go 写的函数优雅地拼成一个应用;异步只能靠宿主线程阻塞;更严重的是,它和「组件」概念完全脱节。
2025 年 10 月,Wasm 3.0 正式发布,带来了两个硬核能力:memory64(64 位线性内存) 和 GC(垃圾回收)提案。前者让单个 Wasm 实例能寻址超过 4GB 的内存,后者让带 GC 的语言(Go、Java、C#、Kotlin)能高效地编译进 Wasm,而不必把整个运行时和标记-清除器塞进线性内存。但当时有篇标题很精准的文章写道:「Wasm 3.0 发布,64 位内存、垃圾回收全都有,先别激动,组件模型还没做完。」
真正的临门一脚,是 WASI 0.3。2026 年 3 月,WASI 小组投票批准 WASI 0.3.0,最关键的一句变更说明是:
「WASI 0.3 is official, and async is now native to WebAssembly Components.」
(WASI 0.3 正式发布,异步从此成为 WebAssembly 组件的原生能力。)
这意味着组件模型终于把 async、 future<T>、stream<T> 做成了一等公民,WASI 也顺势从「自己攒了一套 async 杂技」回退到「直接站在组件模型的异步原语之上」。本文认为:到这一步,WebAssembly 才真正补齐了「服务端可移植执行单元」的最后一块拼图,那个被预言了十年的 Docker 时刻,算是真的来了。
二、核心概念:把 WASI 的三个时代讲清楚
要把「现在为什么不一样」说透,必须先厘清 WASI 的三段演进。很多人对 Wasm 服务端的印象还停留在 Preview 1,那是认知偏差的最大来源。
2.1 Preview 1(wasi_snapshot_preview1):能用,但只是「系统调用翻译层」
Preview 1 的接口长这样(WIT 描述,仅为示意):
// 这是 Preview1 风格的抽象,实际是扁平的 C 函数
interface wasi:cli/stdout@0.2.0 {
// 直接写 stdout,没有缓冲、没有类型
fd_write: func(fd: fd, iovs: list<iovec>) -> result<size, errno>;
}
问题清单:
- 无类型契约:参数全是
i32/u64句柄和裸指针,跨语言靠各自手写绑定,容易错。 - 无组合:一个 wasm 文件就是一个孤立模块,想复用别人写的能力只能重新编译链接。
- 异步 = 阻塞:要读一个 socket,要么阻塞宿主线程,要么手动
poll一堆pollable,把控制权交还给宿主,编程模型非常别扭。 - 无资源生命周期:文件、流这些「资源」没有结构化的 ownership,全靠
fd整数硬管。
2.2 WASI 0.2(Preview 2):组件模型登场,但异步仍「夹生」
WASI 0.2 第一次把整个标准建在 Component Model 之上。world、 package、 interface、 resource 这些概念成为一等公民,WIT(WebAssembly Interface Type)作为 IDL 描述跨语言契约。它的异步范式是:
package wasi:io@0.2.0;
interface stream {
// 读一块字节,可能还没就绪
read: func(len: u64) -> result<list<u8>, error-code>;
// 订阅这个流,返回一个 pollable 句柄
subscribe: func() -> pollable;
}
interface poll {
// 阻塞等待某个 pollable 就绪
poll: func(in: list<pollable>) -> list<u32>;
}
resource pollable {
// 轮询是否就绪
ready: func() -> bool;
}
看到问题了吗?异步是「外包」出去的:你得先 read 拿数据,再 subscribe 拿 pollable,再 poll 等它,再回去 read。一套 IO 操作被拆成四五个来回,宿主 runtime 要在 C 栈和 Wasm 栈之间反复切换。WASI 0.2 的文档自己也承认这是一个「acrobatics(杂技)」式的妥协——因为那时的组件模型还没有原生异步。
2.3 WASI 0.3:异步成为组件模型的原生公民
组件模型补上了 async 函数、future<T>、stream<T> 这些类型后,WASI 0.3 把上面的 wasi:io 整个重写:
package wasi:io@0.3.0;
interface stream {
// 直接返回一个 future,调用方 await 即可,无需 pollable 杂技
read: func(len: u64) -> future<result<list<u8>, error-code>>;
}
interface task {
// 组件模型原生的任务/并发原语
spawn: func(f: func()) -> future<_>;
}
对比一下两种写法的体感(以宿主侧 Rust 为例):
// ===== WASI 0.2(夹生异步)=====
let pollable = stream.subscribe(); // 1. 订阅
pollable.block(); // 2. 阻塞等就绪
let chunk = stream.read(4096)?.unwrap(); // 3. 再读
// ===== WASI 0.3(原生异步)=====
let chunk = stream.read(4096).await?; // 一个 await 搞定
这才是「Docker 时刻」真正成熟的技术信号:以前 Wasm 在云端最被诟病的「IO 模型不顺手、并发能力弱」,在 0.3 里被系统性解决了。组件可以自然地 await 网络、文件、消息,宿主 runtime(如 wasmtime)负责在底层用真正的异步 reactor 调度,Wasm 栈和宿主栈之间不再频繁打断。
2.4 组件模型与 WIT:跨语言协作的真正契约
很多人把 WIT 简单理解为「Wasm 的 IDL」,这是低估了它。WIT 解决的问题是:让任何语言编译出的组件,都能在二进制层面互相调用,且不需要共享同一个运行时。
一个最小可用的 WIT 契约:
// greeter.wit
package example:greeter@1.0.0;
interface greeter {
/// 生成一句问候语
greet: func(name: string) -> string;
}
world greeter-world {
// 本组件对外导出 greeter 能力
export greeter;
}
world 是「一个组件的完整能力边界」:它声明这个组件 import(依赖宿主/其他组件提供)什么,又 export(对外提供)什么。package 是命名空间与版本(@1.0.0)。interface 是能力分组。resource 则可以描述带生命周期的状态对象(比如数据库连接)。
关键在于 Canonical ABI。当 Rust 组件 export greeter,而 JS 组件 import greeter 时,两边并不直接交换语言对象,而是交换经过 Canonical ABI 编码的「规范值」:字符串编成 (ptr, len),list 编成 (ptr, len, cap),variant/result 编成带标签的联合。这套 ABI 与语言无关、与字节序无关,保证「Rust 写的导出,Go 写的导入能零胶水对接」。这正是容器做不到的——容器之间只能靠网络协议(HTTP/gRPC)通信,而组件之间是进程内、零拷贝、类型安全的函数调用。
三、架构分析:组件 vs 容器,到底差在哪
理解差异,才不会把 Wasm 当成「又一个容器」。
3.1 隔离发生在哪一层
| 维度 | 容器(Docker) | WebAssembly 组件 |
|---|---|---|
| 隔离基础 | Linux namespace + cgroup(内核级) | Wasm 线性内存 + 字节码验证(语言运行时级) |
| 是否共享内核 | 共享宿主内核 | 不直接接触内核,全部经宿主 runtime 中转 |
| 启动代价 | 创建进程、挂载 FS、初始化 libc(数十~数百 ms) | 实例化字节码 + 验证(亚毫秒~毫秒级) |
| 体积 | 镜像几十 MB~数 GB | 组件通常几 KB~几 MB |
| 系统调用 | 直接 syscall 宿主内核 | 全部经 import 表,宿主可逐项拦截/鉴权 |
| 攻击面 | 内核 + 容器运行时 | 仅宿主 runtime 的 import 实现 |
| 跨语言组合 | 只能通过网络 | 进程内直接函数调用 |
一句话:容器的隔离是「操作系统给你的」,组件的隔离是「字节码格式保证的」。前者强但重,后者轻但需要宿主 runtime 足够靠谱——而 wasmtime 这类引擎经过多年审计,已经可以放心承载多租户。
3.2 组件如何「组合」成一个应用
容器编排应用靠的是「起一堆容器 + service mesh 串联」。组件的组合则在编译/链接期就完成了。假设我们有两个组件:
auth.wasm(Rust 写):exportauth: func(token: string) -> boolbilling.wasm(Go 写):importauth,exportcharge: func(...) -> result<_, _>
组合时,我们用一个「宿主壳」把 auth 的 export 接到 billing 的 import 上,再用 wasm-tools compose 产出最终组件。整个过程是静态链接式的,运行期没有任何网络往返。这是微服务架构梦寐以求的「把网络调用变成函数调用」。
# 把 auth 的能力注入 billing,得到 billing-composed.wasm
wasm-tools compose billing.wasm \
--adapt auth=billing.wasm \
-o billing-composed.wasm
(示意命令,--adapt 是 wasm-tools 提供的适配/组合机制,真实参数以你所用的 wasm-tools 版本为准。)
3.3 WASI 0.3 的异步运行时架构
当组件里写下 stream.read(4096).await,背后发生了什么?
- 组件调用被编译成对
future类型的subscribe; - 宿主 runtime(如 wasmtime 的 async 支持)返回一个真正的 async task 句柄;
- runtime 把底层 IO 交给宿主的 reactor(tokio / async-std / 自研 epoll),Wasm 实例在 await 点被挂起,让出执行权;
- IO 就绪后,runtime 恢复该组件实例的栈,把结果填进
future; - 组件从
await之后继续执行。
关键点:挂起的是 Wasm 实例,不是 OS 线程。所以你可以用几十个 OS 线程驱动上万个并发组件实例,密度远超「一连接一线程」的容器模型。这对 Serverless、边缘函数、AI Agent 沙箱是降维打击。
四、代码实战:从零写一个能跑的组件栈
下面用一个完整例子把上面所有概念落到代码。技术栈:Rust + cargo-component + wasmtime + Fermyon Spin。全部可本地复现。
4.1 第一步:用 WIT 定义契约
// wit/greeter.wit
package example:greeter@1.0.0;
interface greeter {
/// 根据名字生成问候语
greet: func(name: string) -> string;
}
world greeter-world {
export greeter;
}
再加一个异步版契约,体现 0.3 的能力:
// wit/fetcher.wit
package example:fetcher@1.0.0;
interface fetcher {
/// 异步抓取远程内容,返回 future
fetch: func(url: string) -> future<string>;
}
world fetcher-world {
export fetcher;
}
4.2 第二步:用 cargo-component 实现一个 Rust 组件
cargo-component 是 Bytecode Alliance 提供的高层工具,把 wit-bindgen 的流程封装成 cargo 子命令,让你像写普通 Rust crate 一样写组件。
# Cargo.toml
[package]
name = "greeter"
version = "1.0.0"
edition = "2021"
[dependencies]
# wit-bindgen 的运行时与宏
wit-bindgen-rt = { version = "0.3", features = ["bitflags"] }
// src/lib.rs
// 让 wit-bindgen 根据 world 生成 Rust trait 与绑定
wit_bindgen::generate!({
world: "greeter-world",
});
// 实现 WIT 里声明的 greeter 接口
struct Greeter;
impl exports::example::greeter::Greeter for Greeter {
fn greet(name: String) -> String {
// 这里可以安全地调用宿主 import 的能力
format!("Hello, {name}! 这条消息由 WebAssembly 组件生成。")
}
}
// cargo-component 的入口宏:把 struct 暴露为组件导出
export!(Greeter);
构建出组件:
# 安装工具链(一次)
cargo install cargo-component wasm-tools
# 构建:产出 greeter.wasm(是 component,不是普通 core wasm)
cargo component build --release
# 产物:target/wasm32-wasip1/release/greeter.wasm
如果你想自己掌控更底层的构建,标准管线是:
# 1. 普通编译成 core wasm
cargo build --target wasm32-wasip1 --release
# 2. 用 wit-bindgen 生成适配层
wit-bindgen component ./target/.../greeter.wasm \
--world greeter-world -o greeter_core.wasm
# 3. 用 wasm-tools 提升为 component
wasm-tools component new greeter_core.wasm -o greeter.wasm
cargo-component 本质上就是把这三步打包了。
4.3 第三步:用 wasmtime 在宿主里加载并调用组件
宿主侧我们写一个 Rust 程序,用 wasmtime 的 component API 实例化并调用:
use anyhow::Result;
use wasmtime::component::{Component, Linker, Engine, Store, ResourceTable};
use wasmtime::{Config, WasmFeatures};
// 由 wit-bindgen 生成的绑定(命令行:wit-bindgen rust --world greeter-world ...)
// 这里假设生成到了 bindings 模块
mod bindings;
use bindings::GreeterWorld;
struct HostState {
// 宿主可在这里持有需要注入组件的能力,例如日志器、配置
table: ResourceTable,
}
fn main() -> Result<()> {
// 1. 配置引擎:开启组件模型 + 异步
let mut config = Config::new();
config.wasm_component_model(true); // 关键:启用组件模型
config.async_support(true); // 关键:启用异步支持
config.wasm_features(WasmFeatures {
component_model: true,
..WasmFeatures::default()
});
let engine = Engine::new(&config)?;
// 2. 读取组件
let component = Component::from_file(&engine, "greeter.wasm")?;
// 3. 构建 linker,并把 WASI 等宿主能力接上
let mut linker: Linker<HostState> = Linker::new(&engine);
wasmtime_wasi::add_to_linker_async(&mut linker)?; // 注入 wasi:cli / wasi:io 0.3
// 4. 实例化并调用
let mut store = Store::new(
&engine,
HostState { table: ResourceTable::new() },
);
let bindings = GreeterWorld::instantiate(&mut store, &component, &linker)?;
let reply = bindings.call_greet(&mut store, "三哥")?;
println!("{reply}"); // Hello, 三哥! 这条消息由 WebAssembly 组件生成。
Ok(())
}
注意 wasmtime_wasi::add_to_linker_async 这一行——它就是 WASI 0.3 异步能力的注入点。宿主 runtime 在这里把 wasi:io 的 future/stream 接到真正的异步 reactor 上。
4.4 第四步:跨语言组合(Rust 组件 + JS 组件)
组件模型的杀手锏是跨语言。我们用 JS(通过 jco / Bytecode Alliance 的 JS 组件工具链)写一个调用者,import Rust 组件的 greeter:
// caller.js(会被编译成 JS 组件)
import { greet } from "example:greeter/greeter@1.0.0";
// JS 组件直接以类型安全的方式调用 Rust 组件导出的函数
const msg = greet("前端同学");
console.log(msg);
# 用 jco 把 JS 转成组件
jco componentize caller.js -o caller.wasm
# 用 wasm-tools 把 rust 的 greeter 适配进 caller 的 import
wasm-tools compose caller.wasm --adapt greeter=greeter.wasm -o app.wasm
最终 app.wasm 是一个完全自包含、进程内跨语言调用的应用:JS 壳调用 Rust 内核,没有序列化、没有 IPC、没有网络。对比微服务里「Go 调 Rust 得走 gRPC」,这省掉的是一整个网络栈的延迟与运维。
4.5 第五步:用 Fermyon Spin 部署一个 Wasm 微服务
Spin 是 Fermyon 出品的 Wasm 应用框架,把组件模型直接变成「Serverless 运行时」。
# spin.toml
spin_manifest_version = 2
[application]
name = "greeter-service"
version = "1.0.0"
[[trigger.http]]
route = "/greet/:name"
component = "greeter"
[component.greeter]
source = "target/wasm32-wasip1/release/greeter.wasm"
# 只授予它必要的 capability,例如允许写日志
allowed_outbound_hosts = []
// Spin 的 HTTP 触发器 handler
use spin_sdk::http::{Request, Response};
use spin_sdk::http_component;
#[http_component]
fn handle_greet(req: Request) -> Response {
let name = req
.headers()
.get("spin-path-params")
.and_then(|v| v.to_str().ok())
.unwrap_or("world");
Response::builder()
.status(200)
.header("content-type", "text/plain")
.body(format!("Hello, {name}! 来自 Fermyon Spin + WebAssembly。"))
.build()
.unwrap()
}
# 本地起服务
spin up --listen 127.0.0.1:3000
curl http://127.0.0.1:3000/greet/三哥
# Hello, 三哥! 来自 Fermyon Spin + WebAssembly。
spin up 的冷启动通常是毫秒级——因为底层就是实例化一个已验证的 wasm 组件,没有容器运行时、没有镜像拉取。这正好对应 Serverless 最痛的「冷启动」痛点。
五、性能优化:Wasm 真正比容器强在哪
讲深度,必须把性能指标摊开。以下对比基于公开基准与工程经验,数字量级供决策参考,具体请以你的硬件实测为准。
5.1 冷启动:亚毫秒 vs 数百毫秒
| 场景 | 容器(容器运行时就绪) | Wasm 组件(wasmtime) |
|---|---|---|
| 空实例启动 | 50~300 ms(看镜像大小) | 0.1~2 ms |
| 含业务逻辑 | 100 ms~1 s+ | 1~5 ms |
| 千实例并发拉起 | 受限于节点进程数 | 轻松上万实例 |
原因前面说过:Wasm 没有「进程创建」这一步,实例化一个验证过的字节码几乎只是指针与栈的分配。
5.2 内存与体积:KB 级 vs MB/GB 级
一个做了 greet 的组件编译后约 几十 KB;一个等效的容器镜像(哪怕 FROM scratch)往往 数 MB 起步,带运行时的语言镜像(JVM / Node)动辄 数百 MB。在边缘节点、Serverless 平台这种「按实例计费、密度即成本」的场景,体积直接等于钱。
5.3 memory64:突破 4GB 天花板
Wasm 1.x 的线性内存地址是 32 位,单实例最多 4GB(且实际常受页表限制)。Wasm 3.0 的 memory64 把地址扩展到 64 位,意味着:
- 内存数据库、向量检索、大图计算这类「吃内存」的工作负载,终于能直接在单个 Wasm 实例里跑;
- 不必为了绕开 4GB 限制而拆实例、做外部状态存储;
- 配合组件模型,一个「大内存计算组件」可以被安全多租户地托管。
// memory64 下,组件可声明更大的内存需求(示意)
// 宿主按组件声明分配,而非按进程
5.4 GC 提案:让带 GC 的语言真正高效入场
过去 Go、Java、C# 想进 Wasm,得把自带的 GC 和系统库整个塞进线性内存,体积大、启动慢、和宿主 GC 打架。Wasm 3.0 的 GC 提案引入了 struct、array、i31ref、eqref 等引用类型,让这些语言可以复用宿主的 GC 语义或轻量自管,大幅瘦身。
这对服务端意义巨大:今天你能把 Go 写的服务编译成组件,体积和启动都接近 Rust 组件水平。语言选型不再被「能不能进 Wasm」绑架。
5.5 隔离密度:单进程跑上千组件
因为隔离发生在 runtime 层而非 OS 层,一个 wasmtime 进程可以安全地托管成千上万个互不信任的组件实例,彼此的内存和 IO 完全隔离。对照容器:一节点通常跑几十到几百个容器就到上限(每个都是进程 + 自己的 FS 视图 + cgroup 开销)。
这带来一种新的部署拓扑:
传统: LB → 容器(服务A) → gRPC → 容器(服务B) → gRPC → 容器(服务C)
Wasm: LB → 单个 runtime → [组件A | 组件B | 组件C](进程内调用)
网络跳数从 N 降到 0,延迟、故障域、运维面同步缩小。
5.6 一个实测基准片段(示意)
// 用 criterion 对比「容器内 HTTP 调用」与「进程内组件调用」
use std::time::Instant;
fn bench_component_inprocess() {
let start = Instant::now();
let r = app::call_greet("bench").unwrap(); // 进程内组件调用
println!("组件内调用耗时: {:?} -> {}", start.elapsed(), r);
}
// 同等逻辑走容器 + gRPC 的基准,通常慢 1~2 个数量级(含序列化+网络+调度)
注意:这类微基准不能脱离业务场景下结论。Wasm 的优势在「高密度、低延迟、强隔离、快速弹性」的场景最显著;对「长连接、重 CPU、需要完整 OS 能力」的工作负载,容器依然更合适。
六、总结与展望:2026 是 Wasm 云原生的真正起点
回到开头的那句话。Docker 解决的痛点是「环境一致 + 轻量隔离 + 快速启动」。Wasm 组件在 2026 年补齐的,恰恰是过去十年一直缺的两块:
- Wasm 3.0 的 memory64 + GC:让 Wasm 从「小工具字节码」升级为「能承载真实服务端负载的执行格式」;
- WASI 0.3 的原生异步组件模型:让 Wasm 在服务端的 IO、并发、组合问题上第一次有了顺手的、标准化的答案。
我的判断(也是给一线工程师的实用建议):
适合用 Wasm 组件的场景
- Serverless / 函数计算 / 边缘函数(冷启动敏感,毫秒级是刚需);
- 多租户插件/规则引擎(用户上传代码,宿主要绝对隔离);
- AI Agent 的工具沙箱(每次调用都是不可信代码,需要快速拉起/销毁);
- 需要跨语言组合的高频内部调用(把网络调用压成函数调用);
- 资源受限的边缘/嵌入式节点(体积和内存密度压倒一切)。
暂时别急着换的场景
- 需要完整 POSIX、重度文件系统、长驻进程的传统单体;
- 依赖特定内核特性(eBPF、特定 syscall)的工作负载;
- 团队生态尚未成熟、调试工具链缺失的内部系统。
生态现状(2026 年中)
- runtime:wasmtime(Bytecode Alliance,事实标准)、WAMR、WasmEdge;
- 语言:Rust 最成熟,Go/C#/Java 经 GC 提案后快速补齐,JS/TS 通过 jco 体系;
- 平台:Fermyon Spin、Fastly Compute、Cloudflare Workers、Wasm 云(WASI 0.3 兼容层);
- 标准接口:wasi:http、wasi:keyvalue、wasi:sql、wasi:config、wasi:logging、wasi:cli 等正逐步落到 0.3 基线。
仍要正视的不足
- 组件内线程/共享内存仍在演进,重并发计算负载要谨慎评估;
- 调试、profiling 工具链相比容器生态还年轻;
- WASI 0.3 是 2026 年 3 月才批准,部分 runtime 与宿主平台的适配还在灰度。
结语:WebAssembly 在浏览器赢了,是因为它「快且安全」。它在服务端迟到,是因为过去缺了「和系统打交道的标准 + 好用的异步 + 跨语言组合」。WASI 0.3 把这三块一次性补齐,Docker 时刻这个老预言,终于不再只是预言。对后端工程师来说,2026 年最值得投入学习的「下一件大事」里,Wasm 组件模型应当有一席之地——不是因为它会取代容器,而是因为它补上了容器一直补不上的那块版图:在不可信与高密度之间,给出一个毫秒级、KB 级、类型安全的答案。
参考资料与延伸阅读:WASI 官方文档(wasi.dev)、WebAssembly Component Model 规范仓库、Wasmtime 文档、Fermyon Spin 文档、Wasm 3.0 / WASI 0.3 发布公告。文中命令以你所用的工具版本为准,建议先以 wasm-tools --help、cargo component --help 核对参数。