So führen Sie Benutzersuchen auf Github mit VanillaJS durch

Guten Tag. Mein Name ist Alexander und ich bin ein Vanilla ES5.1-Entwickler im Jahr 2018.


Dieser Artikel ist eine Antwort auf die Artikelantwort "So suchen Sie auf GitHub nach Benutzern ohne React + RxJS 6 + Recompose" , die uns die Verwendung von SvelteJS zeigte.


Ich schlage vor, eine der Optionen zu prüfen, wie dies implementiert werden kann, ohne Abhängigkeiten außer dem Browser zu verwenden. Darüber hinaus gab GitHub selbst an, ein Frontend ohne Frameworks zu entwickeln .


Wir werden die gleiche Eingabe machen und die GitHub-Benutzerplatte anzeigen:



Haftungsausschluss

Dieser Artikel ignoriert absolut alle möglichen Praktiken der modernen Javascript- und Webentwicklung.


Vorbereitung


Wir müssen keine Konfigurationen konfigurieren und schreiben, sondern index.html mit dem erforderlichen Layout erstellen:


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> 

Wenn sich jemand für CSS interessiert, können Sie es im Repository sehen .


Wir haben die gängigsten Stile, keine CSS-Module und andere Bereiche. Wir markieren die Komponenten einfach mit Klassen, die mit x- beginnen, und garantieren, dass das Projekt nicht mehr vorhanden ist. Wir schreiben alle Selektoren, die sie betreffen.


Eingabefeld


Alles, was wir von unserem Eingabefeld erwarten, sind die entprellten Ereignisse seiner Änderung sowie das Ereignis, mit dem die Eingabe gestartet wurde, so dass sofort die Lastanzeige angezeigt wird. Es stellt sich so heraus:


 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; }; } }); 

Hier haben wir einige nützliche Funktionen verwendet, wir werden sie durchgehen:


Da wir kein webpack , kein CommonJS , kein RequireJS haben, webpack wir alles mit der folgenden Funktion in Objekte ein:


packages.js
 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; } }); }; 

Die Funktion instantiateTemplate() gibt uns eine tiefe Kopie des DOM-Elements, die von der Funktion consumeTemplates() aus dem Element #templates in unserer index.html abgerufen wird.


templates.js
 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); }); }); 

Die Funktion Dom.binding() akzeptiert ein Element, Optionen, sucht nach bestimmten Dom.binding() und führt die Aktionen aus, die wir mit den Elementen benötigen. Beispielsweise fügt es für das data-element dem Ergebnis ein Feld mit einem Link zum markierten Element hinzu. Für das data-onedit Attribut werden die keyup und change mit dem Optionshandler an das Element gehängt.


bind.js
 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; }); }); 

Nun, delay befasst sich mit der Art der Entprellung, die wir brauchen:


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); }; }); }); 

Benutzerkarte


Es hat keine Logik, nur eine Vorlage, die mit Daten gefüllt ist:


 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); }; } }); 

Natürlich ist es nicht sehr gut, jedes Mal so viele querySelectorAll wenn wir die Daten ändern, aber es funktioniert und wir lassen es zu. Wenn sich plötzlich herausstellt, dass sich dadurch alles verlangsamt, schreiben wir die Daten in das gespeicherte data-element . Oder wir machen eine andere Bindungsfunktion, die selbst die Elemente speichert und die neuen Daten lesen kann. Oder wir unterstützen die Übertragung von Optionen auf das Objekt, nicht nur statische Werte, sondern einen Strom ihrer Änderungen, damit die Ordner ihnen folgen können.


Anzeige von Lade- / Anforderungsfehlern


Wir gehen davon aus, dass diese Darstellungen auch statisch sind, nur an einer Stelle verwendet werden und die Wahrscheinlichkeit, dass sie ihre eigene Logik haben, extrem gering ist (im Gegensatz zur Karte des Benutzers), daher werden wir keine separaten Komponenten für sie erstellen. Sie sind nur Vorlagen für die Anwendungskomponente.


