【原创】桌面应用框架 Wails 中 Go 和 TypeScript 交互原理解析

Wails 是一个基于 Go 语言的跨平台桌面应用开发框架,允许开发者使用现代前端技术(如 Vue、React)构建用户界面,同时结合 Go 的高效后端逻辑处理能力。

以下是其核心作用和原理的概要:

1. 主要作用

  • 轻量级替代 Electron:Wails 不嵌入完整的浏览器引擎(如 Chromium),而是复用操作系统原生 WebView(如 Windows 的 WebView2、macOS 的 WebKit),显著减小应用体积(通常 <10 MB)和内存占用。
  • 高效前后端交互:提供 Go 与 JavaScript 的双向通信机制,前端可直接调用 Go 方法,后端也能通过事件系统触发前端更新。
  • 跨平台支持:编译为单一可执行文件,支持 Windows、macOS 和 Linux,保持原生窗口体验(如菜单、对话框、系统托盘)。

2. 简单原理

  • 前端渲染:使用操作系统原生 WebView 渲染前端界面(HTML/CSS/JS),支持主流框架(Vue、React 等)。
  • 后端逻辑:Go 处理业务逻辑(如文件操作、网络请求),并通过 wails.Bind() 将方法暴露给前端调用。
  • 通信机制:
    • 绑定调用:前端通过生成的 JavaScript 包装函数调用 Go 方法,返回 Promise。
    • 事件系统:前后端通过发布/订阅模式传递事件和数据。
  • 打包分发:将前端资源嵌入 Go 二进制文件,生成平台特定的可执行程序。

3. 典型应用场景

  • 轻量级工具:如系统监控、文件管理工具。
  • 跨平台 GUI 应用:需兼顾性能和原生体验的应用。
  • 替代 Electron 的场景:对体积和启动速度敏感的项目。

4. 对比 Electron

维度WailsElectron
运行时大小~10 MB(复用系统 WebView)数百 MB(内嵌 Chromium)
性能更高(低资源消耗)较低(浏览器引擎开销)
开发体验支持现代前端框架 + Go 类型安全依赖 Node.js 生态

通过结合 Go 的并发能力和现代前端技术,Wails 为开发者提供了一种高效、轻量的桌面应用开发方案。

看完估计很多人还是不太了解,下面还是需要深入了解一下它的工作过程和工作原理,特别是Golang如何与TS结合的地方。

Wails 中 Go 和 TypeScript 交互过程:

想象 Wails 应用就像一个餐厅,其中:

  • Go 代码是厨房(负责处理复杂的食物准备)
  • TypeScript/React 是前厅(负责与客人交流并展示食物)

基本交互原理

假设我们用餐厅做饭(比如KFC)的场景来比喻理解。

  1. 1. 菜单系统(函数注册)

Go 代码就像是餐厅的菜单,它提前"登记"了所有可以提供的菜品(函数):

// Go代码(厨房)声明了它能做的"菜"
func (a *App) 做汉堡(配料 string) string {
    return "一个美味的" + 配料 + "汉堡"
}

Wails 在建造餐厅(编译应用)时,会自动创建一份菜单卡片(TypeScript绑定文件):

// 自动生成的"菜单卡片"
export function 做汉堡(配料: string): Promise<string>;
  1. 2. 点餐流程(前端调用后端)

当客人(用户)想要点一个汉堡时:

// 前厅(TypeScript)收到客人的汉堡订单
import { 做汉堡 } from '../wailsjs/go/main/App';
async function 点汉堡() {
    // 服务员把订单传到厨房
    const 成品 = await 做汉堡("芝士");
    // 然后把厨房做好的汉堡端给客人
    console.log(成品); // "一个美味的芝士汉堡"
}

这里重要的是,服务员(TypeScript)不需要知道厨师(Go)是如何做汉堡的,他只需要把订单传过去,然后等待成品返回。

  1. 3. 厨房通知前厅(后端调用前端)

