Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add WebAssembly support #232

Merged
merged 38 commits into from
Jul 1, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
b52c7b4
wasm WIP
emidoots Jan 20, 2019
3acd7a8
todomvc
emidoots Jan 21, 2019
feb7f76
docs
emidoots Jan 21, 2019
f4a95d2
example/markdown: fix compilation on Go 1.12
emidoots May 13, 2019
7e8b849
go get ./...`
emidoots May 13, 2019
6cfab50
fix gopherjs building
emidoots May 13, 2019
4c67cb7
example: update README
emidoots May 13, 2019
d36b550
example/markdown: fix "go test" building
emidoots May 26, 2019
40ae324
unify DOM implementation using syscall/js
emidoots Jun 29, 2019
7d6521f
correct function names in test strings
emidoots Jun 30, 2019
e73af9c
update function name mocks + test data
emidoots Jun 30, 2019
2d119e2
fix running most tests
emidoots Jun 30, 2019
1e12297
accept RAF test change
emidoots Jun 30, 2019
9e4a667
require GopherJS version 1.12+ for syscall/js
emidoots Jun 30, 2019
44e64b4
simplify import block
emidoots Jun 30, 2019
2c18136
gofmt
emidoots Jun 30, 2019
3e0a1ed
example/todomvc: use syscall/js
emidoots Jun 30, 2019
f4cd907
travis: update for GopherJS + WebAssembly + native builds
emidoots Jan 20, 2019
d350103
gofmt
emidoots Jun 30, 2019
c024de9
example/markdown: use blackfriday fork to avoid import complexity
emidoots Jun 30, 2019
d549908
fix infinite recursive call
emidoots Jun 30, 2019
b30a410
example/todomvc: order imports
emidoots Jun 30, 2019
5ff9550
make tests non-reliant on syscall/js
emidoots Jun 30, 2019
88a31b3
remove mention of separate node types
emidoots Jun 30, 2019
1ecc946
testsuite: do not accidently consume panics
emidoots Jun 30, 2019
d951e4b
fix test suite on native architectures
emidoots Jun 30, 2019
c6aee08
gofumpt / gofumports
emidoots Jun 30, 2019
f69e0dd
fix native build / type checking
emidoots Jun 30, 2019
b1075c8
correct build tag
emidoots Jun 30, 2019
4d3a512
undo
emidoots Jul 1, 2019
e1d54f8
fix native build / type checking
emidoots Jul 1, 2019
1455159
update testdata
emidoots Jul 1, 2019
da9db7b
correctly release JS callbacks
emidoots Jul 1, 2019
c7dc7af
example: update README
emidoots Jul 1, 2019
c4a4263
doc/CHANGELOG: mention move to syscall/js
emidoots Jul 1, 2019
a0ff746
example: add hellovecty (useful for measuring minimal bundle size)
emidoots Jul 1, 2019
8fe4af3
README: modernize
emidoots Jul 1, 2019
9cf66b5
gofmt
emidoots Jul 1, 2019
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 38 additions & 16 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -1,14 +1,30 @@
sudo: false
language: go
go:
- 1.11.x
install:
- nvm install 9.11
# Manually download and install Go 1.12 because the Travis / gimme version
# is broken (see https://travis-ci.community/t/goos-js-goarch-wasm-go-run-fails-panic-newosproc-not-implemented/1651/6)
- wget -O go.tar.gz https://dl.google.com/go/go1.12.linux-amd64.tar.gz
- tar -C ~ -xzf go.tar.gz
- rm go.tar.gz
- export GOROOT=~/go
- export GOPATH=/home/travis/gopath
- export PATH=$GOROOT/bin:$PATH
- go version
- go env

# Install NodeJS 10 (GopherJS, WebAssembly)
- nvm install 10.10.0

# GopherJS and support tooling
# source-map-support for GopherJS
- cd / && go get -u github.com/gopherjs/gopherjs
- npm install -g source-map-support
- go get -u github.com/gopherjs/gopherjs
- curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | sh -s -- -b $(go env GOPATH)/bin v1.12.5
- go get -u github.com/haya14busa/goverage
- npm install --global node-gyp

# Linters, etc.
- curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | sh -s -- -b $(go env GOPATH)/bin v1.17.1
- go get -u github.com/haya14busa/goverage
- go get -u mvdan.cc/gofumpt
- go get -u mvdan.cc/gofumpt/gofumports
before_script:
- export NODE_PATH="/usr/local/lib/node_modules"

Expand All @@ -21,21 +37,27 @@ before_script:
- cd $TRAVIS_BUILD_DIR
script:
# Fetch dependencies.
- go get -t -v ./...
- GO111MODULE=on go get -v ./...
- GO111MODULE=off go get -d .
- GO111MODULE=on go get -d .
- GOOS=js GOARCH=wasm GO111MODULE=off go get -d ./...
- GOOS=js GOARCH=wasm GO111MODULE=on go get -d ./...

# Consult Go fmt to ensure consistent code style.
- diff -u <(echo -n) <(gofmt -d -s .)
# Ensure consistent code style.
- diff -u <(echo -n) <(gofumpt -d -s .)
- diff -u <(echo -n) <(gofumports -d .)

# Consult golangci-lint (multiple Go linting tools).
- golangci-lint run . ./elem/... ./event/... ./example/...
- golangci-lint run . ./elem/... ./event/...
- golangci-lint run --exclude 'exported .* should have comment .*or be unexported' ./prop/... ./style/... # https://github.com/gopherjs/vecty/issues/227
- GOOS=js GOARCH=wasm golangci-lint run --build-tags 'js wasm' . ./elem/... ./event/... ./example/...
- GOOS=js GOARCH=wasm golangci-lint run --build-tags 'js wasm' . ./elem/... ./event/...
- GOOS=js GOARCH=wasm golangci-lint run --build-tags 'js wasm' --exclude 'exported .* should have comment .*or be unexported' ./prop/... ./style/... # https://github.com/gopherjs/vecty/issues/227
- bash -c 'cd example && golangci-lint run ./markdown'
- bash -c 'cd example && GOOS=js GOARCH=wasm golangci-lint run --build-tags 'js wasm' ./...'

# Test with Go compiler and GopherJS compiler.
- go test -v -race ./...
- gopherjs test -v ./...
# Test with Go compiler (under amd64 and wasm architectures) and GopherJS compiler.
- go test -race ./...
- GO111MODULE=on GOOS=js GOARCH=wasm go test -exec="$(go env GOROOT)/misc/wasm/go_js_wasm_exec" ./...
- gopherjs test ./...

# Generate and upload coverage to codecov.io
- goverage -covermode=atomic -coverprofile=coverage.out $(go list ./... | grep -v -e vecty/elem -e vecty/event -e vecty/example -e vecty/prop -e vecty/style)
Expand Down
66 changes: 47 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,42 +1,70 @@
<p align="center">
<img src="https://github.com/vecty/vecty-logo/raw/master/horizontal_color.png" />
<img src="https://github.com/vecty/vecty-logo/raw/master/horizontal_color_tagline.png" />
</p>

Vecty is a [React](https://facebook.github.io/react/)-like library for [GopherJS](https://github.com/gopherjs/gopherjs) so that you can do frontend development in Go instead of writing JavaScript/HTML/CSS.
Vecty is a library for building responsive and dynamic web frontends in Go instead of in JavaScript, HTML & CSS. It competes with modern web frameworks like React & VueJS, and supports compilation to both WebAssembly and vanilla JavaScript.

[![Build Status](https://travis-ci.org/gopherjs/vecty.svg?branch=master)](https://travis-ci.org/gopherjs/vecty) [![GoDoc](https://godoc.org/github.com/gopherjs/vecty?status.svg)](https://godoc.org/github.com/gopherjs/vecty) [![codecov](https://img.shields.io/codecov/c/github/gopherjs/vecty/master.svg)](https://codecov.io/gh/gopherjs/vecty)

Features
Benefits
========

- Share frontend and backend code.
- Write everything in Go -- not JS/HTML/CSS!
- XSS protection: unsafe HTML must be explicitly denoted as such.
- Reusability: share components by making Go packages that others can import!
- Go developers can be competitive frontend developers.
- Share Go code between your frontend & backend.
- Reusability by sharing components via Go packages so that others can simply import them.

Goals
=====

- Simplicity
- Keep things as simple as possible to understand *for newcomers*.
- Designed from the ground up to be easily mastered (like Go)!
- Performance
- As efficient as possible, make it clear what each operation in your webpage will do.
- Same performance as just using plain JS/HTML/CSS.
- Composability
- Nest components to form your entire user interface, seperate them logically as you would any normal Go package.
- _Simple_
- Designed from the ground up to be easily mastered _by newcomers_ (like Go).
- _Performant_
- Efficient & understandable performance, small bundle sizes, same performance as raw JS/HTML/CSS.
- _Composable_
- Nest components to form your entire user interface, seperating them logically as you would any normal Go package.
- _Designed for Go (implicit)_
- Written from the ground up asking the question _"What is the best way to solve this problem in Go?"_, not simply asking _"How do we translate $POPULAR_LIBRARY to Go?"_

Features
========

- Compiles to WebAssembly (via standard Go compiler) and vanilla JavaScript (via [GopherJS](https://github.com/gopherjs/gopherjs)).
- Small bundle sizes: 0.5 KB hello world (see section below).
- Fast expectation-based browser DOM diffing ('virtual DOM', but less resource usage).

Current Status
==============

**Vecty is currently considered to be an experimental work-in-progress.**
**Vecty is currently considered to be an experimental work-in-progress.** Prior to widespread production use, we must meet our [v1.0.0 milestone](https://github.com/gopherjs/vecty/issues?q=is%3Aopen+is%3Aissue+milestone%3A1.0.0) goals, which are being completed slowly and steadily as contributors have time (Vecty is over 4 years in the making!).

- APIs will change.
- The scope of Vecty is only ~80% defined currently.
- There are a number of important [open issues](https://github.com/gopherjs/vecty/issues).
Early adopters may make use of it for real applications today as long as they are understanding and accepting of the fact that:

- APIs will change (maybe extensively).
- A number of important things are not ready:
- Extensive documentation, examples and tutorials
- URL-based component routing
- Ready-to-use component libraries (e.g. material UI)
- Server-side rendering
- And more, see [milestone: v1.0.0 ](https://github.com/gopherjs/vecty/issues?q=is%3Aopen+is%3Aissue+milestone%3A1.0.0)
- The scope of Vecty is only ~80% defined currently.
- There are a number of important [open issues](https://github.com/gopherjs/vecty/issues).

For a list of projects currently using Vecty, see the [doc/projects-using-vecty.md](doc/projects-using-vecty.md) file.

Small bundle sizes
==================

Vecty uses extremely minimal dependencies and prides itself on producing very small bundle sizes (mostly limited by the compiler), making it suitable for modern web development:

| Example | Compiler | Bundle size | Compressed (gzip) |
|--------------|-------------------------|-------------|-------------------|
| `hellovecty` | Go + WebAssembly | 2.3 KB | 0.5 KB |
| `markdown` | Go + WebAssembly | 4.2 KB | 0.9 KB |
| `todomvc` | Go + WebAssembly | 3.4 KB | 0.7 KB |
| `hellovecty` | GopherJS | 0.5 KB | 0.1 KB |
| `markdown` | GopherJS | 2.6 KB | 0.4 KB |
| `todomvc` | GopherJS | 1.7 KB | 0.3 KB |

Community
=========

Expand Down
5 changes: 5 additions & 0 deletions doc/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ Although v1.0.0 [is not yet out](https://github.com/gopherjs/vecty/milestone/1),
Pre-v1.0.0 Breaking Changes
---------------------------

## June 30, 2019 ([PR #232](https://github.com/gopherjs/vecty/pull/232)): major breaking change

- `(*HTML).Node` now returns a `syscall/js.Value` instead of `*gopherjs/js.Object`. Users will need to update to the new `syscall/js` API in their applications.
- Go 1.12+ is now required by Vecty, as we make use of [synchronous callback support](https://go-review.googlesource.com/c/go/+/142004) not present in earlier versions.

## May 25, 2019 ([PR #235](https://github.com/gopherjs/vecty/pull/235)): minor breaking change

- `prop.TypeUrl` has been renamed to `prop.TypeURL`.
Expand Down
117 changes: 35 additions & 82 deletions dom.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
package vecty

import (
"reflect"

"github.com/gopherjs/gopherjs/js"
)
import "reflect"

// batch renderer singleton
var batch = &batchRenderer{idx: make(map[Component]int)}
Expand Down Expand Up @@ -149,17 +145,6 @@ type HTML struct {
lastRenderedChild *HTML
}

// Node returns the underlying JavaScript Element or TextNode.
//
// It panics if it is called before the DOM node has been attached, i.e. before
// the associated component's Mounter interface would be invoked.
func (h *HTML) Node() *js.Object {
if h.node == nil {
panic("vecty: cannot call (*HTML).Node() before DOM node creation / component mount")
}
return h.node.(wrappedObject).j
}

// Key implements the Keyer interface.
func (h *HTML) Key() interface{} {
return h.key
Expand Down Expand Up @@ -236,15 +221,20 @@ func (h *HTML) reconcileProperties(prev *HTML) {
// Wrap event listeners
for _, l := range h.eventListeners {
l := l
l.wrapper = func(jsEvent *js.Object) {
l.wrapper = funcOf(func(this jsObject, args []jsObject) interface{} {
jsEvent := args[0]
if l.callPreventDefault {
jsEvent.Call("preventDefault")
}
if l.callStopPropagation {
jsEvent.Call("stopPropagation")
}
l.Listener(&Event{Object: jsEvent, Target: jsEvent.Get("target")})
}
l.Listener(&Event{
Value: jsEvent.(wrappedObject).j,
Target: jsEvent.Get("target").(wrappedObject).j,
})
return undefined
})
}

// Properties
Expand Down Expand Up @@ -350,6 +340,7 @@ func (h *HTML) removeProperties(prev *HTML) {
// Event listeners
for _, l := range prev.eventListeners {
h.node.Call("removeEventListener", l.Name, l.wrapper)
l.wrapper.Release()
}
}

Expand Down Expand Up @@ -1153,17 +1144,21 @@ func unmount(e ComponentOrHTML) {

// requestAnimationFrame calls the native JS function of the same name.
func requestAnimationFrame(callback func(float64)) int {
return global.Call("requestAnimationFrame", callback).Int()
var cb jsFunc
cb = funcOf(func(this jsObject, args []jsObject) interface{} {
cb.Release()

callback(args[0].Float())
return undefined
})
return global.Call("requestAnimationFrame", cb).Int()
}

// RenderBody renders the given component as the document body. The given
// Component's Render method must return a "body" element.
func RenderBody(body Component) {
// block batch until we're done
batch.scheduled = true
defer func() {
requestAnimationFrame(batch.render)
}()
nextRender, skip, pendingMounts := renderComponent(body, nil)
if skip {
panic("vecty: RenderBody Component.SkipRender illegally returned true")
Expand All @@ -1173,20 +1168,32 @@ func RenderBody(body Component) {
}
doc := global.Get("document")
if doc.Get("readyState").String() == "loading" {
doc.Call("addEventListener", "DOMContentLoaded", func() { // avoid duplicate body
// avoid duplicate body
var cb jsFunc
cb = funcOf(func(this jsObject, args []jsObject) interface{} {
cb.Release()

doc.Set("body", nextRender.node)
mount(pendingMounts...)
if m, ok := body.(Mounter); ok {
mount(m)
}
requestAnimationFrame(batch.render)
return undefined
})
doc.Call("addEventListener", "DOMContentLoaded", cb)
return
}
doc.Set("body", nextRender.node)
mount(pendingMounts...)
if m, ok := body.(Mounter); ok {
mount(m)
}
requestAnimationFrame(batch.render)
if !isTest {
// run Go forever
select {}
}
}

// SetTitle sets the title of the document.
Expand All @@ -1202,10 +1209,9 @@ func AddStylesheet(url string) {
global.Get("document").Get("head").Call("appendChild", link)
}

var (
global = wrapObject(js.Global)
undefined = wrappedObject{js.Undefined}
)
type jsFunc interface {
Release()
}

type jsObject interface {
Set(key string, value interface{})
Expand All @@ -1218,67 +1224,14 @@ type jsObject interface {
Float() float64
}

func wrapObject(j *js.Object) jsObject {
if j == nil {
return nil
}
if j == js.Undefined {
return undefined
}
return wrappedObject{j}
}

type wrappedObject struct {
j *js.Object
}

func (w wrappedObject) Set(key string, value interface{}) {
if v, ok := value.(wrappedObject); ok {
value = v.j
}
w.j.Set(key, value)
}

func (w wrappedObject) Get(key string) jsObject {
return wrapObject(w.j.Get(key))
}

func (w wrappedObject) Delete(key string) {
w.j.Delete(key)
}

func (w wrappedObject) Call(name string, args ...interface{}) jsObject {
for i, arg := range args {
if v, ok := arg.(wrappedObject); ok {
args[i] = v.j
}
}
return wrapObject(w.j.Call(name, args...))
}

func (w wrappedObject) String() string {
return w.j.String()
}
func (w wrappedObject) Bool() bool {
return w.j.Bool()
}

func (w wrappedObject) Int() int {
return w.j.Int()
}

func (w wrappedObject) Float() float64 {
return w.j.Float()
}

var isTest bool

func init() {
if isTest {
return
}
if global == nil {
panic("vecty: only GopherJS compiler is supported")
panic("vecty: only GopherJS and WebAssembly compilation is supported")
}
if global.Get("document") == undefined {
panic("vecty: only running inside a browser is supported")
Expand Down
Loading