Berkat WebAssembly, Anda dapat menulis Frontend on Go

Artikel asli

Pada bulan Februari 2017, seorang anggota tim Brad Fitzpatrick go mengusulkan pembuatan dukungan WebAssembly dalam bahasa tersebut. Empat bulan kemudian, pada November 2017, penulis GopherJS Richard Muziol mulai menerapkan gagasan itu. Dan akhirnya, implementasi penuh ditemukan di master. Pengembang akan menerima wasme sekitar Agustus 2018, dengan versi go 1.11 . Akibatnya, pustaka standar mengatasi hampir semua kesulitan teknis dengan mengimpor dan mengekspor fungsi yang Anda kenal jika Anda sudah mencoba mengkompilasi C dalam wasm. Kedengarannya menjanjikan. Mari kita lihat apa yang bisa dilakukan dengan versi pertama.



Semua contoh dalam artikel ini dapat diluncurkan dari wadah buruh pelabuhan yang ada di repositori penulis :

docker container run -dP nlepage/golang_wasm:examples # Find out which host port is used docker container ls 

Lalu pergi ke localhost : 32XXX /, dan pergi dari satu tautan ke tautan lainnya.

Hai Wasm!


Penciptaan dasar "hello world" dan konsepnya sudah cukup terdokumentasi dengan baik (bahkan dalam bahasa Rusia ), jadi mari kita beralih ke hal-hal yang lebih halus.

Yang paling penting adalah versi Go yang baru dikompilasi yang mendukung wasm. Saya tidak akan menjelaskan langkah demi langkah instalasi , hanya tahu bahwa apa yang dibutuhkan sudah di master.

Jika Anda tidak ingin khawatir tentang hal ini, Dockerfile c go tersedia di repositori golub-wasm di github , atau Anda dapat mengambil gambar dari nlepage / golang_wasm lebih cepat.

Sekarang Anda dapat menulis helloworld.go tradisional dan kompilasi dengan perintah berikut:

 GOOS=js GOARCH=wasm go build -o test.wasm helioworld.go 

Variabel lingkungan GOOS dan GOARCH telah disetel di gambar nlepage / golang_wasm , sehingga Anda dapat menggunakan file Dockerfile seperti ini untuk dikompilasi:

 FROM nlepage/golang_wasm COPY helloworld.go /go/src/hello/ RUN go build -o test.wasm hello 

Langkah terakhir adalah menggunakan file wasm_exec.html dan wasm_exec.js tersedia di repositori go di misc/wasm atau di docker image nlepage / golang_wasm di /usr/local/go/misc/wasm/ untuk menjalankan test.wasm di browser (wasm_exec.js mengharapkan file binary test.wasm , jadi kami menggunakan nama ini).
Anda hanya perlu memberikan 3 file statis menggunakan nginx, misalnya, lalu wasm_exec.html akan menampilkan tombol “run” (ini akan menyala hanya jika test.wasm dimuat dengan benar).

Patut dicatat bahwa test.wasm harus disajikan dengan application/wasm jenis MIME, jika tidak browser akan menolak untuk mengeksekusinya. (mis. nginx membutuhkan file mime.types yang diperbarui ).

Anda dapat menggunakan gambar nginx dari nlepage / golang_wasm , yang sudah menyertakan tipe MIME tetap, wasm_exec.html dan wasm_exec.js dalam kode> / usr / share / nginx / html / direktori.

Sekarang klik tombol "run", lalu buka konsol browser Anda dan Anda akan melihat ucapan console.log ("Hello Wasm!").


Contoh lengkap tersedia di sini .

Panggil JS dari Go


Sekarang kita telah berhasil meluncurkan biner WebAssembly pertama yang dikompilasi dari Go, mari kita lihat lebih dekat fitur yang disediakan.

Paket syscall / js baru telah ditambahkan ke perpustakaan standar. Pertimbangkan file utama, js.go
Tipe js.Value baru js.Value yang mewakili nilai JavaScript.

Ini menawarkan API sederhana untuk mengelola variabel JavaScript:

  • js.Value.Get() dan js.Value.Set() mengembalikan dan mengatur nilai bidang objek.
  • js.Value.Index() dan js.Value.SetIndex() mengakses objek dengan membaca dan menulis indeks.
  • js.Value.Call() memanggil metode objek sebagai fungsi.
  • js.Value.Invoke() menyebut objek itu sendiri sebagai fungsi.
  • js.Value.New() memanggil operator baru dan menggunakan pengetahuannya sendiri sebagai konstruktor.
  • Beberapa metode lagi untuk mendapatkan nilai JavaScript dalam tipe Go yang sesuai, misalnya js.Value.Int() atau js.Value.Bool() .

