Vugu takes a number of steps to make the development process easy to rapidly get started and prototype user interfaces. That said, it's important to understand the overall program structure and how a Vugu program works (at least by default), so you can customize it to your needs when building a sophisticated application. The basics are fairly simple.
When you use the HTTP handler in development mode (as shown on the Getting Started page), upon page load Vugu will convert your .vugu files into .go and also by default attempt to generate a main_wasm.go if it does not exist. (You can edit main_wasm.go as needed. It is only a template to get your started. Vugu will not overwrite it.)
WebAssembly main()
Like any Go program, it starts with main()
. The build constraint at the top (see
Dual-Build below) indicates this is the WebAssembly entry point:
// +build wasm package main import ( // ... "github.com/vugu/vugu" ) func main() {
Root Component
To render a page to HTML, you need to have "root" component. This is the top level component that houses everything else. By default this component lives in root.vugu, gets code generated to root_vgen.go, which has a Root struct type. The generated main_wasm.go file creates an instance of this Root struct and uses it as the beginning of the tree of components to be rendered.
BuildEnv
BuildEnv keeps track of component state across render cycles. Component re-use from render to render and detecting which components are changed are handled here. You can get a new instance by calling vugu.NewBuildEnv.
JSRenderer
Once we have a root component instance, we need an environment. There are two environments currently implemented: the DOM renderer for use in WebAssembly applications, and the static renderer which can be used for server-side rendering and tests. domrender.JSRenderer is what performs the syncing of the virtual DOM from our root component and any nested components to the browser DOM.
Render Loop
The render loop is where the magic happens. Your components' virtual DOM output (see BuildOut) is generated and then synchronized with the browser's DOM to give you a matching HTML page. This involves various optimizations including keeping track of which components have changed and caching those that haven't.
for ok := true; ok; ok = renderer.EventWait() { buildResults := buildEnv.RunBuild(rootBuilder) err = renderer.Render(buildResults) if err != nil { panic(err) } }
This will call RunBuild
and then Render
immediately the first time and then wait for
renderer.EventWait()
to return and render again.
Important Note
When
DOM Events
are handled, a (write) lock is acquired against the environment automatically
and then released when your event handler returns. When things that would block (like fetching
data from the server over HTTP) need to be done, this must be run in a goroutine which uses
event.EventEnv()
to acquire their own lock before modifying any component data, to ensure they don't interfere with the render loop or other code.
Locking should only be done during data modification and then unlocked immediately afterward. Do not put a
Lock()
before http.Get()
or other such
blocking calls. Instead Lock()
after
you have your data and before updating
the state of your component.
EventEnv.UnlockRender()
will cause the renderer.EventWait()
call above to return and update the page.
(Whereas EventEnv.UnlockOnly()
will release the lock but not cause the page update. This is useful if
you need to do several updates to data at different times
but only care to refresh the page after they are all done.)
See Code for a correct example.
EventWait
will return false if it detects something wrong with the environment
and the program should exit.
This should release any resources and be a clean exit from the program when the page goes away.
main_wasm.go
If we take a look at the main_wasm.go file that is generated by default, it gives us a pretty good idea of how these fit together:
// +build wasm package main import ( "log" "fmt" "flag" "github.com/vugu/vugu" "github.com/vugu/vugu/domrender" ) func main() { mountPoint := flag.String("mount-point", "#vugu_mount_point", "The query selector for the mount point for the root component, if it is not a full HTML component") flag.Parse() fmt.Printf("Entering main(), -mount-point=%q\n", *mountPoint) defer fmt.Printf("Exiting main()\n") rootBuilder := &Root{} buildEnv, err := vugu.NewBuildEnv() if err != nil { log.Fatal(err) } renderer, err := domrender.NewJSRenderer(*mountPoint) if err != nil { log.Fatal(err) } defer renderer.Release() for ok := true; ok; ok = renderer.EventWait() { buildResults := buildEnv.RunBuild(rootBuilder) err = renderer.Render(buildResults) if err != nil { panic(err) } } }
The Dual-Build Approach
The discussion above is only about the WebAssembly side of your application.
Using Go's build constraints it is easy to output two different executables from your same package directory. The common case is that you want a client-side build that compiles to WebAssembly (as discussed above), as well as a server-side executable to act as a web server. These each need different main() and likely other functions, but your components and other functionality should be available both in your WebAssembly output and in your server program.
This is why the main()
function for your client-side application lives in
main_wasm.go. The "_wasm" part indicates that the file should be included during
a WebAssembly build. You can and should include a server-side main()
in another file
and use // +build !wasm