如何使用VanillaJS在Github上进行用户搜索

你好 我叫Alexander,我是2018年的Vanilla ES5.1开发人员。


本文是对文章答案“如何在不使用React + RxJS 6 + Recompose的情况下在GitHub上搜索用户”的答复,该文章向我们展示了如何使用SvelteJS。


我建议您看一下其中一个选项,如何在不使用浏览器以外的任何依赖的情况下实现此目标。 此外, GitHub本身表示,他们正在开发没有框架的前端。


我们将进行相同的输入,显示GitHub用户板:



免责声明

本文绝对忽略了现代javascript和Web开发的所有可能实践。


准备工作


我们不需要配置和编写配置,只需创建具有所有必需布局的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 ,因此我们使用以下函数将所有内容放入对象中:


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

consumeTemplates() instantiateTemplate()函数为我们提供了DOM元素的深层副本,该副本将由consumeTemplates()函数从index.html#templates元素获取。


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

Dom.binding()函数接受一个元素,选项,搜索特定的数据属性,并对元素执行我们需要的操作。 例如,对于data-element属性,它向结果中添加一个字段,并带有指向被标记元素的链接;对于data-onedit属性, keyupchange处理程序将通过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; }); }); 

好吧, 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并不是很好,但是它可以工作并且我们忍受了。 如果突然发现一切都因此而变慢,我们会将数据写入存储的data-element 。 或者,我们将制作另一个绑定函数,该函数本身可以保存元素并可以读取新数据。 或者,我们将支持将选项转移到不仅是静态值的对象,还包括选项更改的流,以便绑定程序可以跟随它们。


指示加载/请求错误


我们假设这些表示形式也将是静态的,仅会在一个地方使用,并且它们具有自己的逻辑的机会非常小(与用户卡不同),因此我们不会为它们制作单独的组件。 它们只是应用程序组件的模板。


资料要求


我们将使用用户请求方法创建一个类,在这种情况下,我们可以轻松地将其实例替换为moch /其他实现:


 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因为它不支持请求的中断,也出于相同的原因,我们也不希望与promise进行通信。


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

最终申请


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。 这比设置webpack更加笨拙和容易,并且在一年后发现已经存在一个完全不同的版本,您需要重写配置并使用其他加载程序。


建议使用某种标志来支持js / html / css连接方案,并在index.html提供大量列表。 这样就不会有任何延迟,并且在chrome中的Sources中,您可以将每个文件放在单独的标签中,并且不需要sourcemap。


聚苯乙烯


这只是可以全部使用VanillaJS的选项之一 。 在评论中,听到其他用例会很有趣。


谢谢您的关注。

Source: https://habr.com/ru/post/zh-CN419893/


All Articles