اعمل مع العامل "كما يحلو لك" وليس "قدر المستطاع"

سوف تستخدم هذه المقالة طريقة DIRTY ، غير الآمنة ، "المزيفة" ، المخيفة ، إلخ. باهتة القلب لا تقرأ!


يجب أن أقول على الفور أنه لم يكن من الممكن حل بعض مشكلات قابلية الاستخدام: لا يمكنك استخدام الإغلاق في الكود الذي سيتم تمريره إلى العامل.
العمل مع العامل "كما يحلو لك" ، ولكن ليس "قدر الإمكان"


نحب جميعًا التقنيات الحديثة ، ونحبها عندما يكون من المناسب استخدام هذه التقنيات. لكن في حالة العامل ، هذا ليس صحيحًا تمامًا. يعمل العامل مع ملف أو رابط ملف ، لكن هذا غير مريح. أود أن أكون قادرًا على وضع أي مهمة في العامل ، وليس فقط الكود المخطط له خصيصًا.


ما هو المطلوب لجعل العمل مع العامل أكثر ملاءمة؟ في رأيي ، ما يلي:


  • القدرة على تشغيل قانون التعسفي في عامل في أي وقت
  • القدرة على تمرير البيانات المعقدة إلى العامل (مثيلات الفصل ، الدوال)
  • القدرة على الحصول على وعد مع استجابة من العامل.

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


  • إضافة المكتبات / الملفات إلى العامل
  • إطلاق العمل

ملف داخل العامل


قبل البدء في إنشاء عامل ، تحتاج إلى وصف ملف يعمل في عامل ويدعم البروتوكول الذي وصفناه. أنا أحب OOP ، لذلك ستكون فئة تسمى WorkerBody. يجب أن يشترك هذا الفصل في الحدث من النافذة الأصل.


 self.onmessage = (message) => { this.onMessage(message.data); }; 

الآن يمكننا الاستماع إلى الأحداث من النافذة الأصل. لدينا نوعان من الأحداث: تلك التي يتضمنها الجواب ، وكل الأحداث الأخرى. نحن نعالج الأحداث.
تتم إضافة المكتبات والملفات إلى العامل باستخدام importScripts API.


وأسوأ شيء: سوف نستخدم eval لتشغيل وظيفة تعسفية.


 ... onMessage(message) { switch (message.type) { case MESSAGE_TYPE.ADD_LIBS: this.addLibs(message.libs); break; case MESSAGE_TYPE.WORK: this.doWork(message); break; } } doWork(message) { try { const processor = eval(message.job); const params = this._parser.parse(message.params); const result = processor(params); if (result && result.then && typeof result.then === 'function') { result.then((data) => { this.send({ id: message.id, state: true, body: data }); }, (error) => { if (error instanceof Error) { error = String(error); } this.send({ id: message.id, state: false, body: error }); }); } else { this.send({ id: message.id, state: true, body: result }); } } catch (e) { this.send({ id: message.id, state: false, body: String(e) }); } } send(data) { data.body = this._serializer.serialize(data.body); try { self.postMessage(data); } catch (e) { const toSet = { id: data.id, state: false, body: String(e) }; self.postMessage(toSet); } } 

طريقة onMessage المسؤولة عن تلقي الرسالة وتحديد المعالج ، doWork - تبدأ الوظيفة التي تم تمريرها ، send يرسل استجابة إلى الإطار الأصل.


المحلل اللغوي والمسلسل


الآن بما أن لدينا محتويات العامل ، نحتاج إلى معرفة كيفية إجراء تسلسل وتحليل أي بيانات لتمريرها إلى العامل. لنبدأ مع المسلسل. نريد أن نكون قادرين على نقل أي بيانات إلى العامل ، بما في ذلك مثيلات الفصل ، والفصول ، والوظائف. ولكن مع الميزات الأصلية للعامل ، يمكننا فقط نقل البيانات المشابهة لـ JSON. للتغلب على هذا الحظر ، نحن بحاجة إلى eval . كل ما لا يقبل JSON ، سنلتف في إنشاءات السلسلة المقابلة ونعمل على الجانب الآخر. للحفاظ على القابلية للتغير ، يتم استنساخ البيانات التي تم الحصول عليها فورًا ، ولا يمكن استبدال الكائنات التي لا يمكن تسلسلها بالطريقة المعتادة بكائنات الخدمة ، ويتم استبدالها في المقابل بالمحلل على الجانب الآخر. للوهلة الأولى ، قد يبدو أن هذه المهمة ليست صعبة ، ولكن هناك العديد من المزالق. أسوأ قيود هذا النهج هو عدم القدرة على استخدام الإغلاق ، والذي يحمل نمطًا مختلفًا قليلاً من كود الكتابة. لنبدأ بالأبسط ، مع الوظيفة. تحتاج أولاً إلى معرفة كيفية التمييز بين الوظيفة وبين مُنشئ الفصل.


