بفضل WebAssembly ، يمكنك كتابة Frontend on Go

المقالة الأصلية .

في فبراير 2017 ، اقترح أحد أعضاء فريق go Brad Fitzpatrick تقديم دعم WebAssembly باللغة. بعد أربعة أشهر ، في نوفمبر 2017 ، بدأ مؤلف GopherJS ريتشارد Muziol في تنفيذ الفكرة. وأخيرًا ، تم العثور على التنفيذ الكامل في الماجستير. سيتلقى المطورون خدمة wasm في أغسطس 2018 تقريبًا ، مع إصدار go 1.11 . نتيجة لذلك ، تواجه المكتبة القياسية جميع الصعوبات التقنية تقريبًا في استيراد وتصدير الوظائف المألوفة لك إذا كنت قد حاولت بالفعل تجميع C في wasm. يبدو ذلك واعدًا. دعونا نرى ما يمكن القيام به مع الإصدار الأول.



يمكن إطلاق جميع الأمثلة في هذه المقالة من حاويات عمال السفن الموجودة في مستودع المؤلف :

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

ثم انتقل إلى localhost : 32XXX / وانتقل من رابط إلى آخر.

مرحبًا يا وسيم!


إن إنشاء "عالم الترحيب" الأساسي والمفهوم موثقان جيدًا بالفعل (حتى بالروسية ) ، لذا دعنا ننتقل إلى الأشياء الأكثر دقة.

الأكثر أهمية هو نسخة مجمعة حديثًا من Go تدعم wasm. لن أشرح خطوة بخطوة التثبيت ، فقط أعرف أن ما هو مطلوب بالفعل في الماجستير.

إذا كنت لا تريد أن تقلق بشأن هذا الأمر ، فإن Dockerfile c go متوفر في مستودع golub-wasm على github ، أو أسرع يمكنك التقاط صورة من nlepage / golang_wasm .

الآن يمكنك كتابة helloworld.go التقليدي helloworld.go باستخدام الأمر التالي:

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

تم تعيين متغيرات البيئة GOOS و GOARCH بالفعل في صورة nlepage / golang_wasm ، بحيث يمكنك استخدام ملف Dockerfile مثل هذا لتجميع:

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

الخطوة الأخيرة هي استخدام ملفات wasm_exec.js و wasm_exec.js المتوفرة في مستودع go في دليل misc/wasm أو في nlepage / golang_wasm في صورة docker في الدليل /usr/local/go/misc/wasm/ لتشغيل test.wasm في المستعرض (wasm_exec.js يتوقع اختبار الملف الثنائي. test.wasm ، لذلك نستخدم هذا الاسم).
تحتاج فقط إلى إعطاء 3 ملفات ثابتة باستخدام nginx ، على سبيل المثال ، ثم سيقوم wasm_exec.html بعرض زر "تشغيل" (سيتم تشغيله فقط إذا test.wasm تحميل test.wasm بشكل صحيح).

من الجدير بالذكر أن test.wasm يجب أن يتم تقديمه مع application/wasm نوع MIME ، وإلا فإن المستعرض سيرفض تنفيذه. (على سبيل المثال ، يحتاج nginx إلى ملف mime.types محدث ).

يمكنك استخدام صورة nginx من nlepage / golang_wasm ، والتي تتضمن بالفعل نوع MIME الثابت ، wasm_exec.html و wasm_exec.js في الكود> / usr / share / nginx / html / directory.

الآن انقر فوق الزر "تشغيل" ، ثم افتح وحدة تحكم المستعرض الخاص بك وسترى تحية وحدة التحكم. blog ("Hello Wasm!").


يتوفر مثال كامل هنا .

استدعاء JS من Go


الآن بعد أن أطلقنا بنجاح أول ثنائي WebAssembly الذي تم تجميعه من Go ، دعنا نلقي نظرة فاحصة على الميزات المقدمة.

تمت إضافة حزمة syscall / js الجديدة إلى المكتبة القياسية. ضع في الاعتبار الملف الرئيسي ، js.go
يتوفر نوع js.Value جديد يمثل قيمة JavaScript.

يوفر واجهة برمجة تطبيقات بسيطة لإدارة متغيرات جافا سكريبت:

  • js.Value.Get() و js.Value.Set() وتعيين قيم الحقول للكائن.
  • js.Value.Index() و js.Value.SetIndex() إلى الكائن بواسطة فهرس القراءة والكتابة.
  • js.Value.Call() أسلوب الكائن كدالة.
  • js.Value.Invoke() الكائن نفسه كدالة.
  • js.Value.New() عامل التشغيل الجديد ويستخدم معرفته الخاصة js.Value.New() .
  • بعض الطرق الأخرى للحصول على قيمة JavaScript في نوع Go المطابق ، على سبيل المثال js.Value.Int() أو js.Value.Bool() .

وطرق إضافية مثيرة للاهتمام:

  • js.Undefined() ستعطي js.Value المقابل undefined .
  • js.Null() js.Value null المقابلة.
  • js.Global() js.Value مما يتيح الوصول إلى النطاق العالمي.
  • يقبل js.ValueOf() أنواع Go البدائية js.Value الصحيح

