- โครงสร้าง URL ไม่ได้เป็นเพียงที่อยู่เท่านั้น แต่ยังทำงานเป็น วิธีเก็บและกู้คืนสถานะของแอปพลิเคชัน
- ยกตัวอย่างกรณีอย่าง หน้าดาวน์โหลด PrismJS ที่สามารถสร้างการตั้งค่าธีม ภาษา และปลั๊กอินกลับมาได้ครบถ้วนด้วย URL เพียงเส้นเดียว
- องค์ประกอบแต่ละส่วน เช่น พาธ, คิวรีพารามิเตอร์, ฟรากเมนต์ สามารถใช้แทนสถานะได้หลากหลาย ทั้งการนำทางแบบลำดับชั้น การกรอง และการนำทางภายในฝั่งไคลเอนต์
- สถานะอย่าง ตัวกรองการค้นหา, pagination, โหมดการแสดงผล, ช่วงวันที่ เหมาะจะใส่ไว้ใน URL ขณะที่ ข้อมูลอ่อนไหวหรือสถานะ UI ชั่วคราว ไม่เหมาะ
- URL ที่ออกแบบมาดีจะช่วยเพิ่ม ความสามารถในการแชร์, ความคาดเดาได้, ประสิทธิภาพการแคช และเสริม ความน่าเชื่อถือกับประสบการณ์ผู้ใช้ ของเว็บแอปพลิเคชัน
ศักยภาพของ URL
- URL ไม่ได้เป็นแค่ที่อยู่ของทรัพยากร แต่ยังทำหน้าที่เป็น ส่วนติดต่อผู้ใช้ (UI) และ คอนเทนเนอร์ของสถานะ
- ช่วยเก็บสถานะโดยอัตโนมัติในการแชร์, บุ๊กมาร์ก, ประวัติเบราว์เซอร์, deep link
- ทำหน้าที่เป็นกลไกจัดการสถานะพื้นฐานของเว็บมาตั้งแต่ปี 1991
- องค์ประกอบแต่ละส่วนของ URL ใช้แสดงสถานะคนละแบบ
- พาธ (Path) : การนำทางทรัพยากรแบบลำดับชั้น (
/users/123/posts)
- คิวรี (Query) : ตัวกรอง ตัวเลือก และการตั้งค่า (
?theme=dark&lang=en)
- ฟรากเมนต์ (Fragment) : ตำแหน่งภายในเอกสารหรือการ route ใน SPA (
#features, #/dashboard)
- ฟีเจอร์ Text Fragments ช่วยลิงก์ไปยังข้อความเฉพาะภายในหน้าได้โดยตรง
แพตเทิร์นของคิวรีพารามิเตอร์
- ใช้ ตัวคั่น (delimiter) เพื่อเก็บหลายค่าไว้ในคีย์เดียว (
?tags=frontend,react,hooks)
- ทำ ข้อมูลแบบซ้อน ให้เป็นซีเรียลไลซ์ด้วย JSON หรือ Base64 (
?config=eyJyaWNrIjoicm9sbCJ9==)
- Boolean flag แสดงด้วยการมีอยู่ของคีย์ (
?mobile)
- รูปแบบอาร์เรย์ (bracket notation) ใช้แทนค่าหลายค่าในรูป
tags[]=frontend&tags[]=react
- เครื่องมืออย่าง
qs ของ Node หรือ Express middleware รู้จำได้อัตโนมัติ แต่ยังไม่ใช่มาตรฐาน
- หัวใจสำคัญคือ ต้องรักษาความสม่ำเสมอ
ตัวอย่างการแทนสถานะผ่าน URL
- PrismJS: เก็บการตั้งค่าธีม ภาษา และปลั๊กอินทั้งหมดไว้ใน URL hash
- GitHub: ใช้
#L108-L136 เพื่อไฮไลต์ช่วงบรรทัดโค้ดที่ต้องการ
- Google Maps: ใส่พิกัด ระดับการซูม และประเภทแผนที่ไว้ใน URL
- Figma: แชร์บริบทการทำงาน เช่น ตำแหน่งบนแคนวาส การซูม และองค์ประกอบที่เลือก ผ่าน URL
- เว็บไซต์อีคอมเมิร์ซ: ใส่ตัวกรอง การเรียงลำดับ และช่วงราคาไว้ใน URL เพื่อกู้คืนสถานะการค้นหา
แพตเทิร์นทางวิศวกรรมฟรอนต์เอนด์
- สถานะที่เหมาะจะใส่ใน URL
- คำค้นหา ตัวกรอง หน้าและการเรียงลำดับ โหมดการแสดงผล ช่วงวันที่ แท็บที่เปิดอยู่ การจัดวาง UI และฟีเจอร์แฟลก
- สถานะที่ไม่เหมาะกับ URL
- ข้อมูลอ่อนไหวอย่างรหัสผ่านหรือโทเค็น สถานะ UI ชั่วคราว ข้อมูลที่ยังไม่บันทึก ข้อมูลขนาดใหญ่ และสถานะที่เปลี่ยนถี่มาก
- เกณฑ์ตัดสินคือ: ถ้าผู้ใช้อีกคนคลิก URL เดียวกัน เขาควรเห็นสถานะเดียวกันหรือไม่
การทำงานใน JavaScript
- ใช้ API
URLSearchParams เพื่ออ่านและเขียนคิวรีพารามิเตอร์
- ใช้
pushState เพื่อเพิ่มรายการใหม่ในประวัติ และใช้ replaceState เพื่ออัปเดตรายการปัจจุบัน
- ใช้อีเวนต์
popstate เพื่อกู้คืน UI เมื่อผู้ใช้กดย้อนกลับในเบราว์เซอร์
การทำงานใน React
- ใช้ฮุก
useSearchParams ของ React Router เพื่อจัดการสถานะใน URL ได้อย่างกระชับ
- เมื่ออ่านหรืออัปเดตพารามิเตอร์ ระบบจะซิงก์ URL กับ UI ให้อัตโนมัติ
แนวปฏิบัติที่ดีในการจัดการสถานะด้วย URL
- อย่าใส่ค่าเริ่มต้นไว้ใน URL (เก็บไว้เฉพาะ
?theme=dark และให้โค้ดจัดการค่าเริ่มต้น)
- ใช้ debouncing เพื่อป้องกันการอัปเดต URL ถี่เกินไประหว่างพิมพ์ (
lodash.debounce)
- pushState vs replaceState
pushState: เหมาะกับสถานะที่ควรย้อนกลับได้ เช่น การเปลี่ยนตัวกรองหรือเปลี่ยนหน้า
replaceState: เหมาะกับการแก้ไขย่อย ๆ เช่น การพิมพ์ในช่องค้นหา
มอง URL เป็นสัญญา (Contract)
- URL ที่ออกแบบมาดีทำหน้าที่เป็น สัญญาที่ชัดเจนระหว่างแอปพลิเคชันกับผู้ใช้
- ช่วยกำหนดขอบเขตระหว่างสถานะแบบสาธารณะ/ส่วนตัว, ฝั่งไคลเอนต์/ฝั่งเซิร์ฟเวอร์, แบบแชร์ได้/แบบเซสชัน ได้ชัดเจน
- URL ที่อ่านเข้าใจง่าย ช่วยอธิบายเจตนา และให้ทั้งคนกับเครื่องเข้าใจได้
- รูปแบบอย่าง
example.com/products/laptop?color=silver&sort=price สื่อความหมายได้ดี
- ช่วยเพิ่ม ประสิทธิภาพการแคช
- URL เดียวกันจะถูกมองเป็นทรัพยากรเดียวกัน จึงเพิ่มอัตราแคชฮิต
- ควบคุมการแปรผันของแคชได้ผ่านคิวรีพารามิเตอร์
- การจัดการเวอร์ชันและการทดลอง
- ใช้
?v=2, ?beta=true, ?experiment=new-ui เพื่อแยกเวอร์ชัน API หรือการทดสอบ A/B
แอนติแพตเทิร์นที่ควรหลีกเลี่ยง
- เก็บสถานะไว้แค่ในหน่วยความจำของ SPA จนรีเฟรชแล้วสถานะหาย
- ใส่ข้อมูลอ่อนไหวไว้ใน URL (
?password=secret123)
- ใช้ชื่อพารามิเตอร์ที่ไม่ชัดเจน (
?foo=true&bar=2 แทนที่จะเป็น ?mobile=true&page=2)
- เข้ารหัส JSON ที่ซับซ้อนด้วย Base64 จน URL ยาวเกินจำเป็น
- เกินข้อจำกัดความยาวของ URL (มีข้อจำกัดจากเบราว์เซอร์ เซิร์ฟเวอร์ และ CDN)
- ทำให้ปุ่มย้อนกลับใช้งานไม่ได้ (มักเกิดจากการใช้
replaceState มากเกินไป)
บทสรุป
- URL ที่ดีไม่ได้แค่ชี้ไปยังเนื้อหา แต่ยังแสดงบทสนทนาระหว่างผู้ใช้กับแอปพลิเคชัน
- URL คือเครื่องมือจัดการสถานะที่เก่าแก่และสง่างามที่สุดในการเก็บ เจตนา บริบท และความสามารถในการแชร์
- แม้จะมีเครื่องมือจัดการสถานะที่ซับซ้อนอย่าง Redux, MobX, Zustand, Recoil
การไม่ลืมความสามารถพื้นฐานของ URL คือจุดแข็งที่แท้จริงของเว็บ
- แอปที่ทำสถานะหายเมื่อรีเฟรช กำลังพลาดคุณสมบัติพื้นฐานของเว็บไป
2 ความคิดเห็น
ความเห็นบน Hacker News
เวลาทำ code review ฉันพยายามเก็บ state ให้มากที่สุดเท่าที่จะทำได้ไว้ใน URL
หลังรีเฟรชแล้วกระโดดไปคนละตำแหน่งโดยสิ้นเชิง หรือ URL ที่แชร์กลับเปิดหน้าจอผิด ๆ ถือเป็นประสบการณ์ที่แย่มากสำหรับผู้ใช้
วิธีนี้ทำให้การพัฒนาช้าลง แต่ก็ช่วยให้ทีมตระหนักเรื่อง UX มากขึ้น และเห็นชัดว่าใน view หนึ่ง ๆ มี state อยู่มากแค่ไหน
บางคนกังวลว่า URL จะกลายเป็นเหมือน public API ที่สร้างข้อจำกัดขึ้นมา แต่ฉันคิดว่า URL ส่วนใหญ่ถูกใช้งานแค่ช่วงสั้น ๆ จึงไม่ใช่ปัญหาใหญ่
ถ้าจำเป็นก็แก้ได้ด้วยโค้ด migration ที่แปลง URL เก่าเป็น URL ใหม่ตอนโหลด
ฉันคิดว่าใช้ query parameter แทน path จะดีกว่าเล็กน้อย
จากมุมผู้ใช้ คำว่า “ย้อนกลับ” มันผูกกับปุ่มของเบราว์เซอร์อยู่แล้ว เลยทำให้สับสน
การที่รีเฟรชแล้ว state ถูกรีเซ็ตยังน่าหงุดหงิดน้อยกว่า เพราะคนมักเข้าใจว่า “รีเฟรช = เริ่มใหม่ตั้งแต่ต้น”
แต่พอให้ JS จัดการทุกอย่าง ฟีเจอร์พื้นฐานแบบนี้มักเสียไปแบบแนบเนียน
แต่แม้จะเคยทำงานกับ UX designer มากกว่า 30 คน ก็ไม่เคยได้รับแนวทางเรื่อง URL เลย
โดยเฉพาะบนมือถือ การพาหน้ากลับไปสู่ state เริ่มต้นทำได้ยาก เลยทำให้รีเฟรชเป็นทางออกที่เร็วที่สุด
ใน UI ที่มี infinite scroll หรือ filter ซับซ้อน ยิ่งมี state อยู่ใน URL มาก การรีเซ็ตก็ยิ่งน่ารำคาญ
ถ้าผู้ใช้ไม่พอใจกับ UX อยู่แล้ว แล้วยังต้องมาจัดการ URL อีก นั่นคือความเครียดซ้ำซ้อนสำหรับผู้ใช้
ฉันรู้สึกว่าแม้แต่คนที่มี digital literacy สูงก็ยัง เข้าใจ URL และ DNS ต่ำ
ควรรู้วิธีลดความเสี่ยงจากฟิชชิง เข้าใจความหมายของ URL parameter (
?t=_,utm_) และลบข้อมูลส่วนตัวก่อนแชร์ได้และควรรู้ด้วยว่าแม่กุญแจ HTTPS ไม่ได้หมายถึง ‘ความน่าเชื่อถือ’
ถ้าใช้ URL เป็น state container โครงสร้างภายในก็จะ ถูกเปิดเผย และต้องมีการจัดการเวอร์ชัน
ยังอาจมีปัญหาเรื่องความเข้ากันได้ระหว่างเบราว์เซอร์หรือ flow การยืนยันตัวตนด้วย
ถึงอย่างนั้นฉันก็ยังพยายามเปิดเผย state ให้มากที่สุดใน URL เหมือนกับ argument ของ command line
แต่ทั้งหมดนี้เป็น trade-off ที่ตั้งใจเลือก ไม่ใช่เพราะไม่รู้หรือขาดประสบการณ์
ขอแนะนำ Rison แม้จะเป็นไลบรารีเก่าแต่ยังมีประโยชน์มาก
มันช่วยเก็บ JSON ลงใน URL ได้อย่างเรียบร้อย และยังถูกใช้ใน Kibana ของ Elastic ด้วย
ตัวอย่าง: http://example.com/service?query=q:'*',start:10,count:10
เมื่อระบบพัฒนาไป โครงสร้างของ state ก็เปลี่ยนตาม ดังนั้นการใส่ state ไว้ใน URL จึงทำให้การวิวัฒนาการของระบบ ถูกจำกัด
เพราะ URL โดยพื้นฐานแล้วคือ สตริงถาวร
ทางที่ดีกว่าคือมอง URL เป็นเหมือน protocol ชนิดหนึ่ง แล้วเข้ารหัส/ถอดรหัส state ผ่านมัน
ถ้าเป็นหน้าที่เรียบง่าย ก็อาจใส่ state ทั้งหมดลงใน URL ได้
แต่สำหรับสิ่งอย่าง feed ก็ขึ้นอยู่กับ ความคาดหวังของผู้ใช้ เช่น “พอกดรีเฟรชแล้วควรกลับไปเป็นสถานะล่าสุดหรือไม่?”
ข้อจำกัดเรื่องความยาว URL แตกต่างกันไปตามการตั้งค่าของเบราว์เซอร์ เซิร์ฟเวอร์ CDN และเสิร์ชเอนจิน แต่โดยทั่วไปจะอยู่ที่ ไม่เกิน 2000 อักขระ
เลยต้องคิดว่าจะยัด state ได้มากแค่ไหนภายใต้ข้อจำกัดนี้ หรือควรใช้วิธีอื่นแทน
- . _ ~) ดังนั้น ความหนาแน่นของข้อมูล จึงค่อนข้างสูงdraw.io สามารถเก็บ state ทั้งหมดไว้ใน URL เพื่อแชร์ได้
ข้อมูลไดอะแกรมจะถูกเข้ารหัสแบบ Base64 ทำให้กู้คืนได้ครบถ้วนจากลิงก์เดียว
แต่ก็ยังไม่แน่ใจว่าสิ่งนี้เข้ากับนิยามของ ‘state container’ หรือไม่
ฉันใช้ hash routing (#/dashboard) ในแอป self-hosted
เลยไม่ต้องพึ่งการ rewrite URL ฝั่งเซิร์ฟเวอร์ (.htaccess เป็นต้น) ซึ่งถึงจะไม่สมบูรณ์แบบ แต่ก็ช่วยลด ข้อจำกัดของสภาพแวดล้อมในการ deploy ได้
Microsoft Teams รุ่นล่าสุดจัดการทุกหน้าด้วย URL เดียว ทำให้ บุ๊กมาร์กไม่ได้
เปิดไปยังทีมหรือแชนเนลที่ต้องการโดยตรงไม่ได้เลย จึงไม่สะดวกมาก
HATEOAS ไม่ค่อยได้รับความสนใจเพราะชื่อไม่ดี แต่จริง ๆ แล้วมันคือแนวคิดพื้นฐานของเว็บ
แต่ในสภาพแวดล้อมที่ควบคุมทั้งเซิร์ฟเวอร์และไคลเอนต์ได้ มันกลับเพิ่ม ความซับซ้อนส่วนเกิน
โดยเฉพาะถ้าไคลเอนต์ยังต้องรู้โครงสร้างของ endpoint อยู่ดี มันก็แค่ทำให้ URL ดูทึบขึ้นเท่านั้น
ฉันใช้ฟีเจอร์พักแท็บเป็นประจำ แต่เว็บแอปที่ตรึงอยู่กับ URL เดียวและเคลื่อนย้ายเป็นก้อนเดียวกัน พอเข้าสู่โหมดพัก ข้อมูลก็หายไป
แต่ในขณะเดียวกัน หน้าเว็บพวกนั้นก็มักจะหนักกันทุกเว็บจนไม่พักแท็บก็ไม่ได้