Web開發(fā)中為什么需要 WebAssembly ,以及在實際開發(fā)中如何使用 WebAssembly喜爷?帶著這些問題開始今天分享。
在進入正題前我們簡單地回顧一下 web 發(fā)展的歷史
- 第一個web 網(wǎng)頁在 1991 當時只是提供一些可以跳轉(zhuǎn)的靜態(tài)頁
- 隨著 10 天就設計出來的 javascript 的出現(xiàn),出現(xiàn) web 應用 gmail
- 隨后就是 javascript 庫盛行的時代
- 使用 jquery 操作 dom 的輕便靈巧拦英,幾乎讓我們忘記了 javascript 原生是如何操作 DOM
- angularjs 帶來第一個真正意義上的 SPA 的框架
- three.js 讓用戶在 web 端可以體驗到 3D 效果
- 然后就是現(xiàn)代 web 時代V8、WegGL平窘、Html5炉旷、serviceworks
有了這些才讓 google docs trello 和 mirosoft365 這些原來的桌面應用成為了 web 端的應用。
不過這些還不夠地熄,沒有用根本上解決在瀏覽器端對圖形處理以及渲染還有大型計算的天生不足的問題华临。
那么首先看一看什么是WebAssembly呢
- 用于 web 的全新的底層的字節(jié)碼
- 字節(jié)碼是可以由其他語言(相對于 javascript 來說的其他語言)編譯得來
- 可以提供 web 的性能
現(xiàn)在不僅是理論上,會給大家分享一些 webassembly 的實現(xiàn)离斩,并且會展望一些未來
首先我們來看一看 WebAssembly 究竟給我們帶來了什么银舱,以及選擇 WebAssembly 的理由。來回答第一個問題
- 當然首先選擇了 WebAssembly 的目的就是為了提高 web 的性能跛梗,運行在瀏覽器端字節(jié)碼會更快這點毋庸置疑寻馏。所以 WebAssembly 所能夠提供的性能是無法通過 javascript 能夠?qū)崿F(xiàn)的。雖然 javascript 的引擎已經(jīng)盡可能地提高了 javascript 的性能核偿。但是 WebAssembly 在大型計算诚欠、圖形處理上還是有距離的。
- 還有就是 WebAssembly 的友好性漾岳,可以將其他類似 c++ 的語言編譯為 WebAssembly 來使用轰绵,也可以使用開源的第三方。我們知道 c++ 可以 Android 和 ios 平臺編寫尼荆,同樣也支持 Web 端
- 給我們編寫 Web 應用除了 javascript 以外提供更多語言的選擇(c++ go rust 這是我知道的可能更多)以后 WebAssembly 還將會支持 kotlin 和 .Net
現(xiàn)在我們這里用 go 語言來給大家演示我們?nèi)绾问褂?WebAssembly 來回答第二個問題左腔。
今天我?guī)Т蠹矣?go 語言來實現(xiàn)一個 hello world 的 demo。go 語言對于 web 開發(fā)者總是那么友好捅儒。
準備工作
需要做一些準備工作安裝 gopherjs 液样,gopherjs 可以將 go 編譯為 javascript 后就可以運行在瀏覽器上。
go get -u github.com/gopherjs/gopherjs
我這里使用 gopm 這個工具下載安裝gopherjs
巧还。
gopm get -v -g github.com/gopherjs/gopherjs
搭建項目
cp $(go env GOROOT)/misc/wasm/wasm_exec.{html,js} .
通過上面的命令可以初始化以下兩個文件為:
- wasm_exec.js
- wasm_exec.html
WebAssembly.instantiateStreaming(fetch("test.wasm"), go.importObject)
在wasm_exec.html
文件中鞭莽,我們看到調(diào)用assembly
文件代,所以我們需要將 main.go 文件編譯為test.wasm
文件麸祷,這樣在 js 中才可以訪問到 WebAssembly 所提供的方法澎怒。
下面為完整的 wasm_exec.html
,可以將 html 名修改為 index.html 便于訪問阶牍。
<!doctype html>
<!--
Copyright 2018 The Go Authors. All rights reserved.
Use of this source code is governed by a BSD-style
license that can be found in the LICENSE file.
-->
<html>
<head>
<meta charset="utf-8">
<title>Go wasm</title>
</head>
<body>
<script src="wasm_exec.js"></script>
<script>
if (!WebAssembly.instantiateStreaming) { // polyfill
WebAssembly.instantiateStreaming = async (resp, importObject) => {
const source = await (await resp).arrayBuffer();
return await WebAssembly.instantiate(source, importObject);
};
}
const go = new Go();
let mod, inst;
WebAssembly.instantiateStreaming(fetch("test.wasm"), go.importObject).then((result) => {
mod = result.module;
inst = result.instance;
document.getElementById("runButton").disabled = false;
});
async function run() {
console.clear();
await go.run(inst);
inst = await WebAssembly.instantiate(mod, go.importObject); // reset instance
}
</script>
<button onClick="run();" id="runButton" disabled>Run</button>
</body>
</html>
if (!WebAssembly.instantiateStreaming) { // polyfill
WebAssembly.instantiateStreaming = async (resp, importObject) => {
const source = await (await resp).arrayBuffer();
return await WebAssembly.instantiate(source, importObject);
};
}
首先需要判斷WebAssembly.instantiateStreaming
是否存在喷面,如果不存在我們通過其他方法實現(xiàn)WebAssembly.instantiateStreaming
函數(shù)功能星瘾。
WebAssembly.instantiateStreaming
異步加載以 *.wasm
后綴結(jié)束的 webAssembly 的文件乖酬,成功加載test.wasm
文件后取消 run 按鈕的禁用死相,run 按鈕的點擊事件是運行
WebAssembly.instantiateStreaming(fetch("test.wasm"),
這里可以寫簡單的 hello world ,用 go run main.go
命令查看一下輸出审洞。
package main
func main() {
println("Hello World")
}
然后通過命令下面的命令來將main.go
編譯為test.wasm
文件
GOARCH=wasm GOOS=js go build -o test.wasm main.go
接下來我們還需要寫一個server.go
來啟動我們服務運行 wasm_exec.html
创淡。
package main
import(
"flag"
"log"
"net/http"
// "strings"
)
var (
listen = flag.String("listen",":8080","listen address")
dir = flag.String("dir",".","directory to serve")
)
func main() {
flag.Parse()
log.Printf("listening on %q...",*listen)
log.Fatal(http.ListenAndServe(*listen, http.FileServer(http.Dir(*dir))))
// log.Fatal(http.ListenAndServe(*listen, http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request){
// if strings.HasSuffix(req.URL.Path,".wasm"){
// resp.Header().Set("content-type","application/wasm")
// }
// http.FileServer(http.Dir(*dir)).ServeHTTP(resp, req)
// })))
}
在地址欄中輸入localhost:8080/index.html"
看到我們?nèi)缦聢D,這里有一個 run 按鈕點擊就可以調(diào)用我們的 test.wasm 中輸出方法
這樣在 main.go 中輸出的
hello world
在瀏覽器中就成功輸出了驹止。
接下來讓 webAssembly 提供一些計算函數(shù)供 web 調(diào)用岸售,創(chuàng)建add
函數(shù)接收兩個參數(shù),進行加法運算集灌。然后在js.Global()
提供的 Set()
方法以回調(diào)方式將 WebAssembly 提供的 add 方法掛在 javascript 的全局對象的add
屬性上,這樣在 chrome 的 console 中輸入 add 就可以調(diào)用 webAssembly 的提供 add 方法梯找。
package main
import(
"syscall/js"
)
func add(i []js.Value){
js.Global().Set("output",js.ValueOf(i[0].Int() + i[1].Int()))
println(js.ValueOf(i[0].Int() + i[1].Int()).String())
}
func registerCallbacks(){
js.Global().Set("add",js.NewCallback(add))
}
func main() {
c := make(chan struct{},0)
println("Go WebAssembly Initialized")
registerCallbacks()
<-c
}
在 registerCallbacks 函數(shù)中丁寄,
js.Global()
獲取 javascript 全局對象然后,通過Set
方法 add 函數(shù)以回調(diào)的方式掛接到 javascript 的全局的 add 屬性上屑埋。這里是通過js.NewCallback(add)
實現(xiàn)的豪筝。c := make(chan struct{},0)
創(chuàng)建 channel,然后在<-c 讓 goroutine 阻塞,以便代碼被執(zhí)行到
func subtract(i []js.Value){
js.Global().Set("output",js.ValueOf(i[0].Int() - i[1].Int()))
println(js.ValueOf(i[0].Int() - i[1].Int()).String())
}
func registerCallbacks(){
js.Global().Set("add",js.NewCallback(add))
js.Global().Set("subtract",js.NewCallback(subtract))
}
if (!WebAssembly.instantiateStreaming) { // polyfill
WebAssembly.instantiateStreaming = async (resp, importObject) => {
const source = await (await resp).arrayBuffer();
return await WebAssembly.instantiate(source, importObject);
};
}
const go = new Go();
let mod, inst;
WebAssembly.instantiateStreaming(fetch("test.wasm"), go.importObject).then((result) => {
mod = result.module;
inst = result.instance;
document.getElementById("runButton").disabled = false;
});
async function run() {
console.clear();
await go.run(inst);
inst = await WebAssembly.instantiate(mod, go.importObject); // reset instance
}
修改下無需點擊run
我們將 test.wasm 進行加載续崖。
const go = new Go();
let mod, inst;
WebAssembly.instantiateStreaming(fetch("test.wasm"), go.importObject).then(async (result) => {
mod = result.module;
inst = result.instance;
document.getElementById("runButton").disabled = false;
await go.run(inst);
});
// async function run() {
// console.clear();
// inst = await WebAssembly.instantiate(mod, go.importObject); // reset instance
// }
通過 gopherjs
實現(xiàn)獲取 dom 元素的值敲街,因為類型是字符,通過strconv.Atoi
方法將字符串轉(zhuǎn)為int 型進行計算严望。
func add(i []js.Value){
value1 := js.Global().Get("document").Call("getElementById",i[0].String()).Get("value").String()
value2 := js.Global().Get("document").Call("getElementById",i[1].String()).Get("value").String()
int1, _ := strconv.Atoi(value1)
int2, _ := strconv.Atoi(value2)
js.Global().Set("output",int1 + int2)
println(int1 + int2)
}
<input type="text" id="value1" />
<input type="text" id="value2" />
<button onClick="add('value1','value2');" id="runButton">Add</button>
<button onClick="subtract(3,2);" id="runButton">Subtract</button>
這樣做還不夠我們還需要將計算的結(jié)果輸出到 input 中多艇,所以繼續(xù)對程序改造。這里并不對 gopherjs 進行過多解釋像吻,大家可能對這些代碼有些陌生峻黍,不過稍微熟悉 web 開發(fā),這個應該不難理解一看就懂拨匆。
func add(i []js.Value){
value1 := js.Global().Get("document").Call("getElementById",i[0].String()).Get("value").String()
value2 := js.Global().Get("document").Call("getElementById",i[1].String()).Get("value").String()
int1, _ := strconv.Atoi(value1)
int2, _ := strconv.Atoi(value2)
js.Global().Get("document").Call("getElementById",i[2].String()).Set("value",int1 + int2)
// js.Global().Set("output",int1 + int2)
// println(int1 + int2)
}
對應修改一下 onclick 方法的參數(shù) onClick="add('value1','value2','result');"
奸披。
<input type="text" id="value1" />
<input type="text" id="value2" />
<button onClick="add('value1','value2','result');" id="runButton">Add</button>
<button onClick="subtract(3,2);" id="runButton">Subtract</button>
<input id="result" type="text" />