بدلاً من عرض الرسالة في os.StdOut ، دعنا نعرضها في نافذة الإعلام باستخدام window.alert() .

نظرًا لأننا في المتصفح ، فإن النطاق العالمي هو نافذة ، لذا تحتاج أولاً إلى الحصول على تنبيه () من النطاق العالمي:

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

الآن لدينا متغير alert ، في شكل js.Value ، وهو مرجع إلى window.alert JS ، ويمكنك استخدام الوظيفة لاستدعاء js.Value.Invoke() :

 alert.Invoke("Hello wasm!") 

كما ترى ، ليست هناك حاجة لاستدعاء js.ValueOf () قبل تمرير الوسيطات إلى Invoke ، يستغرق الأمر كمية عشوائية من interface{} ويمرر القيم من خلال ValueOf نفسها.

الآن يجب أن يبدو برنامجنا الجديد كما يلي:

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

كما في المثال الأول ، ما عليك سوى إنشاء ملف يسمى test.wasm ، وترك wasm_exec.html و wasm_exec.js كما كان.
الآن ، عندما نضغط على الزر "تشغيل" ، تظهر نافذة تنبيه مع رسالتنا.

يوجد مثال عملي في مجلد examples/js-call .

اتصل بـ Go من JS.


يعد استدعاء JS من Go أمرًا بسيطًا جدًا ، فلنلقِ نظرة أقرب على حزمة syscall/js ، الملف الثاني الذي syscall/js هو callback.go .

  • نوع غلاف js.Callback لوظيفة Go ، للاستخدام في JS.
  • js.NewCallback() وظيفة تأخذ دالة (تقبل شريحة js.Value ولا تُرجع شيئًا) ، وتعيد js.Callback .
  • بعض الآليات لإدارة عمليات الاسترجاعات النشطة و js.Callback.Release() ، والتي يجب استدعاؤها لتدمير الاستدعاء.
  • js.NewEventCallback() js.NewCallback() ، ولكن الدالة الملتفة تقبل وسيطة واحدة فقط - حدث.

دعنا نحاول القيام بشيء بسيط: قم بتشغيل Go fmt.Println() من جانب JS.

wasm_exec.html بعض التغييرات على wasm_exec.html حتى نتمكن من الحصول على رد من Go للاتصال به.

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

يؤدي هذا إلى تشغيل برنامج wasm ثنائي وينتظر اكتماله ، ثم يعيد تكوينه للمرحلة التالية.

دعنا نضيف وظيفة جديدة ستتلقى وحفظ استدعاء Go وتغيير حالة Promise عند الانتهاء:

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

الآن دعنا نعدل وظيفة run() لاستخدام رد الاتصال:

 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 } 

وهذا على جانب شبيبة!

الآن ، في الجزء Go ، تحتاج إلى إنشاء رد اتصال وإرساله إلى جانب JS وانتظر الوظيفة المطلوبة.

  var done = make(chan struct{}) 

ثم يجب أن يكتبوا الوظيفة الحقيقية printMessage() :

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

يتم تمرير الوسيطات عبر الشريحة []js.Value ، لذلك تحتاج إلى استدعاء js.Value.String() على عنصر الشريحة الأول للحصول على الرسالة في سطر Go.
الآن يمكننا أن نلف هذه الوظيفة في رد اتصال:

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

ثم استدعاء دالة JS setPrintMessage() ، تمامًا مثل استدعاء window.alert() :

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

آخر ما عليك فعله هو انتظار استدعاء الاستدعاء بشكل رئيسي:

 <-done 

هذا الجزء الأخير مهم لأن عمليات الاستدعاء يتم تنفيذها في جوروتين مخصص ، ويجب أن ينتظر goroutine الرئيسي لاستدعاء الاستدعاء ، وإلا سيتم إيقاف wasm ثنائي قبل الأوان.

يجب أن يبدو برنامج Go الناتج كما يلي:

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

كما في الأمثلة السابقة ، قم بإنشاء ملف يسمى test.wasm . نحتاج أيضًا إلى استبدال wasm_exec.html ، wasm_exec.js إعادة استخدام wasm_exec.js .

الآن ، عندما تضغط على زر "تشغيل" ، كما في المثال الأول ، تتم طباعة الرسالة في وحدة تحكم المتصفح ، ولكن هذه المرة أفضل بكثير! (وأصعب.)

يتوفر مثال عملي في عرض ملف docker في مجلد examples/go-call .

عمل طويل


Calling Go من JS أكثر تعقيدًا من استدعاء JS من Go ، خاصةً على جانب JS.

ويرجع ذلك أساسًا إلى حقيقة أنك تحتاج إلى الانتظار حتى يتم تمرير نتيجة رد الاتصال Go إلى جانب JS.

