Go 每日一庫之 go-app

簡介

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

在前面的例子中咱們已經看到了如何使用聲明式語法編寫 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中包含h1pp中包含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()

IfElseIf接收兩個參數,第一個參數爲bool值。若是爲true,則顯示第二個參數(類型爲app.UI),不然不顯示。

Else必須在IfElseIf後使用,若是前面的條件都不知足,則顯示傳入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處理客戶端的請求。咱們只設置了簡單的兩個屬性AuthorTitleapp.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😄

參考

  1. go-app GitHub:https://github.com/maxence-charriere/go-app
  2. Go 每日一庫 GitHub:https://github.com/darjun/go-daily-lib

個人博客:https://darjun.github.io

歡迎關注個人微信公衆號【GoUpUp】,共同窗習,一塊兒進步~

相關文章
相關標籤/搜索