Wails简介
Wails 是一个能让你使用 Go 和 Web 技术栈构建桌面应用程序的项目。你可能听说过 Electron,这是 Github 开源的一个跨平台桌面应用程序开发的解决方案。像我们熟知的 VS Code、Typora 都是使用 Electron 开发的,界面部门使用 Web 技术栈实现,所以你会感觉跟使用浏览器无异。
今天要介绍的 Wails 和 Electron 区别是:Wails 后端使用 Go 实现,利用 JS 函数绑定 Go 函数,然后调用,由 Go 的运行时来实现调用的过程。而 Electron 的后端是使用 Node.js 实现,渲染进程和主进程之间通过内置的 IPC(Inter Process Communication) 模块实现进程间通信。注意,这里的后端不是指 Web 后端,如 Java 的 SpringBoot、Python 的 Flask、Django 等 Web 后端程序。这里的后端是指本地的服务进程,其实和 Web 后端区别不大,只不过渲染进程和主进程之间通信使用本地接口调用(可能是某种二进制协议、也可能是像 DLL、SO 之类的动态库),传统 Web 应用程序使用 HTTP、WebSocket 协议进行前后端通信。 这里的渲染进程也就是前端。
你也可能听过 Tauri 跨平台桌面应用程序解决方案,后端使用 Rust 开发。今天在这里不做讨论,因为笔者对 Rust 不是很熟悉。
工作原理
Wails 使用 Webkit 作为内核,Go 语言部分的应用程序由应用程序代码和一个提供了很多实用功能的运行时库组成。比如,控制应用程序窗口。在前端,Webkit 窗口会展示前端资源,同时前端还有一个 JavaScript 运行时库。最后前端的和后端的 Go 函数绑定到一起,并且这些看起来像是可以被调用本地 JavaScript 函数。
快速入门
安装
支持的系统
- Windows 10/11 AMD64/ARM64
- MacOS 10.13+ AMD64
- MacOS 11.0+ ARM64
- Linux AMD64/ARM64
依赖项
- Go 1.18+
- Node 15+
安装Wails CLI
这里还需要安装 Wails 脚手架命令行工具,它本身是使用 Go 开发的一个二进制可执行文件。你可以使用它来创建工程。
输入命令安装:
shell
go install github.com/wailsapp/wails/v2/cmd/wails@latest
这一切前提是你安装了 Go SDK,并且配置了 GOPATH 环境变量。GOPATH 用于存放下载的 Go 二进制文件(命令行工具)还有第三方库文件。
创建一个工程
生成脚手架工程
使用 wails init
命令可以初始化一个标准的 Wails 工程。Wails 官方提供了多种 Web 前端框架可供选择,选择自己熟悉的 Web 前端框架即可:
我这里使用 React 结合 TS 语言演示,输入命令创建工程:
shell
wails init -n wails-first -t react-ts
目录结构
go
.
├── build/
│ ├── appicon.png
│ ├── darwin/
│ └── windows/
├── frontend/
├── go.mod
├── go.sum
├── main.go
└── wails.json
/main.go
- 主程序/frontend/
- 前端工程文件/build/
- 打包过后的文件夹/build/appicon.png
- 应用程序图标/build/darwin/
- Mac 系统特定文件/build/windows/
- Windows 系统特定文件/wails.json
- 工程配置文件/go.mod
- Go module 文件/go.sum
- Go module 版本锁文件
开发应用程序
前端是使用 vite 构建的所以对热更新支持得很棒。
输入 wails dev
命令可以初始化工程依赖库并启动应用程序。应用程序界面如下:
简单看一下 App.tsx 文件代码:
tsx
import { useState } from 'react'
import logo from './assets/images/logo-universal.png'
import './App.css'
import { Greet } from '../wailsjs/go/main/App'
function App() {
const [resultText, setResultText] = useState('Please enter your name below 👇')
const [name, setName] = useState('')
const updateName = (e: any) => setName(e.target.value)
const updateResultText = (result: string) => setResultText(result)
function greet() {
Greet(name).then(updateResultText)
}
return (
<div id="App">
<img src={logo} id="logo" alt="logo" />
<div id="result" className="result">{resultText}</div>
<div id="input" className="input-box">
<input id="name" className="input" onChange={updateName} autoComplete="off" name="input" type="text" />
<button className="btn" onClick={greet}>Greet</button>
</div>
</div>
)
}
export default App
可以看出来是一个输入一个名字然后替换屏幕上一段话的程序。点击按钮触发点击事件并且触发 input 元素 onChange 事件调用 updateName 函数更新 name 变量,然后调用 greet 函数,greet 函数最终又调用了跟 Go 函数绑定的 JS 函数:
ts
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT
export function Greet(arg1:string):Promise<string>;
这是自动生成的文件,不可以修改,不然会出毛病。最终这个 ts 文件被编译成 js 文件:
js
// @ts-check
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT
export function Greet(arg1) {
return window['go']['main']['App']['Greet'](arg1);
}
可以看到调用了 window 对象的某个属性的函数。按照顺序来讲就是调用 go 对象的 main 文件对象的 App 结构体的 Greet 方法,这个是按照文件目录层级调用的。
顺藤摸瓜找到 go 文件里面的函数:
go
package main
import (
"context"
"fmt"
)
// App struct
type App struct {
ctx context.Context
}
// NewApp creates a new App application struct
func NewApp() *App {
return &App{}
}
// startup is called when the app starts. The context is saved
// so we can call the runtime methods
func (a *App) startup(ctx context.Context) {
a.ctx = ctx
}
// Greet returns a greeting for the given name
func (a *App) Greet(name string) string {
return fmt.Sprintf("Hello %s, It's show time!", name)
}
可以清晰看到 Greet 方法返回一个模板字符串,这个用起来确实比 Electron 的 IPC 模块通信简单一些。
打包应用程序
可以使用 wails build
命令来打包应用程序:
shell
wails build
最终编译的文件在 build/bin 文件夹内,在 Mac OS 上会编译成一个二进制可执行文件,在 Windows 就是 .exe 文件。