大厂技术 坚持周更 精选好文
本文为来自 教育-智能学习-前端团队 成员的文章,已授权 ELab 发布。
智能学习前端团队 自创立以来,团队专注于打破大众对教育的刻板印象,突破固有的教学思维,攻破各类教学屏障。旨在为每一位学生制定最合适的学习方案,予以因材施教,使优质教育随 "触" 可达。
桌面应用开发
在处于移动互联网的当下,虽然桌面应用的重要性已经不能同往日而语,但在我们平常的日常工作和生活中,还是扮演着非常重要的角色和地位。在我们的日常工作中,离不开Lark、VSCode等桌面应用。
相比较于移动端而言,桌面端应用的生态多种多样,因此也诞生了各种各样的桌面应用开发技术栈。本次分享将会对相关常用的一些桌面应用开发框架进行介绍和分析,同时对当下比较流行(GitHub 50k star)的跨平台桌面应用开发框架Tauri进行介绍。
原生技术栈
原生技术栈是指通过操作系统相关API或者操作系统厂家(如Apple/Microsoft)提供的SDK/工具来开发桌面应用的方式。使用原生技术开发的应用,通常能够在性能、体积以及系统的交互等方面做到非常不错的效果。
- 优点
- 构建产物体积小
- 性能好
- 系统API调用方便
- 兼容性好
- 和系统应用的交互融合度高,如要实现如下的一些系统原生UI组件非常方便
- 缺点
- 无法做到跨平台,所开发的应用只能在对应的平台上运行,如果需要跨平台运行,则需要在不同的操作系统上分别开发,开发成本高
- 对于使用的技术栈有限制(Windows使用C#,macOS使用ObjC/Swift)
Windows平台
作为目前使用率最高的操作系统,Windows平台的GUI程序开发经历了漫长的迭代和演化的过程:Win32 API作为Windows GUI开发的鼻祖,通过C语言调用Windows底层的绘图函数来进行开发;在Win32 API之后出现了MFC(Microsoft Fundation Class),MFC通过C++语法将原有的Win32 API封装成了控件类(对话框控件、按钮控件等);在MFC之后,微软推出了Windows Form(2002年),Windows Form依赖于.NET的运行时,提供了组件化的开发能力;在此之后,微软推出了WPF(Windows Presentation Fundation,2006年),WPF提供了基于XML的语言XAML来描述UI;在Windows8的时代,微软又推出了UWP(Universal Windows Platform,2015年),UWP支持在各种平台上运行(PC/Windows Phone/Xbox),API也支持多种语言(C++/VB/C#/JS)。
从Windows平台应用的开发技术迭代来看,也可以大致看出GUI程序的技术发展史:
- Win32API时代:函数调用,指令式,Windows系统处理
- MFC时代:面向对象,把一些指令式调用封装成类,由来自UI的消息驱动程序处理数据
- Windows Form时代:组件化,在类的基础上封装成组件,消息被封装成事件,事件驱动
- WPF时代:使用类XML语言来描述UI,引入数据驱动UI的理念
- UWP时代:跨平台、多语言
macOS平台
现有的macOS原生应用主要基于Cocoa框架开发,Cocoa是从1980年代由NeXT(macOS的前身)开发的编程环境NeXTSTEP和OPENSTEP演变而来,是面向对象的API。
在2020年的WWDC上,苹果推出了新一代的UI框架SwiftUI,和Flutter/React等现代GUI框架类似,支持声明式的方式使用Swift语言作为DSL来编写UI,同时也支持跨平台的特性,可以在macOS/iOS/tvOS等多平台运行。
Linux平台
Linux其源码只包含了操作系统内核的部分,桌面并不属于Linux源码的一部分,因此严格意义上来说,从「使用系统API和操作系统厂商提供的SDK开发的应用为原生应用」的定义上来说,并无所谓「原生技术栈」的概念。我们日常使用的发行版提供了桌面环境如KDE、Gnome等,Linux发行版的这些桌面环境也提供了相关的一些库或者API来帮助绘制GUI程序,如gtk+等,可以认为是「原生技术栈」。
跨平台技术栈
Web技术栈
Atwood's Law: Any application that can be written in JavaScript, will eventually be written in JavaScript.
一个你或许不知道的冷知识,macOS的系统设置页面是Webview+React写的[1]
提到跨平台,就不得不提Web生态了,Web相关的技术在跨平台中永远是最受青睐的选择,无论是开发的便捷程度,还是庞大的JS开发者生态等等因素,都使得Web技术无论是在移动端还是桌面端的跨平台应用开发上都稳坐使用率最高的技术栈。
- 优点
- 开发成本比原生低,可以方便做到一套代码在不同操作系统上运行
- 实现复杂的UI和动效方便,可以更快地实现一些比较炫酷的UI界面
- 缺点
- 调用系统原生API不方便通常需要使用打包其他的运行时环境或JSBridge的方式来进行调用
Electron
Electron(原名为Atom Shell)是GitHub开发的一个开源框架,最初用来开发Atom编辑器。它通过使用Node.js(作为后端)和Chromium的渲染引擎(作为前端)完成跨平台的桌面GUI应用程序的开发。
- 代表应用:VSCode(303M)、Figma(213M)、Bilibili(397M)、Discord(367M)、 Beta(747M)、1Password8(343M)、MS Teams(264M,根据参考文献[2],微软正在替换Electron的实现,但目前看我电脑中下载的版本解包中,依然还有Electron.framework的文件)
- 优点
- 开发方便,技术栈适合前端同学(UI使用Web技术,系统API交互部分使用NodeJS)
- 缺点
- 打包体积大,需要打包Chromium和NodeJS的运行时环境
- 内存消耗大:Chromium本身比较吃内存,同时NodeJS是JIT运行的,相比较C++等AOT的语言来说内存消耗也更大。
- 性能需要多花点时间优化
实际上,并不代表Electron技术开发的应用性能就一定不如其他技术栈,总的来说,具体的性能表现还是取决于开发者的投入,例如微软在VSCode的博客中给到了一个例子,能够将VSCode在渲染括号颜色匹配的速度提高10000倍[3]。
CEF(Chromium Embedded Framework)
由于需要将Chromium和NodeJS的运行时打包进去,所以Electron构建应用的体积都会非常大,但是CEF的存在解决了Electron的这个问题(实际上,CEF出现的时间比Electron早多了)。由于Chromium里面有许多第三方组件(如ffmpeg等),在开发应用的过程中,我们通常不会使用到Chromium的全部能力,因此CEF提供了一个轻量级的嵌入式Chromium,同时还可以根据自己的需求进行裁剪。
CEF提供了将Chromium嵌入到应用中,展示Webview的能力,同时也提供了C++的一些API,在需要做一些浏览器无法实现的原生API依赖的功能时(如系统文件读写等),则需要使用C++(或其他语言,但是CEF的原生接口是C++的)来编写相关的能力,并提供JSBridge给前端代码进行调用。
- 代表应用:网易云音乐、Spotify、飞书等
自渲染技术栈
要实现跨平台的GUI应用,比较流行的的方式是实现自渲染的管线。上层通过提供类Canvas的绘制、渲染和排版能力,下层使用OpenGL/Vulkan/Metal等图形API进行绘制。在Web的跨平台桌面应用开发技术栈发展之前,许多应用开发框架都采用了类似的思路去实现跨平台的应用开发,如QT(C++语言)、Flutter(Dart语言,基于Skia渲染)和Swing(Java语言)等。相比于Electron和CEF的方案,由于不需要打包运行时环境(Swing除外,需要打包JRE)和减少了Bridge转换,所以体积和运行效率通常会优于Web技术栈。
- 优点
- 自绘性能通常会优于Web跨平台技术(具体还是取决于框架实现)
- 缺点
- 开发成本略高于Web技术栈
- 实现复杂效果的能力不如原生和Web技术栈,通常情况下需要写更多复杂的代码(取决于具体框架的设计,这一点Flutter做得比较好)
Qt(C++)
Qt(/ˈkjuːt/)是一个跨平台的C++应用程序开发框架,广泛用于开发GUI程序,在工业、嵌入式等领域的桌面程序中有着非常深入的使用。
- 代表应用:WPS Office、剪映桌面版、AutoDesk
- 优点
- 性能好,与Native开发的应用性能相差无几
- 支持的操作系统丰富,跨平台性能好
- 缺点
- C++开发成本高
- GPL协议,商业版本需要给钱,不符合咱们去肥增瘦的理念
Flutter(Dart)
Flutter是一个由Google开发的跨平台应用开发框架,最初只用于移动端为Android、iOS开发应用。2015年4月,Flutter正式发布,其目标是希望可以在跨平台的特性上,实现120FPS的渲染性能。2018年,Flutter 1.0发布,是该框架的第一个稳定版本。2022年5月,Google 在 Google I/O 2022 发布了 Flutter 3.0版本,宣布对 Windows、macOS、Linux 桌面操作系统提供支持。
- 代表应用:?(Flutter在2022年5月份发布3.0版本,此时桌面应用才进入正式版支持,目前还并不成熟,所以在线上使用的较少,暂时没听过有啥桌面应用是用Flutter写的)
- 优点
- 性能好(相比较Web技术栈)
- Dart语言容易学习和上手、开发成本低
- 缺点
- 桌面端才刚刚发布稳定版支持,生态和稳定性都有待考量
Swing(Java)
Swing是一个用于开发Java GUI应用的框架。它采用纯 Java 实现,不再依赖于本地平台的图形界面,所以可以在所有平台上保持相同的运行效果,对跨平台支持比较出色。
- 代表应用:Jetbrains IDE
- 优点
- 跨平台性能好:write once run anywhere (write once debug everywhere)
- 缺点
- 需要打包JRE,体积大
总的来说,虽然不同大类技术栈的应用具体实现原理有所不同,但是相关开发的技术栈的大致特点可以归纳如下:
- 系统API调用和交互:原生应用 > 自渲染应用 > Web应用
- 开发便捷程度:Web应用 >> 自渲染应用 > 原生应用
- 打包体积:Web应用 > 自渲染应用 > 原生应用
- 性能:原生应用 > 自渲染应用 > Web应用
Tauri介绍
从上面的介绍可以看出,不同的技术栈的实现原理和特点各有区别,但是很难做到开发便捷程度、UI复杂效果、打包体积和性能等多个方面的兼顾,只能是根据应用的类型和具体的业务场景去决定到底使用哪种框架。
所以有没有一种开发方式,可以在性能、体积、开发等多个角度上,取得一个比较好的平衡呢?这就来到了我们今天需要介绍的桌面应用开发框架Tauri。
Build an optimized, secure, and frontend-independent application for multi-platform deployment.
从上面Tauri官网的宣传语可以看出Tauri主打的几个卖点[4]:
- optimized:性能高、体积小
- secure:安全性强
- frontend-independent:前端独立
- multiplatform:跨平台
那么Tauri是如何通过在框架层面的设计来保证这样的一些特性的呢?我们一起接着往下看⬇️
Rust
Tauri框架是由Rust语言实现的,同时Tauri应用的后端也是由Rust来编写的。Rust是由Mozilla主导开发的通用、编译型的系统编程语言。设计准则为“安全、并发、实用”,支持函数式、并发式、过程式以及面向对象的编程风格。[5]相比较其他语言,Rust有如下的一些特性:
- 性能高(optimized):Rust的性能和C/C++的性能不相上下,由于Rust的「所有权」机制,Rust不需要GC,同时也能避免如C/C++之类需要手动管理内存的语言忘记释放内存导致的内存泄露的问题;
- 安全性强(secure):Rust设计了一个所有权系统,其中所有值都有一个唯一的所有者,并且值的作用域与所有者的作用域相同。值可以通过不可变引用(&T)、可变引用(&mut T)或者通过值本身(T)传递。任何时候,一个变量都可以有多个不可变引用或一个可变引用,这实际上是一个显式的读写锁。Rust编译器在编译时强制执行这些规则,并检查所有引用是否有效。能够有效避免C/C++等语言中的悬垂指针等问题;
- FFI编译友好(multiplatform):FFI是可以用一种编程语言写的程序能调用另一种编程语言写的代码的机制,使用Rust可以方便地提供接口给其他语言调用;
WRY[1]:Webview Render Library
由于Web技术的表现力强、开发成本低的特点,与Electron、CEF等框架类似,Tauri应用的前端实现也是使用Web技术栈编写的。那么Tauri是如何解决Electron/CEF等框架遇到的Chromium内核体积过大的问题呢?
大家可能会想,如果每一个应用都需要把浏览器内核打包进去才能实现Web页面的渲染,要是所有的应用都共享同一个内核就好了,这样我们在分发应用的时候,不需要打包浏览器内核,只需要打包Web页面的资源不就好了吗?所以Tauri就采用了这样的一个方案,WRY是Tauri封装的Webview框架,它在不同操作系统的平台上,封装了系统Webview的实现:在macOS上使用WebKit.WKWebview[2],在Windows上使用Webview2[3],在Linux上使用WebKitGTK[4]。这样在运行Tauri应用时,会直接使用系统Webview来渲染应用前端的展示。
API接口
对于不会使用Rust的同学来说,学习Rust还是存在着不少的学习成本,但是别担心,在需求简单的情况下,你完全可以不写Rust代码。Tauri框架提供了如下的一些API,可以方便地在JS中对原生能力进行调用:
- cli:解析应用启动时的命令行参数
- clipboard:对系统剪贴板的读写
- dialog:展示系统文件选择、文件保存弹窗
- event:给后端发出一些事件,后端监听并处理
- fs:文件系统的操作,提供文件读写等能力
- globalShortcut:注册全局快捷键
- http:使用Rust的Http客户端进行网络请求
- notification:系统通知
- os:获取操作系统的一些信息
- path:文件和文件夹路径处理的一些工具
- process:对当前的进程进行一些操作
- shell:对系统shell的一些操作
需要注意的是,上述的一些API,为了保证安全性,对于权限都有着严格的限制,都是默认关闭的状态,需要修改配置以手动启用相关的功能。
进程模型
和Electron类似,Tauri也采用的是多进程的架构(Electron中有主进程和渲染进程),多进程的好处是能够更好更有效地利用现代多核CPU的能力,同时一个组件的崩溃也不会影响到其他组件的运行,因为组件被隔离在了不同的进程中。如果应用中的某个进程崩溃了,我们只要重启该进程即可。还可以通过只给每个进程分配足够完成工作的最低限度的权限,来限制潜在漏洞的破坏范围。这种模式被称为最小权限原则。
在Tauri中,进程分为两类:主进程和Webview进程。每个 Tauri 应用程序都有一个主进程,它作为应用程序的入口点,是唯一可以完整访问操作系统的组件。主进程的主要职责是使用访问权限来创建和管理应用程序窗口、系统托盘菜单或通知。Tauri 实现了必要的跨平台抽象来简化该操作。它还通过核心进程路由所有的IPC,通过类似于事件总线的机制,可以方便地拦截、过滤和操作 IPC 消息。主进程还应该负责管理全局状态,例如数据库连接。这使你能够轻松地在窗口间同步状态,并保护你的业务敏感数据。主进程自身并不渲染实际的用户界面,它会直接利用操作系统提供的 WebView 库来实现页面渲染,不同的窗口之间会拥有不同的WebView进程,WebView进程用来负责渲染对应的UI。
IPC模式
Brownfield 模式(默认)
Brownfield 软件开发是指在现有或遗留软件系统存在的情况下开发和部署新的软件系统。[6]
使用 Tauri 的最简单和直接的模式,因为 Brownfield 模式会尽最大可能尝试与现有的前端项目兼容。在这种模式下,无需现有的浏览器前端项目进行任何操作即可迁移。
隔离模式
隔离模式是一种在到达 Tauri 主进程前,拦截并修改由Webview进程发送的 Tauri API 信息的IPC模式,其完全使用 JS 编写。由隔离模式保障的JS代码即称为隔离应用。隔离模式的目的是为开发者提供一种保护机制,防止其应用程序被预料之外或恶意的Webview进程调用 Tauri 主进程。隔离模式的需求来自于前端中不可信任的内容所带来的威胁,常见于需要许多依赖的应用。隔离模式在设计之初时设想的最大威胁为开发威胁,因为前端构建工具不仅仅由许许多多嵌套很深的依赖组成,而且还有很多嵌套很深的依赖被打包到最终的网页构建产物中。
- 原理:隔离模式就是在Webview进程和 Tauri 主进程之间注入一个安全的应用程序,用以拦截和修改传入的 IPC 信息。它使用
<iframe>
的沙盒特性,与Webview进程一起安全地执行 JS 代码。Tauri 在加载页面时会强制执行隔离模式,使所有对 Tauri 主进程的 IPC 调用必须先通过沙盒隔离应用程序。当消息准备被传递给 Tauri 主进程时,其就会被使用浏览器的 SubtleCrypto API[5] 实现加密,并传递回主前端程序,之后,它将会被直接传递给 Tauri 主进程来解密和读取数据。 - 步骤:
- 缺点
- 由于信息经过加密,所以隔离模式相比 Brownfield 模式而言存在额外开销。除去性能敏感的应用 (使用很少依赖来提升性能的应用) 之外,使用 AES-GCM 算法来加解密 IPC 信息会对大部分应用造成相对较小的性能影响。
- Windows平台上,Webview限制了因为沙盒环境下加载
<iframe>
标签内的外部文件,Tauri在构建时实现一些步骤将脚本注入,但是ES Modules可能无法正常加载。
安全性
- 动态AOT:Tauri 应用启动时将多次进行编译。通过使用Tauri提供的动态预编译器,可以生成每个会话都不同的代码引用,但技术上一致的代码单元;
- 函数 ASLR:函数地址空间布局随机化 (Address Space Layout Randomization) 将在运行时随机调整函数名称,且可以实现 OTP 哈希功能,这样将永远不会出现相同的会话。在Tauri应用启动时,或可选在每次执行后,随机生成函数名称。每个函数指针均使用 UID 以防止静态攻击;
- 自杀式函数注入:一种高级的ASLR技术,Rust在运行时载入进入Webview的闭包Promise和随机的句柄,API接口在Promise处理中就被锁定,在执行完毕后被立即设置成null;
可以有效防止恶意页面被加载
- 使用Event Bridge:Tauri内部的信息通信使用Event Bridge,Event Bridge用来保证只能传递信息和指令给一个指定的中间代理,而不是直接传递不安全的函数调用;
- API Allow List:Tuari提供的API都是关闭状态,若没有启用相关API,应用构建时不会包括相关功能函数的代码。这可以减少二进制文件大小及攻击面。同时API还有严格的权限选项,如文件读写相关API可以设置只读/只写/指定目录或文件等功能;
- CSP:Tauri会对本地的HTML页面强制使用内容安全策略,本地脚本经过哈希处理,同时样式、外部脚本经由加密随机字符串引用,防止禁止内容被加载;
- 反编译难:Tauri在构建时,会将相关前端代码直接构建在二进制可执行文件中,这意味着和Electron的ASAR文件不同,具体的代码无法被轻松地反编译;
- 一次性Token和哈希:使用OTP加盐哈希处理重要的信息,可以在前端和Rust后端之间加密信息;
Tauri-egui[6]
因为Tauri应用的前端是用了Web相关的技术栈,因此在运行时总有办法来使用开发者模式/调试工具等来进行检查元素等。在某些情况下,如密码输入等场景,我们希望能够保证前端的UI展示是无法被修改的,这时可以使用Tauri-egui这个库,这个库对egui进行了封装,可以使用Rust来编写前端的UI。
优点
Benchmark
- 内存使用
- 构建产物体积
缺点
- 兼容性:由于Tauri使用的是系统Webview,因此在构建时前端的代码需要做polyfill;且Windows平台上,由于Webview2只在Windows10/11上有默认推送安装,要想在Windows7/8等较低版本的平台上运行的话,还需要另外安装Webview2的运行时。
参考资料
[1] 在macOS中打开系统Webview的检查元素开关, https://blog.jim-nielsen.com/2022/inspecting-web-views-in-macos/
[2] 微软开始在Teams应用中放弃Electron, https://blog.devgenius.io/microsoft-is-finally-ditching-electron-9e081757f9db
[3] VSCode优化括号颜色匹配, https://code.visualstudio.com/blogs/2021/09/29/bracket-pair-colorization
[4] Tauri官网, https://tauri.app
[5] Rust Wikipedia, https://zh.wikipedia.org/zh-cn/Rust
[6] Brownfield, https://en.wikipedia.org/wiki/Brownfield_(software_development)
参考资料
[1]
WRY: https://github.com/tauri-apps/wry
[2]
WebKit.WKWebview: https://developer.apple.com/documentation/webkit
[3]
Webview2: https://developer.microsoft.com/zh-cn/microsoft-edge/webview2/
[4]
WebKitGTK: https://webkitgtk.org/
[5]
SubtleCrypto API: https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto
[6]
Tauri-egui: https://github.com/tauri-apps/tauri-egui
- END -
往期回顾
#
如何使用 TypeScript 开发 React 函数式组件?
#
11 个需要避免的 React 错误用法
#
6 个 Vue3 开发必备的 VSCode 插件
#
3 款非常实用的 Node.js 版本管理工具
#
6 个你必须明白 Vue3 的 ref 和 reactive 问题
#
6 个意想不到的 JavaScript 问题
#
试着换个角度理解低代码平台设计的本质