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】,共同窗习,一块儿进步~