3 คะแนน โดย GN⁺ 2024-08-17 | 1 ความคิดเห็น | แชร์ทาง WhatsApp

ตัวอย่างของ Vanilla JSX

ถ้า JSX คืนค่าเป็นองค์ประกอบ DOM ล่ะ?

  • ฟังก์ชัน ClickMe มีหน้าที่สร้างปุ่มและแสดงจำนวนครั้งที่คลิก
  • ทุกครั้งที่คลิกปุ่ม ข้อความจะถูกอัปเดต
export default function ClickMe() {
  let i = 0;
  const el = Click me as HTMLButtonElement;
  el.onclick = (e) => {
    el.textContent = `Clicked ${++i} times`;
  };
  return el;
}

การนำกลับมาใช้ซ้ำ

  • สามารถใช้คอมโพเนนต์ ClickMe ได้หลายครั้ง โดยแต่ละตัวจะคงสถานะของตัวเองไว้
import ClickMe from "./sample1.js";
export default () =>

;

การสร้าง DOM tree แบบโต้ตอบได้

  • สามารถใช้คลาส TodoInput และ TodoList เพื่อจัดการรายการสิ่งที่ต้องทำได้
  • เพิ่มรายการและคลิกเพื่อลบออกได้
function TodoInput(attrs: { add: (v: string) => void }) {
  const input =  as HTMLInputElement;
  input.placeholder = 'Add todo item...';
  input.onkeydown = (e) => {
    if (e.key === 'Enter') {
      attrs.add(input.value);
      input.value = '';
    }
  };
  return input;
}

class TodoList {
  ul =  as HTMLUListElement;
  add(v: string) {
    const item =
- {v}
 as HTMLLIElement;
    item.onclick = () => item.remove();
    this.ul.append(item);
  }
}

export default () => {
  const list = new TodoList();
  list.add('foo');
  list.add('bar');
  return
     list.add(v)} />
    {list.ul}
  ;
};

การประมวลผลข้อมูลขนาดใหญ่

  • ฟังก์ชัน FindNames ประมวลผลและกรองข้อมูลจำนวนมากเพื่อแสดงผลลัพธ์
  • อัปเดตรายการที่ตรงกันแบบเรียลไทม์ตามค่าที่ป้อน
import { data } from "../fetch-dataset.js";
export default function FindNames() {
  const status =  as HTMLParagraphElement;
  const results =  as HTMLUListElement;
  const input =  as HTMLInputElement;
  updateMatches();

  function updateMatches() {
    const matched = (data.entries().filter(([k]) => k.match(input.value)).toArray());
    const matches = (Iterator.from(matched).map(match => ).take(30));
    results.replaceChildren(...matches);
    status.textContent = `${matched.length} / ${data.size}`;
  }

  return
    {input}
    {status}
    {results}

;
}

function Item(attrs: { match: [string, number], regex: string }) {
  const [name, count] = attrs.match;
  const total = ({count});
  return
-
     {total}

;
}

function highlight(str: string, regex: string) {
  if (!regex) return str;
  const r = new RegExp(`(${regex})`, 'gi');
  return str.replace(r, '$1');
}

แนะนำ imlib

  • imlib เป็นไลบรารีที่พัฒนาขึ้นสำหรับ immaculatalibrary.com
  • ถูกใช้ในการสร้าง minigamemaker.com และเว็บไซต์ที่คุณกำลังอ่านอยู่ในตอนนี้
  • ถูกพัฒนาขึ้นเพราะสถานะเดิมที่มีอยู่นั้นไม่เพียงพอ และเป็นวิธีที่ผู้พัฒนาชอบที่สุดในการสร้างแอป

สรุปโดย GN⁺

  • บทความนี้อธิบายวิธีสร้างและโต้ตอบกับองค์ประกอบ DOM โดยตรงด้วย JSX
  • นำเสนอวิธีจัดการข้อมูลจำนวนมากอย่างมีประสิทธิภาพได้โดยไม่ต้องใช้ virtual DOM แบบเดิม
  • ไลบรารี imlib ช่วยให้พัฒนาแอปได้ด้วยวิธีที่เรียบง่ายและตรงไปตรงมา
  • โปรเจกต์อื่นที่มีความสามารถคล้ายกัน ได้แก่ React, Vue.js เป็นต้น

