可能是世界上最简单的用 Go 来写 WebAssembly 的教程
原標(biāo)題:The world’s easiest introduction to WebAssembly???? 原文鏈接:The world’s easiest introduction to WebAssembly - freeCodeCamp.org - Medium? 作者:Martin Olsansky (olso)
一個(gè)與貓咪交互的 Canvas 手機(jī)游戲,這個(gè)項(xiàng)目完全由 Golang 編寫(xiě)。圖里這只小貓正在體驗(yàn)我編寫(xiě)的小游戲你認(rèn)為 WebAssembly (WASM) 只用于圖像處理、復(fù)雜的數(shù)學(xué)計(jì)算或者 Web 上的小小應(yīng)用嗎?
你是否經(jīng)常將 WASM 與 Web Workers 和 Service Workers 的概念混淆?
你對(duì) WASM 不感興趣,是因?yàn)槟阏J(rèn)為現(xiàn)在的 Web 應(yīng)用程序在未來(lái) 10 年里依舊是 JavaScript 主導(dǎo)?
你是否想過(guò)用 JS 以外的語(yǔ)言做 Web 前端開(kāi)發(fā)?
如果你不想細(xì)讀,你可以看下我做的 demo ?頁(yè)面或者直接看下 ???? go-wasm-cat-game-on-canvas-with-docker 這個(gè)項(xiàng)目,我會(huì)講的簡(jiǎn)潔一些,盡量不浪費(fèi)你的時(shí)間。以下是我這個(gè)項(xiàng)目的一些關(guān)鍵的代碼解析。
故事開(kāi)始了 ????
我們的目標(biāo)是給貓 ???? 做一個(gè)簡(jiǎn)單的小游戲:做一個(gè)小紅點(diǎn)在手機(jī)上不停的移動(dòng),整個(gè)過(guò)程還有 HiFi 音樂(lè) ????還有震動(dòng)。整個(gè)項(xiàng)目我們會(huì)用 ?Golang (Go)這門(mén)語(yǔ)言來(lái)實(shí)現(xiàn),包括 DOM 操作、邏輯還有相關(guān)的狀態(tài)。
而且,由于貓咪不會(huì)使用鼠標(biāo),我們還需要給貓爪 ???? 做一些點(diǎn)擊觸摸的交互。
說(shuō)一下我的理解!
把 WASM 想象成一個(gè) 通用虛擬機(jī)(UVM, Universal Virtual Machine) 或者一個(gè)沙箱,你只需編寫(xiě)一次任何代碼,它便可以在任何地方運(yùn)行。
WASM 是一個(gè)編譯目標(biāo),而不是一種語(yǔ)言。就像你要同時(shí)針對(duì) Windows,Mac OS 和 Linux 進(jìn)行編譯一樣!
我不認(rèn)為 WASM 會(huì)廢棄 JS,你可以有其他選擇而不用付出任何代價(jià)。
想象一下使用 Go,Swift,Rust,Ruby,C ++,OCaml 或者其他語(yǔ)言的開(kāi)發(fā)人員。現(xiàn)在,他們可以使用自己喜歡的語(yǔ)言來(lái)創(chuàng)建交互式,聯(lián)網(wǎng),快速,具有脫機(jī)功能的網(wǎng)站和Web 應(yīng)用。
你是否曾經(jīng)參與過(guò)類似「一個(gè)項(xiàng)目是用一個(gè)代碼倉(cāng)庫(kù)管理還是多個(gè)代碼倉(cāng)庫(kù)管理?」問(wèn)題的討論?
好吧,不管你有沒(méi)有,你現(xiàn)在也要想一下現(xiàn)在這個(gè)項(xiàng)目打算用一門(mén)語(yǔ)言實(shí)現(xiàn)還是多門(mén)語(yǔ)言實(shí)現(xiàn)了。
當(dāng)大家可以使用相同的技術(shù)棧時(shí),一切都會(huì)變得更加容易,尤其是團(tuán)隊(duì)之間的溝通。
你可以依舊使用 React 或者 Vue,但你現(xiàn)在開(kāi)始也可以不用使用 JS 來(lái)開(kāi)發(fā)了。
WASM 跟 Service Workers 還有 Web Workers 有什么區(qū)別?
Service Workers 還有 Web Workers 允許應(yīng)用在后臺(tái)運(yùn)行,也可以做到離線運(yùn)行和緩存。它們模仿線程,無(wú)法訪問(wèn)DOM,并且不能共享數(shù)據(jù)(僅能通過(guò)消息傳遞),只能在單獨(dú)的上下文中運(yùn)行。咦,其實(shí)我們甚至可以在其中運(yùn)行 WASM 而不是 JS。對(duì)我來(lái)說(shuō),它們只提供一些具有特殊特權(quán)的抽象層,沒(méi)有人說(shuō)這些層必須執(zhí)行 JS。
Service Workers 還有 Web Workers 是瀏覽器上的功能,不是 JS 的專有功能。
設(shè)置開(kāi)發(fā)環(huán)境 ????
我們將使用 WASM,Go,JS 和 Docker(這個(gè)是可選的)???? 來(lái)進(jìn)行開(kāi)發(fā)。
如果您不了解Go,但了解 JS,請(qǐng) 點(diǎn)擊這里學(xué)習(xí) Go,然后再回來(lái)繼續(xù)閱讀。讓我們從 Go WASM Wiki 開(kāi)始。
你可以使用安裝在電腦本地的 ?go 版本,在這里我使用 Docker 的 golang:1.12-rc 鏡像。只需在此處為 go 編譯器設(shè)置兩個(gè) WASM 標(biāo)志。在 main.go 中創(chuàng)建一個(gè)簡(jiǎn)單的 hello world 進(jìn)行測(cè)試。
$?GOOS=js?GOARCH=wasm?go?build?-o?game.wasm?main.go build_go:docker?run?--rm?\-v?`pwd`/src:/game?\--env?GOOS=js?--env?GOARCH=wasm?\golang:1.12-rc?\/bin/bash?-c?"go?build?-o?/game/game.wasm?/game/main.go;?cp?/usr/local/go/misc/wasm/wasm_exec.js?/game/wasm_exec.js"現(xiàn)在,讓我們利用好 Go 團(tuán)隊(duì)提供的 wasm_exec.js ?代碼。代碼里的全局變量 Go 對(duì) WASM 進(jìn)行了初始化操作,我們不必自己從頭開(kāi)始做好任何 DOM 的實(shí)現(xiàn)。等我們編譯好 wasm 文件后,它會(huì)獲取 .wasm 文件并運(yùn)行我們的游戲。
總而言之,它應(yīng)該看起來(lái)像這樣:
<!DOCTYPE?html> <html><head><meta?charset="utf-8"?/><meta?name="viewport"?content="width=device-width,initial-scale=1.0"?/><style>body{height:100%;width:100%;padding:0;margin:0;background-color:#000000;color:#FFFFFF;font-family:Arial,Helvetica,sans-serif}</style><script?type="text/javascript"?src="./wasm_exec.js"></script><script?type="text/javascript">async?function?run(fileUrl)?{try?{const?file?=?await?fetch(fileUrl);const?buffer?=?await?file.arrayBuffer();const?go?=?new?Go();const?{?instance?}?=?await?WebAssembly.instantiate(buffer,?go.importObject);go.run(instance);}?catch?(err)?{console.error(err);}}setTimeout(()?=>?run("./game.wasm"));</script></head><body></body> </html>放碼過(guò)來(lái)!(當(dāng)然是 Go 的碼)
要渲染我們的這個(gè)小游戲,<canvas> 這個(gè)標(biāo)簽應(yīng)該足夠了。我們可以直接從 Go 代碼創(chuàng)建 DOM 結(jié)構(gòu)和元素!這個(gè) syscall/js 文件 (包含在標(biāo)準(zhǔn) Go 庫(kù)中)為我們處理了與 DOM 交互的方法。
main() 方法
我敢打賭,你很久沒(méi)見(jiàn)過(guò) main() 方法了????。
package?mainimport?(//?https://github.com/golang/go/tree/master/src/syscall/js"syscall/js" )var?(//?js.Value?可以是任意的?JS?對(duì)象、類型或者構(gòu)造函數(shù)window,?doc,?body,?canvas,?laserCtx,?beep?js.ValuewindowSize?struct{?w,?h?float64?} )func?main()?{setup() }func?setup()?{window?=?js.Global()doc?=?window.Get("document")body?=?doc.Get("body")windowSize.h?=?window.Get("innerHeight").Float()windowSize.w?=?window.Get("innerWidth").Float()canvas?=?doc.Call("createElement",?"canvas")canvas.Set("height",?windowSize.h)canvas.Set("width",?windowSize.w)body.Call("appendChild",?canvas)//?這個(gè)是小紅點(diǎn)??????Canvas?對(duì)象laserCtx?=?canvas.Call("getContext",?"2d")laserCtx.Set("fillStyle",?"red")//?http://www.iandevlin.com/blog/2012/09/html5/html5-media-and-data-uri/beep?=?window.Get("Audio").New("data:audio/mp3;base64,SUQzBAAAAAAAI1RTU0UAAAAPAAADTGF2ZjU2LjI1LjEwMQAAAAAAAAAAAAAA/+NAwAAAAAAAAAAAAFhpbmcAAAAPAAAAAwAAA3YAlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaWlpaW8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDw8PDwAAAAAExhdmYAAAAAAAAAAAAAAAAAAAAAACQAAAAAAAAAAAN2UrY2LgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP/jYMQAEvgiwl9DAAAAO1ALSi19XgYG7wIAAAJOD5R0HygIAmD5+sEHLB94gBAEP8vKAgGP/BwMf+D4Pgh/DAPg+D5//y4f///8QBhMQBgEAfB8HwfAgIAgAHAGCFAj1fYUCZyIbThYFExkefOCo8Y7JxiQ0mGVaHKwwGCtGCUkY9OCugoFQwDKqmHQiUCxRAKOh4MjJFAnTkq6QqFGavRpYUCmMxpZnGXJa0xiJcTGZb1gJjwOJDJgoUJG5QQuDAsypiumkp5TUjrOobR2liwoGBf/X1nChmipnKVtSmMNQDGitG1fT/JhR+gYdCvy36lTrxCVV8Paaz1otLndT2fZuOMp3VpatmVR3LePP/8bSQpmhQZECqWsFeJxoepX9dbfHS13/aysppUblm//8t7p2Ez7xKD/42DE4E5z9pr/nNkRw6bhdiCAZVVSktxunhxhH//4xF+bn4//6//3jEvylMM2K9XmWSn3ah1L2MqVIjmNlJtpQux1n3ajA0ZnFSu5EpXuGatn///1r/pYabq0mKT//TRyTEFNRTMuOTkuNaqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq/+MQxNIAAANIAcAAAKqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqg==") }看起來(lái)是不是很像 JS 代碼?
是的,這就是與 DOM 交互所需的全部?jī)?nèi)容!現(xiàn)在只需要幾個(gè) get 方法還有調(diào)用函數(shù)即可。
awsl???? 它就在那!在這一點(diǎn)上,我問(wèn)自己:在某種程度上,我仍然在寫(xiě) JS … 這怎么算是升級(jí)?因?yàn)槲覀冞€不能直接訪問(wèn) DOM,所以我們必須(通過(guò) JS)調(diào)用 DOM 來(lái)做任何事情。想象一下如何用 JSX / React 來(lái)抽象化它。
實(shí)際上,已經(jīng)可以做到了,請(qǐng)期待我的下篇文章 ????。
「渲染」還有事件處理
直接使用 syscall / js 庫(kù),這個(gè)寫(xiě)法看起來(lái)有點(diǎn)像 ES5 的回調(diào)。但我們能夠監(jiān)聽(tīng) DOM 事件,而且那些靜態(tài)類型看起來(lái)很干凈!
func?main()?{setup()//?在編譯時(shí)聲明渲染器var?renderer?js.Func//?沒(méi)有錯(cuò),看起來(lái)很像?JS?的回調(diào)?????renderer?=?js.FuncOf(func(this?js.Value,?args?[]js.Value)?interface{}?{updateGame()//?實(shí)現(xiàn)?60FPS?的動(dòng)畫(huà)window.Call("requestAnimationFrame",?renderer)return?nil})window.Call("requestAnimationFrame",?renderer)//?讓我們處理下?鼠標(biāo)/手勢(shì)?點(diǎn)擊事件var?mouseEventHandler?js.Func?=?js.FuncOf(func(this?js.Value,?args?[]js.Value)?interface{}?{updatePlayer(args[0])return?nil})window.Call("addEventListener",?"pointerdown",?mouseEventHandler) }func?updatePlayer(event?js.Value)?{} func?updateGame()?{}日志記錄、音頻播放以及「異步」執(zhí)行
在 Go 中,有一個(gè)慣例是把所有的函數(shù)都寫(xiě)成同步的方式,由調(diào)用者決定函數(shù)的執(zhí)行是否是異步的。異步運(yùn)行函數(shù)非常簡(jiǎn)單,只要在前面加上 go 就行了!它使用自己的上下文創(chuàng)建一個(gè)線程,你仍然可以將父級(jí)上下文綁定給它,不要擔(dān)心哈。
func?updatePlayer(event?js.Value)?{mouseX?:=?event.Get("clientX").Float()mouseY?:=?event.Get("clientY").Float()//?`go`?關(guān)鍵字是主要用來(lái)實(shí)現(xiàn)線程、異步、并行的功能//?TODO?與?Web?Workers?的區(qū)別//?TODO?與?Service?Workers?的區(qū)別//?https://gobyexample.com/goroutinesgo?log("mouseEvent",?"x",?mouseX,?"y",?mouseY)//?下一個(gè)關(guān)鍵點(diǎn)if?isLaserCaught(mouseX,?mouseY,?gs.laserX,?gs.laserY)?{go?playSound()} }//?不要以為我用了什么黑魔法,這里直接使用了?HTML5?的?API //?https://developer.mozilla.org/en-US/docs/Web/API/HTMLAudioElement#Basic_usage func?playSound()?{beep.Call("play")window.Get("navigator").Call("vibrate",?300) }//?這里主要用了?JS?的解構(gòu)賦值語(yǔ)法 //?這里的?`...interface{}`?有點(diǎn)像?TS?的?`any`?語(yǔ)法 //?https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/rest_parameters#Description func?log(args?...interface{})?{window.Get("console").Call("log",?args...) }讓游戲一直跑下去!
該代碼創(chuàng)建一個(gè)非緩沖通道,并嘗試從該通道接收數(shù)據(jù)。因?yàn)闆](méi)有人向它發(fā)送任何東西,它本質(zhì)上是一個(gè)永久的阻塞操作,允許我們永遠(yuǎn)運(yùn)行我們的程序。
func?main()?{//?https://stackoverflow.com/a/47262117//?創(chuàng)建空通道runGameForever?:=?make(chan?bool)setup()//?嘗試從空通道接收//?由于沒(méi)有人向它發(fā)送任何數(shù)據(jù),它本質(zhì)上是一個(gè)永久阻塞操作//?我們有一個(gè)?daeomon?/?service?/?background?程序//?在?WASM?里,我們的游戲會(huì)一直運(yùn)行?????<-runGameForever }更新游戲狀態(tài)并移動(dòng)小紅點(diǎn)
這里沒(méi)有狀態(tài)管理,只有一個(gè)簡(jiǎn)單的聲明類型的結(jié)構(gòu)體,它不允許在內(nèi)部傳遞任何不正確的值。
import?("math" )type?gameState?struct{?laserX,?laserY,?directionX,?directionY,?laserSize?float64?}var?(//?gs?處于最高范圍,小于這個(gè)范圍小紅點(diǎn)??????都能都能訪問(wèn)gs?=?gameState{laserSize:?35,?directionX:?3.7,?directionY:?-3.7,?laserX:?40,?laserY:?40} )func?updateGame()?{//?邊界判斷if?gs.laserX+gs.directionX?>?windowSize.w-gs.laserSize?||?gs.laserX+gs.directionX?<?gs.laserSize?{gs.directionX?=?-gs.directionX}if?gs.laserY+gs.directionY?>?windowSize.h-gs.laserSize?||?gs.laserY+gs.directionY?<?gs.laserSize?{gs.directionY?=?-gs.directionY}//?移動(dòng)小紅點(diǎn)?????gs.laserX?+=?gs.directionXgs.laserY?+=?gs.directionY//?清除畫(huà)布laserCtx.Call("clearRect",?0,?0,?windowSize.w,?windowSize.h)//畫(huà)一個(gè)小紅點(diǎn)?????laserCtx.Call("beginPath")laserCtx.Call("arc",?gs.laserX,?gs.laserY,?gs.laserSize,?0,?math.Pi*2,?false)laserCtx.Call("fill")laserCtx.Call("closePath") }//?判斷點(diǎn)擊的點(diǎn)是不是在小紅點(diǎn)??????內(nèi)部 func?isLaserCaught(mouseX,?mouseY,?laserX,?laserY?float64)?bool?{//?直接這樣返回是不行的//?return?laserCtx.Call("isPointInPath",?mouseX,?mouseY).Bool()//?所以這里我通過(guò)勾股定理??????來(lái)實(shí)現(xiàn)//?同時(shí)我給?laserSize?屬性的值加上?15,讓貓爪更容易點(diǎn)擊??????return?(math.Pow(mouseX-laserX,?2)?+?math.Pow(mouseY-laserY,?2))?<?math.Pow(gs.laserSize+15,?2) }總結(jié)
事實(shí)上,WASM 仍然被認(rèn)為是一個(gè) [MVP](https://hacks.mozilla.org/2018/10/webassembly -post- MVP -future/) (MAP),你可以不用編寫(xiě)一行 JS,就能創(chuàng)建一個(gè)像這樣的游戲。驚不驚訝!CanIUse 上 WASM 的支持已經(jīng)是一片綠色了,沒(méi)有人可以阻止你去創(chuàng)建基于 WASM 的網(wǎng)站和應(yīng)用。
你可以組合所有你想要的語(yǔ)言,像是把 JS 轉(zhuǎn)成 WASM。最后,它們都將編譯成 WASM 字節(jié)碼。如果你需要在他們之間分享任何東西,也沒(méi)問(wèn)題,因?yàn)樗鼈兛梢怨蚕碓純?nèi)存。
我擔(dān)心的是,在最近的新聞中,我們關(guān)注到 微軟正在開(kāi)發(fā) Chromium 瀏覽器 還有 Firefox市場(chǎng)份額低于9%。這使谷歌在 WASM 上有了致命的切換能力。如果他們不愿意配合,大眾可能永遠(yuǎn)不會(huì)知道有這個(gè)特性。
你看這只貓玩的多開(kāi)心 ????現(xiàn)在都有誰(shuí)在用 WASM?
你必須得承認(rèn),我的項(xiàng)目已經(jīng)在用了。這個(gè)項(xiàng)目?jī)H僅是畫(huà)了一個(gè)全屏的畫(huà)布,這里有一些更高級(jí)的例子,它們關(guān)注于語(yǔ)義 Web awesome-wasm#web-frameworks-libraries。
同時(shí),也有相當(dāng)多的項(xiàng)目已經(jīng)上了 WASM 的車了。我對(duì) Spotify、Twitch 和 Figma 和 EWASM 更感興趣。
Web3 時(shí)代的 WASM
現(xiàn)在,如果你想在手機(jī)上使用以太坊錢(qián)包(Ethereum wallet),你必須從應(yīng)用商店下載一個(gè)類似于 Status.im 的移動(dòng)端錢(qián)包 App,并且信任所有商家。
如果有一個(gè)先進(jìn)的 Web App,可以運(yùn)行 geth (Go Ethereum 客戶端),并且能在 WebRTC 上光速同步,這會(huì)怎么樣?它可以使用 Service Worker 來(lái)更新它的 WASM 代碼并在后臺(tái)運(yùn)行,可以托管在 IPFS/Dat 上。
一些有用的關(guān)于 WASM 的文章、資源還有學(xué)習(xí)資料 ????
WebAssembly is more than the web
WebAssembly and Go: A look at the future ?還有 ?HN comments
Mozilla Hacks ?和 Hacker News 發(fā)布的文章
WebAssembly architecture for Goawesome-wasm , awesome-wasm-langs , ?gowasm-experiments , ?WasmWeekly , WasmRocks , ?SPA with C++ , ?better DOM bindings for Go
感謝 twifkak 在 Android Chrome 上對(duì) Go 的優(yōu)化!
總結(jié)
以上是生活随笔為你收集整理的可能是世界上最简单的用 Go 来写 WebAssembly 的教程的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 推荐系统 embedding 技术实践总
- 下一篇: 「递归」第7集 | 腾讯开源联盟出征!