本文做者:張延卿javascript
雲音樂廣告 Dsp(需求方平臺)平臺分爲合約平臺(Vue 框架)和競價平臺(React 框架),因歷史緣由框架選型未能統一,最近來了新需求,須要同時在兩個平臺增長同樣的模塊,由於都是 Dsp 平臺,後期這樣的需求可能會不少,因此考慮到組件複用以及下降維護成本,在想怎麼統一技術棧,把 React 系統塞到 Vue 項目中進行呈現。html
系統是傳統的左右佈局,左側側邊欄展現菜單欄,頭部導航展現基礎信息,應用內容所有填充到藍色的內容區。前端
說實話,第一反應我直接想嵌套 iframe ,可是應用過 iframe 技術的,你們都知道它的痛:vue
postMessage
了
iframe
應用更新上線後,打開系統會發現系統命中緩存顯示舊內容,須要用時間戳方案解決或強制刷新
另外就是使用 MPA + 路由分發,當用戶訪問頁面時,由 Nginx 等負責根據路由分發到不一樣的業務應用,由各個業務應用完成資源的組裝後返回給瀏覽器,這種方式就須要把界面、導航都作成相似的樣子。
java
還有就是目前比較主流的幾種微前端方案:react
總的來講,iframe 主要用於簡單而且性能要求不高的第三方系統;MPA 不管在實現成本和體驗上面都不能知足當前業務需求;基座模式和 EMP 都是不錯的選擇,因 qiankun 在業內使用比較廣,較爲成熟,最後仍是選擇了 qiankunwebpack
qiankun(乾坤)是由螞蟻金服推出的基於Single-Spa實現的前端微服務框架,本質上仍是路由分發式的服務框架,不一樣於本來 Single-Spa 採用 JS Entry 加載子應用的方案,qiankun 採用 HTML Entry 方式進行了替代優化。git
JS Entry
的使用限制要求:github
對比 JS Entry, HTML Entry 使用就方便太多了,項目配置給定入口文件後,qiankun 會自行 Fetch 請求資源,解析出 JS 和 CSS 文件資源後,插入到給定的容器中,完美~
web
JS Entry 的方式一般是子應用將資源打成一個 Entry Script, 相似 Single-Spa 的 例子;
HTML Entry 則是使用 HTML 格式進行子應用資源的組織,主應用經過 Fetch html 的方式獲取子應用的靜態資源,同時將 HTML Document 做爲子節點塞到主應用的容器中。可讀性和維護性更高,更接近最後頁面掛載後的效果,也不存在須要雙向轉義的問題。
因爲 Vue 項目已經開發完成,咱們須要在原始項目中進行改造,很明顯選定 Vue 項目做爲基座應用,新需求開發採用 Create React App 搭建 React 子應用,接下來咱們看一下具體實現
基座(main)採用是的 vue-cli 搭建的,咱們保持其本來的代碼結構和邏輯不變,在此基礎上單獨爲子應用提供一個掛載的容器 DIV,一樣填充在相同的內容展現區域。
qiankun 只須要在基座應用中引入,爲了方便管理,咱們新增目錄,命名爲 micro ,標識目錄裏面是微前端改造代碼,進行全局配置初始化,改造以下:
路由配置文件 app.js
// 路由配置
const apps = [
{
name: 'ReactMicroApp',
entry: '//localhost:10100',
container: '#frame',
activeRule: '/react'
}
];
複製代碼
應用配置註冊函數
import { registerMicroApps, start } from "qiankun";
import apps from "./apps";
// 註冊子應用函數,包裝成高階函數,方便後期若是有參數注入修改app配置
export const registerApp = () => registerMicroApps(apps);
// 導出 qiankun 的啓動函數
export default start;
複製代碼
Layout 組件
<section class="app-main">
<transition v-show="$route.name" name="fade-transform" mode="out-in">
<!-- 主應用渲染區,用於掛載主應用路由觸發的組件 -->
<router-view />
</transition>
<!-- 子應用渲染區,用於掛載子應用節點 -->
<div id="frame" />
</section>
複製代碼
import startQiankun, { registerApp } from "../../../micro";
export default {
name: "AppMain",
mounted() {
// 初始化配置
registerApp();
startQiankun();
},
};
複製代碼
這裏會用到 qiankun 的兩個重要的 API :
注意點:咱們選擇在 mounted 生命週期中進行初始化配置,是爲了保證掛載容器必定存在
咱們來經過圖示具體理解一下 qiankun 註冊子應用的過程:
xx_QIANKUN__
,用於子應用判斷所處環境等activeRule
的規則來判斷是否激活子應用
activeRule
爲字符串時,以路由攔截方式進行自主攔截activeRule
爲函數時,根據函數返回值判斷是否激活咱們基於 Create React App 建立一個 React 項目應用,由上述的流程描述,咱們知道子應用得向外暴露一系列生命週期函數供 qiankun 調用,在 index.js 文件中進行改造:
增長 public-path.js 文件
目錄外層添加 public-path.js
文件,當子應用掛載在主應用下時,若是咱們的一些靜態資源沿用了 publicPath=/
的配置,咱們拿到的域名將會是主應用域名,這個時候就會形成資源加載出錯,好在 Webpack 提供了修改方法,以下:
if (window.__POWERED_BY_QIANKUN__) {
__webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}
複製代碼
路由 base 設置
由於一般來講,主應用會攔截瀏覽器路由變化以激活加載子應用。好比,上述的代碼裏咱們的路由配置,激活規則寫了 activeRule: /react
,這是什麼意思呢?這意味着,當瀏覽器 pathname
匹配到 /react
時,會激活子應用,可是若是咱們的子應用路由配置是下面這樣的:
<Router>
<Route exact path="/" component={Home} />
<Route path="/list" component={List} />
</Router>
複製代碼
咱們怎麼實現域名 /react
能正確加載對應的組件呢?你們必定經歷過用域名二級目錄訪問的需求,這裏是同樣的,咱們判斷是否在 qiankun 環境下,調整下 base 便可,以下:
const BASE_NAME = window.__POWERED_BY_QIANKUN__ ? "/react" : "";
...
<Router base={BASE_NAME}>
...
</Router>
複製代碼
增長生命週期函數
子應用的入口文件加入生命週期函數初始化,方便主應用調用資源完成後按應用名稱調用子應用的生命週期
/** * bootstrap 只會在微應用初始化的時候調用一次,下次微應用從新進入時會直接調用 mount 鉤子,不會再重複觸發 bootstrap。 * 一般咱們能夠在這裏作一些全局變量的初始化,好比不會在 unmount 階段被銷燬的應用級別的緩存等。 */
export async function bootstrap() {
console.log("bootstraped");
}
/** * 應用每次進入都會調用 mount 方法,一般咱們在這裏觸發應用的渲染方法 */
export async function mount(props) {
console.log("mount", props);
render(props);
}
/** * 應用每次切出/卸載 會調用的方法,一般在這裏咱們會卸載微應用的應用實例 */
export async function unmount() {
console.log("unmount");
ReactDOM.unmountComponentAtNode(document.getElementById("root"));
}
複製代碼
注意:全部的生明周期函數都必須是 Promise
修改打包配置
module.exports = {
webpack: (config) => {
// 微應用的包名,這裏與主應用中註冊的微應用名稱一致
config.output.library = `ReactMicroApp`;
// 將你的 library 暴露爲全部的模塊定義下均可運行的方式
config.output.libraryTarget = "umd";
// 按需加載相關,設置爲 webpackJsonp_ReactMicroApp 便可
config.output.jsonpFunction = `webpackJsonp_ReactMicroApp`;
config.resolve.alias = {
...config.resolve.alias,
"@": path.resolve(__dirname, "src"),
};
return config;
},
devServer: function (configFunction) {
return function (proxy, allowedHost) {
const config = configFunction(proxy, allowedHost);
// 關閉主機檢查,使微應用能夠被 fetch
config.disableHostCheck = true;
// 配置跨域請求頭,解決開發環境的跨域問題
config.headers = {
"Access-Control-Allow-Origin": "*",
};
// 配置 history 模式
config.historyApiFallback = true;
return config;
};
},
};
複製代碼
注意:配置的修改成了達到兩個目的,一個是暴露生命週期函數給主應用調用,第二點是容許跨域訪問,修改的注意點能夠參考代碼的註釋。
小結:跳轉流程梳理,在主應用 router 中定義子應用跳轉 path ,以下圖,在調用組件 mounted 生命週期中使用 qiankun 暴露的
loadMicroApp
方法加載子應用,跳轉到子應用定義的路由,同時使用addGlobalUncaughtErrorHandler
和removeGlobalUncaughtErrorHandler
監聽並處理異常狀況(例如子應用加載失敗),當子應用監聽到跳轉路由時,加載子應用(上述<Router>
組件中)定義的 component,完成主應用到子應用的跳轉。
{
path: '/xxx',
component: Layout,
children: [
{
path: '/xxx',
component: () => import('@/micro/app/react'),
meta: { title: 'xxx', icon: 'user' }
}
]
},
複製代碼
一、子應用未成功加載
若是項目啓動完成後,發現子應用系統沒有加載,咱們應該打開控制檯分析緣由:
二、基座應用路由模式
基座應用項目是 hash 模式路由,這種狀況下子應用的路由模式必須跟主應用保持一致,不然會加載異常。緣由很簡單,假設子應用採用 history 模式,每次切換路由都會改變 pathname,這個時候很難再經過激活規則去匹配到子應用,形成子應用 unmount
三、CSS 樣式錯亂
因爲默認狀況下 qiankun 並不會開啓 CSS 沙箱進行樣式隔離,當主應用和子應用產生樣式錯亂時,咱們能夠啓用 { strictStyleIsolation: true }
配置開啓嚴格隔離樣式,這個時候會用 Shadow Dom 節點包裹子應用,相信你們看到這個也很熟悉,和微信小程序中頁面和自定義組件的樣式隔離方案一致。
四、另外,在接入過程當中,總結了幾個須要注意的點
onClick
或 addEventListener
給 <body>
添加了一個點擊事件,JS 沙箱並不能消除它的影響,還得靠平時的代碼規範
五、將來可能須要考慮一些問題
其實寫下來整個項目,最大的感覺 qiankun 的開箱可用性很是強,須要更改的項目配置基本不多,固然遇到的一些坑點也確定是踩過才能更清晰。
若是文章有什麼問題或者錯誤,歡迎指正交流,謝謝!
本文發佈自 網易雲音樂大前端團隊,文章未經受權禁止任何形式的轉載。咱們常年招收前端、iOS、Android,若是你準備換工做,又剛好喜歡雲音樂,那就加入咱們 grp.music-fe (at) corp.netease.com!