Dan metode menarik tambahan:

  • js.Undefined() akan memberikan js.Value undefined sesuai.
  • js.Null() akan memberikan js.Value null sesuai.
  • js.Global() akan mengembalikan js.Value memberikan akses ke lingkup global.
  • js.ValueOf() menerima tipe Go primitif dan mengembalikan js.Value benar

Alih-alih menampilkan pesan di os.StdOut, mari kita tampilkan di jendela notifikasi menggunakan window.alert() .

Karena kita berada di browser, ruang lingkup global adalah sebuah jendela, jadi pertama-tama Anda perlu mendapatkan lansiran () dari ruang lingkup global:

 alert := js.Global().Get("alert") 

Sekarang kita memiliki variabel alert , dalam bentuk js.Value , yang merupakan referensi ke window.alert JS, dan Anda dapat menggunakan fungsi untuk memanggil melalui js.Value.Invoke() :

 alert.Invoke("Hello wasm!") 

Seperti yang Anda lihat, tidak perlu memanggil js.ValueOf () sebelum meneruskan argumen ke Invoke, dibutuhkan interface{} sewenang-wenang dan meneruskan nilai melalui ValueOf itu sendiri.

Sekarang program baru kita akan terlihat seperti ini:

 package main import ( "syscall/js" ) func main() { alert := js.Global().Get("alert") alert.Invoke("Hello Wasm!") } 

Seperti pada contoh pertama, Anda hanya perlu membuat file bernama test.wasm , dan biarkan wasm_exec.html dan wasm_exec.js seperti semula.
Sekarang, ketika kita mengklik tombol "Jalankan", jendela peringatan muncul dengan pesan kami.

Contoh yang berfungsi ada di folder examples/js-call .

Panggil Pergi dari JS.


Memanggil JS dari Go cukup sederhana, mari kita lihat lebih dekat pada paket syscall/js , file kedua yang akan dilihat adalah callback.go .

  • js.Callback tipe pembungkus untuk fungsi Go, untuk digunakan dalam JS.
  • js.NewCallback() fungsi yang mengambil fungsi (menerima sepotong js.Value dan mengembalikan apa-apa), dan mengembalikan js.Callback .
  • Beberapa mekanisme untuk mengelola panggilan balik aktif dan js.Callback.Release() , yang harus dipanggil untuk menghancurkan panggilan balik itu.
  • js.NewEventCallback() mirip dengan js.NewCallback() , tetapi fungsi yang dibungkus hanya menerima 1 argumen - sebuah peristiwa.

Mari kita coba melakukan sesuatu yang sederhana: jalankan Go fmt.Println() dari sisi JS.

