Pattern: Widget-Based Decomposition (แพตเทิร์น: Widget-Based Decomposition)

ใน widget-based decomposition หน้าจอในส่วนติดต่อผู้ใช้แบบกราฟิกจะมี widget ต่างๆ ที่สามารถเปลี่ยนแปลงได้อย่างอิสระ ใน Figure 14-5 เราจะเห็นตัวอย่างของ frontend ของ MusicCorp ที่มี widget สองตัวรองรับ functionality ของ UI สำหรับตะกร้าสินค้าและคำแนะนำสินค้า

bms2 1405

Figure 14-5. widget ตะกร้าสินค้าและคำแนะนำสินค้าที่ใช้อยู่บน MusicCorp

widget คำแนะนำสินค้าของ MusicCorp ดึงข้อมูล carousel ของคำแนะนำที่สามารถเลื่อนดูและกรองได้ ดังที่เราเห็นใน Figure 14-6 เมื่อผู้ใช้โต้ตอบกับ widget คำแนะนำสินค้า เช่น เลื่อนไปยังชุดคำแนะนำถัดไป หรือเพิ่มสินค้าลงใน wishlist อาจส่งผลให้มีการเรียก backing microservice เช่นในกรณีนี้อาจเป็น microservice Recommendations และ Wishlist ซึ่งอาจสอดคล้องดีกับทีมที่เป็นเจ้าของทั้ง backing microservice เหล่านั้นและตัว component เอง

bms2 1406

Figure 14-6. การโต้ตอบระหว่าง recommendation micro frontend กับไมโครเซอร์วิสที่รองรับ

โดยทั่วไปแล้ว คุณจะต้องมีแอปพลิเคชัน "container" ที่กำหนดสิ่งต่างๆ อย่างเช่นการนำทางหลักสำหรับ interface และ widget ใดบ้างที่ต้องรวมเข้าไป ถ้าเราคิดในแง่ของทีม stream-oriented แบบ end-to-end เราสามารถจินตนาการได้ถึงทีมเดียวที่มอบ widget คำแนะนำสินค้าและยังรับผิดชอบ microservice Recommendations ที่รองรับด้วย

แพตเทิร์นนี้พบได้บ่อยในโลกจริง ตัวอย่างเช่น ส่วนติดต่อผู้ใช้ของ Spotify ใช้แพตเทิร์นนี้อย่างหนัก widget หนึ่งอาจเก็บ playlist อีก widget หนึ่งอาจเก็บข้อมูลเกี่ยวกับศิลปิน และ widget ที่สามอาจเก็บข้อมูลเกี่ยวกับศิลปินและผู้ใช้ Spotify คนอื่นๆ ที่คุณติดตาม widget เหล่านี้ถูกนำมาผสมผสานกันในรูปแบบต่างๆ ในสถานการณ์ที่แตกต่างกัน

คุณยังต้องมี assembly layer บางอย่างเพื่อดึงชิ้นส่วนเหล่านี้มารวมกัน ซึ่งอาจง่ายเพียงแค่การใช้ template ฝั่งเซิร์ฟเวอร์หรือฝั่งไคลเอนต์ก็ได้

Implementation (การนำไปใช้งาน)

วิธีการต่อ widget เข้าไปใน UI ของคุณจะขึ้นอยู่กับว่า UI ของคุณถูกสร้างขึ้นมาอย่างไรเป็นหลัก สำหรับเว็บไซต์ธรรมดา การรวม widget เป็น HTML fragment โดยใช้ template ฝั่งไคลเอนต์หรือฝั่งเซิร์ฟเวอร์นั้นทำได้ค่อนข้างตรงไปตรงมา แม้ว่าคุณอาจเจอปัญหาถ้า widget มีพฤติกรรมที่ซับซ้อนกว่า ยกตัวอย่างเช่น ถ้า widget คำแนะนำสินค้าของเรามี JavaScript functionality เยอะมาก เราจะมั่นใจได้อย่างไรว่ามันจะไม่ชนกับพฤติกรรมที่โหลดเข้ามาในส่วนอื่นๆ ของหน้าเว็บ ตามหลักการแล้ว widget ทั้งหมดควรถูก package ในลักษณะที่จะไม่ทำให้แง่มุมอื่นๆ ของ UI พัง

