第弐拾弐話
Asuka 与 SwiftUI、Expo 的功能对照
Asuka 已经有什么,还缺什么。
下面每一行都是应用经常需要的一项功能,分别列出 SwiftUI 和 Expo 的做法,以及 Asuka 提供的对应能力。除非该行另有说明,✓ 表示 Asuka 支持的所有平台(iOS、Android、Mac、Windows、Linux 和 Web)都能用;如果某个平台本身不具备某项能力(比如 Mac 没有传感器,浏览器没有主屏幕小组件),对应的包会如实告诉调用方不支持。至于浏览器做不到手机能做的事时各个包会如何替代,见 Web。
SwiftUI 也能在 Mac 上运行。Expo 支持 iOS、Android 和 Web;React Native 则借助微软的 React Native for Windows and macOS 覆盖 Windows 和 Mac,不过这不在 Expo 的范围内。Asuka 与桌面端常用框架(Electron、Tauri、Wails)的对比,见与桌面端框架对比;桌面应用在窗口之外还需要哪些能力,见桌面应用的能力;与自己绘制 UI 的 MyGo 的对比,见 Asuka 与 MyGo。
✓ 已支持 · ◐ 部分支持(会注明缺什么) · ✗ 尚未支持
布局
| SwiftUI | Expo (React Native) | Asuka | |
|---|---|---|---|
| 堆叠、间距、内边距、对齐 | HStack、VStack、Spacer |
flexbox | ✓ Row、Column、Style(flexbox:Grow、Gap、Align、Justify、Wrap) |
| 覆盖层、绝对定位 | ZStack、.overlay |
position: absolute |
✓ Style.Absolute、Top/Left/... |
| 滚动 | ScrollView |
ScrollView |
✓ Scroll(纵向、横向;移出视野的行会离开视图树) |
| 惰性列表、单元格复用 | List、LazyVStack |
FlatList |
✓ List(用平台自己的:UICollectionView、RecyclerView;在 Mac、Windows、Linux 和 Web 上,由宿主为可见范围内的行绑定数据并复用) |
| 网格 | LazyVGrid |
FlatList numColumns |
✓ ListProps.Columns、Gap |
| 聊天列表:从末尾开始显示,往上加载更早的消息 | defaultScrollAnchor(.bottom)、scrollPosition |
FlatList inverted、maintainVisibleContentPosition |
✓ ListProps.Anchor、OnStartReached;在可见内容上方插入行时,可见内容保持不动,所有列表都是如此 |
| 分区、吸顶标题 | List 里的 Section |
SectionList、stickySectionHeadersEnabled |
✓ ListProps.Section、Header(在 plain 和 bare 列表里吸顶) |
| 选择行 | List(selection:)、EditButton |
社区方案 | ✓ ListProps.Selecting、Selected、OnSelect、OnSelectBegin(iOS 的编辑模式和双指拖动;Android 的复选框和长按) |
| 安全区域、键盘 | 自动处理 | SafeAreaView、KeyboardAvoidingView |
✓ SafeArea、UseEnvironment().Keyboard |
| 读取视图的尺寸 | GeometryReader |
onLayout |
✓ ViewProps.OnLayout |
| 宽高比、最小/最大尺寸 | .aspectRatio、.frame(min:max:) |
样式 | ✓ Style |
控件
| SwiftUI | Expo | Asuka | |
|---|---|---|---|
| 文本、带样式的文本、链接 | Text、AttributedString |
Text |
✓ Text、RichText(文本片段、链接、行内视图、文本选择)、asuka/sdk/markdown |
| 按钮(变体、图标、菜单) | Button、Menu |
Pressable |
✓ Button(filled、tinted、gray、bordered;图标;菜单)、Menu、上下文菜单;菜单项可以禁用、打勾、放进子菜单或带标题的分区(Action.Disabled、Checked、Children、Inline;ui.Submenu、ui.MenuSection),导航栏按钮也能弹出菜单 |
| 文本框 | TextField、SecureField、TextEditor |
TextInput |
✓ TextField(密码输入、键盘类型、回车键类型、多行) |
| 光标、在光标处插入 | TextSelection |
selection、onSelectionChange |
✓ TextFieldProps.Selection、ui.InsertText |
| 富文本编辑(比如邮件正文) | 配合 AttributedString 的 TextEditor(iOS 26) |
社区方案(10tap、pell) | ✓ TextFieldProps.Rich、ToggleFormat、SetLink、OnFormats |
| 键盘上方的工具条 | .toolbar { ToolbarItemGroup(placement: .keyboard) } |
InputAccessoryView(iOS) |
✓ TextFieldProps.Accessory(两个平台都支持) |
| 粘贴的图片、键盘里的 GIF 和贴纸 | onPasteCommand、PasteButton |
社区方案(onPaste) |
✓ TextFieldProps.PasteTypes、OnPaste(也支持 Android 的 commitContent) |
| 开关、滑块 | Toggle、Slider |
社区方案 | ✓ Toggle、Slider |
| 分段控件 | Picker(.segmented) |
社区方案 | ✓ Segmented |
| 日期和时间 | DatePicker |
社区方案 | ✓ DatePicker(日期、时间,或两者都选) |
| 进度条、加载指示器 | ProgressView |
ActivityIndicator |
✓ Progress、ProgressRing(圆形,有值无值都行)、Activity |
| 滚轮选择器、菜单选择器、步进器 | Picker、Stepper |
社区方案 | ✓ Wheel、Picker(iOS 和 Mac 上是弹出按钮,Android 上是 Material 的 exposed dropdown menu)、Stepper |
| 颜色选择器 | ColorPicker |
社区方案 | ✓ ColorPicker(iOS 上用系统的选择器;Android 没有系统选择器,就用一个 Material 对话框展示 Google 的调色板,支持不透明度) |
| 图标 | SF Symbols | @expo/vector-icons |
✓ Symbol(iOS 和 Mac 上用 SF Symbols;同样的名字在 Android 和 Web 上对应 Material Symbols,在 Windows 上对应 Segoe Fluent Icons,在 Linux 上对应图标主题里的图标或 Material Symbols:这些是精选出来的一组图标) |
| 图片(远程、缓存、占位图) | AsyncImage |
expo-image |
✓ Image(内存和磁盘缓存、Placeholder、BlurHash 和 ThumbHash 占位图、交叉淡入的 Transition;平台能解码时支持 WebP、HEIC 和 AVIF,Windows 上通过 WIC 已安装的编解码器;GIF、APNG 和 WebP 动图在所有平台上播放,支持 Animated 和“减弱动态效果”;Mac、Linux 和 Web 上支持 SVG) |
| Lottie 动画 | 社区方案(lottie-ios) | lottie-react-native |
✓ asuka/sdk/lottie(lottie-ios、lottie-android、lottie-web;Windows 和 Linux 上用自带的渲染器绘制:支持 JSON 和 dotLottie、播放、循环、速度、可读可写的 Progress、在 ui.Animate 中动画过渡、片段、标记、用于主题和深色模式的 keypath 覆盖、“减弱动态效果”) |
| 视频 | VideoPlayer |
expo-video |
✓ Video(iOS 和 Mac 上用 AVKit,Android 上用 Media3,Windows 上用 WebView2,Linux 上用 GStreamer,Web 上用 <video>) |
| 网页内容 | WKWebView |
react-native-webview |
✓ WebView(iOS 和 Mac 上是 WKWebView,Android 上是系统的 WebView,Windows 上是 WebView2,Linux 上是 WebKitGTK;支持 URL、HTML、双向消息、先于页面脚本运行的脚本、拦截请求、页面状态和历史记录、下载、新窗口、cookie、获取脚本的返回值;Android 上还能逐个处理请求;在 Web 上,应用自己站点的页面和通过 postMessage 配合的页面能用上以上全部功能,其他站点的页面则只能加载);asuka/sdk/js 可以在屏幕之外运行 JavaScript |
| 地图 | Map |
expo-maps |
✓ asuka/sdk/maps(Apple 地图;Google 地图需要 API 密钥:支持区域、标记、用户位置、点按;Linux 上用 libshumate 的 OpenStreetMap 地图;Windows 上由宿主自己绘制 OpenStreetMap 的瓦片) |
| 自定义绘制 | Shape、Path、Canvas |
react-native-svg、Skia |
✓ Shape 和 Path(填充、描边、线帽、连接样式、虚线、可做动画的 trim);Canvas(路径、渐变、文本、变换、裁剪、不透明度);以原生方式绘制的 SVG 图标(asuka/sdk/svg) |
| 图表 | Swift Charts | victory-native、社区方案 |
✓ asuka/sdk/chart(柱状图、折线图、面积图、散点图) |
| 着色器 | .colorEffect(ShaderLibrary...) |
Skia 的 runtime effect | ✓ Shader(用 Kage 编写,语法与 Go 相同:iOS 和 Mac 上用 Metal,Android 上用 AGSL,Web 上用 WebGL 2,Windows 上用 Direct3D 11,Linux 上用 OpenGL) |
| 调用原生代码 | UIViewRepresentable |
原生模块 | ✓ 原生包:在 Go 里声明带类型的模块和视图,在 iOS 和 macOS 上用 Swift 实现,在 Android 上用 Kotlin,在 Web 上用 JavaScript,在 Windows 和 Linux 上用 Go(原生代码) |
| 分页、页面指示点 | .scrollTargetBehavior(.paging)、TabView(.page) |
react-native-pager-view |
✓ ScrollProps.Paging、PageControl |
| 复选框 | Toggle(.checkbox,macOS) |
社区方案 | ✓ ToggleProps.Checkbox |
导航与呈现
| SwiftUI | Expo (expo-router) | Asuka | |
|---|---|---|---|
| 带原生推入、弹出和返回手势的导航栈 | NavigationStack |
Stack | ✓ Stack、UseNavigator |
| 标签页 | TabView |
Tabs | ✓ Tabs |
| 侧边栏、抽屉 | NavigationSplitView |
Drawer | ✓ SplitView(iPhone 上是导航栈,Android 上是抽屉;Mac 上是 NSSplitViewController,Windows 上是导航窗格,Linux 上是 libadwaita 的分栏视图) |
| 标签页和侧边栏行上的角标 | badge |
tabBarBadge、drawerItem 计数 |
✓ Tab.Badge、Destination.Badge(badgeValue、BadgeDrawable、抽屉计数;Mac 上 NSTabView 的标签文字、源列表的计数;Fluent 的 InfoBadge;libadwaita 的 badge-number) |
| 导航栏按钮、标题、搜索 | .toolbar、.searchable |
header 选项 | ✓ ScreenOptions(Actions、Search) |
| 底部工具栏、主操作(FAB) | ToolbarItem(placement: .bottomBar) |
— | ✓ ScreenOptions(Toolbar、Primary、PrimaryExtended):UIKit 的工具栏加一个醒目的末尾按钮,Material 的 BottomAppBar 和(扩展)FAB,Mac 上是窗口工具栏,Windows 上是命令栏,Linux 上是 GtkActionBar,Web 上是一个底栏加一个浮动按钮 |
| 带停靠高度的弹出面板 | .sheet、.presentationDetents |
modal | ✓ Sheet(Detents、Grabber) |
| 全屏模态 | .fullScreenCover |
presentation: "fullScreenModal" |
✓ 设置了 FullScreen 的 Sheet(iOS 的 .fullScreen;Material 的全屏对话框,铺满屏幕边缘,支持预测性返回;桌面端是盖住整个窗口内容的弹出面板或窗口;Web 上是盖住整个页面的 <dialog>) |
| 拦截返回、阻止关闭 | .navigationBarBackButtonHidden、.interactiveDismissDisabled |
BackHandler、usePreventRemove |
✓ OnBack(页面、弹出面板、整个应用) |
| 气泡弹窗 | .popover |
— | ✓ Popover(在 iPhone 上也是 popover) |
| 警告框、操作表 | .alert、.confirmationDialog |
Alert |
✓ Alert(两种样式都支持;警告框可以带输入框:Fields,密码框用 Secure,按钮通过 Disabled 跟随输入内容) |
| Snackbar、toast(带“撤销”的简短提示) | — | 社区方案(react-native-paper 的 Snackbar) |
✓ Snackbar(Android 上是 Material 的 Snackbar,Linux 上是 AdwToast;iOS、macOS、Windows 和 Web 上按平台的样式绘制) |
| 深度链接、通用链接和 App Links | .onOpenURL |
Linking | ✓ Router 自己处理能匹配其路由的链接(应用 scheme 的链接、通用链接和 App Links、通知里的 URL),其余交给 OnOpenURL;asuka.json 里的 scheme 和 links |
| URL 路由:路径模式、嵌套导航器、链接下方的初始页面、重定向、守卫、未找到页面 | 配合 Codable 值的 NavigationPath |
expo-router([id]、[...rest]、unstable_settings、<Redirect>、Stack.Protected、+not-found) |
✓ Router 和 Route(:id、*rest、Initial、Redirect、Guard、NotFound;导航栈、标签页、分栏视图、slot 和弹出面板都可以是路由),Navigate、Push、Replace、Back、SetQuery,NavLink 以及视图和按钮上的 Href;导航状态能从地址恢复(刷新页面、桌面窗口上次的位置、asuka dev 的重新加载) |
| 类型安全的链接 | NavigationPath 的值 |
类型化路由(生成的 .d.ts) |
✓ asuka vet 的 routes 检查(没有路由能匹配的常量 href、HrefOf 的参数个数);ui.HrefOf 按路由的模式填入参数 |
| 下拉刷新、滑动操作 | .refreshable、.swipeActions |
RefreshControl |
✓(两侧都支持:SwipeActions、LeadingSwipeActions) |
动画
| SwiftUI | Expo (Reanimated) | Asuka | |
|---|---|---|---|
| 为变化添加动画 | withAnimation |
withTiming、withSpring |
✓ Animate、AnimateWith(缓动、线性、弹簧动画;重复、自动反向) |
| 变换 | .rotationEffect、.scaleEffect、.offset |
transform |
✓ Style.Transform |
| 进入和退出过渡 | .transition |
entering/exiting | ✓ Style.Transition(Fade、Pop、SlideUp,也可以自定义) |
| 逐帧动画 | TimelineView(.animation) |
useFrameCallback |
✓ OnFrame |
| 阴影、渐变、模糊 | .shadow、LinearGradient、材质 |
样式、expo-linear-gradient、expo-blur | ✓ Shadow、LinearGradient、Material |
| 手势 | 点按、长按、拖动、缩放、旋转 | Gesture Handler | ✓ 点按、长按、平移、捏合、旋转、轻扫 |
| 硬件键盘的按键 | .onKeyPress、.keyboardShortcut |
onKeyPress(TextInput 上的;React Native for Windows and macOS 上是 onKeyDown) |
✓ ViewProps.Keys、TextFieldProps.Keys(从获得焦点的视图逐层向外传递,先于控件和菜单处理)、UseKeys(整个窗口的) |
| 按下和悬停状态 | ButtonStyle 的 isPressed、.onHover |
Pressable 的 pressed、onHoverIn/onHoverOut |
✓ Style.Pressed、Hovered 变体;用 Interaction 跟随分组(group)或相邻元素(peer)的状态(Style.When);OnPressedChange、OnHoverChange |
| 拖放 | .draggable、.dropDestination |
(没有内置) | ✓ Drag、Drop(应用内可以拖放 Go 值;与其他应用之间可以拖放文本、链接和文件;Dragging 和 DragOver 变体) |
| 共享元素过渡 | matchedGeometryEffect、.navigationTransition(.zoom) |
shared transitions | ✓ Style.Match(一个视图从另一个视图的位置和大小中展开)、ScreenOptions.ZoomFrom(推入的页面从某个视图中缩放展开:iOS 18 的缩放过渡;Material 的容器变换) |
| 触感反馈 | sensoryFeedback |
expo-haptics |
✓ Haptic |
| 减弱动态效果 | accessibilityReduceMotion |
useReducedMotion |
✓ Environment.ReduceMotion(iOS 的过渡改为淡入淡出) |
数据与服务
| SwiftUI / Apple | Expo | Asuka | |
|---|---|---|---|
| 状态 | @State、@Observable |
hooks | ✓ signal(细粒度:一次写入只更新读取了它的地方) |
| 持久化少量数据 | @AppStorage |
AsyncStorage | ✓ asuka/sdk/store |
| SQLite | SwiftData / GRDB | expo-sqlite |
✓ asuka/sdk/sqlite(迁移、带类型的查询、事务、在主线程之外写入、实时查询:读取的表被写入时会重新运行,参数变化时也会,见 WatchWith;浏览器里也能用,基于 SQLite 的 WebAssembly 版本) |
| 安全存储 | Keychain | expo-secure-store |
✓ asuka/sdk/secure(在 Web 上:加密后存进 IndexedDB,密钥由浏览器保管、无法导出;并不是真正的钥匙串) |
| 文件、网络 | Foundation | expo-file-system、fetch |
✓ Go 的 os、net/http |
| 照片 | PhotosPicker |
expo-image-picker |
✓ device.PickPhotos |
| 文档 | UIDocumentPickerViewController |
expo-document-picker |
✓ device.PickFiles(MIME 类型、多选) |
| 相机 | AVFoundation | expo-camera |
✓ asuka/sdk/camera:Take(系统相机;在 Mac、Windows 和 Linux 上是一个带实时预览的对话框;Windows 上用 Media Foundation 的采集引擎;Linux 上用相机 portal,或通过 GStreamer 调用 Video4Linux)、View(实时预览,前置或后置摄像头)、Capture;asuka/sdk/camera/record 可以录制带声音的视频 |
| 条形码、二维码 | AVFoundation、Vision | expo-camera 的扫描功能 |
✓ asuka/sdk/scan:可以在 camera.View 里实时识别(Scan: scan.Codes(...)),也可以识别图片文件(scan.File);Android 上用 ML Kit,只会打包进导入了它的应用;Linux 上用 ZBar;Windows 上用 Asuka 自己用 Go 写的识别器(internal/barcode:QR、Data Matrix、EAN、UPC、Code 128、Code 39) |
| 定位 | Core Location | expo-location |
✓ asuka/sdk/location(地名查询:Windows 上用 OpenStreetMap 的 Nominatim);“使用期间”和“始终”两级权限,精确或大致位置;后台定位更新和地理围栏(相当于 expo-task-manager 的任务:iOS 和 Android 上会为这些事件启动应用;系统不提供时,在应用运行期间根据位置计算) |
| 翻译、复数、区域格式 | String Catalogs、FormatStyle | expo-localization、i18next |
✓ asuka/i18n、翻译目录(多语言) |
| 从右到左布局 | layoutDirection |
I18nManager |
✓ 跟随语言,也可用 ui.WithDirection 设置 |
| 本地通知 | UserNotifications | expo-notifications |
✓ asuka/sdk/notify(立即或定时发送,支持图片;iOS、Android、macOS、Linux;Windows 上是 toast 通知,通过应用的 COM 激活器支持按钮和回复;Web 上通过应用的 service worker 发送,所以页面关闭后通知仍然保留、仍能响应操作;定时通知在应用有页面打开时会按时显示,否则等下次打开页面时再显示:浏览器无法预约通知) |
| 推送通知 | APNs | expo-notifications |
✓ asuka/sdk/push(iOS 和 macOS 上用 APNs,另有 Firebase Cloud Messaging,Web 上用基于 VAPID 的 Web Push:获取令牌、接收消息、处理点按;asuka/sdk/push/webpush 可以从 Go 服务器发送 Web Push) |
| 通知操作、内联回复 | UNNotificationCategory、UNTextInputNotificationAction |
setNotificationCategoryAsync |
✓ notify.SetCategories、OnAction(后台操作会运行应用的 Go 代码,应用没在运行时会以无界面方式启动;Response.Defer;Android 上回复会被添加回对话里;在 Web 上用 service worker 的通知操作,Chrome 支持的地方还有内联回复) |
| 通知分组、当前显示的通知、角标 | threadIdentifier、getDeliveredNotifications、setBadgeCount |
getPresentedNotificationsAsync、setBadgeCountAsync |
✓ Thread(Android:一个通知组,并保留它的摘要通知)、notify.Showing、Remove、RemoveThread、SetBadge(Android:启动器根据通知显示的角标) |
| 前台通知 | willPresent |
setNotificationHandler |
✓ notify.Present(横幅、静默、隐藏) |
| Android 通知渠道 | — | setNotificationChannelAsync |
✓ notify.SetChannels、SetChannelGroups;推送的 channel_id(iOS 没有渠道) |
| 对话类消息 | 通讯通知(INSendMessageIntent) |
社区方案 | ✓ Notification.From、push.Message.From:iOS 的通讯通知(只给用到它的应用添加 entitlement),Android 的 MessagingStyle 和对话快捷方式;macOS 上显示为普通通知;不支持气泡 |
| 在推送显示前修改内容 | Notification Service Extension | 社区方案(expo-notifications 不支持) |
✓ push.Rewrite:iOS 上在扩展里运行应用的 Go 代码(由 asuka build 构建并签名),Android 的数据消息则在应用进程里处理 |
| 剪贴板、分享面板、浏览器 | UIPasteboard、ShareLink、SFSafariViewController |
expo-clipboard、expo-sharing、expo-web-browser |
✓ Copy、device.Clipboard(文本;图片用 CopyImage、ClipboardImage;ClipboardHas、OnClipboardChange)、Share(文本)、ShareFiles(文件,支持 expo-sharing 那样的 MIME 类型、UTI、标题和 iPad 锚点;分享面板关闭时才返回)、device.Browse |
| 预览文件 | QLPreviewController、.quickLookPreview |
react-native-file-viewer |
✓ asuka/sdk/preview(iOS 和 Mac 上用 Quick Look,支持多个文件和标记(Markup);Android 上交给查看器应用;Windows 和 Linux 上用默认应用打开) |
| 打印、从 HTML 生成 PDF | UIPrintInteractionController、UIPrintPageRenderer |
expo-print |
✓ asuka/sdk/print(可以打印文件、HTML 或 WebView,使用系统的打印对话框并拿到结果;PDF:自动分页、按地区选择纸张、页边距) |
| 邮件和短信编辑界面 | MFMailComposeViewController、MFMessageComposeViewController |
expo-mail-composer、expo-sms |
✓ asuka/sdk/compose(收件人、主题、HTML 正文、附件;iOS 上能知道用户最后做了什么;Android 上只列出邮件应用;Mac 上支持“信息”) |
| 保持屏幕常亮、设备信息 | isIdleTimerDisabled、UIDevice |
expo-keep-awake、expo-device |
✓ device.KeepAwake、device.GetInfo |
| 网络状态 | NWPathMonitor |
expo-network |
✓ asuka/sdk/network(是否在线、Wi-Fi 还是蜂窝网络、是否按流量计费、流量节省程序 / 低数据模式) |
| 保存到相册、下载目录 | PHPhotoLibrary |
expo-media-library |
✓ asuka/sdk/library(Android 上是 MediaStore 的各个集合;iOS 上是“照片”,或“文件”里的 Documents) |
| 后台任务 | beginBackgroundTask、BGTaskScheduler |
expo-task-manager、expo-background-task |
✓ asuka/sdk/background:Android 会让应用保持运行,进度和按钮显示在一条通知里;iOS 大约给 30 秒;Mac 会让应用保持运行(关闭 App Nap),Linux 和 Windows 会阻止系统休眠。asuka/sdk/periodic:由系统不定期运行的任务,以无界面方式启动应用的 Go 代码(BGTaskScheduler、WorkManager;Mac 上在应用运行期间用 NSBackgroundActivityScheduler;Linux 和 Windows 上在应用运行期间用定时器) |
| 接收其他应用的分享 | Share extension | expo-sharing(useIncomingShare) |
✓ ui.OnShare:文本、链接、图片、视频、音频和文件,单个或多个,附带名称、MIME 类型和大小(由 asuka.json 的 shareTarget 指定类型和数量上限,并据此生成 Android 的 intent filter 和 iOS 的激活规则;iOS 上由分享扩展打开应用,文件通过 App Group 传递);app.ShareSheet:在发起分享的应用上方显示应用自己的界面(iOS 上是它的分享扩展;Android 上是运行在独立进程里的底部面板),数据与应用共享;不解析链接内容(请用 net/http) |
| 音频 | AVAudioPlayer、AVAudioRecorder | expo-audio |
✓ asuka/sdk/audio(加载、播放、暂停、跳转、音量、循环);asuka/sdk/audio/record(语音或音乐音质、暂停、电平表) |
| 正在播放、锁屏控制、媒体键 | MPNowPlayingInfoCenter、MPRemoteCommandCenter |
expo-audio 的锁屏控制、社区方案(react-native-track-player) |
✓ asuka/sdk/nowplaying(标题、艺人、专辑、封面、时长、由系统推进的播放位置,以及播放、暂停、下一首、上一首、跳转、快进快退、停止,设置了哪些处理函数就启用哪些控制;会自动跟随 audio.Player;Android 上用 MediaSession 和媒体播放服务;Mac 上是控制中心,Windows 上是媒体浮出控件,Linux 上是 MPRIS,Web 上是 Media Session API) |
| 通过 Apple 登录 | SignInWithAppleButton |
expo-apple-authentication |
✓ asuka/sdk/appleauth(Apple 的登录按钮、凭据及其状态;iOS 和 macOS) |
| Apple Pay、Google Pay | PassKit(PayWithApplePayButton) |
@stripe/stripe-react-native 的 Platform Pay、react-native-payments |
✓ asuka/sdk/pay(钱包的支付面板和按钮;交给应用支付处理商的令牌;修改配送、优惠码和卡片时更新总额;按字段显示错误;订阅、延后扣款和自动充值;iOS、macOS、Android,Safari 中的 Apple Pay 和其他浏览器中的 Google Pay) |
| 面容 ID、指纹 | LocalAuthentication | expo-local-authentication |
✓ asuka/sdk/biometric(Windows 上用 Windows Hello;Web 上通过 WebAuthn 调用平台认证器) |
| 应用内购买、订阅 | StoreKit 2 | expo-iap、react-native-iap、RevenueCat |
✓ asuka/sdk/purchase(带本地化价格和优惠的商品、购买、“询问购买”和待付款、从应用启动起的交易流:续订、退款、在应用外的购买;完成和消耗交易、作为 signal 的已购权益、恢复购买、订阅状态、Play 的升级和降级、管理订阅、申请退款和兑换优惠码的界面、供服务器验证的 JWS 和购买令牌;iOS 和 macOS 上用 StoreKit 2,Android 上用 Play Billing 9,Windows 上用 Microsoft Store,Trusted Web Activity 里用 Digital Goods API;测试用 fake.Store) |
| 应用商店评分 | requestReview |
expo-store-review |
✓ asuka/sdk/review(App Store 的评分弹窗,Play 的应用内评价) |
| 跟踪权限 | App Tracking Transparency | expo-tracking-transparency |
✓ asuka/sdk/tracking |
| 应用完整性 | App Attest | expo-app-integrity |
✓ asuka/sdk/integrity(App Attest、Play Integrity) |
| 年龄段 | Declared Age Range | expo-age-range |
✓ asuka/sdk/agerange(Declared Age Range、Play Age Signals) |
| 传感器 | Core Motion | expo-sensors |
✓ asuka/sdk/sensors(加速度计、陀螺仪、磁力计、带姿态和朝向的设备运动、气压计、光线、计步器;两个平台上坐标轴和单位一致;Mac 和 Windows 没有传感器;Linux 上是 iio-sensor-proxy 提供的光线和方向) |
| NFC 标签 | Core NFC | react-native-nfc-manager |
✓ asuka/sdk/nfc(读写 NDEF,支持文本、URI、MIME、外部类型和智能海报(smart poster)记录,可将标签设为只读、查询容量;标签的 ID 和技术类型;ISO 7816 APDU、MIFARE、FeliCa 和 ISO 15693 命令,以及 Android 的 NfcA/B/F/V、IsoDep 和 MIFARE Classic;iOS 的系统面板、Android 的读卡器模式;能打开应用的标签:iOS 通过通用链接,Android 通过 asuka.json 中配置的 NDEF_DISCOVERED 和 TECH_DISCOVERED;Web 上是 Chrome for Android 的 Web NFC,仅支持 NDEF;桌面端没有) |
| 低功耗蓝牙 | Core Bluetooth | 社区方案(react-native-ble-plx) |
✓ asuka/sdk/bluetooth(按服务过滤的扫描、重复上报和 RSSI、广播数据;连接及其断开原因、服务、读取、带响应和不带响应的写入、通知和指示、MTU、RSSI、Android 的连接优先级;已知设备和系统已连接的设备;广播和 GATT 服务端;iOS 的后台模式和状态恢复、Android 的后台扫描;Windows 上是 Windows.Devices.Bluetooth,Linux 上是 BlueZ;Web 上是 Web Bluetooth 的设备选择器,仅限 Chrome 和 Edge,没有外围设备角色) |
| 图片编辑 | Core Graphics、ImageIO | expo-image-manipulator |
✓ asuka/sdk/imageedit(缩放、裁剪、旋转、翻转、填充、像素化;可以读入 HEIC,输出 JPEG 或 PNG,方向摆正,并去掉原图的元数据;Linux 上用 GdkPixbuf 的解码器,Windows 上用 WIC 的解码器) |
| 识别图片里的文字、人脸和条码 | Vision | 无 | ✓ asuka/sdk/vision,会给出每一项的位置;Android 上用 ML Kit,只会打包进导入了它的应用;Linux 上用 ZBar 识别条码、Tesseract 识别文字,不支持人脸;Windows 上用系统 OCR 识别文字和人脸,用 Asuka 自己的识别器识别条码 |
| 照片图库 | PhotoKit | expo-media-library |
✓ asuka/sdk/photos:权限、列出照片、缩略图、原始文件(Windows 上是“图片”文件夹) |
| 视频缩略图 | AVAssetImageGenerator |
expo-video-thumbnails |
✓ imageedit.VideoFrame |
| 文字转语音 | AVSpeechSynthesizer |
expo-speech |
✓ asuka/sdk/speech(朗读队列、声音、音调、语速、在某个词处暂停) |
| 通讯录 | Contacts、ContactsUI | expo-contacts |
✓ asuka/sdk/contacts(联系人选择器和新建联系人界面不需要权限;有权限后可以访问通讯录;Windows 上添加联系人会打开一个 vCard,无法访问通讯录) |
| 日历 | EventKit、EventKitUI | expo-calendar |
✓ asuka/sdk/calendar(事件编辑器不需要权限;有权限后可以访问日历和事件;Windows 上添加事件会打开一个 iCalendar 文件,无法访问日历) |
| 主屏幕小组件 | WidgetKit | expo-widgets |
✓ asuka/sdk/widget 和 asuka.json 的 widgets(所有尺寸共用一个模板,iOS 上还有锁屏小组件;时间线、倒计时、点按时打开 URL) |
| 实时活动 | ActivityKit | expo-live-activity |
✓ asuka/sdk/liveactivity(iOS:锁屏和灵动岛;Android:常驻通知,即 Android 16 的实时更新;倒计时、进度、提醒,以及由服务器推送来更新和启动) |
| App Intents、快捷指令 | App Intents、App Shortcuts | — | ✓ asuka.json 的 shortcuts(iOS 上是带 Siri 短语的 App Shortcuts,会出现在聚焦搜索和“快捷指令”里;Android 上是启动器快捷方式) |
| 自定义字体 | .font(.custom) |
expo-font |
✓ Font.Family |
| 生命周期 | scenePhase |
AppState | ✓ app.OnLifecycle |
| 启动屏 | UILaunchScreen |
expo-splash-screen |
✓ asuka.json 的 splash;asuka/sdk/splash 可以在应用加载期间让它一直显示 |
| 状态栏、导航栏 | statusBarHidden、preferredStatusBarStyle |
expo-status-bar、expo-navigation-bar |
✓ asuka/sdk/screen 的 SetBars |
| 锁定屏幕方向 | supportedInterfaceOrientations |
expo-screen-orientation |
✓ asuka/sdk/screen 的 Lock |
| 截屏 | isCaptured、userDidTakeScreenshotNotification |
expo-screen-capture |
✓ asuka/sdk/screen 的 PreventCapture(Android 上截图是空白的;iOS 上录屏时内容会被遮住;macOS 和 Windows 上截图里不包含这些窗口)、OnScreenshot(Linux 和 Windows 上通过 Pictures/Screenshots 里新出现的图片来判断) |
| 崩溃报告 | MetricKit、Crashlytics、Sentry | Sentry、Crashlytics(@react-native-firebase/crashlytics) |
✓ asuka/sdk/crash:Go 的 panic 和致命错误、未捕获的异常、MetricKit 记录的崩溃和卡顿、Android 的原生崩溃和 ANR,附带面包屑和自定义值;报告发到哪里由应用决定(没有内置服务);asuka build 会生成 dSYM 和原生调试符号 |
外观与无障碍
| SwiftUI | Expo | Asuka | |
|---|---|---|---|
| 深色模式、语义色、强调色 | 自动处理 | useColorScheme |
✓ 语义色、asuka.json 的强调色(Android 上用 Material 3 动态取色;Mac 上 AppKit 自己的控件保留用户设置的强调色) |
| 动态字体、字体缩放 | 自动处理 | allowFontScaling |
✓ 文本样式和自定义字体都会跟着缩放 |
| 屏幕阅读器的标签、角色、隐藏 | .accessibilityLabel... |
accessibilityLabel... |
✓ Label、Hint、Role、Decorative;可点按的视图会被识别为按钮 |
| 播报 | AccessibilityNotification.Announcement |
announceForAccessibility |
✓ Announce |
| 移动屏幕阅读器的焦点 | @AccessibilityFocusState |
setAccessibilityFocus |
✓ AccessibilityFocus |
| 屏幕阅读器是否开启 | voiceOverEnabled |
isScreenReaderEnabled |
✓ Environment.ScreenReader |
| 自定义操作、可调节控件 | .accessibilityAction、.accessibilityAdjustableAction |
accessibilityActions |
✓ ViewProps.Actions、OnAdjust(List 的滑动操作也会成为 TalkBack 的操作) |
工具链
| Xcode / Expo | Asuka | |
|---|---|---|
| 实时重载并保留状态 | 预览、Fast Refresh | ✓ asuka dev:保存即重载,并保留 signal 的值(在手机上,Go 代码运行在你的电脑上;在桌面端,应用会重新构建并运行;在浏览器里,页面会重新加载) |
| 在手机上运行 | Xcode 的 Run、Expo 的开发构建 | ✓ asuka dev --device:iOS 模拟器、Android 模拟器、Android 手机(adb reverse),以及通过 USB 或 Wi-Fi 连接的 iPhone(由你的团队签名,描述文件由 Xcode 生成;优先走 usbmuxd 的隧道,否则走局域网) |
| 发布构建和签名 | Xcode、EAS Build | ✓ asuka build:.ipa、.aab、经过签名和公证的通用 .app(或 Mac App Store 用的 .pkg)、包含已签名 .exe 的 Windows 文件夹、附带 .tar.gz 和 Flatpak 清单的 Linux 安装前缀、静态网站(发布) |
| 上传到应用商店 | Xcode、EAS Submit | ✓ asuka publish:TestFlight、App 审核、Google Play 的各个发布轨道;暂不支持 Mac App Store |
| GUI 测试 | XCUITest、Detox、Maestro | ✓ e2e/:在 iOS 模拟器、Android 模拟器、Xvfb 下的 Linux 和无头 Chrome 里运行示例;Mac 和 Windows 的宿主测试会驱动真实的 AppKit 视图和 Win32 窗口 |
| 基准测试 | Instruments | ✓ bench/(与 SwiftUI 和 Compose 对比) |
| 嵌入现有应用 | — | brownfield(React Native 的方案) |
| OTA 更新 | — | EAS Update:✗ |
与桌面端框架对比
在 Mac、Windows 和 Linux 上,很多应用是用 web view 包一层 HTML 做出来的。这些框架和 Asuka 的对比如下:
| Electron | Tauri | Wails | React Native for Windows and macOS | Asuka | |
|---|---|---|---|---|---|
| 平台 | Mac、Windows、Linux | Mac、Windows、Linux;iOS 和 Android(Tauri 2) | Mac、Windows、Linux | Windows、Mac(配合 React Native 的 iOS 和 Android) | Mac、Windows、Linux、iOS、Android、Web |
| 界面由什么呈现 | Chromium 里的 HTML 和 CSS | 系统 web view(WKWebView、WebView2、WebKitGTK)里的 HTML 和 CSS | 系统 web view 里的 HTML 和 CSS | 平台的原生视图 | 平台的原生控件:AppKit;Win32 和通用控件,按 Windows 11 的样子绘制;GTK 4 和 libadwaita |
| 应用使用的语言 | JavaScript(页面之外还有 Node.js) | Rust,加上页面里的 JavaScript | Go,加上页面里的 JavaScript | JavaScript | Go |
| 随应用分发的内容 | Chromium 和 Node.js | 应用的二进制文件(web view 用系统自带的) | 应用的二进制文件(同上) | 一个 JavaScript 引擎和 React Native 的运行时 | 应用的 Go 二进制文件(Linux 上的 GTK 4 和 libadwaita 由系统提供);除非应用显示了 WebView,否则不带 web view |
web view 应用在所有系统上都长得一样,完全按它的 HTML 和 CSS 呈现,并且能用上 Web 生态里的各种库。Asuka 应用则和各平台的原生应用看起来一样(不设置任何样式的应用,在 Mac 上就是一个 Mac 应用,在 Linux 上就是一个 GNOME 应用,在 Windows 上就是一个 Windows 11 应用),也能获得它们的无障碍支持、文本输入和菜单,同时所有平台共用一套用 Go 写的布局引擎。桌面宿主在应用自己的进程里运行 Go 代码,不借助 cgo 就能调用平台接口,所以 asuka build 在任何系统上都能构建 Windows 和 Linux 版本。
桌面应用的能力
桌面应用在窗口和菜单栏之外还需要哪些能力,这里与 Electron 和 MyGo 做个对比(见桌面端)。除非该行另有说明,✓ 表示 Mac、Windows 和 Linux 都支持。
| Electron | MyGo | Asuka | |
|---|---|---|---|
| 控制窗口 | BrowserWindow |
Window |
✓ ui.UseWindow、ui.MainWindow:显示、隐藏、聚焦、最小化、最大化、全屏、大小、位置、置顶,窗口状态以 signal 形式提供;OnCloseRequest;按 StateKey 记住窗口位置;子窗口和模态窗口;隐藏标题栏(Wayland 上无法设置位置、居中和置顶,这些由合成器决定;X11 上通过 Xlib 实现) |
| 托盘图标 | Tray |
托盘图标 | ✓ tray.Show(Mac 上是 NSStatusItem;Windows 上是通知区域;Linux 上是 StatusNotifierItem):图标或符号、标题、悬停提示、菜单、点击 |
| 子菜单、退出菜单项 | MenuItem.submenu、roles |
roles、子菜单 | ✓ MenuItem.Submenu、ui.MenuQuit,可用于菜单栏、托盘菜单和程序坞菜单 |
| 程序坞菜单、跳转列表任务 | app.dock.setMenu、setUserTasks |
程序坞菜单、跳转列表 | ✓ tray.DockMenu(程序坞菜单、跳转列表里的“任务”、Linux 启动器的快捷列表) |
| 在后台运行的应用 | app.dock.hide,以及在 window-all-closed 时不退出 |
后台应用 | ✓ app.KeepRunning、app.RunInBackground、asuka.json 的 background |
| Flyout:从托盘图标弹出的窗口 | 由应用自己把无边框的 BrowserWindow 摆到 tray.getBounds() 的位置 |
flyout | ✓ WindowProps.Flyout,从 tray.Icon.Anchor() 或所在窗口的某个矩形区域弹出:在显示器范围内摆放,放不下时翻转方向,点击别处或按 Escape 关闭,上面还可以再打开模态对话框(Linux 上只有 X11 能定位) |
| 启动器面板 | type: 'panel'(macOS) |
面板 | ✓ Panel:浮在所有应用和所有桌面空间(Space)之上,在 Mac 上能接收键盘输入而不激活应用,按 Escape 或点击别处时关闭(Linux 的 Wayland 下就是普通窗口) |
| 窗口大小随内容变化 | 由应用自己调用 setContentSize |
FitContent |
✓ FitContent:内容变化时,由核心的布局来决定窗口大小 |
| 点击穿透窗口 | setIgnoreMouseEvents |
SetIgnoreMouseEvents、SetInputRegion |
✓ ClickThrough、WindowHandle.SetClickThrough(作用于整个窗口) |
| 列出已安装的应用(用于启动器) | 无 | InstalledApps、FileIcon、LaunchApp |
✓ asuka/sdk/apps 的 List、Search、Icon、Open |
| 全局快捷键 | globalShortcut |
全局快捷键 | ✓ hotkey.Register(Mac 上用 Carbon 的热键;Windows 上用 RegisterHotKey;Linux 上用 X11 的按键抓取或 Wayland 的 GlobalShortcuts portal) |
| 打开、保存、选择文件夹对话框 | dialog.showOpenDialog、showSaveDialog |
对话框 | ✓ asuka/sdk/dialog 的 Open、Save、Folder(Mac 上以 sheet 显示;Web 上用 File System Access 选择器或文件输入框;手机上只支持 Open) |
| 在文件夹中显示、用默认应用打开、移到废纸篓 | shell.showItemInFolder、openPath、trashItem |
shell | ✓ asuka/sdk/shell 的 Reveal、Open、Trash、Beep |
| 文件关联 | fileAssociations(electron-builder)、open-file |
文件关联 | ✓ asuka.json 的 fileTypes、ui.OnOpenFiles(iOS、Android 和已安装的 Web 应用也支持) |
| 单实例,以及处理再次启动 | requestSingleInstanceLock、second-instance |
单实例 | ✓ app.OnSecondInstance、asuka.json 的 singleInstance(Mac 上传过来的是文件,而不是命令行参数) |
| 退出,以及退出前询问 | app.quit、before-quit |
退出 | ✓ app.Quit、app.OnQuit(Cancel、SessionEnding) |
| 应用的各类目录 | app.getPath |
目录 | ✓ app.DataDir、ConfigDir、CacheDir、LogDir(所有平台都可用) |
| 最近使用的文档 | app.addRecentDocument |
最近使用的文档 | ✓ asuka/sdk/recent(Mac 上的“打开最近使用”、Windows 的跳转列表、Linux 上用户的最近文件) |
| 基于文档的应用 | — | 文档 | ✓ asuka/sdk/document:每个文件一个窗口、File 菜单、自动保存、恢复副本、关闭和退出时询问是否保存、处理被其他应用改动的文件(手机和 Web 上一次只能打开一个文档,原地保存) |
| 登录时启动 | app.setLoginItemSettings |
登录时启动 | ✓ asuka/sdk/login(Mac 上用 SMAppService,需要 macOS 13+;Windows 上用注册表的 Run 键;Linux 上用 XDG autostart 或 Background portal) |
| 监视文件 | Node 的 fs.watch(chokidar) |
WatchFiles、watch 插件 |
✓ asuka/sdk/watch 的 Path、New(FSEvents、inotify、ReadDirectoryChangesW;也支持 iOS 的 kqueue、Android 的 inotify,以及 Web 上应用自己的文件):重命名会成对报告,原子保存视为一次写入,短时间内的连续变化会合并 |
| 显示器 | screen |
显示器 | ✓ screen.UseDisplays、Nearest、Matching(Wayland 上拿不到工作区域;手机和 Web 上返回各自的屏幕) |
| 睡眠、唤醒、锁屏、空闲时间、电池 | powerMonitor |
电源 | ✓ asuka/sdk/power(手机上支持电池、低电量模式和锁屏;Web 上只有电池) |
| 应用图标上的进度和提醒 | setProgressBar、flashFrame、dock.bounce |
程序坞和任务栏 | ✓ dock.SetProgress、RequestAttention(Linux 上需要 dock 支持 Unity launcher API) |
| 强调色、增强对比度、降低透明度 | systemPreferences |
强调色和无障碍设置 | ✓ asuka/sdk/appearance(Linux 上没有降低透明度) |
| 正在播放和媒体键 | (无:只能用页面里的 navigator.mediaSession) |
正在播放 | ✓ asuka/sdk/nowplaying(Mac 上用 MPNowPlayingInfoCenter;Windows 上用窗口的 SystemMediaTransportControls;Linux 上是一个 MPRIS 播放器) |
| 自动更新:签名发布、增量更新、Sparkle 那样的更新窗口 | autoUpdater(Squirrel)、electron-updater |
更新器及其插件 | ✓ asuka/sdk/update(Window、CheckForUpdates、Check、Install、Relaunch;Ed25519 签名、增量更新、更新渠道;可以发布到 GitHub releases、S3 存储桶、任意 HTTPS 目录,或者你自己的服务器;asuka publish --platform mac|windows|linux);通过系统包管理器、Flatpak 或应用商店安装的应用由它们负责更新;Web 则重新加载页面 |
MyGo 有而 Asuka 还没有的:文档方面,缺少版本(Mac 上的“浏览所有版本”)和锁定;点击穿透窗口方面,缺少输入区域(也就是窗口里仍然接收点击的那部分:Asuka 的窗口要么整个让鼠标穿过,要么完全不穿过)。
尚未支持
按应用最常缺的程度排序:带参数和实体的 App Intents(目前的快捷指令都不能带参数)、使用应用自定义布局的小组件(目前只有一个模板),以及 OTA 更新(即 EAS Update 那样的功能;Asuka 应用是编译好的 Go 代码,而应用商店不允许应用自行替换它)。Mac 上还缺:ScreenOptions.ZoomFrom 的缩放效果(推入时改为滑动)、用手势开始列表的多选,以及用 asuka publish 上传到 Mac App Store(目前请用 Transporter 上传 .pkg)。