Kami akan membuat beberapa perubahan pada wasm_exec.html untuk bisa mendapatkan panggilan balik dari Go untuk memanggilnya.

 async function run() { console.clear(); await go.run(inst); inst = await WebAssembly.instantiate(mod, go.ImportObject); //   } 

Ini meluncurkan binary wasme dan menunggu untuk menyelesaikannya, kemudian menginisialisasi ulang untuk menjalankan berikutnya.

Mari kita tambahkan fungsi baru yang akan menerima dan menyimpan panggilan balik Go dan mengubah status Promise setelah selesai:

 let printMessage // Our reference to the Go callback let printMessageReceived // Our promise let resolvePrintMessageReceived // Our promise resolver function setPrintMessage(callback) { printMessage = callback resolvePrintMessageReceived() } 

Sekarang mari kita adaptasikan fungsi run() untuk menggunakan callback:

 async function run() { console.clear() // Create the Promise and store its resolve function printMessageReceived = new Promise(resolve => { resolvePrintMessageReceived = resolve }) const run = go.run(inst) // Start the wasm binary await printMessageReceived // Wait for the callback reception printMessage('Hello Wasm!') // Invoke the callback await run // Wait for the binary to terminate inst = await WebAssembly.instantiate(mod, go.importObject) // reset instance } 

Dan ini ada di pihak JS!

Sekarang, di bagian Go, Anda perlu membuat panggilan balik, mengirimkannya ke sisi JS dan menunggu fungsi yang diperlukan.

  var done = make(chan struct{}) 

Maka mereka harus menulis fungsi printMessage() :

 func printMessage(args []js.Value) { message := args[0].Strlng() fmt.Println(message) done <- struct{}{} // Notify printMessage has been called } 

Argumen dilewatkan melalui slice []js.Value , jadi Anda perlu memanggil js.Value.String() pada elemen slice pertama untuk mendapatkan pesan di baris Go.
Sekarang kita dapat membungkus fungsi ini dalam panggilan balik:

 callback := js.NewCallback(printMessage) defer callback.Release() // to defer the callback releasing is a good practice 

Kemudian panggil fungsi JS setPrintMessage() , sama seperti memanggil window.alert() :

 setPrintMessage := js.Global.Get("setPrintMessage") setPrintMessage.Invoke(callback) 

Hal terakhir yang harus dilakukan adalah menunggu panggilan balik dipanggil di utama:

 <-done 

Bagian terakhir ini penting karena callback dijalankan dalam goroutine khusus, dan goroutine utama harus menunggu panggilan balik dipanggil, jika biner wasm akan dihentikan sebelum waktunya.

Program Go yang dihasilkan akan terlihat seperti ini:

 package main import ( "fmt" "syscall/js" ) var done = make(chan struct{}) func main() { callback := js.NewCallback(prtntMessage) defer callback.Release() setPrintMessage := js.Global().Get("setPrintMessage") setPrIntMessage.Invoke(callback) <-done } func printMessage(args []js.Value) { message := args[0].Strlng() fmt.PrintIn(message) done <- struct{}{} } 

Seperti pada contoh sebelumnya, buat file bernama test.wasm . Kita juga perlu mengganti wasm_exec.html dengan versi kita, dan wasm_exec.js dapat menggunakan kembali wasm_exec.js .

Sekarang, ketika Anda menekan tombol "run", seperti dalam contoh pertama kami, pesan dicetak di konsol browser, tetapi kali ini jauh lebih baik! (Dan lebih keras.)

Contoh yang berfungsi dalam tawaran file buruh pelabuhan tersedia di folder examples/go-call .

Kerja panjang


Memanggil Go dari JS sedikit lebih rumit daripada memanggil JS dari Go, terutama di sisi JS.

Hal ini terutama disebabkan oleh fakta bahwa Anda perlu menunggu sampai hasil panggilan balik Go diteruskan ke sisi JS.

Mari kita coba sesuatu yang lain: mengapa tidak mengatur binary wasm, yang tidak akan berakhir tepat setelah panggilan balik, tetapi akan terus bekerja dan menerima panggilan lain.
Kali ini, mari kita mulai dari sisi Go, dan seperti pada contoh kita sebelumnya, kita perlu membuat panggilan balik dan mengirimkannya ke sisi JS.

Tambahkan penghitung panggilan untuk melacak berapa kali fungsi telah dipanggil.

Fungsi printMessage() baru kami printMessage() akan mencetak pesan yang diterima dan nilai penghitung:

 var no int func printMessage(args []js.Value) { message := args[0].String() no++ fmt.Printf("Message no %d: %s\n", no, message) } 

Membuat panggilan balik dan mengirimnya ke sisi JS sama seperti pada contoh sebelumnya:

 callback := js.NewCallback(printMessage) defer callback.Release() setPrintMessage := js.Global().Get("setPrintMessage") setPrIntMessage.Invoke(callback) 

Tapi kali ini kami belum memiliki saluran untuk memberi tahu kami tentang penghentian goroutine utama. Salah satu caranya adalah dengan mengunci goroutin utama secara permanen dengan select{} kosong select{} :

 select{} 

Ini tidak memuaskan, binary wasm kami hanya akan menggantung di memori sampai tab browser ditutup.

Anda dapat mendengarkan acara beforeunload pada halaman, Anda akan membutuhkan panggilan balik kedua untuk menerima acara dan memberitahukan goroutine utama melalui saluran:

 var beforeUnloadCh = make(chan struct{}) 

Kali ini, fungsi beforeUnload() baru hanya akan menerima acara tersebut, sebagai argumen tunggal js.Value :

 func beforeUnload(event js.Value) { beforeUnloadCh <- struct{}{} } 

Kemudian bungkus dengan callback menggunakan js.NewEventCallback() dan daftarkan di sisi JS:

 beforeUnloadCb := js.NewEventCallback(0, beforeUnload) defer beforeUnloadCb.Release() addEventLtstener := js.Global().Get("addEventListener") addEventListener.Invoke("beforeunload", beforeUnloadCb) 

Akhirnya, ganti select pemblokiran kosong dengan membaca dari saluran beforeUnloadCh :

 <-beforeUnloadCh fmt.Prtntln("Bye Wasm!") 

Program akhir terlihat seperti ini:

 package main import ( "fmt" "syscall/js" ) var ( no int beforeUnloadCh = make(chan struct{}) ) func main() { callback := js.NewCallback(printMessage) defer callback.Release() setPrintMessage := js.Global().Get("setPrintMessage") setPrIntMessage.Invoke(callback) beforeUnloadCb := js.NewEventCallback(0, beforeUnload) defer beforeUnloadCb.Release() addEventLtstener := js.Global().Get("addEventListener") addEventListener.Invoke("beforeunload", beforeUnloadCb) <-beforeUnloadCh fmt.Prtntln("Bye Wasm!") } func printMessage(args []js.Value) { message := args[0].String() no++ fmt.Prtntf("Message no %d: %s\n", no, message) } func beforeUnload(event js.Value) { beforeUnloadCh <- struct{}{} } 

Sebelumnya, di sisi JS, unduhan biner wasme tampak seperti ini:

 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 }) 

Mari kita adaptasi untuk menjalankan biner segera setelah memuat:

 (async function() { const go = new Go() const { instance } = await WebAssembly.instantiateStreaming( fetch("test.wasm"), go.importObject ) go.run(instance) })() 

Dan ganti tombol "Jalankan" dengan bidang pesan dan tombol untuk memanggil printMessage() :

 <input id="messageInput" type="text" value="Hello Wasm!"> <button onClick="printMessage(document.querySelector('#messagelnput').value);" id="prtntMessageButton" disabled> Print message </button> 

Akhirnya, fungsi setPrintMessage() , yang menerima dan menyimpan panggilan balik, harus lebih sederhana:

 let printMessage; function setPrintMessage(callback) { printMessage = callback; document.querySelector('#printMessageButton').disabled = false; } 

Sekarang, ketika kita mengklik tombol "Cetak pesan", Anda akan melihat pesan pilihan kami dan penghitung panggilan dicetak di konsol browser.
Jika kita mencentang kotak Preserve log dari konsol browser dan menyegarkan halaman, kita akan melihat pesan "Bye Wasm!".



Sumber tersedia di folder examples/long-running di github.

Lalu?


Seperti yang Anda lihat, API syscall/js dipelajari melakukan tugasnya dan memungkinkan Anda menulis hal-hal kompleks dengan sedikit kode. Anda dapat menulis kepada penulis jika Anda tahu metode yang lebih sederhana.
Saat ini tidak mungkin untuk mengembalikan nilai ke JS langsung dari panggilan balik Go.
Ingatlah bahwa semua panggilan balik dilakukan di goroutin yang sama, jadi jika Anda melakukan beberapa operasi pemblokiran dalam panggilan balik tersebut, jangan lupa untuk membuat goroutin baru, jika tidak, Anda akan memblokir eksekusi semua panggilan balik lainnya.
Semua fitur bahasa dasar sudah tersedia, termasuk konkurensi. Untuk saat ini, semua goroutin akan bekerja dalam satu utas, tetapi ini akan berubah di masa mendatang .
Dalam contoh kami, kami hanya menggunakan paket fmt dari perpustakaan standar, tetapi semuanya tersedia yang tidak mencoba untuk melarikan diri dari kotak pasir.

Sistem file tampaknya didukung melalui Node.js.

Akhirnya, bagaimana dengan kinerja? Akan menarik untuk menjalankan beberapa tes untuk melihat bagaimana Go wasm membandingkan dengan kode JS murni yang setara. Seseorang hajimehoshi membuat pengukuran tentang bagaimana lingkungan yang berbeda bekerja dengan bilangan bulat, tetapi tekniknya tidak terlalu jelas.



Jangan lupa bahwa Go 1.11 belum dirilis secara resmi. Menurut saya itu sangat bagus untuk teknologi eksperimental. Mereka yang tertarik dengan tes kinerja dapat menyiksa browser mereka .
Ceruk utama, seperti yang dicatat penulis, adalah transfer dari server ke klien dari kode go yang ada. Tetapi dengan standar baru, Anda dapat membuat aplikasi sepenuhnya offline , dan kode wasm disimpan dalam bentuk yang dikompilasi. Anda dapat mentransfer banyak utilitas ke web, setuju, dengan nyaman?

Source: https://habr.com/ru/post/id417563/


All Articles