مغامرات في دفق منفصل. تقرير ياندكس

كيفية العمل مع الصور على العميل ، مع الحفاظ على واجهة مستخدم سلسة؟ تحدث مطور الواجهة Pavel Smirnov عن ذلك على أساس تجربة تطوير البحث عن الصور في السوق. من التقرير ، يمكنك معرفة كيفية استخدام Web Workers و OffscreenCanvas بشكل صحيح.



- سنتحدث عن هذه المغامرة لمدة نصف ساعة. سوف أخبركم عن مغامرتي وآمل حقًا في أن يلهم تقريري لك وسوف تتخذ نفس الشيء في المنزل.

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

دعونا أعرض نفسي مرة أخرى أولاً. اسمي باشا ، أنا مطور واجهة في فريق السوق.



أنا أساسا التعامل مع واجهات المحمول - خريطة البحث ، بطاقة العرض. أقوم أيضًا بإعادة كتابة الكود من المكدس القديم إلى المكدس الجديد ، ثم من المكدس الجديد إلى المكدس الأحدث. وأنا أحاول أن أجعل واجهاتي جيدة. هنا تجدر الإشارة إلى ما واجهة جيدة.

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



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

دعنا نعود إلى المهمة. قبل بضعة أشهر ، اتصل بي Luba ، مدير منتجاتنا. تتعامل Lyuba مع مشاكل اختيار المنتج في السوق. الآن لدينا العديد من الخيارات لإيجاد السلع. واحد منهم هو إدخال شيء في شريط البحث.



على سبيل المثال ، "شراء iPhone X أحمر في سمارة". وسوف نجد شيئا. أو يمكننا استخدام شجرة الكتالوج. في هذا الكتالوج لدينا فئات وفئات فرعية.

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



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

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

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

شاهد العرض الأول

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

وجدنا بعض السلع وتحديدا هذا الشيء. ويسمى هذا الشيء مصفاة. للعثور على شيء آخر ، صورت كتابًا واحدًا على مكتب أحد الزملاء بالأمس ، دعنا نبحث عنه. إليكم هذا الكتاب ، ربما يقرأه شخص ما. يطلق عليه "رمز مثالي". يجدها أيضًا بطريقة ما ، ولسبب ما بحد أقصى 18+. ربما هذا غريب بعض الشيء.

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



كيف سنفعل هذا؟ لدينا ملف. وسوف نقرأ هذا الملف بطريقة أو بأخرى. سوف نقرأ باستخدام FileReader API. سأخبرك باختصار ما هو عليه.



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



الرمز سيبدو هكذا. لا يوجد شيء معقد هنا حتى الآن. لدينا كائن قارئ تم إنشاؤه من مُنشئ FileReader ، حيث نقوم بتعليق مطور حدث التحميل. بعد ذلك سوف نقرأ هذا الملف باسم DataURL. DataURL - سلسلة تمثل محتويات الملف المشفر من خلال Base64. كما نقرأ ، نحن بحاجة إلى قطعه بطريقة أو بأخرى. أولاً ، دعنا نحمّل كل شيء في صورة. لدينا علامة أو عنصر img ، ونحن نقوم بتحميله هناك.



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

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



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

لدينا أحجام صور ، ونحن ننظر إليها. هناك بعض الثوابت المسموح بها لنا. إذا تجاوزت أحجام الصور ثوابتنا ، فسنقوم فقط بقصها تحتها ونضبط قماشنا على نفس الأحجام.

بعد ذلك سوف نرسم صورتنا على هذا القماش.



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

علاوة على ذلك ، من هذه اللوحة ، سوف نأخذ سلسلة Base64 المشفرة من DataURL بنفس الطريقة تمامًا ونحولها إلى blob - كائن خاص مناسب لنا لإرساله إلى الخادم. يبدو أن كل شيء. كل شيء يعمل. يتم اقتصاص الصورة ، يتم إرسال الصورة ، يتم التعرف على الصورة.

ولكن بعد ذلك بدأت تلاحظ شيئا. عندما اختبرت هذا الحل ، عندما حملت صورة ، خاصة على الأجهزة الضعيفة ، تباطأت الواجهة قليلاً. إما أنه لم يتم الضغط على الزر ، ثم لم ينتقل العنصر إلى ذلك. هل شعرت أن كودك يعمل في 99 ٪ من الحالات ويعمل بشكل جيد ، ولكن في بعض الأحيان أنها لا تعمل؟ ويمكنك إعطائها للاختبار ، وربما لن يلاحظها أحد. ولن يلاحظ المستخدمون ، خاصة على الأجهزة الضعيفة.

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

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



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

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

