Migrating a Product Management Backend to Micro‑Frontend with micro‑app: Lessons Learned
The article details the product‑center's shift from an iframe‑based UI to a micro‑frontend architecture, exposing the style‑isolation, sandbox, multi‑instance, routing and Sentry issues encountered with qiankun, and explains how micro‑app was adopted with concrete configuration, custom fetch, domain handling, and routing strategies to achieve a flexible, maintainable solution.
Project Background
The Zhuanzhuan product management center performs a large volume of create and edit operations, making operation efficiency the primary metric. Users requested features such as resetting a secondary page to the menu’s default view and tab‑page navigation similar to other internal systems, which were difficult to implement with an iframe solution.
Technical Selection
Because the backend projects use the umi framework, the team initially considered qiankun for micro‑frontend integration. Although qiankun’s documentation describes a simple integration, numerous unresolved problems forced the team to abandon it and adopt micro‑app , a lightweight WebComponent‑based micro‑frontend framework.
Qiankun Pitfalls
Style isolation – Different antd versions in the base and sub‑applications caused menu and navigation style anomalies. The default sandbox ( sandbox:true) isolates styles only for single‑instance scenarios, leaving base‑to‑sub‑application and multi‑instance style interference.
Enabling strictStyleIsolation:true uses shadow DOM to isolate styles, but in React this disables event callbacks, breaking functionality—a limitation noted in qiankun’s official docs.
Enabling experimentalStyleIsolation:true rewrites sub‑application CSS selectors to limit their scope. This avoids event loss, but elements mounted on body (e.g., pop‑ups) still lose styles. The issue can be mitigated by configuring antd ’s ConfigProvider to change the popup container.
<ConfigProvider getPopupContainer={(triggerNode) => {
if (window.__POWERED_BY_QIANKUN__) {
return getMicroAppContainer();
}
return document.body;
}}>
<App />
</ConfigProvider>Sentry cleanup – Sentry instances are not destroyed when a qiankun sub‑application is unmounted, leading to massive console warnings and a frozen console. Reported issues in the qiankun repository have no effective solution yet.
Multi‑instance problems – Implementing tab‑page functionality with qiankun’s multi‑instance mode caused unexpected interactions: closing one instance could remove another, and reopening an instance sometimes resulted in a blank page and console errors. Manual unmounting or downgrading qiankun versions did not resolve the issues.
Switching to micro‑app
micro‑app is a WebComponent‑based micro‑frontend framework from JD.com that works with any framework and offers a simple iframe‑like integration.
<micro-app name='my-app' url='http://localhost:3000/'></micro-app>Advantages
Compatible with all frameworks.
One‑line component renders a micro‑frontend.
Provides JavaScript sandbox, style isolation, element isolation, route isolation, pre‑loading, and data communication.
Initial Configuration
micro‑app fetches sub‑application assets without cookies by default. Two preparatory steps are required:
Customize fetch and set credentials to include cookies.
Configure CORS on sub‑applications: set Access-Control-Allow-Origin to the specific domain (not *) and enable Access-Control-Allow-Credentials: true.
Dynamic Routing and MyMicroApp Component
The system hosts multiple projects under a single menu, so a routing scheme like #/0/1/AppC/route1 was designed, where the first number denotes the top‑level menu, the second the sub‑menu, and AppC the target application.
Path parsing (splitPath)
const splitPath = (path) => {
if (!path) return { indexPath: "", appName: "", appUrl: "" };
const regex = /^(\/([^\/]+)?)?(\/[^\/]+)?(\/.*)?$/;
const match = path.match(regex);
if (match) {
const indexPath = match[1] ? match[1].replace(/\/$/, "") : "";
const appName = match[2] || "";
const appUrl = match[3] || "";
return { indexPath, appName, appUrl };
}
return { indexPath: "", appName: "", appUrl: "" };
};micro‑app Props name: unique identifier, composed of appName + tabKey (pathname + search). data: global data such as user info, permissions, and helper methods. url: points to the sub‑application’s index.html; the base menu is hidden. defaultPage: the page to display, derived from splitPath. router-mode: pure or state for this scenario.
<micro-app
router-mode="state"
name={`${appName.slice(1)}-${tabKey}`}
data={globalState}
url={replaceUrl(microAppMap[appName.slice(1)]) + "?hideSidebar=true"}
defaultPage={`#${appUrl}`}
/>Router‑mode Decision
Four modes were evaluated:
search : appends routing info to query string – deemed untidy.
native : shares browser routing – conflicts with multi‑tab requirements.
natve‑scope : similar to native – also unsuitable.
pure and state : isolate sub‑application routing from the base, matching the multi‑tab micro‑frontend needs.
In both pure and state modes, sub‑applications do not affect the base router, preventing address‑bar conflicts.
Sub‑application Navigation Refactor
In iframe mode each sub‑application could manipulate history.pushState/replaceState or its own router without affecting others. Under micro‑frontend, this leads to URL mismatches and 404 errors because the base application must orchestrate navigation.
Solution: expose a custom history from the base, wrap push and replace to prepend the index path and app name, and provide it to sub‑applications.
const genPath = (oriTo) => {
if (typeof oriTo === "string") {
return `${indexPath}${appName}${oriTo}`;
}
return { ...oriTo, pathname: `${indexPath}${appName}${oriTo.pathname}` };
};
const customHistory = {
...umiHistory,
push: (to, state) => umiHistory.push(genPath(to), state),
replace: (to, state) => umiHistory.replace(genPath(to), state),
};
const globalState = useRef({ history: customHistory, microAppMap, userInfo, permissionList });Sub‑applications retrieve this history via window.microApp.getData() and replace their own history methods accordingly.
export async function getInitialState() {
const data = window?.microApp?.getData();
if (data?.history) {
const { push, replace } = data.history;
history.push = push;
history.replace = replace;
}
if (data?.permissionList?.length) {
const { userInfo, permissionList } = data;
return { userInfo, permissionList, settings: defaultSettings };
}
// non‑micro‑frontend fallback
}Wildcard Domain Support
To simplify access to development, test, and production environments, a wildcard domain system replaces the host part of sub‑application URLs based on a curTag derived from the parent domain.
const replaceUrl = (url) => {
if (!curTag) return url;
const host = url.split("/")[2];
const temp = host.split('.')[0];
const newHost = host.replace(temp, `${temp}-${curTag}.test`);
return url.replace(host, newHost);
};Future Optimizations
Pre‑loading – analyze user behavior to preload likely sub‑application pages, reducing white‑screen time.
UMD mode – expose mount and unmount methods so that only these run on subsequent renders, improving performance and memory usage.
Document the migration process to replicate the micro‑frontend architecture in other internal systems facing comparable challenges.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
