10 คะแนน โดย GN⁺ 2025-04-23 | 3 ความคิดเห็น | แชร์ทาง WhatsApp
  • 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 แต่ทุกองค์ประกอบสร้างขึ้นบนพื้นฐานของ ฟังก์ชัน

อธิบายโครงสร้าง

โครงสร้างโดยรวม

  • ประกอบด้วย templateclone()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 ความคิดเห็น

 
wfedev 2025-04-24

สุดท้ายก็ลงเอยที่ Web Components..

 
ahwjdekf 2025-04-23

ขอแสดงความยินดี มีเฟรมเวิร์ก js อีกตัวถือกำเนิดขึ้นแล้ว

 
GN⁺ 2025-04-23
ความคิดเห็นจาก Hacker News
  • สำหรับนักพัฒนา JS หลายคน นี่อาจเป็นความเห็นนอกรีต แต่คิดว่าตัวแปร state เป็น anti-pattern

    • เมื่อใช้เว็บคอมโพเนนต์ แทนที่จะเพิ่มตัวแปร state สำหรับชนิดตัวแปรแบบ "แบน" จะใช้ value/textContent/checked ขององค์ประกอบ DOM เป็นแหล่งความจริงเพียงหนึ่งเดียว
    • เพิ่ม setter และ getter เมื่อจำเป็น
    • แม้จำนวนโค้ดจะน้อย แต่หลายอย่างกลับทำงานได้อย่างเป็นธรรมชาติอย่างถูกต้อง
    • การใช้ WebComponents ทำให้ HTML template ที่อยู่ติดกับอ็อบเจ็กต์ถูกแยกออกมา จึงไม่ได้เป็น spaghetti code แต่เป็นการแยกย่อยละเอียดแบบ fusilli หรือ macaroni มากกว่า
  • เอกสารบอกว่าแนวทางนี้ดูแลรักษาได้ง่ายมาก แต่ไม่เห็นด้วย

    • design pattern อิงอยู่กับธรรมเนียมปฏิบัติเท่านั้น
    • เมื่อมีนักพัฒนาหลายคนทำงานพร้อมกันในแอปที่ซับซ้อน อย่างน้อยหนึ่งคนน่าจะหลุดจากธรรมเนียมปฏิบัติ
    • UI framework แบบอิงคลาสอย่าง UIKit ของ iOS บังคับให้นักพัฒนาทุกคนใช้ชุด API มาตรฐาน ทำให้โค้ดคาดเดาได้และดูแลรักษาง่าย
  • ช่วงนี้กำลังเขียนแอปพลิเคชันด้วย TypeScript แบบ "vanilla" ล้วน ๆ ร่วมกับ vite และเริ่มตั้งคำถามกับแนวปฏิบัติ "ที่ดีที่สุด" ของฝั่งฟรอนต์เอนด์มากขึ้นเรื่อย ๆ

    • ยังสรุปเรื่องการขยายขนาดไม่ได้ แต่ในด้านประสิทธิภาพมีข้อได้เปรียบชัดเจนมาก
    • สนุก ได้เรียนรู้อะไรเยอะ ดีบักง่าย และเข้าใจสถาปัตยกรรมได้ง่าย
    • สิ่งที่คิดถึงที่สุดคือ templating
  • แนวทางนี้ทำให้นึกถึงไลบรารี backbone js รุ่นเก่า

    • ยังมีที่เก็บ GitHub ที่มีตัวอย่างแพตเทิร์น MVC ซึ่งปรับให้เข้ากับเว็บแพลตฟอร์มด้วย
  • ไม่นานมานี้ก็คิดอะไรคล้าย ๆ กันขึ้นมาได้ แต่ไม่ได้ใช้องค์ประกอบ template

    • ใช้ฟังก์ชันและ template literal เพื่อคืนค่าสตริง แล้วใส่ลงใน innerHTML ขององค์ประกอบที่มีอยู่ หรือสร้างองค์ประกอบ div ใหม่แล้วใส่เข้าไป
    • ฟังก์ชันซ้อนกันจนจัดโครงสร้างให้ออกมาสมเหตุสมผลได้ยาก
  • โค้ดนี้ดูเหมือนกับโค้ดอัปเดตแบบแมนนวลที่ไลบรารี view แบบ reactive พยายามเข้ามาแทนที่ทุกประการ

  • เขียนโปรแกรมมาราว 20 ปีแล้ว แต่ก็ยังไม่คุ้นกับเฟรมเวิร์กฟรอนต์เอนด์

    • ถนัดฝั่งแบ็กเอนด์มากกว่า จึงคิดว่าการโต้ตอบที่เกี่ยวกับความปลอดภัยควรต้องผ่านเซิร์ฟเวอร์
    • มองว่า JS คือการเพิ่มความสามารถฝั่งไคลเอนต์บนฐาน HTML และ CSS ที่แข็งแรง
  • ใช้เฮลเปอร์ที่คล้ายกับ React.createElement

    • มีตัวอย่างการทำงานของ mock server dashboard อยู่
  • กำลังทำงานอยู่ที่ deja-vu.junglecoder.com โดยพยายามสร้าง JS toolkit สำหรับเครื่องมือที่อิง HTML

    • reactive/two-way data binding ที่เหมาะสมยังแก้ไม่ได้ แต่ grab/patch ค่อนข้างใช้ได้ดี
    • วิธีที่ใช้ template ทำให้ย้ายบางส่วนของ template ได้ง่ายมาก
  • หลังเรียนจบมหาวิทยาลัย งานทางการแรกคือทำเว็บเวอร์ชันของซอฟต์แวร์ Delphi

    • ตอนนั้นทีมกำลังเขียนฟรอนต์เอนด์ใหม่เป็นรอบที่สามอยู่แล้ว และต้องเปลี่ยนเฟรมเวิร์ก
    • เคยยืนกรานว่าควรเขียนเฟรมเวิร์กเอง แต่ทีมไม่ชอบข้อเสนอของตน
    • หลังจากนั้นก็ย้ายออกไปเพราะได้รับข้อเสนอที่ดีกว่าจากบริษัทอื่น
    • ต่อมาก็ลองทำเฟรมเวิร์กอีกตัวชื่อ tiny.js และกำลังใช้อยู่ในโปรเจกต์ส่วนตัว