Tokamak logo

用于构建 WebAssembly 浏览器应用的 SwiftUI 兼容框架

CI status Discord

目前,Tokamak 仅实现了 SwiftUI 的一个非常基础的子集。它的 DOM 渲染器支持一些视图类型和修饰符(您可以在进度文档中查看当前列表),以及一个新的 HTML 视图用于构建任意 HTML。Tokamak 的长期目标是尽可能多地实现 SwiftUI API,并提供一些有用的补充功能,以简化 HTML 和 CSS 交互。

如果缺少您想使用的某个 SwiftUI API,请查看现有的 问题PR 以获取有关当前状态的更多详细信息,或者创建一个新问题,以便我们根据需求确定开发的优先级。我们也在努力使视图和修饰符的开发更加容易(借助 HTML 视图,请参阅下面的示例),因此非常欢迎 pull request!请不要忘记首先查看“贡献”部分

如果您想参与不断壮大的 SwiftWasm 社区,也欢迎您加入我们的 Discord 服务器,或 SwiftPM Slack 中的 #webassembly 频道。

示例代码

Tokamak API 尝试尽可能地模仿 SwiftUI API。 主要区别在于您在文件中使用 import TokamakShim 而不是 import SwiftUI。 前者使您的视图与 Apple 平台以及 Tokamak 支持的平台(目前只有 WebAssembly/WASI,未来会支持更多)兼容。

import TokamakShim

struct Counter: View {
  @State var count: Int
  let limit: Int

  var body: some View {
    if count < limit {
      VStack {
        Button("Increment") { count += 1 }
        Text("\(count)")
      }
      .onAppear { print("Counter.VStack onAppear") }
      .onDisappear { print("Counter.VStack onDisappear") }
    } else {
      VStack { Text("Limit exceeded") }
    }
  }
}

@main
struct CounterApp: App {
  var body: some Scene {
    WindowGroup("Counter Demo") {
      Counter(count: 5, limit: 15)
    }
  }
}

任意 HTML

使用 HTML 视图,您还可以渲染任何您想要的 HTML,包括内联 SVG

struct SVGCircle: View {
  var body: some View {
    HTML("svg", ["width": "100", "height": "100"]) {
      HTML("circle", [
        "cx": "50", "cy": "50", "r": "40",
        "stroke": "green", "stroke-width": "4", "fill": "yellow",
      ])
    }
  }
}

HTML 不支持事件监听器,并在 TokamakStaticHTML 模块中声明,TokamakDOM 会重新导出它。 HTML 的好处在于,您可以将其用于 TokamakVaporTokamakPublish 等库中的静态渲染。

另一种选择是由 TokamakDOM 模块提供的 DynamicHTML 视图,它具有一个带有相应初始化器参数的 listeners 属性。 您可以在 listeners 字典中传递可以处理 onclickonmouseover 和其他 DOM 事件的闭包。 查看 MDN 文档 以获取完整列表。

使用 DynamicHTML 处理鼠标事件的示例代码如下:

struct MouseEventsView: View {
  @State var position: CGPoint = .zero
  @State var isMouseButtonDown: Bool = false

  var body: some View {
    DynamicHTML(
      "div",
      ["style": "width: 200px; height: 200px; background-color: red;"],
      listeners: [
        "mousemove": { event in
          guard
            let x = event.offsetX.jsValue.number,
            let y = event.offsetY.jsValue.number
          else { return }

          position = CGPoint(x: x, y: y)
        },
        "mousedown": { _ in isMouseButtonDown = true },
        "mouseup": { _ in isMouseButtonDown = false },
      ]
    ) {
      Text("position is \(position), is mouse button down? \(isMouseButtonDown)")
    }
  }
}

任意样式和脚本

虽然 JavaScriptKit 是偶尔与 JavaScript 交互的一个很好的选择,但有时您需要注入任意脚本或样式,这可以通过直接 DOM 访问来实现

import JavaScriptKit

let document = JSObject.global.document
let script = document.createElement("script")
script.setAttribute("src", "https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.27.0/moment.min.js")
document.head.appendChild(script)

