3

从零开始Wails2编写Web桌面应用

前端要写桌面应用的话首先想到的肯定是Electron,Electron的应用成熟度已经无可置疑,但包体积始终是个令人头疼的问题。如果很在意体积问题,而且喜欢尝试新技术的话,在现代其他编程语言不断侵入前端生态的情况下,我们选择的眼光也不一定要局限在JavaScript上。

Wails就是基于Go+Web技术的桌面应用程序生成的项目,其前端渲染层使用的是脱胎于Edge的WebView2,相比于Electron,打包编译体积小了非常多。微软的Teams 项目在此前从Electron切换到Webview2 内存消耗直接减半。两者更多的差异Electron Blog上有一篇文章可以查看:WebView2 and Electron。(Rust生态中热门的Tauri项目也是使用Webview2 作为渲染)

如果你是第一次接触Go或是查看Wails基础教程,可以配合wails-data-filter 项目代码,从零开始,感性认识一下如何实现一个简单的桌面应用:

安装环境

目标平台是Windows10,我们就在这里制作编译桌面应用。

需要安装两个依赖:

  • NPM (Node 15+)

    这个前端应该很熟悉了,选择版本适合的Node即可。

  • Go 1.17+

    https://go.dev/dl/下载Windows安装包。

    安装成功后在命令行下输入

    go version

    如果返回版本信息即安装成功。

    如果报错提示找不到,则需要检查一下系统环境变量Path里是否有Go安装的目录,Windows默认安装在C:\Program Files\Go。如果存在,可以尝试重新打开命令行窗口或是重启一下。

    几个相关的环境变量:

    • GOROOT,安装目录路径
    • GOPATH,开发工作区路径
    • GOBIN,编译存放的路径
  • Wails

    当可以运行Go之后就可以下载Go程序了,执行命令

    go install github.com/wailsapp/wails/v2/cmd/wails@latest

    安装成功后可以输入

    wails doctor

    检查wails命令是否能正确运行,同时也会输出检查报告。按着提示做相应操作即可。

创建项目

wails 提供了创建命令init,比如需要生成Vue项目

wails init -n wails-vue -t vue

需要使用TypeScript的话

wails init -n wails-vue -t vue-ts

React、原生JavaScript也一样,具体可以查看官网文档

目录结构

├── build/              # 项目构建目录
│   ├── appicon.png     
│   ├── darwin/
│   └── windows/
├── frontend/           # 前端项目目录 可以放任意前端项目
│   └── wailsjs/          # wails与前端通信的工具方法 会自动同步Go代码中的公共方法
├── go.mod              # Go的依赖管理文件
├── go.sum              # Go的依赖树校验文件
├── main.go             # 项目入口文件
├── app.go              # 项目App结构体定义
└── wails.json          # wails配置文件

初次使用

Go是类C语言,但比C语言强大了不少,在编译、效率、开发难易度中尽力做出了平衡。所以有了C语言基础,对于Go的上手就能轻松不少。

没学过也不用担心,只要多看看基础文档,熟悉一下语法风格也能很快上手。而且开发中除非涉及到与系统交互JavaScript没办法处理,否则可以不编写Go代码。

main.go中存放着项目入口main()函数,其中只有相关配置需要我们根据实际情况做调整。

考虑到如果第一次接触Go语言,直接从编写逻辑开始可能会有一点抗拒,所以还是先看看main.go代码,了解一下Go语法的样子:

package main

import (
  "embed"

  "github.com/wailsapp/wails/v2"
  "github.com/wailsapp/wails/v2/pkg/options"
)

//go:embed 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:            "数据过滤工具",
    Width:            640,
    Height:           480,
    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())
  }
}
  • package

    package用来指定包名,可以视为命名空间。在不同文件中如果在同一个包名下,之间的定义是可以互相调用的。

    package必须在第一行且小写。对于可执行程序必须要有main包,并且有且只有一个main()函数。如果没有main()函数,在应用程序启动时将会报错。

  • import

    import用来导入其他包。像是以上代码就导入了三个包,一个内置的embed用来将静态资源嵌入程序中,另外两个就是wails。

  • var

    var是申明变量,var assets embed.FS即是申明assets类型为embed.FS的变量。这里使用了embed包提供的指令//go:embed,将指定路径资源转换为embed.FS类型(匹配多个文件时使用)并绑定到变量上,这里即绑定了前端目录下的dist目录中所有的资源。

  • main函数逻辑

    • :=

      在Go语言中,除了使用var申明变量外,还可以使用简写方法:=,一次性申明并赋值。NewApp()即是来自同个包名的另一个文件app.go,暂时先不关注。

    • wails

      wails.Run即是调用wails的启动方法,其中参数是wails提供的配置,可以自定义调整。详细的配置可以看文档参数选项

    • if err != nil

      Go中的if语句写法,无需括号。nil代表空值。

