从零开始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/[email protected]
装置胜利后能够输出
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
监听事件:EventsOn
、EventsOnce
、EventsOnMultiple
Go中是在runtime
中调用,前端是在window.runtime
中调用(wailsjs/runtime
下曾经列举了runtime所有可用函数)。
间接调用Go函数
前端能够间接调用Go办法。wails会在window对象上绑定go对象,其中会裸露相干公共函数。
默认代码有提供App构造体的一个公共函数Greet
,按以下门路拜访
window.go.main.App.Greet
返回的是一个promis对象。
在前端目录下有一个wailsjs
文件夹,外面有两个文件夹runtime
、go
。
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 我的项目