كيفية إجراء عمليات بحث المستخدم على Github باستخدام VanillaJS

مرحبًا. اسمي ألكسندر وأنا مطور Vanilla ES5.1 في 2018.


هذه المقالة هي ردا على مقالة الإجابة "كيفية البحث عن المستخدمين على GitHub دون React + RxJS 6 + Recompose" ، والتي أظهرت لنا كيفية استخدام SvelteJS.


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


سنفعل نفس المدخلات ، مع عرض لوحة مستخدم GitHub:



تنويه

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


تحضير


لسنا بحاجة إلى تكوين وكتابة التكوينات ، وإنشاء index.html بكل التخطيط اللازم:


index.html
<!doctype html> <html> <head> <meta charset='utf-8'> <title>GitHub users</title> <link rel='stylesheet' type='text/css' href='index.css'> </head> <body> <div id='root'></div> <div id='templates' style='display:none;'> <div data-template-id='username_input'> <input type='text' data-onedit='onNameEdit' placeholder='GitHub username'> </div> <div data-template-id='usercard' class='x-user-card'> <div class='background'></div> <div class='avatar-container'> <a class='avatar' data-href='userUrl'> <img data-src='avatarImageUrl'> </a> </div> <div class='name' data-text='userName'></div> <div class='content'> <a class='block' data-href='reposUrl'> <b data-text='reposCount'></b> <span>Repos</span> </a> <a class='block' data-href='gistsUrl'> <b data-text='gistsCount'></b> <span>Gists</span> </a> <a class='block' data-href='followersUrl'> <b data-text='followersCount'></b> <span>Followers</span> </a> </div> </div> <div data-template-id='error'><b data-text='status'></b>: <span data-text='text'></span></div> <div data-template-id='loading'>Loading...</div> </div> </body> </html> 

إذا كان أي شخص مهتم بـ CSS ، يمكنك رؤيته في المستودع .


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


مجال الإدخال


كل ما نريده من حقل الإدخال الخاص بنا هو الأحداث التي تم رفضها لتغييره ، وكذلك الحدث الذي بدأ فيه الإدخال ، بحيث يعرض مؤشر الحمل على الفور. اتضح مثل هذا:


 in_package('GitHubUsers', function() { this.provide('UserNameInput', UserNameInput); function UserNameInput(options) { var onNameInput = options.onNameInput, onNameChange = options.onNameChange; var element = GitHubUsers.Dom.instantiateTemplate('username_input'); var debouncedChange = GitHubUsers.Util.delay(1000, function() { onNameChange(this.value); }); GitHubUsers.Dom.binding(element, { onNameEdit: function() { onNameInput(this.value); debouncedChange.apply(this, arguments); } }); this.getElement = function() { return element; }; } }); 

استخدمنا هنا بعض الوظائف النفعية ، وسنستعرضها:


نظرًا لعدم وجود webpack ، لا يوجد CommonJS ، لا RequireJS ، فإننا نضع كل شيء في الكائنات باستخدام الوظيفة التالية:


الحزم
 window.in_package = function(path, fun) { path = path.split('.'); var obj = path.reduce(function(acc, p) { var o = acc[p]; if (!o) { o = {}; acc[p] = o; } return o; }, window); fun.call({ provide: function(name, value) { obj[name] = value; } }); }; 

consumeTemplates() وظيفة consumeTemplates() نسخة عميقة من عنصر DOM ، والتي سيتم الحصول عليها بواسطة وظيفة consumeTemplates() من العنصر #templates في index.html .


قوالب
 in_package('GitHubUsers.Dom', function() { var templatesMap = new Map(); this.provide('consumeTemplates', function(containerEl) { var templates = containerEl.querySelectorAll('[data-template-id]'); for (var i = 0; i < templates.length; i++) { var templateEl = templates[i], templateId = templateEl.getAttribute('data-template-id'); templatesMap.set(templateId, templateEl); templateEl.parentNode.removeChild(templateEl); } if (containerEl.parentNode) containerEl.parentNode.removeChild(containerEl); }); this.provide('instantiateTemplate', function(templateId) { var templateEl = templatesMap.get(templateId); return templateEl.cloneNode(true); }); }); 

تقبل وظيفة Dom.binding() عنصرًا وخيارات وعمليات بحث عن سمات بيانات محددة وتقوم بتنفيذ الإجراءات التي نحتاجها مع العناصر. على سبيل المثال ، بالنسبة لسمة data-element ، فإنه يضيف حقلاً للنتيجة برابط للعنصر المميز ، data-onedit ، يتم تعليق معالجات keyup change على العنصر باستخدام معالج الخيارات.