启动文件只有配置信息需要根据实际情况修改,其余的保持默认即可。

来试着运行一下,输入编译命令:

wails build

等待编译成功后,在build/bin目录下会生成exe文件,双击即可运行。此时能够看到前端项目的网页,这样一个独立的前端Web应用就生成了。

开发

既然分为前端与Go项目,那么肯定得先知道两者是通过什么方式进行通信的。

前端与Go互相通信

使用事件通信

当Go需要主动推送消息给前端时,或者前端推送消息给Go时,可以使用事件函数。

发送事件:EventsEmit

监听事件:EventsOnEventsOnceEventsOnMultiple

Go中是在runtime中调用,前端是在window.runtime中调用(wailsjs/runtime下已经罗列了runtime所有可用函数)。

直接调用Go函数

前端可以直接调用Go方法。wails会在window对象上绑定go对象,其中会暴露相关公共函数。

默认代码有提供App结构体的一个公共函数Greet,按以下路径访问

window.go.main.App.Greet

返回的是一个promis对象。

在前端目录下有一个wailsjs文件夹,里面有两个文件夹runtimego

wailsjs/runtime中提供了wails内置的公共函数。

wailsjs/go中提供了结构体对应的公共函数,当go代码变动时,这里文件会自动更新。

为了导出方便,我们可以在根目录创建一个index.js文件,统一导出子目录中的方法。

export * from "./go/main/App";
export * from "./runtime/runtime";

在使用时就不用从window里写一长串代码调用了,只需import即可

import { Greet } from "../wailsjs";

其他内置函数也以同样方式调用。

向Go中添加公共函数

如何添加公共函数供前端调用?这就需要看一看app.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)
}
  • struct

    Go中没有类的概念,对于复合型数据,特别是还需要带有函数的数据,需要使用结构体来实现。

    使用语法与前端对象看起来差不多。

    type 名称 struct {
      成员名称 成员类型
    }

    但添加函数就差别比较大了,没办法在结构体里直接定义函数,需要使用func关键字与结构体绑定。

    func (a *App) startup(ctx context.Context) {
      a.ctx = ctx
    }

    如果是普通函数申明

    func startup(ctx context.Context) {
      //
    }

    二者差异就在func之后的括号,跟随的是需要绑定的结构体定义,a是结构体在函数内的调用名。

  • 变量名命名规范

    Go为了简化语法,变量名是否可外部访问依赖首字母是否大写来判断。

    func (a *App) Greet(name string) string {
      return fmt.Sprintf("Hello %s, It's show time!", name)
    }

    Greet相对于上面的函数就是以首字母大写开头, 在编译时也会自动生成到前端文件中供调用。

    所以需要添加函数,只需要将此代码复制一下,修改成所需名称,只要保持首字母大写即可被前端访问到。

通信数据转换

Go接收前端返回的值会涉及到类型问题。wails会做自动类型转换。大致如下:

  • 数组、切片:JavaScript数组
  • Map:JavaScript对象
  • 结构体:JavaScript Map对象

开发模式

开发的时候使用dev命令会自动编译构建

wails dev

程序会自动监控,Go与前端资源变化后都会自动刷新。

接下来就很简单了,就不太详细的描述了。

前端页面

这里使用Vue3+Tailwindcss+Daisyui构建前端页面。这里默认大家都有必要的前端基础了,代码也复杂,过程就省略了,实现的页面样式:

主要功能就是点击按钮,让用户选择文件,按配置的规则会过滤其中的数据,最后在文件当前目录下生成处理后的文件。部分关键点进行说明,完整代码可以查看项目:

获取文件路径

