Consuming Everywhere
สิ่งที่จะสร้าง
หัวข้อที่มีชื่อว่า “สิ่งที่จะสร้าง”primitive สามตัวเดิม — <m-button>, <m-card>, <m-badge> จาก Tokens and Primitives → — render จาก React (catalog และ shell), Svelte (cart), และ Astro (content site) package เดียว import สามแบบ ให้ UI ที่เหมือนกันระดับ pixel ข้าม runtime สามตัว
นี่คือผลตอบแทนของการเลือก custom element เป็น shared-UI layer ของ Mosaic ที่แนะนำครั้งแรกใน Web Components Interop → ตรงนี้เราพิสูจน์ให้เห็นครบตั้งแต่ต้นจนจบ
ทุก framework ใน Mosaic ท้ายที่สุด render DOM custom element คือ DOM — tag จริงที่ browser เข้าใจ — framework ไหนก็ผลิตได้ด้วย templating syntax ของตัวเองโดยไม่ต้องมี adapter นั่นคือประเด็นทั้งหมด: shared layer อยู่ ใต้ framework ที่ระดับ platform ที่ทั้งสามตัวเห็นตรงกันอยู่แล้ว
ทางเลือกอื่น — React component library — จะบังคับให้ทีม Svelte และ Astro reimplement หรือ wrap ทุก component และ copy พวกนั้นจะ drift ด้วย custom element ไม่มีอะไรต้อง port งานของ framework หดเหลือ “emit tag แล้วส่ง attribute”; shadow DOM ของ primitive ทำที่เหลือ
ข้อควรระวังหนึ่งที่ต้องพูดตั้งแต่แรก: การส่ง rich data (object, array) ให้ custom element ต่างกันไปตาม framework attribute เป็น string React 19 และ Svelte 5 ต่างก็ set JS property บน custom element เมื่อค่าไม่ใช่ string ซึ่งจัดการ object ได้สะอาด; React รุ่นเก่าต้องใช้ ref เพื่อ set property เอง สำหรับ primitive ของ Mosaic เราคง public API ให้เป็นมิตรกับ attribute (string, boolean, slot) ซึ่งเลี่ยงปัญหานั้นไปเลย
ข้อดีข้อเสีย
หัวข้อที่มีชื่อว่า “ข้อดีข้อเสีย”Custom elements as the shared layer vs. a per-framework component library
- Pros: เขียนครั้งเดียว render ใน framework ไหนก็ได้; ไม่มี wrapper, ไม่มี drift, ไม่มี framework runtime ที่ ship มากับ design system primitive upgrade ให้ทุกทีมพร้อมกันเมื่อ package อัปเดต
- Cons: ergonomics ต่ำกว่า native framework component — ไม่มี typed props, การจัดการ event เป็นแบบ
addEventListenerและ SSR ของ shadow DOM ต้องระวัง (declarative shadow DOM) คุณแลก developer sugar กับความ universal
One shared token sheet vs. each app theming itself
- Pros:
tokens.cssimport ครั้งเดียวต่อแอปแปลว่าทั้งสาม surface อ่าน#8b5cf6จาก source เดียวกัน; rebrand ในไฟล์เดียว - Cons: แต่ละแอปต้องจำให้ import sheet; ลืมแล้ว surface นั้นจะ fall back ไปที่ default ที่ hard-code ของ primitive เงียบ ๆ แทนที่จะ fail แบบดัง ๆ
ติดตั้ง
หัวข้อที่มีชื่อว่า “ติดตั้ง”ทุกแอป install package และ import tokens.css ครั้งเดียวที่ entry แล้ว register primitive
1. apps/catalog/src/main.tsx (React)
หัวข้อที่มีชื่อว่า “1. apps/catalog/src/main.tsx (React)”React render custom element เป็น JSX tag ธรรมดา attribute map ตรง ๆ; slot children ก็คือ children React 19 ปรับปรุงการรองรับ custom element ดังนั้น boolean/attribute prop ทำงานตามที่คุณคาด
import "@mosaic/design-system/tokens.css";import "@mosaic/design-system"; // registers <m-button>, <m-card>, <m-badge>
export function ProductCard({ name, isNew }: { name: string; isNew: boolean }) { return ( <m-card> {isNew && <m-badge>New</m-badge>} <p>{name}</p> <m-button onClick={() => bus.emit("cart:add", { name })}> Add to cart </m-button> </m-card> );}2. apps/cart/src/Summary.svelte (Svelte 5)
หัวข้อที่มีชื่อว่า “2. apps/cart/src/Summary.svelte (Svelte 5)”Svelte render custom element ได้แบบ native — ไม่มีพิธี import อะไรนอกจากการ register package สังเกตว่า primitive ตรงนี้คือ custom element ของ design-system ต่างจาก <cart-app> element ของ cart remote เองจาก Mount via Web Component →
<script lang="ts"> import "@mosaic/design-system/tokens.css"; import "@mosaic/design-system";
let { items = [] } = $props();</script>
<m-card> <m-badge tone="danger">{items.length}</m-badge> {#each items as item} <p>{item.name}</p> {/each} <m-button>Checkout</m-button></m-card>3. apps/content/src/pages/index.astro (Astro)
หัวข้อที่มีชื่อว่า “3. apps/content/src/pages/index.astro (Astro)”Astro ship client JS เป็นศูนย์โดย default เราจึง register primitive ใน module <script> tag render เป็น HTML ธรรมดาบน server แล้วนิยาม custom-element ค่อย upgrade ให้บน client นี่คือ WC bundle เดียวกับที่ Astro expose สำหรับ path การ compose (ดู Composing SSR →)
---import "@mosaic/design-system/tokens.css";---
<m-card> <m-badge>Sale</m-badge> <p>Independent teams, one storefront.</p> <m-button>Shop the catalog</m-button></m-card>
<script> import "@mosaic/design-system"; // upgrades the tags on the client</script>4. Why this is the right shared layer for multi-framework MFE
หัวข้อที่มีชื่อว่า “4. Why this is the right shared layer for multi-framework MFE”ถอยออกมาแล้ว pattern ชัดเจน: primitive ถูกนิยาม ครั้งเดียว ใน package ที่เป็นกลางต่อ framework และ MFE แต่ละตัวแค่ เรียกชื่อ ออกมา ไม่มี React tree ร่วม, ไม่มีการเดินสาย prop ข้าม framework, ไม่มี build-time coupling ระหว่าง design system กับแอปไหน — ตรงกับการ decouple ที่ Mosaic ที่เหลือสร้างขึ้นพอดี design system deploy ตามจังหวะของตัวเอง และเพราะ customElements registry ของ browser resolve tag ตอน runtime primitive version ใหม่จึงถึงทุก remote แบบเดียวกับที่ remote ใหม่ถึง shell: ตอนโหลดครั้งถัดไป
ตรวจสอบผล
หัวข้อที่มีชื่อว่า “ตรวจสอบผล”รันสองแอปคู่กันแล้วเทียบ primitive ที่ share กัน
pnpm --filter catalog dev # React, http://localhost:5001pnpm --filter cart dev # Svelte, http://localhost:5002pnpm --filter content dev # Astro, http://localhost:5003จากนั้นเช็ค:
<m-button>ใน React catalog, Svelte cart และหน้า Astro เหมือนกันในเชิงภาพ — สีม่วง, radius, padding เดียวกัน — เพราะทั้งสามอ่านtokens.cssเดียวกัน- ใน DevTools ของแต่ละแอป ปุ่มพก shadow root ที่ open; framework render แค่ tag ชั้นนอก
- comment out
import "@mosaic/design-system/tokens.css"ชั่วคราวในแอปหนึ่ง: surface นั้น fall back ไปที่ default ในตัวของ primitive ขณะที่ตัวอื่นยังมี theme — หลักฐานที่มองเห็นว่า token sheet คือ single source of truth
ยืนยันว่าแต่ละแอปยัง build ผ่านโดยมี shared package ต่อสายอยู่:
pnpm --filter catalog build && pnpm --filter cart build && pnpm --filter content build# all three succeedCheck your understanding:
- ทำไม ไม่มี framework ไหนต้องมี adapter เพื่อ render
<m-button>ในเมื่อแต่ละตัวต้องมี component ของตัวเองสำหรับทุกอย่างที่เหลือ? - Astro register primitive ข้างใน
<script>ขณะที่ React import ที่ entry module ทำไมถึงต่างกัน และแต่ละแบบทำอะไรสำเร็จ? - การส่ง string attribute ให้ custom element ได้ผลเหมือนกันทุกที่; การส่ง object ไม่ ทำไม และการคง API ของ primitive ให้เป็นมิตรกับ attribute เลี่ยงปัญหานี้ยังไง?
- ถ้า package design-system ship style
<m-button>ใหม่ style นั้นไปถึง catalog, cart และ content app ได้ยังไง — และนั่นเหมือนกับวิธีที่ remote ใหม่ไปถึง shell อย่างไร?
เรา render primitive ของ @mosaic/design-system จาก React, Svelte และ Astro โดยไม่มี wrapper ต่อ framework — นิยามเดียว สาม runtime หน้าตาที่สอดคล้องกันเดียว custom element เป็น shared-UI layer ที่ถูกต้องสำหรับ multi-framework micro-frontend เพราะอยู่ใต้ framework ที่ระดับ browser ที่ทุกทีมเห็นตรงกันอยู่แล้ว
นั่นครอบคลุม หน้าตา ที่ share กัน ต่อไปเราประสาน navigation ที่ share กัน: shell และ remote แบ่ง URL กันยังไงใน Routing Across MFEs →