加快跨平台开发
我在跨平台移动开发方面的经验缺少一些重要的元素,例如 Flutter 或 Xamarin。因此,本文并非对该领域工具的全面分析。
多年来,我尝试过一些不同的跨平台开发工具,包括 PhoneGap(事后看来,我可能应该避免使用它)、React Native、Qt 以及一些 Kotlin Native。总的来说,我坚信 UI 应该是原生的,而像 PhoneGap 这样的工具只能用于开发简单的应用。虽然 React Native 有其优缺点,但它并没有赢得我作为一名开发者的青睐。相反,我更喜欢拥有跨平台核心和原生 UI 的想法。作为一个在 KitKat(4.4?🤔)时期从 Android 转到 iOS 的人,由于我天生更倾向于基于 llvm 的语言,所以 C++ 是我的跨平台代码首选。在 iOS 上,通过名为 Objective-C++ 的混合体来桥接 C++ 和 Objective-C 相对容易。我参与过一些基于此理念的大型项目,我可以证明这是一个可行的解决方案。然而,Objective-C 的流行度正在日益下降,而 Objective-C++ 的使用则更是令人望而生畏。我很难说写它有什么乐趣。此外,我看不出有什么令人信服的理由用 C++ 编写应用程序级代码。或许可以考虑用 C++ 编写操作系统级代码,但那是另一个话题了。在尝试了几次 C++ 之后,我尝试了 Kotlin Native (KN),即使在早期版本中,它也拥有更好的工具和 IDE 支持。Kotlin 是一种阅读和编写都很有趣语言,而且有了“原生”功能,我们甚至可以摆脱 JVM 的束缚。所以,如果你已经沉浸在 Android 生态系统中,热爱 Kotlin,并且喜欢使用 Android Studio,那么 KN 应该是一个不错的选择。然而,在本文中,我想探讨一个更“生疏”的视角。让我们深入探讨一下。
我曾在 iOS 上尝试过几次 Rust,它看起来和 C++ 很像。你构建一个静态库,用 C 头文件作为粘合剂,最终调试起来非常困难。当你只提取一小部分逻辑到共享库中并通过一个精简的接口与之交互时,这种方法很简单。但是,如果你想把大部分应用逻辑都放到共享库中呢?这时事情就变得棘手了。
最近,我在 Rust 伦敦大会上偶然发现了一个吸引我眼球的项目。它叫做Crux,是一个可以帮助你实现函数式核心和命令式 Shell 范式的库。换句话说,它允许你将应用逻辑与 UI 代码分离,并在不同平台之间共享。
虽然函数式核心和命令式外壳的概念听起来很简单,但实际实现起来却可能很棘手。当你开始着手实现它时,你不可避免地会遇到障碍和挑战,尤其是在将核心逻辑与用户界面分离时。
继“变量命名”之后,第二大挑战是找到合适的架构。传统的 MVC/MVP 架构可能并非总是最佳选择,而且我发现很难跟踪我以前使用过的应用程序中的所有数据流。此外,现实世界的用户界面可能非常复杂且动态,这会给 UI 层带来更多状态和交互。
这就是函数式核心“无副作用”理念的由来。Crux 有助于构建基础。对我来说,它非常有助于我理解如何构建代码结构,以及如何以一种既符合人体工程学又易于阅读的方式隔离核心逻辑。几个小时内,我创建了一个可以与 DALL-E API 交互的小应用(很明显,对吧?),并且可以在 3 个平台上运行(实际上是 2.5 个平台,因为我的 Web 开发还没完成😅)。在接下来的部分中,我将分享我的初步印象。
设置
由于该项目尚处于早期阶段,因此设置起来不如 React Native 那么流畅。不过,如果您决定在实际项目中使用此技术栈,那么内部贡献工具也并非难事。事实上,大多数大型项目,即使是单平台项目,也包含大量不同的 Bash 脚本和 Make 文件。这本书很好地解释了它的工作原理,甚至还提供了示例应用程序。
就我个人而言,我觉得最好还是照着书上的方法从头开始搭建项目。这样,如果出现问题,我就能提前知道所有需要查找的地方。我花了不到一个小时就搭建好了核心和 iOS 项目,整个过程非常简单。幸运的是,核心配置都保存在 .rs 和 toml 文件中,非常容易理解。
对于 iOS,你需要一些 Bash 脚本(哦,我讨厌写 Bash)。但就我而言,复制粘贴就足够了,即使需要在 Bash 中进行一些自定义,ChatGPT 也能让生活变得轻松。长话短说,你需要将核心编译为静态库,使用uniffi crate 生成 UI 语言绑定,并将这些步骤添加到 Xcode 项目中,这样你就无需手动重建和重新链接核心了。uniffi 需要编写一个 IDL 接口定义语言文件,描述目标语言可用的方法和数据结构。我分别为 iOS/Android 和 Web 生成了 Swift/Kotlin 和 TS。
UDL 看起来像这样:
namespace core {
sequence<u8> handle_event([ByRef] sequence<u8> msg);
sequence<u8> view();
};
最后,项目结构如下所示(屏幕截图上没有 Android 和 Web):
发展
说到开发,你可能会把时间分配在 Xcode/Android Studio 以及你偏爱的 Rust 和 Web 开发工具之间。我见过一些勇敢的人尝试用 Emacs 进行移动开发,但最终他们的速度明显比队友慢。
好消息是,先处理核心代码非常方便,可以先设计界面、编写测试,然后再切换到 Xcode/Studio 并行完善核心代码。我个人使用 CLion 来开发 Rust,而且我不敢同时打开超过 2 个(CLion/Xcode/Android Studio)。Rust 的编译速度很慢,但这对我来说不是问题,因为我工作中的 Swift/ObjC 项目在一台顶级配置的 MacPro(不是 MacBook🐌)上花了大约 50 分钟才完成干净的构建。然而,对于 Web 开发者来说,这可能有点麻烦。但适当的项目模块化可以解决这个问题。
一开始用 Rust 写代码可能有点难,但我发现很多想法和 Swift 很像,所以体验也差不多。枚举和 Swift 的很像,不是吗?😁
#[derive(Serialize, Deserialize)]
pub enum Event {
Reset,
Ask(String),
Gen(String),
#[serde(skip)]
Set(Result<Response<gpt::ChatCompletion>>),
#[serde(skip)]
SetImage(Result<Response<gpt::PictureMetadata>>),
}
在调试方面,您可以通过lldb 的“breakpoint set”命令设置断点,同时调试链接静态库中的 Swift 和 Rust 代码。虽然它不如在 Android Studio 中调试纯 Kotlin 项目那么方便,但仍然可以完成工作。
例如,即使在 Xcode 内部也可以轻松识别缺少 .env 变量的错误。
日志中的确切行:
然而,我发现单独调试核心和外壳并没有什么问题。事实上,能够独立调试每个组件非常有帮助,因为它可以更容易地找出任何错误或问题的根源。
那么互操作性呢……说实话,它并不理想。具体来说,Rust 和 Swift 之间的互操作不如 Swift/Objective-C 和 Kotlin/Java 之间的无缝衔接。例如,f64无法通过边界传递(这合乎逻辑,但仍然如此)。不过,有一些速查表可以帮助理解互操作规则。对于 Swift,适用以下规则:
- 原语映射到它们明显的 Swift 对应部分(例如
u32
becomeUInt32
、string
becomeString
等)。 - 声明为的对象接口表示
interface T
为 Swift 协议和符合该协议的TProtocol
具体 Swift 类。T
- 枚举声明
enum T
或[Enum] interface T
表示为T
具有适当变体的 Swift 枚举。 - 可选类型使用 Swift 的内置可选类型语法表示
T?
。 - 序列表示为 Swift 数组,映射表示为 Swift 字典。
- 错误表示为符合
Error
协议的 Swift 枚举。 throws
在 Swift 中,具有相关错误类型的函数调用以 标记。
我记得 Kotlin Native 也有类似的规则。实际上,核心和外壳之间的接口应该简洁。我认为这些限制并不好,但也不会造成太大的损害。
建筑学
说到架构模式。你见过不谈模式的移动工程师吗? Crux 的灵感来自 Elm,书中有一篇相当不错的文章,Elm 的文档也值得一读,所以我们就略过描述吧。总的来说,我看到了向单向和消息传递架构的趋势。它们简洁且非常严格,这使得代码更新更容易,并且当一个文本字段在不同层级上呈现三种不同状态时,不会引入不一致。诚然,UIKit 或 Vanila Android 库并非最佳选择(尽管仍然可以复用一些想法),但 SwiftUI 和 Jetpack Compose 非常合适。如果你编写的是手势交互和动画密集型的 UI,这将很有挑战性。比如,如果你进行一些手势驱动的过渡,你应该将当前状态保留在 UI 中还是将其传递给核心?或者,UITableView(iOS)和 RecyclerView(Android)的单元格生命周期略有不同,因此对于单元格模型,核心将如何处理它。这有点挑战性,但仍然可能,一如既往地没有灵丹妙药。
不过,我最喜欢的部分是功能特性。功能特性提供了一种简洁明了的方式来处理副作用,例如网络、数据库和系统框架。当然,您可以用 C 语言编写一个 HTTP 库并将其应用于所有平台,甚至可以将持久化标准化为仅使用 SQLite。但是,需要考虑的因素有很多,例如音频/视频、文件系统、通知、生物识别,甚至 Apple Pencil 等外设。您的系统已经拥有处理这些事务的优秀库,这些库甚至可能经过优化(例如 iOS 上的服务质量或 URLSession 配置),以提高效率。这就是功能特性的作用所在——它们允许您声明所需的功能,同时保留平台代码的实现细节。这是一种保持代码模块化和可维护性的好方法。
当核心处理需要进行 HTTP 调用的事件时,它实际上是在指示 Shell 进行调用。
fn update(&self, event: Self::Event, model: &mut Self::Model, caps: &Self::Capabilities) {
match event {
Event::Ask(question) => {
model.questions_number += 1;
gpt::API::new().make_request(&question, &caps.http).send(Event::Set);
},
...
并且 shell 正在发送请求
switch req.effect {
...
case .http(let hr):
// create and start URLSession task
}
同样的逻辑可以应用于数据库(仅分离 KV 存储和关系)、生物识别等等。
最后的想法
尽管我对 Crux 还不熟悉,并且还不熟练使用 Rust,但我能够构建一个可以在 iOS、Android 和 Web 上运行的简单应用程序,而且所花的时间比从头开始构建这三个应用程序所需的时间要少得多。
Crux 仍处于早期阶段,例如,在我撰写本文时,其 HTTP 功能尚不支持标头和正文。但我对这个项目的持续发展充满期待,因为它背后的想法非常酷,能够吸引更多贡献者。
即使你不想用 Rust 进行跨平台开发,我认为也值得看看这个项目,看看如何在你最喜欢的技术栈中复用其中的一些想法。归根结底,任何能帮助我们编写更好、更模块化、更易于维护的代码的东西都是有益的。
鏂囩珷鏉ユ簮锛�https://dev.to/complexityclass/rusdling-up-cross-platform-development-5en