การจำลอง React และ JSX ใน Vanilla JS

เผยแพร่แล้ว: 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 มาดูตัวอย่างของ JSX โดยใช้หนึ่งรายการจาก GitHub API และดูว่ามันแปลว่าอะไร

 <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 เสมือน (การแสดงเสมือนของหน้า) เพื่อติดตามการเปลี่ยนแปลงและการอัปเดต แทนที่จะเขียน HTML ใหม่ทั้งหมด React จะแก้ไข DOM ของหน้าทุกครั้งที่มีการอัปเดตข้อมูล นี่เป็นหนึ่งในปัญหาหลักที่ React สร้างขึ้นเพื่อแก้ไข

วิธีการ jQuery

นักพัฒนาเคยใช้ jQuery บ่อยมาก ฉันอยากจะพูดถึงมันที่นี่เพราะมันยังคงได้รับความนิยมและเพราะมันค่อนข้างใกล้เคียงกับโซลูชันใน JavaScript ล้วนๆ jQuery ได้รับการอ้างอิงถึงโหนด DOM (หรือคอลเลกชันของโหนด DOM) โดยการสอบถาม DOM นอกจากนี้ยังรวมการอ้างอิงนั้นด้วยฟังก์ชันต่างๆ สำหรับการปรับเปลี่ยนเนื้อหา

แม้ว่า jQuery จะมีเครื่องมือสร้าง DOM ของตัวเอง แต่สิ่งที่ฉันพบเห็นบ่อยที่สุดก็คือการต่อ HTML เท่านั้น ตัวอย่างเช่น เราสามารถแทรกโค้ด HTML ลงในโหนดที่เลือกโดยเรียกใช้ฟังก์ชัน html() ตามเอกสาร jQuery หากเราต้องการเปลี่ยนเนื้อหาของโหนด div ด้วยคลาส demo-container เราสามารถทำได้ดังนี้:

 $( "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 โดยตรงอาจมีประสิทธิภาพมากกว่า เราสามารถสร้างโหนด DOM ใหม่โดยใช้ฟังก์ชัน _document.createElement_ ซึ่งจะคืนค่าการอ้างอิงไปยังโหนดที่สร้างขึ้น การติดตามข้อมูลอ้างอิงเหล่านี้ทำให้เรามีวิธีง่ายๆ ในการแก้ไขเฉพาะโหนดที่มีส่วนที่จำเป็นต้องอัปเดต

การใช้โครงสร้างและแหล่งข้อมูลเดียวกันกับในตัวอย่าง 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);

หากสิ่งเดียวที่คุณนึกถึงคือประสิทธิภาพของการเรียกใช้โค้ด วิธีนี้เป็นวิธีที่ดีมาก อย่างไรก็ตาม ประสิทธิภาพไม่ได้วัดจากความเร็วในการดำเนินการเท่านั้น แต่ยังวัดความง่ายในการบำรุงรักษา ความสามารถในการปรับขนาด และความเป็นพลาสติกอีกด้วย ปัญหาของวิธีนี้คือมันละเอียดมากและบางครั้งก็ซับซ้อน เราจำเป็นต้องเขียนการเรียกใช้ฟังก์ชันจำนวนมากแม้ว่าเราจะเพียงสร้างโครงสร้างพื้นฐานก็ตาม ข้อเสียใหญ่ประการที่สองคือจำนวนตัวแปรที่สร้างและติดตาม สมมติว่าองค์ประกอบที่คุณกำลังทำงานอยู่ประกอบด้วยองค์ประกอบ DOM 30 องค์ประกอบ คุณจะต้องสร้างและใช้องค์ประกอบและตัวแปร DOM ที่แตกต่างกัน 30 รายการ คุณสามารถนำบางส่วนกลับมาใช้ใหม่ได้และเล่นกลโดยเสียค่าใช้จ่ายในการบำรุงรักษาและความยืดหยุ่น แต่อาจกลายเป็นเรื่องยุ่งและรวดเร็วจริงๆ

ข้อเสียที่สำคัญอีกประการหนึ่งคือเนื่องจากจำนวนบรรทัดของโค้ดที่คุณต้องเขียน เมื่อเวลาผ่านไป การย้ายองค์ประกอบจากผู้ปกครองรายหนึ่งไปยังอีกรายหนึ่งจะยากขึ้นเรื่อยๆ นั่นเป็นสิ่งหนึ่งที่ฉันซาบซึ้งจาก React ฉันสามารถดูไวยากรณ์ JSX และรับในไม่กี่วินาทีว่าโหนดใดมีอยู่ ที่ไหน และเปลี่ยนแปลงหากจำเป็น และแม้ว่าในตอนแรกอาจดูเหมือนไม่ใช่เรื่องใหญ่โต แต่โครงการส่วนใหญ่มีการเปลี่ยนแปลงอย่างต่อเนื่องที่จะทำให้คุณมองหาวิธีที่ดีกว่า

วิธีแก้ปัญหาที่เสนอ

การทำงานโดยตรงกับ DOM จะทำงานและทำงานให้เสร็จลุล่วง แต่ยังทำให้การสร้างหน้าละเอียดมาก โดยเฉพาะอย่างยิ่งเมื่อเราต้องการเพิ่มแอตทริบิวต์ HTML และโหนดที่ซ้อนกัน ดังนั้น แนวคิดคือการรวบรวมข้อดีบางประการของการทำงานร่วมกับเทคโนโลยี เช่น JSX และทำให้ชีวิตของเราง่ายขึ้น ข้อดีที่เรากำลังพยายามทำซ้ำมีดังต่อไปนี้:

  1. เขียนโค้ดในรูปแบบ HTML เพื่อให้การสร้างองค์ประกอบ DOM อ่านและแก้ไขได้ง่าย
  2. เนื่องจากเราไม่ได้ใช้ DOM เสมือนอย่างในกรณีของ React เราจึงต้องมีวิธีง่ายๆ ในการระบุและติดตามโหนดที่เราสนใจ

นี่คือฟังก์ชันง่ายๆ ที่จะทำสิ่งนี้ให้สำเร็จโดยใช้ข้อมูลโค้ด 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 และกรอกข้อมูล (หรืออัปเดต) ข้อมูลเมื่อพร้อมใช้งาน

บทสรุป

ในขณะที่พวกเราบางคนไม่ชอบความคิดที่จะเปลี่ยนไปใช้เฟรมเวิร์กและมอบการควบคุม สิ่งสำคัญคือเราต้องตระหนักถึงประโยชน์ของเฟรมเวิร์กเหล่านั้น มีเหตุผลที่พวกเขาเป็นที่นิยมมาก

และแม้ว่ากรอบงานอาจไม่เหมาะกับสไตล์หรือความต้องการของคุณเสมอไป แต่ฟังก์ชันและเทคนิคบางอย่างสามารถนำมาใช้ จำลอง หรือบางครั้งอาจแยกจากกรอบงานได้ บางสิ่งจะหายไปในการแปลเสมอ แต่สามารถได้รับและใช้งานจำนวนมากด้วยต้นทุนเพียงเล็กน้อยที่เฟรมเวิร์กดำเนินการ