分享
  1. 首页
  2. 文章

Go 每日一库之 go-app

darjun · · 4561 次点击 · · 开始浏览
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

简介

go-app是一个使用 Go + WebAssembly 技术编写渐进式 Web 应用的库。WebAssembly 是一种可以运行在现代浏览器中的新式代码。近两年来,WebAssembly 技术取得了较大的发展。我们现在已经可以使用 C/C++/Rust/Go 等高级语言编写 WebAssembly 代码。本来就来介绍go-app这个可以方便地使用 Go 语言来编写 WebAssembly 代码的库。

快速使用

go-app对 Go 语言版本有较高的要求(Go 1.14+),而且必须使用Go module。先创建一个目录并初始化Go Module(Win10 + Git Bash):

$ mkdir go-app && cd go-app
$ go mod init
复制代码

然后下载安装go-app包:

$ go get -u -v github.com/maxence-charriere/go-app/v6
复制代码

至于Go module的详细使用,去看煎鱼大佬的Go Modules 终极入门

首先,我们要编写 WebAssembly 程序:

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()会使该组件重新渲染显示。

最后将该组件挂载到路径/上。

编写 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中包含h1p,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()

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:github.com/maxence-cha...
  2. Go 每日一库 GitHub:github.com/darjun/go-d...

我的博客:darjun.github.io

欢迎关注我的微信公众号【GoUpUp】,共同学习,一起进步~


有疑问加站长微信联系(非本文作者)

本文来自:掘金

感谢作者:darjun

查看原文:Go 每日一库之 go-app

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

关注微信
4561 次点击
1 回复 | 直到 2020年04月25日 11:27:54
暂无回复
添加一条新回复 (您需要 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传

用户登录

没有账号?注册
(追記) (追記ここまで)

今日阅读排行

    加载中
(追記) (追記ここまで)

一周阅读排行

    加载中

关注我

  • 扫码关注领全套学习资料 关注微信公众号
  • 加入 QQ 群:
    • 192706294(已满)
    • 731990104(已满)
    • 798786647(已满)
    • 729884609(已满)
    • 977810755(已满)
    • 815126783(已满)
    • 812540095(已满)
    • 1006366459(已满)
    • 692541889

  • 关注微信公众号
  • 加入微信群:liuxiaoyan-s,备注入群
  • 也欢迎加入知识星球 Go粉丝们(免费)

给该专栏投稿 写篇新文章

每篇文章有总共有 5 次投稿机会

收入到我管理的专栏 新建专栏