轉載請注明出處,點擊此處 查看更多精彩內容。
qiankun 是一個基于 single-spa 的微前端實現(xiàn)庫,旨在幫助大家能更簡單、無痛的構建一個生產可用微前端架構系統(tǒng)。
qiankun 孵化自螞蟻金融科技基于微前端架構的云產品統(tǒng)一接入平臺,在經過一批線上應用的充分檢驗及打磨后,我們將其微前端內核抽取出來并開源,希望能同時幫助社區(qū)有類似需求的系統(tǒng)更方便的構建自己的微前端系統(tǒng),同時也希望通過社區(qū)的幫助將 qiankun 打磨的更加成熟完善。
目前 qiankun 已在螞蟻內部服務了超過 2000+ 線上應用,在易用性及完備性上,絕對是值得信賴的。
快速上手
主應用
主應用不限技術棧,只需要提供一個容器 DOM,然后注冊子應用并 start 即可。
安裝 qiankun
yarn add qiankun
pnpm add qiankun
npm i qiankun
添加子應用容器
在需要渲染子應用的位置添加子應用容器。
<div id="micro-app-container"></div>
注冊子應用并啟動
進入子應用前必須先注冊子應用并啟動 qiankun。
- 注冊子應用:registerMicroApps()
- 啟動 qiankun:start()
import { registerMicroApps, start } from 'qiankun';
registerMicroApps([
{
name: "Vue2App",
entry: "http://localhost:8381/",
container: "#micro-app-container",
activeRule: "/vue2-app",
},
{
name: "Vue3App",
entry: "http://localhost:8382/",
container: "#micro-app-container",
activeRule: "/vue3-app",
},
]);
start();
子應用配置解析:
- name: 子應用的名稱,子應用之間必須確保唯一。
- entry: 子應用的入口。
- 支持配置是子應用的訪問地址字符串。
- 支持配置對象
{ scripts?: string[]; styles?: string[]; html?: string },html 的值是子應用的 html 內容字符串,而不是子應用的訪問地址。子應用的publicPath將會被設置為/。
- container: 子應用容器節(jié)點的選擇器或者 Element 實例。
- activeRule: 子應用的激活規(guī)則。
- 支持直接配置字符串或字符串數(shù)組,如
'/app1'或['/app1', '/app2'],當配置為字符串時會直接跟 url 中的路徑部分做前綴匹配,匹配成功表明當前應用會被激活。 - 支持配置一個函數(shù)或函數(shù)數(shù)組。函數(shù)會傳入當前
location作為參數(shù),函數(shù)返回 true 時表明當前子應用會被激活。如location => location.pathname.startsWith('/app1')。
- 支持直接配置字符串或字符串數(shù)組,如
子應用預加載
默認情況下,qinakun 會在第一個子應用 mounted 完成后開始預加載其他子應用的靜態(tài)資源。
qiankun 的啟動函數(shù) start() 接收可選配置,使用 prefetch 屬性可配置子應用預加載規(guī)則。
start({
prefetch: true
})
- prefetch:
boolean | 'all' | string[] | (( apps: RegistrableApp[] ) => { criticalAppNames: string[]; minorAppsName: string[] })是否開啟預加載,默認為true。- 配置為
true則會在第一個子應用 mount 完成后開始預加載其他子應用的靜態(tài)資源。 - 配置為
string[]則會在第一個子應用mounted后開始加載數(shù)組內的子應用資源。 - 配置為
function則可完全自定義應用的資源加載時機 (首屏應用及次屏應用)。 - 配置為
'all'則主應用start后即開始預加載所有子應用靜態(tài)資源。
- 配置為
Vue2 + Webpack 子應用
動態(tài)配置資源路徑
- 在
src目錄新增public-path.js:
if (window.__POWERED_BY_QIANKUN__) {
__webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}
- 在應用入口
index.js頂部導入public-path.js。
import "./public-path";
公開生命周期函數(shù)
import "./public-path";
import Vue from "vue";
import App from "./App.vue";
import router from "./router";
import store from "./store";
Vue.config.productionTip = false;
let app: Vue | null;
function renderApp(container: string | HTMLElement) {
app = new Vue({
router,
store,
render: (h) => h(App),
}).$mount(container);
}
if (!window.__POWERED_BY_QIANKUN__) {
renderApp("#app");
}
export async function bootstrap() {
console.log("vue2 app bootstrap");
}
export async function mount(props: any) {
renderApp(props.container.querySelector("#app"));
}
export async function unmount() {
if (!app) {
return;
}
app.$destroy();
app.$el.innerHTML = "";
app = null;
}
設置路由前綴
const router = new VueRouter({
mode: "history",
base: window.__POWERED_BY_QIANKUN__ ? "/vue2-app/" : process.env.BASE_URL,
routes,
});
修改打包配置(vue.config.js)
const { defineConfig } = require("@vue/cli-service");
const { name } = require("./package.json");
module.exports = defineConfig({
...,
devServer: {
headers: {
"Access-Control-Allow-Origin": "*",
},
},
configureWebpack: {
output: {
library: `${name}-[name]`,
libraryTarget: "umd",
jsonpFunction: `webpackJsonp_${name}`,
},
},
});
Vue3 + Vite 子應用
qiankun 默認不支持 Vite,要加載 Vite 構建的子應用,需要借助 vite-plugin-qiankun 插件。
安裝 vite-plugin-qiankun
yarn add vite-plugin-qiankun
pnpm add vite-plugin-qiankun
npm i vite-plugin-qiankun
公開生命周期函數(shù)
import { createApp, type App as VueApp } from "vue";
import {
renderWithQiankun,
qiankunWindow,
type QiankunProps,
} from "vite-plugin-qiankun/dist/helper";
import App from "./App.vue";
import router from "./router";
import "./assets/main.css";
let app: VueApp<Element>;
function renderApp(container: any) {
app = createApp(App);
app.use(router);
app.mount(container);
}
if (qiankunWindow.__POWERED_BY_QIANKUN__) {
renderWithQiankun({ bootstrap, mount, unmount, update });
} else {
renderApp("#app");
}
async function bootstrap() {
console.log("vue3 app bootstrap");
}
async function mount(props: QiankunProps) {
renderApp(props.container?.querySelector("#app"));
}
async function unmount() {
app?.unmount();
}
async function update() {
console.log("vue3 app update");
}
設置路由前綴
import { qiankunWindow } from "vite-plugin-qiankun/dist/helper";
import { name } from "../../package.json";
const base = qiankunWindow.__POWERED_BY_QIANKUN__ ? name : import.meta.env.BASE_URL;
const router = createRouter({
history: createWebHistory(base),
routes: routes,
});
修改打包配置(vite.config.ts)
...
import qiankun from "vite-plugin-qiankun";
const port = 8382;
const base = `http://localhost:${port}/`;
export default defineConfig({
base: base,
server: {
port: port,
origin: base,
cors: true,
},
plugins: [
...,
qiankun("vue3-app", {
useDevMode: true,
}),
],
...,
});
React 子應用
動態(tài)配置資源路徑
- 在
src目錄新增public-path.js:
if (window.__POWERED_BY_QIANKUN__) {
__webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}
- 在應用入口
index.js頂部導入public-path.js。
import "./public-path";
公開生命周期函數(shù)
import "./public-path";
import React from "react";
import ReactDOM from "react-dom";
import { BrowserRouter } from "react-router-dom";
import App from "./App";
function renderRoot(container: HTMLElement) {
ReactDOM.render(
<React.StrictMode>
<BrowserRouter>
<App />
</BrowserRouter>
</React.StrictMode>,
container
);
}
if (!window.__POWERED_BY_QIANKUN__) {
renderRoot(document.querySelector("#root")!);
}
export async function bootstrap() {
console.log("react app bootstrap");
}
export async function mount(props: any) {
renderRoot(props.container?.querySelector("#root"));
}
export async function unmount(props: any) {
ReactDOM.unmountComponentAtNode(props.container?.querySelector("#root"));
}
設置路由前綴
<BrowserRouter basename={window.__POWERED_BY_QIANKUN__ ? '/react-app' : '/'}>
修改 webpack 配置
- 安裝插件
@rescripts/cli,當然也可以選擇其他的插件,例如react-app-rewired。
yarn add -D @rescripts/cli
pnpm add -D @rescripts/cli
npm i -D @rescripts/cli
- 應用根目錄新增
.rescriptsrc.js
const { name } = require("./package");
module.exports = {
webpack: (config) => {
config.output.library = `${name}-[name]`;
config.output.libraryTarget = "umd";
config.output.jsonpFunction = `webpackJsonp_${name}`;
config.output.globalObject = "window";
return config;
},
devServer: (_) => {
const config = _;
config.headers = {
"Access-Control-Allow-Origin": "*",
};
config.historyApiFallback = true;
config.hot = false;
config.watchContentBase = false;
config.liveReload = false;
return config;
},
};
- 修改
package.json
- "start": "react-scripts start",
+ "start": "rescripts start",
- "build": "react-scripts build",
+ "build": "rescripts build",
應用間通信
props 傳參
注冊子應用時可以通過 props 傳遞參數(shù)給子應用。
registerMicroApps([
{
name: "Vue2App",
entry: "http://localhost:8381/",
container: "#micro-app-container",
activeRule: "/vue2-app",
props: {
name: "StoneHui",
age: 30
}
},
]);
子應用掛載時可通過 props 獲取主應用傳遞的參數(shù)。
export async function mount(props) {
console.log(props.name, props.age); // StoneHui 30
}
initGlobalState(state)
定義全局狀態(tài),并返回通信方法,建議在主應用使用,微應用通過 props 獲取通信方法。
相關類型定義:
/**
* 定義全局狀態(tài),并返回通信方法
*
* @param state: 全局狀態(tài)
* @retuns 通信方法實例
*/
function initGlobalState(state: Record<string, any>): MicroAppStateActions
/**
* 通信方法
*/
type MicroAppStateActions {
/**
* 在當前應用監(jiān)聽全局狀態(tài),有變更觸發(fā) callback
* @param callback 狀態(tài)變更回調函數(shù)
* @param fireImmediately,是否立即觸發(fā) callback 函數(shù)
*/
onGlobalStateChange: (callback: OnGlobalStateChangeCallback, fireImmediately?: boolean) => void
/**
* 按一級屬性設置全局狀態(tài),微應用中只能修改已存在的一級屬性
* @param callback 變更的全局狀態(tài)
* @retuns 修改結果
*/
setGlobalState: (state: Record<string, any>) => boolean
/**
* 移除當前應用的狀態(tài)監(jiān)聽,微應用 umount 時會默認調用
* @retuns 移除結果
*/
offGlobalStateChange: () => boolean
}
/**
* 全局狀態(tài)變更的回調函數(shù)
* @param state 變更后的全局狀態(tài)
* @param prevState 變更前的全局狀態(tài)
*/
type OnGlobalStateChangeCallback = (state: Record<string, any>, prevState: Record<string, any>) => void;
使用示例:
- 主應用
import { initGlobalState, MicroAppStateActions } from 'qiankun';
const state = {
...
}
// 初始化全局狀態(tài)并獲取操作函數(shù)
const actions: MicroAppStateActions = initGlobalState(state);
// 監(jiān)聽全局狀態(tài)變更
actions.onGlobalStateChange((state, prev) => {
console.log(state, prev);
});
// 更新全局狀態(tài)
actions.setGlobalState(state);
- 子應用
export function mount(props) {
// 監(jiān)聽全局狀態(tài)變更
props.onGlobalStateChange((state, prev) => {
console.log(state, prev);
});
// 更新全局狀態(tài)
props.setGlobalState(state);
}
常見問題
TypeScript cannot find name __webpack_public_path__。
在 src 目錄新增 global.d.ts 文件:
declare let __webpack_public_path__: string;
interface Window {
__POWERED_BY_QIANKUN__: boolean;
__INJECTED_PUBLIC_PATH_BY_QIANKUN__: string;
}
__webpack_public_path__ 無效,靜態(tài)資源路徑錯誤。
將 public-path.js 的導入語句放在應用入口文件的第一行。
configuration.output has an unknown property 'jsonpFunction'.
將 output.jsonpFunction 更名為 output.chunkLoadingGlobal。
// jsonpFunction: `webpackJsonp_${name}`,
chunkLoadingGlobal: `webpackJsonp_${name}`,
子應用內部路由跳轉后無法切換到主應用或其他子應用且路由棧異常。
-
Vue通過路由守衛(wèi)更新state
router.afterEach(() => {
// Vue2
Object.assign(history.state, { current: location.pathname });
// Vue3
Object.assign(history.state, {
current: history.state.current === "/" ? "/" : location.pathname,
});
});
-
React通過popstate監(jiān)聽器更新state
window.addEventListener("popstate", () => {
Object.assign(history.state, { current: location.pathname });
});