Skip to content

断桥重建

背景:Wails v2 停止维护,联邦必须在三个月内迁移到 v3——框架 API 全链路变更(go.mod/main.go/bindings/事件协议/配置)。 过程:十步迁移——换地基、拆房子、换家具、建新桥、拆旧桥、更新协议、重建配置、加固地基、盖房子、打扫工地。


信使从北方带回了一封信。

"联邦的框架要换了。" 信使说,"v2 桥塌了,v3 新桥正在搭建。"

外交官展开信纸,上面写着:

Wails v2 停止维护。所有城邦必须在三个月内迁移到 v3。 —— 框架议会

织工正在检修布料机,听到消息后放下了手中的工具。

"桥塌了?" 他问,"那数据怎么从地上传到天空?"

"桥还没完全塌," 信使说,"但桥面已经开裂。新的桥有不同的架构——链式结构,不是以前的积木式。"


第一本账

外交官召集了议会。

"迁移计划有十步。" 他说,"第一步,换地基。"

织工看着 go.mod 文件,上面写着:

github.com/wailsapp/wails/v2 v2.11.0

"把 v2 换成 v3。" 外交官说。

织工修改了 go.mod:

github.com/wailsapp/wails/v3 v3.0.0-alpha2.105

"地基换了。" 织工说,"但房子还没拆。"


第二本账

"第二步,拆房子。" 外交官说。

主城门(main.go)是 v2 架构的核心——wails.Run() 函数像一根巨大的木桩,支撑着整个联邦。

织工看着旧代码:

go
wails.Run(&wails.AppConfig{
    Title: "MikuMikuAR",
    Bind: []interface{}{
        &App{},
    },
})

"旧桥是积木式的。" 外交官说,"把所有东西塞进一个配置块。新桥是链式的——一块一块搭起来。"

织工重写了主城门:

go
app := application.New(application.Options{
    Name:        "MikuMikuAR",
    Description: "PMX Player with physics simulation",
})
app.Service.Add(&App{})
app.Window.NewWithOptions(...)

"房子拆了," 织工说,"但旧家具还在里面。"


第三本账

"第三步,换家具。" 外交官说。

旧家具包括:

  • runtime.OpenFileDialog — 对话框使者
  • runtime.LogInfof — 日志信使
  • runtime.EventsEmit — 事件鸽子

织工打开 app.go,看到满屋子的旧家具。

"对话框使者换了名字。" 他说,"以前叫 runtime.OpenFileDialog,现在叫 application.Dialog.OpenFile()。"

他修改了代码:

go
// 旧:runtime.OpenFileDialog(...)
// 新:
dialog := a.wailsApp.Dialog.OpenFile()
dialog.SetTitle(title)
dialog.CanChooseFiles(true)

"日志信使也换了。" 织工继续,"以前用 runtime.LogInfof,现在用 slog。"

他替换了日志调用:

go
// 旧:runtime.LogInfof("Loading model: %s", path)
// 新:
slog.Info("Loading model", "path", path)

"事件鸽子换了巢。" 织工最后说,"以前是 runtime.EventsEmit,现在是 app.Event.Emit。"


第四本账

"第四步,建新桥。" 外交官说。

联邦和天空之间的桥(bindings)需要重建。以前是 wailsjs/go/main/App.js,现在是 bindings/mikumikuar/app.js

织工看着旧的绑定文件,上面写着:

typescript
import { GetConfig, SetLibraryRoot } from '../wailsjs/go/main/App';

"桥的名字变了," 他说,"但桥上的车还是那些车。"

他创建了新的桥入口(wails-bindings.ts):

typescript
export * from '../bindings/mikumikuar/app.js';
export { Events } from '@wailsio/runtime';

"所有进城的路都要经过这座桥。" 外交官说。

织工遍历了十六个城门,把所有旧桥引用换成了新桥:

typescript
// 旧:import { GetConfig } from '../wailsjs/go/main/App';
// 新:import { GetConfig } from './wails-bindings';

第五本账

"第五步,拆旧桥。" 外交官说。

旧桥(wailsjs 目录)已经废弃,需要拆除。织工删除了 frontend/wailsjs/ 目录,然后更新了 .gitignore:

gitignore
# Wails3 build temp
.bindings-tmp-*
.task/
frontend/wailsjs/
bin/

"旧桥拆了," 织工说,"但还有一个问题——天空和地面的通信协议变了。"


第六本账

"第六步,更新通信协议。" 外交官说。

以前,事件鸽子从地面飞到天空时,携带的消息格式是:

typescript
// v2
EventsOn('watch:newfile', (path: string, name: string, type: string) => {
    // ...
});

现在,鸽子携带的是一个包裹:

typescript
// v3
Events.On('watch:newfile', (ev) => {
    const payload = ev.data as { path: string; name: string; type: string };
});

织工修改了 main.ts 中的事件监听代码。

"包裹里装的东西没变," 他说,"只是包装方式变了。"


第七本账

"第七步,重建地基配置。" 外交官说。

旧的地基配置文件(wails.json)已经废弃,需要换成新的配置体系:

  • build/config.yml — 地基蓝图
  • Taskfile.yml — 施工计划

织工创建了新的配置文件,把旧 wails.json 中的配置迁移过来。

"旧蓝图是 JSON," 他说,"新蓝图是 YAML。内容差不多,只是格式不同。"


第八本账

"第八步,加固地基。" 外交官说。

联邦的地基(app.go)需要实现两个新接口:

  • ServiceStartup — 地基启动时调用
  • ServiceShutdown — 地基关闭时调用

织工添加了这两个接口:

go
func (a *App) ServiceStartup(ctx context.Context, options application.ServiceOptions) error {
    a.restoreWatcher()
    return nil
}

func (a *App) ServiceShutdown(ctx context.Context) error {
    // 清理工作
    return nil
}

"地基加固了," 他说,"但还没盖房子。"


第九本账

"第九步,盖房子。" 外交官说。

织工运行了 wails3 build,看着命令行输出:

[Wails3] Building application...
[Wails3] Compiling frontend...
[Wails3] Embedding assets...
[Wails3] Compiling Go...
[Wails3] Build complete: bin/MikuMikuAR.exe

"房子盖好了。" 织工说,"23MB,和以前差不多。"

外交官启动了应用。

"桥通了。" 他说,"模型能加载,纹理能显示,布料机能运行。"


第十本账

"第十步,打扫工地。" 外交官说。

织工清理了临时构建文件:

  • .bindings-tmp-* — 绑定临时文件
  • .task/ — 施工缓存
  • build/windows/windows/ — 嵌套的窗户

"工地干净了。" 织工说,"但还有一件事——旧的命令怎么处理?"

"保留旧命令," 外交官说,"wails 是旧桥,wails3 是新桥。用户想用哪个用哪个。"


断桥重建的法则:地基换了,桥换了,家具换了,但联邦的灵魂没变。迁移不是推倒重来,而是在新的框架下继续守护城邦。


教训摘要:链式 API 比积木式更灵活,抽象层(wails-bindings.ts)让框架切换变得简单,临时文件要及时清理。