qiankun 实战
Base(基座)定义路由
- App.tsx(Base 基座)
// 父应用是什么路由,子应用就要是什么路由,不然子应用无法加载
import { BrowserRouter as Router, Link } from "react-router-dom";
import "./registerMicroApps"; //引入子应用注册文件
function App() {
return (
<div className="App">
<Router>
{/* 基座路由可以动态加载 */}
<Link to="/vue">vue子应用</Link>
<Link to="/react">react子路由</Link>
</Router>
{/* 切换导航,将微应用渲染到container容器中 */}
<div id="container"></div>
</div>
);
}
(Base)基座配置路由
- registerMicroApps.js(配置子应用)
// 底层基于single-spa
import { registerMicroApps, start } from "qiankun";
const loader = (loading) => {
// loading为true时,子应用加载中
// loading为false时,子应用加载完成
};
// 可以注册多个子应用
registerMicroApps(
[
{
name: "vue", // 子应用名称
entry: "//localhost:7100", // 子应用地址
container: "#container", // 子应用渲染到哪个容器中
activeRule: "/vue", // 子应用激活规则,
loader,
},
{
name: "react",
entry: "//localhost:8100",
container: "#container",
activeRule: "/react",
loader,
},
],
// 生命周期
{
beforeLoad: () => {
console.log("加载前");
},
beforeMount: () => {
console.log("挂载前");
},
afterMount: () => {
console.log("挂载后");
},
beforeUnmount: () => {
console.log("卸载前");
},
afterUnmount: () => {
console.log("卸载后");
},
}
);
// 启动
start({
sandbox: {
//沙箱配置
experimentalStyleIsolation: true, //样式隔离(会加前缀)
},
});
子应用配置
- (vue)vue.config.js
module.exports = {
publicPath: "//localhost:7100", // 子应用需要暴露publicPath,否则主应用无法访问子应用(保证子应用静态资源都是向7100端口发请求)
devServer: {
port: 7100, // fetch
headers: {
"Access-Control-Allow-Origin": "*", //允许跨域
},
},
configureWebpack: {
output: {
// 需要获取到打包的内容
libraryTarget: "umd", //定义打包方式,umd
library: "my-vue", //定义打包后的文件名
},
},
};
// 3000 -> 7100 基座回去找7100端口中的资源
// publicPath: / -> "/" 已3000端口发请求
- router.js
import { createRouter, createWebHistory } from "vue-router";
import Home from "./views/Home.vue";
const routes = [
{
path: "/",
name: "Home",
component: Home,
},
{
path: "/about",
name: "About",
component: () =>
import(/* webpackChunkName: "about" */ "./views/About.vue"),
},
];
export default routes;
子应用配置
- main.js(子应用 vue3 项目)
import { createApp } from "vue";
import { createRouter, createWebHistory } from "vue-router";
import App from "./App.vue";
import routes from "./router";
let history; // 子应用需要暴露history,否则主应用无法访问子应用
let router;
let app;
// 不能直接挂载,需要切换的时候,调用mount方法时候再取挂载
function render(props = {}) {
history = createWebHistory("/vue");
router = createRouter({
history,
routes,
});
// 创建应用
app = createApp(App);
let { container } = props;
// 挂载到哪里就是个问题了? 有个container就挂载到父级的#app上,没有就挂载到自己#app上
app.use(router).mount(container ? container.querySelector("#app") : "#app");
}
// qiankun在渲染前会给一个变量 window.__POWERED_BY_QIANKUN__
if (!window.__POWERED_BY_QIANKUN__) {
render(); //独立运行
}
// 需要暴露接入协议,否则主应用无法访问子应用(返回的是一个Promise)
export async function bootstrap() {
console.log("vue3 项目启动了");
}
export async function mount(props) {
// props中有渲染的容器位置
console.log("vue3 项目挂载了");
render(props);
}
export async function unmount() {
console.log("vue3 项目卸载了");
history = null;
app = null;
router = null;
}
react 子应用配置
- yarn add @rescripts/cli -D
- 可以重写 react 配置文件
- package.json 的命令行用
rescripts
.rescripts.js
module.exports = {
webpack: () => {
config.output.library = "my-react";
config.output.libraryTarget = "umd";
config.output.publicPath = "//localhost:8100/";
},
devServer: () => {
config.headers = {
"Access-Control-Allow-Origin": "*", //允许跨域
};
return config;
},
};
.env
PORT=8100
WDS_SOCKET_PORT=8100
main.js
中配置
function render(props = {}) {
const { container } = props;
ReactDOM.render(
<App />,
container
? container.querySelector("#root")
: document.querySelector("#root")
);
}
if (!window.__POWERED_BY_QIANKUN__) {
render();
}
export async function bootstrap() {
console.log("react app bootstraped");
}
export async function mount(props) {
render(props);
}
export async function unmount() {}
export async function bootstrap(props) {}
export async function mount(props) {
render(props);
}
export async function unmount(props) {
ReactDOM.unmountComponentAtNode(
props.container
? props.container.querySelector("#root")
: document.querySelector("#root")
);
}
// qiankun 样式处理,要把样式隔离了
// 默认情况切换应用它会采用动态样式表 -> 加载的时候添加样式、删除的时候卸载样式 -> 动态样式表切换应用的时候会闪烁
// shadowDOM -> 完全隔离了样式,但是兼容性不好 -> video标签中的 快进 放大功能 增加全局样式就会有问题
应用通信
- 服务之间的应用通信
- 父应用和子应用,以及兄弟应用之间的参数传递
- 通信方式目前常见的 2 种
-
- 本地存储的方式来获取数据如:token
-
- 采用 qiankun 提供的全局对象(数据容器),来提供数据,每个应用都可以共享这个容器中的数据
-
- 本地存储:浏览器分了域,同域下面才能获取本地存储的数据
- qiankun 里子应用可以获取父应用存储的本地数据.子应用最终打包静态资源,放在父应用
开发步骤
- 在父应用定义一个全局状态,将数据存储到状态池中(登录成功后)
- 在子应用中注册一个观察者(绑定事件监听)
- 在子应用获取数据,并使用
- 一旦全局状态池发生变化,注册监听器子应用都会收到更新结果
import { initGlobalState } from "qiankun"; // 1. 定义全局状态
const actions = initGlobalState({ username: "jxb" }); // 2. 将数据存储到状态池中
// 3. 在子应用中注册一个观察者(绑定事件监听) -> 父应用中这个不是必须的
actions.onGlobalStateChange((state, prev) => {
console.log("主应用改变后的状态", state, prev);
console.log("prev旧值", prev);
console.log("state新值", state);
}, true); // true -> 代表立即侦听(开头就执行一次)
// 4. 在子应用获取数据,并使用
// 5. 一旦全局状态池发生变化,注册监听器子应用都会收到更新结果
const username = actions.getGlobalState("username");
- 子应用封装工具
- 子应用中封装一个工具函数,来获取全局状态池中的数据
- 子应用没有安装图 qiankun,就
class Actions {
actions = {
onGlobalStateChange: null,
setGlobalState: null,
};
// 设置actions 通过父应用传下来
setActions(actions) {
this.actions = actions;
}
//
onGlobalStateChange() {
return this.actions.onGlobalStateChange(...arguments);
}
//修改全局状态
setGlobalState() {
this.actions.setGlobalState(...arguments);
}
}
const actions = new Actions();
export default actions;
- 子应用使用
import actions from "./actions";
// 子应用挂载完成
export async function mount(props) {
actions.setActions(props);
render(props);
}
- 子应用中使用 actions
import react, { useEffect, useState } from "react";
import actions from "actions";
const Demo = () => {
const [username, setUsername] = useState();
useEffect(() => {
actions.onGlobalStateChange((state, prev) => {
// 做自己想要的操作
console.log("state 子应用", state);
setUsername(state.username);
}, true);
}, []);
return (
<div>
<h1>{username}</h1>
<button
onClick={() => {
actions.setGlobalState({ username: "子应用修改" });
}}
>
修改
</button>
</div>
);
};
export default Demo;