Golang webview 做桌面应用及跨平台编译
收藏

这是一个小型跨平台webview库,用于构建跨平台GUI。 还支持Rust、Python、Nim、Haskell和 C# 绑定。支持双向JavaScript绑定。

它在macOS上使用Cocoa / WebKit,在Linux上使用gtk-webkit2,在Windows上使用MSHTML(IE10 / 11)。

webview

使用 URL data

下面是一个简单的应用示例:

package main

import (
	"encoding/base64"
	"fmt"
	"github.com/zserge/webview"
	"html/template"
	"io/ioutil"
	"log"
	"net/url"
)

func callback(w webview.WebView, data string) {
	if data == "open" {
		path := w.Dialog(webview.DialogTypeOpen, 0, "Open image", "")
		if path == "" {
			return
		}
		b, err := ioutil.ReadFile(path)
		if err != nil {
			log.Fatalln(err)
		}
		err = w.Eval(fmt.Sprintf(`
			var img = document.createElement('img');
			img.src = 'data:image/png;base64,%s';
			img.style.maxWidth = '%s';
			var first = document.getElementById('gallery').firstChild;
			document.getElementById('gallery').insertBefore(img, first);
			`,
			template.JSEscapeString(base64.StdEncoding.EncodeToString(b)),
			template.JSEscapeString("100%"),
		))
		if err != nil {
			log.Fatalln(err)
		}
	}
}

const (
	preJS     = `window.console.log=function(s){external.invoke('{"type":"log","data":"'+s+'"}')};window.console.debug=function(s){external.invoke('{"type":"debug","data":"'+s+'"}')}`
	indexHTML = `<!doctype html><html><head><meta charset="utf-8"/></head><body><button onclick="external.invoke('open')">Open image</button><div id="gallery"></div></body></thml>`
)

func main() {
	w := webview.New(webview.Settings{
		Width:                  320,
		Height:                 640,
		Title:                  "Gallery",
		URL:                    "data:text/html," + url.PathEscape(indexHTML),
		ExternalInvokeCallback: callback,
		Debug:                  true,
	})
	defer w.Exit()
	w.Dispatch(func() { w.Eval(template.JSEscapeString(preJS)) })
	w.Run()
}

上面的例子是直接使用data:[<mediatype>][;base64],<data> 的方式在浏览器打开文件。

支持浏览器

  • Firefox 2+
  • Opera 7.2+
  • Chrome (所有版本)
  • Safari (所有版本)
  • Internet Explorer 8+

长度限制

长度限制,长度超长,在一些应用下会导致内存溢出,程序崩溃

  • Opera 下限制为 4100 个字符,目前已经去掉了这个限制
  • IE 8+ 下限制为 32,768 个字符(32kb),IE9 之后移除了这个限制

在 IE 下,data URI 只允许被用到如下地方:

  • object (images only)
  • img、input type=image、link
  • CSS 中允许使用 URL 声明的地方,如 background
  • 在 IE 下,Data URI 的内容必须是经过编码转换的,如 “#”、”%”、非 US-ASCII 字符、多字节字符等,必须经过编码转换

启动WebServer

也可以通过下面的方式,在本地起一个webserver,建立桌面应用。

package main

//go:generate go get -u github.com/jteeuwen/go-bindata/...
//go:generate go-bindata -pkg $GOPACKAGE -o assets.go -prefix assets/ assets/

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"log"
	"mime"
	"net"
	"net/http"
	"path/filepath"

	"github.com/zserge/webview"
)

func startServer() string {
	ln, err := net.Listen("tcp", "127.0.0.1:0")
	if err != nil {
		log.Fatal(err)
	}
	go func() {
		defer ln.Close()
		http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
			path := r.URL.Path
			if len(path) > 0 && path[0] == '/' {
				path = path[1:]
			}
			if path == "" {
				path = "index.html"
			}
			if bs, err := Asset(path); err != nil {
				w.WriteHeader(http.StatusNotFound)
			} else {
				w.Header().Add("Content-Type", mime.TypeByExtension(filepath.Ext(path)))
				io.Copy(w, bytes.NewBuffer(bs))
			}
		})
		log.Fatal(http.Serve(ln, nil))
	}()
	return "http://" + ln.Addr().String()
}

// Task is a data model type, it contains information about task name and status (done/not done).
type Task struct {
	Name string `json:"name"`
	Done bool   `json:"done"`
}

// Tasks is a global data model, to keep things simple.
var Tasks = []Task{}

func render(w webview.WebView, tasks []Task) {
	b, err := json.Marshal(tasks)
	if err == nil {
		w.Eval(fmt.Sprintf("rpc.render(%s)", string(b)))
	}
}

func handleRPC(w webview.WebView, data string) {
	cmd := struct {
		Name string `json:"cmd"`
	}{}
	if err := json.Unmarshal([]byte(data), &cmd); err != nil {
		log.Println(err)
		return
	}
	switch cmd.Name {
	case "init":
		render(w, Tasks)
	case "log":
		logInfo := struct {
			Text string `json:"text"`
		}{}
		if err := json.Unmarshal([]byte(data), &logInfo); err != nil {
			log.Println(err)
		} else {
			log.Println(logInfo.Text)
		}
	case "addTask":
		task := Task{}
		if err := json.Unmarshal([]byte(data), &task); err != nil {
			log.Println(err)
		} else if len(task.Name) > 0 {
			Tasks = append(Tasks, task)
			render(w, Tasks)
		}
	case "markTask":
		taskInfo := struct {
			Index int  `json:"index"`
			Done  bool `json:"done"`
		}{}
		if err := json.Unmarshal([]byte(data), &taskInfo); err != nil {
			log.Println(err)
		} else if taskInfo.Index >= 0 && taskInfo.Index < len(Tasks) {
			Tasks[taskInfo.Index].Done = taskInfo.Done
			render(w, Tasks)
		}
	case "clearDoneTasks":
		newTasks := []Task{}
		for _, task := range Tasks {
			if !task.Done {
				newTasks = append(newTasks, task)
			}
		}
		Tasks = newTasks
		render(w, Tasks)
	}
}

func main() {
	url := startServer()
	w := webview.New(webview.Settings{
		Width:  320,
		Height: 480,
		Title:  "Todo App",
		URL:    url,
		ExternalInvokeCallback: handleRPC,
	})
	defer w.Exit()
	w.Run()
}

跨平台编译

给windows 系统编译,需要mingw-w64

我在Mac 上给windows 系统编译

brew install mingw-w64
GOOS=windows GOARCH="386" CGO_ENABLED=1 CC="i686-w64-mingw32-gcc" go build -ldflags="-H windowsgui" -o webview-example_32bit.exe 
GOOS=windows GOARCH="amd64" CGO_ENABLED=1 CC="x86_64-w64-mingw32-gcc" go build -ldflags="-H windowsgui" -o webview-example_64bit.exe 

相关资源:

Relative Articles

    公众号
    关注公众号订阅更多技术干货!