🌒

Hi,

桌面跨平台框架选型

从兴趣使然到从业,本人用过的跨平台GUI框架也不少,特地写这篇文章总结下经验,希望能帮到你。接下来就开始我恬不知耻的侃侃而谈,不一定对,仅我本人拙见。

见标题👆🏼👆🏼👆🏼,因为是桌面跨平台开发的文章,所以文中不会提到原生或移动端开发的技术。

百花齐放的框架

跨平台开发这个词已经不新鲜了,也有许多的成熟的框架被大家使用和比较着。现在是2025年中旬。桌面端讨论最多的是:

  • Electron
  • Tauri
  • Flutter

另外我还想在这提及下其他的较热门的框架

  • React Native(也对桌面端进行了支持)
  • Wails(Golang + Webview的 GUI方案)
  • Fyne(Golang GUI方案)
  • Avalonia(.net方案)

Web渲染和Native的优劣

  • Web对比Native的优势

    对比原生渲染,web开发的优势就是社区生态丰富、样式控制方便、开发效率高、开发成本低。原子化CSS流行到2025年了,TailwindUnocssWindcss等库的周边生态越来越健壮,各种妙曼动画库玲琅满目,能够很轻松的做出酷炫的效果。

  • Web对比Native的劣势

    老生常谈的就是性能问题,尤其是在3D渲染场景显得更加捉襟见肘。其次,就是对native原生功能的使用比较局限(其实Electron这方面相当出色,其他桌面跨平台方案也没有很差,不过几乎所有的移动端跨平台方案对原生api的使用都比较局限)。

    除此之外,有一个比较突出的缺点就是「出活太快,显得自己没水平。工作不够饱和。」


我想着重跟大家探讨下Electron、Wails这三个

他们的共同的优缺点就不过多将了,实际上就是web套壳的优缺点。


Electron

说到Electron,绕不开的话题就是Chromium。它给Electron带来了极高的兼容性适配,也是Electron在多平台上的兼容性表现出色的核心原因。不过带来的问题就是构建出来的产物太大,不过多赘述了。看图👇🏻👇🏻👇🏻

Snipaste_2022-12-07_00-03-24

较小的心智负担

Electron在所有的Web跨平台框架中,算是历史悠久的了。遇到问题在网上总能查到解决方案,所有的坑都有老前辈替我们踩过了。对系统级API的开放也比较丰富。在整个开源和闭源项目中也有很多Awesome Project,比如:

  • Visual Studio Code

    vscode是一个令人没话讲的一个项目,也是Electron Based App的标杆项目

  • Atom

    同样也是一个很屌的编辑器

  • Slack、Figma、QQ、Postman、Skype、Whatsup、Teams……写到吐也写不完

网上的解决方案很多,可以说是心智负担最小的Web框架了。这段内容中,我主要想表明他是一个生态繁荣的框架。

兼容性满屌的

对比其他web框架,Electron的兼容性可以说相当的屌了。开发者无需过多担心兼容性问题,在使用其他webview类的框架开发时,可能会经常遇到以下情况:

  • “这个图标在mac上好好的,怎么在windows上歪到姥姥家了”
  • “这个CSS规则怎么在mac上不生效”
  • “Linux arm64上没跑过,跑一下不会炸肛吧”
  • “🐴的,用Electron写早下班了”

学习成本低

渲染进程就是纯粹写Web,主进程使用Node。对于前端开发人员比较友好,不用花时间去学习其他的语言,只要会node。写客户端就跟写老本行一样欢愉。

构建方便

构建产物这方面,Electron绝对的牛,各种开发脚手架和构建工具玲琅满目,我用的最多的脚手架就是Electron-Vite 。另外Electron-Egg都挺不错的。构建配置上,Electron-Forge和Builder也提供了相当丰富的配置项,能满足绝大部分场景的需求。在开发环境使用Vite构建也能享受到极致的HMR体验。

背后有大哥

背靠Github和微软,Bug少、更新快、论坛好。


Wails

这是一个类似Tauri的一个Webview解决方案,不过是使用Go来做访问底层和系统交互。不过这个框架还比较年轻,总体来说就是系统级接口不够,兼容性一般,打包体验一般。不过,这些缺点也是Webview方案的缺点。我个人挺喜欢这个项目,前景也是相当不错的。如果是个Gopher应该,应该难以拒绝这个框架。

有兴趣的厚密可以看下我这个项目,就是使用Wails写的。

Transok - 基于 wails + react 实现的局域网文件传输工具

产物极极极小

因为使用的webview + Go,所以构建出的产物极小(Go本身就是一个产物很轻量的语言),对比Electron的产物可以说是草履虫级别了。不过包体积小并不代表运行效率,内存使用率就低。事实上web跨平台方案的运行效率都半斤八两。