因为安全限制用浏览器的type为file的 input无法获取到文件真实路径。好在wails提供了调用系统选择文件的方法,调用runtime.OpenFileDialog即可打开系统选择文件弹窗。

app.go文件中添加代码

// SelectFile 选择需要处理的文件
func (a *App) SelectFile(filetype string) string {
  if filetype == "" {
    filetype = "*.txt;*.json"
  }
  selection, err := runtime.OpenFileDialog(a.ctx, runtime.OpenDialogOptions{
    Title: "选择文件",
    Filters: []runtime.FileFilter{
      {
        DisplayName: "文本数据",
        Pattern:     filetype,
      },
    },
  })
  if err != nil {
    return fmt.Sprintf("err %s!", err)
  }
  return selection
}

之后从wailsjs文件夹中导入即可使用

import { SelectFile } from "../../wailsjs";

const handleSelectFile = async () => {
  const path = await SelectFile();
  console.log("选择文件的路径", path)
};
超大文件读取

对于文本处理,假如用户上传的一个超大文件,一次性读取会占用过多内存导致问题。可以换成逐行读取,节约处理内存

// FilterFile 处理文件数据
func (a *App) FilterFile(filePath string) string {
  fileHandle, err := os.Open(filePath)
  if err != nil {
    fmt.Println(err.Error())
    return ""
  }
  defer fileHandle.Close()
  
  accumulationLine := 0
  lineScanner := bufio.NewScanner(fileHandle)
  for lineScanner.Scan() {
    newStr := lineScanner.Text()
    accumulationLine++
    runtime.EventsEmit(a.ctx, "filter-change", accumulationLine)
    // ...
  }
}

这里同时调用runtime.EventsEmit向前端推送事件,提供正在读取的行数信息。

编码问题

文本文件会遇到各种编码,Go中默认是以UTF-8保存读取。如果遇到GBK编码就会乱码了。好在有一些方案可以处理。

判断是否是UTF-8编码:

import "unicode/utf8"

utf8.ValidString(str)

GBK转UTF-8编码:

import (
  "bytes"
  "golang.org/x/text/encoding/simplifiedchinese"
  "golang.org/x/text/transform"
  "io/ioutil"
)

func GbkToUtf8(s []byte) ([]byte, error) {
  reader := transform.NewReader(bytes.NewReader(s), simplifiedchinese.GBK.NewDecoder())
  t, e := ioutil.ReadAll(reader)
  if e != nil {
    return nil, e
  }
  return t, nil
}

实际的功能编写就如写往常前端一样,需要存储数据、一些前端不方便做的操作,写在Go里,前端像是调用接口一样传参调用即可。

无边框界面

wails提供无边框模式,只需要在wails.Run配置中添加Frameless配置即可

  err := wails.Run(&options.App{
    Title:            "数据过滤工具",
    Width:            640,
    Height:           480,
    Assets:           assets,
    BackgroundColour: &options.RGBA{R: 27, G: 38, B: 54, A: 1},
    OnStartup:        app.startup,
    Frameless:        true, // 无边框模式
    DisableResize:    true,
    Bind: []interface{}{
      app,
    },
  })

启动后应用程序就没用边框,此时因为没有边框,所以程序没有可拖动窗体的区域。

只需要在要拖动的元素上增加data-wails-no-drag属性,即可重新获得拖动能力。如果其中子元素不需要响应拖动,则添加data-wails-no-drag

生成应用程序

wails build

Webview2是调用系统自带的,如果系统未携带,或者版本太旧就很麻烦了。编译有提供-webview2命令,可以配置这种情况下的处理方案。一共有四种:Error(报错),Browser(打开浏览器引导程序下载页)、Download(自动下载引导程序并运行)、Embed(内嵌文件)。

但我实际测试了一下,编译文件变大了(大约增加150k体积),换了台Win10运行时依然需要网络下载,不确定是否要同时配置什么参数。

更换图标

更换build/windows下的icon.ico文件,打包时就会使用新的图标了。

报毒问题

如果在使用360安全卫士的电脑上会直接报毒,这个也是制作小工具最令人头疼的地方,要不然让对方将软件加入白名单,要不然只能给360提交误报了。

链接

Wails官网

wails-data-filter 项目


LnEoi
707 声望17 粉丝