ملزمة
 in_package('GitHubUsers.Dom', function() { this.provide('binding', function(element, options) { options = options || {}; var binding = {}; handleAttribute('data-element', function(el, name) { binding[name] = el; }); handleAttribute('data-text', function(el, key) { var text = options[key]; if (typeof text !== 'string' && typeof text !== 'number') return; el.innerText = text; }); handleAttribute('data-src', function(el, key) { var src = options[key]; if (typeof src !== 'string') return; el.src = src; }); handleAttribute('data-href', function(el, key) { var href = options[key]; if (typeof href !== 'string') return; el.href = href; }); handleAttribute('data-onedit', function(el, key) { var handler = options[key]; if (typeof handler !== 'function') return; el.addEventListener('keyup', handler); el.addEventListener('change', handler); }); function handleAttribute(attribute, fun) { var elements = element.querySelectorAll('[' + attribute + ']'); for (var i = 0; i < elements.length; i++) { var el = elements[i], attributeValue = el.getAttribute(attribute); fun(el, attributeValue); } } return binding; }); }); 

حسنًا ، يتعامل delay مع نوع الخصم الذي نحتاجه:


debounce.js
 in_package('GitHubUsers.Util', function() { this.provide('delay', function(timeout, fun) { var timeoutId = 0; return function() { var that = this, args = arguments; if (timeoutId) clearTimeout(timeoutId); timeoutId = setTimeout(function() { timeoutId = 0; fun.apply(that, args); }, timeout); }; }); }); 

بطاقة المستخدم


ليس له منطق ، فقط قالب مليء بالبيانات:


 in_package('GitHubUsers', function() { this.provide('UserCard', UserCard); function UserCard() { var element = GitHubUsers.Dom.instantiateTemplate('usercard'); this.getElement = function() { return element; }; this.setData = function(data) { GitHubUsers.Dom.binding(element, data); }; } }); 

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


إشارة إلى أخطاء التحميل / الطلب


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


طلب بيانات


