在 Vanilla JS 中模拟 React 和 JSX
已发表: 2022-03-11很少有人不喜欢框架,但即使您是其中之一,您也应该注意并采用使生活更轻松的功能。
我过去反对使用框架。 然而,最近,我在一些项目中获得了使用 React 和 Angular 的经验。 前几次我打开我的代码编辑器并开始用 Angular 编写代码,感觉很奇怪和不自然。 尤其是在没有使用任何框架的情况下进行了十多年的编码之后。 一段时间后,我决定致力于学习这些技术。 很快,一个很大的区别就显现出来了:操作 DOM 非常容易,在需要时调整节点的顺序非常容易,而且构建 UI 不需要一页又一页的代码。
尽管我仍然更喜欢不依赖于框架或架构的自由,但我不能忽视一个事实,即在一个中创建 DOM 元素要方便得多。 所以我开始寻找在 vanilla JS 中模拟体验的方法。 我的目标是从 React 中提取其中一些想法,并演示如何在纯 JavaScript(通常称为 vanilla JS)中实现相同的原则,以使开发人员的生活更轻松一些。 为此,让我们构建一个简单的应用程序来浏览 GitHub 项目。
无论我们使用 JavaScript 构建前端的哪种方式,我们都将访问和操作 DOM。 对于我们的应用程序,我们需要构建每个存储库的表示(缩略图、名称和描述),并将其作为列表元素添加到 DOM。 我们将使用 GitHub Search API 来获取我们的结果。 而且,由于我们正在讨论 JavaScript,让我们搜索 JavaScript 存储库。 当我们查询 API 时,我们会得到以下 JSON 响应:
{ "total_count": 398819, "incomplete_results": false, "items": [ { "id": 28457823, "name": "freeCodeCamp", "full_name": "freeCodeCamp/freeCodeCamp", "owner": { "login": "freeCodeCamp", "id": 9892522, "avatar_url": "https://avatars0.githubusercontent.com/u/9892522?v=4", "gravatar_id": "", "url": "https://api.github.com/users/freeCodeCamp", "site_admin": false }, "private": false, "html_url": "https://github.com/freeCodeCamp/freeCodeCamp", "description": "The https://freeCodeCamp.org open source codebase "+ "and curriculum. Learn to code and help nonprofits.", // more omitted information }, //... ] }
React 的方法
React 使得将 HTML 元素写入页面变得非常简单,这是我在使用纯 JavaScript 编写组件时一直想要拥有的功能之一。 React 使用 JSX,它与常规 HTML 非常相似。
但是,这不是浏览器读取的内容。
在底层,React 将 JSX 转换为一组对React.createElement
函数的调用。 让我们看一个使用 GitHub API 中的一项的 JSX 示例,看看它转换成什么。
<div className="repository"> <div>{item.name}</div> <p>{item.description}</p> <img src={item.owner.avatar_url} /> </div>;
; React.createElement( "div", { className: "repository" }, React.createElement( "div", null, item.name ), React.createElement( "p", null, item.description ), React.createElement( "img", { src: item.owner.avatar_url } ) );
JSX 非常简单。 您编写常规 HTML 代码并通过添加大括号从对象中注入数据。 括号内的 JavaScript 代码将被执行,并且值将被插入到生成的 DOM 中。 JSX 的优点之一是 React 创建了一个虚拟 DOM(页面的虚拟表示)来跟踪更改和更新。 每当信息更新时,React 都会修改页面的 DOM,而不是重写整个 HTML。 这是 React 旨在解决的主要问题之一。
jQuery 方法
开发人员过去经常使用 jQuery。 我想在这里提一下,因为它仍然很流行,也因为它非常接近纯 JavaScript 中的解决方案。 jQuery 通过查询 DOM 获取对 DOM 节点(或 DOM 节点集合)的引用。 它还使用各种功能包装该引用以修改其内容。
虽然 jQuery 有自己的 DOM 构建工具,但我最常看到的只是 HTML 连接。 例如,我们可以通过调用html()
函数将 HTML 代码插入到选定的节点中。 根据 jQuery 文档,如果我们想使用类demo-container
更改div
节点的内容,我们可以这样做:
$( "div.demo-container" ).html( "<p>All new content.<em>You bet!</em></p>" );
这种方法可以很容易地创建 DOM 元素; 但是,当我们需要更新节点时,我们需要查询我们需要的节点,或者(更常见的是)在需要更新时回退到重新创建整个片段。
DOM API 方法
浏览器中的 JavaScript 有一个内置的 DOM API,它让我们可以直接访问创建、修改和删除页面中的节点。 这反映在 React 的方法中,通过使用 DOM API,我们离这种方法的好处更近了一步。 我们只修改页面中实际需要更改的元素。 然而,React 也跟踪一个单独的虚拟 DOM。 通过比较虚拟 DOM 和实际 DOM 之间的差异,React 能够识别哪些部分需要修改。
这些额外的步骤有时很有用,但并非总是如此,直接操作 DOM 会更有效。 我们可以使用_document.createElement_
函数创建新的 DOM 节点,该函数将返回对所创建节点的引用。 跟踪这些引用为我们提供了一种简单的方法来仅修改包含需要更新的部分的节点。
使用与 JSX 示例中相同的结构和数据源,我们可以通过以下方式构造 DOM:
var item = document.createElement('div'); item.className = 'repository'; var nameNode = document.createElement('div'); nameNode.innerHTML = item.name item.appendChild(nameNode); var description = document.createElement('p'); description.innerHTML = item.description; item.appendChild(description ); var image = new Image(); Image.src = item.owner.avatar_url; item.appendChild(image); document.body.appendChild(item);
如果你只关心代码执行的效率,那么这种方法非常好。 然而,效率不仅仅体现在执行速度上,还体现在易于维护、可扩展性和可塑性上。 这种方法的问题是它非常冗长,有时令人费解。 即使我们只是构建一个基本结构,我们也需要编写一堆函数调用。 第二大缺点是创建和跟踪的变量数量庞大。 假设您正在使用的组件包含 30 个 DOM 元素,您将需要创建和使用 30 个不同的 DOM 元素和变量。 您可以重用其中的一些并以可维护性和可塑性为代价做一些杂耍,但它可能会变得非常混乱,非常快。

另一个显着的缺点是您需要编写的代码行数。 随着时间的推移,将元素从一个父级移动到另一个父级变得越来越困难。 这是我非常欣赏 React 的一件事。 我可以查看 JSX 语法并在几秒钟内获得包含哪个节点、在哪里,并在需要时进行更改。 而且,虽然一开始看起来没什么大不了的,但大多数项目都会不断变化,这会让你寻找更好的方法。
建议的解决方案
直接使用 DOM 可以工作并完成工作,但它也使得构建页面非常冗长,尤其是当我们需要添加 HTML 属性和嵌套节点时。 因此,我们的想法是利用 JSX 等技术的一些好处,让我们的生活更简单。 我们试图复制的优势如下:
- 以 HTML 语法编写代码,以便 DOM 元素的创建变得易于阅读和修改。
- 由于我们没有像 React 那样使用等效的虚拟 DOM,因此我们需要一种简单的方法来指示和跟踪我们感兴趣的节点。
这是一个简单的函数,可以使用 HTML 片段来完成此任务。
Browser.DOM = function (html, scope) { // Creates empty node and injects html string using .innerHTML // in case the variable isn't a string we assume is already a node var node; if (html.constructor === String) { var node = document.createElement('div'); node.innerHTML = html; } else { node = html; } // Creates of uses and object to which we will create variables // that will point to the created nodes var _scope = scope || {}; // Recursive function that will read every node and when a node // contains the var attribute add a reference in the scope object function toScope(node, scope) { var children = node.children; for (var iChild = 0; iChild < children.length; iChild++) { if (children[iChild].getAttribute('var')) { var names = children[iChild].getAttribute('var').split('.'); var obj = scope; while (names.length > 0) { var _property = names.shift(); if (names.length == 0) { obj[_property] = children[iChild]; } else { if (!obj.hasOwnProperty(_property)){ obj[_property] = {}; } obj = obj[_property]; } } } toScope(children[iChild], scope); } } toScope(node, _scope); if (html.constructor != String) { return html; } // If the node in the highest hierarchy is one return it if (node.childNodes.length == 1) { // if a scope to add node variables is not set // attach the object we created into the highest hierarchy node // by adding the nodes property. if (!scope) { node.childNodes[0].nodes = _scope; } return node.childNodes[0]; } // if the node in highest hierarchy is more than one return a fragment var fragment = document.createDocumentFragment(); var children = node.childNodes; // add notes into DocumentFragment while (children.length > 0) { if (fragment.append){ fragment.append(children[0]); }else{ fragment.appendChild(children[0]); } } fragment.nodes = _scope; return fragment; }
这个想法很简单但很强大; 我们将要创建的 HTML 作为字符串发送给函数,在 HTML 字符串中,我们向要为我们创建引用的节点添加 var 属性。 第二个参数是将存储这些引用的对象。 如果未指定,我们将在返回的节点或文档片段上创建一个“节点”属性(以防最高层次节点超过一个)。 一切都在不到 60 行代码中完成。
该功能分三个步骤工作:
- 创建一个新的空节点并在该新节点中使用 innerHTML 来创建整个 DOM 结构。
- 遍历节点,如果存在 var 属性,则在范围对象中添加一个属性,该属性指向具有该属性的节点。
- 返回层次结构中的顶部节点,如果有多个,则返回文档片段。
那么我们渲染示例的代码现在看起来如何呢?
var UI = {}; var template = ''; template += '<div class="repository">' template += ' <div var="name"></div>'; template += ' <p var="text"></p>' template += ' <img var="image"/>' template += '</div>'; var item = Browser.DOM(template, UI); UI.name.innerHTML = data.name; UI.text.innerHTML = data.description; UI.image.src = data.owner.avatar_url;
首先,我们定义对象 (UI),我们将在其中存储对已创建节点的引用。 然后,我们将要使用的 HTML 模板编写为字符串,用“var”属性标记目标节点。 之后,我们使用模板和将存储引用的空对象调用函数 Browser.DOM。 最后,我们使用存储的引用将数据放置在节点内。
这种方法还将构建 DOM 结构和将数据插入到单独的步骤中分开,这有助于保持代码的组织和结构良好。 这使我们能够单独创建 DOM 结构并在数据可用时填充(或更新)数据。
结论
虽然我们中的一些人不喜欢切换到框架并移交控制权的想法,但重要的是我们要认识到这些框架带来的好处。 它们如此受欢迎是有原因的。
虽然框架可能并不总是适合您的风格或需求,但可以采用、模拟或有时甚至与框架分离一些功能和技术。 有些东西在翻译中总是会丢失,但可以以框架所承担成本的一小部分获得和使用很多东西。