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

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 พร้อม requiredVersion Module Federation เจรจา version ตอนโหลด: ถ้า catalog ใหม่ต้องการ React ที่ shell ให้ไม่ได้ runtime จะ warn แทนที่จะ render กับ React สองตัวเงียบ ๆ การ version code ของ remote เอง นั้นฟรี; การเปลี่ยน shared major คือกรณีที่ต้องประสานงาน
  • A manifest for provenance. เปิด manifest: true จะ emit mf-manifest.json ไว้ข้าง ๆ remoteEntry.js และ additionalData hook 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.js cache ที่ config ผิดคือ bug คลาสสิก “ทำไม version เก่ายัง live อยู่”

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

บน 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, immutable

เปลี่ยนแค่ catalog, build ด้วย build id สด, deploy asset ของ catalog ไม่มีอะไรของ shell หรือ cart ถูกแตะ

Terminal window
# 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 assets
pnpm dlx wrangler pages deploy apps/catalog/dist --project-name mosaic-catalog
# ✔ https://catalog.mosaic.example — new remoteEntry.js is live

ถ้าคุณอยากเลือก 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

Terminal window
# The live entry is served fresh (not cached), so a new deploy is seen promptly
curl -sI https://catalog.mosaic.example/remoteEntry.js | grep -i 'cache-control'
# cache-control: no-cache
# The manifest reports the build that's actually live
curl -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 เก่าอยู่:

  1. จำหน้าตาปัจจุบันของ catalog ใน browser ที่ URL ของ shell ไว้
  2. deploy การเปลี่ยนเฉพาะ catalog ที่มองเห็นได้ (ตามขั้นตอนด้านบน) shell ไม่ ถูก rebuild หรือ redeploy
  3. hard-reload shell catalog ตัวใหม่โผล่ขึ้นมา — เพราะ shell re-fetch remoteEntry.js แล้วได้ manifest ใหม่ ค่า buildId ใน mf-manifest.json ตรงกับ GIT_SHA ล่าสุดของคุณ ยืนยันได้ว่า version ไหนที่ live อยู่พอดี
Terminal window
# Confirm the shell was untouched: its last deploy predates the catalog change
pnpm 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 กัน ทั้งหมดมีอยู่เพื่อทำให้เรื่องนี้ปลอดภัย

ตรวจสอบความเข้าใจ:

  1. เมื่อ catalog team deploy build ใหม่ shell ต้อง rebuild อะไร — และทำไมคำตอบถึงเป็น “ไม่มีอะไรเลย”?
  2. ทำไม remoteEntry.js ต้องมี header no-cache ขณะที่ hashed /assets/* chunk cache ได้เป็นปี?
  3. การ version code ของ remote เอง นั้นฟรี แต่การเปลี่ยน major ของ shared dependency ไม่ฟรี Module Federation ทำอะไรตอนโหลดเพื่อรักษาความปลอดภัยตรงนั้น และมีต้นทุนอะไรเทียบกับ single-build monolith?
  4. 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 →