دعنا نحاول التمييز:


 static isFunction(Factory){ if (!Factory.prototype) { // Arrow function has no prototype return true; } const prototypePropsLength = Object.getOwnPropertyNames(Factory.prototype) .filter(item => item !== 'constructor') .length; return prototypePropsLength === 0 && Serializer.getClassParents(Factory).length === 1; } static getClassParents(Factory) { const result = [Factory]; let tmp = Factory; let item = Object.getPrototypeOf(tmp); while (item.prototype) { result.push(item); tmp = item; item = Object.getPrototypeOf(tmp); } return result.reverse(); } 

أولاً ، سنكتشف ما إذا كانت الوظيفة تحتوي على نموذج أولي. إذا لم يكن كذلك ، فهذه وظيفة بالتأكيد. بعد ذلك ، ننظر إلى عدد الخصائص في النموذج الأولي ، وإذا لم يكن المُنشئ والوظيفة في ورثة فئة أخرى سوى الورثة لفئة أخرى ، فإننا نعتبر ذلك دالة.


بعد العثور على دالة ، استبدلناها ببساطة بكائن خدمة بالحقول __type = "serialized-function" template ، وهو مساوٍ لقالب هذه الوظيفة ( func.toString() ).


الآن ، تخطي الفصل وتحليل مثيل الفصل. علاوة على ذلك ، نحتاج إلى تمييز الأشياء العادية عن مثيلات الفصل.


 static isInstance(some) { const constructor = some.constructor; if (!constructor) { return false; } return !Serializer.isNative(constructor); } static isNative(data) { return /function .*?\(\) \{ \[native code\] \}/.test(data.toString()); } 

نعتبر أن كائنًا عاديًا إذا لم يكن له مُنشئ أو كان مُنشئه وظيفة أصلية. بعد تحديد مثيل الفئة ، نستبدلها بكائن خدمة مع الحقول:


  • __type - "مثيل تسلسلي"
  • data - البيانات التي كانت في المثال
  • index - index الفئة لهذا المثيل في قائمة خدمة الفئة.

لنقل البيانات ، نحتاج إلى إنشاء حقل إضافي: حيث سنقوم بتخزين قائمة بجميع الفئات الفريدة التي نجتازها. الجزء الأكثر صعوبة هو أنه عندما يتم الكشف عن فئة ما ، لا تأخذ فقط قالبها ، بل أيضًا قالب جميع الفئات الرئيسية وحفظها كصفوف منفصلة - بحيث لا يتم تمرير كل "أصل" أكثر من مرة واحدة - وحفظ التحقق من مثيله. من السهل تحديد فئة: هذه وظيفة لم تنجح في اختبار Serializer.isFunction الخاص بنا. عند إضافة فئة ، نتحقق من وجود مثل هذه الفئة في قائمة البيانات المتسلسلة ونضيف فقط بيانات فريدة. الرمز الذي يجمع الفصل في قالب كبير جدًا ويكمن هنا .


في المحلل اللغوي ، نلتف أولاً بجميع الطبقات التي تم تمريرها إلينا وتجميعها إذا لم يتم اجتيازها من قبل. بعد ذلك ، نعبر كل حقل بيانات بشكل متكرر ونستبدل كائنات الخدمة ببيانات مترجمة. الشيء الأكثر إثارة للاهتمام هو في المثال الفئة. لدينا فئة وهناك بيانات موجودة في المثال الخاص به ، لكن لا يمكننا إنشاء مثيل لها ، لأن الدعوة إلى المُنشئ قد تحتوي على معلمات ليست لدينا. تأتي أداة Object.create المنسية تقريبًا في مساعدتنا ، والتي تُرجع كائنًا بنموذج أولي محدد. لذلك نتجنب استدعاء المنشئ والحصول على مثيل للفئة ، ثم أعد كتابة الخصائص في المثيل.


عامل الخلق


لكي يعمل العامل بنجاح ، نحتاج إلى أن يكون لدينا محلل ومسلسل داخل العامل وخارجه ، لذلك نأخذ المسلسل ونحول المتسلل ومحلل ونص العامل إلى قالب. نقوم بإنشاء نقطة من القالب وإنشاء رابط تنزيل عبر URL.createObjectURL (قد لا تعمل هذه الطريقة مع بعض "سياسة أمان المحتوى"). هذه الطريقة مناسبة أيضًا لتشغيل تعليمات برمجية عشوائية من السلسلة.


 _createWorker(customWorker) { const template = `var MyWorker = ${this._createTemplate(customWorker)};`; const blob = new Blob([template], { type: 'application/javascript' }); return new Worker(URL.createObjectURL(blob)); } _createTemplate(WorkerBody) { const Name = Serializer.getFnName(WorkerBody); if (!Name) { throw new Error('Unnamed Worker Body class! Please add name to Worker Body class!'); } return [ '(function () {', this._getFullClassTemplate(Serializer, 'Serializer'), this._getFullClassTemplate(Parser, 'Parser'), this._getFullClassTemplate(WorkerBody, 'WorkerBody'), `return new WorkerBody(Serializer, Parser)})();` ].join('\n'); } 

يؤدي


وبالتالي ، حصلنا على مكتبة سهلة الاستخدام يمكنها تشغيل تعليمات برمجية عشوائية في العامل. وهو يدعم الطبقات من TypeScript. على سبيل المثال:


 const wrapper = workerWrapper.create(); wrapper.process((params) => { // This code in worker. Cannot use closure! // do some hard work return 100; // or return Promise.resolve(100) }, params).then((result) => { // result = 100; }); wrapper.terminate() // terminate for kill worker process 

خطط أخرى


هذه المكتبة ، للأسف ، ليست مثالية. من الضروري إضافة دعم لأجهزة التسوية والحروف على الفئات والكائنات والنماذج الأولية والخصائص الثابتة. نود أيضًا إضافة التخزين المؤقت ، وتشغيل برنامج نصي بديل دون eval عبر URL.createObjectURL وإضافة ملف به محتويات العامل إلى التجميع (إذا لم يكن الإنشاء متاحًا في الحال). تعال إلى المستودع !

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


All Articles