LiveView เข้ากันได้ดีที่สุดกับ Svelte
(blog.sequin.io)- ทีม Sequin ลองใช้ทั้ง React SPA และ Phoenix LiveView แบบล้วนแล้ว สรุปว่าวิธีที่ผสาน LiveView กับ Svelte ผ่าน LiveSvelte ให้ประสิทธิภาพการพัฒนาดีที่สุด
- LiveView ช่วยสร้าง UI ที่สมบูรณ์ได้โดยไม่ต้องมี SPA ด้วยสถานะฝั่งเซิร์ฟเวอร์และการ อัปเดต DOM แบบเพิ่มส่วนต่าง แต่เมื่อถึงจุดที่ต้องใช้สถานะฝั่งไคลเอนต์ ขอบเขตจะเริ่มไม่ชัดเจนเพราะโมดูล JS, hooks และสถานะของ LiveView ปะปนกัน
- ใน LiveView แบบล้วน LiveView, LiveComponent, Component มี lifecycle, การส่ง props และวิธีสื่อสารต่างกัน ทำให้การรีแฟกเตอร์และการจัดวางสถานะยุ่งยากกว่า React
- LiveSvelte ให้ ขอบเขตสองทางที่ชัดเจน โดย Elixir ส่ง props ลงมา และ Svelte ส่ง event ขึ้นไปด้วย
live.pushEvent - ในการผสานแบบนี้ สามารถวาง routing, business logic และการเตรียมข้อมูลไว้บนเซิร์ฟเวอร์ ส่วนสถานะ UI, animation และ conditional rendering ให้ Svelte รับผิดชอบ เพื่อลดความซับซ้อนของ SPA
โมเดลการพัฒนาที่ LiveView มอบให้
- แอป server rendering แบบดั้งเดิมคือเซิร์ฟเวอร์ ไม่มีสถานะ และเรนเดอร์หน้าตามคำขอจากไคลเอนต์แต่ละครั้ง
- SPA ให้ไคลเอนต์รับผิดชอบการประกอบหน้า อ่านและเขียนข้อมูลผ่าน backend API และจัดการสถานะฝั่งไคลเอนต์ด้วยวิธีอย่าง
useStateของ React - LiveView ให้เซิร์ฟเวอร์ยังคงรับผิดชอบการเรนเดอร์หน้า ขณะเดียวกันก็ คงสถานะไว้ ประมวลผล action จาก frontend แล้วอัปเดต DOM แบบเพิ่มส่วนต่างเหมือน SPA
- ความซับซ้อนของ SPA เพิ่มขึ้นจากการที่แอป JS ฝั่งไคลเอนต์ทำงานแทบจะเหมือน frontend microservice ตัวหนึ่ง
- route และ controller ฝั่ง backend เพิ่มขึ้นเพราะข้อกำหนดเฉพาะของ frontend
- คำขอหนึ่งครั้งอาจผ่าน
onMount, การเรียก API, การ parse response, router, auth plug, controller, domain function และชั้นเตรียม response
- จุดเด่นของ LiveView คือสามารถสร้างประสบการณ์ฝั่งไคลเอนต์ที่สมบูรณ์ได้โดยไม่ต้องมี frontend microservice
- สามารถ query database ใกล้กับฟังก์ชันเรนเดอร์ตารางได้
- เมื่อมีแถวใหม่เข้ามา ก็ push ไปยังตาราง แล้ว LiveView อัปเดตไคลเอนต์
- สร้างการโต้ตอบที่ใกล้เคียงกับเฟรมเวิร์ก frontend แบบมีสถานะได้มากกว่าวิธี server rendering เดิมที่สร้างสถานะทั้งหมดใหม่ทุกคำขอ
แรงเสียดทานที่พบใน LiveView แบบล้วน
-
หลีกเลี่ยงสถานะฝั่งไคลเอนต์ไม่ได้
- LiveView ไม่อาจหลีกเลี่ยง ความหน่วงแบบไป-กลับ ระหว่างเซิร์ฟเวอร์กับผู้ใช้ได้
- งานอย่าง animation, tooltip, การแสดง·ซ่อน DOM, การปิดใช้งาน field ในฟอร์ม บางกรณีจำเป็นต้องประมวลผลบนไคลเอนต์
- ในฟอร์มที่มี dropdown สองตัวซึ่งพึ่งพากัน หลังเลือก dropdown แรก ควรปิดใช้งาน dropdown ที่สองทันที แล้วเมื่อเซิร์ฟเวอร์เติมรายการใหม่จึงเปิดใช้งานอีกครั้ง เพื่อให้ UX ดี
- หากต้องการทำพฤติกรรมนี้ด้วย LiveView เพียงอย่างเดียว ต้องใช้หลายแนวคิดร่วมกัน
- เมื่อ JS, hook และสถานะของ LiveView ถูกใช้ร่วมกัน สถานะ DOM บางส่วนจะอยู่ นอก LiveView และยากที่จะวาง pattern ให้สม่ำเสมอ
-
ขอบเขตสถานะที่ต่างจาก React
- React จัดการ transition ส่วนใหญ่ด้วยแนวคิดหลักคือสถานะและ action และขอบเขตระหว่างสถานะ DOM กับสถานะ component ค่อนข้างชัดเจน
- เพราะไม่มีความหน่วงระหว่าง client action กับ client state React จึงจัดการ action และ transition ด้วย state paradigm ได้
- สถานะของ LiveView อยู่ฝั่งเซิร์ฟเวอร์ จึงต้องคำนึงถึง ความหน่วง ระหว่าง client action กับ server state
- ในตัวอย่างช่อง input React จะผ่านเส้นทางสถานะก่อนที่อักขระจะถูกป้อน และ component จะเรนเดอร์ใหม่ทุกครั้งที่กดปุ่ม
- LiveView ใกล้เคียงกับโมเดลที่ผู้ใช้เปลี่ยนช่อง input ก่อน แล้วหลังจากนั้นไม่นาน server state จึงรับรู้และตอบสนอง
- หากความหน่วงแทบไม่มี ก็จะดูเหมือน React แต่ยิ่งความหน่วงมากขึ้น ความแตกต่างของสองโมเดลก็ยิ่งชัดเจน
ความซับซ้อนจาก component สามประเภท
- LiveView มี component สามประเภทคือ LiveViews, LiveComponents, Components
- LiveViews และ LiveComponents ใกล้เคียงกับ stateful component ของ React ส่วน Components ใกล้เคียงกับ functional component
- LiveView จะเป็น component แม่ระดับบนสุดเสมอ และเรนเดอร์ LiveComponents กับ Components ใต้ลงมา
- ใน React สามารถสลับระหว่าง stateful component กับ functional component ได้ง่าย ด้วยการเพิ่มหรือลบ hook
useState- ทั้งคู่รับ props ด้วยวิธีเดียวกัน
- หากไม่นับสถานะ ชุดความสามารถเหมือนกัน
- วิธีลงทะเบียนและตอบสนองต่อ DOM event ก็เหมือนกัน
- ใน LiveView ความแตกต่างระหว่าง component ทั้งสามประเภทมีมาก ทำให้การรีแฟกเตอร์ LiveView เป็น LiveComponent ยุ่งยาก
- LiveViews และ LiveComponents มี syntax สำหรับเรนเดอร์และส่ง props ต่างกัน
- lifecycle ต่างกัน
- ตัวเลือกการสื่อสาร ต่างกัน โดย LiveViews ใช้
sendส่วน LiveComponents ใช้send_update - LiveComponents ไม่ใช่ process จึงไม่สามารถโต้ตอบกับส่วนอื่นของระบบได้เหมือน LiveViews
ข้อจำกัดของ LiveComponent
- LiveView เป็น Elixir process หนึ่งตัว จึงมี
pid, สถานะ และ inbox และทำงานได้เหมือน process อื่นในระบบ Elixir/OTP - ใน LiveView สามารถใช้ pub/sub เพื่อ subscribe การเปลี่ยนแปลงทั่วทั้งระบบได้
- LiveComponent ไม่ใช่ process ของตัวเอง แต่เป็น module ที่ LiveView เรียกใช้
- เนื่องจาก process ของ LiveView แม่ถือสถานะของ component ลูกไว้ LiveView จึงต้องจัดการการ route message ของ LiveComponents ลูกด้วย
- หาก LiveComponents จะมีความสามารถจัดการสถานะและประมวลผล action อย่างอิสระ แต่ละตัวควรเป็น process แต่โมเดลปัจจุบันไม่ใช่แบบนั้น
- ในการพัฒนาจริง แม้อยากส่ง event หรือ action ไปยัง LiveComponent ก็ไม่มีวิธีที่ดี และจะเริ่มกำกวมว่าจะใช้
send_updateเพื่อส่ง action หรือใช้เป็น state patch- หากใช้เป็น state patch ก็ต้องคิดต่อว่าจะใช้
updateclause แยก mount กับ update อย่างไร
- หากใช้เป็น state patch ก็ต้องคิดต่อว่าจะใช้
ขอบเขตที่ LiveSvelte สร้างขึ้น
- LiveSvelte ทำให้สามารถเรนเดอร์ Svelte component ใน LiveView ได้
- ภายใน
renderของ LiveView สามารถ encode โครงสร้างข้อมูล Elixir เป็น plain map สำหรับ frontend แล้วส่งเป็น props ให้ Svelte component ได้ - Svelte component รับ props ด้วย
export letและสื่อสารกับ LiveView ผ่าน propliveที่ LiveSvelte มอบให้ - ฝั่ง Svelte สามารถส่ง event ไปยังเซิร์ฟเวอร์ด้วย
live.pushEvent- ในตัวอย่าง reactive block ของ Svelte ส่ง event
"form_updated"ไปยังเซิร์ฟเวอร์เมื่อformเปลี่ยน - LiveView decode parameter จาก frontend และอัปเดต changeset ใน
handle_event("form_updated", ...) - ข้อผิดพลาดจากการตรวจสอบ changeset จะถูกส่งกลับไปยัง frontend ผ่าน prop
errors
- ในตัวอย่าง reactive block ของ Svelte ส่ง event
- ข้อมูลไหลจาก Elixir ลงไปยัง Svelte ผ่าน props และจาก Svelte ขึ้นไปยัง LiveView ผ่าน websocket event
- process ของ LiveView สามารถอัปเดต props ได้ทุกเมื่อเพื่อให้ Svelte component เรนเดอร์ใหม่
คุณสมบัติหลักของการผสาน LiveView กับ Svelte
- หัวใจของการผสานนี้คือ backend process แบบมีสถานะ กับ frontend component แบบมีสถานะ ทำงานร่วมกัน
- มีคุณสมบัติสามข้อที่รองรับการผสานนี้
- backend ควบคุม props ของ frontend component
- ทั้ง frontend และ backend ต่างมีสถานะ
- ทั้งสองฝั่งมีช่องทางสื่อสารสองทางแบบส่วนตัวที่แต่ละฝั่งสามารถเริ่มส่ง message ถึงกันได้
- คุณสมบัติแรกเป็นไปได้ด้วยวิธีเรนเดอร์ของ LiveView
- ผลการ rerender จากเซิร์ฟเวอร์จะถูก push ไปยังไคลเอนต์และนำไปใช้โดยอัตโนมัติ
- เซิร์ฟเวอร์สามารถอัปเดต props ของ component ได้เหมือน JS parent component
- คุณสมบัติที่สองมาจากการที่ LiveView เป็น Elixir process
- Elixir process คือวิธี encapsulate สถานะ
- คุณสมบัติที่สามเป็นไปได้ด้วยการเชื่อมต่อ websocket แบบต่อเนื่องที่ LiveView มอบให้
ความแตกต่างจาก SPA
- browser routing ทั้งหมดเกิดขึ้นที่ backend ทำให้โครงสร้างง่ายขึ้น
- ใน SPA ทั่วไป ต้องดูแล route สองประเภทคือ browser route และ API route
- backend มีสถานะและรู้จัก route ปัจจุบันกับ resource ที่กำลังทำงานอยู่
- แต่ละ action สามารถจัดการได้ด้วยการเปลี่ยนสถานะของ backend เอง แทนที่จะสร้างสถานะใหม่ตั้งแต่ต้น
- การสื่อสารระหว่าง frontend และ backend เป็นแบบส่วนตัวและผูกกันแน่น
- ไม่เพิ่ม public route ของเซิร์ฟเวอร์ด้วย RPC call เพื่อรองรับ component เดี่ยว
- เมื่อเห็น
pushEventในไคลเอนต์ ก็รู้ได้ว่า handler ที่เกี่ยวข้องอยู่ใน module Elixir ที่ทำงานร่วมกัน
- ฟีเจอร์มักแบ่งอยู่ในสองไฟล์
- module backend เรียกฟังก์ชันฝั่ง backend เช่น query database
- frontend นำเข้า component และ style
- รอบการสื่อสารระหว่าง frontend กับ backend ไม่ผ่าน API module, router และ controller stack
- ขั้นตอนการสื่อสารก็ลดลง
- backend แจ้งการเปลี่ยนแปลงให้ frontend ได้ด้วยการอัปเดต props เท่านั้น
- frontend ส่ง
pushEventได้โดยไม่ต้องมี handler สำหรับ token หมดอายุ, timeout หรือการจัดการความล้มเหลว - ถ้า websocket เปิดอยู่ แสดงว่าเซิร์ฟเวอร์กำลังทำงาน และถ้าปิดอยู่ LiveView จะแสดงแบนเนอร์ “disconnected” ระดับ global
การแบ่งความรับผิดชอบและโครงสร้างโค้ด
- โครงสร้างนี้ทำงานเหมือน frontend microservice ถูกกำจัดออกไป
- วาง business logic ไว้ที่ backend
- วิธีโหลดข้อมูล
- จะโหลดข้อมูลใด
- วิธี sort และ filter
- validator
- โค้ด frontend ของ Svelte ง่ายขึ้น
- block
if/endสำหรับ conditional rendering - animation
- ฟังก์ชัน
pushEventง่าย ๆ ที่ส่งไปยังเซิร์ฟเวอร์
- block
- frontend แบบ SPA ทั่วไปอาจมี logic
map,reduce,filterจำนวนมากสำหรับประมวลผลข้อมูลจากเซิร์ฟเวอร์ เตรียมข้อมูลเพื่อแสดงผล และเตรียมข้อมูลเพื่อส่งกลับเซิร์ฟเวอร์ - ในแอป LiveSvelte งานประมวลผลเหล่านี้ทำบนฝั่งเซิร์ฟเวอร์ได้
- LiveView เตรียมข้อมูลให้อยู่ในรูปแบบที่ Svelte component ต้องการ
- ความซับซ้อนคงอยู่ในภาษาเซิร์ฟเวอร์ โครงสร้างข้อมูลฝั่งเซิร์ฟเวอร์ และ test suite ฝั่งเซิร์ฟเวอร์
- LiveView กับ Svelte component ใกล้เคียงกับสองครึ่งของฟีเจอร์เดียวมากกว่าจะเป็นสององค์ประกอบที่ loosely coupled
- LiveView เรนเดอร์เฉพาะ Svelte component นั้น
- Svelte component นั้นก็ถูกเรนเดอร์โดย LiveView นั้นเท่านั้น
สิ่งที่เปลี่ยนไปเมื่อเทียบกับ LiveView แบบล้วน
- วิธีของ LiveSvelte ยอมรับ สถานะและ state transition ของ frontend
- boundary layer ระหว่าง frontend กับ backend ชัดเจนขึ้น
- ใช้ component paradigm ที่เติบโตเต็มที่ของ Svelte ได้
- ใช้ ecosystem ของ JavaScript framework ที่เข้าถึงได้ยากหากใช้ LiveView แบบล้วนได้ด้วย
- Svelte มี animation primitives ให้ใช้
- เมื่อย้ายความรับผิดชอบไป frontend มากขึ้น ความรู้สึกติดอยู่ในพื้นที่กึ่งกลางที่ฝืนธรรมชาติก็ลดลง
เหตุผลที่เลือก Svelte และตำแหน่งของ LiveView
- เหตุผลที่เลือก LiveSvelte คือไม่มีไลบรารี LiveView สำหรับ React ที่สมบูรณ์ใกล้เคียงกัน
- Svelte ได้รับการประเมินว่าเป็น frontend framework ที่เบาและเร็วในด้านสถานะพื้นฐานและ reactivity
- เพราะ LiveView รับหน้าที่ส่วนใหญ่ของ state management ฝั่ง Svelte จึงคงการจัดการสถานะให้เรียบง่ายได้
- ฟีเจอร์ template ของ Svelte ก็ถูกชอบมากกว่า React
- ใช้
if/elseแทน ternary operator ได้ - ใช้การตั้งค่า attribute แบบมีเงื่อนไขได้
- ใช้
- runes ของ Svelte 5 คาดว่าจะทำให้ Svelte เรียนรู้ง่ายขึ้นและอนุมานได้ง่ายขึ้น
- LiveView เปล่งประกายที่สุดในฐานะ backend-for-frontend ที่เรนเดอร์ frontend component, ทำ incremental update, คง backend process แบบมีสถานะ และให้ websocket API
1 ความคิดเห็น
ความคิดเห็นจาก Hacker News
แพตเทิร์นที่มักใช้กันในเกมแบบผู้เล่นหลายคนก็คือการรัน โค้ดเดียวกันทั้งฝั่งไคลเอนต์และเซิร์ฟเวอร์ โดยพื้นฐาน
โค้ดฝั่งไคลเอนต์จะคาดเดาสถานะของเซิร์ฟเวอร์แล้วรันไปก่อน และเมื่อสถานะจากเซิร์ฟเวอร์มาถึงก็เขียนทับสถานะฝั่งไคลเอนต์
ในเกม ไคลเอนต์สามารถเดาผลลัพธ์จากอินพุตของตัวเองได้ค่อนข้างแม่น แต่ไม่รู้อินพุตของผู้เล่นคนอื่น คำว่า “การคาดเดา” จึงเหมาะมาก
แต่แนวทางนี้ยังใช้กับการตอบสนองทันทีระหว่างรอสถานะทางการจากเซิร์ฟเวอร์ได้ด้วย เช่น การเปิด/ปิด dropdown หรือ loading spinner
และยังมีสถานะฝั่งไคลเอนต์อีกมาก เช่น particle system หรือ ragdoll ที่ไม่จำเป็นต้องรันบนเซิร์ฟเวอร์เลย ดังนั้นถ้ามีการเชื่อมต่อกับเซิร์ฟเวอร์อย่างต่อเนื่อง ก็ดูไม่มีเหตุผลว่าทำไมมันจะใช้กับ กระบวนทัศน์แบบ reactive ไม่ได้
มันเป็นเกมโคลนของ https://generals.io/ และสร้างด้วย LiveSvelte
โดยพื้นฐานมันแทบจะเป็นบอร์ดเกมและมีแค่ 2 tick ต่อวินาที จึงไม่จำเป็นต้องมีการคาดเดา แต่ก็ใช้ optimistic update เพื่อแสดงสถานะเกมล่วงหน้าก่อนที่อัปเดตจากเซิร์ฟเวอร์จะมาถึง
ถ้าซิงก์กับเซิร์ฟเวอร์ไม่ตรง เซิร์ฟเวอร์ก็จะเขียนทับสถานะเกม
ลอจิกของเกมทั้งหมดอยู่ใน Elixir และถ้าจะทำการคาดเดาให้ถูกต้อง เซิร์ฟเวอร์กับไคลเอนต์จำเป็นต้องแชร์ลอจิกร่วมกัน
ไม่อย่างนั้นก็ต้องเขียนลอจิกซ้ำสองรอบ ซึ่งเป็นทางไปสู่หายนะ
แม้จะยังไม่ได้ลองศึกษาจริงจัง แต่แนวทางที่เป็นไปได้คือเขียนลอจิกเกมทั้งหมดด้วย gleam(https://gleam.run/)
Gleam เข้ากันได้กับ Elixir และคอมไพล์เป็น JavaScript ได้ด้วย ดังนั้นในทางทฤษฎีจึงรันโค้ดเดียวกันบนเซิร์ฟเวอร์และไคลเอนต์ได้
อย่างไรก็ตาม ถ้าจะบอกว่า “ก็เขียนทุกอย่างเป็น js ไปเลยสิ” ก็ถือว่าเป็นข้อโต้แย้งที่มีน้ำหนักมาก และผมเองก็น่าจะเห็นด้วย
ถึงอย่างนั้น ข้อดีที่ได้คือสามารถใช้ BEAM และความสามารถที่เหมาะกับระบบกระจายแบบเรียลไทม์ ซึ่งถ้าไม่นับฝั่งฟรอนต์เอนด์แล้ว มันเหมาะกับเกมหลายผู้เล่นมาก
เวลามีคนตายแล้วศพกระเด็นไปหรือค้างอยู่ในท่าตลก ๆ คนอื่นจะไม่มีทางเห็นเลย และในรีเพลย์ก็ย้อนดูไม่ได้เพราะแต่ละไคลเอนต์เรนเดอร์ต่างกัน
ถ้าไม่ได้จับไว้ด้วยการบันทึกหน้าจอแบบเรียลไทม์ มันก็หายไปตลอดกาล
สำหรับเกมที่บางครั้งดูขำ ๆ อย่าง Overwatch มันน่าเสียดายที่ไม่มีใครได้เห็นร่วมกัน
https://arstechnica.com/gaming/2019/10/explaining-how-fighti... อธิบายไว้ได้ยอดเยี่ยม และมีการพูดคุยที่เกี่ยวข้องใน https://news.ycombinator.com/item?id=34399790 และ https://news.ycombinator.com/item?id=26289933
ถ้ามีความคงอยู่ของเซิร์ฟเวอร์แบบ MMO เข้ามาเกี่ยวข้อง ก็ต้องคิดเรื่องคอขวดของ I/O ไปพร้อมกันด้วย
https://prdeving.wordpress.com/2023/09/29/mmo-architecture-s... เป็นบทความที่น่าสนใจจากมุมมองนั้น และมีการอภิปรายใน https://news.ycombinator.com/item?id=37702632
ตัวอย่างเช่น ฟอร์มแบบ live/interactive อาจดูเหมือนทำงานดีเพราะอัปเดตข้อมูลอนุพันธ์บนหน้าได้ แต่ถ้าภายหลังเกิดข้อผิดพลาดฝั่งเซิร์ฟเวอร์หรือปัญหาเครือข่ายแล้วทุกอย่างถูกย้อนกลับ ก็จะเป็นปัญหา
ที่แย่กว่านั้นคือผู้ใช้อาจไม่รู้ตัว แล้วปิดหน้าหรือย้ายไปหน้าอื่นก่อนด้วยซ้ำ
ชอบการจับคู่ LiveView + Svelte มาก
ผมเคยนำเสนอวิธีผสานทั้งสองอย่างในงาน ElixirConf 2022 แต่ที่ทำให้มันใช้งานได้จริงคือผู้มีส่วนร่วมของ live_svelte
ผมมองว่าแอปที่มีประสบการณ์ผู้ใช้ที่สมบูรณ์มักต้องมีสถานะฝั่งไคลเอนต์เสมอ
โดยเฉพาะถ้าอาศัยอยู่ในนิวยอร์ก การมีสัญญาณเครือข่ายระหว่างเดินทางไม่ใช่เรื่องที่เชื่อถือได้เสมอไป
ฟีเจอร์ทรงพลังอย่างหนึ่งที่บทความไม่ได้พูดถึงคือ การใช้ pubsub ของ Phoenix เพื่อ push การเปลี่ยนแปลงสถานะฝั่งเซิร์ฟเวอร์ ที่เกิดจากเซิร์ฟเวอร์เครื่องอื่นไปยังไคลเอนต์ทั้งหมดแบบ reactive ได้ด้วย
หากต้องรองรับทราฟฟิกระดับปานกลางขึ้นไป การมีเว็บเซิร์ฟเวอร์หลายเครื่องถือเป็นเรื่องที่พบได้ค่อนข้างบ่อย
ถ้าคุณต้องการประสบการณ์แบบ LiveView แต่ก็อยากให้ทนต่อเครือข่ายที่ไม่เสถียร เราได้เพิ่ม Svelte 5 binding ให้ Triplit[1] แล้ว
คุณสามารถเขียนคิวรีฝั่งไคลเอนต์และซิงก์กับเซิร์ฟเวอร์ผ่าน WebSocket ได้
มันอาจไม่ดึงดูดนักพัฒนา Elixir มากนัก แต่ถ้า Svelte เป็นศูนย์กลาง Triplit ก็เป็นตัวเลือกที่ดี
[1] https://www.triplit.dev/docs/frameworks/svelte
วิธีแค่เพิ่มแถวใหม่เข้าไปในตารางแล้วให้ LiveView อัปเดตฝั่งไคลเอนต์นั้น ควรหลีกเลี่ยงใน แอปงานธุรกิจ ที่แถวเหล่านั้นโต้ตอบได้
เพราะความหน่วงในการรับรู้ ผู้ใช้จึงกดรายการผิด ส่งอีเมลหาลูกค้าผิดคน หรือคืนเงินให้ธุรกรรมผิดรายการได้ง่าย
ประสบการณ์ผู้ใช้ที่น่าพอใจกว่าคือแบนเนอร์คงที่ประมาณว่า “ข้อมูลมีการเปลี่ยนแปลง คลิกที่นี่เพื่อรีเฟรช”
ถ้าจำเป็นจริง ๆ ก็ควรเรนเดอร์แถวใหม่แบบเพิ่มอย่างเดียว และอย่าให้ตำแหน่งการเลื่อนเปลี่ยน
ใน BeaconCMS มีการใช้ Svelte ร่วมกับ LiveView
แน่นอนว่ามีกรณีใช้งานที่ดีซึ่งต้องควบคุม UI ฝั่งไคลเอนต์ให้ละเอียดขึ้น แต่ก็ควรระวังหากทีมจะนำ Svelte + LiveView มาใช้กับทุกอย่างแบบเต็มรูปแบบ
แม้แต่ใน Phoenix เอง LiveView ก็ไม่ใช่คำตอบเสมอไป และบางครั้งหน้าที่เรนเดอร์แบบปกติก็เพียงพอ
อย่าเลือกทุกอย่างแบบสุดโต่งว่าต้องทั้งหมดหรือไม่เอาเลย
อย่างที่บทความบอก มีกรณีใช้งานที่ดีสำหรับการออกจาก “แนวทางแบบ LiveView”
ถ้าเวลาไปกลับอยู่ที่ 1,000ms ก็อาจต้องพิจารณาอย่างอื่น แต่ก็มีทีมที่ไม่สามารถวางเซิร์ฟเวอร์ใกล้ทางภูมิศาสตร์ได้เพราะเหตุผลอย่างต้นทุน ดังนั้นการจัดการสถานะฝั่งไคลเอนต์อาจเป็นทางออกได้
หมายถึงแทนที่จะจัดการสถานะฝั่งไคลเอนต์ จะไปจัดการ สถานะทั้งฝั่งไคลเอนต์และเซิร์ฟเวอร์ ใช่ไหม?
แม้จะมีข้อดีว่าไม่ต้องสร้าง API เพิ่มอีกชุด แต่ก็ไม่ได้ดูเหมือนเป็นการปรับปรุง
มีทั้งสถานะที่ไคลเอนต์ต้องติดตามและสถานะที่เซิร์ฟเวอร์ต้องติดตามอยู่เสมอ และยังซ้อนทับกับสถานะชั่วคราวที่อยู่แค่ในหน่วยความจำ กับสถานะที่ต้องคงอยู่ข้ามเซสชัน
วิธีจัดการเรื่องนี้แทบจะมีได้ไม่สิ้นสุด และใน JavaScript ก็เป็นแนวว่า “ทำแบบไหนก็ไม่มีคำตอบเดียวที่ถูก” เลยลงเอยด้วยการใช้วิธีที่เฟรมเวิร์กกำหนดไว้ หรือไม่ก็ใช้วิธีเฉพาะหน้า ซึ่งก็คือความโกลาหล
ใน single-page app ที่ซับซ้อนตัวล่าสุดที่เคยเขียน ผมให้ไคลเอนต์ซิงก์ทุกครั้งที่โหลดหน้า
ทุกสถานะโลคัลหรือแอสเซ็ตจะมีแฮชตามวันที่ติดอยู่ และถ้าไม่ตรงกับค่าบนเซิร์ฟเวอร์ เซิร์ฟเวอร์ก็จะส่งเวอร์ชันอัปเดตลงมา
ไม่ว่าจะเป็นตัวแปรธรรมดา อ็อบเจ็กต์ JSON ขนาดใหญ่ หรือทั้ง image/audio blob ก็เหมือนกัน
ถ้าไคลเอนต์ทำสิ่งที่ต้องเปลี่ยนสถานะบนเซิร์ฟเวอร์ ก็จะส่งอัปเดตสถานะนั้นผ่าน WebSocket และ 99% ของแอปก็ทำงานบน WebSocket
ไม่ได้ใช้เฟรมเวิร์กหรือแพตเทิร์นพิเศษอะไร แค่เขียนโค้ดไปแล้วถ้ารู้สึกว่า “อันนี้เซิร์ฟเวอร์ควรเป็นคนติดตาม” ก็ส่งสถานะใหม่ไปให้เซิร์ฟเวอร์ทางข้อความ แล้วให้เซิร์ฟเวอร์ตัดสินใจว่าจะซิงก์ตอนโหลดหน้าหรือไม่
สำหรับการจัดการสถานะแบบนี้ ผมคิดว่านี่แทบจะเป็นกลไกที่เรียบง่ายที่สุดแล้ว
WebSocket ช่วยเรื่องการจัดการสถานะได้มากจริง ๆ
แอปพลิเคชันจริงจำเป็นต้องมีสถานะทั้งฝั่งไคลเอนต์และเซิร์ฟเวอร์
เซิร์ฟเวอร์ต้องมี สถานะที่เป็นแหล่งอ้างอิงหลัก เพราะเชื่อถือไคลเอนต์ไม่ได้ แต่ไคลเอนต์เองก็ต้องสามารถตอบสนองต่อการโต้ตอบของผู้ใช้และเรนเดอร์ใหม่ได้โดยไม่ต้องรอรอบไปกลับ
แต่ก็ไม่ควรทำแบบนั้นกับทุกอย่าง ใช้เมื่อมันสมเหตุสมผลก็พอ
อีกอย่างที่ชอบคือสามารถใช้ Svelte เป็นภาษาเทมเพลต แทน Heex ได้
ถ้าไม่จำเป็นก็ไม่มีเหตุผลต้องทำ และเหตุผลที่มันจำเป็นก็คือประสิทธิภาพกับประสบการณ์ผู้ใช้
ถ้าเว็บแอปมีการโต้ตอบของผู้ใช้มากจนต้องใช้ JavaScript ฝั่งไคลเอนต์ และมีการทดลองมากจนบันเดิลแตกต่างกันไปในแต่ละรีเควสต์ คุณก็จะอยากแยกลอจิกไว้ทั้งฝั่งเซิร์ฟเวอร์และไคลเอนต์เพื่อลดปริมาณ JavaScript ที่ต้องส่งไปฝั่งไคลเอนต์
ไม่อย่างนั้นคุณก็จะได้ JavaScript บันเดิลขนาดเป็น MB ที่รวมทุกความเป็นไปได้ทั้งหมดที่แอปอาจต้องเจอ
ถ้าเป็นแอปอย่าง Photoshop เวอร์ชันเว็บที่ผู้ใช้เข้าใจเวลาโหลดเริ่มต้น ก็อาจไม่เป็นไร แต่ถ้าเป็นแอปอย่าง Stripe หรือ Gmail ที่ผู้ใช้คาดหวังให้โหลดได้ทันที คุณก็อยากลดดีเลย์เริ่มต้นของหน้า
คุณอาจย้ายทุกอย่างไปไว้ฝั่งเซิร์ฟเวอร์ได้ แต่เหมือนที่ GitHub เจอ การกระทำของผู้ใช้ที่ควรตอบสนองทันทีจะต้องรอรอบไปกลับกับเซิร์ฟเวอร์ ทำให้ความโต้ตอบของหน้าลดลง
จากนั้นข้อจำกัดเรื่องความเร็วแสงกับการกระจายของเซิร์ฟเวอร์ก็จะกลายเป็นปัญหา
คุณอาจใช้สิ่งอย่าง async import เพื่อโหลดบันเดิลแบบหน่วงเวลาได้ แต่แล้ว JavaScript ที่ถูกโหลดทีหลังก็จะส่งรีเควสต์อีกครั้งเพื่อดึงข้อมูลของตัวเอง ทำให้เกิด ปัญหา waterfall
เมื่อเจอปัญหาทั้งหมดนี้ สุดท้ายคุณก็จะมองหาแนวทางที่ทำให้ปัญหาซับซ้อนเรื่องการแชร์ลอจิก/สถานะระหว่างไคลเอนต์กับเซิร์ฟเวอร์จัดการได้ง่ายขึ้น
แนวทางนี้มีข้อจำกัดตามตัวอักษรเลยคือ ข้อจำกัดจากความเร็วแสง ว่า “เซิร์ฟเวอร์จะอยู่ใกล้ผู้ใช้ได้แค่ระดับหนึ่ง”
ขั้นต่อไปคือคอมไพล์เซิร์ฟเวอร์เป็น WebAssembly แล้วส่งไปยังไคลเอนต์
จากนั้นคุณก็สามารถเรนเดอร์การตอบสนองแบบมองโลกในแง่ดีระหว่างที่รอการตอบกลับจริงจากเซิร์ฟเวอร์ได้
ฟังดูเหมือนบ้าหน่อย ๆ แต่ผมเคยทำได้ในโปรเจกต์จริง และมันเหมือนเวทมนตร์เลย
ต่อให้รันเซิร์ฟเวอร์บนไคลเอนต์ก็ไม่ได้แก้เรื่องนั้น และสุดท้าย ความหน่วงของเครือข่ายสำหรับ persistence ก็ยังคงอยู่
สถานะของเซิร์ฟเวอร์ที่ท้ายที่สุดต้องเป็นฝ่ายชนะก็คือ persistence นั่นเอง
overengineering จำนวนมากก็ถูกทำขึ้นจริงโดยไม่มีเหตุผลพิเศษอะไร
ผมเป็นคนสร้าง LiveSvelte ถ้ามีอะไรสงสัยก็บอกได้
โดยรวมแล้วอยากสร้างแอปด้วยโมเดลแบบนี้มาตลอด
เป็นแนวทางแบบ event-driven, อัปเดตแบบเรียลไทม์สองทางระหว่างเซิร์ฟเวอร์กับไคลเอนต์, มีลำดับของเหตุการณ์, และมีสถานะแบบ local/remote
ถึงจะไม่รู้จัก LiveView มาก่อนและไม่เคยใช้ภาษาในตระกูล Erlang แต่ก็รู้สึกได้ชัดว่ามันจับอะไรบางอย่างได้ถูกจุด
โมเดล request-response แบบดั้งเดิมมักก่อปัญหาเรื่องความสอดคล้องและข้อมูลล้าสมัยแบบแฝง ๆ อยู่บ่อยครั้ง
ถ้า 10 ปีที่ผ่านมาเป็นช่วงที่แนวคิด functional programming ถูกผสานเข้าสู่ภาษากระแสหลัก ก็หวังว่า 10 ปีข้างหน้าจะเป็นทิศทางที่ การเขียนโปรแกรมเชิงข้อความแบบมีสถานะ ถูกผสานเข้าสู่ฟูลสแตกกระแสหลัก
อนึ่ง สามตัวแรกนั้นฉันเป็นคนสร้างเองหรือช่วยสร้าง
ถ้าลองทำตามไกด์และเล่นดูนิดหน่อย ก็น่าจะรัน แอปแชต Phoenix พื้นฐานในเบราว์เซอร์ได้ภายในราวสองชั่วโมง
เป็นทางออกที่ดี
ในแอปของฉันใช้ Stimulus controller ที่นำกลับมาใช้ซ้ำได้ร่วมกับ LiveView และมันก็ทำงานได้ลื่นมาก
โดยรวมแล้วการพัฒนาด้วย LiveView เป็นเรื่องสนุก แต่ยิ่งใช้กับสถานการณ์จริงมากขึ้น ก็ยิ่งสัมผัสได้ถึงข้อดีของเฟรมเวิร์ก HTTP แบบไร้สถานะอย่าง Hotwire
มันให้ความรู้สึกว่าประสิทธิภาพดีกว่า ทนต่อการเชื่อมต่อใหม่ได้ดีกว่า และลดความจำเป็นในการวางเซิร์ฟเวอร์เพิ่มใกล้ผู้ใช้เพื่อความเสถียรลงด้วย
แต่ Stimulus ไม่ได้มีวิธีที่สวยงามในการเก็บสถานะไว้ที่ฝั่งไคลเอนต์
มันเป็นแนวประมาณว่า “สถานะของแอปพลิเคชัน Stimulus อยู่ในแอตทริบิวต์ของ DOM” ซึ่งไม่ได้ดีกว่า vanilla JS หรือ jQuery
ฉันยังไม่เคยใช้ Stimulus ในโปรเจกต์จริง จึงเป็นไปได้ว่าค่าและ callback ตอนมีการเปลี่ยนแปลงอาจให้ประสบการณ์ที่ดีกว่าที่คิดไว้ตอนแรก
ชุดผสมนี้มีระดับความซับซ้อนที่กำลังพอดี และพอใช้ Svelte ร่วมกับ LiveView ก็ให้ความรู้สึกว่าไปจัดการหลายอย่างที่จริง ๆ แล้วไม่ใช่ปัญหาตั้งแต่แรก
สิ่งที่ต้องการจริง ๆ คือ vanilla JS หรือเลเยอร์บาง ๆ ที่อยู่บนมัน ไม่ใช่เฟรมเวิร์กทั้งก้อน
ยังไงเสียก็ไม่จำเป็นต้องใช้ JavaScript มากนัก
JS ในแอปที่รันจริงมีไม่ถึง 200 บรรทัด และน่าจะเอา Stimulus controller ออกได้อีกบางตัว
LiveView ดีขนาดนั้นเลย
ฉันยังทำอะแดปเตอร์ hook สำหรับ Stimulus-LiveView แบบแฮ็ก ๆ ไว้ด้วย เพื่อให้ Stimulus controller ส่งอีเวนต์ไปยังโปรเซส LiveView ได้โดยตรง
และถ้าไม่ได้ดื่ม Kool-Aid แบบ fly.io มากเกินไป LiveView ก็ไม่ได้บังคับว่าต้องใช้เซิร์ฟเวอร์กระจายตามภูมิศาสตร์
มันจัดการการเชื่อมต่อหลุดได้ดีมาก
ฉันไม่ได้ทำอะไรเพิ่มเติมเลยเพื่อรองรับการอัปเดตแบบไม่สะดุด
ทั้งกระบวนการที่ไคลเอนต์สูญเสียการเชื่อมต่อ WebSocket แล้วเชื่อมต่อใหม่กับเวอร์ชันใหม่พร้อมกู้สถานะกลับมา ล้วนถูกจัดการอัตโนมัติเป็นค่าเริ่มต้น
จะต้องการอะไรไปมากกว่านี้อีก?
บริษัทของเราได้ประโยชน์มากจากการใช้ LiveView ร่วมกับ Alpine และโดยเฉพาะอย่างยิ่งเราใช้ โหมด CSP เพราะไม่สามารถอนุญาต
evalฝั่งไคลเอนต์ได้ตอนที่เห็น LiveSvelte ครั้งแรก มันดูใหม่มากและยังผ่านการพิสูจน์น้อย รวมถึงมีผลกระทบที่ยังไม่ได้คลี่คลายในสภาพแวดล้อม CSP ที่เข้มงวด
ตอนนี้ดูเหมือนว่ามันจะมีประโยชน์ขึ้นมากแล้ว ซึ่งก็น่ายินดี
แพตเทิร์นของเราพอจะเรียกว่า LiveAlpine ได้
ถ้าคอมโพเนนต์ต้องการ HTML ก็ใช้ HTML Component และถ้ามีสถานะฝั่งเซิร์ฟเวอร์ก็ใช้ Live Component
ถ้าต้องการพฤติกรรมหรือสถานะฝั่งไคลเอนต์ ก็จะนิยาม Alpine component เพิ่ม
ถ้าต้องรับอีเวนต์จากไคลเอนต์ที่ฝั่งเซิร์ฟเวอร์หรือจำเป็นต้องส่ง HTTP request ก็จะนิยาม Phoenix Hook เพิ่ม