_ = document.head.insertAdjacentHTML("beforeend", #"""
<link
  rel="stylesheet"
  href="https://cdnjs.cloudflare.com/ajax/libs/semantic-ui/2.4.1/semantic.min.css">
"""#)

这样,Semantic UI 样式和 moment.js 本地化的日期格式化(或以这种方式添加的任何任意样式/脚本/字体)都可以在您的应用程序中使用。

Fiber 渲染器

一个新的协调器(reconciler),模仿 React 的 Fiber 协调器,可以选择使用。它可以提供更快的更新并允许更大的 View 层次结构。它还包括布局步骤,可以比 CSS 近似值更接近 SwiftUI 布局。

您可以在 App 的配置中指定要使用的协调器

struct CounterApp: App {
  static let _configuration: _AppConfiguration = .init(
    // Specify `useDynamicLayout` to enable the layout steps in place of CSS approximations.
    reconciler: .fiber(useDynamicLayout: true)
  )

  var body: some Scene {
    WindowGroup("Counter Demo") {
      Counter(count: 5, limit: 15)
    }
  }
}

注意:并非所有 ViewViewModifier 都受到 Fiber 渲染器的支持。

要求

对于应用开发者

对于依赖 Tokamak 的应用程序的用户

任何支持 WebAssembly所需的 JavaScript 功能的最新浏览器应该都可以正常工作,目前包括

如果您需要支持旧版本的浏览器,您需要使用 JAVASCRIPTKIT_WITHOUT_WEAKREFS 标志进行构建,在编译时传递 -Xswiftc -DJAVASCRIPTKIT_WITHOUT_WEAKREFS 标志。 这应该会将浏览器要求降低到以下版本:

但并非所有这些版本都经过定期测试,欢迎提供兼容性报告!

入门

Tokamak 依赖 carton 作为主要构建工具。 作为这些步骤的一部分,您将在 macOS 上通过 Homebrew 安装 carton(不幸的是,您必须在 Linux 上手动构建它)。 假设您已经安装了 Homebrew,您可以按照以下步骤创建一个新的 Tokamak 应用程序:

  1. 安装 carton
brew install swiftwasm/tap/carton

如果您之前安装过 carton,请确保您拥有 0.15.0 或更高版本

carton --version
  1. 为您的项目创建一个目录并将其设为当前目录
mkdir TokamakApp && cd TokamakApp
  1. 使用 carton 从模板初始化项目
carton init --template tokamak
  1. 构建项目并启动开发服务器,carton dev 可以在开发过程中保持运行
carton dev
  1. 在您的浏览器中打开 http://127.0.0.1:8080/ 以查看正在运行的应用。 您可以在您喜欢的编辑器中编辑应用源代码并保存它,carton 将立即重建应用并重新加载所有打开该应用的浏览器选项卡。

您也可以克隆此存储库并在其根目录中运行 carton dev --product TokamakDemo。 这将构建演示应用,该应用展示了几乎所有当前实现的 API。

如果您有任何疑问,请查看FAQ 文档,和/或加入 SwiftWasm Discord 服务器上的 #tokamak 频道。

安全

默认情况下,DOM 渲染器将转义 Text 视图中的 HTML 控制字符。 如果您想覆盖此功能,您可以使用 _domTextSanitizer 修饰符

Text("<font color='red'>Unsanitized Text</font>")
  ._domTextSanitizer(Sanitizers.HTML.insecure)

您也可以使用自定义的 sanitizer; _domTextSanitizer 的参数只是一个 String -> String 闭包。 如果 _domTextSanitizer 应用于非 Text 视图,它将应用于子视图中的所有 Text,除非被覆盖。

如果您在其他地方使用用户生成或其他不安全的字符串,请确保自己正确地进行 sanitize。

问题排查

构建时出现 unable to find utility "xctest" 错误

此错误只能发生在 macOS 上,因此请确保您已安装了 Xcode,如要求中所列。 如果您确实安装了 Xcode,但仍然收到此错误,请参阅 此 StackOverflow 答案

语法高亮和自动完成在 Xcode 中不起作用

使用 Xcode 打开您的项目中依赖 Tokamak 的 Package.swift 文件,并为 macOS 构建它。 由于 Xcode 目前不支持非 Apple 平台的交叉编译,因此如果您的项目没有为 macOS 构建,则无法对其进行索引,即使它在运行时在 macOS 上并不完全起作用。 如果您需要在您自己的应用中排除一些在 macOS 上无法编译的 WebAssembly 特定代码,您可以依赖 #if os(WASI) 编译器指令。

Tokamak 的所有相关模块(包括 TokamakDOM)都应该在 macOS 上编译。 您可能会在 macOS Catalina 上看到 TokamakShim 的问题,其中相关 SwiftUI API 不受支持,但将 import TokamakShim 替换为 import TokamakDOM 应该可以解决该问题,直到您可以更新到 macOS Big Sur。

如果您偶然发现 Tokamak 中未在 macOS 上构建并阻止语法高亮或自动完成在 Xcode 中工作的代码,请将其报告为 bug

语法高亮和自动完成在 VSCode 中不起作用

确保您已安装SourceKit LSP 扩展。 如果您不信任此非官方版本,请按照手动构建和安装指南进行操作。 不幸的是,Apple 目前没有在 VSCode Marketplace 上提供该扩展的官方版本。

贡献

欢迎所有贡献,无论多么微小。 您不必是 Web 开发人员或 SwiftUI 专家才能做出有意义的贡献。 事实上,通过查看 Tokamak 中一些最简单的视图是如何实现的,您可以了解更多关于 SwiftUI 在底层是如何工作的。

更新我们的文档 并承担入门错误也值得赞赏。 不要忘记加入我们的Discord 服务器以与维护者和其他用户联系。 请参阅CONTRIBUTING.md 了解更多详情。

行为准则

本项目遵守 Contributor Covenant 行为准则。 通过参与,您需要遵守此准则。 请向 conduct@tokamak.dev 报告不可接受的行为。

赞助

如果这个库为您节省了时间和金钱,请考虑在他们的赞助页面上赞助维护者的工作:@carson-katri, @kateinoigakukun, 和 @MaxDesiatov。虽然某些赞助级别会为您提供优先支持甚至咨询时间,但任何金额都将不胜感激,并有助于维护该项目。

维护者

按字母顺序排列:Carson Katri, Ezra Berch, Jed Fox, Morten Bek Ditlevsen, Yuta Saito

致谢

SwiftUI 是 Apple Inc. 的商标。 作为 Tokamak 项目一部分维护的软件与 Apple Inc. 无关。

许可证

Tokamak 在 Apache 2.0 许可证下可用。 除非适用法律要求或以书面形式约定,否则根据许可证分发的软件按“原样”分发,不附带任何形式的明示或暗示的保证或条件。 有关更多信息,请参见 LICENSE 文件。