File: README.md
text</a>
</a>Go-app is a package for building progressive web apps (PWA) with the Go programming language (Golang) and WebAssembly (Wasm).
Shaping a UI is done by using a declarative syntax that creates and compose HTML elements only by using the Go programing language.
It uses Go HTTP standard model.
An app created with go-app can out of the box runs in its own window, supports offline mode, and is SEO friendly.
Documentation (built with go-app)
Install
go-app requirements:
go mod init
go get -u github.com/maxence-charriere/go-app/v11/pkg/appDeclarative syntax
Go-app uses a declarative syntax so you can write reusable component-based UI elements just by using the Go programming language.
Here is a Hello World component that takes an input and displays its value in its title:
type hello struct {
app.Compo
name string
}
func (h *hello) Render() app.UI {
return app.Div().Body(
app.H1().Body(
app.Text("Hello, "),
app.If(h.name != "", func() app.UI {
return app.Text(h.name)
}).Else(func() app.UI {
return app.Text("World!")
}),
),
app.P().Body(
app.Input().
Type("text").
Value(h.name).
Placeholder("What is your name?").
AutoFocus(true).
OnChange(h.ValueTo(&h.name)),
),
)
}Standard HTTP
Apps created with go-app complies with Go standard HTTP package interfaces.
func main() {
// Components routing:
app.Route("/", func() app.Composer { return &hello{} })
app.Route("/hello", func() app.Composer { return &hello{} })
app.RunWhenOnBrowser()
// HTTP routing:
http.Handle("/", &app.Handler{
Name: "Hello",
})
if err := http.ListenAndServe(":8000", nil); err != nil {
log.Fatal(err)
}
}Getting started
Read the Getting Started document.
Built with go-app
Contributors
Code Contributors
This project exists thanks to all the people who contribute. [Contribute].
Financial Contributors
Become a financial contributor and help us sustain go-app development. [Contribute]
Individuals
Organizations
Support this project with your organization. Your logo will show up here with a link to your website. [Contribute]
File: docs/web/documents/actions.md
What is an Action?
An action is a custom event propagated across the app. It can be asynchronously handled in a separate goroutine or by any component.
Create
An action is created from a Context by calling NewAction(name string, tags ...Tagger):
func (h *hello) onInputChange(ctx app.Context, e app.Event) {
ctx.NewAction("greet")
}A payload can also be attached with NewActionWithValue(name string, v interface{}, tags ...Tagger):
func (h *hello) onInputChange(ctx app.Context, e app.Event) {
name := ctx.JSSrc().Get("value").String()
ctx.NewActionWithValue("greet", name)
}A bit like an HTTP header, additional info can be attached to actions by setting tags:
func (h *hello) onInputChange(ctx app.Context, e app.Event) {
name := ctx.JSSrc().Get("value").String()
ctx.NewActionWithValue("greet", name,
app.T("source", "input"),
app.T("event", "change"),
)
}Handling
Once an action is created, it is propagated across the app. It can then be handled at global and/or component levels with an ActionHandler:
type ActionHandler func(Context, Action)Global Level
Dealing with actions at a global level is done by registering an ActionHandler with the Handle function:
func main() {
app.Handle("greet", handleGreet) // Registering action handler.
app.Route("/", &hello{})
app.RunWhenOnBrowser()
// ...
}
// Action handler that is called on a separate goroutine when a "greet" action
// is created.
func handleGreet(ctx app.Context, a app.Action) {
name, ok := a.Value.(string) // Checks if a name was given.
if !ok {
fmt.Println("Hello, World")
return
}
fmt.Println("Hello,", name)
}Executed asynchronously on a separate goroutine, handling an action globally is used to centralize and separate the business logic from the UI.
Component Level
Actions can also be handled at component level by registering an ActionHandler from a Context:
func (h *hello) OnMount(ctx app.Context) {
ctx.Handle("greet", h.handleGreet) // Registering action handler.
}
func (h *hello) onChange(ctx app.Context, e app.EventHandler) {
name := ctx.JSSrc().Get("value").String()
ctx.NewActionWithValue("greet", name) // Creating "greet" action.
}
// Action handler that is called on the UI goroutine when a "greet" action is
// created.
func (h *hello) handleGreet(ctx app.Context, a app.Action) {
name, ok := a.Value.(string) // Checks if a name was given.
if !ok {
return
}
h.name = name
}Executed on the UI goroutine, handling actions from components can help to send data from a component to another.
Next
File: docs/web/documents/architecture.md
Overview
Like traditional websites, progressive web apps are provided by a server and are displayed in a web browser. This document provide a description of the different elements which interact each other to operate a PWA.
Web browser
A web browser is where the PWA is displayed. Here is a list of well-known web browser:
When a user wants to use an app, the web browser requests an HTML pages and their associated resources to the server.
Once the required resources are gathered, it displays the app to the user.
Server
The server is what serves the files to make a go-app progressive web app work into the web browser:
It is implemented with the standard Go HTTP package and the Handler.
HTML pages
HTML pages are pages that indicate to web browsers what other resources are required to run the progressive web app:
They also contain the markup that provides a pre-rendered version of the requested page and that will be replaced by the app dynamic content once app.wasm is loaded.
Package resources
Package resources are the mandatory resources to run a go-app progressive web app into web browsers. Those resources are:
| Package resource | Description |
|---|---|
| wasm_exec.js | Script to interop Go and Javascrip APIs. |
| app.js | Script that loads app.wasm and go-app service workers. |
| app-worker.js | Script that implements go-app required service workers. |
| manifest.webmanifest | Manifest that describes the progressive web app. |
| app.css | go-app widgets styles. |
They are served by the server's go-app Handler and are accessible from the root of the app domain. Eg: /app.js.
app.wasm
app.wasm is the binary that contains the UI logic of the progressive web app. It is the app code, built to run on wasm architecture.
GOARCH=wasm GOOS=js go build -o web/app.wasmIt is a static resource that is always located at /web/app.wasm, and can be served by the server or is available from a remote bucket such as S3 or Google Cloud Storage.
Once loaded in a web browser, it displays the app content and handles user interactions.
Static resources
Static resource are files such as:
- CSS files
- JS files
- Images
- Videos
- Sounds
- Documents
They are always located within a directory named web, and can be served by the server or are available from a remote bucket such as S3 or Google Cloud Storage.
Static resources are located in a single directory referred to as the web directory:
/web/RESOURCE_NAMEBy default served by the server, the web directory can also be located on a remote bucket such as S3 or Google Cloud Storage.
Next
File: docs/web/documents/built-with.md
Built with go-app
Here is a list of progressive web app built with go-app.
Lofimusic.app
App to listen Lo-fi radios.
Murlok.io
World of Warcraft class guides app.
Astextract
Tool to converts Go code into its go/ast representation.
Liwasc
An app to list, wake and scan nodes in a network.
Next
File: docs/web/documents/components.md
What is a component?
A component is a customizable, independent, and reusable UI element that allows your UI to be split into independent and reusable pieces.
Create
Creating a component is done by embedding Compo into a struct:
type hello struct {
app.Compo
}Customize Look
Customizing a component look is done by implementing the Render() method.
func (h *hello) Render() app.UI {
return app.H1().Text("Hello World!")
}The code above displays an H1 HTML element that shows the Hello World! text.
Render() returns a UI element that can be either an HTML element or another component. Refer to the Declarative Syntax topic to know more about how to customize a component look.
Fields
Fields are struct fields that store data that can be used to customize a component when rendering. The example below shows a component that displays a name stored in a component field:
type hello struct {
app.Compo
Name string // Exported field.
}
func (h *hello) Render() app.UI {
return app.Div().Text("Hello, " + h.Name) // The Name field is display after "Hello, "
}Exported vs Unexported
In addition to the Go distinction between exported and unexported fields, go-app uses that distinction to define whether a component needs to be updated.
When a UI element update is triggered (done internally), a UI element tree is rendered and compared to the currently displayed one. When 2 child components of the same type are compared to check differences, the comparison is based on the value of exported fields.
Here is a pseudo-Go code that illustrates how it works internally:
type hello struct {
app.Compo
ExportedName string
unexportedName string
}
func updateFromExportedField() {
current := &hello{
ExportedName: "Max",
unexportedName: "Eric",
}
new := &hello{
ExportedName: "Maxence",
unexportedName: "Erin",
}
update(app.Div().Body(current), app.Div().Body(new))
// Current component exported field is updated:
fmt.Println("current exported name:" + current.ExportedName) // Updated: "Maxence"
fmt.Println("current unexported name:" + current.unexportedName) // Not Updated: "Eric"
}
func updateFromUnexportedField() {
current := &hello{
ExportedName: "Max",
unexportedName: "Eric",
}
new := &hello{
ExportedName: "Max",
unexportedName: "Erin",
}
update(app.Div().Body(current), app.Div().Body(new))
// Current component is not updated (no different exported field value):
fmt.Println("current exported name:" + current.ExportedName) // Not Updated: "Max"
fmt.Println("current unexported name:" + current.unexportedName) // Not Updated: "Eric"
}Child components are updated only when there is diff with their exported fields, and only exported field are updated.
How chose between exported and unexported?
| Component field type | Triggers update | Value change | Usecase |
|---|---|---|---|
| Exported field | Yes | Yes | Component attribute |
| Unexported field | No | No | Component state |
Lifecycle Events
During its life, a component goes through several steps where actions can be performed to initialize or release data and resources.
It is possible to trigger instructions when those different steps happen by implementing the corresponding interfaces in the component.
Prerender
A component is prerendered when it is used on the server-side to generate HTML markup that is included in a requested HTML page, allowing search engines to index contents created with go-app.
Custom actions can be performed by implementing the PreRenderer interface:
type foo struct {
app.Compo
}
func (f *foo) OnPreRender(ctx app.Context) {
fmt.Println("component prerendered")
}Mount
A component is mounted when it is inserted into the webpage DOM.
Custom actions can be performed by implementing the Mounter interface:
type foo struct {
app.Compo
}
func (f *foo) OnMount(ctx app.Context) {
fmt.Println("component mounted")
}Nav
A component is navigated when a page is loaded, reloaded, or navigated from an anchor link or an HREF change. It can occur multiple times during a component life.
Custom actions can be performed by implementing the Navigator interface:
type foo struct {
app.Compo
}
func (f *foo) OnNav(ctx app.Context) {
fmt.Println("component navigated:", u)
}Dismount
A component is dismounted when it is removed from the webpage DOM.
Custom actions can be performed by implementing the Dismounter interface:
type foo struct {
app.Compo
}
func (f *foo) OnDismount() {
fmt.Println("component dismounted")
}Reference
Here is a list of all the component lifecycle events available:
| Interface | Description | Frequency |
|---|---|---|
| PreRenderer | Listen to component prerendering. | Once on server-side |
| Mounter | Listen to component mounting. | Once on client-side |
| Dismounter | Listen to component dismounting. | Once |
| Navigator | Listen to page navigation. | Once |
| Updater | Listen to component update triggered by a parent element. | Can occur multiple times |
| AppUpdater | Listen to available app update. | Can occur once |
| AppInstaller | Listen to whether an app is installable. | Can occur once |
| Resizer | Listen to the app and parent components resizes. | Each time a component is resized |
Updates
Components are meant to be responsive to different events, modifying their appearance when they occur.
When this is happening, go-app internally starts an update mechanism that checks modifications in the currently displayed UI element tree and, performs the necessary modifications to achieve the desired state.
This update mechanism is automatically trigerred when the following scenario occurs:
Manually Trigger an Update
In the event where it is not automatically triggered with your use case, the component update mechanism can be manually launched by using Compo.Update.
type myCompo struct {
app.Compo
Number int
}
func (c *myCompo) Render() app.UI {
return app.Div().Text(c.Number)
}
func (c *myCompo) customTrigger() {
c.Number = rand.Intn(42)
c.Update() // Manual updated trigger
}Next
File: docs/web/documents/components-update.md
Components update
The component now displays the username in its title and provides input for the user to type his/her name. When the user does so, an event handler is called and the name is stored in the component field named name.
The Update() method call is what tells the component that its state changed and that its appearance must be updated.
It internally triggers the Render() method and performs a diff with the current component state in order to define and process the changes. Here is how rendering diff behave:
| Diff | Modification |
|---|---|
| Different types of nodes (Text, HTML element or Component) | Current node is replaced |
| Different texts | Current node text value is updated |
| Different HTML elements | Current node is replaced |
| Different HTML element attributes | Current node attributes are updated |
| Different HTML element event handlers | Current node event handlers are updated |
| Different component types | Current node is replaced |
| Different exported fields on a same component type | Current component fields are updated |
| Different non-exported fields on a same component type | No modifications |
| Extra node in the new the tree | Node added to the current tree |
| Missing node in the new tree | Extra node is the current tree is removed |
File: docs/web/documents/concurrency.md
Intro
In go-app, every event and user interaction are handled on a single goroutine. Because some scenarios can have a long execution time, like performing an HTTP request, there is chances that the UI feels slow or unresponsive.
This document describes how it works and what tools go-app provides to solve this problem.
UI goroutine
The UI goroutine is the app's main goroutine. Under the hood, it is an event loop where each event is executed synchronously.
Here are the events that are always executed on the UI goroutine:
- Component lifecycle events (OnMount, OnNav, OnDismount)
- Component updates
- HTML element event handlers
- Dispatch() calls
- Defer() calls
Async
func (ctx Context) Async(fn func())Async() is a Context method that executes a given function on a new goroutine. It is usually used to perform long or blocking operations.
Here is an example where an HTTP request is performed when a page is loaded.
type foo struct {
app.Compo
}
func (f *foo) OnNav(ctx app.Context) {
// Launching a new goroutine:
ctx.Async(func() {
r, err := http.Get("/bar")
if err != nil {
app.Log(err)
return
}
defer r.Body.Close()
b, err := ioutil.ReadAll(r.Body)
if err != nil {
app.Log(err)
return
}
app.Logf("request response: %s", b)
})
}The difference with manually launching a goroutine is that go-app has no insights about when a manually launched goroutine ceases its execution. It's not a problem on the client-side but when prerendering on the server-side, go-app has to wait for all launched goroutines to finish their jobs in order to properly generate HTML markup. Therefore, manually launching a goroutine for UI-related purposes introduces reliability issues on the server-side.
Prefer the use of Async() rather than manually launching a goroutine when dealing with UI.
Dispatch
func (ctx Context) Dispatch(fn func(Context))Dispatch() is a Context method that executes a given function on the UI goroutine. It is used to update the UI after an Async() call, in order to avoid concurrent calls when updating a component field.
Here is an example where an HTTP request is performed when a page is loaded, and its result is stored in a component field:
type foo struct {
app.Compo
response []byte
}
func (f *foo) OnNav(ctx app.Context) {
// Launching a new goroutine:
ctx.Async(func() {
r, err := http.Get("/bar")
if err != nil {
app.Log(err)
return
}
defer r.Body.Close()
b, err := ioutil.ReadAll(r.Body)
if err != nil {
app.Log(err)
return
}
// Storing HTTP response in component field:
ctx.Dispatch(func(ctx app.Context) {
f.response = b
})
})
}Always modifiy component fields on the UI goroutine!
Defer
func (ctx Context) Defer(fn func(Context))Defer() is a Compo method that like Dispatch(), executes a given function on the UI goroutine. The difference is that the given function is executed after a component has its UI updated.
Here is an example where an HTTP request is performed when a page is loaded and its result printed on the UI goroutine after the component UI gets updated:
type foo struct {
app.Compo
response []byte
}
func (f *foo) OnNav(ctx app.Context) {
// Launching a new goroutine:
ctx.Async(func() {
r, err := http.Get("/bar")
if err != nil {
app.Log(err)
return
}
defer r.Body.Close()
b, err := ioutil.ReadAll(r.Body)
if err != nil {
app.Log(err)
return
}
// Storing HTTP response in component field from UI goroutine and
// automatically trigger component update:
ctx.Dispatch(func(ctx app.Context) {
f.response = b
})
// Printing response from UI goroutine after component UI is updated:
f.Defer(func(app.Context) {
app.Log(string(b))
})
})
}Next
File: docs/web/documents/declarative-syntax.md
Intro
The go-app declarative syntax is to customize components' look.
It uses a chaining mechanism made from the Go programming language syntax that allows composing HTML elements and components in order to craft beautiful and usable UIs.
Here is an example where HTML elements are used to display a title and a paragraph:
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 Elements
Go-app provides interfaces for each standard HTML element. Those interfaces describe setters for attributes and event handlers.
Here is a simplified version of the interface for a [](/reference#HTMLDiv):
type HTMLDiv interface {
// Attributes:
Body(children ...UI) HTMLDiv
Class(v string) HTMLDiv
ID(v string) HTMLDiv
Style(k, v string) HTMLDiv
// Event handlers:
OnClick(h EventHandler, scope ...interface{}) HTMLDiv
OnKeyPress(h EventHandler, scope ...interface{}) HTMLDiv
OnMouseOver(h EventHandler, scope ...interface{}) HTMLDiv
}Create
An HTML element is created by calling a function named after its name. The example below shows how to create a [](/reference#Div):
func (c *myCompo) Render() app.UI {
return app.Div()
}Standard Elements
A standard HTML element is an element that can contain other UI elements. Other HTML elements, texts, and components are nested by using the Body() method:
func (c *myCompo) Render() app.UI {
return app.Div().Body( // Div Container
app.H1().Text("Title"), // First child
app.P().Text("Content"), // Second child
)
}Self Closing Elements
A self-closing element is an HTML element that cannot contain other UI elements.
func (c *myCompo) Render() app.UI {
return app.Img().Src("/myImage.png")
}Attributes
HTML element interfaces provide methods to set element attributes. Here is an example that set a ``'s class:
func (c *myCompo) Render() app.UI {
return app.Div().Class("my-class")
}Multiple attributes are set by using the chaining mechanism:
func (c *myCompo) Render() app.UI {
return app.Div().
ID("id-name").
Class("class-1").
Class("class-2")
}Style
Style is an attribute that sets the element style with CSS.
func (c *myCompo) Render() app.UI {
return app.Div().Style("width", "400px")
}Like the Class() attribute, multiple styles are set by using the chaining mechanism:
func (c *myCompo) Render() app.UI {
return app.Div().
Style("width", "400px").
Style("height", "200px").
Style("background-color", "deepskyblue")
}Event handlers
Event handlers are functions that are called when an HTML event occurs. They must have the following signature:
func(ctx app.Context, e app.Event)Like attributes, HTML element interfaces provide methods to associate an event to a given handler:
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")
}The Context argument embeds several go-app tools that help in creating responsive UIs. Usable with any function accepting a Go standard context, it is canceled when the source of the event is dismounted. The source element value can be retrieved with the JSSrc field:
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")
}ctx.JSSrc and Event are JavaScript objects wrapped in Go interfaces.
Text
Text() represents simple HTML text. Here is an example that display a Hello World text:
func (c *myCompo) Render() app.UI {
return app.Div().Body( // Container
app.Text("Hello"), // First text
app.Text("World"), // Second text
)
}When an HTML element embeds a single text element, HTML element's Text() method can be used instead:
func (c *myCompo) Render() app.UI {
return app.Div().Text("Hello World")
}Raw elements
Raw elements are elements representing plain HTML code. Be aware that using them is unsafe since there is no check on HTML format.
Here is an example that creates a <svg> element.
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>
`)
}Nested Components
Components are structs that let you split the UI into independent and reusable pieces. They can be used within other components to achieve more complex UIs.
Here is an example where a component named foo embeds a text and another component named bar.
// 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!")
}Condition
A Condition is a construct that selects the UI elements that satisfy a condition. They are created by calling the If() function.
If
Here is an If example that shows a title only when the showTitle value is true:
type myCompo struct {
app.Compo
showTitle bool
}
func (c *myCompo) Render() app.UI {
return app.Div().Body(
app.If(c.showTitle, func() app.UI {
return app.H1().Text("hello")
}),
)
}ElseIf
Here is an ElseIf example that shows a title in different colors depending on an int value:
type myCompo struct {
app.Compo
color int
}
func (c *myCompo) Render() app.UI {
return app.Div().Body(
app.If(c.color > 7, func() app.UI {
return app.H1().
Style("color", "green").
Text("Good!")
}).ElseIf(c.color < 4, func() app.UI {
return app.H1().
Style("color", "red").
Text("Bad!")
}).Else(func() app.UI {
return app.H1().
Style("color", "orange").
Text("So so!")
}),
)
}Else
Here is an Else example that shows a simple text when the showTitle value is false:
type myCompo struct {
app.Compo
showTitle bool
}
func (c *myCompo) Render() app.UI {
return app.Div().Body(
app.If(c.showTitle, func() app.UI {
return app.H1().Text("hello")
}).Else(func() app.UI {
return app.Text("world") // Shown when showTitle == false
}),
)
}Range
Range represents a range loop that shows UI elements generated from a slice or map. They are created by calling the Range() function.
Slice
Here is a slice example that shows an unordered list from a []string:
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])
}),
)
}Map
Here is a map example that shows an unordered list from a map[string]int:
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)
}),
)
}Form helpers
Form helpers are component methods that help to map HTML form element values to component fields.
ValueTo
ValueTo maps, when it exists, an HTML element value property to a given variable.
Here is a Hello component version that uses the ValueTo() method to get the username from its input rather than defining an event handler:
type hello struct {
app.Compo
name string
}
func (h *hello) Render() app.UI {
return app.Div().Body(
app.H1().Text("Hello " + h.name),
app.P().Body(
app.Input().
Type("text").
Value(h.name).
Placeholder("What is your name?").
AutoFocus(true).
// Here the username is directly mapped from the input's change
// event.
OnChange(h.ValueTo(&h.name)),
),
)
}Next
File: docs/web/documents/getting-started.md
Intro
go-app is a package to build progressive web apps (PWA) with the Go programming language and WebAssembly.
You are about to learn how to get started with this package by building and running an app that displays Hello World.
Prerequisite
Using this package requires a fully functional Go environment installed with Go 1.18 or greater.
Go installation can be checked with the following command in a terminal:
go versionInstall
Create a Go package for your PWA and change directory to the newly created location:
mkdir -p $GOPATH/src/github.com/YOUR_GITHUB_ID/hello
cd $GOPATH/src/github.com/YOUR_GITHUB_ID/helloThen Initialize the go module and download the go-app package.
go mod init
go get -u github.com/maxence-charriere/go-app/v11/pkg/appCode
Here is the code used to create a progressive web app that displays a simple Hello World.
Hello component
package main
import (
"log"
"net/http"
"github.com/maxence-charriere/go-app/v11/pkg/app"
)
// hello is a component that displays a simple "Hello World!". A component is a
// customizable, independent, and reusable UI element. It is created by
// embedding app.Compo into a struct.
type hello struct {
app.Compo
}
// The Render method is where the component appearance is defined. Here, a
// "Hello World!" is displayed as a heading.
func (h *hello) Render() app.UI {
return app.H1().Text("Hello World!")
}Main
// The main function is the entry point where the app is configured and started.
// It is executed in 2 different environments: A client (the web browser) and a
// server.
func main() {
// The first thing to do is to associate the hello component with a path.
//
// This is done by calling the Route() function, which tells go-app what
// component to display for a given path, on both client and server-side.
app.Route("/", func() app.Composer { return &hello{} })
// Once the routes set up, the next thing to do is to either launch the app
// or the server that serves the app.
//
// When executed on the client-side, the RunWhenOnBrowser() function
// launches the app, starting a loop that listens for app events and
// executes client instructions. Since it is a blocking call, the code below
// it will never be executed.
//
// When executed on the server-side, RunWhenOnBrowser() does nothing, which
// lets room for server implementation without the need for precompiling
// instructions.
app.RunWhenOnBrowser()
// Finally, launching the server that serves the app is done by using the Go
// standard HTTP package.
//
// The Handler is an HTTP handler that serves the client and all its
// required resources to make it work into a web browser. Here it is
// configured to handle requests with a path that starts with "/".
http.Handle("/", &app.Handler{
Name: "Hello",
})
if err := http.ListenAndServe(":8000", nil); err != nil {
log.Fatal(err)
}
}Build and run
Running a progressive app with go-app requires 2 Go programs:
- A client that runs in a web browser
- A server that serves the client and its resources
At this point, the package has the following content:
.
โโโ go.mod
โโโ go.sum
โโโ main.go
0 directories, 4 filesBuild the client
GOARCH=wasm GOOS=js go build -o web/app.wasmNote that the build output is explicitly set to web/app.wasm. The reason why is that the Handler expects the client to be a static resource located at the /web/app.wasm path.
Build the server
go buildRun the app
Now the client and server built, the package has the following content:
.
โโโ go.mod
โโโ go.sum
โโโ hello
โโโ main.go
โโโ web
โโโ app.wasm
1 directory, 6 filesThe server is ran with the following command:
./helloThe app is now accessible from a web browser at http://localhost:8000.
Use a Makefile
The build process can be simplified by writing a makefile:
build:
GOARCH=wasm GOOS=js go build -o web/app.wasm
go build
run: build
./helloIt can now be built and run with this single command:
make runNext
File: docs/web/documents/github-deploy.md
Intro
GitHub Pages is a pretty easy way to have a running and available website. The limitation is that only static websites can be hosted.
PWAs created with go-app are usually served by a standard Go HTTP server. That said, it is possible to generate them as completely static websites in order to have them running from hosts like GitHub Pages.
Generate a Static Website
A static website can be generated with the help of the GenerateStaticWebsite() function:
func main() {
app.Route("/", func() app.Composer { return &hello{} })
app.Route("/hello", func() app.Composer { return &hello{} })
app.RunWhenOnBrowser()
err := app.GenerateStaticWebsite(".", &app.Handler{
Name: "Hello",
})
if err != nil {
log.Fatal(err)
}
}It can then be generated by building and running the go executable:
# Build app.wasm:
GOARCH=wasm GOOS=js go build -o /test-app/web/app.wasm
# Build and generate static website:
go build
./helloThe current directory will then have all the required files to run the PWA as a static website:
. # Current dir.
โโโ app-worker.js # Service-worker file (Generated).
โโโ app.js # Js support file (Generated).
โโโ index.html # Index page (Generated from "/").
โโโ hello.html # hello page (Generated from "/hello").
โโโ manifest.webmanifest # PWA manifest (Generated).
โโโ wasm_exec.js # Wasm support file (Generated).
โโโ web # Web directory.
โโโ app.wasm # Wasm app (Manually built).Deployment
Once generated, the static website can directly be dropped in a GitHub repository, either in the root or in the docs directory depending on how GitHub Pages is configured.
Domainless Repository
By default, no domain is associated with a GitHub repository. The website is accessed from a GitHub address that looks like this:
https://USERNAME.github.io/REPOSITORY_NAMEIn that scenario, the Handler resource provider must be changed to be a GitHubPages provider:
func main() {
app.Route("/", func() app.Composer { return &hello{} })
app.Route("/hello", func() app.Composer { return &hello{} })
app.RunWhenOnBrowser()
err := app.GenerateStaticWebsite(".", &app.Handler{
Name: "Hello",
Resources: app.GitHubPages("REPOSITORY_NAME"),
})
if err != nil {
log.Fatal(err)
}
}This will fix static resources issues caused by GitHub adding the repository name as an URL path prefix.