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  
}