Cómo hacer búsquedas de usuarios en Github usando VanillaJS

Hola Mi nombre es Alexander y soy desarrollador de Vanilla ES5.1 en 2018.


Este artículo es una respuesta a la respuesta del artículo "Cómo buscar usuarios en GitHub sin React + RxJS 6 + Recompose" , que nos mostró cómo usar SvelteJS.


Sugiero mirar una de las opciones de cómo se puede implementar esto sin usar ninguna dependencia, excepto el navegador. Además, GitHub mismo declaró que están desarrollando una interfaz sin marcos .


Haremos la misma entrada, mostrando la placa de usuario de GitHub:



Descargo de responsabilidad

Este artículo ignora absolutamente todas las prácticas posibles de javascript moderno y desarrollo web.


Preparación


No necesitamos configurar y escribir configuraciones, crear index.html con todo el diseño necesario:


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 alguien está interesado en CSS , puede verlo en el repositorio .


Tenemos los estilos más comunes, sin módulos css y otros alcances. Simplemente marcamos los componentes con clases que comienzan con x- y garantizamos que no habrá más en el proyecto. Escribimos los selectores que les conciernen.


Campo de entrada


Todo lo que queremos de nuestro campo de entrada son los eventos sin rebote de su cambio, así como el evento en que se inició la entrada, para que muestre inmediatamente la indicación de carga. Resulta así:


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

Aquí usamos algunas funciones utilitarias, las veremos:


Como no tenemos webpack , ni CommonJS , ni RequireJS , ponemos todo en objetos mediante la siguiente función:


paquetes.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 función instantiateTemplate() nos da una copia profunda del elemento DOM, que será obtenida por la función consumeTemplates() del elemento #templates en nuestro 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 función Dom.binding() acepta un elemento, opciones, busca atributos de datos específicos y realiza las acciones que necesitamos con los elementos. Por ejemplo, para el atributo de data-element , agrega un campo al resultado con un enlace al elemento marcado, para el atributo de data-onedit , los keyup change keyup y keyup se cuelgan en el elemento con el controlador de opciones.


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

Bueno, el delay trata con el tipo de rebote que necesitamos:


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

Tarjeta de usuario


No tiene lógica, solo una plantilla que está llena de datos:


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

Por supuesto, hacer tantos querySelectorAll cada vez que cambiamos los datos no es muy bueno, pero funciona y lo querySelectorAll . Si de repente resulta que todo se está desacelerando debido a esto, escribiremos los datos en el data-element almacenado. O haremos otra función de enlace, que guarda los elementos y puede leer los nuevos datos. O apoyaremos la transferencia de opciones al objeto no solo de valores estáticos, una secuencia de sus cambios para que las carpetas puedan seguirlos.


Indicación de errores de carga / solicitud


Suponemos que estas representaciones también serán estáticas, se utilizarán solo en un lugar y las posibilidades de que tengan su propia lógica son extremadamente pequeñas (a diferencia de la tarjeta del usuario), por lo tanto, no crearemos componentes separados para ellas. Serán solo plantillas para el componente de la aplicación.


Solicitud de datos


Crearemos una clase con un método de solicitud de usuario, en cuyo caso podemos reemplazar fácilmente su instancia con una implementación moch / otra:


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

Por supuesto, necesitamos un contenedor sobre XMLHttpRequest . No utilizamos fetch porque no admite la interrupción de solicitudes, ni queremos comunicarnos con promesas por la misma razón.


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

Solicitud final


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

Obtuvimos bastante código, la mitad del cual está en la inicialización de todos los componentes que necesitamos, la mitad en la lógica de enviar solicitudes y mostrar la carga / error / resultado. Pero todo es completamente transparente, obvio y podemos cambiar la lógica en cualquier lugar, si es necesario.


Utilizamos la utilidad DisplayOneOf , que muestra uno de los elementos dados, oculta el resto:


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

Para que funcione al final, necesitamos inicializar las plantillas y soltar la instancia de la App en la página:


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

Resultado?


Como puede ver por sí mismo, escribimos mucho código para un ejemplo tan pequeño. Nadie hace toda la magia por nosotros, lo logramos todo nosotros mismos. Nosotros mismos creamos la magia que necesitamos si la queremos.


DemoCódigo


Escribe un código tonto y eterno. Escriba sin marcos, lo que significa que duerme mejor y no tenga miedo de que mañana todo su código quede obsoleto o no esté lo suficientemente de moda.


Que sigue


Este ejemplo es demasiado pequeño para escribirlo en VanillaJS en principio. Creo que escribir en vainilla solo tiene sentido si su proyecto planea vivir mucho más tiempo que cualquiera de los marcos y no tiene los recursos para reescribirlo en su totalidad.


Pero si fuera más grande de todos modos, esto es lo que haríamos:


Plantillas HTML que haríamos con respecto a los módulos / componentes. Estarían en las carpetas con los componentes e instantiateTemplate tomaría el nombre del módulo más el nombre de la plantilla, y no solo el nombre global.


Por el momento, todo el CSS que tenemos está en index.css , obviamente, también tenemos que ponerlo al lado de los componentes.


No hay suficiente ensamblaje de paquetes, conectamos todos los archivos con nuestras manos en index.html , esto no es bueno.


No hay problema en escribir un script que, a través de las listas de módulos que deben incluirse en los paquetes, recopile todos los js, html, css de estos módulos y nos haga un js por cada paquete. Será mucho más tonto y fácil que configurar el paquete web , y después de un año descubra que ya existe una versión completamente diferente y que necesita reescribir la configuración y usar otros cargadores.


Es aconsejable tener algún tipo de indicador que admita el esquema de conexión js / html / css con una lista enorme en index.html . Entonces no habrá demoras en el ensamblaje, y en Fuentes en Chrome tendrá cada archivo en una pestaña separada y no se necesitan mapas de origen.


PS


Esta es solo una de las opciones de cómo se puede usar VanillaJS . En los comentarios, sería interesante escuchar sobre otros casos de uso.


Gracias por su atencion

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


All Articles