go version go1.20 windows/amd64
先要了解一些第三方库
1、webview/webview
它是一个跨平台
的轻量级的webview库,面向的是C/C++,使用它可以构建跨平台的GUI。webview就是浏览器内核,在不同操作系统上是不同的库,比如在windows上为webview2,所以webview与webview2不要搞混了。从功能上,它实现了Javascript与C/C++之间的相互调用,即bindings。具体实现原理应该与wails差不多。
在不同平台上的支持如下
Platform | Technologies |
---|---|
Linux | GTK, WebKitGTK |
macOS | Cocoa, WebKit |
Windows | Windows API, WebView2 |
windows api 是windows系统提供的DLL(user32.dll),以此来构建windows上原生的GUI程序。
webview2是微软的Edge浏览器内核,用它来渲染前端页面,下载地址,所以这是两种不同的实现方式。而且webview2只支持windows系统。如果你的系统是Windows 10+,那么 WebView2 Runtime会默认安装了。
2、webview/webview_go
它使用Golang将webview/webview包装了一下,但是使用的是CGO,即webview/webview_go依赖于webview/webview,并使用CGO将依赖引入。
项目还不完善,我的本地系统x86_64-w64-mingw32
下面找不到EventToken.h
头文件,所以代码无法运行,Support for EventToken.h on mingw64 #45 依然没有解决。
3、jchv/go-webview2
它借鉴了webview/webview,使用Golang包装了webview2,不需要使用CGO,这是与webview_go的不同,当然此包只支持windows,因为它只是包装了webview2。在wails中使用的就是 jchv/go-webview2
它提供了一个示例:cmd/demo/main.go
import (
"log"
"github.com/jchv/go-webview2"
)
func main() {
w := webview2.NewWithOptions(webview2.WebViewOptions{
Debug: true,
AutoFocus: true,
WindowOptions: webview2.WindowOptions{
Title: "Minimal webview example",
Width: 800,
Height: 600,
IconId: 2, // icon resource id
Center: true,
},
})
if w == nil {
log.Fatalln("Failed to load webview.")
}
defer w.Destroy()
w.SetSize(800, 600, webview2.HintFixed)
w.Navigate("https://en.m.wikipedia.org/wiki/Main_Page")
w.Run()
}
go-webview2简要说明
webviewloader\module.go
先使用windows.NewLazyDLL("WebView2Loader")
加载WebView2Loader.dll
,如果没有找到,它会使用go-winloader库来加载go-webview2自带的WebView2Loader.dll
文件(webviewloader/sdk/x64下)。
webview2的参考文档:https://learn.microsoft.com/zh-cn/microsoft-edge/webview2/
go-webview2基本已经封装的很好了,下面是一个WebView对象提供的功能。
// WebView is the interface for the webview.
type WebView interface {
// Run runs the main loop until it's terminated. After this function exits -
// you must destroy the webview.
Run()
// Terminate stops the main loop. It is safe to call this function from
// a background thread.
Terminate()
// Dispatch posts a function to be executed on the main thread. You normally
// do not need to call this function, unless you want to tweak the native
// window.
Dispatch(f func())
// Destroy destroys a webview and closes the native window.
Destroy()
// Window returns a native window handle pointer. When using GTK backend the
// pointer is GtkWindow pointer, when using Cocoa backend the pointer is
// NSWindow pointer, when using Win32 backend the pointer is HWND pointer.
Window() unsafe.Pointer
// SetTitle updates the title of the native window. Must be called from the UI
// thread.
SetTitle(title string)
// SetSize updates native window size. See Hint constants.
SetSize(w int, h int, hint Hint)
// Navigate navigates webview to the given URL. URL may be a data URI, i.e.
// "data:text/text,<html>...</html>". It is often ok not to url-encode it
// properly, webview will re-encode it for you.
Navigate(url string)
// SetHtml sets the webview HTML directly.
// The origin of the page is `about:blank`.
SetHtml(html string)
// Init injects JavaScript code at the initialization of the new page. Every
// time the webview will open a the new page - this initialization code will
// be executed. It is guaranteed that code is executed before window.onload.
Init(js string)
// Eval evaluates arbitrary JavaScript code. Evaluation happens asynchronously,
// also the result of the expression is ignored. Use RPC bindings if you want
// to receive notifications about the results of the evaluation.
Eval(js string)
// Bind binds a callback function so that it will appear under the given name
// as a global JavaScript function. Internally it uses webview_init().
// Callback receives a request string and a user-provided argument pointer.
// Request string is a JSON array of all the arguments passed to the
// JavaScript function.
//
// f must be a function
// f must return either value and error or just error
Bind(name string, f interface{}) error
}
Navigate方法的参数
- 网络地址:https://en.m.wikipedia.org/wiki/Main_Page
- 文件系统地址:file:///D:/dev/php/magook/trunk/server/go-webview/test.html,要使用绝对地址。当然,只要是浏览器能识别的文件都行,比如图片,网页,TXT文件等等。
- 直接展示内容:就是将文件的内容复制进去,此时就需要指定内容是什么形式的,如果不指定就无法展示出来。格式为
mimetype,content
,其中 mimetype的常用的有data:image/gif, data:image/webp, data:image/jpeg, data:image/png, data:text/html, data:text/text
,它会严格按照mimetype渲染,所以类型一定要对。
但是不推荐这种用法,我发现它会将本行中#
后面的内容当做注释舍弃掉,比如
......
let option_CrGrDQTOQuKK = {"color":["#5470c6","#91cc75","#fac858","#ee6666","#73c0de","#3ba272","#fc8452","#9a60b4","#ea7ccc"],"legend":{},"series":[{"name":"Category A","type":"bar","data":[{"value":235},{"value":41},{"value":90},{"value":256},{"value":202},{"value":40},{"value":11}]},{"name":"Category B","type":"bar","data":[{"value":277},{"value":69},{"value":158},{"value":274},{"value":116},{"value":43},{"value":233}]}],"title":{"text":"My first bar chart generated by go-echarts","subtext":"It's extremely easy to use, right?"},"toolbox":{},"tooltip":{},"xAxis":[{"data":["Mon","Tue","Wed","Thu","Fri","Sat","Sun"]}],"yAxis":[{}]}
......
你会发现这一行只剩下了let option_CrGrDQTOQuKK = {"color":["
,肯定会影响结果的。所以,最好使用前两种方式。
Bind 方法,进行了绑定之后,js就可以调用name方法,而这个name方法在go中的实现逻辑就是 f。
func (w *webview) Bind(name string, f interface{}) error {
v := reflect.ValueOf(f)
if v.Kind() != reflect.Func {
return errors.New("only functions can be bound")
}
if n := v.Type().NumOut(); n > 2 {
return errors.New("function may only return a value or a value+error")
}
w.m.Lock()
w.bindings[name] = f
w.m.Unlock()
w.Init("(function() { var name = " + jsString(name) + ";" + `
var RPC = window._rpc = (window._rpc || {nextSeq: 1});
window[name] = function() {
var seq = RPC.nextSeq++;
var promise = new Promise(function(resolve, reject) {
RPC[seq] = {
resolve: resolve,
reject: reject,
};
});
window.external.invoke(JSON.stringify({
id: seq,
method: name,
params: Array.prototype.slice.call(arguments),
}));
return promise;
}
})()`)
return nil
}
它这里使用的是window.external.invoke()
方法,即调用外部方法,因为它包装的是new Promise
,所以使用 Promise 的机制来处理返回值。
在RPC调用方面,其内部机制还是postMessage()消息事件,更具体的可以阅读我关于wails的文章。
如果 Debug = true,那么按F12可以打开调试。
我的代码如下:
package main
import (
"errors"
"log"
webview2 "github.com/jchv/go-webview2"
)
func main() {
w := webview2.NewWithOptions(webview2.WebViewOptions{
Debug: true,
AutoFocus: true,
WindowOptions: webview2.WindowOptions{
Title: "Minimal webview example",
Width: 800,
Height: 600,
IconId: 2, // icon resource id
Center: true,
},
})
if w == nil {
log.Fatalln("Failed to load webview.")
}
defer w.Destroy()
w.Bind("test_love", Great)
w.Navigate("file:///D:/dev/php/magook/trunk/server/go-webview/test.html")
w.Run()
}
func Great(param string) (result string, err error) {
if param == "I love you" {
return "I love you too", nil
} else if param == "I hate you" {
return "", errors.New("break up")
} else {
return "I don't know", nil
}
}
特别注意:
Bind() 一定要在 Navigate() 之前调用,否则就是无效的!!
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>爱吗?</title>
</head>
<body>
<div class="box">
<input type="button" value="爱你" class="btn" onclick="love()">
<input type="button" value="不爱了" class="btn" onclick="nolove()">
</div>
</body>
</html>
<style>
.box {
margin-top: 100px;
text-align: center;
}
.btn {
display: inline-block;
width: 100px;
height: 50px;
border-radius: 10px;
border: 2px solid green;
text-align: center;
}
</style>
<script>
function love() {
window["test_love"]("I love you").then(result => {
alert(result);
}).catch(err => {
alert(err);
});
}
function nolove() {
window["test_love"]("I hate you").then(result => {
alert(result);
}).catch(err => {
alert(err);
});
}
</script>
打开调试,console.log(window)
,或者 console.log(window.test_love)
,就能看到你定义的方法已经被注册到了window对象上。
运行效果:
由于wails引入了go-webview2,并做了扩展,所以直接使用wails框架即可。