有时厨房需要主动通知前厅一些事情(比如食材用完了):

// Go代码(厨房)发出通知
func (a *App) 检查库存() {
    // 发现芝士用完了
    runtime.EventsEmit(a.ctx, "库存警告", "芝士用完了")
}

前厅需要有人专门负责接收这些通知:

// TypeScript(前厅)设置一个接收通知的人
import { EventsOn } from '../wailsjs/runtime';
// 设置一个接收通知的服务员
EventsOn("库存警告", (消息) => {
    alert("厨房通知: " + 消息);
    // 显示"厨房通知: 芝士用完了"
});

技术细节

Wails 如何使这一切工作

  1. 1. 自动代码生成:

    • 当你运行wails build或wails dev命令时
    • Wails 会扫描你的 Go 代码,找出所有可导出的函数(大写字母开头)
    • 然后自动在frontend/wailsjs/go/目录下生成对应的 TypeScript 文件
  2. 2. 幕后消息传递:

    • 实际上,Go 和 JavaScript 之间通过一个名为"运行时桥"的系统通信
    • 这个桥使用 JSON 格式的消息在两边传递数据
    • 所有复杂的数据类型(结构体、对象等)都会自动转换为 JSON

数据转换图解

TypeScript 侧                  Go 侧
-----------                  -------
{ name: "张三" }  ------>  struct { Name string }
   对象                        结构体

   数组      ------>       切片/数组
[1, 2, 3]              []int{1, 2, 3}

   原始类型   ------>      原始类型
  "你好"                   "你好"
   123                    123
   true                   true

四种主要交互方式

  1. 3. 直接函数调用(前端 → 后端)

最简单直观的方式,就像你点菜一样:

// 前端
import { 计算税额 } from '../wailsjs/go/main/App';
const 价格 = 100;
const 税额 = await 计算税额(价格);
console.log(税额是: ${税额});  // "税额是: 13"
// 后端
func (a *App) 计算税额(价格 float64) float64 {
    return 价格 * 0.13
}
  1. 4. 事件系统(双向)

就像餐厅里的广播系统,任何人都可以发送消息,所有人都能收到:

// 前端发送事件
import { EventsEmit } from '../wailsjs/runtime';
// 向后端发送用户登录事件
EventsEmit("用户操作", { 类型: "登录", 用户名: "小明" });
// 后端监听事件
func (a *App) 初始化(ctx context.Context) {
    a.ctx = ctx
    // 监听"用户操作"事件
    runtime.EventsOn(ctx, "用户操作", func(数据 ...interface{}) {
        fmt.Println("收到用户操作:", 数据)
    })
}
// 后端发送事件
func (a *App) 通知更新() {
    runtime.EventsEmit(a.ctx, "系统通知", "有新版本可用")
}
// 前端监听事件
import { EventsOn } from '../wailsjs/runtime';
// 监听系统通知
EventsOn("系统通知", (消息) => {
    alert(消息);  // 显示"有新版本可用"
});
  1. 5. 调用浏览器函数(后端 → 前端)

后端可以直接调用前端定义在窗口对象上的函数:

// 前端定义函数
window.显示提示 = (消息) => {
    alert(消息);
    return "用户已看到提示";
};
// 后端调用前端函数
func (a *App) 提醒用户(消息 string) {
    结果, _ := runtime.InvokeJS(a.ctx, "window.显示提示", 消息)
    fmt.Println(结果) // "用户已看到提示"
}
  1. 6. 菜单和系统对话框(特殊交互)

Wails 提供了一些特殊功能让 Go 可以控制应用窗口和系统对话框:

// 在Go中打开文件选择对话框
func (a 
App) 选择文件() {
    文件路径, _ := runtime.OpenFileDialog(a.ctx, runtime.OpenDialogOptions{
        Title: "请选择文件",
        Filters: []runtime.FileFilter{
*            {DisplayName: "文本文件", Pattern: "*
.txt"},
        },
    })

    if 文件路径 != "" {
        // 向前端发送选中的文件路径
        runtime.EventsEmit(a.ctx, "文件已选择", 文件路径)
    }
}

简单应用示例

假设我们做一个简单的温度转换应用:

  1. 1. 后端(Go)提供计算功能:

// 温度转换器.go
func (a 
App) 摄氏转华氏(摄氏度 float64) float64 {
*    return 摄氏度*
9/5 + 32
}
func (a *App) 华氏转摄氏(华氏度 float64) float64 {
    return (华氏度 - 32) * 5/9
}
  1. 2. 前端(TypeScript)提供用户界面:

// App.tsx
import { useState } from 'react';
import { 摄氏转华氏, 华氏转摄氏 } from '../wailsjs/go/main/App';
function App() {
  const [温度, 设置温度] = useState<number>(0);
  const [结果, 设置结果] = useState<number>(0);
  const [单位, 设置单位] = useState<string>("摄氏");
  async function 转换() {
    if (单位 === "摄氏") {
      // 调用Go函数进行计算
      const 华氏温度 = await 摄氏转华氏(温度);
      设置结果(华氏温度);
    } else {
      // 调用Go函数进行计算
      const 摄氏温度 = await 华氏转摄氏(温度);
      设置结果(摄氏温度);
    }
  }
  return (
    <div>
      <h1>温度转换器</h1>
      <input
        type="number"
        value={温度}
        onChange={(e) => 设置温度(parseFloat(e.target.value))}
      />
      <select value={单位} onChange={(e) => 设置单位(e.target.value)}>
        <option value="摄氏">摄氏度</option>
        <option value="华氏">华氏度</option>
      </select>
      <button onClick={转换}>转换</button>
      <div>
        结果: {结果} {单位 === "摄氏" ? "华氏度" : "摄氏度"}
      </div>
    </div>
  );
}

复杂应用示例

需要实现一个类似于Windows资源管理器的文件管理器,目标其中系统级操作由于Golang承担,界面展示操作交互由Vue/TypeScript承担。

Go中程序,入口程序:根目录/main.go

package main

import (
    "embed"
    "github.com/wailsapp/wails/v2"
    "github.com/wailsapp/wails/v2/pkg/options"
    "github.com/wailsapp/wails/v2/pkg/options/assetserver"
)

//go:embed all:frontend/dist
var assets embed.FS

func main() {
    // Create an instance of the app structure
    app := NewApp()

    // Create application with options
    err := wails.Run(&options.App{
        Title:  "Black-Wails-App",
        Width:  1024,
        Height: 768,
        AssetServer: &assetserver.Options{
            Assets: assets,
        },
        BackgroundColour: &options.RGBA{R: 27, G: 38, B: 54, A: 1},
        OnStartup:        app.startup,
        Bind: []interface{}{
            app,
        },
    })

    if err != nil {
        println("Error:", err.Error())
    }
}

Go程序暴露给前端的接口程序:根目录/App.go

package main

import (
    "context"
    "fmt"
    "go_wails_fs/backend/models"
    "go_wails_fs/backend/services"
    "github.com/wailsapp/wails/v2/pkg/runtime"
)

// App 应用结构
type App struct {
    ctx         context.Context
    fileService *services.FileService
}

// NewApp 创建新应用
func NewApp() *App {
    return &App{
        fileService: services.NewFileService(),
    }
}

// startup 在应用启动时调用
func (a *App) startup(ctx context.Context) {
    a.ctx = ctx
    // 注册前端事件监听
    runtime.EventsOn(ctx, "select-directory", a.handleDirectorySelection)
}

// GetDirectoryContents 获取目录内容
func (a *App) GetDirectoryContents(dirPath string) ([]models.FileInfo, error) {
    return a.fileService.GetDirectoryContents(dirPath)
}

// ReadFileContent 读取文件内容
func (a *App) ReadFileContent(filePath string) (string, error) {
    return a.fileService.ReadFileContent(filePath)
}

// SaveFileContent 保存文件内容
func (a *App) SaveFileContent(filePath string, content string) error {
    return a.fileService.SaveFileContent(filePath, content)
}

// CreateNew 创建新文件或目录
func (a *App) CreateNew(path string, isDir bool) error {
    return a.fileService.CreateNew(path, isDir)
}

// Delete 删除文件或目录
func (a *App) Delete(path string) error {
    return a.fileService.Delete(path)
}

// handleDirectorySelection 处理目录选择事件
func (a *App) handleDirectorySelection(optionalData ...interface{}) {
    // 打开系统目录选择对话框
    directory, err := runtime.OpenDirectoryDialog(a.ctx, runtime.OpenDialogOptions{
        Title: "选择目录",
    })

    if err == nil && directory != "" {
        // 向前端发送选中的目录
        runtime.EventsEmit(a.ctx, "directory-selected", directory)

        // 获取目录内容并发送
        contents, err := a.fileService.GetDirectoryContents(directory)
        if err == nil {
            runtime.EventsEmit(a.ctx, "directory-contents", contents)
        } else {
            runtime.EventsEmit(a.ctx, "error", fmt.Sprintf("无法读取目录内容: %s", err))
        }
    }
}

// GetSystemPath 获取系统特殊文件夹路径
func (a *App) GetSystemPath(folderType string) (string, error) {
    return a.fileService.GetSystemPath(folderType)
}

Go底层真实操作的service程序:根目录/backend/services/file_service.go

package services

import (
    "io/ioutil"
    "os"
    "path/filepath"
    "time"
    "golang.org/x/sys/windows"
    "go_wails_fs/backend/models"
)

// FileInfo 表示文件信息
type FileInfo struct {
    Name      string `json:"name"`
    Path      string `json:"path"`
    Size      int64  `json:"size"`
    IsDir     bool   `json:"isDir"`
    ModTime   string `json:"modTime"`
    Extension string `json:"extension"`
}

// FileService 处理文件操作的服务
type FileService struct{}

// NewFileService 创建新的文件服务实例
func NewFileService() *FileService {
    return &FileService{}
}

// GetSystemPath 获取系统特殊文件夹路径
func (s *FileService) GetSystemPath(folderType string) (string, error) {
    var folderID *windows.KNOWNFOLDERID
    switch folderType {
    case "desktop":
        folderID = windows.FOLDERID_Desktop
    case "documents":
        folderID = windows.FOLDERID_Documents
    case "pictures":
        folderID = windows.FOLDERID_Pictures
    case "downloads":
        folderID = windows.FOLDERID_Downloads
    case "computer":
        return filepath.Join(os.Getenv("SystemDrive"), "\\"), nil
    default:
        return "", nil
    }

    path, err := windows.KnownFolderPath(folderID, windows.KF_FLAG_DEFAULT)
    if err != nil {
        return "", err
    }
    return path, nil
}

// GetDirectoryContents 获取目录内容
func (s *FileService) GetDirectoryContents(dirPath string) ([]models.FileInfo, error) {
    files, err := ioutil.ReadDir(dirPath)
    if err != nil {
        return nil, err
    }

    var fileInfos []models.FileInfo
    for _, file := range files {
        fi := models.FileInfo{
            Name:      file.Name(),
            Path:      filepath.Join(dirPath, file.Name()),
            Size:      file.Size(),
            IsDir:     file.IsDir(),
            ModTime:   file.ModTime().Format(time.RFC3339), // 格式化为字符串
            Extension: filepath.Ext(file.Name()),
        }
        fileInfos = append(fileInfos, fi)
    }

    return fileInfos, nil
}

// ReadFileContent 读取文件内容
func (s *FileService) ReadFileContent(filePath string) (string, error) {
    content, err := ioutil.ReadFile(filePath)
    if err != nil {
        return "", err
    }
    return string(content), nil
}

// SaveFileContent 保存文件内容
func (s *FileService) SaveFileContent(filePath string, content string) error {
    return ioutil.WriteFile(filePath, []byte(content), 0644)
}

// CreateNew 创建新文件或目录
func (s *FileService) CreateNew(path string, isDir bool) error {
    if isDir {
        return os.MkdirAll(path, 0755)
    }

    file, err := os.Create(path)
    if err != nil {
        return err
    }
    return file.Close()
}

// Delete 删除文件或目录
func (s *FileService) Delete(path string) error {
    return os.RemoveAll(path)
}

前端桥接程序接口定义:根目录/frontend/wailsjs/go/main/App.d.ts

export namespace models {    
    export class FileInfo {
        name: string;
        path: string;
        size: number;
        isDir: boolean;
        modTime: string;
        extension: string;
    
        static createFrom(source: any = {}) {
            return new FileInfo(source);
        }
    
        constructor(source: any = {}) {
            if ('string' === typeof source) source = JSON.parse(source);
            this.name = source["name"];
            this.path = source["path"];
            this.size = source["size"];
            this.isDir = source["isDir"];
            this.modTime = source["modTime"];
            this.extension = source["extension"];
        }
    }
}

export function CreateNew(arg1:string,arg2:boolean):Promise<void>;

export function Delete(arg1:string):Promise<void>;

export function GetDirectoryContents(arg1:string):Promise<Array<models.FileInfo>>;

export function GetSystemPath(arg1:string):Promise<string>;

export function ReadFileContent(arg1:string):Promise<string>;

export function SaveFileContent(arg1:string,arg2:string):Promise<void>;

前端入口程序:根目录/frontend/src/main.ts

import { createApp } from 'vue'
import App from './App.vue'
import './style.css';

const app = createApp(App)
app.mount('#app')

前端主代码:根目录/frontend/src/App.vue

<script lang="ts" setup>
import { ref, onMounted } from 'vue'
import type { FileInfo } from './types/file'
import { EventsOn, EventsEmit } from '../wailsjs/runtime'
import { GetDirectoryContents, ReadFileContent, SaveFileContent, CreateNew, Delete, GetSystemPath } from '../wailsjs/go/main/App'

// 状态定义
const currentDir = ref<string>('')
const files = ref<FileInfo[]>([])
const selectedFile = ref<FileInfo | null>(null)
const fileContent = ref<string>('')
const isEditing = ref<boolean>(false)
const newItemName = ref<string>('')
const isCreatingDir = ref<boolean>(false)
const isCreating = ref<boolean>(false)
const isGridView = ref<boolean>(false)

// 事件监听设置
onMounted(() => {
  // 监听目录选择事件
  EventsOn('directory-selected', (directory: string) => {
    currentDir.value = directory
  })
//........

其他代码就不一一展示,主体代码逻辑就是如上,基本能够描绘整个工作过程。

编译:wails build

最终程序效果:

Wails内部原理总结

  1. 1. Go是引擎,TypeScript是仪表盘:

    • Go处理复杂计算、文件操作、系统功能
    • TypeScript负责展示界面、收集用户输入
  2. 2. 通信方式:

    • 前端可以直接调用后端函数(同步或异步)
    • 后端可以通过事件系统通知前端
    • 后端可以直接调用前端定义的函数
    • 两边都可以发送和接收广播式的事件
  3. 3. 数据自动转换:

    • Go结构体 ⟷ TypeScript对象
    • Go切片/数组 ⟷ TypeScript数组
    • 基本类型自动对应

这种设计让你能够充分利用Go语言的系统能力和TypeScript/React的界面优势,创建既强大又美观的桌面应用。



【大模型介绍电子书】

快速揭秘DeepSeek背后的AI工作原理

要获取本书全文PDF内容,请在【黑夜路人技术】VX后台留言:“AI大模型基础” 或者 “大模型基础” 就会获得电子书的PDF。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值