qiankun笔记

2 阅读5分钟

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标签中的 快进 放大功能 增加全局样式就会有问题

应用通信

  • 服务之间的应用通信
    1. 父应用和子应用,以及兄弟应用之间的参数传递
  • 通信方式目前常见的 2 种
      1. 本地存储的方式来获取数据如:token
      1. 采用 qiankun 提供的全局对象(数据容器),来提供数据,每个应用都可以共享这个容器中的数据
  • 本地存储:浏览器分了域,同域下面才能获取本地存储的数据
  • qiankun 里子应用可以获取父应用存储的本地数据.子应用最终打包静态资源,放在父应用

开发步骤

  1. 在父应用定义一个全局状态,将数据存储到状态池中(登录成功后)
  2. 在子应用中注册一个观察者(绑定事件监听)
  3. 在子应用获取数据,并使用
  4. 一旦全局状态池发生变化,注册监听器子应用都会收到更新结果
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;