image-20250526162018088

image-20250526162203682

前后端通讯优雅

通常,我们写Electron最大的场景就是进程通讯,进程通讯说破天也就下面几种方案。

  • 关闭上下文隔离直接使用ipc通信
  • 开启上下文隔离,在预渲染进程中写contextBridge api
  • 使用webContents通信
  • 使用Http传统B/S请求
  • 使用remote模块直接硬搞(已废弃)

Wails提供了更优雅的方式

只需要在Wails中绑定了Go结构体实例,写好接口方法,就能自动生成Ts/Js的函数签名,渲染层直接调用就行。实现了自动的Go To Js的转换。

1
2
3
4
5
6
7
8
9
10
11
...
Bind: []interface{}{
sysSvc,
fileSvc,
storageSvc,
ginSvc,
shareSvc,
discoverSvc,
mdns_handlers.GetDiscoverHandler(),
}
...
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
// file_service.go
package services

import (
"context"
"encoding/json"
"os"
"path/filepath"
"strings"
"sync"

"github.com/wailsapp/wails/v2/pkg/runtime"
)

type fileService struct {
ctx context.Context
}

type FileInfo struct {
Path string
Name string
Size int64
Type string
Text string
}

var file *fileService
var fileOnce sync.Once

func File() *fileService {
if file == nil {
fileOnce.Do(func() {
file = &fileService{}
})
}
return file
}

func (c *fileService) Start(ctx context.Context) {
c.ctx = ctx
}

/* 可多选选择文件 */
func (c *fileService) SelectFiles() []string {
// 打开文件选择对话框,允许多选
files, err := runtime.OpenMultipleFilesDialog(c.ctx, runtime.OpenDialogOptions{
Title: "选择文件",
})

if err != nil {
return nil
}

return files
}

/* 通过文件的绝对路径获取File对象 */
func (c *fileService) GetFile(path string) *FileInfo {
// 获取文件信息
fileInfo, err := os.Stat(path)
if err != nil {
return nil
}

// 返回文件对象
return &FileInfo{
Path: path,
Name: fileInfo.Name(),
Size: fileInfo.Size(),
Type: strings.TrimPrefix(strings.ToLower(filepath.Ext(path)), "."),
}
}

func (c *fileService) GetShareList() []FileInfo {
storage := Storage()
shareList, isExist := storage.Get("share-list")

// 初始化一个空数组
result := make([]FileInfo, 0)

if !isExist {
storage.Set("share-list", result)
return result
}

// 使用 json.Marshal 和 json.Unmarshal 进行类型转换
if jsonData, err := json.Marshal(shareList); err == nil {
json.Unmarshal(jsonData, &result)
}

validFiles := make([]FileInfo, 0)
for _, file := range result {
if file.Type == "pure-text" {
validFiles = append(validFiles, file)
}

if _, err := os.Stat(file.Path); err == nil {
validFiles = append(validFiles, file)
}
}

storage.Set("share-list", validFiles)
return validFiles
}

就上上述代码一样,在wails构建时,就会生成对应的申明文件在渲染层

1
2
3
4
5
6
7
8
9
10
11
12
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT
import {services} from '../models';
import {context} from '../models';

export function GetFile(arg1:string):Promise<services.FileInfo>;

export function GetShareList():Promise<Array<services.FileInfo>>;

export function SelectFiles():Promise<Array<string>>;

export function Start(arg1:context.Context):Promise<void>;

调用起来非常之方便

开发体验好

Wails没有什么特定语法和对语言层面的魔改,开发者仅需纯粹的掌握Go和Web前端即可,侵入性低。可以自由的使用Go或者Js的生态。例如可以在Go侧使用Gin、Gorm、Casbin;在Web侧使用Vite、React、Shadcn…沉浸式开发无需过多担心语言直接的割裂感。

目前不足

  • 生态不够健全(毕竟新框架,需要沉淀)
  • 系统级API不够
  • v2版本对多窗口支持不够
  • 构建体验不好,在mac和win上还行,linux不太好

总的来说

个人拙见

本人不太会Tauri(其实是不会Rust),没有能力从Tauri开发者角度来说明这个框架。不过weview方案都大同小异,相信未来webview会越来越好。就目前来说,无论从什么角度来讲,大项目或者公司业务还是优先考虑electron(如果你想按时下班的话,老老实实别折腾)。

  • 如果对系统级API依赖性不高或者是小工具可以使用webview的方案。
  • 如果您的业务涉及到各种Linux适配(有国产信创系统的适配),老老实实Electron
  • 如果您对构建产物包要求比较敏感,可以尝试webview

以上👋 ;

, — 2025年5月26日