一个像gopherjs的用于Wasm测试的HTTP服务器
示例:
运行远程包:wasmserve -tags=example github.com/hajimehoshi/wasmserve/example
这里必须要-tags=example,暂时不知何意。打开浏览器http://localhost:8080/可以看到结果。
运行一个本地包:wasmserve -tags=example ./examples/sprites
示例代码如下,即在浏览器中添加一个p结点,内容为HelloWorld。
package main
import (
"syscall/js"
)
func main() {
p := js.Global().Get("document").Call("createElement", "p")
p.Set("innerText", "Hello, World!")
js.Global().Get("document").Get("body").Call("appendChild", p)
}
服务器运行后,将自动编译好wasm文件:go build -o /tmp/898588238/main.wasm -tags example ./example
但看起来是每次刷新页面时都将编译一次,效率有点低呀。
打开浏览器,代码也简单
<!DOCTYPE html>
<!-- Polyfill for the old Edge browser -->
<script src="https://cdn.jsdelivr.net/npm/text-encoding@0.7.0/lib/encoding.min.js"></script>
<script src="wasm_exec.js"></script>
<script>
(async () => {
const resp = await fetch('main.wasm');
if (!resp.ok) {
const pre = document.createElement('pre');
pre.innerText = await resp.text();
document.body.appendChild(pre);
return;
}
const src = await resp.arrayBuffer();
const go = new Go();
const result = await WebAssembly.instantiate(src, go.importObject);
go.run(result.instance);
})();
</script>
此库的源代码其实并不多,也不复杂。看起来主要是帮你节省些劳动力。
package main
import (
"bytes"
"flag"
"io/ioutil"
"log"
"net/http"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"time"
)
// 通用的页面内容
const indexHTML = `<!DOCTYPE html>
<!-- Polyfill for the old Edge browser -->
<script src="https://cdn.jsdelivr.net/npm/text-encoding@0.7.0/lib/encoding.min.js"></script>
<script src="wasm_exec.js"></script>
<script>
(async () => {
const resp = await fetch('main.wasm');
if (!resp.ok) {
const pre = document.createElement('pre');
pre.innerText = await resp.text();
document.body.appendChild(pre);
return;
}
const src = await resp.arrayBuffer();
const go = new Go();
const result = await WebAssembly.instantiate(src, go.importObject);
go.run(result.instance);
})();
</script>
`
// 参数
var (
flagHTTP = flag.String("http", ":8080", "服务器端口")
flagTags = flag.String("tags", "", "生成标记")
flagAllowOrigin = flag.String("allow-origin", "", "允许指定的原点(或*代表所有原点)向此服务器发出请求")
)
func ensureModule(path string) ([]byte, error) {
_, err := os.Stat(filepath.Join(path, "go.mod"))
if err == nil {
return nil, nil
}
if !os.IsNotExist(err) {
return nil, err
}
log.Print("(", path, ")")
log.Print("go mod init example.com/m")
cmd := exec.Command("go", "mod", "init", "example.com/m")
cmd.Dir = path
return cmd.CombinedOutput()
}
var (
tmpWorkDir = ""
tmpOutputDir = "" //临时目录
)
func ensureTmpWorkDir() (string, error) {
if tmpWorkDir != "" {
return tmpWorkDir, nil
}
tmp, err := ioutil.TempDir("", "")
if err != nil {
return "", err
}
tmpWorkDir = tmp
return tmpWorkDir, nil
}
// 获取临时目录
func ensureTmpOutputDir() (string, error) {
if tmpOutputDir != "" {
return tmpOutputDir, nil
}
tmp, err := ioutil.TempDir("", "")
if err != nil {
return "", err
}
tmpOutputDir = tmp
return tmpOutputDir, nil
}
func hasGo111Module(env []string) bool {
for _, e := range env {
if strings.HasPrefix(e, "GO111MODULE=") {
return true
}
}
return false
}
// HTTP服务功能
func handle(w http.ResponseWriter, r *http.Request) {
// 是否允许跨域访问
if *flagAllowOrigin != "" {
w.Header().Set("Access-Control-Allow-Origin", *flagAllowOrigin)
}
// 获取临时目录
output, err := ensureTmpOutputDir()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
upath := r.URL.Path[1:]
fpath := filepath.Join(".", filepath.Base(upath))
workdir := "."
if !strings.HasSuffix(r.URL.Path, "/") {
fi, err := os.Stat(fpath)
if err != nil && !os.IsNotExist(err) {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if fi != nil && fi.IsDir() {
http.Redirect(w, r, r.URL.Path+"/", http.StatusSeeOther)
return
}
}
switch filepath.Base(fpath) {
case "index.html", ".": // 首页
if _, err := os.Stat(fpath); err != nil && !os.IsNotExist(err) {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
http.ServeContent(w, r, "index.html", time.Now(), bytes.NewReader([]byte(indexHTML)))
return
case "wasm_exec.js": // wasm_exec.js文件
if _, err := os.Stat(fpath); err != nil && !os.IsNotExist(err) {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
f := filepath.Join(runtime.GOROOT(), "misc", "wasm", "wasm_exec.js") // js文件地址
http.ServeFile(w, r, f)
return
case "main.wasm":
if _, err := os.Stat(fpath); err != nil && !os.IsNotExist(err) {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// 编译wasm文件
args := []string{"build", "-o", filepath.Join(output, "main.wasm")}
if *flagTags != "" {
args = append(args, "-tags", *flagTags)
}
if len(flag.Args()) > 0 {
args = append(args, flag.Args()[0])
} else {
args = append(args, ".")
}
log.Print("go ", strings.Join(args, " "))
cmdBuild := exec.Command("go", args...)
cmdBuild.Env = append(os.Environ(), "GOOS=js", "GOARCH=wasm")
// If GO111MODULE is not specified explicilty, enable Go modules.
// Enabling this is for backward compatibility of wasmserve.
if !hasGo111Module(cmdBuild.Env) {
cmdBuild.Env = append(cmdBuild.Env, "GO111MODULE=on")
}
cmdBuild.Dir = workdir
out, err := cmdBuild.CombinedOutput()
if err != nil {
log.Print(err)
log.Print(string(out))
http.Error(w, string(out), http.StatusInternalServerError)
return
}
if len(out) > 0 {
log.Print(string(out))
}
f, err := os.Open(filepath.Join(output, "main.wasm"))
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer f.Close()
http.ServeContent(w, r, "main.wasm", time.Now(), f)
return
}
http.ServeFile(w, r, filepath.Join(".", r.URL.Path))
}
func main() {
flag.Parse()
http.HandleFunc("/", handle)
log.Fatal(http.ListenAndServe(*flagHTTP, nil))
}