2026/8/27 1:28:44

Swift 到 WASM 实战:逻辑复用、Tokamak 与 SwiftUI 边界

Swift 到 WASM 实战:逻辑复用、Tokamak 与 SwiftUI 边界 把 Swift 编译到 WASM再把 SwiftUI 也带上这个组合最近被问得很多。先说我的判断Swift 业务逻辑编译成 WASM 模块在浏览器、Node 或 WASI 运行时里执行这条路目前是能走通的但“SwiftUI 直接编译到 WASM 后在浏览器里渲染”现阶段还没有这么理想。SwiftUI 依赖 Apple 平台那套 UI 运行时和系统组件浏览器里没有对应实现。真正可落地的路线是两条一条是逻辑层用 Swift 编译成 WASMUI 用 Web 技术来做另一条是用 Tokamak 这类提供 SwiftUI 风格 API 的库在浏览器里获得接近 SwiftUI 的开发体验。下面按实际落地顺序拆一遍从环境准备到最小 Demo再讲清楚边界和坑。1. 先搞清楚Swift 到 WASM 能做什么不能做什么1.1 WASM 和沙箱执行模型很多人第一次接触 WASM是听说它能在浏览器里跑高性能代码。它确实做到了WASM 是一种二进制指令格式浏览器和 wasmtime、wasmer 这类运行时都能执行。和 JavaScript 不同WASM 模块在加载后先在受限环境里实例化有自己的线性内存和调用约定。“沙箱”是这个过程中最值得记住的概念。WASM 模块默认不能直接访问宿主系统的文件、网络、窗口和 DOM只能在宿主导出的接口范围里工作。这个限制是安全性的来源也是 SwiftUI 这类 UI 框架难以直接编译过来的原因。UI 框架需要的不是“能算数”而是“能渲染、能响应事件、能访问系统控件”这些能力浏览器不会凭空提供给一个 WASM 模块。理解了沙箱模型就能明白为什么“SwiftUI 编成 WASM”和“SwiftUI 在浏览器里跑起来”是两件事。前者说的是产物格式后者还要求浏览器具备 SwiftUI 需要的全部宿主能力。1.2 Swift 编译到 WASM 的稳定场景从实际使用看Swift 到 WASM 最稳定的场景是纯逻辑代码数据解析、协议编解码、序列化、业务规则计算、算法实现、加密摘要等。只要代码不依赖 Apple 平台框架编译成功率很高。Foundation 里的大多数类型也能编译进去但代价是产物体积变大。如果代码里用了 Dispatch、Combine、CoreGraphics 或 iOS/macOS 专属能力就要小心了。这些模块在 WASM 目标下不一定存在即使存在行为也可能和 Apple 平台不一致。我一般会先做一个“依赖检查”把目标文件里 import 的模块列出来凡是来自 Apple 平台 SDK 的先确认有没有 WASM 对应实现。这一步能省掉后面大量莫名其妙的链接报错。1.3 三种可落地路线可以把标题里的诉求拆成三条路线路线做法适合场景逻辑层复用Swift 编译成 WASM通过 JavaScriptKit 和 JS 交互UI 用 Web 技术已有 Swift 业务逻辑需要复用给前端SwiftUI 风格 UI用 Tokamak 等库以 SwiftUI 风格 API 渲染到 DOM想保持 Swift 单语言开发UI 不复杂服务端 WASI编译成 WASI 模块跑在 wasmtime 等运行时命令行工具、服务端逻辑、边缘计算三条路线不是互斥的。一个项目可以先跑通逻辑层再逐步引入 Tokamak。关键是心里清楚SwiftUI 本体不会出现在浏览器里出现的是“像 SwiftUI 的开发体验”。如果团队里有人把这件事理解成“把现有 iOS 工程直接编到浏览器”一定要提前纠正这个预期。2. 环境准备SwiftWasm 工具链、carton 和依赖2.1 工具链选择与安装思路要做 Swift 到 WASM 的交叉编译首先要有一套支持 WASM 目标的 Swift 工具链。常见选择是社区维护的 SwiftWasm 工具链它是在开源 Swift 工具链基础上加入 WASM 目标支持的分支产物。这里用“交叉编译”这个词是准确的你是在 macOS 或 Linux 上编译但目标平台是 wasm32和本机架构不一样。宿主系统和目标系统不同所以叫 cross compiling。安装步骤我没办法给一套万能命令因为工具链更新频繁不同系统的差异也大。最靠谱的方式是去 SwiftWasm 项目仓库按当时的安装说明操作。下载完成之后把工具链加入 PATH然后执行swift --version确认。注意只确认 swift 版本号还不够。要确认你用的这份工具链带 WASM 目标支持。有些本机 Swift 环境很新但并没有编译 WASM 的目标文件直接跑交叉编译会报目标不支持。2.2 carton 的作用carton 是 SwiftWasm 生态里的开发工具主要负责两件事构建出浏览器能加载的 WebAssembly 产物以及启动本地开发服务器。你可以把它理解成 SwiftWasm 项目的前端脚手架。没有 carton 也不是不能做但你会花大量时间处理静态资源、JS 胶水代码和加载顺序。有 carton 之后流程通常是在项目根目录运行构建命令。启动开发服务器。浏览器打开页面。控制台看日志。carton 安装一般通过 Homebrew 或 GitHub Releases。不同系统差异很大以项目文档为准不要照搬网络上过时的安装命令。2.3 依赖选型JavaScriptKit、Tokamak 和 WASI 组件依赖可以分成三类JavaScriptKitSwift 代码和 JavaScript 之间互操作的桥梁用来读写 JS 对象、调用浏览器 API、处理 Promise。Tokamak模拟 SwiftUI 声明式 API 的 Web 渲染库。WASI 相关组件用于编译成非浏览器的 WASI 模块。第一次做 Demo建议只引入 JavaScriptKit。不要一上来就上 Tokamak。原因很简单先验证“Swift 代码真的能在浏览器里执行”再叠加 UI 层问题定位会容易很多。如果一开始就同时引入多个库报错时你分不清是工具链、依赖、构建还是浏览器加载的问题。3. 最小 Demo从创建项目到浏览器看到结果3.1 用 SwiftPM 声明项目SwiftWasm 项目仍然基于 SwiftPM。创建好目录结构后先写 Package.swift// swift-tools-version:5.9 import PackageDescription let package Package( name: WasmDemo, dependencies: [ .package( url: https://github.com/swiftwasm/JavaScriptKit.git, branch: main ), ], targets: [ .executableTarget( name: WasmDemo, dependencies: [JavaScriptKit] ), ] )这里有两处需要手动确认。第一swift-tools-version要和你工具链支持的版本一致上面写 5.9 只是示例。第二依赖写法里用 branch 是为了演示正式项目建议锁定具体 tag避免依赖漂移。版本不匹配时最常见的报错是 module not found 或 API 签名不一致。3.2 第一段可运行代码在 Sources/WasmDemo/main.swift 里写import JavaScriptKit let console JSObject.global.console console.log(Hello from Swift WASM)这段代码的意思是从全局 JavaScript 环境里拿到 console 对象调用它的 log 方法。在浏览器里执行时控制台会输出这一行日志。这是我能想到的最小交叉编译验证不涉及 UI不涉及复杂参数只确认 Swift 到 WASM 再到浏览器的完整链路。确认 JavaScriptKit 的 API 名称可能随版本变化这里只是示意。关键是理解数据是怎么从 Swift 流到 JS 的。用 carton 构建carton build构建成功后再启动开发服务器carton dev浏览器打开提示的地址按 F12 打开开发者工具控制台里应该能看到输出。3.3 成功标准不只“页面打开了”我一般会从三个维度判断 Demo 是否真的成功。第一控制台输出。有没有看到 Hello 日志有没有报错。没有日志等于没有验证。第二网络面板。.wasm 文件是否正常返回 200MIME type 是不是正确的application/wasm。如果 MIME 不对浏览器会拒绝执行。第三构建产物。最终 .wasm 文件是否存在体积多大。这里建议记录一下初始体积后面每加一个依赖就对比一次能直观看到体积增长来源。如果页面打开了但控制台什么都没有不要急着改代码。先看网络面板、浏览器报错和 .wasm 加载状态。很多时候不是逻辑问题而是路径、MIME、CSP 或跨域配置问题。4. 带 UI 怎么做Tokamak 和混合架构4.1 Tokamak 的 SwiftUI 风格开发如果想要 SwiftUI 风格的 Web UITokamak 是目前比较成熟的选择。它提供的 API 看起来很像 SwiftUIApp、Scene、View、Text、Button、List 这些概念都有。一个很简单的结构长这样import TokamakDOM struct ContentView: View { var body: some View { Text(Hello from Tokamak) .font(.system(size: 24)) } } main struct WebApp: App { var body: some Scene { WindowGroup(Demo) { ContentView() } } }注意这只是示例具体 API 以你引入的 Tokamak 版本为准。它解决的是“用 SwiftUI 风格写 Web UI”的体验问题不是把 SwiftUI 运行时完整搬到浏览器。复杂动画、平台专属修饰符、部分系统组件在 SwiftUI 和 Tokamak 之间会有差异。如果你真的需要现有 SwiftUI 代码原样在浏览器里运行目前没有官方现成方案。更实际的做法是评估现有 View 的依赖面把能替换的抽出来不能替换的部分另写 Web 实现。4.2 状态管理和渲染边界SwiftUI 里的 State、Binding、ObservableObject 这类状态管理Tokamak 有对应实现但不能假设 100% 一致。数据变化、视图更新的时序在不同实现下可能有细微差别。我的建议是如果项目里有复杂状态流转、多页面导航、深度动画先用小范围原型验证再决定是否全量使用 Tokamak。如果 UI 只是展示文本、按钮和简单列表Tokamak 完全够用。这里最容易踩的坑是“看着像 SwiftUI用起来发现行为不一样”。写 SwiftUI 时你依赖的是 Apple 平台的运行时行为换到浏览器后事件循环、布局系统、渲染时机全变了。遇到诡异问题时先查是不是渲染环境差异而不是 Swift 语法问题。4.3 混合方案Swift 逻辑层 前端 UI如果 UI 复杂度很高或者团队前端能力更强推荐混合方案。Swift 模块只负责纯逻辑接收输入、计算、返回输出。JS 负责页面渲染、事件绑定、接口请求和动画。这个方案的关键是设计好模块导出函数。不要把一堆逻辑细节暴露出去尽量把数据出入口收敛成几个稳定函数。这样做的优点很明显逻辑可测试不依赖浏览器。Swift 代码可以继续用于 iOS、macOS 和服务端。前端可以自由选择框架。出问题时边界清楚不会出现 UI 和逻辑搅在一起的困境。4.4 服务端场景WASI 运行时不带 UI 的 Swift 逻辑可以编译成 WASI 模块在 wasmtime、wasmer 等运行时里执行。适合命令行工具、批量任务、边缘函数。WASI 环境下要注意文件系统访问、环境变量、网络能力都由宿主控制不是代码里写个路径就能随便读写的。如果只是想在服务端跑 Swift 而不面向浏览器WASI 是比浏览器 WASM 更稳的路径。因为它不需要处理 DOM、不需要考虑浏览器兼容性宿主环境也更容易控制权限。5. 性能、体积和兼容性不要被 Demo 骗了5.1 判断标准体积、启动、占用和稳定性Demo 能跑和能用于生产中间隔了很多项判断。至少要看这些指标判断方式关注点产物体积查看最终 .wasm 文件大小是否适合首屏加载启动时间浏览器里从请求到可执行的时间模块实例化是否过慢内存占用DevTools 里的内存面板多次调用后是否异常增长稳定性连续调用导出函数对比结果是否出现不一致或崩溃Swift 标准库如果没有被裁剪编译出来的 WASM 体积可能明显偏大。这就是为什么很多项目只把核心逻辑编进去而不是把整个 App 编进去。我的经验是先记录第一个可运行版本的体积和启动时间之后每次加依赖都重新测量。不要等到最后才发现体积翻了几倍却找不到是哪一次改动引入的。5.2 线程、网络和文件系统的边界浏览器里的 WASM 和传统 Swift 运行环境差别很大。线程方面WASM 线程模型需要 SharedArrayBuffer 支持而且和 Swift 的并发模型不完全一致。涉及并发时先确认浏览器兼容性和响应头配置。网络方面Swift 系统网络库在浏览器里不会直接工作。通常用 JavaScriptKit 调 fetch把数据拿回来再交给 Swift 逻辑层处理。这意味着你要把“网络取数”和“业务计算”明确拆开。文件系统方面浏览器环境没有真实文件系统。WASI 环境下文件系统由宿主导出访问路径范围受权限控制。任何假设“本地能读文件浏览器里也能读”的代码都会在这里卡住。5.3 常见报错和排查顺序遇到问题先从低层往上排查先看现象报错、卡住、无输出、输出异常。再看构建构建是否成功是否用了错误的 target。再看依赖有没有引入平台专属模块版本是否匹配。再看加载浏览器网络面板、MIME type、CSP、跨域。最后才改代码。容易忽略的点是源代码里不小心 import 了 UIKit、Darwin、CoreGraphics 等平台框架。这种问题经常表现为“本地编过了换 WASM 目标就报错”本质上不是代码错了而是依赖了目标环境不存在的模块。6. 什么时候值得用什么时候不建议用6.1 值得用的场景团队已经有大量 Swift 业务逻辑希望直接复用给前端或服务端。需要同一套计算逻辑在多个端保持结果一致。对沙箱隔离有要求的场景希望代码运行在受限环境里。团队以 Swift 为主不想为一个小工具再引入整个前端技术栈。这几个场景的共同点是核心价值在逻辑不在 UI。逻辑复用收益明显UI 复杂度低WASM 方案才划算。6.2 不建议用的场景目标是复杂、高交互、强动画的 Web 应用Tokamak 或 WASM 方案不如成熟前端框架。团队不熟悉 Swift学习成本会抵消复用收益。对首包体积和首屏加载极敏感Swift 标准库带来的体积可能不适合。需要大量浏览器高级 API、WebGL 渲染管线、设备硬件能力纯 WASM 方案实现起来很别扭。如果项目只是“想用 Swift 写网页”但没有任何跨端复用诉求我会先劝你评估一下投入产出。SwiftWasm 生态还在快速变化生产环境下每一层依赖都需要自己盯版本。6.3 落地建议我个人更建议先把单任务跑稳再考虑批量先把纯逻辑模块编译成 WASM验证 JS 调用方式再决定是否引入 UI 层。如果只是学习最小 Demo 已经足够。如果要长期使用需要提前解决几件事锁定工具链和依赖版本、把构建脚本写进 CI、记录产物体积变化、统一日志格式、设计好 WASM 与 JS 之间的数据接口。这块内容也常被当作 Swift 相关面试里的加分话题但面试时能把“SwiftUI 能不能直接编成 WASM”的边界讲清楚比背几条命令更有价值。真正动手跑一次最小 Demo你对“交叉编译”“沙箱”“宿主能力”这些概念的理解会完全不一样。最后留几个我排查时会优先看的点工具链和依赖版本是否锁定.wasm 产物体积是否异常增长JS 和 Swift 之间是不是频繁做大量数据拷贝失败时控制台有没有有效信息以及你需要的到底是“真的 SwiftUI”还是“像 SwiftUI 的开发体验”。这两个目标的落地成本差一个数量级。