Como fazer pesquisas de usuários no Github usando o VanillaJS

Olá. Meu nome é Alexander e sou desenvolvedor Vanilla ES5.1 em 2018.


Este artigo é uma resposta à resposta do artigo "Como procurar usuários no GitHub sem React + RxJS 6 + Recompose" , que nos mostrou como usar o SvelteJS.


Eu sugiro olhar para uma das opções de como isso pode ser implementado sem usar nenhuma dependência, exceto o navegador. Além disso, o próprio GitHub afirmou que eles estão desenvolvendo um frontend sem frameworks .


Faremos a mesma entrada, exibindo a placa do usuário do GitHub:



Isenção de responsabilidade

Este artigo ignora absolutamente todas as práticas possíveis de desenvolvimento moderno de javascript e web.


Preparação


Não precisamos configurar e escrever configurações, criar index.html com todo o layout necessário:


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> 

Se alguém estiver interessado em CSS , você pode vê-lo no repositório .


Temos os estilos mais comuns, sem módulos CSS e outros escopos. Simplesmente marcamos os componentes com classes começando com x- e garantimos que não haverá mais no projeto. Nós escrevemos quaisquer seletores a respeito deles.


Campo de entrada


Tudo o que queremos do nosso campo de entrada são os eventos rejeitados de sua alteração, bem como o evento em que a entrada foi iniciada, para que ela mostre imediatamente a indicação de carga. Acontece assim:


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

Aqui nós usamos algumas funções utilitárias, vamos passar por elas:


Como não temos webpack , nem CommonJS , nem RequireJS , colocamos tudo em objetos usando a seguinte função:


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

A função instantiateTemplate() fornece uma cópia profunda do elemento DOM, que será obtida pela função consumeTemplates() do elemento #templates em nosso 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); }); }); 

A função Dom.binding() aceita um elemento, opções, procura atributos de dados específicos e executa as ações que precisamos com os elementos. Por exemplo, para o atributo do data-element , ele adiciona um campo ao resultado com um link para o elemento marcado; para o atributo data-onedit , os manipuladores de keyup e change são pendurados no elemento com o manipulador de opções.


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

Bem, o delay lida com o tipo de devolução que precisamos:


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

Cartão de usuário


Não possui lógica, apenas um modelo preenchido com dados:


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

Obviamente, fazer tantas querySelectorAll toda vez que querySelectorAll os dados não é muito bom, mas funciona e nós os querySelectorAll . Se, de repente, tudo ficar mais lento por causa disso, gravaremos os dados no data-element armazenado. Ou criaremos outra função de ligação, que salva os elementos e pode ler os novos dados. Ou apoiaremos a transferência de opções para o objeto não apenas de valores estáticos, um fluxo de suas alterações para que os ligantes possam segui-las.


Indicação de erros de carregamento / solicitação


Assumimos que essas representações também serão estáticas, serão usadas apenas em um local e as chances de que elas tenham sua própria lógica são extremamente pequenas (ao contrário do cartão de um usuário); portanto, não faremos componentes separados para eles. Eles serão apenas modelos para o componente de aplicativo.


Pedido de dados


Criaremos uma classe com um método de solicitação do usuário; nesse caso, podemos substituir facilmente sua instância por uma implementação 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); }); }; } }); 

Obviamente, precisamos de um wrapper sobre XMLHttpRequest . Não usamos a fetch porque ela não suporta interrupção de solicitações, nem queremos nos comunicar com promessas pelo mesmo motivo.


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

Aplicação 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; }; } }); 

Temos bastante código, metade do qual está na inicialização de todos os componentes que precisamos, metade na lógica de enviar solicitações e exibir a carga / erro / resultado. Mas tudo é completamente transparente, óbvio e podemos mudar a lógica em qualquer lugar, se necessário.


Usamos o utilitário DisplayOneOf , que mostra um dos elementos fornecidos, oculta o restante:


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 fazê-lo funcionar no final, precisamos inicializar os modelos e soltar a instância do App na 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 você pode ver por si mesmo, escrevemos muito código para um exemplo tão pequeno. Ninguém faz toda a mágica para nós, nós alcançamos tudo sozinhos. Nós mesmos criamos a mágica de que precisamos, se a queremos.


DemonstraçãoCódigo


Escreva um código idiota e eterno. Escreva sem estruturas, o que significa que você dorme melhor e não tem medo de que amanhã todo o seu código seja preterido ou não esteja na moda o suficiente.


O que vem a seguir?


Este exemplo é muito pequeno para escrevê-lo no VanillaJS em princípio. Acredito que escrever em baunilha só faz sentido se o seu projeto planeja viver muito mais do que qualquer uma das estruturas e você não tem recursos para reescrevê-lo na íntegra.


Mas se ele fosse maior de qualquer maneira, aqui está o que faríamos:


Modelos HTML que faríamos em relação aos módulos / componentes. Eles estariam nas pastas com os componentes e o instantiateTemplate levaria o nome do módulo mais o nome do modelo, e não apenas o nome global.


No momento, todo o CSS que temos está no index.css , obviamente, também precisamos colocá-lo ao lado dos componentes.


Não há montagem de pacotes suficiente, conectamos todos os arquivos com as mãos em index.html , isso não é bom.


Não há problema em escrever um script que, através das listas de módulos que devem ser incluídos nos pacotes configuráveis, colete todos os js, html e css desses módulos e nos torne um js para cada pacote configurável. Será muito mais difícil e fácil do que configurar o webpack e, depois de um ano, descubra que já existe uma versão completamente diferente e você precisará reescrever a configuração e usar outros carregadores.


É recomendável ter algum tipo de sinalizador que suporte o esquema de conexão js / html / css com uma lista enorme em index.html . Não haverá atrasos na montagem e, no Sources in chrome, você terá cada arquivo em uma guia separada e não serão necessários mapas de origem.


PS


Essa é apenas uma das opções de como tudo pode estar usando o VanillaJS . Nos comentários, seria interessante ouvir sobre outros casos de uso.


Obrigado pela atenção.

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


All Articles