Datenanforderung


Wir werden eine Klasse mit einer Benutzeranforderungsmethode erstellen. In diesem Fall können wir ihre Instanz leicht durch eine moch / andere Implementierung ersetzen:


 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); }); }; } }); 

Natürlich brauchen wir einen Wrapper über XMLHttpRequest . Wir verwenden fetch nicht, weil es keine Unterbrechung von Anfragen unterstützt, und wir möchten aus demselben Grund auch nicht mit Versprechungen kommunizieren.


ajax.js
 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; } } }; }); }); 

Endgültige Bewerbung


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; }; } }); 

Wir haben ziemlich viel Code, von denen die Hälfte in der Initialisierung aller benötigten Komponenten, die andere Hälfte in der Logik des Sendens von Anforderungen und der Anzeige des Ladevorgangs / Fehlers / Ergebnisses liegt. Aber alles ist völlig transparent, offensichtlich und wir können die Logik bei Bedarf überall ändern.


Wir haben das Dienstprogramm DisplayOneOf , das eines der angegebenen Elemente DisplayOneOf und den Rest verbirgt:


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 = ''; }; }); }); 

Damit es am Ende funktioniert, müssen wir die Vorlagen initialisieren und die App Instanz auf der Seite ablegen:


 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()); } 

Ergebnis?


Wie Sie selbst sehen können, haben wir für ein so kleines Beispiel viel Code geschrieben. Niemand macht die ganze Magie für uns, wir erreichen alles selbst. Wir selbst erschaffen die Magie, die wir brauchen, wenn wir es wollen.


DemoCode


Schreiben Sie einen dummen und ewigen Code. Schreiben Sie ohne Frameworks, was bedeutet, dass Sie besser schlafen und keine Angst haben, dass Ihr gesamter Code morgen veraltet oder nicht modisch genug ist.


Was weiter?


Dieses Beispiel ist zu klein, um es im Prinzip in VanillaJS zu schreiben. Ich glaube, dass das Schreiben in Vanille nur dann Sinn macht, wenn Ihr Projekt viel länger als eines der Frameworks leben soll und Sie nicht über die Ressourcen verfügen, um es vollständig neu zu schreiben.


Aber wenn er trotzdem größer wäre, würden wir Folgendes tun:


HTML-Vorlagen, die wir in Bezug auf Module / Komponenten erstellen würden. Sie befinden sich in den Ordnern mit den Komponenten, und instantiateTemplate übernimmt den Namen des Moduls sowie den Namen der Vorlage und nicht nur den globalen Namen.


Im Moment befindet sich das gesamte CSS in index.css müssen wir es auch neben die Komponenten stellen.


Es gibt nicht genug Zusammenstellungen von Bundles, wir verbinden alle Dateien mit unseren Händen in index.html , das ist nicht gut.


Es ist kein Problem, ein Skript zu schreiben, das über die Liste der Module, die in den Bundles enthalten sein sollten, alle js, html und css dieser Module sammelt und uns für jedes Bundle ein js macht. Es ist viel dümmer und einfacher als das Einrichten eines Webpacks . Nach einem Jahr stellen Sie fest, dass es bereits eine völlig andere Version gibt und Sie die Konfiguration neu schreiben und andere Loader verwenden müssen.


Es ist ratsam, eine Art Flag zu haben, das das Verbindungsschema js / html / css mit einer riesigen Liste in index.html . Dann gibt es keine Verzögerungen bei der Assembly, und in Sources in Chrome befindet sich jede Datei auf einer separaten Registerkarte, und es werden keine Quellenzuordnungen benötigt.


PS


Dies ist nur eine der Optionen, wie VanillaJS verwendet werden kann . In den Kommentaren wäre es interessant, etwas über andere Anwendungsfälle zu erfahren.


Vielen Dank für Ihre Aufmerksamkeit.

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


All Articles