Wails 前后端通信只有两种机制:Binding 和 Event。
Binding 是有去有回,Event 是发完不管。下面先讲 Binding。
Binding:JS 调用 Go
Binding 的本质是把 Go 方法暴露给前端,JS 直接调用,像调本地函数一样。一步步来。
第一步:创建 struct
type TodoService struct {
}
struct 名首字母大写,不导出也行,但建议大写保持统一。
第二步:写方法
在 struct 上挂方法,方法名必须首字母大写(Go 的导出规则),否则 Wails 不会暴露给前端。
// 方法名大写,暴露给前端
func (t *TodoService) Add(title string, done bool) string {
return fmt.Sprintf("added: %s, done: %v", title, done)
}
func (t *TodoService) GetAll() []string {
return []string{"学习 Wails", "写博客"}
}
参数和返回值可以是 string、int、struct、slice 等基础类型,Wails 会自动序列化。
第三步:注册 Binding
在 main.go 的 Bind 字段里注册:
func main() {
err := wails.Run(&options.App{
Title: "wails_study",
Width: 1024,
Height: 768,
Bind: []interface{}{
&TodoService{},
},
})
}
可以注册多个 struct:
Bind: []interface{}{
&TodoService{},
&UserService{},
&FileService{},
},
第四步:自动生成 JS 代码
每注册一个 struct,Wails 启动时就会在 frontend/wailsjs/go/ 下生成对应的 JS 文件。目录结构如下:
frontend/wailsjs/
├── go/
│ ├── main/
│ │ ├── TodoService.js # 包装好的 JS 方法
│ │ └── TodoService.d.ts # TypeScript 类型定义
│ └── runtime/
│ ├── runtime.js
│ └── runtime.d.ts
每个 struct 对应一个 JS 文件,里面的方法名跟 Go 方法名一致。
第五步:JS 中调用
前端直接 import 并调用:
import { Add, GetAll } from '../wailsjs/go/main/TodoService'
const result = await Add("买咖啡", false)
console.log(result) // "added: 买咖啡, done: false"
const list = await GetAll()
console.log(list) // ["学习 Wails", "写博客"]
几点说明:
../wailsjs/go/main/这个路径固定,main是 Go 的包名- 方法名跟 Go 一致,大小写也一样,JS 这边首字母也是大写
- 调用返回 Promise——JS 把请求发出去,不知道啥时候能收到结果,Promise 就是占个位。用
await等结果,或者用.then()指定结果回来了再执行,两种写法效果一样 - 不需要手写任何通信代码,全自动
传参数
Go 方法定义参数,JS 那边按顺序传就行:
func (t *TodoService) Update(id int, title string, done bool) string {
return fmt.Sprintf("updated %d: %s, done=%v", id, title, done)
}
const r = await Update(1, "买咖啡", true)
Wails 会自动做类型转换,但类型要匹配——Go 那边是 int 就别传字符串。
接收返回值
返回 struct:
type Todo struct {
ID int
Title string
Done bool
}
func (t *TodoService) Get(id int) Todo {
return Todo{ID: id, Title: "买咖啡", Done: false}
}
const todo = await Get(1)
console.log(todo.ID) // 1
console.log(todo.Title) // "买咖啡"
console.log(todo.Done) // false
Go 的 struct 字段映射到 JS 对象属性,字段名首字母大写。
错误处理
Go 方法返回 error,JS 那边会 throw:
func (t *TodoService) Get(id int) (Todo, error) {
if id <= 0 {
return Todo{}, fmt.Errorf("id must be positive")
}
return Todo{ID: id, Title: "买咖啡"}, nil
}
try {
const todo = await Get(-1)
} catch (err) {
console.error(err) // "id must be positive"
}
返回 slice
func (t *TodoService) List() ([]Todo, error) {
return []Todo{
{ID: 1, Title: "学习 Wails", Done: false},
{ID: 2, Title: "写博客", Done: true},
}, nil
}
const list = await List()
list.forEach(item => {
console.log(item.Title, item.Done)
})
Binding 的数据流
JS 参数 → JSON 序列化 → Go 收到 → 处理 → return → JSON 反序列化 → JS 拿到
全程自动,不需要写任何序列化代码。
Event:Go 和 JS 互相通知
Binding 是有去有回——JS 必须主动调用,Go 被动响应。
但有些场景 Binding 做不了:Go 主动给 JS 推数据(比如进度通知)。这时候用 Event。
Event 是发布/订阅模式。一端发出事件,另一端监听,没有返回值。
JS 发事件,Go 接收
JS 端:
import { EventsEmit } from '../wailsjs/runtime'
EventsEmit("todo_updated", { id: 1, title: "买咖啡" })
Go 端:
import "github.com/wailsapp/wails/v2/pkg/runtime"
func (t *TodoService) startup(ctx context.Context) {
t.ctx = ctx
runtime.EventsOn(ctx, "todo_updated", func(optData ...interface{}) {
// optData[0] 就是传来的数据
data := optData[0].(map[string]interface{})
log.Printf("updated: %v", data)
})
}
Go 发事件,JS 接收
Go 端:
runtime.EventsEmit(t.ctx, "progress", 50)
JS 端:
import { EventsOn, EventsOnce, EventsOff } from '../wailsjs/runtime'
EventsOn("progress", function(percent) {
console.log("进度:", percent) // 50
})
// 只监听一次
EventsOnce("progress", function(percent) {
console.log("只触发一次:", percent)
})
// 取消监听
EventsOff("progress")
Event 的几个关键问题
1. 不保证顺序
连续 emit:
runtime.EventsEmit(t.ctx, "msg", "A")
runtime.EventsEmit(t.ctx, "msg", "B")
runtime.EventsEmit(t.ctx, "msg", "C")
JS 收到的顺序可能是 A、C、B 或任意顺序。因为每个 Event 内部是 goroutine 并发通知的,调度顺序不确定。
所以 Event 不能用来传递有顺序依赖的消息。如果 B 必须在 A 之后处理,用 Binding 或者自己在数据里加序号。
2. 没有返回值,没有确认
Event 是 fire-and-forget,emit 完就结束了,发送方不知道对方收到没有、处理完了没有。
EventsEmit("do_something", data)
// 到这行,不知道 Go 那边处理到哪了
如果需要确认对方收到了、拿到了处理结果,那就是 Binding 的工作。
有人想用 Event 模拟请求-响应:
// 错误用法 ❌
EventsEmit("ask_go", "hello")
EventsOn("go_reply", function(data) {
// 想在这里拿到返回值
})
问题在于:
- 不知道
go_reply什么时候回来 - 如果 Go 那边没处理或者处理慢了,你这边只能干等
- 多次调用时回调会叠加
3. 监听时机
EventsEmit("hello", "world") // 先发
EventsOn("hello", fn) // 后监听 —— 收不到
emit 时如果还没有注册监听,事件就丢了。建议在应用启动阶段就把监听注册好。
什么时候用 Event
适合:
- 进度通知 —— Go 处理耗时任务,通过 Event 告诉 JS 更新进度条
- 状态广播 —— 某个状态变了,通知另一端做响应
- 一次性通知 —— 配
EventsOnce,触发后自动取消
不适合:
- 需要返回值 → Binding
- 有顺序依赖 → Binding 或自己管理序号
- 需要确认送达 → Binding
Event 典型用法:进度通知
func (t *TodoService) ProcessItems() {
go func() {
for i := 0; i <= 100; i += 10 {
runtime.EventsEmit(t.ctx, "task_progress", i)
time.Sleep(500 * time.Millisecond)
}
}()
}
EventsOn("task_progress", function(percent) {
progressBar.value = percent
})
Binding 触发操作,Event 广播进度——这是最常见的搭配。
总结
| Binding | Event | |
|---|---|---|
| 数据流向 | 双向(请求-响应) | 单向(发完不管) |
| 有返回值 | ✅ await 拿结果 | ❌ |
| 顺序保证 | ✅ | ❌ |
| 确认送达 | ✅ | ❌ |
| 谁主动 | JS 调用 | 两端都能主动发 |
| 适用场景 | CRUD、数据查询、业务调用 | 进度通知、广播、状态推送 |
一句话:有去有回用 Binding,只管通知用 Event。