سننشئ فئة باستخدام طريقة طلب المستخدم ، وفي هذه الحالة يمكننا بسهولة استبدال مثيلها بآخر / تنفيذ آخر:


 in_package('GitHubUsers', function() { this.provide('GitHubApi', GitHubApi); function GitHubApi() { this.getUser = function(options, callback) { var url = 'https://api.github.com/users/' + options.userName; return GitHubUsers.Http.doRequest(url, function(error, data) { if (error) { if (error.type === 'not200') { if (error.status === 404) callback(null, null); else callback({ status: error.status, message: data && data.message }); } else { callback(error); } return; } // TODO: validate `data` against schema callback(null, data); }); }; } }); 

بالطبع ، نحتاج إلى غلاف فوق XMLHttpRequest . لا نستخدم ميزة fetch لأنها لا تدعم انقطاع الطلبات ، ولا نريد التواصل مع الوعود لنفس السبب.


أجاكس
 in_package('GitHubUsers.Http', function() { this.provide('doRequest', function(options, callback) { var url; if (typeof options === "string") { url = options; options = {}; } else { if (!options) options = {}; url = options.url; } var method = options.method || "GET", headers = options.headers || [], body = options.body, dataType = options.dataType || "json", timeout = options.timeout || 10000; var old_callback = callback; callback = function() { callback = function(){}; // ignore all non-first calls old_callback.apply(this, arguments); }; var isAborted = false; var request = new XMLHttpRequest(); // force timeout var timeoutId = setTimeout(function() { timeoutId = 0; if (!isAborted) { request.abort(); isAborted = true; } callback({msg: "fetch_timeout", request: request, opts: options}); }, timeout); request.addEventListener("load", function() { var error = null; if (request.status !== 200) { error = { type: 'not200', status: request.status }; } if (typeof request.responseText === "string") { if (dataType !== "json") { callback(error, request.responseText); return; } var parsed; try { parsed = JSON.parse(request.responseText); } catch (e) { callback(e); return; } if (parsed) { callback(error, parsed); } else { callback({msg: "bad response", request: request}); } } else { callback({msg: "no response text", request: request}); } }); request.addEventListener("error", function() { callback({msg: "request_error", request: request}); }); request.open(method, url, true /*async*/); request.timeout = timeout; request.responseType = ""; headers.forEach(function(header) { try { request.setRequestHeader(header[0], header[1]); } catch (e) {} }); try { if (body) request.send(body); else request.send(); } catch (e) { callback({exception: e, type: 'send'}); } return { cancel: function() { if (!isAborted) { request.abort(); isAborted = true; } if (timeoutId) { clearTimeout(timeoutId); timeoutId = 0; } } }; }); }); 

التطبيق النهائي


app.js
 in_package('GitHubUsers', function() { this.provide('App', App); function App(options) { var api = options.api; var element = document.createElement('div'); // Create needed components var userNameInput = new GitHubUsers.UserNameInput({ onNameInput: onNameInput, onNameChange: onNameChange }); var userCard = new GitHubUsers.UserCard(); var errorElement = GitHubUsers.Dom.instantiateTemplate('error'); var displayElements = [ { type: 'loading', element: GitHubUsers.Dom.instantiateTemplate('loading') }, { type: 'error', element: errorElement }, { type: 'userCard', element: userCard.getElement() } ]; // Append elements to DOM element.appendChild(userNameInput.getElement()); userNameInput.getElement().style.marginBottom = '1em'; // HACK displayElements.forEach(function(x) { var el = x.element; el.style.display = 'none'; element.appendChild(el); }); var contentElements = new GitHubUsers.DomUtil.DisplayOneOf({ items: displayElements }); // User name processing var activeRequest = null; function onNameInput(name) { name = name.trim(); // Instant display of `loading` or current request result if (activeRequest && activeRequest.name === name) { activeRequest.activateState(); } else if (name) { contentElements.showByType('loading'); } else { contentElements.showByType(null); } } function onNameChange(name) { name = name.trim(); // Cancel old request if (activeRequest && activeRequest.name !== name) { activeRequest.request.cancel(); activeRequest = null; } else if (activeRequest) { // same name return; } if (!name) return; // Do new request activeRequest = { name: name, request: api.getUser({ userName: name }, onUserData), // method for `onNameInput` activateState: function() { contentElements.showByType('loading'); } }; activeRequest.activateState(); function onUserData(error, data) { if (error) { activeRequest = null; contentElements.showByType('error'); GitHubUsers.Dom.binding(errorElement, { status: error.status, text: error.message }); return; } if (!data) { activeRequest.activateState = function() { GitHubUsers.Dom.binding(errorElement, { status: 404, text: 'Not found' }); contentElements.showByType('error'); }; activeRequest.activateState(); return; } activeRequest.activateState = function() { userCard.setData({ userName: data.name || data.login, // `data.name` can be `null` userUrl: data.html_url, avatarImageUrl: data.avatar_url + '&s=80', reposCount: data.public_repos, reposUrl: 'https://github.com/' + data.login + '?tab=repositories', gistsCount: data.public_gists, gistsUrl: 'https://gist.github.com/' + data.login, followersCount: data.followers, followersUrl: 'https://github.com/' + data.login + '/followers' }); contentElements.showByType('userCard'); }; activeRequest.activateState(); } } this.getElement = function() { return element; }; } }); 

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


استخدمنا الأداة DisplayOneOf ، التي تُظهر أحد العناصر المعينة ، تُخفي الباقي:


dom-util.js
 in_package('GitHubUsers.DomUtil', function() { this.provide('DisplayOneOf', function(options) { var items = options.items; var obj = {}; items.forEach(function(item) { obj[item.type] = item; }); var lastDisplayed = null; this.showByType = function(type) { if (lastDisplayed) { lastDisplayed.element.style.display = 'none'; } if (!type) { lastDisplayed = null; return; } lastDisplayed = obj[type]; lastDisplayed.element.style.display = ''; }; }); }); 

لجعلها تعمل في النهاية ، نحتاج إلى تهيئة القوالب وإسقاط نسخة App على الصفحة:


 function onReady() { GitHubUsers.Dom.consumeTemplates(document.getElementById('templates')); var rootEl = document.getElementById('root'); var app = new GitHubUsers.App({ api: new GitHubUsers.GitHubApi() }); rootEl.appendChild(app.getElement()); } 

النتيجة؟


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


عرض توضيحيالرمز


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


ما هي الخطوة التالية؟


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


ولكن إذا كان أكبر على أي حال ، فإليك ما سنفعله:


قوالب HTML التي نقوم بها فيما يتعلق بالوحدات / المكونات. سيكونون في المجلدات مع المكونات وسيأخذ instantiateTemplate اسم الوحدة النمطية بالإضافة إلى اسم القالب ، وليس فقط الاسم العام.


في الوقت الحالي ، كل CSS لدينا موجود في index.css ، من الواضح أننا نحتاج أيضًا إلى وضعه بجوار المكونات.


لا يوجد تجميع كافٍ للحزم ، نحن نربط جميع الملفات بأيدينا في index.html ، وهذا ليس جيدًا.


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


من المستحسن أن يكون لديك نوع من العلم الذي يدعم نظام اتصال js / html / css مع قائمة ضخمة في index.html . بعد ذلك لن يكون هناك أي تأخير في التجميع ، وفي المصادر في الكروم سيكون لديك كل ملف في علامة تبويب منفصلة ولن تكون هناك حاجة إلى خرائط مصادر.


ملاحظة


هذا مجرد أحد الخيارات التي يمكن من خلالها استخدام VanillaJS . في التعليقات ، سيكون من المثير للاهتمام أن نسمع عن حالات الاستخدام الأخرى.


شكرا لكم على اهتمامكم.

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


All Articles