- Writing JavaScript Views the Hard Way : บทความที่อธิบายวิธีสร้างวิวด้วย JavaScript ล้วนโดยไม่ใช้เฟรมเวิร์ก
- ใช้ แนวทางเชิงคำสั่งโดยตรง เพื่อให้ได้ทั้งประสิทธิภาพ การบำรุงรักษา และความสามารถในการพกพา
- แยก การอัปเดตสถานะ และ การอัปเดต DOM ออกจากกันอย่างชัดเจน และใช้ กฎการตั้งชื่อที่เข้มงวดกับแพตเทิร์นเชิงโครงสร้าง ตามบทบาทของแต่ละส่วน
- วิธีนี้มีข้อดีใหญ่คือ ดีบักง่าย, รับประกัน ความเข้ากันได้กับทุกเบราว์เซอร์ และเป็นแบบ 0 dependencies
- แม้อาจยากสำหรับผู้เริ่มต้น แต่เมื่อเรียนรู้แล้วจะช่วยให้ เข้าใจเชิงลึกว่าระบบจริงทำงานอย่างไร
เขียน JavaScript View แบบ 'Hard Way'
นี่คืออะไร?
- วิธีนี้คือ แพตเทิร์นสำหรับประกอบวิวด้วย JavaScript เพียงอย่างเดียว โดยไม่ใช้เฟรมเวิร์กอย่าง React, Vue, lit-html
- มันไม่ใช่ไลบรารีหรือเครื่องมือเฉพาะ แต่เป็น ตัวแพตเทิร์นการเขียนโค้ดเอง ที่ช่วยป้องกันปัญหาโค้ดสปาเก็ตตี
- การใช้ แนวทางเชิงคำสั่งโดยตรง ช่วยลดชั้นนามธรรมและเพิ่มความตรงไปตรงมา
ข้อดีเมื่อเทียบกับเฟรมเวิร์ก
- ประสิทธิภาพ: โค้ดเชิงคำสั่งทำงานได้โดยไม่มีการคำนวณที่ไม่จำเป็น และเหมาะทั้งกับ hot path และ cold path
- 0 dependencies: ไม่ต้องกังวลเรื่องอัปเกรดไลบรารีหรือปัญหาความเข้ากันได้
- ความสามารถในการพกพา: โค้ดที่เขียนสามารถย้ายไปใช้กับเฟรมเวิร์กใดก็ได้
- การบำรุงรักษา: โครงสร้างส่วนต่างๆ และกฎการตั้งชื่อที่ชัดเจน ทำให้หาตำแหน่งโค้ดได้ง่าย
- รองรับเบราว์เซอร์: ใช้งานร่วมกับเบราว์เซอร์ส่วนใหญ่ตั้งแต่ IE9 ขึ้นไป และยังรองรับถึง IE6 ได้ด้วยการปรับแต่งบางส่วน
- ดีบักง่าย: ไม่มีเลเยอร์คั่นกลาง จึงได้ stack trace แบบตื้น
- โครงสร้างเชิงฟังก์ชัน: แม้ไม่ใช่ immutable แต่ทุกองค์ประกอบสร้างขึ้นบนพื้นฐานของ ฟังก์ชัน
อธิบายโครงสร้าง
โครงสร้างโดยรวม
- ประกอบด้วย
template → clone() → init()
- ฟังก์ชัน
init() จะสร้างวิวอินสแตนซ์หนึ่งชุดที่มี ตัวแปรสถานะ, การอ้างอิง DOM, ฟังก์ชันอัปเดต, event listener เป็นต้น
ตัวอย่างโครงสร้างโค้ด (Hello World)
const template = document.createElement('template');
template.innerHTML = `<div>Hello <span id="name">world</span>!</div>`;
function clone() {
return document.importNode(template.content, true);
}
function init() {
let frag = clone();
let nameNode = frag.querySelector('#name');
let name;
function setNameNode(value) {
nameNode.textContent = value;
}
function setName(value) {
if(name !== value) {
name = value;
setNameNode(value);
}
}
function update(data = {}) {
if(data.name) setName(data.name);
return frag;
}
return update;
}
องค์ประกอบภายในฟังก์ชัน init()
1. ตัวแปร DOM
frag คือชิ้นส่วนเทมเพลตที่สร้างจาก clone()
- องค์ประกอบภายในอ้างอิงด้วย
querySelector() และใช้ชื่อตัวแปรในรูปแบบ fooNode
2. DOM View
- ส่วนที่รวมวิวอื่นไว้ภายใน (sub-view ที่นำกลับมาใช้ซ้ำได้)
- ตัวอย่าง:
let updateChildView = childView();
- ฟังก์ชันอัปเดตวิวตั้งชื่อในรูปแบบ
updateFoo
3. ตัวแปรสถานะ
- ค่าข้อมูลที่สามารถเปลี่ยนแปลงได้ภายในวิว
- เพื่อให้อัปเดต DOM ได้อย่างมีประสิทธิภาพ จะเทียบกับค่าปัจจุบันก่อนและเปลี่ยน DOM เฉพาะเมื่อจำเป็น
4. ฟังก์ชันอัปเดต DOM
- ใช้เมื่อจำเป็นต้องเปลี่ยนสถานะขององค์ประกอบ DOM
- ตัวอย่าง:
function setNameNode(value) {
nameNode.textContent = value;
}
- การจัดการ DOM ต้องทำภายในฟังก์ชันนี้เท่านั้น
5. ฟังก์ชันอัปเดตสถานะ
- รวมทั้งตรรกะการเปลี่ยนสถานะและการสะท้อนไปยัง DOM
- ค่าที่ไม่เปลี่ยนจะถูกละเลยเพื่อ ป้องกันการแก้ไข DOM ที่ไม่จำเป็น
- ตัวอย่าง:
function setName(value) {
if(name !== value) {
name = value;
setNameNode(value);
}
}
ฟังก์ชัน template และ clone()
template
- ใช้องค์ประกอบ
<template> เพื่อสร้างโครงสร้าง HTML แบบคงที่
- มันจะไม่ถูกแทรกลง DOM โดยตรง แต่จะสร้างสำเนาผ่าน clone
clone()
- โคลนด้วย
document.importNode(template.content, true)
- หากจำเป็น สามารถใช้
.firstElementChild เพื่อคืนค่า root element ได้
รูปแบบการโต้ตอบ
การไหลของข้อมูลจากพาเรนต์ → ชิลด์
- พาเรนต์จะเรียก
init() ของชิลด์เพื่อรับ ฟังก์ชันอัปเดต แล้วเรียกในรูปแบบ update({ name: 'foo' })
การกระจายข้อมูลแบบอิงอีเวนต์
- โดยพื้นฐานใช้โมเดล props down, events up
- วิวชั้นล่างจะ dispatch อีเวนต์ขึ้นไปยังชั้นบนเพื่อสื่อสารกัน
เปรียบเทียบกับ React
constructor() (React) → init() (Hard Way)
- ทำหน้าที่ตั้งค่าเริ่มต้นของคอมโพเนนต์
render() (React) → update(data) (Hard Way)
- ทำหน้าที่รีเฟรชหน้าจอและอัปเดต UI
this.setState() (React) → setX(value) (Hard Way)
- ถูกแทนที่ด้วยวิธีตั้งค่าสถานะโดยตรง
props (React) → ค่าที่ส่งผ่าน update(data) (Hard Way)
- วิธีจัดการข้อมูลที่ส่งมาจากคอมโพเนนต์พาเรนต์
- JSX / Virtual DOM (React) → HTML template + DOM API (Hard Way)
- ใช้เทมเพลตและการจัดการ DOM แบบแมนนวลแทน UI แบบประกาศ
สรุป
- วิธีนี้แม้จะมี กำแพงการเริ่มต้นสูงกว่า เฟรมเวิร์กที่คุ้นเคย แต่ก็มีจุดแข็งดังนี้:
- การปรับแต่งประสิทธิภาพ
- การควบคุมได้อย่างสมบูรณ์
- ความเข้าใจเชิงลึกผ่านการเรียนรู้
- ด้วยการแยกฟังก์ชันตามบทบาทและกฎการตั้งชื่อ ทำให้สามารถ สร้าง UI ที่ดูแลรักษาได้โดยไม่ต้องพึ่งเฟรมเวิร์ก
ความเข้ากันได้
- แม้ตัวอย่างล่าสุดจะใช้ API สำหรับเบราว์เซอร์สมัยใหม่ แต่ก็ยังรองรับถึง IE9 หรือต่ำกว่านั้นได้ผ่าน ทางเลือกแบบฟังก์ชัน
- และยังขยายไปถึง IE6 ได้ด้วยแนวทาง ส่งฟังก์ชันผ่าน props แทนการใช้อีเวนต์
3 ความคิดเห็น
สุดท้ายก็ลงเอยที่ Web Components..
ขอแสดงความยินดี มีเฟรมเวิร์ก js อีกตัวถือกำเนิดขึ้นแล้ว
ความคิดเห็นจาก Hacker News
สำหรับนักพัฒนา JS หลายคน นี่อาจเป็นความเห็นนอกรีต แต่คิดว่าตัวแปร
stateเป็น anti-patternเอกสารบอกว่าแนวทางนี้ดูแลรักษาได้ง่ายมาก แต่ไม่เห็นด้วย
ช่วงนี้กำลังเขียนแอปพลิเคชันด้วย TypeScript แบบ "vanilla" ล้วน ๆ ร่วมกับ vite และเริ่มตั้งคำถามกับแนวปฏิบัติ "ที่ดีที่สุด" ของฝั่งฟรอนต์เอนด์มากขึ้นเรื่อย ๆ
แนวทางนี้ทำให้นึกถึงไลบรารี backbone js รุ่นเก่า
ไม่นานมานี้ก็คิดอะไรคล้าย ๆ กันขึ้นมาได้ แต่ไม่ได้ใช้องค์ประกอบ template
โค้ดนี้ดูเหมือนกับโค้ดอัปเดตแบบแมนนวลที่ไลบรารี view แบบ reactive พยายามเข้ามาแทนที่ทุกประการ
เขียนโปรแกรมมาราว 20 ปีแล้ว แต่ก็ยังไม่คุ้นกับเฟรมเวิร์กฟรอนต์เอนด์
ใช้เฮลเปอร์ที่คล้ายกับ React.createElement
กำลังทำงานอยู่ที่ deja-vu.junglecoder.com โดยพยายามสร้าง JS toolkit สำหรับเครื่องมือที่อิง HTML
หลังเรียนจบมหาวิทยาลัย งานทางการแรกคือทำเว็บเวอร์ชันของซอฟต์แวร์ Delphi