1 ความคิดเห็น

 
GN⁺ 2024-08-17
ความคิดเห็นจาก Hacker News
  • ถ้าส่งคืน โหนด DOM จริง ๆ ก็แทบจะทำให้จุดแข็งใหญ่ของ JSX และไลบรารีที่ไม่ใช่ JSX อย่าง Lit—คือ API แบบ immediate mode และโมเดล UI=f(state)—หมดพลังไป
    ต้องส่งคืนคำอธิบายของ DOM ไม่ใช่ DOM จริง จึงจะประเมินเทมเพลตซ้ำด้วยสถานะใหม่ แล้วอัปเดต DOM ที่เทมเพลตนั้นเรนเดอร์ออกมาได้อย่างมีประสิทธิภาพ
    ตัวอย่างต่าง ๆ อัปเดตด้วย API DOM แบบ imperative เช่น input.onkeydown = ... หรือ this.ul.append(item) แต่การหลีกเลี่ยงโค้ดแบบนี้ แล้ววนรายการในเทมเพลต น่าจะเป็นข้อดีหลักของ Virtual DOM
    อย่างไรก็ดี ปัญหาของ Virtual DOM คือการคำนวณ diff ช้า ซึ่งแก้ได้ด้วยเทมเพลตที่แยกส่วนคงที่กับส่วนไดนามิกออกจากกันแบบ Lit

    • จากประสบการณ์ที่เคยสร้างเว็บ UI ซับซ้อนมา ผมมองว่าในแอปที่ซับซ้อนพอ ข้อได้เปรียบด้านประสิทธิภาพของ Virtual DOM แทบจะเป็นภาพลวงตา
      กลับกัน มันทำให้คิดว่า “จะ re-render UI ใหญ่ ๆ ตามใจยังไง Virtual DOM ก็คงจัดการให้” ซึ่งให้ผลตรงข้าม ประสิทธิภาพจริงแย่ แต่โครงสร้างแอปก็แข็งตัวไปแล้วจนแก้ยากหากไม่เขียนใหม่
      ผมเห็นรูปแบบเดียวกันนี้ในสี่ทีม และในฐานะสถาปนิก ผมเคยแนะนำโครงสร้าง vanilla ที่คำนึงถึงประสิทธิภาพโดยอ้างอิงปัญหาที่จะเจอใน React แต่ก็ไม่ถูกยอมรับ เพราะถูกมองว่า “มีวิธีหลีกเลี่ยงเยอะ”
      สุดท้ายหลายกรณีก็คงต้องเรียนรู้จากความเจ็บปวดด้วยตัวเอง
    • เรื่องนี้ขึ้นอยู่กับผู้ใช้ มองอีกมุมหนึ่ง JSX ก็เป็นเพียง DSL อีกรูปแบบหนึ่งของแพตเทิร์นการเรียกฟังก์ชัน createElement และข้อดีที่พูดถึงนั้นมาจากเฟรมเวิร์กกับ runtime
      พูดให้สั้นลง JSX คือไวยากรณ์ทางเลือกสำหรับการเรียกฟังก์ชันที่มีกรณีใช้งานที่เป็นประโยชน์
      บริษัทเก่าของผมเคยใช้ JSX กับ factory สำหรับข้อมูลทดสอบ มันดูเหมือน XML แต่ในขั้นตอนสร้างองค์ประกอบสามารถตัดสินใจตามบริบทให้เหมาะกับข้อมูลสุดท้ายได้ด้วย builder pattern
      ไม่เกี่ยวกับ React หรือ DOM และไม่ได้หมายความว่าถ้าไม่มี JSX แล้วจะอธิบายแบบ declarative ไม่ได้
    • “สิ่งที่ต้องการ” ขึ้นอยู่กับว่าใครเป็นคนต้องการ โดยส่วนตัวผมชอบอัปเดตโหนด DOM แบบ in-place โดยไม่ต้องมี กลไก reconciliation ใด ๆ มากกว่า
    • ผมเข้าใจความฝันที่ว่า “ไม่ต้องทำการอัปเดตแบบ imperative อีกต่อไป” ใช้เฟรมเวิร์กแบบนี้มาราว 10 ปีแล้วและคุ้นเคยดี
      ส่วนตัวผมชอบ อัปเดต DOM แบบ imperative มากกว่า ควบคุมได้ละเอียดมากว่าเกิดอะไรขึ้น และออกแบบให้มีโครงสร้างที่มีประสิทธิภาพสูงได้
      ทำให้เพิ่ม เปลี่ยน ลบ แก้ไขฟีเจอร์ของแอปได้ง่าย โดยไม่ต้องคิดให้เข้ากับแนวทางอันเข้มงวดของใครบางคน
    • เห็นด้วยกับที่บอกว่าถ้าส่งคืนโหนด DOM จริง จะทำให้ข้อดีของ JSX และไลบรารีที่ไม่ใช่ JSX อย่าง Lit—คือ API แบบ immediate mode และโมเดล UI=f(state)—อ่อนลง แต่สิ่งนี้ให้ความรู้สึกเหมือนเป็น abstraction ที่รั่ว ขั้นสุดในโลกคอมพิวติ้ง
      มีเหตุผลที่รอบ ๆ React เกิด ecosystem เกี่ยวกับวิธีไม่ให้ render หลายครั้ง หรือวิธีป้องกัน refresh ที่ไม่จำเป็น
  • ผมสร้างโปรเจกต์นี้เพราะรู้สึกอึดอัดกับสภาพของ static site generator ตลอด 10 ปีที่ผ่านมา
    เนื่องจากส่วนใหญ่ทำเว็บไซต์แบบ static ผมจึงต้องการเครื่องมือที่เรียบง่ายและตรงไปตรงมา และ JSX ดูเข้ากันดี แต่ก็เหนื่อยกับความซับซ้อนเกินจำเป็นของเฟรมเวิร์ก JSX อย่าง React
    เลยสร้าง static site generator ที่ render JSX เป็นสตริง และการขยายแนวทางนี้ไปฝั่งเบราว์เซอร์ให้ render JSX เป็นองค์ประกอบ DOM ก็เป็นเรื่องเป็นธรรมชาติ
    ในบางกรณี โดยเฉพาะ layout มันเข้ากับคอมโพเนนต์ที่แชร์กันได้ดี โดยรวมพอใจกับผลลัพธ์ แต่บางส่วนก็ค่อนข้าง hacky และการรองรับจาก IDE ยังดีขึ้นได้อีก
    อีกอย่าง วิธีนี้ยังเหมาะกับ SEO ด้วย ซึ่งเป็นปัญหาที่ผมไม่รู้สึกว่าเฟรมเวิร์ก JSX อื่นแก้ได้ดีจริง ๆ

    • อยากรู้ว่าเคยดู Astro หรือยัง มันให้ความรู้สึกเหมือน static site generator ที่สมบูรณ์แบบ แต่ VanillaJSX อาจมีจุดคันอื่นที่ต้องการแก้อยู่ก็ได้
    • ไอเดียดี หน้าเดโมของแอป todo ขึ้น 404
      ในเอกสาร น่าจะเขียนเต็มว่า “static site generator” แทน “SSG” จะดีกว่า ผมเคยใช้ static site generator มาแล้ว แต่ยังไม่รู้ว่า SSG คืออะไรจนต้องไปถาม AI
    • ถ้ามี JSX สำหรับฝั่งเซิร์ฟเวอร์ของ Django ก็น่าจะเจ๋ง
    • อยากรู้ว่าเคยดู lit-html หรือยัง
      ผมย้ายมาจาก Vue แล้วแปลกใจมาก เพราะ lit-html ทำหลายอย่างที่เทมเพลตของ Vue ทำได้ รวมถึงการผูก event โดยใช้แค่เทมเพลต vanilla JS แบบเรียบง่าย
      ถ้าใช้ส่วนขยาย lit ของ VSCode ก็จะได้ syntax highlighting และการตรวจ type แบบเต็มภายในเทมเพลตด้วย
      ผมรู้จักมันหลังจาก Marc Grabanski ทวีตว่าเขาใช้ lit-html คู่กับ vanillajs ไม่ใช่ Lit และพอลองเองก็พบว่ามันทำงานได้ดีมาก ดูคล้ายกับสิ่งที่คุณกำลังพยายามแก้อยู่พอสมควร
      ถ้าใช้แพ็กเกจเทมเพลต lit-html ก็ทำสิ่งส่วนใหญ่ที่อธิบายไว้ในบทเทมเพลตได้: https://lit.dev/docs/templates/overview/
      ทำได้แม้ไม่มี abstraction ของคอมโพเนนต์จาก lit-element: https://lit.dev/docs/libraries/standalone-templates/#renderi...
  • เหตุผลที่ “ถ้าเกิดว่า” แบบนี้น่าสนุก คือเราสามารถย้อนต้นกำเนิดของ JSX กลับไปถึง XHP ของ Facebook ได้ และ XHP เองก็ได้รับแรงบันดาลใจอย่างชัดเจนจากมาตรฐาน JS ยุคแรกอย่าง E4X ซึ่งมีหน้าตาและพฤติกรรมคล้ายกับไลบรารีที่อธิบายไว้ที่นี่
    [1] https://engineering.fb.com/2010/02/09/developer-tools/xhp-a-...
    [2] https://en.m.wikipedia.org/wiki/ECMAScript_for_XML

    • E4X มีข้อเสียที่น่าเสียดายคือมันคืนค่าเป็นอินสแตนซ์ DOM จริง ๆ จึงต้องอัปเดตแบบ imperative
      นั่นก็เป็นเหตุผลที่ JSX ไปไกลกว่า E4X และหลังจากนั้นก็ไม่มีข้อเสนอจริงจังเกี่ยวกับเทมเพลต HTML ใน JS อีก
      ถึงอย่างนั้นก็น่าจะยังสามารถนำไอเดียทั่วไปนี้กลับมาปัดฝุ่นให้ทันสมัยได้: https://github.com/WICG/webcomponents/issues/1069
    • ข้อเท็จจริงที่น่าสนใจคือ เหตุผลที่ JavaScript ได้ for(of) แทน for each ก็คือ E4X
      ส่วนเหตุผลที่เราไม่ได้ for (:) นั้นยิ่งน่าขันกว่า เพราะมันอาจชนกับคอมเมนต์ :type ที่สมาชิก TC39 บางคนเชื่อว่าสักวันหนึ่งจะถูกใส่เข้ามาในภาษาได้ราวกับเวทมนตร์
  • เคยสร้างไลบรารี UI ด้วยไอเดียที่ให้ expression ของเทมเพลต JSX สร้างโหนด DOM จริง
    โดยผูกอ็อบเจกต์โมเดลเข้ากับ property ทำให้ลด boilerplate ของ event handler แบบ imperative ได้บางส่วน แน่นอนว่าผมก็ต้องคิดแบบนั้นอยู่แล้ว แต่ผมมองว่านี่เป็นไอเดียที่ดี
    https://github.com/tomtheisen/mutraction
    สามารถเขียนโค้ดทำนองสร้างโมเดลด้วย track({ clicks: 0 }) แล้วเมื่อคลิกก็เพิ่มค่า model.clicks และสะท้อนค่านั้นไปยัง DOM โดยตรงได้

  • ตรงนี้ควรพูดถึง VanJS ด้วย: https://vanjs.org/
    ไลบรารี JSX อื่น ๆ อย่าง Solid.JS ก็คืนค่าเป็นโหนด DOM เช่นกัน และผมชอบที่ไอเดียแบบนี้กำลังได้รับแรงส่ง
    ผมมองว่ายิ่งเราเข้าใกล้แพลตฟอร์มที่ใช้อยู่มากเท่าไรก็ยิ่งดี บางครั้งเลเยอร์ abstraction ก็มีประโยชน์ แต่ในทางปฏิบัติผมยังไม่พบความจำเป็นที่จะต้องซ่อนแพลตฟอร์ม
    โปรเจกต์ยักษ์อย่าง Facebook อาจได้ประโยชน์ก็ได้ แต่ผมไม่เคยทำงานในสเกลระดับนั้น

    • ในการเทียบขนาด bundle นั้น SolidJS แทบไม่มีความหมายไม่ใช่หรือ?
  • ตัวอย่างต่าง ๆ ดูเจ๋ง แต่ประเด็นสำคัญคือมันไม่ได้แสดง props ของคอมโพเนนต์ ที่อาจเปลี่ยนไปตามเวลา
    ดูเหมือนความสามารถนั้นไม่ได้ถูก model ไว้เลย ถ้าไม่จำเป็นต้องใช้ก็เป็นวิธีที่ฉลาด แต่ยังนึกภาพไม่ออกว่าจะขยายไปเป็นแอปที่ซับซ้อนกว่านี้อย่างไร

    • เทคนิคที่ใช้ที่นี่และในโค้ดฝั่งเบราว์เซอร์ทั้งหมด เหมือนกับวิธีที่ VS Code ใช้ภายในพอดี และขยายได้ค่อนข้างดี
      ความต่างมีแค่ว่าเขียนโค้ด 10 บรรทัดที่สร้างและตั้งค่าองค์ประกอบ DOM ด้วยวิธีทั่วไปให้กระชับขึ้นเท่านั้น
      จริง ๆ แล้วส่วนที่น่าสนใจจริง ๆ ของเฟรมเวิร์กนี้คือแทบทุกอย่างที่อยู่นอกเหนือจากนั้น เช่น ฝั่ง static site generator ที่ให้ JSX คืนค่าเป็นสตริง, การนำไดเรกทอรีซอร์สดิบเข้ามาแล้วจัดการ string|Buffer ณ ตอน SSG, และระบบโมดูลที่สร้างเองบน chokidar กับ swc ซึ่งมีประสิทธิภาพและเร็วมาก
      ส่วนที่ใช้ JSX เป็น DOM เป็นเพียงส่วนที่สะดุดตาทางภาพที่สุด แต่ในทางปฏิบัติใกล้เคียงกับกลเม็ดโชว์มากกว่า
      ตัวอย่างเช่นซอร์สของ vanillajsx.com นั้นกระชับ ชัดเจน และสั้นมาก วันนี้เขียนทั้งหมดได้ภายในวันเดียวโดยไม่มี dependency นอกจาก imlib และเดโม JSX-as-DOM เป็นส่วนที่มีความแปลกใหม่น้อยที่สุด: https://github.com/sdegutis/vanillajsx.com/tree/main/site
    • ถ้าจะขยายแนวทางนี้ไปใช้งานในสเกลจริง และมั่นใจว่าทุกอย่างทำงานร่วมกันตามที่คาดไว้ จำเป็นต้องมี เลเยอร์ reactivity ที่เหมาะสม
      ถ้าชอบวิธีที่ JSX คืนค่าเป็นองค์ประกอบ DOM แบบเป็นรูปธรรมแบบนี้ Solid ก็ทำงานแบบนี้พอดี และมีเลเยอร์ reactivity ที่เหมาะสมเพิ่มเข้ามาด้วย
    • ผมอาจพลาดอะไรไปก็ได้ แต่ไม่เข้าใจว่าวิธีนี้ป้องกันการใช้ setTimeout หรือ setInterval ได้อย่างไร
      อย่างไรก็ตาม เห็นด้วยว่าโปรเจกต์แบบนี้มักทำงานได้ดีใน use case เล็ก ๆ แต่พอเป็นสถานการณ์ “โลกจริง” ก็มักพังอย่างรวดเร็ว
    • ไม่ได้จะปกป้องไลบรารีนี้และก็ไม่มีแผนจะใช้ แต่ถ้าต้องการให้การเปลี่ยนแปลงของ props ทำให้เกิดการ re-render แบบ React/Vue ตรงนี้น่าจะใช้ JS Proxy ได้
      การ implement ไม่น่าจะยากขนาดนั้น
  • นี่คล้ายกับ Vanilla TSX มาก: https://github.com/wisercoder/uibuilder
    ตัวอย่างแอปที่เขียนด้วย Vanilla TSX อยู่ที่นี่: https://github.com/wisercoder/eureka/tree/master/webapp/Clie...

  • ทำให้นึกถึง ActionScript 3 ที่มี XML อยู่ในแกนของภาษา
    เป็นภาษาที่เขียนสนุก แต่ก็ล้มเหลวอย่างโด่งดังในการกลายเป็น ES4 สุดท้ายกว่าจะมาถึงจุดคล้าย ๆ กันด้วย TypeScript และ JSX ก็ใช้เวลามากกว่า 10 ปี

    • https://en.wikipedia.org/wiki/ECMAScript_for_XML — Firefox ก็เคยมีเช่นกัน แต่ผู้คนไม่ได้ต้องการ และสุดท้ายก็ถูกถอดออก
      ถูกปิดใช้งานสำหรับเว็บเพจใน Firefox 17 ซึ่งก่อน React รุ่นแรก 6 เดือน
    • จำไม่ได้ว่าเคยสร้าง XML แบบ inline ด้วยวิธีนี้ได้ บางทีถ้าเป็นฟีเจอร์ฝั่งเซิร์ฟเวอร์ของ Flex ก็อาจทำได้
  • อยากรู้ว่ามีใครอีกไหมที่รันตัวอย่างสุดท้ายบน Firefox ไม่ได้ ใน Edge ใช้ได้ แต่บน Firefox ล้มเหลว
    Uncaught (in promise) TypeError: Map.groupBy(...).entries().map is not a function

  • ยังไม่ค่อยเข้าใจว่าทำไม JSX ถึงน่าสนใจกว่าการเขียนแบบด้านล่าง
    h("div", {}, [ h("p", {}, "this is easy"), ...list.map((l) => h("li", {}, l)) ])
    ถ้าเขียนแบบนี้ ก็ได้ลูปและการแทรกค่าตัวแปร ฯลฯ มาโดยอัตโนมัติ โดยไม่ต้องสร้างคอมไพเลอร์กับไวยากรณ์ใหม่ ถ้าใครอธิบายได้ก็ดี

    • คิดว่าหลายคนน่าจะรู้สึกว่าเวอร์ชัน JSX อ่านง่ายและคุ้นเคยกว่ามาก แน่นอนว่าเป็นเรื่องรสนิยม
      ถ้าพูดอย่างยุติธรรม ใน JSX ก็ใช้ลูปทั่วไปและการแทรกค่าได้เหมือนเดิม {} รับ expression ของ JS ล้วน ๆ
      เพียงแต่ต้องมี ขั้นตอนคอมไพล์ เพื่อคลี่แท็ก element ออกเป็นการเรียก createElement
    • ส่วนตัวแล้วพอเห็นโค้ดที่ยกมา ก็ไม่ค่อยเข้าใจคนที่คิดว่ามันดีพอ ๆ กับ JSX
      แต่ละคนประมวลผลข้อมูลต่างกันก็จริง แต่ตัวอย่างนั้นมีเครื่องหมายวรรคตอนเยอะ ทำให้จับใจความเร็ว ๆ ได้ยากมาก
      ถ้าไม่แยกไวยากรณ์ในหัวเอง ก็จะมองไม่เห็นโครงสร้าง DOM ที่ถูกสร้างขึ้น และการ เข้าใจโครงสร้าง DOM ได้ในพริบตา สำคัญกว่าการต้องมีคอมไพเลอร์หรือไม่มาก
      อนึ่ง ผมเป็นคนที่มีความหลากหลายทางระบบประสาท และหวังว่าคำอธิบายนี้จะช่วยได้
    • ถ้าตามตรรกะนั้นลงไปในโพรงกระต่ายเรื่อย ๆ สุดท้ายก็จะไปถึง Common Lisp
    • เคยคิดแบบเดียวกัน ข้อดีอย่างหนึ่งน่าจะเป็นตรงที่มันดูเหมือน HTML
      คล้ายกับสิ่งที่เห็นในเครื่องมือนักพัฒนาเบราว์เซอร์ จึงเปรียบเทียบและดีบักได้ง่าย
    • ถ้าเป็นไฟล์เดียวหรือแอปง่าย ๆ สไตล์นั้นก็โอเค
      แต่ในแอปที่ซับซ้อน เมื่อมีคอมโพเนนต์เป็นร้อย ๆ และเริ่ม import มาใช้กัน การเขียนด้วยแท็ก JSX จะให้ความรู้สึกเหมือนไวยากรณ์ที่เรียบง่ายและเข้าใจง่าย
      กระแสต่อต้าน React สุดท้ายแล้วก็มักมาจากประเด็นว่ามันเกินจำเป็นสำหรับงานง่าย ๆ และจริง ๆ ก็เกินจำเป็นจริง
      แต่แอปที่แต่ละคนสร้าง ความซับซ้อน และความต้องการนั้นต่างกัน จึงไม่สามารถสรุปง่าย ๆ ได้ว่าฝั่งไหนน่าสนใจกว่า มันเป็นเครื่องมือคนละแบบสำหรับปัญหาคนละแบบ