محاكاة React و JSX في Vanilla JS

نشرت: 2022-03-11

قليل من الناس يكرهون الأطر ، ولكن حتى لو كنت واحدًا منهم ، يجب عليك تدوين الملاحظات واعتماد الميزات التي تجعل الحياة أسهل قليلاً.

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

على الرغم من أنني ما زلت أفضل حرية عدم التعلق بإطار عمل أو بنية ، إلا أنني لا أستطيع تجاهل حقيقة أن إنشاء عناصر DOM في واحد هو أكثر ملاءمة. لذلك بدأت في البحث عن طرق لمحاكاة التجربة في Vanilla JS. هدفي هو استخراج بعض هذه الأفكار من React وإثبات كيف يمكن تنفيذ نفس المبادئ في JavaScript عادي (يشار إليه غالبًا باسم Vanilla JS) لتسهيل حياة المطور قليلاً. لتحقيق ذلك ، دعنا نبني تطبيقًا بسيطًا لتصفح مشاريع GitHub.

تطبيق بحث GitHub بسيط

التطبيق الذي نقوم ببنائه.

أيًا كانت الطريقة التي نبني بها واجهة أمامية باستخدام JavaScript ، فسنقوم بالوصول إلى DOM ومعالجته. بالنسبة لتطبيقنا ، سنحتاج إلى إنشاء تمثيل لكل مستودع (الصورة المصغرة والاسم والوصف) ، وإضافته إلى DOM كعنصر قائمة. سنستخدم GitHub Search API لجلب نتائجنا. وبما أننا نتحدث عن JavaScript ، فلنبحث في مستودعات JavaScript. عندما نستعلم عن واجهة برمجة التطبيقات ، نحصل على استجابة JSON التالية:

 { "total_count": 398819, "incomplete_results": false, "items": [ { "id": 28457823, "name": "freeCodeCamp", "full_name": "freeCodeCamp/freeCodeCamp", "owner": { "login": "freeCodeCamp", "id": 9892522, "avatar_url": "https://avatars0.githubusercontent.com/u/9892522?v=4", "gravatar_id": "", "url": "https://api.github.com/users/freeCodeCamp", "site_admin": false }, "private": false, "html_url": "https://github.com/freeCodeCamp/freeCodeCamp", "description": "The https://freeCodeCamp.org open source codebase "+ "and curriculum. Learn to code and help nonprofits.", // more omitted information }, //... ] }

نهج React

تجعل React كتابة عناصر HTML في الصفحة أمرًا سهلاً للغاية وهي إحدى الميزات التي طالما رغبت في الحصول عليها أثناء كتابة المكونات في JavaScript خالص. تستخدم React لغة JSX ، والتي تشبه إلى حد بعيد لغة HTML العادية.

ومع ذلك ، هذا ليس ما يقرأه المتصفح.

تحت الغطاء ، يحول React JSX إلى مجموعة من الاستدعاءات لوظيفة React.createElement . دعنا نلقي نظرة على مثال على JSX باستخدام عنصر واحد من GitHub API ونرى ما يترجم إليه.

 <div className="repository"> <div>{item.name}</div> <p>{item.description}</p> <img src={item.owner.avatar_url} /> </div>;
 ; React.createElement( "div", { className: "repository" }, React.createElement( "div", null, item.name ), React.createElement( "p", null, item.description ), React.createElement( "img", { src: item.owner.avatar_url } ) );

JSX بسيط للغاية. تكتب كود HTML عادي وتضخ البيانات من الكائن عن طريق إضافة أقواس متعرجة. سيتم تنفيذ كود JavaScript بين الأقواس ، وسيتم إدراج القيمة في DOM الناتج. تتمثل إحدى مزايا JSX في أن React تنشئ DOM افتراضيًا (تمثيل افتراضي للصفحة) لتتبع التغييرات والتحديثات. بدلاً من إعادة كتابة HTML بالكامل ، تقوم React بتعديل DOM للصفحة كلما تم تحديث المعلومات. هذه إحدى المشكلات الرئيسية التي تم إنشاء React لحلها.

نهج jQuery

اعتاد المطورون على استخدام jQuery كثيرًا. أود أن أذكرها هنا لأنها لا تزال شائعة وأيضًا لأنها قريبة جدًا من الحل في JavaScript خالص. يحصل jQuery على مرجع إلى عقدة DOM (أو مجموعة من عُقد DOM) عن طريق الاستعلام عن DOM. كما أنه يلف هذا المرجع بوظائف مختلفة لتعديل محتوياته.

بينما يحتوي jQuery على أدوات إنشاء DOM الخاصة به ، فإن الشيء الذي أراه غالبًا في البرية هو مجرد تسلسل HTML. على سبيل المثال ، يمكننا إدخال كود HTML في العقد المحددة عن طريق استدعاء دالة html() . وفقًا لوثائق jQuery ، إذا أردنا تغيير محتويات عقدة div باستخدام demo-container للفئة ، فيمكننا القيام بذلك على النحو التالي:

 $( "div.demo-container" ).html( "<p>All new content.<em>You bet!</em></p>" );

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

نهج DOM API

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

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

باستخدام نفس البنية ومصدر البيانات كما في مثال JSX ، يمكننا إنشاء DOM بالطريقة التالية:

 var item = document.createElement('div'); item.className = 'repository'; var nameNode = document.createElement('div'); nameNode.innerHTML = item.name item.appendChild(nameNode); var description = document.createElement('p'); description.innerHTML = item.description; item.appendChild(description ); var image = new Image(); Image.src = item.owner.avatar_url; item.appendChild(image); document.body.appendChild(item);

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

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

الحل المقترح

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

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

إليك وظيفة بسيطة من شأنها أن تنجز ذلك باستخدام مقتطف HTML.

 Browser.DOM = function (html, scope) { // Creates empty node and injects html string using .innerHTML // in case the variable isn't a string we assume is already a node var node; if (html.constructor === String) { var node = document.createElement('div'); node.innerHTML = html; } else { node = html; } // Creates of uses and object to which we will create variables // that will point to the created nodes var _scope = scope || {}; // Recursive function that will read every node and when a node // contains the var attribute add a reference in the scope object function toScope(node, scope) { var children = node.children; for (var iChild = 0; iChild < children.length; iChild++) { if (children[iChild].getAttribute('var')) { var names = children[iChild].getAttribute('var').split('.'); var obj = scope; while (names.length > 0) { var _property = names.shift(); if (names.length == 0) { obj[_property] = children[iChild]; } else { if (!obj.hasOwnProperty(_property)){ obj[_property] = {}; } obj = obj[_property]; } } } toScope(children[iChild], scope); } } toScope(node, _scope); if (html.constructor != String) { return html; } // If the node in the highest hierarchy is one return it if (node.childNodes.length == 1) { // if a scope to add node variables is not set // attach the object we created into the highest hierarchy node // by adding the nodes property. if (!scope) { node.childNodes[0].nodes = _scope; } return node.childNodes[0]; } // if the node in highest hierarchy is more than one return a fragment var fragment = document.createDocumentFragment(); var children = node.childNodes; // add notes into DocumentFragment while (children.length > 0) { if (fragment.append){ fragment.append(children[0]); }else{ fragment.appendChild(children[0]); } } fragment.nodes = _scope; return fragment; }

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

تعمل الوظيفة في ثلاث خطوات:

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

إذن ، كيف تبدو الكود الخاص بنا لتقديم المثال الآن؟

 var UI = {}; var template = ''; template += '<div class="repository">' template += ' <div var="name"></div>'; template += ' <p var="text"></p>' template += ' <img var="image"/>' template += '</div>'; var item = Browser.DOM(template, UI); UI.name.innerHTML = data.name; UI.text.innerHTML = data.description; UI.image.src = data.owner.avatar_url;

أولاً ، نحدد الكائن (UI) حيث سنخزن المراجع للعقد التي تم إنشاؤها. ثم نقوم بتكوين نموذج HTML الذي سنستخدمه ، كسلسلة ، لتمييز العقد المستهدفة بسمة "var". بعد ذلك ، نسمي الوظيفة Browser.DOM بالقالب والكائن الفارغ الذي سيخزن المراجع. أخيرًا ، نستخدم المراجع المخزنة لوضع البيانات داخل العقد.

يفصل هذا النهج أيضًا بناء بنية DOM وإدخال البيانات في خطوات منفصلة مما يساعد في الحفاظ على الكود منظمًا ومنظمًا جيدًا. يتيح لنا ذلك إنشاء بنية DOM بشكل منفصل وملء (أو تحديث) البيانات عندما تصبح متاحة.

خاتمة

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

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