- Notifications
You must be signed in to change notification settings - Fork570
JavaScript Tips and Gotchas
Some gotchas to not forget when interacting with #"https://github.com/gopherjs/gopherjs#goroutines">https://github.com/gopherjs/gopherjs#goroutines.
Do not use items or fields of type js.Object directly. *js.Object must always be a pointer. A struct that embeds a *js.Object must always be a pointer.
typetstruct {*js.Object// so far so goodiint`js:"i"`}varv=t{}// wrong: not a pointervarv2=&t{}// right
When creating a struct literal with a *js.Object in it, you have to initialize any other fields separately.Try it.
typeTstruct {*js.ObjectIint`js:"i"`}// You can't initialize I in the struct literal.// This doesn't work.t1:=&T{Object:js.Global.Get("Object").New(),I:10}fmt.Printf("%#v\n",t1.I)// prints 0// You have to do thist2:=&T{Object:js.Global.Get("Object").New()}t2.I=10fmt.Printf("%#v\n",t2.I)// prints 10
In structs with *js.Object and js-tagged fields, if the *js.Object is in the struct directly, it must be the first field. If it's in the struct indirectly (in an embedded struct, however deep), then a) it must be the first field in the struct it's in, and b) the struct it's in must be the first field in the outer struct. Because when GopherJS looks for the *js.Object field, it only looks at the field at index 0.
// This doesn't worktypes1struct {iint`js:"i"`*js.Object// Must be first}// This doesn't worktypes2struct {*s1// the *js.Object in s1 is not firsti2int`js:"i2"`}// This definitely doesn't worktypes3struct {i2int`js:"i2"`*s1// s1 isn't first, AND *js.Object in s1 isn't first}
In structs with *js.Object fields, the object exists in two parts. The inner *js.Object part stores the fields JavaScript will see. Those fieldsmust be tagged withjs:"fieldName". Other untagged fields will be stored in the outer object.Try it.
typeTstruct {*js.ObjectIint`js:"i"`Jint}t1:=&T{Object:js.Global.Get("Object").New(),J:20}t1.I=10fmt.Printf("%#v\n",t1)// &main.T{Object:(*js.Object)(0x1), I:10, J:20}println(t1)// Console shows something similar to// { I: 0, J: 20, Object: { i: 10 } }
Putting "non-scalar" types (like slices, arrays, and maps) in JS-structs doesn't work. Seehttps://github.com/gopherjs/gopherjs/issues/460.
Decoding JSON
- Decoding JSON into pure-Go data structures (where no struct has
*js.Objectfields orjs:"..."tags) usingencoding/jsonworks. - Decoding JSON into pure-JS data structures (where every struct has
*js.Objectfields and every field hasjs:"..."tags) usingthe native JSON parser works. - Decoding JSON into mixed Go/JS data structures (where every struct has a
*js.Objectfield but not all fields havejs:"..."tags) is problematic.encoding/jsondoes not initialize the*js.Objectfield, and if you try to writeUnmarshalJSONmethods for your types to do so, you have trouble reading embedded structs.
Goroutines not waking up from sleep when you expect. (E.g. in a small app that creates a new div with the current time in it every minute, I've seen it be late by several seconds.) In Chrome, at least, if a tab loses focus, Chrome will delay timers firing to no more than once per second. (It may have to be completely hidden or off screen, not just lose focus, for this to happen. In the example above, I was in a completely different workspace.) Timers are how GopherJS implements goroutine scheduling, includingtime.Sleep. Web workers do not have this, uh, feature. There's a small Javascript shim to replace regular timers with web worker timers, and it works with GopherJS. Seehere andhere. Just load HackTimer (from the 2nd link) before your GopherJS code (and possibly before any other JS code, if appropriate) and you're good. (Even with this shim, timers still aren't completely reliable, but they're a lot closer than before.)
Some gotchas when writing Go for transpilation:
- The
fmtlibrary is very large, so excluding it from your codebase can drastically reduce JS size.- There is a package in-progress for the purposes of avoiding
fmt:fmtless - Beware of dependencies that import fmt; in Go 1.6, this includes
encoding/json(andthis won't change) - Don't use
fmt.Errorf, useerrors.New. - Don't use
fmt.Sprintf, usestrconv. - Don't use
fmt.Println, useprintln(which gopherjs converts toconsole.log)
- There is a package in-progress for the purposes of avoiding
- For making HTTP requests, don't use net/http, as importing this will also compile-in the entire TLS stack! UseGopherJS bindings to XmlHTTPRequest instead. For code that'll be compiled either to Go or JS, use a function that's constrained by build tags, so in JS it uses XHR and in Go it uses net/http.
- The built-in
encoding/jsonlibrary can add a lot of bloat. If you only need tounmarshal JSON, usegithub.com/cathalgarvey/fmtless/encoding/json, which can save a lot of space.
Some tips for interacting with #"wrap" JavaScript objects in Go structs to access their fields more natively.Try it.
// Simulate an object you get from JavaScript, like an eventeJs:=js.Global.Get("Object").New()eJs.Set("eventSource","Window")println(eJs)// {eventSource: "Window"}// Create a type to access the event slots via gotypeEventstruct {*js.ObjectEventSourcestring`js:"eventSource"`}// "Wrap" the JS object in a Go struct, access its fields directlyeGo:=&Event{Object:eJs}fmt.Printf("%#v\n",eGo.EventSource)// "Window"
When making a call via(*js.Object).Call to non-GopherJS code, a thrown error can be caught by usingdefer andrecover.Try it.
funcparse(sstring) (res*js.Object,errerror) {deferfunc() {e:=recover()ife==nil {return}ife,ok:=e.(*js.Error);ok {err=e}else {panic(e)}}()res=js.Global.Get("JSON").Call("parse",s)returnres,err}
Some tips for shrinking the compiled output
- Use the
--minifyflag in the compiler to shrink the produced JavaScript as much as possible - Further shrinking be achieved by using GZIP compression. Modern web browsers allow for GZIP compressed JavaScript to be sent instead of JavaScript. This can be done by GZIP compressing the GopherJS output and then serving it with a wrapper around the http.ServeContent function:
// compress the GopherJS output with the command: gzip -9 -k -f frontend/js/myproject.js// creating the file myproject.js.gz//servePreCompress serves the GZIP version when the web browsers asks for the normal versionfunc servePreCompress(path, dir string) func(w http.ResponseWriter, r *http.Request) {return func(w http.ResponseWriter, r *http.Request) {if !strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") {w.WriteHeader(http.StatusNotImplemented)fmt.Fprintln(w, "Only send via gzip")return}content, err := os.Open(dir + path )if err != nil {w.WriteHeader(http.StatusInternalServerError)fmt.Fprintln(w, "cannot open file: "+err.Error())return}w.Header().Set("Content-Encoding", "gzip")http.ServeContent(w, r, path, time.Now(), content)}}...//register the handler to catch the request for the normal js file//This request it trigger by the foillowing html:// <script src="./js/myproject.js"></script> http.HandleFunc("/js/myproject.js", servePreCompress("/js/myproject.js.gz", "/var/www"))