go-app
是一個使用 Go + WebAssembly 技術編寫漸進式 Web 應用的庫。WebAssembly 是一種能夠運行在現代瀏覽器中的新式代碼。近兩年來,WebAssembly 技術取得了較大的發展。咱們如今已經可使用 C/C++/Rust/Go 等高級語言編寫 WebAssembly 代碼。原本就來介紹go-app
這個能夠方便地使用 Go 語言來編寫 WebAssembly 代碼的庫。css
go-app
對 Go 語言版本有較高的要求(Go 1.14+),並且必須使用Go module
。先建立一個目錄並初始化Go Module
(Win10 + Git Bash):html
$ mkdir go-app && cd go-app $ go mod init
而後下載安裝go-app
包:node
$ go get -u -v github.com/maxence-charriere/go-app/v6
至於Go module
的詳細使用,去看煎魚大佬的Go Modules 終極入門。git
首先,咱們要編寫 WebAssembly 程序:github
package main import "github.com/maxence-charriere/go-app/v6/pkg/app" type Greeting struct { app.Compo name string } func (g *Greeting) Render() app.UI { return app.Div().Body( app.Main().Body( app.H1().Body( app.Text("Hello, "), app.If(g.name != "", app.Text(g.name), ).Else( app.Text("World"), ), ), ), app.Input(). Value(g.name). Placeholder("What is your name?"). AutoFocus(true). OnChange(g.OnInputChange), ) } func (g *Greeting) OnInputChange(src app.Value, e app.Event) { g.name = src.Get("value").String() g.Update() } func main() { app.Route("/", &Greeting{}) app.Run() }
在go-app
中使用組件來劃分功能模塊,每一個組件結構中必須內嵌app.Compo
。組件要實現Render()
方法,在須要顯示該組件時會調用此方法返回顯示的頁面。go-app
使用聲明式語法,徹底使用 Go 就能夠編寫 HTML 頁面,上面繪製 HTML 的部分比較好理解。上面代碼中還實現了一個輸入框的功能,併爲它添加了一個監聽器。每當輸入框內容有修改,OnInputChange
方法就會調用,g.Update()
會使該組件從新渲染顯示。golang
最後將該組件掛載到路徑/
上。web
編寫 WebAssembly 程序以後,須要使用交叉編譯的方式將它編譯爲.wasm
文件:瀏覽器
$ GOARCH=wasm GOOS=js go build -o app.wasm
若是編譯出現錯誤,使用go version
命令檢查 Go 是不是 1.14 或更新的版本。緩存
接下來,咱們須要編寫一個 Go Web 程序使用這個app.wasm
:安全
package main import ( "log" "net/http" "github.com/maxence-charriere/go-app/v6/pkg/app" ) func main() { h := &app.Handler{ Title: "Go-App", Author: "dj", } if err := http.ListenAndServe(":8080", h); err != nil { log.Fatal(err) } }
go-app
提供了一個app.Handler
結構,它會自動查找同目錄下的app.wasm
(這也是爲何將目標文件設置爲app.wasm
的緣由)。而後咱們將前面編譯生成的app.wasm
放到同一目錄下,執行該程序:
$ go run main.go
默認顯示"Hello World"
:
在輸入框中輸入內容以後,顯示會隨之變化:
能夠看到,go-app
爲咱們設置了一些基本的樣式,網頁圖標等。
GitHub 上這張圖很好地說明了 HTTP 請求的執行流程:
用戶請求先到app.Handler
層,它會去app.wasm
中執行相關的路由邏輯、去磁盤上查找靜態文件。響應經由app.Handler
中轉返回給用戶。用戶就看到了app.wasm
渲染的頁面。實際上,在本文中咱們只須要編寫一個 Go Web 程序,每次編寫新的 WebAssembly 以後,將新編譯生成的 app.wasm 文件拷貝到 Go Web 目錄下從新運行程序便可。注意,若是頁面未能及時刷新,多是緩存致使的,可嘗試清理瀏覽器緩存。
自定義一個組件很簡單,只須要將app.Compo
內嵌到結構中便可。實現Render()
方法可定義組件的外觀,實際上app.Compo
有一個默認的外觀,咱們能夠這樣來查看:
func main() { app.Route("/app", &app.Compo{}) app.Run() }
編譯生成app.wasm
以後,一開始的 Go Web 程序不須要修改,直接運行,打開瀏覽器查看:
在快速開始中,咱們還介紹瞭如何使用事件。使用聲明式語法app.Input().OnChange(handler)
便可監聽內容變化。事件處理函數必須爲func (src app.Value, e app.Event)
類型,app.Value
是觸發對象,app.Event
是事件的內容。經過app.Value
咱們能夠獲得輸入框內容、選擇框的選項等信息,經過app.Event
能夠獲得事件的信息,是鼠標事件、鍵盤事件仍是其它事件:
type ShowSelect struct { app.Compo option string } func (s *ShowSelect) Render() app.UI { return app.Div().Body( app.Main().Body( app.H1().Body( app.If(s.option == "", app.Text("Please select!"), ).Else( app.Text("You've selected "+s.option), ), ), ), app.Select().Body( app.Option().Body( app.Text("apple"), ), app.Option().Body( app.Text("orange"), ), app.Option().Body( app.Text("banana"), ), ). OnChange(s.OnSelectChange), ) } func (s *ShowSelect) OnSelectChange(src app.Value, e app.Event) { s.option = src.Get("value").String() s.Update() } func main() { app.Route("/", &ShowSelect{}) app.Run() }
上面代碼顯示一個選擇框,當選項改變時上面顯示的文字會作相應的改變。初始時:
選擇後:
組件能夠嵌套使用,即在一個組件中使用另外一個組件。渲染時將內部的組件表現爲外部組件的一部分:
type Greeting struct { app.Compo } func (g *Greeting) Render() app.UI { return app.P().Body( app.Text("Hello, "), &Name{name: "dj"}, ) } type Name struct { app.Compo name string } func (n *Name) Render() app.UI { return app.Text(n.name) } func main() { app.Route("/", &Greeting{}) app.Run() }
上面代碼在組件Greeting
中內嵌了一個Name
組件,運行顯示:
go-app
提供了組件的 3 個生命週期的鉤子函數:
OnMount
:當組件插入到 DOM 時調用;OnNav
:當一個組件所在頁面被加載、刷新時調用;OnDismount
:當一個組件從頁面中移除時調用。例如:
type Foo struct { app.Compo } func (*Foo) Render() app.UI { return app.P().Body( app.Text("Hello World"), ) } func (*Foo) OnMount() { fmt.Println("component mounted") } func (*Foo) OnNav(u *url.URL) { fmt.Println("component navigated:", u) } func (*Foo) OnDismount() { fmt.Println("component dismounted") } func main() { app.Route("/", &Foo{}) app.Run() }
編譯運行,在瀏覽器中打開頁面,打開瀏覽器控制檯觀察輸出:
component mounted component navigated: http://localhost:8080/
在前面的例子中咱們已經看到了如何使用聲明式語法編寫 HTML 頁面。go-app
爲全部標準的 HTML 元素都提供了相關的類型。建立這些對象的方法名也比較好記,就是元素名的首字母大寫。如app.Div()
建立一個div
元素,app.P()
建立一個p
元素,app.H1()
建立一個h1
元素等等。在go-app
中,這些結構都是暴露出對應的接口供開發者使用的,如div
對應HTMLDiv
接口:
type HTMLDiv interface { Body(nodes ...Node) HTMLDiv Class(v string) HTMLDiv ID(v string) HTMLDiv Style(k, v string) HTMLDiv OnClick(h EventHandler) HTMLDiv OnKeyPress(h EventHandler) HTMLDiv OnMouseOver(h EventHandler) HTMLDiv }
能夠看到每一個方法都返回該HTMLDiv
自身,因此支持鏈式調用。調用這些方法能夠設置元素的各方面屬性:
Class
:添加 CSS Class;ID
:設置 ID 屬性;Style
:設置內置樣式;Body
:設置元素內容,能夠隨意嵌套。div
中包含h1
和p
,p
中包含img
等;和設置事件監聽:
OnClick
:點擊事件;OnKeyPress
:按鍵事件;OnMouseOver
:鼠標移過事件。例以下面代碼:
app.Div().Body( app.H1().Body( app.Text("Title"), ), app.P().ID("id"). Class("content").Body( app.Text("something interesting"), ), )
至關於 HTML 代碼:
<div> <h1>title</h1> <p id="id" class="content"> something interesting </p> </div>
咱們能夠在app.Raw()
中直接寫 HTML 代碼,app.Raw()
會生成對應的app.UI
返回:
svg := app.Raw(` <svg width="100" height="100"> <circle cx="50" cy="50" r="40" stroke="green" stroke-width="4" fill="yellow" /> </svg> `)
可是這種寫法是不安全的,由於沒有檢查 HTML 的結構。
咱們在最開始的例子中就已經用到了條件語句,條件語句對應 3 個方法:If()/ElseIf()/Else()
。
If
和ElseIf
接收兩個參數,第一個參數爲bool
值。若是爲true
,則顯示第二個參數(類型爲app.UI
),不然不顯示。
Else
必須在If
或ElseIf
後使用,若是前面的條件都不知足,則顯示傳入Else
方法的app.UI
:
type ScoreUI struct { app.Compo score int } func (c *ScoreUI) Render() app.UI { return app.Div().Body( app.If(c.score >= 90, app.H1(). Style("color", "green"). Body( app.Text("Good!"), ), ).ElseIf(c.score >= 60, app.H1(). Style("color", "orange"). Body( app.Text("Pass!"), ), ).Else( app.H1(). Style("color", "red"). Body( app.Text("fail!"), ), ), app.Input(). Value(c.score). Placeholder("Input your score?"). AutoFocus(true). OnChange(c.OnInputChange), ) } func (c *ScoreUI) OnInputChange(src app.Value, e app.Event) { score, _ := strconv.ParseUint(src.Get("value").String(), 10, 32) c.score = int(score) c.Update() } func main() { app.Route("/", &ScoreUI{}) app.Run() }
上面咱們根據輸入的分數顯示對應的文字,90
及以上顯示綠色的Good!
,60-90
之間顯示橙色的Pass!
,小於60
顯示紅色的Fail!
。下面是運行結果:
Range
假設咱們要編寫一個 HTML 列表,當前有一個字符串的切片。若是一個個寫就太繁瑣了,並且不夠靈活,且容易出錯。這時就可使用Range()
方法了:
type RangeUI struct { app.Compo name string } func (*RangeUI) Render() app.UI { langs := []string{"Go", "JavaScript", "Python", "C"} return app.Ul().Body( app.Range(langs).Slice(func(i int) app.UI { return app.Li().Body( app.Text(langs[i]), ) }), ) } func main() { app.Route("/", &RangeUI{}) app.Run() }
Range()
能夠對切片或map
中每一項生成一個app.UI
,而後平鋪在某個元素的Body()
方法中。
運行結果:
在go-app
中,咱們能夠很方便的自定義右鍵彈出的菜單,而且爲菜單項編寫響應:
type ContextMenuUI struct { app.Compo name string } func (c *ContextMenuUI) Render() app.UI { return app.Div().Body( app.Text("Hello, World"), ).OnContextMenu(c.OnContextMenu) } func (*ContextMenuUI) OnContextMenu(src app.Value, event app.Event) { event.PreventDefault() app.NewContextMenu( app.MenuItem(). Label("item 1"). OnClick(func(src app.Value, e app.Event) { fmt.Println("item 1 clicked") }), app.MenuItem().Separator(), app.MenuItem(). Label("item 2"). OnClick(func(src app.Value, e app.Event) { fmt.Println("item 2 clicked") }), ) } func main() { app.Route("/", &ContextMenuUI{}) app.Run() }
咱們在OnContextMenu
中調用了event.PreventDefault()
阻止默認菜單的彈出。看運行結果:
點擊菜單項,觀察控制檯輸出~
app.Handler
上面咱們都是使用go-app
內置的app.Handler
處理客戶端的請求。咱們只設置了簡單的兩個屬性Author
和Title
。app.Handler
還有其它不少字段能夠定製:
type Handler struct { Author string BackgroundColor string CacheableResources []string Description string Env Environment Icon Icon Keywords []string LoadingLabel string Name string RawHeaders []string RootDir string Scripts []string ShortName string Styles []string ThemeColor string Title string UseMinimalDefaultStyles bool Version string }
Icon
:設置應用圖標;Styles
:CSS 樣式文件;Scripts
:JS 腳本文件。CSS 和 JS 文件必須在app.Handler
中聲明。下面是一個示例app.Handler
:
h := &app.Handler{ Name: "Luck", Author: "Maxence Charriere", Description: "Lottery numbers generator.", Icon: app.Icon{ Default: "/web/icon.png", }, Keywords: []string{ "EuroMillions", "MEGA Millions", "Powerball", }, ThemeColor: "#000000", BackgroundColor: "#000000", Styles: []string{ "/web/luck.css", }, Version: "wIKiverSiON", }
本文中 WebAssembly 代碼都在各自的目錄中。Go Web 演示代碼在 web 目錄中。先進入某個目錄,使用下面的命令編譯:
$ GOARCH=wasm GOOS=js go build -o app.wasm
而後將生成的app.wasm
拷貝到web
目錄:
$ cp app.wasm ../web/
切換到 web 目錄,啓動服務器:
$ cd ../web/ $ go run main.go
本文介紹如何使用go-app
編寫基於 WebAssembly 的 Web 應用程序。可能有人會以爲,go-app
編寫 HTML 的方式有點繁瑣。可是咱們能夠寫一個轉換程序將普通的 HTML 代碼轉爲go-app
代碼,感興趣能夠本身實現一下。WebAssembly 技術很是值得關注一波~
你們若是發現好玩、好用的 Go 語言庫,歡迎到 Go 每日一庫 GitHub 上提交 issue😄
歡迎關注個人微信公衆號【GoUpUp】,共同窗習,一塊兒進步~