Appearance
断桥重建
背景: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)让框架切换变得简单,临时文件要及时清理。