跨平台不是一个新的话题,它已经被讨论了几十年了。在最近的一些尝试,让我对跨平台有了一些新的想法。在想法真正落地之前,我梳理了一下不同跨平台方案的一些特征,便有了它的几种模式。
故事的开始是这样的,受整洁架构思维的影响,从 2019 年,我便开始一种合适的模式来共享代码模式。我尝试了几种不同的思路:
除此,还有更伟大的 Chapi,可以将任意语言转换为任意语言。当然了,你知道我只是在 YY。
当我们谈论到跨平台的时候,要谈论到桌面操作系统、移动端操作系统。桌面操作系统的跨平台模式和移动端不太一样 —— 桌面端可以使用同一语言。而移动端 Android 主要使用的是 Java、Kotlin,配合游戏开发等使用的 NDK;iOS 主要使用的是 Objective-C、Swift,它们可以直接编译、调用 C++ 库。
在没有操作系统限制编程语言的时候,我们在同一个世界下,使用着同一种编程语言。
模式库,是一系列可复用代码的合集,如前端的组件,通用的工具函数等等。
在我还没有接触 Web 开发之前,我是一个 Qt 粉(Qt 是一个跨平台的 C++ 应用程序开发框架。因为,十几年前对于桌面应用的开发,你并没有太多的选择,要么 GTK 要么 Qt。而我还是一个 KDE 粉,顺带还是一个 OpenSuSE 粉,因为有着最稳定的桌面环境。
过去,CPU 的性能没有这么好,JavaScript 引擎速度没有这么快,Web 浏览器只是个辅助工具。若是想开发跨平台应用,得从底层库开始。
嗯,所以,开发游戏的人们,选择了Qt、wxWidgets、Gtk+ 等框架,作为应用的基础设施。我习惯于将这样的工具称为模式库,因为它们抽象了各种模式到代码中,否则怎么跨平台呢?
在有了 IDE 之后,我们已经不关注于这些底层细节了。但是,我们仍然是基于这些模式库。
交叉编译是指,在一个平台上生成另一个平台上的可执行代码。
在我的大学校园里,我接触最多的就是嵌入式应用的交叉编译,所以我一点儿也不喜欢这个东西。因为它算不上是跨平台的,还依赖于特定 MCU、SoC 的 IDE,我的代码只能运行在特定的平台上。
当我因为贫穷的缘故,我以为我离交叉编译远了——毕竟,你开始一个需要三台机器 Windows、macOS、GNU/Linux,又或者是通过持续集成服务器来做这样的事情。当我只有一台机器的时候,只有卡卡的虚拟机能解决我的矛盾。
直到去年,我使用 Golang 写了 Coca ,我重新认识了一下交叉编译。在 macOS 下,我可以直接编译出可以在 GNU/Linux、Windows 操作系统下运行的
通过平台封装细节,而后提供语言作为 API 来给外部系统调用。
这一点实际上是非常容易理解的,比如我们日常使用的 Ruby、Python 等语言,都能归属于此类。
它们封装了操作系统底层的各种细节,提供了各种 API 抽象。除去部分平台特定代码,只需要拿起源码,便能直接到另外一个平台上运行。
而对于那些没有解释器的操作系统来说,可以采用诸如 Pyinstaller 便可以打包成目标平台的可执行文件。
考虑到嵌入式设备的特殊性, 我将嵌入式运行时,视为一个独立的模式。因为在嵌入式设备上跑语言解释器,你一定需要一个操作系统。反过来,针对于不同的硬件情况,还需要定制大量的 API。采用这一类架构模式的开源应用有:采用 Lua 语言的 NodeMCU,采用 JavaScript 语言的 IoT.js 等。
毫无疑问,这是游戏领域使用 Lua 作为脚本语言,还是 Web 世界被广泛使用的 JavaScript 的一种跨平台架构模式。
Web 应用,是我们使用最广泛的跨平台应用了。甚至于,你并不需要使用同一个厂商的浏览器,就可以运行起同一个 Web 应用。而这些正是浏览器提供了 JavaScript + HTML + CSS。JavaScript,是少数几个可以直接抄起记事本就能撸代码,并能跑起来的语言 —— 毕竟操作系统都提供了 Web 浏览器。
而正由于前端技术的速度发展,生态变得日益完善,使得诸如于 Electron 这样的框架,让越来越多的公司采用它来作为桌面应用开发框架,最具代表性的便是:Visual Studio Code。
PS:Emacs 即是最好的编辑器,也是最好的操作系统。
除了浏览器之外,Emacs 还内置了一个名为 Emacs Lisp 的直译式脚本语言,通过这个语言来扩展这个操作系统的功能。
毫无疑问这种模式的主要目的是,将平台语言作为扩展的开发语言。
构建跨语言平台并不是一件容易的事情。这一部分讲的主要是跨平台移动应用和跨前后端应用。
在过去的几年里,跨平台的移动应用框架非常火热,其中呈上升趋势的便是: React Native 和 Flutter。尽管两个框架的运行机制不是很相同,但是考虑到都是框架 + 语言来封装 Android + iOS 平台的差异性,我还是把它们划到同一类。
PS:顺事一吐槽,尽管从架构上说 Flutter 更加优秀,但是它那该死的布局也只有原生应用开发者会喜欢了。
然而,要开发这样一个 DSL 或者语言,并不是一件容易的事情。从某种意义上来说,我们至少需要 Android x 1 + iOS x 1 + Web x 1 + AppDev x 1。
通过 AST 来进行语言转换,再借助于一系列的 wrapper,来封装目标语言上的框架,以实现使用 A 语言开发 B 语言应用的目标。这一点常见于 Web 前端开发领域。
直接从 A 语言转换 B 语言,并没有太大的问题。但是,在转换的时候,我们需要考虑一下核心是什么?
这一类的工具过于小众了,而且它永远跟不上前端的变化速度。除此,它的写法可能有些奇怪,举个 Scala.js-React 的示例:
val Hello =
ScalaComponent.builder[String]("Hello")
.render_P(name => <.div("Hello there ", name))
.build
这……,好丑。
在我最近的一次 Kotlin2js 的实践中,我发现对于领域模型的转换可能才是语言转换器的核心所在。
即存在一个单独的项目使用 Kotlin 编写,通过它的多平台编译,把它转为其它平台的代码。这样一来,便可以轻松地达到领域模型在其它端的使用。
你知道我在说 JVM,毕竟:Write once, run anywhere。不过 JVM 只是其中的一个,除了它还有 .NET、Parrot 等。
程序语言级别的虚拟化,会将高阶语言转译成一种名为位元组码(Bytecode)的语言,透过虚拟机器转译成为可以直接执行的命令。
嗯,经编译完生成特定的格式后,通过自已的虚拟机就可以转译为可执行命令,就是这么简单直接。
对于一个开发人员来说,我们经常接触到这样的工具,也写过一些。我们也通过它们来做一些 GUI 应用,比如我用得比较多的 ClassyShark。
这一类跨平台、跨语言工具并不常用,因为转成中间语言再编译的话,除了微架构,并不常见。
这里让我们先用暂存器传递语言作为一个示例,我没有这方面的经验。我隐隐约约觉得存在一些情况,需要它,但是我还没有找到合适的例子证明。
暂存器传递语言(英语:register transfer language,缩写为 RTL),又译为暂存器转换语言、寄存器转换语言,一种中间语言,使用于编译器中。
(set (reg:SI 140)
(plus:SI (reg:SI 138)
(reg:SI 139)))
GCC 的前端(frontend)会先将程式语言转译成 RTL,之后再利用后端(backend)转化成机器码。
WebAssembly 是便携式的抽象语法树,被设计来提供比 JavaScript 更快速的编译及运行。对于性能要求高的应用来说,这是一个非常好的技术。有了这一项技术,那么那些使用原生语言开发的桌面应用,就可以更容易地迁移到 Web 平台。
现在,你可以将你的 Golang 编写的代码编译到 WASM,然后提供给 JavaScript 调用了。
我不知道为什么又扯到了这个话题。
我总以为人们会以一种中间 DSL 或者数据格式来作为中间格式,这样一来,可以实现解耦的目的,以适应未来的变化。但是没想到还可能直接生成了对应平台的代码。然后,你拿着代码去各个平台编译一遍。
没的毛病,挺好的,效率更高。
事实上,我相信上面的大部分模式,你都是的懵逼。它们都过于 NB,以致于不是一般人能做的。所以,我们就有了多重平台技术。它利用了各种平台提供的能力,以帮助自己更好地构建跨平台能力。
当然了,随之也提升了 debug 的难度。
移动端应用的第一大挑战是,面对不同移动平台带来的 API 挑战。所以,Cordova 站了出来,支持了九个平台,现在只剩下了五个。
当我们开发一个基于 Cordova 混合应用时,我们便是基于 WebView + Cordova 之上构建我们的应用。大家都已经很熟悉了,这里就不熟悉说明了。
即通过一层层的框架和平台,来打造自己的能力。它对于使用者人员来说,可能相当的友好,但是对于开发者来说,不一定如此。举两个例子:
嗯,随着层数的上升,调试复杂度会越来越多,也越需要一个尽可能的全才。
没有银弹。
围观我的Github Idea墙, 也许,你会遇到心仪的项目