Go -app是一个使用Go编程语言和WebAssembly构建渐进式web应用程序(PWA)的包。
看起来手机和电脑主流浏览器都支持(Chrome Edge Firefox Opera Safari)
安装
mkdir -p $GOPATH/src/YOUR_PACKAGE
cd $GOPATH/src/YOUR_PACKAGE
go mod init
go get -u github.com/maxence-charriere/go-app/v7/pkg/app
Hello
app.go 网页内容
package main
import "github.com/maxence-charriere/go-app/v7/pkg/app"
type hello struct {
app.Compo
}
func (h *hello) Render() app.UI {
return app.H1().Text("Hello World!")
}
func main() {
app.Route("/", &hello{})
app.Run()
}
main.go 服务端
package main
import (
"log"
"net/http"
"github.com/maxence-charriere/go-app/v7/pkg/app"
)
func main() {
http.Handle("/", &app.Handler{
Name: "Hello",
Description: "An Hello World! example",
})
err := http.ListenAndServe(":8000", nil)
if err != nil {
log.Fatal(err)
}
}
GOARCH=wasm GOOS=js go build -o web/app.wasm app.go 编译网页
go run main.go 服务端运行,即可以打开浏览器看到效果。
架构
服务器
package main
import (
"log"
"net/http"
"github.com/maxence-charriere/go-app/v7/pkg/app"
)
func main() {
http.Handle("/", &app.Handler{
Name: "Hello", // 应用名称
Title: "Hello", // 页面标题
Description: "An Hello World! example", // 页面注释
Styles: []string{
"/web/hello.css", // 包含中的css文件
},
})
// Launches the server.
err := http.ListenAndServe(":8000", nil)
if err != nil {
log.Fatal(err)
}
}
组件
创建
type hello struct {
app.Compo
}
自定义
func (h *hello) Render() app.UI {
return app.H1().Text("Hello World!")
}
更新
type hello struct {
app.Compo
Name string // Name field
}
func (h *hello) Render() app.UI {
return app.Div().Body(
app.H1().Body(
app.Text("Hello "),
app.Text(h.Name),
),
app.Input().
Value(h.Name).
OnChange(h.OnInputChange),
)
}
func (h *hello) OnInputChange(ctx app.Context, e app.Event) {
h.Name = ctx.JSSrc.Get("value").String() // 修改名称
h.Update() // 触发Render()
}
更新机制
触发组件更新时,将调用Render()方法,并生成UI元素的新树。 然后将此新树与当前组件树进行比较,仅修改或替换不匹配的节点。
生命周期
OnMount
type foo struct {
app.Compo
}
func (f *foo) OnMount(ctx app.Context) {
fmt.Println("component mounted")
}
OnNav
当页面被加载、重新加载,或者从锚点链接或HREF更改导航时,组件就会被导航。
type foo struct {
app.Compo
}
func (f *foo) OnNav(ctx app.Context, u *url.URL) {
fmt.Println("component navigated:", u) //将在控制台输出
}
OnDismount
type foo struct {
app.Compo
}
func (f *foo) OnDismount() {
fmt.Println("component dismounted")
}
并发性
UI goroutine是应用程序的主要goroutine。 在后台,这是一个事件循环,其中每个事件都同步执行。
如果这些操作导致组件字段修改,请确保通过调用Dispatch()在UI goroutine上执行这些操作。
Dispatch是使给定功能在UI goroutine上执行的调用。
以下示例是通过网页,请求网址内容的示例
type httpCall struct {
app.Compo
response string
}
func (c *httpCall) Render() app.UI {
return app.Div().Body(
app.H1().Text("HTTP Call"),
app.H2().Text("URL:"),
app.Input().
Placeholder("Enter an URL").
OnChange(c.OnURLChange),
app.H2().Text("Response:"),
app.P().Text(c.response),
)
}
func (c *httpCall) OnURLChange(ctx app.Context, e app.Event) {
// Reseting response value:
c.response = ""
c.Update()
// Launching HTTP request:
url := ctx.JSSrc.Get("value").String()
go c.doRequest(url) // Performs blocking operation on a new goroutine.
}
func (c *httpCall) doRequest(url string) {
r, err := http.Get(url)
if err != nil {
c.updateResponse(err.Error())
return
}
defer r.Body.Close()
b, err := ioutil.ReadAll(r.Body)
if err != nil {
c.updateResponse(err.Error())
return
}
c.updateResponse(string(b))
}
func (c *httpCall) updateResponse(res string) {
app.Dispatch(func() { // Ensures response field is updated on UI goroutine.
c.response = res
c.Update()
})
}
声明式语法
链式定义
func (c *myCompo) Render() app.UI {
return app.Div().Body(
app.H1().
Class("title").
Text("Build a GUI with Go"),
app.P().
Class("text").
Text("Just because Go and this package are really awesome!"),
)
}
HTML元素
div接口
type HTMLDiv interface {
// Attributes:
Body(nodes ...Node) HTMLDiv
Class(v string) HTMLDiv
ID(v string) HTMLDiv
Style(k, v string) HTMLDiv
// Event handlers:
OnClick(h EventHandler) HTMLDiv
OnKeyPress(h EventHandler) HTMLDiv
OnMouseOver(h EventHandler) HTMLDiv
}
创建
func (c *myCompo) Render() app.UI {
return app.Div()
}
标准元素
func (c *myCompo) Render() app.UI {
return app.Div().Body( // Div Container
app.H1().Text("Title"), // First child
app.P(), Text("Content"), // Second child
)
}
自动关闭元素
自闭元素是不能包含其他元素的元素
func (c *myCompo) Render() app.UI {
return app.Img().Src("/myImage.png")
}
属性
func (c *myCompo) Render() app.UI {
return app.Div().
ID("id-name").
Class("class-name")
}
样式
func (c *myCompo) Render() app.UI {
return app.Div().Style("width", "400px")
}
func (c *myCompo) Render() app.UI {
return app.Div().
Style("width", "400px").
Style("height", "200px").
Style("background-color", "deepskyblue")
}
事件句柄
func(ctx app.Context, e app.Event)
func (c *myCompo) Render() app.UI {
return app.Div().OnClick(c.onClick)
}
func (c *myCompo) onClick(ctx app.Context, e app.Event) {
fmt.Println("onClick is called")
}
func (c *myCompo) Render() app.UI {
return app.Div().OnChange(c.onChange)
}
func (c *myCompo) onChange(ctx app.Context, e app.Event) {
v := ctx.JSSrc().Get("value")
}
jssrc()和Event是封装在Go接口中的JavaScript对象。
文本
func (c *myCompo) Render() app.UI {
return app.Div().Body( // Container
app.Text("Hello"), // First text
app.Text("World"), // Second text
)
}
原始元素
func (c *myCompo) Render() app.UI {
return app.Raw(`
<svg width="100" height="100">
<circle cx="50" cy="50" r="40" stroke="green" stroke-width="4" fill="yellow" />
</svg>
`)
}
嵌套组件
// foo component:
type foo struct {
app.Compo
}
func (f *foo) Render() app.UI {
return app.P().Body(
app.Text("Foo, "), // Simple HTML text
&bar{}, // Nested bar component
)
}
// bar component:
type bar struct {
app.Compo
}
func (b *bar) Render() app.UI {
return app.Text("Bar!")
}
条件
If
type myCompo struct {
app.Compo
showTitle bool
}
func (c *myCompo) Render() app.UI {
return app.Div().Body(
app.If(c.showTitle,
app.H1().Text("hello"),
),
)
}
ElseIf
type myCompo struct {
app.Compo
color int
}
func (c *myCompo) Render() app.UI {
return app.Div().Body(
app.If(c.color > 7,
app.H1().
Style("color", "green").
Text("Good!"),
).ElseIf(c.color < 4,
app.H1().
Style("color", "red").
Text("Bad!"),
).Else(
app.H1().
Style("color", "orange").
Text("So so!"),
),
)
}
ELSE
type myCompo struct {
app.Compo
showTitle bool
}
func (c *myCompo) Render() app.UI {
return app.Div().Body(
app.If(c.showTitle,
app.H1().Text("hello"),
).Else(
app.Text("world"), // Shown when showTitle == false
),
)
}
切片
func (c *myCompo) Render() app.UI {
data := []string{
"hello",
"go-app",
"is",
"sexy",
}
return app.Ul().Body(
app.Range(data).Slice(func(i int) app.UI {
return app.Li().Text(data[i])
}),
)
}
字典
func (c *myCompo) Render() app.UI {
data := map[string]int{
"Go": 10,
"JavaScript": 4,
"Python": 6,
"C": 8,
}
return app.Ul().Body(
app.Range(data).Map(func(k string) app.UI {
s := fmt.Sprintf("%s: %v/10", k, data[k])
return app.Li().Text(s)
}),
)
}
Javascript和DOM
由于WebAssembly是基于浏览器的技术,因此某些情况下可能需要DOM访问和JavaScript调用。
包括JS文件
处理函数
handler := &app.Handler{
Name: "My App",
Scripts: []string{
"/web/myscript.js",
"https://foo.com/remoteScript.js",
},
}
或者使用原代码的方式
handler := &app.Handler{
Name: "My App",
RawHeaders: []string{
`<!-- Global site tag (gtag.js) - Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=UA-xxxxxxx-x"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'UA-xxxxxx-x');
</script>
`,
},
}
内联
通过使用Script元素,Javascript文件也可以包含在组件中。
下面是一个异步加载Youtube Iframe API脚本的例子。
type youtubePlayer struct {
app.Compo
}
func (p *youtubePlayer) Render() app.UI {
return app.Div().Body(
app.Script().
Src("//www.youtube.com/iframe_api").
Async(true),
app.IFrame().
ID("youtube-player").
Allow("autoplay").
Allow("accelerometer").
Allow("encrypted-media").
Allow("picture-in-picture").
Sandbox("allow-presentation allow-same-origin allow-scripts allow-popups").
Src("https://www.youtube.com/embed/LqeRF_0DDCg"),
)
}
窗口
app.Window()
Window()返回一个全局javascript对象,代表一个浏览器窗口,可以用来调用带有Window和空命名空间的函数。
通过ID获取元素
elem := app.Window().GetElementByID(“YOUR_ID”)
等价于:
elem := app.Window().
Get("document").
Call("getElementById","YOUR_ID")
创建JS对象
通过从Window获取其名称并调用New()函数,可以完成从库中创建对象的过程。
// JS内容:
let player = new YT.Player("player", {
height: "390",
width: "640",
videoId: "M7lc1UVf-VE",
});
// Go版本:
player := app.Window().
Get("YT").
Get("Player").
New("player", map[string]interface{}{
"height": 390,
"width": 640,
"videoId": "M7lc1UVf-VE",
})
取消事件
在实现事件处理程序时,可以通过调用PreventDefault()取消事件。
type foo struct {
app.Compo
}
func (f *foo) Render() app.UI {
return app.Div().
OnChange(f.onContextMenu).
Text("Don't copy me!")
}
func (f *foo) onContextMenu(ctx app.Context, e app.Event) {
e.PreventDefault()
}
获得输入值
type foo struct {
app.Compo
}
func (f *foo) Render() app.UI {
return app.Input().OnChange(f.onInputChange)
}
func (f *foo) onInputChange(ctx app.Context, e app.Event) {
v := ctx.JSSrc.Get("value").String()
}
生命周期
路由
第一次导航时,该应用程序已加载到浏览器中。 然后,每次请求页面时,都会拦截导航事件,然后go-app路由系统读取URL路径并显示相应的组件。
定义路线
简单路由
func main() {
app.Route("/", &hello{}) // hello component is associated with default path "/".
app.Route("/foo", &foo{}) // foo component is associated with "/foo".
app.Run() // Launches the app in the web browser.
}
正则表达式路由
func main() {
app.RouteWithRegexp("^/bar.*", &bar) // bar component is associated with all paths that start with /bar.
app.Run() // Launches the app in the web browser.
}
静态资源
访问静态资源
无论静态资源位于本地还是远程位置,静态资源总是位于称为web目录的单个目录中
在处理程序中设置
http.Handle("/", &app.Handler{
Name: "Hello",
Description: "An Hello World! example",
Icon: app.Icon{
Default: "/web/logo.png", // Specify default favicon.
AppleTouch: "/web/logo-apple.png", // Specify icon on IOS devices.
},
Styles: []string{
"/web/hello.css", // Loads hello.css file.
},
Scripts: []string{
"/web/hello.js", // Loads hello.js file.
},
})
在组件中
func (f *foo) Render() app.UI {
return app.Img().
Alt("An image").
Src("/web/foo.png") // Specify image source to foo.png.
}
在CSS文件中
.bg {
background-image: url("/web/bg.jpg");
}
设置本地Web目录
http.Handle("/", &app.Handler{
Name: "Hello",
Description: "An Hello World! example",
Resources: app.LocalDir("/tmp/web"),
})
设置远程web目录
http.Handle("/", &app.Handler{
Name: "Hello",
Description: "An Hello World! example",
Resources: app.RemoteBucket("https://storage.googleapis.com/myapp.appspot.com"),
})
完全静态的应用
使用这个包构建的应用程序可以生成为一个完全静态的网站。它对于部署在诸如GitHub页面这样的平台上是很有用的。静态网站文件是用GenerateStaticWebsite()函数生成的
func
main() {
err := app.GenerateStaticWebsite("/test-app", &app.Handler{
Name: "Hello",
Description: "An Hello World! example",
})
if err != nil {
log.Fatal(err)
}
}
在应用载入时,会显示一个图标,看起来不爽,研究一下换成自己的Logo。
发现它藏在go-app/pkg/app/http.go文件中。Handler有Icon属性可以用来修改载入图标及标题图标。
func main() {
myIcon := app.Icon{
Default: "/web/favicon.ico",
}
http.Handle("/", &app.Handler{
Name: "Hello",
Title: "Hello 测试",
Icon: myIcon,
Description: "An Hello World! example",
Styles: []string{
"/web/hello.css", // 包含中的css文件
},
})
err := http.ListenAndServe(":8000", nil)
if err != nil {
log.Fatal(err)
}
}
啃啃原码,看还有些什么可以挖掘的。
type Handler struct {
Author string // 网页中: <meta name="author" content="">
BackgroundColor string // 应用程序页在加载其样式表之前要显示的占位符背景色 DEFAULT: #2d2c2c.
CacheableResources []string // 浏览器为提供脱机模式而缓存的静态资源的路径。请注意,默认情况下,图标、样式和脚本已经被缓存。路径相对于根目录。
Description string // 网页中: <meta name="description" content="">
Env Environment // 传递给渐进式web应用程序的环境变量。
Icon Icon // 用于PWA、favicon、load和default not found组件的图标。
Keywords []string
LoadingLabel string // 加载页面时显示的文本。
Name string // web应用程序的名称
ProxyResources []ProxyResource // 可从自定义路径访问的静态资源。默认情况下代理的文件是/robots.txt, /sitemap.xml以及/ads.txt.
RawHeaders []string // 要在head元素中添加的其他头
// app.Handler{
// Scripts: []string{
// "/web/test.js", // Static resource
// "https://foo.com/test.js", // External resource
// },
// },
Scripts []string // 用于页面的JavaScript文件的路径或url。
ShortName string // 当没有足够的空间显示名称时,向用户显示的web应用程序的名称。
// "/web/main.css"
// Default: LocalDir("web")
Resources ResourceProvider // 提供静态资源的资源提供程序。静态资源总是从以“/web/”开头的路径访问。
// app.Handler{
// Styles: []string{
// "/web/test.css", // Static resource
// "https://foo.com/test.css", // External resource
// },
// },
Styles []string // 用于页面的CSS文件的路径或URL。
ThemeColor string // 应用程序的主题颜色。 DEFAULT: #2d2c2c.
Title string // 页面标题
Version string // 版本号。这用于更新浏览器中的PWA应用程序。在实时系统上部署时必须设置此选项,以防止重复更新。
appWasmPath string // wasm文件路径,看起来是私有的,不能设置
// etag用版本生成,当没有版本信息时,用当前时间变码后作为标识字符串
// w.Header().Set("ETag", h.etag)
// 当ETag没有被修改时,服务器将发送http.StatusNotModified。表明此次请求为条件请求,用于内容缓冲。
// 实现了当版本修改时,刷新到新内容。而没有修改时,应用缓冲的内容。
etag string
}