
1. 為什么今天還在聊乾坤它不是“過氣技術(shù)”而是被低估的工程基建能力微前端這個詞這兩年被講得太多也太泛。很多人一提微前端腦子里立刻蹦出“拆應(yīng)用”“獨立部署”“技術(shù)棧無關(guān)”這些漂亮話但真到落地時要么卡在路由沖突上動彈不得要么子應(yīng)用加載后樣式全局污染、狀態(tài)互相覆蓋最后發(fā)現(xiàn)理想很豐滿上線后天天救火。我?guī)н^三個中大型項目做微前端改造其中兩個用的是 qiankun一個試過其他方案又切回 qiankun——不是因為它最炫而是它把“能跑通、能維護(hù)、能交接”這三件事真正做成了可量化的工程標(biāo)準(zhǔn)。qiankun 不是萬能膠但它是一把被反復(fù)打磨過的螺絲刀不花哨但擰得緊、不滑絲、換人也能接著擰。它解決的從來不是“要不要微前端”的戰(zhàn)略問題而是“怎么讓十個團(tuán)隊寫的代碼在同一個頁面里互不打架”的戰(zhàn)術(shù)問題。關(guān)鍵詞乾坤、qiankun、微前端這三個詞連在一起代表的不是某種時髦架構(gòu)而是一套經(jīng)過真實業(yè)務(wù)高壓驗證的隔離機(jī)制——沙箱、生命周期、通信、資源加載控制。如果你正面臨主應(yīng)用臃腫、新業(yè)務(wù)不敢加、老系統(tǒng)不敢動、前端團(tuán)隊協(xié)作成本越來越高這些具體痛點那 qiankun 就不是“可選項”而是“止損線”。它適合兩類人一類是技術(shù)負(fù)責(zé)人需要快速建立跨團(tuán)隊協(xié)作邊界另一類是資深前端工程師想搞懂現(xiàn)代 Web 應(yīng)用底層運(yùn)行時的可控性到底怎么實現(xiàn)。別被“微前端”三個字嚇住qiankun 的核心邏輯其實就一句話把每個子應(yīng)用當(dāng)成一個受控的 iframe但不用 iframe。這句話背后藏著對瀏覽器運(yùn)行時、JavaScript 執(zhí)行上下文、CSS 作用域、資源加載鏈路的全鏈路干預(yù)能力。接下來我們就從真實踩坑現(xiàn)場出發(fā)一層層剝開它到底怎么做到的。2. 乾坤不是框架而是一套“運(yùn)行時治理協(xié)議”2.1 它不接管你的代碼只接管你的執(zhí)行環(huán)境很多初學(xué)者一上來就問“qiankun 怎么集成 VueReact 怎么配”這個問題本身就錯了方向。qiankun 本身不關(guān)心你用什么框架寫子應(yīng)用它只關(guān)心一件事當(dāng)這個子應(yīng)用的 JS 被執(zhí)行時它的全局變量、DOM 操作、樣式注入、資源請求是否被限制在它自己的“領(lǐng)地”里。換句話說qiankun 不是像 Vue Router 那樣提供一套新 API 讓你改寫路由邏輯而是像給每個子應(yīng)用發(fā)了一張“臨時工牌”——你只能訪問自己工位上的電腦window 對象、只能在自己工位區(qū)域貼便簽CSS、只能用自己的打印機(jī)fetch 請求攔截。這種能力叫“沙箱隔離”是 qiankun 的立身之本。它分兩種模式快照沙箱SnapshotSandbox和代理沙箱ProxySandbox??煺丈诚溥m用于非單頁應(yīng)用比如 jQuery 時代的老系統(tǒng)原理簡單粗暴在子應(yīng)用 mount 前先拍一張 window 全局對象的快照unmount 時把所有新增或修改的屬性按快照還原回去。實測下來它在 IE11 上依然穩(wěn)如老狗但缺點也很明顯——無法處理動態(tài)添加的全局監(jiān)聽器比如 window.addEventListener(resize, ...)因為快照拍不到函數(shù)引用。而代理沙箱是主流選擇它用 ES6 Proxy 包裹 window 對象所有讀寫操作都走代理層。比如子應(yīng)用執(zhí)行window.foo bar代理層會把它存到子應(yīng)用專屬的 map 里而不是真寫進(jìn)全局當(dāng)子應(yīng)用讀window.foo時代理層優(yōu)先從自己的 map 返回沒找到才查真實 window。這就實現(xiàn)了真正的“變量隔離”。但注意Proxy 在 Safari 10 以下不支持所以如果你的用戶群還有大量舊版 iOS 設(shè)備就得 fallback 到快照沙箱。這不是配置開關(guān)那么簡單而是要提前做 UA 檢測 動態(tài)加載策略——我見過有團(tuán)隊直接硬編碼sandbox: true結(jié)果在某銀行內(nèi)部 iPad 上白屏排查三天才發(fā)現(xiàn)是 Safari 版本兼容問題。2.2 生命周期不是概念而是強(qiáng)制約定的“入場券”qiankun 把子應(yīng)用的啟動、掛載、卸載、重載全部標(biāo)準(zhǔn)化為四個函數(shù)bootstrap、mount、unmount、update。這不是讓你“建議實現(xiàn)”而是必須導(dǎo)出否則整個子應(yīng)用根本不會被加載。為什么這么強(qiáng)硬因為這是唯一能保證主應(yīng)用和子應(yīng)用“步調(diào)一致”的方式。舉個典型反例某電商后臺系統(tǒng)子應(yīng)用用了 Vue CLI 默認(rèn)的main.js啟動方式里面直接 new Vue({ el: #app })。結(jié)果 qiankun 加載時它自己還沒準(zhǔn)備好 DOM 容器#sub-appVue 就報錯“找不到掛載點”整個流程卡死。后來我們逼著團(tuán)隊改寫入口mount函數(shù)里才真正初始化 Vue 實例并把 el 指向 qiankun 提供的容器節(jié)點unmount里主動調(diào)用 vm.$destroy() 并清空 DOM。這個過程看似多此一舉實則解決了兩個致命問題一是避免子應(yīng)用在錯誤時機(jī)操作 DOM比如主應(yīng)用還沒渲染完子應(yīng)用就急著找 #app二是確保資源可回收Vue 實例、事件監(jiān)聽器、定時器全被顯式銷毀。更關(guān)鍵的是update函數(shù)的存在讓子應(yīng)用支持熱更新成為可能——主應(yīng)用不用刷新頁面就能觸發(fā)子應(yīng)用重新 mount。我們在灰度發(fā)布時就靠它先加載新版本子應(yīng)用 JS調(diào)用update切換再對比監(jiān)控指標(biāo)沒問題再全量。這套生命周期本質(zhì)上是在瀏覽器原生執(zhí)行模型之上人為構(gòu)建了一層“應(yīng)用級調(diào)度器”。它不改變 JavaScript 運(yùn)行本質(zhì)但通過約定把不可控的腳本執(zhí)行變成了可編排、可中斷、可回滾的操作序列。2.3 路由不是搶奪戰(zhàn)而是協(xié)商制微前端最大的撕逼現(xiàn)場永遠(yuǎn)是路由。主應(yīng)用用 history.pushState(/dashboard)子應(yīng)用也 pushState(/user/list)瀏覽器地址欄變成/dashboard/user/list但誰來響應(yīng)qiankun 的解法很務(wù)實主應(yīng)用負(fù)責(zé)路由分發(fā)子應(yīng)用只管自己那一段路徑。主應(yīng)用配置一個activeRule比如/app1/表示所有以/app1/開頭的 URL都交給 app1 處理。子應(yīng)用內(nèi)部的路由Vue Router 或 React Router完全不用改它看到的location.pathname是/user/list而不是/app1/user/list。qiankun 在底層做了兩件事一是劫持history.pushState和replaceState自動剝離前綴再調(diào)用原生方法二是監(jiān)聽popstate事件當(dāng)瀏覽器前進(jìn)/后退時解析當(dāng)前 URL匹配 activeRule決定哪個子應(yīng)用該 mount/unmount。這里有個極易忽略的細(xì)節(jié)子應(yīng)用的 base 配置必須和 activeRule 嚴(yán)格對齊。比如主應(yīng)用配置activeRule: /app1/子應(yīng)用 Vue Router 就必須設(shè)base: /app1/如果子應(yīng)用設(shè)成base: /user/那它永遠(yuǎn)收不到/app1/user/list的路由變化。我們曾因此導(dǎo)致子應(yīng)用路由白屏debug 時發(fā)現(xiàn) console 里一堆NavigationDuplicated錯誤根源就是 base 和 activeRule 不一致。qiankun 不會幫你校驗這個它默認(rèn)你已理解路徑前綴的語義——這恰恰說明它不是黑盒而是一套需要你深度參與的協(xié)議。3. 從零搭一個可上線的乾坤主應(yīng)用避開 90% 的新手坑3.1 初始化主應(yīng)用npm create qiankun 別信官方文檔推薦npm create qiankunlatest快速生成模板但實際項目中我建議手動初始化。原因很簡單模板為了通用性引入了大量非必要依賴比如 qiankunjs/cli、webpack-plugin-qiankun而真實業(yè)務(wù)中你大概率要用已有的 webpack/vite 配置。我們以 Vite 為例這是目前最主流的選擇。第一步創(chuàng)建主應(yīng)用npm create vitelatest main-app -- --template react。第二步安裝核心依賴npm install qiankun。注意不要裝 qiankunjs/cli那個是早期腳手架現(xiàn)在已被棄用。第三步修改main.jsxReact或main.tsVue這是最關(guān)鍵的入口改造import { registerMicroApps, start } from qiankun; // 定義子應(yīng)用列表 const apps [ { name: app1, entry: //localhost:7100, // 子應(yīng)用開發(fā)服務(wù)器地址 container: #subapp-1, // 主應(yīng)用中預(yù)留的 DOM 容器 activeRule: /app1, // 激活規(guī)則注意這里沒有尾部斜杠 }, { name: app2, entry: //localhost:7101, container: #subapp-2, activeRule: /app2, } ]; // 注冊子應(yīng)用 registerMicroApps(apps); // 啟動 qiankun start({ // 關(guān)鍵配置sandbox 默認(rèn) true但生產(chǎn)環(huán)境建議顯式聲明 sandbox: { strictStyleIsolation: true, // 強(qiáng)制樣式隔離每個子應(yīng)用樣式僅作用于自身 experimentalStyleIsolation: true, // 實驗性樣式隔離更徹底Vite 環(huán)境推薦 }, // 路由基礎(chǔ)路徑必須和主應(yīng)用路由 base 一致 prefetch: true, // 預(yù)加載子應(yīng)用資源提升首屏速度 }); // 注意這里不能調(diào)用 ReactDOM.render() 或 createApp().mount() // qiankun 會接管 DOM 渲染主應(yīng)用只負(fù)責(zé)提供容器節(jié)點提示activeRule的寫法極其關(guān)鍵。/app1表示匹配/app1、/app1/、/app1/user但不匹配/app11而/app1/帶尾部斜杠會匹配/app1/、/app1/user但不匹配/app1。線上環(huán)境務(wù)必統(tǒng)一規(guī)范我們團(tuán)隊約定全部用無尾部斜杠寫法避免歧義。3.2 子應(yīng)用改造不是“加個插件”而是“重構(gòu)入口”子應(yīng)用改造是最大雷區(qū)。很多團(tuán)隊以為只要在 webpack 配置里加個qiankun-webpack-plugin就完事結(jié)果上線后子應(yīng)用白屏、樣式錯亂、接口 404。真相是子應(yīng)用必須同時滿足“可獨立運(yùn)行”和“可被 qiankun 加載”兩個條件。以 Vue 3 Vite 項目為例原始main.jsimport { createApp } from vue; import App from ./App.vue; createApp(App).mount(#app);改造后必須變成import { createApp } from vue; import App from ./App.vue; // 導(dǎo)出 qiankun 要求的生命周期函數(shù) export async function bootstrap() { console.log(app1 bootstrap); } export async function mount(props) { const { container } props; // 關(guān)鍵掛載點不再是 #app而是 qiankun 傳入的 container createApp(App).mount(container ? container.querySelector(#app) : #app); } export async function unmount(props) { const { container } props; if (container) { // 清空容器內(nèi)容避免殘留 container.innerHTML ; } } // 本地開發(fā)時仍需獨立運(yùn)行能力 if (!window.__POWERED_BY_QIANKUN__) { mount({}); }同時Vite 配置vite.config.js必須增加export default defineConfig({ build: { // 關(guān)鍵設(shè)置為 umd 格式讓 qiankun 能正確執(zhí)行 lib: { entry: path.resolve(__dirname, src/main.js), name: app1, formats: [umd], }, // 關(guān)鍵關(guān)閉混淆否則 qiankun 無法識別生命周期函數(shù) minify: false, // 關(guān)鍵設(shè)置公共路徑確保靜態(tài)資源圖片、字體能正確加載 assetsDir: static, }, // 關(guān)鍵配置 base必須和主應(yīng)用 activeRule 一致 base: window.__POWERED_BY_QIANKUN__ ? /app1/ : ./, });注意window.__POWERED_BY_QIANKUN__是 qiankun 注入的全局變量用于區(qū)分運(yùn)行環(huán)境。子應(yīng)用必須用它來判斷是否在 qiankun 環(huán)境下從而決定用哪種掛載方式。這個判斷不能省略否則本地開發(fā)和線上環(huán)境會行為不一致。3.3 樣式隔離strictStyleIsolation 不是銀彈但必須開啟樣式污染是微前端最直觀的災(zāi)難。一個子應(yīng)用寫了button { color: red; }另一個子應(yīng)用的按鈕全變紅了。qiankun 提供strictStyleIsolation: true原理是在子應(yīng)用 mount 時把它的style標(biāo)簽內(nèi)容提取出來用 CSS 選擇器重寫比如加前綴.app1-button再插入到主應(yīng)用head中unmount 時把這些動態(tài)插入的 style 標(biāo)簽全部移除。這招很有效但有兩個硬傷一是 CSS 選擇器層級過深時重寫可能導(dǎo)致權(quán)重失效二是import、font-face等規(guī)則無法被重寫。我們遇到過真實案例子應(yīng)用用了 Ant Design 的 icon 字體font-face規(guī)則沒被隔離導(dǎo)致所有子應(yīng)用圖標(biāo)顯示異常。解決方案是在子應(yīng)用中所有全局樣式包括 UI 組件庫都必須用 CSS-in-JS 或 CSS Modules 封裝。Ant Design 我們強(qiáng)制要求使用ConfigProvider的prefixCls屬性自定義前綴再配合:global()寫少量重置樣式。另外experimentalStyleIsolation: true實驗性樣式隔離更激進(jìn)它會給子應(yīng)用容器加一個隨機(jī) ID如>registerMicroApps([ { name: app1, entry: //localhost:7100, container: #subapp-1, activeRule: /app1, props: { userInfo: { id: 123, name: 張三 }, onLogout: () { /* 主應(yīng)用登出邏輯 */ } } } ]);子應(yīng)用接收export async function mount(props) { const { userInfo, onLogout } props; // 直接使用 userInfo 渲染onLogout 綁定到按鈕 click 事件 }為什么不用initGlobalState因為initGlobalState是全局狀態(tài)管理一旦濫用就會退化成“微前端版 Vuex”違背了微前端“解耦”的初衷。我們曾有一個項目所有子應(yīng)用都訂閱同一個 globalState結(jié)果一個子應(yīng)用調(diào)用setGlobalState({ theme: dark })所有子應(yīng)用主題瞬間切換但其中某個子應(yīng)用根本不支持暗色模式直接報錯崩潰。后來我們強(qiáng)制規(guī)定props只傳遞與當(dāng)前子應(yīng)用強(qiáng)相關(guān)的、不可變的數(shù)據(jù)如用戶身份、權(quán)限碼、當(dāng)前語言跨應(yīng)用事件通知必須通過自定義事件window.dispatchEvent或消息總線如 mitt且事件名必須帶子應(yīng)用前綴app1:login-success避免命名沖突。props是橋梁不是高速公路——它只承載必需品不運(yùn)載行李。4.3 錯誤監(jiān)控與降級子應(yīng)用崩潰不能拖垮主應(yīng)用微前端最大的風(fēng)險是“牽一發(fā)而動全身”。一個子應(yīng)用 JS 報錯如果沒處理好可能讓整個頁面卡死。qiankun 提供errorHandler配置但它只捕獲子應(yīng)用bootstrap/mount/unmount階段的同步錯誤。真正的 JS 運(yùn)行時錯誤比如 React 組件 render 報錯需要子應(yīng)用自己兜底。我們在所有子應(yīng)用的根組件里加了componentDidCatchReact或errorCapturedVue鉤子捕獲后上報 Sentry并展示友好的降級 UI如“模塊加載失敗請稍后重試”。更重要的是主應(yīng)用的降級策略我們給每個子應(yīng)用容器加了>