ปัญหาว่าจะส่งมอบ functionality ที่เป็นอิสระในตัวเองเข้าไปใน UI โดยไม่ทำให้ functionality อื่นพังได้อย่างไรนั้น เป็นปัญหาที่ยากเป็นพิเศษมาโดยตลอดสำหรับ single-page app ส่วนหนึ่งเพราะแนวคิดของ modularization ดูเหมือนจะไม่ใช่ประเด็นสำคัญอันดับต้นๆ ตอนที่เฟรมเวิร์ก SPA ที่รองรับสิ่งเหล่านี้ถูกสร้างขึ้นมา ความท้าทายเหล่านี้คุ้มค่าแก่การสำรวจให้ลึกยิ่งขึ้น

Dependencies (การพึ่งพา)

แม้ iFrame จะเป็นเทคนิคที่ถูกใช้อย่างหนักในอดีต แต่เรามักหลีกเลี่ยงการใช้มันเพื่อต่อ widget ต่างๆ เข้าเป็นหน้าเว็บเดียว iFrame มีความท้าทายมากมายในเรื่องขนาดและในแง่ของการทำให้การสื่อสารระหว่างส่วนต่างๆ ของ frontend เป็นเรื่องยาก แทนที่จะทำแบบนั้น widget มักถูกต่อเข้าไปใน UI ผ่าน server-side templating หรือไม่ก็ถูกแทรกเข้าไปในเบราว์เซอร์แบบ dynamic ทางฝั่งไคลเอนต์ ในทั้งสองกรณี ความท้าทายคือ widget กำลังทำงานอยู่ในหน้าเบราว์เซอร์เดียวกันกับส่วนอื่นๆ ของ frontend ซึ่งหมายความว่าคุณต้องระมัดระวังไม่ให้ widget ต่างๆ ขัดแย้งกัน

ตัวอย่างเช่น widget คำแนะนำสินค้าของเราอาจใช้ React v16 ในขณะที่ widget ตะกร้าสินค้ายังใช้ React v15 อยู่ นี่อาจเป็นข้อดีด้วยซ้ำ เพราะช่วยให้เราลองใช้เทคโนโลยีที่ต่างกันได้ (เราสามารถใช้เฟรมเวิร์ก SPA ที่ต่างกันสำหรับ widget ที่ต่างกัน) แต่ก็ยังช่วยเมื่อต้องอัปเดตเวอร์ชันของเฟรมเวิร์กที่ใช้ด้วย ผมเคยคุยกับหลายทีมที่เจอความท้าทายในการย้ายระหว่างเวอร์ชันของ Angular หรือ React ซึ่งส่วนใหญ่เกิดจากความแตกต่างของ convention ที่ใช้ในเฟรมเวิร์กเวอร์ชันใหม่ การอัปเกรด UI แบบ monolithic ทั้งหมดอาจเป็นเรื่องน่าหวาดหวั่น แต่ถ้าคุณสามารถทำแบบค่อยเป็นค่อยไป อัปเดตส่วนต่างๆ ของ frontend ทีละส่วน คุณก็สามารถแบ่งงานออกเป็นชิ้นเล็กๆ และลดความเสี่ยงที่การอัปเกรดจะก่อปัญหาใหม่ได้ด้วย

