ข้ามไปยังเนื้อหา

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.css import ครั้งเดียวต่อแอปแปลว่าทั้งสาม surface อ่าน #8b5cf6 จาก source เดียวกัน; rebrand ในไฟล์เดียว
  • Cons: แต่ละแอปต้องจำให้ import sheet; ลืมแล้ว surface นั้นจะ fall back ไปที่ default ที่ hard-code ของ primitive เงียบ ๆ แทนที่จะ fail แบบดัง ๆ

ทุกแอป install package และ import tokens.css ครั้งเดียวที่ entry แล้ว register primitive

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>
);
}

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>

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>

ถอยออกมาแล้ว 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 กัน

Terminal window
pnpm --filter catalog dev # React, http://localhost:5001
pnpm --filter cart dev # Svelte, http://localhost:5002
pnpm --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 ต่อสายอยู่:

Terminal window
pnpm --filter catalog build && pnpm --filter cart build && pnpm --filter content build
# all three succeed

Check your understanding:

  1. ทำไม ไม่มี framework ไหนต้องมี adapter เพื่อ render <m-button> ในเมื่อแต่ละตัวต้องมี component ของตัวเองสำหรับทุกอย่างที่เหลือ?
  2. Astro register primitive ข้างใน <script> ขณะที่ React import ที่ entry module ทำไมถึงต่างกัน และแต่ละแบบทำอะไรสำเร็จ?
  3. การส่ง string attribute ให้ custom element ได้ผลเหมือนกันทุกที่; การส่ง object ไม่ ทำไม และการคง API ของ primitive ให้เป็นมิตรกับ attribute เลี่ยงปัญหานี้ยังไง?
  4. ถ้า 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 →