مشاهدة التجريبي الثاني

لدي صورة GIF ورسوم CSS متحركة بطرق مختلفة: إحداها تستخدم translatex ، والآخر يستخدم الموضع: اليسار النسبي ، والثالث باستخدام JavaScript ، وهما requestAnimationFrame. هذا هو المكان القنفذ هو الغزل. ماذا أفعل؟

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

ماذا سيحدث؟ لقد لاحظت على الفور أن القنفذ توقف عن الدوران ، وأن القطة السفلية ، التي يتم تحريكها باستخدام translatex ، توقفت أيضًا عن الركوب. ولكن دعونا نرى نفس العرض التوضيحي في متصفح آخر ، على سبيل المثال Safari. توقف القط GIF على التوالي.

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

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

مشاهدة التجريبي الثالث

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

دعونا نعود بعد ذلك إلى مهمتنا ونفكر في كيفية تعاملنا مع هذا.



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

دعنا ننتقل إلى الحل. سأقول على الفور ما استخدمته وماذا فعلت ، وبعد ذلك سنحلل كل هذه الأشياء. أولاً ، لقد استخدمت عمال الويب. أنها تسمح لنا لوضع بعض المهام في موضوع منفصل. وثانيا ، في سياق Web Workers ، فإن DOM ليست متاحة لنا. للتعامل مع هذا الموقف ، سوف نستخدم أدوات أخرى. لن تكون الصورة متاحة لنا ، يتوفر Canvas الكلاسيكي ، وبالتالي نستخدم Canvas وبعض الحيل الأخرى.



دعونا نتذكر بسرعة ما العمال ، ما هم من أجل. إنها تتيح لك تشغيل JavaScript في سلسلة رسائل منفصلة ، وليس بشكل أساسي. ودفق العمال لا يتداخل مع تدفق التقديم للواجهة الرئيسية. لذلك ، يمكننا القيام ببعض المهام الحسابية المعقدة دون إبطاء واجهتنا.

لدينا أداة تسمح لك بنقل شيء ما إلى العمال وإعادة شيء من العمال. دعنا نرى مثالا.



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



ما هو الدعم؟ كل شيء جيد هنا. العمال أداة معروفة ، وليست أداة جديدة ، لكن العديد من أصدقائي يعتقدون أنها غير مدعومة دائمًا. هذا ليس كذلك.



الآن دعونا نلقي نظرة على OffscreenCanvas. كما رأينا من قبل ، فإن Canvas يمثل أداة قوية للغاية ، لكن لسوء الحظ ، لا يتوفر لنا في سياق Web Workers ، لذلك سوف نستخدم بديلاً. هذا شيء جديد إلى حد ما يسمى OffscreenCanvas. إنها تسمح لك بالقيام بنفس الأشياء مثل Canvas ، فقط بالفعل خارج الشاشة ، أي في سياق Web Workers. بالطبع ، يمكننا القيام بذلك أيضًا في سياق النافذة ، لكن الآن لن نفعل ذلك.



ما هو هناك مع الدعم؟ كما ترون ، هناك الكثير من اللون الأحمر. عادةً ما يتم دعم OffscreenCanvas فقط في Chrome. يوجد أيضًا خيار في Firefox ، لكن حتى الآن توجد علامة ، ويعمل Canvas فقط مع سياق WebGL. هنا يمكنك أن تسأل - لماذا أتحدث عن شيء رائع مثل OffscreenCanvas ، والذي لا يعمل في أي مكان؟



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

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



فيما يلي رسم تخطيطي لما سنفعله. لدينا حتى الملفات التي سنقرأها من خلال FileReader. ولكن في الدفق الرئيسي ، سنرسله إلى عمال الويب ، حيث سيتم قصه وضغطه وإعادته إلينا ، وسنرسله بالفعل إلى الخادم.



دعونا نرى رمز العامل لدينا. أولاً ، نقوم بإنشاء مثيل OffscreenCanvas بالعرض والارتفاع الذي نحتاجه.

علاوة على ذلك ، كما قلت ، لا يتوفر عنصر الصورة لنا في سياق "العمال" ، لذلك نحن هنا نستخدم طريقة createImageBitmap ، والتي سوف تجعلنا هيكل البيانات الذي يميز صورتنا.