ข้อเสียคือคุณอาจลงเอยด้วยการมี dependency ซ้ำซ้อนกันมากระหว่าง widget ต่างๆ ซึ่งอาจทำให้ page load size บวมขึ้นมาก ผมอาจลงเอยด้วยการรวม React framework หลายเวอร์ชันพร้อมกับ transitive dependency ที่เกี่ยวข้องเข้าไปด้วย ยกตัวอย่างเช่น ไม่น่าแปลกใจเลยที่หลายเว็บไซต์ตอนนี้มี page load size ใหญ่กว่าขนาดของระบบปฏิบัติการบางตัวหลายเท่า จากการศึกษาแบบไม่เป็นทางการอย่างรวดเร็ว ผมตรวจสอบ page load ของเว็บไซต์ CNN ในช่วงเวลาที่เขียนบทนี้ พบว่ามีขนาด 7.9 MB ซึ่งใหญ่กว่า Alpine Linux ที่มีขนาด 5 MB มาก และ 7.9 MB นี้ยังถือว่าอยู่ในระดับเล็กเมื่อเทียบกับ page load size บางอย่าง ที่ผมเคยเห็น ด้วยซ้ำ

Communication between in-page widgets (การสื่อสารระหว่าง widget ในหน้าเดียวกัน)

แม้ widget ของเราจะถูกสร้างและ deploy ได้อย่างอิสระ แต่เราก็ยังอยากให้พวกมันสามารถโต้ตอบกันได้ ตัวอย่างจาก MusicCorp เมื่อผู้ใช้เลือกอัลบั้มหนึ่งใน best seller chart เราต้องการให้ส่วนอื่นๆ ของ UI อัปเดตตามการเลือกนั้น ดังที่แสดงใน Figure 14-7

bms2 1407

Figure 14-7. widget chart สามารถส่ง event ที่ส่วนอื่นๆ ของ UI คอยฟังอยู่ได้

วิธีที่เราจะทำสิ่งนี้ให้สำเร็จคือให้ widget chart ส่ง custom event ออกไป เบราว์เซอร์รองรับ event มาตรฐานหลายแบบอยู่แล้วที่เราสามารถใช้เพื่อกระตุ้นพฤติกรรมได้ event เหล่านี้ทำให้เราตอบสนองต่อการกดปุ่ม การเลื่อนเมาส์ และอื่นๆ และคุณคงเคยใช้การจัดการ event แบบนี้อย่างหนักมาแล้วถ้าเคยสร้าง frontend ด้วย JavaScript มันเป็นขั้นตอนง่ายๆ ที่จะสร้าง custom event ของคุณเอง

ดังนั้นในกรณีของเรา เมื่อมีการเลือกรายการใน chart widget นั้นจะส่ง custom event Album Selected ออกไป widget คำแนะนำสินค้าและรายละเอียดอัลบั้มทั้งคู่จะ subscribe event นี้และตอบสนองตามนั้น โดยคำแนะนำสินค้าจะอัปเดตตามที่เลือก และรายละเอียดอัลบั้มก็จะถูกโหลดขึ้นมา การโต้ตอบแบบนี้ควรจะคุ้นเคยกับเราอยู่แล้ว เพราะมันเลียนแบบการโต้ตอบแบบ event-driven ระหว่างไมโครเซอร์วิสที่เราคุยกันไปใน “Pattern: Event-Driven Communication” ความแตกต่างเดียวจริงๆ คือการโต้ตอบผ่าน event เหล่านี้เกิดขึ้นภายในเบราว์เซอร์

Web Components (Web Component)

เมื่อมองแวบแรก Web Component Standard น่าจะเป็นวิธีที่ชัดเจนในการ implement widget เหล่านี้ Web Component Standard อธิบายวิธีที่คุณสามารถสร้าง UI component ที่ sandbox แง่มุมด้าน HTML, CSS และ JavaScript ของตัวเองได้ น่าเสียดายที่มาตรฐาน web component ดูเหมือนจะใช้เวลานานมากในการนิ่งตัว และใช้เวลานานยิ่งกว่านั้นกว่าเบราว์เซอร์จะรองรับอย่างเหมาะสม งานเริ่มต้นส่วนใหญ่รอบๆ มันดูเหมือนจะหยุดชะงัก ซึ่งส่งผลกระทบต่อการนำไปใช้อย่างชัดเจน ผมยังไม่เคยเจอองค์กรที่ใช้ web component ที่ให้บริการมาจากไมโครเซอร์วิสเลย ยกเป็น ตัวอย่าง

