(原) go库 -- WasmServe: Wasm的HTTP服务器

原创文章,请后转载,并注明出处。

一个像gopherjs的用于Wasm测试的HTTP服务器

github地址

示例:

运行远程包: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))
}

相关文章