من المثير للاهتمام: نرى هنا النفس. أولئك الذين ليسوا على دراية بعمال الويب ، يشير هذا الشيء إلى سياق التنفيذ. لا يهمنا هنا ، النافذة أو هذا ، نحن نستخدم الذات. هذه الطريقة غير متزامنة ، لقد استخدمت في انتظار هنا للاكتناز والراحة ، لماذا لا؟

بعد ذلك ، نحصل على نفس الصورة ونفعل نفس الشيء الذي فعلناه من قبل. رسم على قماش والعودة.

من البسيط. اعتدنا أن نأخذ DataURL ونحول كل شيء إلى نقطة. ولكن هنا طريقة convertToBlob متاحة على الفور لنا. لماذا لم استخدمها من قبل؟ لأن الدعم كان أسوأ. ولكن بما أننا ذهبنا إلى هنا واستخدام OffscreenCanvas ، فما الذي يمنعنا من استخدام convertToBlob؟



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

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

دعنا نعود إلى العرض التوضيحي الخاص بنا.

مشاهدة التجريبي الرابع

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

ولكن هل يمكن تحسين هذا القرار؟



هنا ، بالمناسبة ، التعريف. هنا لا نرى المهام الصغيرة الضخمة للخمس ثوان التي رأيناها من قبل.

تحسين ممكن. باستخدام كائنات قابلة للتحويل. هنا يستحق العودة مرة أخرى. عندما مررنا DataURL أو النقطة من خلال آلية postMessage ، قمنا بنسخ هذه البيانات. ربما هذا ليس فعالا للغاية. سيكون باردا لتجنب ذلك. لذلك ، لدينا آلية تسمح لك بنقل البيانات إلى Web Workers كما لو كانت في حزمة.

لماذا أقول "أعجبني"؟ عندما ننقل هذه البيانات إلى العمال ، فإننا نفقد السيطرة عليها في التدفق الرئيسي - لا يمكننا التفاعل معهم بأي طريقة. هناك قيود ثانية هنا. لا يمكننا نقل جميع أنواع البيانات إلى عمال الويب. لا يمكننا القيام بذلك باستخدام سلسلة ؛ سنفعل ذلك بطريقة مختلفة.



لنلقِ نظرة على الكود أولاً ، نقوم بنقل البيانات بشكل مختلف قليلاً. هنا هو postMessage لدينا. كما ترى ، يوجد مثل هذا الصفيف مع loadEvent.target.result. تسمح لنا هذه الواجهة بنقل بياناتنا كأشياء قابلة للنقل ، وفقدان السيطرة عليها.

بالمناسبة ، فإن أي شخص يكتب في رست ربما يسمع شيئًا مألوفًا. وسوف نقرأ ملفنا ليس كسلسلة ، ولكن ك ArrayBuffer. هذا دفق من البيانات الثنائية lidar التي لا يوجد الوصول المباشر إليها. لذلك ، سيتعين علينا القيام بشيء آخر معهم.



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

الوسيطة الثالثة ، نمر بمثل هذا الشيء الغريب ، مثل العرض ، مضروبًا في الارتفاع ، مضروبًا في أربعة. لماذا بالضبط أربعة؟ بالضبط ، RGBA. هناك ثلاث قيم لكل لون وواحدة لكل قناة ألفا.

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

دعنا ننتقل إلى الاستنتاجات. لقد أخبرتك اليوم عن مهمة واحدة لم أقوم بها منذ وقت طويل. ماذا لاحظت في ذلك؟



نعومة الواجهة مهمة جدا. عندما يتأخر المستخدم قليلاً ، يتجمد قليلاً ، لا يتم ضغط الزر ، مما قد يؤدي إلى تدهور شديد في UX. المتصفحات تعمل بشكل مختلف. نظرنا إلى مثال كروي مع Safari و Yandex.Browser. نرى أنه إذا قمت بفحص الواجهة الخاصة بك للتأكد من سلاستها في أحد المستعرضات ، فينبغي أن تنظر إلى الآخرين.

تحتاج إلى عمل شيء ما مع حظر البرامج النصية إذا استمرت لفترة طويلة. في حالتي ، أضعها على عمال الويب. ولكن من المحتمل أن تكون هناك طرق أخرى ، يمكنك تقسيمها بطريقة أو بأخرى إلى أساليب أصغر ، وهنا عليك التفكير. , Web Workers, .

? . . . , 200 , .

Web Workers . , , .

:


.

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


All Articles