เมื่อพิจารณาว่า Web Component Standard ตอนนี้ได้รับการรองรับค่อนข้างดีแล้ว ก็เป็นไปได้ว่าเราจะเห็นมันกลายเป็นวิธีทั่วไปในการ implement widget แบบ sandbox หรือ micro frontend ขนาดใหญ่กว่าในอนาคต แต่หลังจากรอคอยมาหลายปีให้สิ่งนี้เกิดขึ้น ผมก็ไม่ได้กลั้นหายใจรอมันแล้ว

When to Use It (เมื่อไรควรใช้)

แพตเทิร์นนี้ทำให้ stream-aligned team หลายทีมสามารถมีส่วนร่วมกับ UI เดียวกันได้ง่ายขึ้น มันให้ความยืดหยุ่นมากกว่า page-based decomposition เพราะ widget ที่ส่งมอบโดยทีมต่างกันสามารถอยู่ร่วมกันใน UI ในเวลาเดียวกันได้ทั้งหมด มันยังสร้างโอกาสให้ enabling team มอบ widget ที่นำกลับมาใช้ใหม่ได้ให้กับ stream-aligned team ซึ่งเป็นตัวอย่างหนึ่งที่ผมเคยเล่าไปก่อนหน้านี้เมื่อพูดถึงบทบาทของทีม Origami ของ Financial Times

แพตเทิร์นการแยกส่วนแบบ widget นั้นมีประโยชน์อย่างมากหากคุณกำลังสร้างส่วนติดต่อผู้ใช้บนเว็บที่มีความซับซ้อนสูง และผมขอแนะนำอย่างยิ่งให้ใช้ widget ในทุกสถานการณ์ที่คุณกำลังใช้เฟรมเวิร์ก SPA และต้องการแบ่งความรับผิดชอบของ frontend ออกจากกัน มุ่งสู่แนวทาง micro frontend เทคนิคและเทคโนโลยีที่รองรับแนวคิดนี้ได้พัฒนาขึ้นอย่างเห็นได้ชัดในช่วงไม่กี่ปีที่ผ่านมา จนถึงขนาดที่เมื่อสร้าง web interface แบบ SPA การแยก UI ของผมออกเป็น micro frontend จะกลายเป็นแนวทางเริ่มต้นของผม

ความกังวลหลักของผมเกี่ยวกับการแยก widget ในบริบทของ SPA เกี่ยวข้องกับงานที่ต้องทำเพื่อตั้งค่า bundling แยกของ component และปัญหาเรื่องขนาด payload ปัญหาแรกมักเป็นต้นทุนแบบครั้งเดียวและแค่ต้องหาว่ารูปแบบการ package แบบไหนเหมาะกับ toolchain ที่มีอยู่ของคุณที่สุด ส่วนปัญหาที่สองมีปัญหามากกว่า การเปลี่ยนแปลงเล็กๆ ง่ายๆ ใน dependency ของ widget หนึ่งอาจส่งผลให้มี dependency ใหม่จำนวนมากถูกรวมเข้าไปในแอปพลิเคชัน ทำให้ page size บวมขึ้นอย่างมาก ถ้าคุณกำลังสร้างส่วนติดต่อผู้ใช้ที่ page weight เป็นเรื่องที่ต้องกังวล ผมขอแนะนำให้ตั้ง automated check ไว้เพื่อแจ้งเตือนถ้า page weight เกินเกณฑ์ที่ยอมรับได้

ในทางกลับกัน ถ้า widget มีลักษณะเรียบง่ายกว่าและเป็น static component เป็นส่วนใหญ่ ความสามารถในการรวมมันเข้าไปด้วยวิธีง่ายๆ อย่าง client-side หรือ server-side templating ก็ตรงไปตรงมามากเมื่อเทียบกัน