There are three sources of tokens, two ownerships of windows, and the same piece of code has to run in three forms – and the 7 pitfalls it brings.
Background: A “Sanqi” sub-application
We maintain an enterprise-level management system with a technology stack of Vue 3 + Vite + TypeScript. It is not an ordinary SPA;Run in three modes at the same timeThe wujie sub-application:
| Running state | who is running | window.$wujie |
Where does token come from | Cross-application communication |
|---|---|---|---|---|
| ① wujie is embedded into the main system | The sandbox in the host 9444 page | have | wujie props injection | wujie bus + postMessage |
| ② Independent access | It is the top page | undefined | sessionStorage (SSO process writing) | none |
| ③ Independent iframe embedding | The host uses an ordinary iframe to directly connect to a certain route | undefined | URL query carries | postMessage (with host) |
Form ③ is a special requirement of the workflow module: the host should embed the “process form page”, “report page” and “stub association view page” into its own page as components without going through Wujie.
All complexity comes from this matrix – the same piece of code has three paths for authentication, three sets of communication channels, and two “windows”.
There’s another fact that makes troubleshooting less effective:During local development, the sub-application runs 5174 and the host runs 9444, cross-origin; after deployment, the same origin. This means:
- half the pitAppears only in development environment(Cross-source interception type), “it happens to be fine” in production;
- The other half’s pitAppears only in production(There is no Service Worker in the HTTP environment), and the development environment can never be reproduced.
If it cannot be reproduced in dev ≠ there is no bug; if it is reproduced in dev ≠ there will be it in production. Each cross-application repair must be tested in “three forms × two environments”.
Below are 7 real-life accidents. Each has: phenomenon → root cause → code to be fixed → lessons learned.
Incident 1: “event validation failed” appears every time I click
Phenomenon
After connecting to wujie, the console was flooded with the same line of warnings during local joint debugging – every time the user clicks any button, one message is displayed:
[Vue warn]: Invalid event arguments: event validation failed for event "click".
The function is completely normal, but the alarm buries all the truly valuable logs.
Root cause: wujie’s sandbox model and cross-realm instanceof
wujie’s sandbox structure = iframe (JS runtime environment) + container (DOM rendering environment): The JavaScript of the sub-application runs in a hidden iframe in the host page, but the rendered DOM is hung in the host document.
This means that each realm has its own set of native constructors. Generated by real user clicks MouseEvent The object is inThe realm of the main application Created in; and the el-button of element-plus in the sub-application performs emits verification on the click event (simplified):
emits:
const token = new URLSearchParams(window.location.search).get('token')
in the validator MouseEvent fromThe realm of the sub-application iframe. Across realms instanceof Always false——So every time there is a real click, the verification “fails” once.
Fix: Directional muting
First confirm the damage surface: only an alarm will be issued if the verification fails, and the event will be passed to the handler as usual, with no loss of functions and pure dev noise. So we didn’t magically modify element-plus, but app.config.warnHandler Make sure you mute this one and let everything else go:
// src/main.ts
app.config.warnHandler = (msg, _instance, trace) =>
lesson
In the micro front-end sandbox, all type judgments that rely on the “same realm native constructor” are not trustworthy. Not just element-plus – any third-party library does it internally instanceof Event / instanceof HTMLElement Such inspections may overturn under Wujie. The troubleshooting idea is to translate the error-reporting library code into a question: “Is the constructor it gets here and the constructor that generates this object in the same realm?”
Incident 2: postMessage failed – targetOrigin filled in “self”
Phenomenon
When the token expires, the sub-application must notify the main system to log in again in the 401 interceptor. window.top broadcast toQuit. During local joint debugging, the main system never receives the message, and there is only one line of inconspicuous error message on the console:
Failed to execute 'postMessage' on 'DOMWindow':
The target origin provided (' does not match
the recipient window's origin ('
Root cause: targetOrigin checks the “receiver”
postMessage The second parameter of targetOriginthe verification isreceive windowThe origin is not the sender’s. Sub-applications naturally pass window.location.origin(own 5174), while the receiver window.top Is the host (9444) → does not match → browser interception.
Some people will say: Then pass it on '*' chant. It works, but it is equivalent to throwing away the only built-in security design of postMessage – any page embedded in you can receive this message with authentication semantics.
Fix: Derivation of origin by scenario
// src/utils/common-utils.ts
export function getTopWindowOrigin(): string {
if (window.top !== window.self && document.referrer)
return window.location.origin // 独立打开时没有宿主,回退自身
}
The two usage scenarios have completely different semantics, so do not mix them:
// 场景一:主动发往顶层宿主 —— 用「推导出的宿主 origin」
window.top?.postMessage?.({ action: 'toQuit', errMsg: '登录已过期' }, getTopWindowOrigin())
// 场景二:回复消息 —— 用「发送方自报的 origin」
window.addEventListener('message', (event) => {
// ...处理 event.data
event.source?.postMessage({ action: 'outSubmitFunBack', result }, event.origin)
})
lesson
postMessage has two sets of origin semantics:Actively send to top, use from document.referrer Deduced host origin; reply to message, use event.origin(The other party reports his/her own address, which is the most credible). Consolidate it into a tool function instead of handwriting it scattered everywhere – we have counted that there are more than ten sites that send messages to top throughout the project. If you write it loosely, someone will definitely write it wrong.
Incident 3: Ghost 401 – The URL carrying the token page is missing registration
Phenomenon
The pile change order associated view opened by the “View” button somewhere in the main system, full page 401. There is no stack pointing to the root cause, and the interface itself, token, and permissions are all checked, and there is no problem. Moreover, it does not necessarily appear every time – it only hangs when entering from that entrance.
Root cause: an implicit agreement that was not registered
The batch of page tokens in form ③ can only be passed from URL query.main.ts according to path keywordRecognize and read:
// src/main.ts(节选)
} else if (
window.location.pathname.indexOf('flow-frame-page') >= 0 ||
window.location.pathname.indexOf('report-frame-page') >= 0 ||
window.location.pathname.indexOf('marker-relate-info') >= 0 ||
window.location.pathname.indexOf('pipeline-related-page') >= 0
) {
const token = new URLSearchParams(window.location.search).get('token') || ''
window.sessionStorage.setItem('token', token)
syncSwToken(token) // 同步给 Service Worker,见事故 4
}
The new page is not registered here → There is no token in sessionStorage. There is a historical legacy in the request interceptorKeep quiet:
// src/utils/request.ts —— 问题代码
const token = `Bearer ${tokenString || '<硬编码的历史 token>'}`
If there is no token, just use a hard-coded old token on top → the request will not report “missing token”, but will be sent to the backend with expired credentials → the whole page will be 401, and the root cause is wrapped in one layer.
repair
- Register the path keyword of the new page;
- Write the “consequences of non-registration” in the branch comments (we wrote the 401 root cause at that time directly into the comments, and later people will know why this exists);
- Clean up the separate items of hard-coded tokens.
lesson
“Implicit agreements” must have a registration list, and the list must state the consequences of violation. A more general one: Silent cover-up turns “configuration missing” into “deeper runtime failure” – the cost of covering up the cause of failure is far greater than the savings of explicit error reporting. It is better to console.warn the line “Token is missing, embedded from URL but not registered” when starting, than to let it run with malfunction.
Incident 4: Service Worker exploded as soon as it was registered
Phenomenon
in order to solve / / CSS background This kind of “native request where JS cannot be injected into the request header” cannot pass the authentication problem. We put the authentication logic into the Service Worker (the network layer intercepts file requests and injects Authorization——This mechanism itself is enough to write another article). Register script written index.html Finally, as soon as it is embedded into the main system, it throws:
SecurityError: Failed to register a ServiceWorker:
The origin of the provided scriptURL ('
does not match the current origin ('.
Root cause: SW is an origin-level asset
Wujie suddenly uses iframe/srcdoc embedded in the host page. Adjust in iframe navigator.serviceWorker.register('/sub-app/sw.js')the origin of the relative path is resolved toThe origin of the top-level document (host)and SW registration requires that the script has the same origin as the current document → must explode.
Fix: Only register in top-level windows
Key insights:Once SW is successfully registered at the top level, it will apply to the entire origin.——After the same origin is deployed, the native requests in the sub-iframe are still intercepted by it. Child applications do not need to (and cannot) register themselves.
By the way, let’s mention the other half: there is no localStorage in the SW environment (synchronous storage does not belong to the SW scope), and token is used IndexedDB Make the transfer of main thread ↔ SW; The default is no-cors request, the browser will strip off the custom request header, which needs to be used after SW interception mode: 'cors' Restart,Authorization Only the head can be carried up.
lesson
When encountering all browser capabilities that are “demarcated by origin” (Service Worker, Cookie, Storage, postMessage) under the micro front end, let me ask first:Which origin does this ability belong to? Who is eligible to register/read and write? The answer is often not the same application as “who is writing this code”.
Incident 5: Clicking once triggers N times – ghost monitoring on the bus
Phenomenon
The list interface in the to-do task pop-up window was called twice; the same problem of “event triggered once and callback executed multiple times” appeared intermittently on multiple pages.
Root cause: bus is a global singleton, which outlives components.
Wujie’s bus life cycle is bound to application-level instances.After the component is destroyed, the monitor hung on the bus still survives.. only in onMounted inside $on and never $offevery time you re-enter the component, a layer of monitoring is superimposed – the event comes once, and the callback runs N times.
Fix: Register/unregister pairing and subscribe as early as possible
// setup 顶层:尽早订阅——主系统可能在子应用 ready 之前就发事件,晚订阅会漏
window.$wujie?.bus.$on('goPage', goPage)
window.$wujie?.bus.$on('open-task', todoTask.open)
onUnmounted(() => {
// 注销,避免重复绑定与内存泄漏
window.$wujie?.bus.$off('goPage', goPage)
window.$wujie?.bus.$off('open-task', todoTask.open)
})
A detail that is easy to ignore:$off Must be passedsame function referenceso the handler needs to use a named function, written in $on Positional anonymous arrow functions can never be turned off.
lesson
Put the “bus event” $on/$off “Pairing” is written into the team’s fixed check items (code review checklist/AI coding standards). After we solidified it into the error case library, such problems will disappear——For frequently made mistakes, rely on lists rather than memory.
Accident 6: White screen when leaving the host – $wujie null pointer
Phenomenon
Access the bus/props page and open it separately (form ②) directly to a white screen:
Cannot read properties of undefined (reading 'bus')
root cause
wujie is only injected in the embedded state window.$wujie. But the developer’s muscle memory is “$wujie must be there” – after all, daily joint debugging is always in the form. When accessed independently it is undefinedthe first .bus Just explode.
Repair: Optional chain across the board, complete even if the host is absent
window.$wujie?.bus?.$on('goPage', goPage)
const token = window.$wujie?.props?.outLinkToken
Subscriptions are silently skipped when accessed independently – there is no host, and there are no events to listen to, which is the expected behavior; the token goes to the sessionStorage/URL branch. The three forms share the same code and rely on one design principle:Each cross-application API allows absence.
lesson
The first design principle of micro front terminal application:The application must remain intact in the absence of the host. All cross-application capabilities such as tokens, events, and navigation are designed to be “optional injection + local protection” rather than “necessary existence.” This is why the typical counterexample in the error case library is naked writing window.$wujie.bus。
Incident 7: The window has changed but you don’t know it – resize fails in the sub-application
Phenomenon
6 large data screens are produced according to the 1920×1080 design draft, using transform: scale Proportional stretch fit. After being embedded in the main system, when the browser window is stretched, the large screen will not move at all; when opened alone, everything is normal.
Root cause: “Window” has two attributes
The large screen runs in the iframe of the sub-application, and when the host window is resized,The window of the sub-application may not receive the resize event. What is needed to calculate the scaling ratio is exactly the size of the host viewport.
Fix: Host broadcast, sub-application recalculation
The main system broadcasts through the wujie bus when the window is resized. resize-message6 large screen pages subscribe and recalculate:
// 子应用大屏页(节选简化)
window.$wujie?.bus.$on('resize-message', handleResize)
onUnmounted(() => window.$wujie?.bus.$off('resize-message', handleResize))
function scaleDivToFit(el: HTMLElement | null, tw = 1920, th = 1080) {
if (!el) return
// 注意:取的是宿主(parent)的视口尺寸,不是子应用自己的 window
const wr = window?.parent?.innerWidth / tw
const hr = window?.parent?.innerHeight / th
el.style.transform = `scale(${wr},${hr})`
el.style.transformOrigin = 'top left'
el.style.width = `${tw}px`
el.style.height = `${th}px`
el.style.position = 'absolute'
}
lesson
In the micro front end, “window”, “viewport” and “scroll bar” have two attributes.When it comes to the synchronization of the host UI state, don’t monitor it yourself, let the host broadcast and sub-applications subscribe. Our wujie event table has a special type of “main → child” downstream broadcast events (resize-message, goPage, open-task), which serve this type of scenario.
Written at the end: 10 sub-application catch-all rules
The 7 accidents were condensed into 10 catch-all rules, which were posted in the warehouse’s coding specifications:
- The application must be complete in the absence of the host:all
window.$wujiethe whole process?.the type declaration is optional. - bus events appear in pairs:setup top level as early as possible
$on,onUnmounted$off; Handler uses named functions. - postMessage distinguishes two origins:Send to top for
getTopWindowOrigin()(referrer derivation), used for replyingevent.origin,prohibit'*'。 - Do not trust cross-realm instanceof: When a third-party library does type verification and alarms, think about the sandbox first and then the bug.
- To add a new independent iframe embedded page, the token branch must be registeredannotate the consequences of not registering.
- The ability to draw boundaries by origin first asks “who does it belong to?”: SW is only registered in the top-level window. Registering once will take effect in all origins.
- Don’t be silent: If the token/configuration is missing, you must explicitly fail. Running with the disease will turn the configuration problem into a ghost fault.
- Host UI status is broadcast by the host: resize, scroll, close pop-up window, sub-application only subscribes but does not detect.
- Confirm the environment first before drawing conclusions:dev cross-origin, production same origin; HTTP production without SW. The repair needs to cover “three forms × two environments”.
- Cross-application interactions must register event tables: Direction, event name, monitoring location, purpose, if one is missing, it will not be allowed to go online.
The last one is actually the most important one: the root cause of most of the pitfalls of micro-frontends is “the agreement exists, but no one knows it exists.”Turn conventions into tables in documents, comments in code, and check items in checklistsmore reliable than any individual’s memory.
*This article is based on the real production problems of an enterprise-level management terminal (Vue 3 + Vite + TypeScript + wujie-vue3). The codes are all excerpted from the online warehouse (desensitized).