Versioning remotes
สิ่งที่จะสร้าง
หัวข้อที่มีชื่อว่า “สิ่งที่จะสร้าง”ผลตอบแทนที่สถาปัตยกรรมทั้งหมดสร้างมาเพื่อไปให้ถึง: การ ship version ใหม่ของ หนึ่ง remote แบบ live โดยไม่ต้อง rebuild หรือ redeploy shell หรือ remote ตัวอื่นใด เราจะ version remoteEntry ของ catalog, roll out, และดู shell หยิบไปใช้ตอนผู้เข้าชมโหลดหน้าครั้งถัดไป — คำสัญญา “ship ทีมเดียว” ที่พิสูจน์ตั้งแต่ต้นจนจบ
เพราะ shell โหลด remote ตอน runtime จาก URL ของ remoteEntry version ของ remote จึงอยู่ที่ฝั่งของ remote ทั้งหมด เมื่อ catalog team deploy build ใหม่ พวกเขา publish remoteEntry.js ใหม่ที่ URL คงที่ที่ shell รู้จักอยู่แล้ว shell ไม่ได้ import catalog ตอน build จึงไม่มีอะไรต้อง rebuild — ครั้งถัดไป ที่ browser โหลด shell ก็จะ fetch remoteEntry.js ปัจจุบันแล้วได้ catalog ใหม่ นั่นคือ release train ที่ถูกแยกออกจากกัน: catalog team ship ตามตารางของตัวเอง, cart team ไม่รู้สึกอะไรเลย, shell ไม่ถูกแตะ
รายละเอียดที่ทำให้สิ่งนี้ปลอดภัย:
- Cache-busting the entry.
remoteEntry.jsคือ manifest ที่ shell อ่านเพื่อหา chunk ปัจจุบัน; จึงต้อง serve ด้วย header แบบ short/no-cache เพื่อให้เห็น deploy ใหม่ทันที ขณะที่ content chunk ที่ entry ชี้ไปเป็น immutable และ hash ไว้เพื่อ cache lifetime ที่ยาว ทำกลับกันแล้วผู้เข้าชมจะโหลด catalog ตัวเก่าจาก cache ต่อไปเรื่อย ๆ - Shared-version compatibility. catalog ตัวใหม่ยังประกาศ
reactเป็นsingletonพร้อมrequiredVersionModule Federation เจรจา version ตอนโหลด: ถ้า catalog ใหม่ต้องการ React ที่ shell ให้ไม่ได้ runtime จะ warn แทนที่จะ render กับ React สองตัวเงียบ ๆ การ version code ของ remote เอง นั้นฟรี; การเปลี่ยน shared major คือกรณีที่ต้องประสานงาน - A manifest for provenance. เปิด
manifest: trueจะ emitmf-manifest.jsonไว้ข้าง ๆremoteEntry.jsและadditionalDatahook stamp build id หรือ git SHA เข้าไปในนั้นได้ — เพื่อให้คุณบอกได้ว่า catalog version ไหน ที่ live อยู่โดยไม่ต้องเดา
ข้อดีข้อเสีย
หัวข้อที่มีชื่อว่า “ข้อดีข้อเสีย”Runtime rollout (new remoteEntry) vs. rebuilding the shell to adopt a remote change
- Pros (runtime): ทีมเดียว ship โดยไม่ต้องประสาน shell release; rollout คือ static deploy ครั้งเดียว; rollback คือการชี้กลับไปที่ asset ของ build ก่อนหน้า ไม่มี rebuild แบบ fan-out ข้ามทีม
- Cons (runtime): ไม่มี build-time type checking ข้าม seam shell↔remote — breaking change ต่อ props ของ exposed component จะไม่ถูก compiler ดัก มีแต่ตอน runtime คุณต้องมี contract (versioned exposes หรือ contract test) มาแทนสิ่งที่ build เดียวของ monolith ให้ฟรี
Immutable hashed chunks + short-cached entry vs. caching everything the same
- Pros (split caching): deploy ใหม่ถูกหยิบไปใช้เร็ว (entry สด) ขณะที่ chunk cache ราคาถูกได้ตลอดกาล (content-hashed) ได้ทั้งสองอย่าง
- Cons (split caching): มีสอง cache policy ให้ config ให้ถูกต่อ host;
remoteEntry.jscache ที่ config ผิดคือ bug คลาสสิก “ทำไม version เก่ายัง live อยู่”
ติดตั้ง
หัวข้อที่มีชื่อว่า “ติดตั้ง”1. apps/catalog/vite.config.ts
หัวข้อที่มีชื่อว่า “1. apps/catalog/vite.config.ts”emit manifest แล้ว stamp build เพื่อให้ทุก remote ที่ deploy แล้วระบุตัวได้ อันนี้เพิ่มเข้าไปใน federation config ตัวเดียวกับบทที่แล้ว
federation({ name: 'catalog', filename: 'remoteEntry.js', exposes: { './Catalog': './src/Catalog.tsx' }, shared: { react: { singleton: true, requiredVersion: '^19.0.0' }, 'react-dom': { singleton: true, requiredVersion: '^19.0.0' }, }, // Emit mf-manifest.json + mf-stats.json and stamp the build id into it. manifest: { additionalData: ({ stats }) => { stats.metaData.buildId = process.env.GIT_SHA ?? 'dev'; stats.metaData.deployedAt = new Date().toISOString(); }, },}),2. Cache headers — apps/catalog/public/_headers
หัวข้อที่มีชื่อว่า “2. Cache headers — apps/catalog/public/_headers”บน Cloudflare Pages ไฟล์ _headers ตั้ง split cache policy: entry ถูก revalidate เสมอ, hashed asset เป็น immutable
# The manifest the shell reads every load — never cache stale./remoteEntry.js Cache-Control: no-cache/mf-manifest.json Cache-Control: no-cache
# Content-hashed chunks — safe to cache forever./assets/* Cache-Control: public, max-age=31536000, immutable3. Ship a new catalog version
หัวข้อที่มีชื่อว่า “3. Ship a new catalog version”เปลี่ยนแค่ catalog, build ด้วย build id สด, deploy asset ของ catalog ไม่มีอะไรของ shell หรือ cart ถูกแตะ
# Make a catalog-only change (e.g. tweak the product grid), then:GIT_SHA=$(git rev-parse --short HEAD) \CATALOG_PUBLIC_URL=https://catalog.mosaic.example/ \ pnpm --filter catalog build
# Deploy ONLY the catalog's static assetspnpm dlx wrangler pages deploy apps/catalog/dist --project-name mosaic-catalog# ✔ https://catalog.mosaic.example — new remoteEntry.js is live4. (Optional) Runtime remote registration
หัวข้อที่มีชื่อว่า “4. (Optional) Runtime remote registration”ถ้าคุณอยากเลือก URL ของ remote — หรือ pin path ที่ version ไว้อย่าง /v2/remoteEntry.js — ทั้งหมดตอน runtime แทนที่จะตอน shell build ให้ register ด้วย runtime API ทำให้ shell adopt remote ใหม่หรือ canary URL ได้โดยไม่ต้อง rebuild เลย
import { registerRemotes, loadRemote } from '@module-federation/runtime';
// Point at a versioned path decided at runtime (flag, canary, A/B).registerRemotes([ { name: 'catalog', entry: 'https://catalog.mosaic.example/v2/remoteEntry.js', type: 'module', },]);
const { default: Catalog } = await loadRemote('catalog/Catalog');ตรวจสอบผล
หัวข้อที่มีชื่อว่า “ตรวจสอบผล”พิสูจน์ว่า rollout ไปถึงผู้เข้าชมและ shell ไม่เคยถูก rebuild
# The live entry is served fresh (not cached), so a new deploy is seen promptlycurl -sI https://catalog.mosaic.example/remoteEntry.js | grep -i 'cache-control'# cache-control: no-cache
# The manifest reports the build that's actually livecurl -s https://catalog.mosaic.example/mf-manifest.json | python3 -c \ 'import sys,json; print(json.load(sys.stdin)["metaData"].get("buildId"))'# 4f2a9c1 ← your new GIT_SHAทีนี้ทำ end-to-end check โดยที่ shell ที่คุณ deploy ในบทที่แล้ว ยังรัน build เก่าอยู่:
- จำหน้าตาปัจจุบันของ catalog ใน browser ที่ URL ของ shell ไว้
- deploy การเปลี่ยนเฉพาะ catalog ที่มองเห็นได้ (ตามขั้นตอนด้านบน) shell ไม่ ถูก rebuild หรือ redeploy
- hard-reload shell catalog ตัวใหม่โผล่ขึ้นมา — เพราะ shell re-fetch
remoteEntry.jsแล้วได้ manifest ใหม่ ค่าbuildIdในmf-manifest.jsonตรงกับGIT_SHAล่าสุดของคุณ ยืนยันได้ว่า version ไหนที่ live อยู่พอดี
# Confirm the shell was untouched: its last deploy predates the catalog changepnpm dlx wrangler pages deployment list --project-name mosaic-shell | head -3# The shell's latest deployment timestamp is OLDER than the catalog's — proof.คุณ ship ทีมเดียวได้โดยไม่ต้อง rebuild ตัวอื่น คุณสมบัติเดียวนั้นคือสิ่งที่ดีไซน์ shell-loads-remotes-at-runtime, Web-Component boundary, per-remote BFF, และ event bus ที่ decouple กัน ทั้งหมดมีอยู่เพื่อทำให้เรื่องนี้ปลอดภัย
ตรวจสอบความเข้าใจ:
- เมื่อ catalog team deploy build ใหม่ shell ต้อง rebuild อะไร — และทำไมคำตอบถึงเป็น “ไม่มีอะไรเลย”?
- ทำไม
remoteEntry.jsต้องมี headerno-cacheขณะที่ hashed/assets/*chunk cache ได้เป็นปี? - การ version code ของ remote เอง นั้นฟรี แต่การเปลี่ยน major ของ shared dependency ไม่ฟรี Module Federation ทำอะไรตอนโหลดเพื่อรักษาความปลอดภัยตรงนั้น และมีต้นทุนอะไรเทียบกับ single-build monolith?
registerRemotes()ตอน runtime ให้คุณทำอะไรได้ที่การตั้งentryในvite.config.tsของ shell ทำไม่ได้?
คุณ version remote แล้ว roll out จริง: remoteEntry.js ใหม่ที่ publish ที่ URL คงที่, ถูก shell หยิบไปใช้ตอนโหลดครั้งถัดไป, พร้อม manifest ที่ stamp ไว้เพื่อพิสูจน์ว่า build ไหน live — และ shell กับ cart ไม่เคย rebuild นั่นคือผลตอบแทน “ship ทีมเดียว” ที่พิสูจน์ตั้งแต่ต้นจนจบ พร้อมรายละเอียดเรื่อง cache และ shared-version ที่รักษาความปลอดภัย
ตอนนี้คุณ build Mosaic จาก repo เปล่ามาจนถึง slice ที่ deploy อิสระ, version อิสระ, compose ตอน runtime แล้ว ถึงเวลาถอยออกมามองแล้วเรียกชื่อสิ่งที่คุณได้เรียนรู้: Wrap-up →