دعنا نجرب شيئًا آخر: لماذا لا تنظم ثنائي wasm ، والذي لن ينتهي مباشرة بعد مكالمة الاستدعاء ، ولكنه سيستمر في العمل وقبول المكالمات الأخرى.
هذه المرة ، لنبدأ من جانب Go ، وكما هو الحال في المثال السابق ، نحتاج إلى إنشاء رد اتصال وإرساله إلى جانب JS.

أضف عداد مكالمات لتتبع عدد مرات استدعاء الوظيفة.

printMessage() الوظيفة الجديدة printMessage() بطباعة الرسالة المستلمة وقيمة العداد:

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

إنشاء رد اتصال وإرساله إلى جانب JS هو نفسه كما في المثال السابق:

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

لكن هذه المرة ليس لدينا قناة لإبلاغنا بإنهاء الغوروتين الرئيسي. يمكن أن تكون إحدى الطرق هي قفل goroutin الرئيسي بشكل دائم مع select{} فارغ select{} :

 select{} 

هذا ليس مرضيا ، سوف يتم تعليق أجهزة الغسل الثنائية في الذاكرة حتى يتم إغلاق علامة تبويب المتصفح.

يمكنك الاستماع إلى حدث ما beforeunload على الصفحة ، ستحتاج إلى رد اتصال ثانٍ لتلقي الحدث وإخطار المغني الرئيسي عبر القناة:

 var beforeUnloadCh = make(chan struct{}) 

هذه المرة ، beforeUnload() الدالة beforeUnload() الجديدة beforeUnload() فقط ، كوسيطة js.Value واحدة:

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

ثم js.NewEventCallback() في رد js.NewEventCallback() باستخدام js.NewEventCallback() وقم بتسجيله على جانب JS:

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

أخيرًا ، beforeUnloadCh select الحظر الفارغ بقراءة من قناة beforeUnloadCh :

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

يبدو البرنامج النهائي كالتالي:

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

سابقًا ، على جانب JS ، بدا تنزيل برنامج wasm الثنائي كما يلي:

 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() { const go = new Go() const { instance } = await WebAssembly.instantiateStreaming( fetch("test.wasm"), go.importObject ) go.run(instance) })() 

واستبدل الزر "تشغيل" بحقل رسالة وزر لاستدعاء printMessage() :

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

وأخيرًا ، يجب أن تكون وظيفة setPrintMessage() ، التي تقبل رد الاتصال setPrintMessage() ، أبسط:

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

الآن ، عند النقر فوق الزر "طباعة رسالة" ، من المفترض أن ترى رسالة من اختيارنا وعداد مكالمات مطبوعًا في وحدة تحكم المستعرض.
إذا حددنا مربع الحفاظ على سجل وحدة التحكم في المتصفح وقمنا بتحديث الصفحة ، فسوف نرى الرسالة "Bye Wasm!".



المصادر متاحة في examples/long-running مجلد examples/long-running على جيثب.

وبعد ذلك؟


كما ترون ، فإن syscall/js API المستفادة تقوم بعملها وتسمح لك بكتابة أشياء معقدة مع القليل من التعليمات البرمجية. يمكنك الكتابة إلى المؤلف إذا كنت تعرف طريقة أبسط.
لا يمكن حاليًا إرجاع قيمة إلى JS مباشرةً من رد اتصال Go.
ضع في اعتبارك أن جميع عمليات رد الاتصال يتم تنفيذها في نفس goroutin ، لذلك إذا قمت ببعض عمليات الحظر في رد الاتصال ، فلا تنس إنشاء goroutin جديدًا ، وإلا فإنك ستحظر تنفيذ جميع عمليات رد الاتصال الأخرى.
جميع ميزات اللغة الأساسية متاحة بالفعل ، بما في ذلك التزامن. في الوقت الحالي ، ستعمل جميع goroutins في خيط واحد ، ولكن هذا سيتغير في المستقبل .
في أمثلةنا ، استخدمنا فقط حزمة fmt من المكتبة القياسية ، ولكن كل شيء متاح لا يحاول الهروب من وضع الحماية.

يبدو أن نظام الملفات مدعوم من خلال Node.js.

أخيرًا ، ماذا عن الأداء؟ سيكون من المثير للاهتمام إجراء بعض الاختبارات لمعرفة كيفية مقارنة Go wasm برمز JS النقي المكافئ. قام شخص hajimehoshi بقياسات لكيفية عمل البيئات المختلفة مع الأعداد الصحيحة ، لكن التقنية ليست واضحة جدًا.



لا تنس أن Go 1.11 لم يتم إصداره رسميًا بعد. في رأيي أنها جيدة جداً للتكنولوجيا التجريبية. أولئك الذين يهتمون باختبارات الأداء يمكنهم تعذيب متصفحهم .
المكان الرئيسي ، كما يلاحظ المؤلف ، هو النقل من الخادم إلى العميل لرمز go الحالي. ولكن باستخدام معايير جديدة ، يمكنك إنشاء تطبيقات غير متصلة تمامًا ، ويتم حفظ رمز wasm في شكل مترجم. يمكنك نقل العديد من المرافق إلى الويب ، توافق ، بشكل ملائم؟

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


All Articles