Tinapos ng post 15 ang seksyong “Ngayon, sirain natin nang tatlong beses” sa sarili nitong limitasyon: “Wala sa mga ito ang nagbibigay sa binary ng ibang lugar na pagkukunan: ipinapakita ng patay na tab ang failure sa halip na itago ito, pero hindi iyon gumaganang app.” Ibinibigay ng post na ito sa binary ang ibang lugar na iyon. Bawat release build ay may dalang kopya na ng bawat remote, at tumatakbo ang app mula sa mga kopyang iyon kapag hindi maabot ang content delivery network (CDN), o kapag hindi ma-load ang isang remote habang patuloy na nagla-load ang isa mula rito.
Sinabi ng post 1 kung sino ang gagawa niyon: “Walang ibinibigay ang Module Federation sa panig ng offline: ang paglalagay ng kopya ng bawat remote sa loob ng binary, para gumana ang na-review na app nang mag-isa at walang network, ay isang arkitekturang ikaw ang gagawa.” Itinakda rin nito ang requirement para sa remote na hindi nagla-load: “kailangang mag-degrade ang app sa isang bagay na ligtas sa halip na magpakita ng blangkong screen.” Natutugunan ng build ng post 15 ang requirement para sa remote na hindi nagla-load, at doon ito humihinto. Kapag hindi maabot ang CDN, nagla-launch ito sa unresolved, na may error state sa bawat tab. Kapag pumalya ang isang remote, ipinapakita ng tab nito ang error state, at hindi kailanman naibabalik ng Try again ang tab.
Binubuo ng unang kalahati ang kopya: isang build step na naghahanda nito, isang phase sa bawat platform na naglalagay nito sa binary, at isang bagong launch mode, bundled, na nagpapatakbo ng bawat remote mula rito. Binubuo ng ikalawang kalahati ang dalawang net para sa isang CDN launch. Sinasalo ng isa ang manifest na hindi kayang i-serve ng CDN at inililipat ang isang remote na iyon sa kopya nito; nakapalibot ang isa sa bawat tab at sinasalo ang load na pumapalya nang mas huli. Kasama ng ikalawang net ang Try again na talagang nagre-retry.
Isang rule ang humuhubog sa kopya: ito ang version na alam nang tumatakbo sa binary na ito, na-freeze sa araw na na-build ang binary. Ito ang guaranteed minimum: dumarating pa rin ang mga bagong version sa mga naka-install na app sa pamamagitan ng CDN, at ginagamit lang ng host ang kopya kapag hindi makapaghatid ang CDN ng version na nagla-load.
Magsimula mula sa huling estado ng post 15, ang tag na post-15-cdn-flip; nagtatapos ang post na ito sa post-16-fallbacks. Gaya ng dati, ikaw ang nagta-type ng mga configuration change at ng maliliit na edit, at kinokopya mo ang iba mula sa end tag: ang build tool, ang code at mga test ng host, ang mga native file, at isang test ng design system.
Isang beses lang pinagpapasyahan ng launch ang mode, bago mag-load ang anumang federated. Ang paggamit sa mga version ng CDN ay nangangahulugang dumating ang isang magagamit na map at nairehistro ang mga version nito; kapag hindi magamit ang mga iyon, ang mga kopya ang inirerehistro ng launch. Nagtatapos lang ito sa unresolved kapag pumalya rin iyon. Kumikilos ang dalawang net sa loob ng isang CDN launch, isang remote sa bawat pagkakataon.
Maglagay ng kopya ng bawat remote sa binary
Pinipili ang kopya ng parehong map na pumipili ng mga version mula sa CDN. Ang version na dala ng binary ay ang tinutukoy ng map ng sarili nitong app version, dahil iyon ang version na alam na tumatakbo sa binary na iyon: para sa 2.0.0 build dito, list 1.2.0 at party 1.0.0.
Nasa kopya ang mga naka-sign na bundle ng version na iyon at ang manifest nito, byte-for-byte gaya ng pagse-serve ng CDN sa mga ito. Hindi binabago ang mga byte dahil vine-verify pa rin ang bawat bundle laban sa signing key kapag nagla-load ito, at pumapalya sa check ang anumang pagbabago sa code nito.
Bini-build na ng tools/build-cdn.mjs ang bawat version sa cdn-root/, at inihahanda na rin nito ngayon ang mga kopya. I-fetch ang end tag nang isang beses, dahil mula rito kumukuha ng mga file ang bawat copy command sa post na ito, at ang tool ang kunin mo muna:
npx degit@3.8.0 --force warrendeleon/react-native-module-federation#post-16-fallbacks /tmp/pokedex-ref-16
cp /tmp/pokedex-ref-16/tools/build-cdn.mjs tools/
Tumatakbo ang bagong stage pagkatapos ma-build ang CDN tree. Para sa bawat platform, kinukuha nito ang bawat version na tinutukoy ng map at kinokopya ang mga bundle at manifest nito sa embed-root/<platform>/<remote>/<version>/:
for (const [remote, version] of Object.entries(embeddedVersions)) {
const published = join(cdnDir, remote, version);
const embedded = join(embedDir, remote, version);
mkdirSync(embedded, { recursive: true });
// Nasa top level ng directory nito ang bawat script ng version, kasama ang manifest nito, at ang
// kopya ay lahat ng iyon, byte-for-byte: ang container, ang mga chunk nito, ang index.bundle, na
// tinutukoy ng manifest bilang isa sa mga asset ng mga shared module, at ang mf-manifest.json, na
// binabasa ng federation runtime mula sa disk gaya ng pagbabasa nito mula sa CDN. Ang assets/
// folder sa tabi ng mga ito ay may mga imaheng dala na ng mga shared copy ng host ng mga library
// na iyon.
for (const file of readdirSync(published)) {
if (file.endsWith('.bundle') || file === 'mf-manifest.json') {
copyFileSync(join(published, file), join(embedded, file));
}
}
bundledVersions[platform][remote] = version;
Pinapanatili ng mga kopya ang mga directory na <remote>/<version>/ sa halip na magbahagi ng iisang folder, dahil puwedeng mag-ship ang dalawang remote ng mga vendor chunk na magkapareho ang file name. Sa iisang folder, mao-overwrite ng pangalawa ang una.
Itinatala rin ng build tool ang mga kinopyang version sa apps/host/src/shell/embedded-versions.ts, isang generated file na kino-compile ng host, para alam ng host kung aling mga kopya ang dala nito bago ito magbasa ng anuman mula sa disk.
Build output ang embed-root/, gaya ng cdn-root/, kaya idagdag ito sa .gitignore:
# The copies build-cdn stages for the native embed phases.
embed-root/
Patakbuhin muna ang key generator, pagkatapos ang tool para sa dalawang platform. Pinapanatili ng generator ang anumang key na nasa checkout na, at ginagawa ang mga ito sa isang bagong checkout ng tag, na wala pa nito dahil hindi kailanman pumapasok sa Git ang mga iyon. Pagkatapos ng CDN tree, iniuulat ng tool ang mga kopya at ang generated file:
node tools/gen-signing-keys.mjs && node tools/build-cdn.mjs
embedded -> embed-root/ios/listApp/1.2.0
embedded -> embed-root/ios/partyApp/1.0.0
embedded -> embed-root/android/listApp/1.2.0
embedded -> embed-root/android/partyApp/1.0.0
wrote -> apps/host/src/shell/embedded-versions.ts
Itinatala ng generated file ang version ng bawat kopya, sa bawat platform:
export const BUNDLED_VERSIONS: Record<string, Record<string, string>> = {
ios: { listApp: '1.2.0', partyApp: '1.0.0' },
android: { listApp: '1.2.0', partyApp: '1.0.0' },
};
Inihahanda ng build tool ang kopya para sa app version na nasa MF_APP_VERSION, o para sa pinakabagong app version na may map kapag hindi naka-set iyon, kaya ang build para sa ibang app version ay nagpapatakbo muna ng tool gamit ang parehong variable.
iOS: ang huling build phase
Sa iOS, pumapasok ang kopya sa loob ng .app, sa tabi ng main.jsbundle. Isang script ang gumagawa ng pagkopya, at isang Run Script phase sa dulo ng Host target ang nagpapatakbo nito. Kopyahin ang script mula sa end tag:
mkdir -p apps/host/scripts && cp /tmp/pokedex-ref-16/apps/host/scripts/embed-remotes-ios.sh apps/host/scripts/
#!/usr/bin/env bash
# --- Ang kopya sa binary, ang kalahati ng iOS. Kinokopya ng huling Run Script phase ng Host target
# ang embedded version ng bawat remote mula sa embed-root/ios papunta sa app, sa
# cdn/ios/<remote>/<version>/ sa tabi ng main.jsbundle, kung saan binabasa ito ng host sa pamamagitan
# ng mga absolute na file:// URL: ang mga naka-sign na bundle at ang mf-manifest.json ng version.
#
# Pinapanatili ang mga directory na <remote>/<version>/ sa halip na i-flatten: puwedeng mag-ship
# ang dalawang remote ng mga vendor chunk na magkapareho ang file name, at sa flat na kopya ay
# mao-overwrite ng isa ang isa pa.
#
# Tumatakbo ito sa bawat configuration. Nagla-load ang Debug build ng sariling bundle ng host sa
# http, kaya hindi nito mababasa ang kopya, at doon ang tanging gastos ng kopya ay ang oras na
# inaabot nito. Mula sa mga dev server ang mga remote nito kapag walang naka-configure na CDN, o mula
# sa CDN kapag mayroon.
#
# Kapag wala ang embed-root, inaalis nito ang anumang kopyang naiwan ng naunang build, nagpi-print
# ng warning at nag-e-exit nang 0, at nagtatagumpay ang build nang walang naka-embed. Iyon ang
# patibong: patakbuhin ang `node tools/build-cdn.mjs` bago ang release build. ---
set -euo pipefail
REPO_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)
SOURCE="$REPO_DIR/embed-root/ios"
: "${CONFIGURATION_BUILD_DIR:?run this from an Xcode build phase}"
: "${UNLOCALIZED_RESOURCES_FOLDER_PATH:?run this from an Xcode build phase}"
DEST="$CONFIGURATION_BUILD_DIR/$UNLOCALIZED_RESOURCES_FOLDER_PATH/cdn/ios"
if [ ! -d "$SOURCE" ]; then
rm -rf "$DEST"
rmdir "$(dirname "$DEST")" 2>/dev/null || true
echo "warning: $SOURCE does not exist, so no remotes are embedded. Run 'node tools/build-cdn.mjs' before a release build."
exit 0
fi
mkdir -p "$DEST"
# Pinapanatili ng --delete ang app sa eksaktong laman ng embed-root, kaya hindi nananatili ang
# version na in-embed ng naunang build. Kinokopya ang mga byte nang walang pagbabago: sakop ng
# signature ng bawat bundle ang mga iyon.
rsync -a --delete --include='*/' --include='*.bundle' --include='mf-manifest.json' --exclude='*' "$SOURCE/" "$DEST/"
echo "embedded $(find "$DEST" -name '*.bundle' | wc -l | tr -d ' ') remote bundles and $(find "$DEST" -name 'mf-manifest.json' | wc -l | tr -d ' ') manifests into $DEST"
Isang pagbabago sa project file ang phase. Sa Xcode, isa itong bagong Run Script phase sa Host target, na may pangalang “Embed federated remotes (offline fallback)”, na nagpapatakbo ng "$SRCROOT/../scripts/embed-remotes-ios.sh", nakalagay pagkatapos ng lahat ng ibang phase, na naka-uncheck ang “Based on dependency analysis”. Nasa huli ito para naka-assemble na ang .app na sinusulatan nito. Kapag naka-uncheck ang box, tumatakbo ito sa bawat build, na ginagawa naman ng phase na walang idinedeklarang output: nilalaktawan lang ng Xcode ang isang script phase kapag may mga output itong puwedeng i-check. Ang alternatibo ay ideklara bilang output ang mga kinopyang file sa ilalim ng cdn/ios ng .app, na ang mga file ng embed-root/ ang mga input. Dala ng project file ng end tag ang phase na naka-uncheck ang box, kaya ang pagkopya rito ay gumagawa ng parehong pagbabago:
cp /tmp/pokedex-ref-16/apps/host/ios/Host.xcodeproj/project.pbxproj apps/host/ios/Host.xcodeproj/
Android: isang Gradle task at isang native module
Sa Android, pumapasok ang kopya sa mga asset ng APK, ang Android package na pinagmumulan ng install ng app. Sa dulo ng apps/host/android/app/build.gradle, pagkatapos ng mga pagbabago ng post 15, idagdag:
// --- Ang kopya sa binary, ang kalahati ng Android. Inihahanda ng build-cdn ang embedded version ng
// bawat remote sa ilalim ng embed-root/android, ang mga naka-sign na bundle at ang mf-manifest.json
// ng version, at mini-mirror ng task na ito ang tree na iyon sa mga asset ng APK sa cdn/android,
// kung saan kinokopya ito palabas ng EmbeddedRemotesModule sa launch. Sync sa halip na Copy, para
// ang file na in-embed ng naunang build at hindi ng build na ito ay maalis sa halip na mai-ship.
//
// Kapag wala ang embed-root, wala itong ini-embed, nagpi-print ng warning ang warnIfNothingToEmbed,
// at nagtatagumpay pa rin ang build. Iyon ang patibong: isang matagumpay na release build na walang
// dalang kopya. Patakbuhin ang build-cdn bago ang release build. ---
def embedSource = file("$rootDir/../../../embed-root/android")
def embeddedAssetsDir = layout.buildDirectory.dir("generated/embedded-remotes").get().asFile
def embedRemotes = tasks.register('embedRemotes', Sync) {
from(embedSource) { include '**/*.bundle', '**/mf-manifest.json' }
into(file("$embeddedAssetsDir/cdn/android"))
}
// --- Nilalaktawan ng Sync na walang source ang mga action nito, kaya hindi kailanman magpi-print
// ang warning sa loob nito. Nililinis pa rin ng Gradle ang in-embed ng task noon kapag nawala ang
// source nito, kaya walang stale na kopyang mai-ship. Walang input o output ang task na ito, kaya
// tumatakbo ito sa bawat build, at nagpi-print ito ng warning kapag wala ang embed-root/android. ---
def warnIfNothingToEmbed = tasks.register('warnIfNothingToEmbed') {
doLast {
if (!embedSource.exists()) {
logger.warn("warning: ${embedSource} does not exist, so no remotes are embedded. Run 'node tools/build-cdn.mjs' before a release build.")
}
}
}
android {
sourceSets {
main {
assets.srcDirs += embeddedAssetsDir
}
}
}
tasks.named('preBuild').configure { dependsOn embedRemotes, warnIfNothingToEmbed }
Hindi file sa disk ang isang asset, at naaabot lang ng Re.Pack ang directory na <remote>/<version>/ ng isang kopya sa pamamagitan ng totoong file path. Kaya isang maliit na TurboModule ang kumokopya ng assets/cdn palabas sa files directory ng app at ang directory na ginamit nito ang resolved value ng promise nito. Sapat na maikli ang spec nito para basahin nang buo:
import type { TurboModule } from 'react-native';
import { TurboModuleRegistry } from 'react-native';
// --- Ang kalahati ng Android ng kopya sa binary. Isang Gradle task ang naglalagay ng embedded
// version ng bawat remote sa mga asset ng APK, at hindi file sa disk ang mga asset: kailangan ng
// file:// loader ng Re.Pack ng totoong path. Kinokopya sila palabas ng prepare() sa files directory
// ng app nang isang beses sa bawat naka-install na build, pinapanatili ang layout na
// <remote>/<version>/, at ang resolved value ng promise nito ay ang directory na pinagbabasehan ng
// host ng mga file:// URL.
//
// Walang implementation para sa iOS, dahil binabasa ng iOS ang mga kopya nang direkta mula sa
// .app. Kaya TurboModuleRegistry.get ang ginagamit nito, na nagbabalik ng null kung saan wala ang
// module, sa halip na getEnforcing, na magti-throw sa iOS sa sandaling mag-load ang file na ito. ---
export interface Spec extends TurboModule {
/** Kopyahin palabas ang mga embedded remote, isang beses sa bawat naka-install na build, at ibalik
* ang directory. */
prepare(appVersion: string): Promise<string>;
}
export default TurboModuleRegistry.get<Spec>('EmbeddedRemotesModule');
Kinokopya ng Kotlin side ang tree nang isang beses sa bawat install. May marker file na naglalaman ng app version at ng oras na na-install o na-update ang APK, kaya ginagamit ulit ng relaunch ang na-extract na tree, at pinapalitan ito ng anumang bagong install, kasama na ang APK na ni-rebuild pero pinanatili ang version number nito. Kinokopya ng Kotlin side ang mga byte nang walang pagbabago, dahil sakop sila ng signature ng bawat bundle. Kunin mula sa end tag ang module, ang spec nito, at ang na-update na HostNativePackage, na nagrerehistro sa module na iyon sa tabi ng navigation module ng post 13:
cp /tmp/pokedex-ref-16/apps/host/specs/NativeEmbeddedRemotesModule.ts apps/host/specs/
cp /tmp/pokedex-ref-16/apps/host/android/app/src/main/java/com/host/{EmbeddedRemotesModule,HostNativePackage}.kt apps/host/android/app/src/main/java/com/host/
| iOS | Android | |
|---|---|---|
| Saan pumupunta ang kopya | ang .app, sa cdn/ios/<remote>/<version>/ | ang mga asset ng APK, sa cdn/android/<remote>/<version>/, kinokopya palabas sa files directory sa launch |
| Ano ang naglalagay nito roon | ang huling Run Script phase, embed-remotes-ios.sh | ang Sync task na embedRemotes, bago ang preBuild |
| Paano ito nahahanap ng host | ang directory ng main.jsbundle, binasa mula sa SourceCode module ng React Native | ang directory na resolved value ng promise ng EmbeddedRemotesModule.prepare() |
Kapag walang embed-root/ | isang warning, isang matagumpay na build, walang kopya | isang warning, isang matagumpay na build, walang kopya |
Mag-load ng kopya mula sa disk
Nagbabago ang host sa limang file at sa mga test ng mga ito. Kunin ang mga iyon mula sa end tag. Tinatanggal ang mf-modules.d.ts, dahil wala nang nagla-load ng remote gamit ang import() (sinasabi ng seksyon ng Try again kung bakit), at nagkakaroon ang design system ng contrast check para sa bagong kulay ng banner:
cp /tmp/pokedex-ref-16/apps/host/src/shell/{remoteLocator,scriptManager,federationErrors}.ts /tmp/pokedex-ref-16/apps/host/src/shell/FederationBanner.tsx apps/host/src/shell/
cp /tmp/pokedex-ref-16/apps/host/App.tsx /tmp/pokedex-ref-16/apps/host/jest.config.js apps/host/
cp /tmp/pokedex-ref-16/apps/host/__mocks__/{module-federation-runtime,partyApp-partySlice}.js apps/host/__mocks__/
cp /tmp/pokedex-ref-16/apps/host/__tests__/{remoteLocator,scriptManager}.test.ts /tmp/pokedex-ref-16/apps/host/__tests__/{App,RemoteBoundary,FederationBanner}.test.tsx apps/host/__tests__/
cp /tmp/pokedex-ref-16/packages/ui/src/tokens/__tests__/contrast.accessibility.ts packages/ui/src/tokens/__tests__/
rm apps/host/mf-modules.d.ts
Kailangang malaman muna ng host kung nasaan ang mga kopya sa device. Sa iOS, galing ang sagot sa sariling SourceCode module ng React Native: nagla-load ang Release build ng file:///…/Host.app/main.jsbundle, at ang directory sa itaas ng file na iyon ang .app. Nagla-load ang development build ng bundle nito mula sa dev server sa http, kaya walang directory na makukuha at walang kopyang magagamit. Sa iOS, Release build ang kailangan ng lahat ng tumatakbo mula sa kopya. Sa Android, ang directory ay ang resolved value ng prepare():
const sourceCode = NativeModules.SourceCode as
| { scriptURL?: string; getConstants?: () => { scriptURL?: string } }
| undefined;
const SCRIPT_URL = sourceCode?.scriptURL ?? sourceCode?.getConstants?.().scriptURL;
const APP_PATH = SCRIPT_URL?.startsWith('file://')
? SCRIPT_URL.replace(/^file:\/\//, '').replace(/\/[^/]+$/, '')
: undefined;
let embeddedRoot: string | undefined = Platform.OS === 'ios' ? APP_PATH : undefined;
Dumadaan pa rin sa resolver ng post 15 ang bawat script ng isang remote. Nagkakaroon ang resolver ng isang branch: ang remote na tumatakbo mula sa kopya nito, sa launch na hindi nakagamit ng mga version ng CDN, o pagkatapos pumalya ang isang remote na iyon sa pag-load mula sa CDN, ay nagre-resolve sa isang file sa loob ng kopya nito:
return {
kind: 'locate',
locator: {
url: `file://${input.embeddedRoot}/cdn/${input.platform}/${remoteName}/${version}/${filename}`,
cache: true,
absolute: true,
verifyScriptSignature: input.verify,
},
};
Ang absolute: true ang detalyeng madaling makaligtaan. Kung wala ito, ang file name lang ng script ang pinapanatili ng Re.Pack at hinahanap ito sa pinakaitaas na level ng app: tinatanong ng iOS ang mga resource ng app bundle sa pamamagitan ng URLForResource:withExtension:, at nagbubukas ang Android ng asset na may ganoong pangalan. Hindi umaabot ang alinman sa dalawa sa directory na <remote>/<version>/. Kapag mayroon ito, binabasa ng Re.Pack ang file sa path na ibinigay.
Nananatiling naka-on ang verification. Pinapatakbo ng file loader ng Re.Pack sa dalawang platform ang parehong signature check ng download path nito, bago tumakbo ang code, sa bawat load. Dahil dito mas mahigpit ang kopya kaysa sa naka-cache na download, na sinabi ng post 15 na pinapatakbo mula sa disk nang walang pangalawang check. Ito rin ang dahilan kung bakit hindi kailanman nire-rewrite ang kopya habang inilalagay ito sa app: baguhin mo ang isang byte ng code ng naka-install na kopya at tatanggihan ito ng tab, dahil pumapalya ito sa parehong hash check gaya ng pinakialamang download. Sa iOS, iniuulat ng file loader ang buong description ng error, kaya kasunod ng generic na prefix ng system ang dahilan:
[Error: The operation couldn’t be completed. The bundle verification failed because the bundle hash is invalid.]
Sa Android, ganoon din ang mensahe gaya ng sa pinakialamang download, na walang prefix.
Hindi nangangailangan ng espesyal na pag-handle ang manifest. Dala ng kopya ang mf-manifest.json sa tabi ng mga bundle nito, at binabasa ito ng federation runtime mula sa file:// URL gaya ng pagbabasa nito ng isa mula sa CDN, dahil nagbubukas ng mga local file ang fetch ng React Native: sa pamamagitan ng file request handler nito sa iOS, at sa Android sa pamamagitan ng handler ng blob module, na tumatanggap ng anumang URL na hindi http o https kapag blob ang response type, ang type na hinihingi ng fetch ng React Native.
Bundled mode: ang araw na hindi maabot ang CDN
Inihahanda na ngayon ng launch ang mga kopya kasabay ng probe, dahil hindi nangangailangan ang isa ng isa pa, at ang launch na walang magagamit na map ay tumatakbo mula sa mga iyon sa halip na sumuko:
const [versions] = await Promise.all([
CDN_CONFIGURED ? fetchVersionMap() : Promise.resolve(null),
prepareEmbeddedCopies(),
]);
if (!versions) {
return runFromCopies(CDN_CONFIGURED ? 'no usable version map' : 'no CDN configured');
}
// --- Ang launch na hindi nakagamit ng CDN: walang magagamit na map, walang naka-configure na CDN, o
// pumalya ang registration. Kapag may mga kopya sa binary, tumatakbo ito mula sa mga iyon, na
// nakaturo ang bawat remote sa manifest ng kopya nito, na binabasa ng runtime mula sa disk.
// Kapag wala, o kapag pumalya rin ang pagrerehistro sa mga iyon, walang mapapatakbo. ---
function runFromCopies(reason: string): FederationStatus {
const embedded = REMOTE_NAMES.filter(hasEmbeddedCopy);
if (embedded.length === 0) {
setStatus({ mode: 'unresolved', source: reason, versions: {}, embedded: [] });
return status;
}
const versions = Object.fromEntries(embedded.map(name => [name, bundledVersions[name]]));
setStatus({ mode: 'bundled', source: 'the copy in the binary', versions, embedded });
try {
registerRemotes(
embedded.map(name => ({ name, entry: embeddedManifestUrlFor(name) })),
{ force: true },
);
} catch (error) {
console.warn('[federation] the copies could not be registered', error);
setStatus({ mode: 'unresolved', source: 'remotes could not be registered', versions: {}, embedded: [] });
}
return status;
}
Ang pagrerehistro ng bawat remote na nakaturo sa manifest ng kopya nito, gamit ang force ng post 15, ay nagpapakuha sa runtime mula sa disk ng lahat ng tungkol sa remote na iyon. Mula roon, wala nang espesyal: kinukuha ng runtime ang manifest, nire-resolve ng resolver ang bawat script sa kopya, at vine-verify ang bawat isa habang nagla-load. Nananatili ang unresolved para sa launch kung saan walang naitala ang binary na kopyang may directory na mapagla-load-an, o kung saan pumalya ang pagrerehistro sa mga kopya.
Subukan ito sa pamamagitan ng pag-alis ng CDN. Simulan ang server gaya ng ginawa ng post 15, pagkatapos ay gawin ang Release build:
npx http-server@14.1.1 cdn-root -p 8000 -c-1 --cors
( cd apps/host && MF_CDN_BASE=http://localhost:8000 MF_APP_VERSION=2.0.0 npm run ios -- --mode Release )
cdn · listApp 1.2.0 · partyApp 1.0.0 ang nakasulat sa banner. Ihinto ang server at i-check na wala na ang map:
curl -s -o /dev/null http://localhost:8000/ios/maps/2.0.0/version-map.json; echo $?
Ibig sabihin ng 7 ay hindi nakakonekta ang curl. Isara ang app at buksan ulit. Pumapalya ang probe sa loob ng 1.5 segundo nito, nagiging purple ang banner at nagiging bundled · listApp 1.2.0 · partyApp 1.0.0 ang nakasulat dito, at bumubukas ang dalawang tab mula sa mga kopya, nang walang hinihingi sa network maliban sa PokéAPI para sa mga Pokémon at GitHub para sa artwork ng mga ito.
Naka-freeze ang kopya sa build time, at iyon ang trade-off. Ang binary na na-build ngayon at ila-launch nang walang CDN sa susunod na taon ay magpapatakbo ng mga version ng ngayong araw, anuman ang na-ship ng CDN mula noon. Iyon ang bahagi ng requirement ng post 1 na kayang tugunan ng kopya: tumatakbo ang na-review na code nang walang CDN. Galing pa rin sa PokéAPI ang mga Pokémon, kaya kapag walang network, pumapalya ang request ng list at ipinapakita ng screen ang sarili nitong error, “Couldn’t reach PokéAPI.”
Wala ring natatandaan tungkol sa failure. Nagtatanong ulit sa CDN ang susunod na launch, at kapag dumating ang magagamit na map at nairehistro ang mga version nito, bumabalik ang app sa mga version na tinutukoy ng map.
Isang remote na pumapalya sa CDN launch
Sakop ng bundled mode ang launch na hindi nakagamit ng mga version ng CDN. Mas makitid ang isa pang failure: ginagamit ng launch ang mga version ng CDN, at pagkatapos ay pumapalya ang isang remote. Puwedeng ang dahilan ay version na inalis sa CDN habang tinutukoy pa rin ito ng map, chunk na naputol sa kalagitnaan, o release na nag-throw habang nag-i-initialise ang module nito, na siyang ikatlong pagsira ng post 15. Sa build ng post 15, nag-iiwan ng isang patay na tab ang bawat isa sa mga iyon. Sa mga failure na iyon, pinapatakbo ng dalawang net ang apektadong tab mula sa kopya ng remote nito, habang nananatili sa CDN ang isa pang remote.
Ang manifest net
Fine-fetch ng Module Federation ang mf-manifest.json ng isang remote bago mag-load ang anumang code nito. Malalaman ng boundary ng tab na pumalya ang fetch na iyon, pero pagkatapos lang sumuko ang sariling fetch ng runtime, at walang inilalagay na time limit ang runtime dito. At hindi lahat ng load ay tumatakbo sa loob ng boundary: humihingi ng manifest ang state at styles module ng party sa boot, mula sa isang effect sa labas ng bawat tab. Kaya nasa lugar kung saan humihingi ang runtime ng bawat manifest ang unang net: ang fetch hook ng isang runtime plugin, na tinatawag ng runtime bago ang sarili nitong fetch, at puwedeng sumagot ng Response o ibalik ang request sa runtime sa pamamagitan ng pagbabalik ng wala:
const embeddedFallback: ModuleFederationRuntimePlugin = {
name: 'embedded-fallback',
fetch(url: string) {
if (status.mode !== 'cdn') {
return undefined;
}
const remote = manifestRemote(url, REMOTE_NAMES);
if (!remote || !hasEmbeddedCopy(remote)) {
return undefined;
}
if (fallbackRemotes.has(remote)) {
return fetch(embeddedManifestUrlFor(remote));
}
return cdnManifestOrEmbedded(url, remote);
},
};
registerPlugins([embeddedFallback]);
Fine-fetch mula sa CDN ang manifest ng isang CDN remote, sa loob ng 1.5-segundong limit ng probe. Kapag pumalya ang request (error status gaya ng 404, timeout, o naputol na koneksyon), inililipat ng hook ang remote na iyon sa kopya nito at ibinabalik ang manifest ng kopya sa halip:
async function cdnManifestOrEmbedded(url: string, remote: string): Promise<Response> {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), PROBE_TIMEOUT_MS);
try {
const response = await fetch(url, { signal: controller.signal });
if (response.ok) {
return response;
}
console.warn(`[federation] ${remote} manifest returned ${response.status}`);
} catch (error) {
console.warn(`[federation] ${remote} manifest could not be fetched`, error);
} finally {
clearTimeout(timer);
}
fallBack(remote);
return fetch(embeddedManifestUrlFor(remote));
}
Ibinabalik sa runtime nang walang pagbabago ang manifest na dumating na may success status, kahit hindi ito mabasa. Pumapalya ang runtime roon pagkatapos, at para sa load ng isang tab, sinasalo ng boundary net ang failure.
Idinaragdag ng fallBack ang remote sa isang set at inilalagay ang remote sa banner, bilang listApp 1.2.0 embedded. Binabasa ng resolver ang parehong set para sa bawat script, kaya ang susunod na script ng remote ay nagre-resolve na sa kopya nito, habang pinapanatili ng bawat ibang remote ang CDN URL nito. Sinadyang nasa memory lang ang set: lumilipat ang remote sa kopya nito hanggang sa susunod na launch lang, na nagtatanong ulit sa CDN, kaya hindi pinapanatili ng failure na network lang ang dahilan ang remote sa kopya nito. Ang pagtatanda ng mga failure sa pagitan ng mga launch, at ang permanenteng pag-rollback ng masamang version, ay sa post 17.
May mga ibang tool na nag-aalok na ng mga bahagi nito, at gagana ang bawat isa sa sarili nitong layer. Puwedeng magbalik ng fallback ang errorLoadRemote hook ng Module Federation kapag pumalya ang load, kasama ang manifest. Nagre-retry ng pumalyang load ang opisyal nitong retry plugin at puwedeng magpalit-palit sa mga backup domain, at tumatanggap ng retry at retryDelay ang script locator ng Re.Pack. Ang fetch hook ang angkop dito sa dalawang dahilan. Nalalaman lang ng errorLoadRemote ang failure pagkatapos sumuko ang fetch ng runtime, gaya ng boundary, samantalang ang fetch hook mismo ang gumagawa ng request at puwedeng maglagay dito ng time limit ng probe. At ibang policy ang pag-retry: gumugugol ito ng oras sa pagtatanong ulit sa parehong CDN, samantalang sumasagot agad ang net na ito mula sa kopyang nasa binary na at iniiwan ang susunod na pagsubok sa susunod na launch. Puwedeng ilagay ang mga retry sa harap ng net, kapalit ng mas mahabang paghihintay bago ang kopya.
Subukan ang net gamit ang version na wala na sa CDN. Simulan ulit ang server, ilipat ang version ng list palabas nito, at i-cold-start ang app:
mv cdn-root/ios/listApp/1.2.0 /tmp/listApp-1.2.0
Bumabalik na 404 ang manifest ng list, tumatakbo ang list mula sa kopya nito, at nagla-load pa rin ang party mula sa CDN, gaya ng ipinapakita ng log ng server:
[2026-09-29T14:40:27.102Z] "GET /ios/listApp/1.2.0/mf-manifest.json" "Host/1 CFNetwork/3860.500.112 Darwin/27.0.0"
[2026-09-29T14:40:27.103Z] "GET /ios/listApp/1.2.0/mf-manifest.json" Error (404): "Not found"
[2026-09-29T14:40:27.120Z] "GET /ios/partyApp/1.0.0/mf-manifest.json" "Host/1 CFNetwork/3860.500.112 Darwin/27.0.0"
[2026-09-29T14:40:27.159Z] "GET /ios/partyApp/1.0.0/partyApp.container.js.bundle" "Host/1 CFNetwork/3860.500.112 Darwin/27.0.0"
Ibalik ang directory bago magpatuloy:
mv /tmp/listApp-1.2.0 cdn-root/ios/listApp/1.2.0
Kapag lumipat sa kopya nito ang bawat remote, i-check kung naaabot ng app ang server. Ang banner na nagmamarka sa dalawang remote bilang embedded, o nagsasabing
bundled, habang tumatakbo ang server ay karaniwang nangangahulugang hindi ito naabot ng app: tinatanggihan ng App Transport Security ng iOS at ng mga cleartext rule ng Android ang plain http kung hindi ito pinapayagan ng build. Pinapayagan ng host na ito ang local networking sa iOS at, sa Android, cleartext papunta lang salocalhost,127.0.0.1at10.0.2.2, kasama ang mga subdomain ng mga ito.
Ang boundary net
May mga failure na dumarating pagkatapos ng manifest na maayos na dumating: container o chunk na hindi dumarating o hindi nave-verify, o module na nag-throw habang ini-evaluate ito. Dahil sa mga iyon, pumapalya ang load ng tab, at tumatakbo ang load ng tab sa loob ng RemoteBoundary na inilagay ng post 11 sa paligid ng bawat tab. Ang boundary ang ikalawang net, at nananatili itong class component, dahil wala pang paraan sa React para isulat ang error boundary bilang function component. Nagtatanong na ito ngayon ng isang bagay bago ipakita ang error state: pumalya ba ang load, kaya hindi kailanman nakagawa ng component ang remote, at may kopya bang hindi pa nililipatan ng remote na ito?
componentDidCatch(error: unknown) {
console.warn(`${this.props.remote} failed`, error);
if (this.dropsToCopy()) {
fallBackAndReload(this.props.remote);
this.nextAttempt();
}
}
// Isang failure na kayang sagutin ng kopya sa binary: pumalya ang load, kaya hindi kailanman
// nakagawa ng component ang remote, at may kopya ang remote na ito na hindi pa nito nililipatan.
dropsToCopy() {
return !this.loaded() && canFallBack(this.props.remote);
}
Kapag kaya nito, inililipat ng boundary ang remote sa kopya nito at nilo-load ulit ang tab, nagre-render ng loading state habang naghihintay, kaya hindi kailanman lumalabas ang error state. Pinapayagan ito ng canFallBack nang isang beses sa bawat remote, sa CDN launch lang, at para lang sa remote na may kopya. Kapag hindi nito kaya, dahil pumalya rin ang kopya, o wala nito, ipinapakita ng tab ang error state gaya ng dati.
Kapag nagbalik na ng component ang load ng isang remote, nananatili ang remote sa error state nito kapag nag-throw ang component na iyon. Isa iyong desisyon, at nakasalalay ito sa kung nakagawa ng component ang remote. Bago iyon mangyari, ipinapakita pa rin ng tab ang loading state nito, kaya hindi nakikita ang paglipat sa kopya. Kapag nagbalik na ng component ang load, pinapanatili ng boundary ang version na iyon, kahit mag-throw ang component sa pinakaunang render nito. Itinatala ng boundary ang attempt na nagbalik ng component ang load nito, hindi kung naipakita ba kailanman ang screen, kaya ang failure sa attempt na iyon ay binibilang bilang in-throw ng sariling code ng remote, na tumakbo na sa session na ito. Hanggang hindi tumutukoy ng ibang version ang map, ang kopya ay ang parehong version din naman. Kaya ipinapakita ng boundary ang “This tab stopped working” at nag-aalok ng Try again, na nagre-render ulit ng remote at nakakabawi mula sa error na hindi umuulit. Ang error na umuulit ay nag-iiwan sa tab sa error state nito sa natitirang bahagi ng session, at pagkatapos ng relaunch hangga’t tinutukoy ng map ang version na iyon, kahit may gumaganang kopya sa binary.
Hindi sakop ng boundary net ang dalawang boot module ng party: nagla-load ang mga iyon mula sa effect sa labas ng bawat tab, kaya nilo-log lang ng host ang failure na hindi sinasagot ng manifest net. Kapag pumalya ang state module, nananatiling disabled ang pagdaragdag sa party hanggang mag-load ang Party tab, dahil ang tab na iyon mismo ang nag-i-inject ng state; hindi naaapektuhan ng pumalyang styles module ang pagdaragdag.
Ang ikatlong pagsira ng post 15 ay isa nang pumalyang load na kayang sagutin ng kopya. Nag-throw ang release nito habang nag-i-initialise ang module nito, at ginagawa ng evaluation window ng post 15 ang throw na iyon na pumalyang load sa halip na fatal report. I-build ulit ito. Idagdag ang parehong linya sa ibaba ng mga import ng apps/list/src/PokedexScreen.tsx:
throw new Error('PokedexScreen failed to initialise');
Pagkatapos, i-ship ito bilang 1.4.0, gaya ng ginawa ng post 15:
( cd apps/list && MF_REMOTE_VERSION=1.4.0 npm run bundle:ios:prod ) && rsync -a --exclude '*.map' --exclude mf-stats.json apps/list/cdn/ios/listApp/1.4.0/ cdn-root/ios/listApp/1.4.0/
Burahin ang linya, ituro ang 2.0.0 map sa 1.4.0, at i-relaunch. Lumilipat ang tab sa 1.2.0 na kopya: 1.2.0 ang sinasabi ng chip, minamarkahan ng banner ang list bilang embedded, at walang RCTFatal sa log ng simulator. Ibalik ang map sa 1.2.0.
Sinasalo rin ng boundary net ang CDN na nawawala sa kalagitnaan ng session. Mag-launch habang tumatakbo ang server at huwag munang buksan ang Party tab. Magdagdag ng Pokémon mula sa Pokédex, ihinto ang server, pagkatapos ay buksan ang Party sa unang pagkakataon. Ang state at styles module lang ng party ang na-load sa boot, kaya wala pa sa device ang mga screen nito: pumapalya ang download, at nagpapakita ang tab ng spinner nang saglit at pagkatapos ay ang party mula sa kopya nito, kasama ang Pokémon na idinagdag mo at ang banner na nagsasabing partyApp 1.0.0 embedded. Gumagana rin ang pag-alis ng Pokémon doon, dahil iisa ang store ng host na ginagamit ng mga screen ng kopya at ng state module na na-load mula sa CDN.
Isang Try again na nagre-retry
Sa build ng post 15, ang Try again sa tab na pumalya ang load nito ay hindi kailanman nagbabalik ng tab. Tatlong record ng pumalyang load ang nakaharang sa pagitan ng Try again at ng panibagong attempt.
Ang una ay sa React. Isang beses tinatawag ng lazy component ang load function nito: sabi ng React documentation, “Both the returned Promise and the Promise’s resolved value will be cached, so React will not call load more than once”, at napupunta ang rejection sa pinakamalapit na error boundary. Na-handle na iyon ng boundary, sa pamamagitan ng pag-mount ng bagong lazy component sa ilalim ng bagong key sa bawat attempt.
Ang pangalawa ay sa host bundle, at ito ang nagpanatiling patay sa mga tab ng post 15. Kino-compile ang import('listApp/ListStack') bilang module ng sariling bundle ng host, at pinapanatili ng bundle ang bawat module na na-evaluate nito sa natitirang bahagi ng session. Pagkatapos ng pumalyang load, pinapanatili nito ang iniwan ng failure, isang module na walang laman, at nagse-settle sa ganoon ang bawat susunod na import(), kahit matagumpay na na-download ng retry ang remote. Sinukat sa iOS Release build: nakuha ng retry ang manifest at ang dalawang chunk, at nag-resolve pa rin ang import nang walang component. Kaya hindi na gumagamit ng import() ang mga tab, at ang dalawang module na nilo-load ng party sa boot. Direktang humihingi sila sa federation runtime, sa bawat pagkakataon:
export async function loadRemoteModule<T>(id: string): Promise<T | undefined> {
const factory = await loadRemote<() => T>(id, { loadFactory: false, from: 'runtime' });
return factory ? factory() : undefined;
}
Hinihingi ng function na ito ang factory ng module sa halip na ang mga export nito, gamit ang loadFactory: false, na siyang paraan para mabalot ng evaluation window ng post 15 ang factory: ibinabalik ng runtime ang wrapper ng window, at kapag tinawag ang wrapper na iyon dito, nae-evaluate ang module sa loob ng window.
Ang pangatlo ay sa federation runtime. Sa @module-federation/runtime-core 2.9.0, ang version na ini-install ng series na ito, nananatili ang record ng runtime tungkol sa isang remote kahit pumalya ang load: pinapanatili ng runtime ang entry ng remote, ang nabasang manifest, ang na-load na container, at ang pumalyang container load na ibinibigay ulit ng runtime sa bawat susunod na request. Ang chunk registry ng remote, isang global array na ipinangalan sa uniqueName ng remote, ay nagpapanatili rin ng bawat chunk na na-fetch ng huling container nito, at ini-install ng bagong container ang lahat ng iyon bago mag-fetch ng anuman. Nililinis ng Try again ang record ng runtime at ang chunk registry bago mag-load ulit:
function reloadRemote(remote: RemoteName, entry: string): void {
try {
registerRemotes([{ name: remote, entry }], { force: true });
} catch (error) {
console.warn(`[federation] ${remote} could not be registered again`, error);
}
delete (globalThis as Record<string, unknown>)[CHUNK_REGISTRY[remote]];
}
Inaalis ng muling pagrerehistro ng remote gamit ang force ang record ng runtime, kasama ang global ng container, at itinuturo ang susunod na load sa entry. Binabasa ng Try again ang entry mula sa runtime sa halip na buuin ito, na gumagana sa bawat mode, kasama ang development, kung saan ang build lang ang nakakaalam ng URL ng dev server. Ang remote na lumipat sa kopya nito ay nagre-retry pa rin ng kopya nito, dahil itinuturo ng manifest net at ng resolver sa kopya nito ang anumang remote na nasa set.
Hindi kailangang linisin ang script cache ng Re.Pack para sa pumalyang load: kapag nag-settle na ang load ng na-resolve na script, ibinabasura ng Re.Pack ang promise nito, at hindi kailanman isinusulat sa cache nito ang download na pumapalya sa verification, sa alinmang platform. Ang refusal mula sa resolver ang exception. Dumarating ito bago ang cleanup na iyon, kaya pinapanatili ng Re.Pack ang rejection at ibinibigay ito sa bawat susunod na request para sa parehong script. Tinatanggihan lang ng resolver ng host na ito ang remote kung walang version o kopya ang host para sa remote na iyon.
May isang gap na nananatiling bukas: hindi kayang kanselahin ng Try again ang download na tumatakbo na. Ang chunk na fine-fetch pa ng pumalyang container ay napupunta sa alinmang registry na umiiral kapag dumating ito, at kapag nagla-load pa ang isang script, sinasagot ng Re.Pack ang bagong request para rito gamit ang promise na nakabinbin na. Kapag ang kopya ay ang version na sine-serve ng CDN, ang default, pareho rin ang mga byte sa alinmang paraan; kapag tumutukoy ng ibang version ang map, puwedeng mapunta sa tabi ng container ng kopya ang chunk mula sa CDN na dumating nang huli.
Ipinapakita ng render error ang Try again na nagbabalik ng tab na na-load na ang code nito. Inihinto ng mid-session demo ng boundary net ang server, kaya simulan ulit ito at hayaang tumakbo:
npx http-server@14.1.1 cdn-root -p 8000 -c-1 --cors
Pagkatapos, gawing mag-throw ang screen ng list habang nagre-render, sa unang walong segundo nito. Sa ibaba ng mga import ng apps/list/src/PokedexScreen.tsx idagdag:
const LOADED_AT = Date.now();
at bilang mga unang linya ng PokedexScreen():
if (Date.now() - LOADED_AT < 8000) {
throw new Error('PokedexScreen render failed');
}
Kailangang magtagal ang throw: nagre-retry ang React ng render na pumapalya, kaya kapag masyadong maagang tumigil ang error, nakakabawi ang tab bago pa maipakita ang error state ng boundary. I-ship ito bilang sarili nitong version, gaya ng pag-ship ng 1.4.0, pagkatapos ay burahin ang mga linya at ituro ang 2.0.0 map sa 1.5.0:
( cd apps/list && MF_REMOTE_VERSION=1.5.0 npm run bundle:ios:prod ) && rsync -a --exclude '*.map' --exclude mf-stats.json apps/list/cdn/ios/listApp/1.5.0/ cdn-root/ios/listApp/1.5.0/
I-relaunch at ipinapakita ng tab ang “This tab stopped working”. Hintaying lumipas ang walong segundo at pindutin ang Try again: nagre-render ang list sa 1.5.0, at walang bagong request na ipinapakita ang log ng server, dahil na-load na ang code at kailangan lang itong i-render ulit. Ibalik ang map sa 1.2.0 pagkatapos.
Magkakatabi ang mga failure at ang kapalit ng bawat isa:
| Failure | Sinasalo ng | Ang nakikita ng user |
|---|---|---|
| Hindi maabot ang CDN sa launch | ang launch, sa bundled mode | bawat tab mula sa kopya nito, at purple na banner |
| Pumapalya ang manifest ng isang remote: 404, timeout, naputol na koneksyon | ang manifest net | ang tab mula sa kopya nito, minarkahang embedded sa banner |
| Ang remote code ng isang tab ay hindi dumarating, hindi nave-verify, o nag-throw habang nag-i-initialise ang module nito | ang boundary net | saglit na loading, pagkatapos ang tab mula sa kopya nito |
| Pumapalya ang state module ng party sa boot, pagkatapos dumating ang manifest nito | wala: nilo-log ito ng host | nananatiling disabled ang pagdaragdag sa party hanggang mag-load ang Party tab |
| Pumapalya ang styles module ng party sa boot, pagkatapos dumating ang manifest nito | wala: nilo-log ito ng host | gumagana pa rin ang pagdaragdag |
| Pumapalya rin ang kopya, o walang kopya | ang boundary | ”This tab could not load”, na may Try again |
| Nag-throw ang component ng remote habang nagre-render | ang boundary | ”This tab stopped working”, na may Try again |
Ngayon, sirain natin
Nagtatagumpay ang dalawang embed step nang walang embed-root/, at iyon ang patibong. Ilipat ito palabas at i-rebuild ang Release build, sa pagkakataong ito gamit ang --verbose:
mv embed-root /tmp/embed-root && ( cd apps/host && MF_CDN_BASE=http://localhost:8000 MF_APP_VERSION=2.0.0 npm run ios -- --mode Release --verbose )
Nagtatagumpay ang build, at ang warning ng phase ang tanging senyales na may kulang. Kapag walang --verbose, nagpapakita ang npm run ios ng spinner sa halip na output ng build (o ipinapasa ang output sa xcbeautify o xcpretty, kapag may naka-install), pagkatapos ay nag-uulat ng success Successfully built the app at nagla-launch. Kapag may --verbose, nasa gitna ng mga linya mismo ng build ang warning:
debug warning: …/embed-root/ios does not exist, so no remotes are embedded. Run 'node tools/build-cdn.mjs' before a release build.
debug ** BUILD SUCCEEDED ** [11.716 sec]
success Successfully built the app
Ihinto ang server at i-cold-start ang app. bundled · listApp 1.2.0 · partyApp 1.0.0 pa rin ang nakasulat sa banner, dahil ni-compile ng host ang mga version na sinabing dala nito, at “The app’s own copy of this remote could not be loaded” ang sinasabi ng dalawang tab.
I-build ang CDN tree, pagkatapos ang release build, sa bawat pagkakataon. Kinokopya ng mga embed phase ang anumang laman ng
embed-root/kapag tumakbo sila, at kapag wala ito, hinahayaan nilang magtagumpay ang build na walang iba kundi warning sa output ng build. Ang release build na ginawa bago angnode tools/build-cdn.mjs, o pagkatapos malinis angembed-root/, ay matagumpay na nabi-build nang walang kopya sa loob, at walang pumapalya hanggang kailanganin ng app ang kopya: kapag hindi magamit ng launch ang mga version ng CDN, o kapag pumalya ang isang remote.
Ibalik ito at i-rebuild bago magpatuloy:
mv /tmp/embed-root embed-root && ( cd apps/host && MF_CDN_BASE=http://localhost:8000 MF_APP_VERSION=2.0.0 npm run ios -- --mode Release )
Patakbuhin mo
Ang mga kopya at ang CDN tree, pagkatapos ang server:
node tools/gen-signing-keys.mjs && node tools/build-cdn.mjs
npx http-server@14.1.1 cdn-root -p 8000 -c-1 --cors
Ang Release build sa iOS, pagkatapos ang Android gamit ang address ng emulator para sa machine:
( cd apps/host && MF_CDN_BASE=http://localhost:8000 MF_APP_VERSION=2.0.0 npm run ios -- --mode Release )
( cd apps/host && MF_CDN_BASE=http://10.0.2.2:8000 MF_APP_VERSION=2.0.0 npm run android -- --mode release )
Habang tumatakbo ang alinman sa dalawa, ihinto ang server at mag-cold-start para sa bundled mode, o ilipat ang isang version palabas ng cdn-root para sa manifest net. Sa Android, nasa files directory ng app ang mga na-extract na kopya, sa ilalim ng files/cdn/android/, na may .installed marker sa tabi ng mga ito sa files/cdn/.
Ang mga suite, ang smoke test, at ang mga test ng key generator:
( for d in apps/host apps/list apps/party packages/ui; do ( cd "$d" && npx jest --silent ) || exit 1; done ) && sh scripts/federation-smoke.sh && node --test tools/gen-signing-keys.test.mjs
Ang binuo mo, at ang susunod
Tumakbo sa iOS Release build ang mga demo sa post na ito, at ang mga kopya, ang pag-extract ng mga ito, at ang bundled mode ay sa Android release build din.
Nakakalimutan sa susunod na launch ang remote na lumipat sa kopya nito, kaya ang version na patuloy na pumapalya ay pumapalya ulit sa bawat launch bago pumalit ang kopya nito. Kasama ng isa pang bukas na problema ang pagtatanda ng mga failure: ang map na pinagkakatiwalaan ng bawat binary ay plain text file pa rin na puwedeng baguhin ng sinumang may write access sa bucket.
Susunod: ang pag-sign sa map na iyon, isang counter na nagpapatanggi sa app ng mas lumang map, at isang app na nagro-rollback nang mag-isa ng pumapalyang version.
Mga Sanggunian
- React: lazy — ang load function na isang beses tinatawag, naka-cache ang promise at resolved value nito, at ang rejection na itina-throw sa pinakamalapit na error boundary
- React source: ReactLazy at 19.2.3 — ang rejected load na nakatago sa lazy component at itina-throw ulit sa bawat susunod na render
- React: Component, catching rendering errors with an error boundary — “There is currently no way to write an Error Boundary as a function component”
- Module Federation: runtime hooks — ang
fetchhook para sa mga manifest request, na nagbabalik ngPromise<Response> | void | false, at angerrorLoadRemote - Module Federation source: runtime-core SnapshotHandler at 2.9.0 — ang manifest na hinihingi muna sa
fetchhook, pagkatapos sa global nafetch, pagkatapos saerrorLoadRemotekapag pumalya - Module Federation source: runtime-core remote/index.ts at utils/load.ts at 2.9.0 — ang
registerRemotesna mayforcena tumatawag saremoveRemote, na nagbubura ng entry ng remote, ng manifest, ng container global at ng container-loading promise; ang promise ng pumalyang container load na pinapanatili at ibinabalik sa mga susunod na load hanggang sa oras na iyon - Module Federation: retry plugin — mga retry at backup domain para sa mga pumalyang load
- Re.Pack source: ScriptManager types at 5.2.5 — ang
absolute,retryatretryDelayng locator - Re.Pack source: ScriptManager.ts at 5.2.5 — ang promise ng na-resolve na script na binubura kapag nag-settle na ang load nito, pumalya man o hindi; dumarating ang rejection ng resolver bago ang cleanup na iyon at pinapanatili
- Re.Pack source: ScriptManager.mm at 5.2.5 — ang file:// script na binabasa sa absolute path nito, o sa pangalan sa pamamagitan ng
URLForResource:withExtension:, at vine-verify bago tumakbo; ang download na isinusulat sa cache pagkatapos lang ma-verify - Re.Pack source: FileSystemScriptLoader.kt at RemoteScriptLoader.kt at 5.2.5 — ganoon din sa Android, na binubuksan ang asset sa file name nito kapag hindi absolute ang path, at vine-verify ang download bago isulat
- Apple: URLForResource:withExtension: — ang resource na hinahanap sa pangalan sa bundle, katabi ng variant na tumatanggap ng subdirectory
- swift-build source: ShellScriptTaskProducer.swift at swift-6.2.1-RELEASE — ang Run Script phase na walang idinedeklarang output, o naka-set na laging tumakbo, ay tumatakbo sa bawat build
- React Native CLI source: buildProject.ts at 20.2.0 — kapag walang
--verbose, ang output ng build na ipinapasa saxcbeautifyoxcprettykapag may naka-install, at spinner kung wala - React Native source: RCTSourceCode.mm at 0.85.3 — ang
scriptURL, ang URL na pinag-load-an ng JavaScript bundle - React Native source: RCTFileRequestHandler.mm at RCTAppSetupUtils.mm at 0.85.3 — ang mga file:// request na sinasagot sa iOS, at ang handler na nirerehistro sa networking ng app
- React Native source: BlobModule.kt at 0.85.3 — binabasa sa Android ang anumang URL na hindi http o https kapag
blobang response type - Android: AssetManager — mga asset na binubuksan at nililista mula sa APK, hindi bilang mga file
- Gradle: Sync — isang copy na nag-aalis din ng mga file na wala na sa source
- react-native-module-federation — ang companion repo, ang build sa tag na
post-16-fallbacks