Comment faire des recherches d'utilisateurs sur Github en utilisant VanillaJS

Bonjour Je m'appelle Alexander et je suis un développeur Vanilla ES5.1 en 2018.


Cet article est une réponse à l'article-réponse "Comment rechercher des utilisateurs sur GitHub sans React + RxJS 6 + Recompose" , qui nous a montré comment utiliser SvelteJS.


Je suggĂšre de regarder l'une des options pour savoir comment cela peut ĂȘtre implĂ©mentĂ© sans utiliser de dĂ©pendances Ă  l'exception du navigateur. De plus, GitHub lui-mĂȘme a dĂ©clarĂ© qu'il dĂ©veloppait un frontend sans frameworks .


Nous ferons la mĂȘme entrĂ©e qui affiche la plaque utilisateur GitHub:



Clause de non-responsabilité

Cet article ignore absolument toutes les pratiques possibles du javascript moderne et du développement web.


La préparation


Nous n'avons pas besoin de configurer et d'écrire des configs, de créer index.html avec toute la mise en page nécessaire:


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> 

Si quelqu'un s'intéresse au CSS , vous pouvez le voir dans le référentiel .


Nous avons les styles les plus courants, pas de modules css et autres oscilloscopes. Nous marquons simplement les composants avec des classes commençant par x- et garantissons qu'il n'y en aura plus dans le projet. Nous écrivons tous les sélecteurs les concernant.


Champ de saisie


Tout ce que nous voulons de notre champ de saisie, ce sont les événements rebondis de son changement, ainsi que l'événement que la saisie a commencé, afin qu'il affiche immédiatement l'indication de charge. Il se présente comme ceci:


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

Ici, nous avons utilisé quelques fonctions utilitaires, nous allons les parcourir:


Comme nous n'avons pas de webpack , pas de CommonJS , pas de RequireJS , nous mettons tout en objets en utilisant la fonction suivante:


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

La fonction consumeTemplates() nous donne une copie consumeTemplates() de l'élément DOM, qui sera obtenue par la fonction consumeTemplates() partir de l'élément #templates dans notre index.html .


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

La fonction Dom.binding() accepte un élément, des options, recherche des attributs de données spécifiques et effectue les actions dont nous avons besoin avec les éléments. Par exemple, pour l'attribut d' data-element , il ajoute un champ au résultat avec un lien vers l'élément marqué, pour l'attribut data-onedit , les keyup et de change sont suspendus à l'élément avec le gestionnaire d'options.


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

Eh bien, le delay concerne le type de rebond dont nous avons besoin:


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

Carte d'utilisateur


Il n'a pas de logique, seulement un modÚle rempli de données:


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

Bien sĂ»r, faire autant de querySelectorAll chaque fois que nous modifions les donnĂ©es n'est pas trĂšs bon, mais cela fonctionne et nous les acceptons. S'il s'avĂšre soudainement que tout ralentit Ă  cause de cela, nous Ă©crirons les donnĂ©es dans l' data-element stockĂ©. Ou nous allons crĂ©er une autre fonction de liaison, qui enregistre elle-mĂȘme les Ă©lĂ©ments et peut lire les nouvelles donnĂ©es. Ou nous prendrons en charge le transfert d'options Ă  l'objet de valeurs non seulement statiques, un flux de leurs modifications afin que les classeurs puissent les suivre.


Indication d'erreurs de chargement / demande


Nous supposons que ces reprĂ©sentations seront Ă©galement statiques, ne seront utilisĂ©es qu’en un seul endroit, et les chances qu’elles aient leur propre logique sont extrĂȘmement faibles (contrairement Ă  la carte de l’utilisateur), par consĂ©quent, nous ne ferons pas de composants sĂ©parĂ©s pour elles. Ce ne seront que des modĂšles pour le composant d'application.


Demande de données


Nous allons crĂ©er une classe avec une mĂ©thode de requĂȘte utilisateur, auquel cas nous pouvons facilement remplacer son instance par une implĂ©mentation moch / other:


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

Bien sĂ»r, nous avons besoin d'un wrapper sur XMLHttpRequest . Nous n'utilisons pas la fetch car elle ne prend pas en charge l'interruption des demandes, et nous ne voulons pas non plus communiquer avec des promesses pour la mĂȘme raison.


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

Application finale


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

Nous avons obtenu beaucoup de code, dont la moitiĂ© dans l'initialisation de tous les composants dont nous avons besoin, la moitiĂ© dans la logique d'envoi des requĂȘtes et d'affichage de la charge / erreur / rĂ©sultat. Mais tout est complĂštement transparent, Ă©vident et nous pouvons changer la logique n'importe oĂč, si nĂ©cessaire.


Nous avons utilisé l'utilitaire DisplayOneOf , qui montre l'un des éléments donnés, masque le reste:


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

Pour le faire fonctionner à la fin, nous devons initialiser les modÚles et déposer l'instance d' App sur la page:


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

Résultat?


Comme vous pouvez le constater par vous-mĂȘme, nous avons Ă©crit beaucoup de code pour un si petit exemple. Personne ne fait toute la magie pour nous, nous rĂ©alisons tout nous-mĂȘmes. Nous crĂ©ons nous-mĂȘmes la magie dont nous avons besoin si nous le voulons.


→ DĂ©mo → Code


Écrivez un code stupide et Ă©ternel. Écrivez sans framework, ce qui signifie que vous dormez mieux et n’ayez pas peur que demain tout votre code soit obsolĂšte ou pas assez Ă  la mode.


Et ensuite?


Cet exemple est trop petit pour l'écrire dans VanillaJS en principe. Je pense qu'écrire en vanille n'a de sens que si votre projet prévoit de vivre beaucoup plus longtemps que n'importe lequel des frameworks et que vous n'avez pas les ressources pour le réécrire dans son intégralité.


Mais s'il était plus gros de toute façon, voici ce que nous ferions:


ModĂšles HTML que nous ferions concernant les modules / composants. Ils seraient dans les dossiers avec les composants et instantiateTemplate prendrait le nom du module plus le nom du modĂšle, et pas seulement le nom global.


Pour le moment, tout le CSS que nous avons est dans index.css , évidemment, nous devons également le mettre à cÎté des composants.


Il n'y a pas assez d'assemblage de bundles, nous connectons tous les fichiers avec nos mains dans index.html , ce n'est pas bon.


Il n'y a aucun problĂšme Ă  Ă©crire un script qui, Ă  travers les listes de modules qui devraient ĂȘtre inclus dans les bundles, collectera tous les js, html, css de ces modules et nous fera un js pour chaque bundle. Ce sera beaucoup plus stupide et plus facile que de configurer un webpack , et aprĂšs un an, dĂ©couvrez qu'il existe dĂ©jĂ  une version complĂštement diffĂ©rente et vous devez réécrire la configuration et utiliser d'autres chargeurs.


Il est conseillé d'avoir une sorte d'indicateur qui prend en charge le schéma de connexion js / html / css avec une énorme liste dans index.html . Ensuite, il n'y aura pas de retards dans l'assemblage, et dans Sources en chrome, vous aurez chaque fichier dans un onglet séparé et aucun sourcemaps n'est nécessaire.


PS


Ceci n'est qu'une des options sur la façon dont tout cela peut ĂȘtre utilisĂ© avec VanillaJS . Dans les commentaires, il serait intĂ©ressant d'entendre parler d'autres cas d'utilisation.


Merci de votre attention.

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


All Articles