May pangako ang pagtatapos ng post 3: hihinto na ang host sa pagiging isang screen lang at magiging tunay na shell, ang may-ari ng tab bar, at bawat tab ay remote na nilo-load sa runtime. Iyon ang post na ito.
Hanggang ngayon, isang screen lang mula sa isang remote ang nilo-load ng host, fullscreen, na walang ibang mapupuntahan. May frame sa paligid ng mga feature nito ang tunay na app: tab bar, navigation, ang mga bahaging laging nasa screen. Ibinibigay ng post na ito ang trabahong iyon sa host, at ibinibigay ang pangalawang tab sa pangalawang remote.
Ang hatian ng trabaho
Isang pangungusap ang nagbubuod sa buong post: ang host ang may-ari ng frame, at ang mga remote ang may-ari ng mga feature sa loob nito.
Ini-install ng host ang navigation library, ini-mount ang tab bar, at siya ang nagpapasya kung anong mga tab ang meron. Nananatiling payak na screen ang mga remote. Hindi sila nag-i-import ng navigator, hindi nila alam na nasa loob sila ng tab, at hindi nila alam na may pangalawang remote. Dapat nakakabagot ang pagdagdag ng feature: gumawa ka ng isa pang remote, magdagdag ka ng isa pang tab, i-ship mo.
Sa ganitong paraan, puwedeng pag-aari ng dalawang team ang dalawang tab, buuin nang hiwalay, at i-deploy sa sarili nilang takbo, dahil walang isa sa kanila ang kailangang mag-compile laban sa isa pa.
Ang panimulang punto ay ang natapos na estado ng post 3. Kung sumabay kang bumuo, iyon ang code na meron ka na. Kung hindi:
git clone https://github.com/warrendeleon/react-native-module-federation
git checkout post-03-shared-singleton
Pangalawang remote para punan ang pangalawang tab
Hindi tab bar ang isang tab. Kaya nagdadagdag tayo ng pangalawang remote, party, sa parehong paraan na binuo ng post 2 ang list remote: bagong React Native app sa Re.Pack, walang AppRegistry.registerComponent, at isang screen ang inilalantad. Gawin mo ito katabi ng iba, i-install ang mga dependency nito nang eksaktong tulad ng ginawa mo sa list, at kopyahin ang rspack.config.mjs ng list.
Apat na field ang nagbabago, at mahalaga ang lahat: ang name ng plugin (partyApp), ang filename ng container (partyApp.container.js.bundle), ang inilantad na screen (./PartyScreen), at ang output.uniqueName ('PartyApp'). Ang huli ang madaling makalimutan. Ang uniqueName ang nagbibigay-pangalan sa mga chunk-loading global ng webpack, kaya nagbabanggaan sa loob ng runtime ng host ang dalawang remote na may parehong halaga.
Ang screen na inilalantad nito, apps/party/src/PartyScreen.tsx, ay may anim na walang lamang puwang:
import React from 'react';
import { StyleSheet, Text, View } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
const SLOTS = [1, 2, 3, 4, 5, 6];
export default function PartyScreen() {
const insets = useSafeAreaInsets();
return (
<View style={[styles.screen, { paddingTop: insets.top + 24 }]}>
<Text style={styles.title}>Party</Text>
<Text style={styles.subtitle}>
Your party is empty. Add up to 6 Pokémon from the Pokédex.
</Text>
<View style={styles.grid}>
{SLOTS.map(slot => (
<View key={slot} style={styles.slot}>
<Text style={styles.slotNumber}>{slot}</Text>
</View>
))}
</View>
</View>
);
}
const styles = StyleSheet.create({
screen: { flex: 1, padding: 24, backgroundColor: '#fff' },
title: { fontSize: 28, fontWeight: '700' },
subtitle: { fontSize: 14, color: '#6b7280', marginBottom: 24 },
// alignItems: 'flex-start' stops the default stretch from overriding each slot's aspectRatio.
grid: { flexDirection: 'row', flexWrap: 'wrap', gap: 12, alignItems: 'flex-start' },
slot: {
width: '47%',
aspectRatio: 1,
borderRadius: 12,
borderWidth: 1,
borderStyle: 'dashed',
borderColor: '#e5e7eb',
backgroundColor: '#f9fafb',
alignItems: 'center',
justifyContent: 'center',
},
slotNumber: { fontSize: 20, fontWeight: '600', color: '#9ca3af' },
});
Wala pang laman ang mga puwang na iyon, at sinadya iyon. Walang store ang app, kaya walang mapaglagyan ng party ang mga miyembro nito; darating iyon sa mga post tungkol sa state. Ngayon, ini-ship ng team na may-ari ng feature na ito ang pinakamaliit na maipapakita nito sa isang user: screen na may hugis at walang datos sa likod. Feature pa rin na kayang i-load ng shell sa runtime ang isang screen na walang props at walang state.
Binabasa nito ang safe-area inset sa pamamagitan ng useSafeAreaInsets, ang shared singleton mula sa post 3 na gumagawa ng trabaho nito: galing sa provider ng host ang inset, minsanang sinukat, at binabasa ito ng remote na ito nang hindi nagdadala ng sariling kopya ng library.
Bigyan mo ito ng sarili nitong dev-server port para hindi bumangga sa list sa 8082. Sa apps/party/package.json:
"scripts": {
"start:remote": "react-native start --config rspack.config.mjs --port 8083"
}
May dalawa nang remote ngayon, sa 8082 at 8083, bawat isa ay screen na naghihintay ng host.
Tumatanggap ng navigation ang host
Sa host ang tab bar, hindi sa mga remote. I-install ang mga navigation package sa host lamang:
cd apps/host
npm install @react-navigation/native @react-navigation/bottom-tabs react-native-screens
cd apps/host/ios && bundle exec pod install
May dalang native code ang react-native-screens, kaya hindi opsyonal ang pod install na iyon, at hindi rin opsyonal ang bagong native build pagkatapos. Laktawan mo iyon at hihinto ang build sa 'RNSViewInteractionAware.h' file not found, na parang sirang library pero kulang na pod lang talaga. Ito ang unang native dependency na binuhat ng shell mula pa noong post 2, at pasilip sa isang limitasyong babalikan ng serye: kayang dumating sa network ang JavaScript, hindi ang native code.
Ang bahaging federation ay ang kontrata ng post 3 na binasa nang pabaligtad. Nananatiling eksaktong pareho ang mga shared singleton: react, react-native, at react-native-safe-area-context. Hindi shared ang React Navigation at ang react-native-screens, dahil walang remote na nag-i-import ng mga ito.
May maipagtatanggol na dahilan ang pagbabahagi ng lahat bilang default: isang desisyong mas kaunti bawat dependency, at walang tsansang makalimutan ang isang bagay na kailangan mo. Pero ang silbi ng pagbabahagi ay para hindi sabay na ma-load ang dalawang kopya ng isang library sa iisang runtime, at hindi kailanman umaalis sa host ang navigation, kaya walang pangalawang kopyang mabanggaan.
Umiikot ang tuntunin sa dalawang direksyon: ibahagi ang isang library kapag may code sa magkabilang panig na humahawak dito, at doon lang.
Ang shell
Isulat muli ang apps/host/App.tsx. May-ari na ngayon ang host ng isang SafeAreaProvider, isang NavigationContainer, at isang bottom tab navigator, at ang laman ng bawat tab ay remote na nilo-load nang lazy:
import React, { Suspense } from 'react';
import { ActivityIndicator, StyleSheet } from 'react-native';
import { SafeAreaProvider } from 'react-native-safe-area-context';
import { NavigationContainer } from '@react-navigation/native';
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
const PokedexScreen = React.lazy(() => import('listApp/PokedexScreen'));
const PartyScreen = React.lazy(() => import('partyApp/PartyScreen'));
// A remote downloads the first time its tab is opened, so each tab renders behind a Suspense
// spinner. Wrapping once here keeps the lazy boundary out of the remotes.
function withSuspense(Remote: React.ComponentType) {
return function Tab() {
return (
<Suspense fallback={<ActivityIndicator style={styles.loader} size="large" />}>
<Remote />
</Suspense>
);
};
}
const PokedexTab = withSuspense(PokedexScreen);
const PartyTab = withSuspense(PartyScreen);
const Tab = createBottomTabNavigator();
export default function App() {
return (
<SafeAreaProvider>
<NavigationContainer>
<Tab.Navigator screenOptions={{ headerShown: false }}>
<Tab.Screen name="Pokédex" component={PokedexTab} />
<Tab.Screen name="Party" component={PartyTab} />
</Tab.Navigator>
</NavigationContainer>
</SafeAreaProvider>
);
}
const styles = StyleSheet.create({
loader: { flex: 1 },
});
Remote sa likod ng React.lazy at Suspense ang bawat tab, kaya ang tab lang na pinagbubuksan ang kinukuha ng app at iniiwan ang isa hanggang sa pindutin mo.
Kailangang malaman ng host kung saan naroon ang pangalawang remote. Idagdag ito sa remotes sa apps/host/rspack.config.mjs:
remotes: {
listApp: `listApp@http://localhost:8082/${platform}/mf-manifest.json`,
partyApp: `partyApp@http://localhost:8083/${platform}/mf-manifest.json`,
},
At sabihin sa TypeScript ang hugis ng bagong federated import, sa apps/host/mf-modules.d.ts:
declare module 'partyApp/PartyScreen' {
import type React from 'react';
const PartyScreen: React.ComponentType;
export default PartyScreen;
}
Isa pang file, o masisira ang mga test bago pa tumakbo. Nag-i-ship ng ES modules ang React Navigation at ang react-native-screens, at ang mga package lang mismo ng React Native ang pinapadaan sa Babel ng base jest preset: nakalista sa transformIgnorePatterns nito ang react-native, @react-native, at @react-native-community, at wala nang iba. Sa apps/host/jest.config.js:
module.exports = {
preset: '@react-native/jest-preset',
transformIgnorePatterns: [
'node_modules/(?!((jest-)?react-native|@react-native(-community)?|@react-navigation|react-native-screens)/)',
],
};
Patakbuhin mo
Apat na terminal na ngayon, isa bawat remote, isa para sa host, kasama ang build:
cd apps/list && npm run start:remote # :8082
cd apps/party && npm run start:remote # :8083
cd apps/host && npm start # :8081
cd apps/host && npm run ios
Umaarangkada ang host sa Pokédex tab at nagre-render ng list remote. Pindutin ang Party at bantayan ang terminal ng 8083: sa sandaling iyon dumarating ang unang request nito, hindi sa pag-arangkada. Kinukuha ng host ang container, pinapatakbo ito, at lumalabas ang walang lamang grid.
Dalawang feature, binuo at inihain ng dalawang magkahiwalay na app, na nasa iisang tab bar na hindi pag-aari ng alinman sa kanila.
Ngayon, sirain mo
Nagkakasundo ang host at ang remote sa isang pangalang nakasulat sa dalawang magkaibang file, at walang tumitingin kung magkatugma pa sila. Sirain mo ang kasunduan nang sadya. Sa apps/party/rspack.config.mjs, palitan ang pangalan ng container:
new Repack.plugins.ModuleFederationPluginV2({
name: 'partyRemote', // partyApp pa rin ang hinihingi ng host
filename: 'partyApp.container.js.bundle',
I-restart ang dev server ng party. Hindi nagre-reload nang mainit ang pagbabago sa config ng rspack, kaya ang lumang manifest pa rin ang inihahain ng payak na refresh at natatago ang depekto. Pagkatapos ay i-relaunch ang app at pindutin ang Party:
[ Federation Runtime ]: Unhandled error. ([ScriptManager] Failed while resolving script locator:)
while loading "./PartyScreen" from webpack/container/reference/partyApp
args: [ { scriptId: 'partyRemote', caller: undefined } ]
originalError: [Error: No resolver was able to resolve script partyRemote]
Basahin mo ito nang pabaligtad at malinaw ang kadena. Humingi ang host ng partyApp, dahil iyon ang key sa remotes map nito. Kinuha nito ang manifest sa 8083, at nagdeklara ang manifest ng container na partyRemote. Naghanap ang runtime ng script na may ganoong pangalan, at walang partyRemote na nakarehistro sa host, kaya walang maitutugma ang resolver nito. Walang nawawala at walang down. Naisagawa ang pagpapalit ng pangalan sa isa lang sa dalawang file na kailangang magkatugma, hindi sa pareho.
Pansinin kung saan lumilitaw ang depekto. Pulang screen ang lalabas, hindi ang spinner: hindi nahuling error, hindi tab na walang katapusang naghihintay. Hinahawakan ng Suspense ang promise na hindi pa tapos; hindi nito nahuhuli ang isang tinanggihan. Walang nagbabantay para roon sa app na ito, na ayos lang sa laptop na may dalawang dev server at hindi na masyadong ayos kapag galing sa CDN (content delivery network) ang remote sa produksyon. Kailangan ng error boundary para mahuli ito nang maayos, at darating iyon sa susunod na bahagi ng serye.
Ibalik mo ang pangalan, i-restart ang server, pindutin ang Party, at babalik ang grid.
Ang nabuo mo, at ang susunod
Shell na ang host ngayon. Ito ang may-ari ng navigation at ng tab bar; bawat tab ay remote na binuo at na-deploy nang mag-isa at nilo-load sa runtime. Nananatiling simple ang mga remote: nagre-render sila ng screen at walang alam sa kung paano sila nakaayos. Pangatlong remote at pangatlong tab ang isang pangatlong feature, nang walang pagbabago sa mga na-ship na.
Ang tapos na code para sa post na ito ay ang tag na post-04-host-shell, para maikumpara mo sa sarili mo:
git checkout post-04-host-shell
Susunod sa serye: gugustuhin ng dalawang tab na buksan ang parehong detail screen ng Pokémon. Ang isang screen na pinaghahatian ng dalawang team ay isang component, hindi isa pang deployable. Ang unang bagay na dapat pagkasunduan ng dalawang app na hiwalay na binuo ay kung ano ang ipapasa rito. Walang panig na puwedeng mag-angkin ng kasunduang iyon, kaya nagiging package ito: isang kontrata, na inilalathala at ini-install ayon sa bersyon.
Mga Sanggunian
- React Navigation — ang bottom tab navigator na pinagtayuan ng host shell
- Module Federation 2.0 — ang mga
name@urlremote na nag-lo-load ng bawat tab sa runtime - react-native-screens — ang native dependency na pinagtatayuan ng React Navigation
- react-native-module-federation — ang companion repo, sa tag na
post-04-host-shell