在 Vanilla JS 中模擬 React 和 JSX

已發表: 2022-03-11

很少有人不喜歡框架,但即使您是其中之一,您也應該注意並採用使生活更輕鬆的功能。

我過去反對使用框架。 然而,最近,我在一些項目中獲得了使用 React 和 Angular 的經驗。 前幾次我打開我的代碼編輯器並開始用 Angular 編寫代碼,感覺很奇怪和不自然。 尤其是在沒有使用任何框架的情況下進行了十多年的編碼之後。 一段時間後,我決定致力於學習這些技術。 很快,一個很大的區別就變得明顯了:操作 DOM 非常容易,在需要時調整節點的順序非常容易,而且構建 UI 不需要一頁又一頁的代碼。

儘管我仍然更喜歡不依賴於框架或架構的自由,但我不能忽視這樣一個事實,即在一個框架中創建 DOM 元素要方便得多。 所以我開始尋找在 vanilla JS 中模擬體驗的方法。 我的目標是從 React 中提取其中的一些想法,並演示如何在純 JavaScript(通常稱為 vanilla JS)中實現相同的原則,以使開發人員的生活更輕鬆一些。 為此,讓我們構建一個簡單的應用程序來瀏覽 GitHub 項目。

簡單的 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 等技術的一些好處,讓我們的生活更簡單。 我們試圖複製的優勢如下:

  1. 以 HTML 語法編寫代碼,以便 DOM 元素的創建變得易於閱讀和修改。
  2. 由於我們沒有像 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 行代碼中完成。

該功能分三個步驟工作:

  1. 創建一個新的空節點並在該新節點中使用 innerHTML 來創建整個 DOM 結構。
  2. 遍歷節點,如果存在 var 屬性,則在範圍對像中添加一個屬性,該屬性指向具有該屬性的節點。
  3. 返回層次結構中的頂部節點,如果有多個,則返回文檔片段。

那麼我們渲染示例的代碼現在看起來如何呢?

 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 結構並在數據可用時填充(或更新)數據。

結論

雖然我們中的一些人不喜歡切換到框架並移交控制權的想法,但重要的是我們要認識到這些框架帶來的好處。 它們如此受歡迎是有原因的。

雖然框架可能並不總是適合您的風格或需求,但可以採用、模擬或有時甚至與框架分離一些功能和技術。 有些東西在翻譯中總是會丟失,但可以以框架所承擔成本的一小部分獲得和使用很多東西。