微前端主应用与子应用如何构建
构建主应用
-
使用 vue-cli 创建主应用
-
npm install qiankun 下载微前端方案依赖
-
改造主项目
- 改造 main.js
// 在和main.js统计目录新建application.js,然后在main.js中引入application.js import { registerMicroApps, start } from "qiankun"; // 注册子应用 const apps = [ { name: "vueApp", entry: "http://localhost:10000/vue/", // 默认会加载这个HTML,解析里面的js,动态的执行 (fetch) container: "#vue", // 容器 activeRule: "/vue", // 激活的路径 props: { a: 1 }, // 传递参数 }, ]; registerMicroApps(apps); // 注册 start({ prefetch: false }); // 开启
- 改造 App.vue
<template> <div> <router-view></router-view> <!-- 子应用盒子 --> <div id="vue"></div> </div> </template>
构建子应用
-
在主应用同级目录下使用 vue-cli 创建一个子项目
-
改造子项目
- 改造 main.js
import Vue from "vue"; import App from "./App.vue"; import router from "./router"; import store from "./store"; // eslint-disable-next-line no-unused-vars let instance = null; function render(props = {}) { instance = new Vue({ router, store, render: (h) => h(App), }).$mount("#app"); // 这里是挂在到自己的HTML中,基座会拿到这个挂在后的HTML将其插入进去 } // 判断当前环境是否为qiankun环境 if (window.__POWERED_BY_QIANKUN__) { // eslint-disable-next-line no-undef,camelcase __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__; } else if (!window.__POWERED_BY_QIANKUN__) { render(); } // 子应用必须导出 以下生命周期 bootstrap、mount、unmount export async function bootstrap(props) {} export async function mount(props) { render(props); } // props 父应用传递过来的值 export async function unmount(props) { instance = null; }
- 改造 vue.config.js
module.exports = { publicPath: "/vue", // 必须设置路由根路径,便于主应用加载子应用 configureWebpack: { output: { library: "singleVue", libraryTarget: "umd", // 把子应用打包成 umd 库格式 }, devServer: { port: 10000, headers: { // 必须设置跨越 "Access-Control-Allow-Origin": "*", }, }, }, };
经过上述改造,一个简易的微前端环境就草草建成了,是不是很简单,你是不是已经跃跃欲试了?
但是,一个基础的微前端架子建成后,我们还有一些无法绕过的问题要处理,例如:应用之间的通信、全局状态的保存…
应用路径配置
为主应用添加项目根路径
-
改造主应用 vue.config.js 的
publicPath
// vue.config.js module.exports = { publicPath: "/base", };
-
改造主应用 application.js 的 app
const apps = [
{
name: "vueApp",
entry: "http://localhost:10000/base/vue/", // 默认会加载这个HTML,解析里面的js,动态的执行 (fetch)
container: "#vue", // 容器
activeRule: "/base/vue", // 激活的路径
props: pagerProps, // 传递参数
},
];
- 改造子应用 vue.config.js 的
publicPath
module.exports = { publicPath: "/base/vue", };
在主应用的某个路由页面加载微应用
- 主应用注册这个路由时给
path
加一个*
,注意:如果这个路由有其他子路由,需要另外注册一个路由,任然使用这个组件即可。const routes = [ { path: "/work/*", name: "work", component: () => import("../views/work.vue"), }, ];
- 微应用的
activeRule
需要包含主应用的这个路由path
// 存放要加载进来的系统 const apps = [ { name: "vueApp", entry: "http://localhost:10000/base/work/vue/", // 默认会加载这个HTML,解析里面的js,动态的执行 (fetch) container: "#vue", // 容器 activeRule: "/base/work/vue", // 激活的路径 props: pagerProps, // 传递参数 }, ];
- 改造子应用 vue.config.js 的
publicPath
module.exports = { publicPath: "/base/work/vue", };
在 work.vue 这个组件的 mounted 周期调用 start 函数, 注意不要重复调用import { start } from "qiankun"; export default { mounted() { if (!window.qiankunStarted) { window.qiankunStarted = true; start(); } }, };
全局状态存储
主应用实现
-
首先我们需要在主应用中使用
Vuex
创建store
用于管理全局状态池import Vue from "vue"; import Vuex from "vuex"; Vue.use(Vuex); export default new Vuex.Store({ state: { token: "", }, mutations: { SET_TOKEN: (state, token) => { state.token = token; }, }, getters: { token: (state) => state.user.token, }, });
-
实现主应用的
shared
实例// /src/shared/shared.js import store from "@/store"; /** * 用于操作全局状态池 */ class Shared { /** * 获取 Token * @returns {any} */ getToken() { return store.getters.token || ""; } /** * 设置 Token * @param token */ setToken(token) { // 将 token 的值记录在 store 中 store.commit("SET_TOKEN", token); } } const shared = new Shared(); export default shared;
-
将
shared
实例通过props
传递给子应用// 在application.js中 import shared from "@/shared/shared"; const pagerProps = { shared, // 将全局状态传递给子应用 }; // 存放要加载进来的系统 const apps = [ { name: "vueApp", entry: "http://localhost:10000/base/work/vue/", // 默认会加载这个HTML,解析里面的js,动态的执行 (fetch) container: "#vue", // 容器 activeRule: "/base/work/vue", // 激活的路径 props: pagerProps, // 传递参数 }, ];
子应用实现
-
子应用也应该实现
shared
,以便在独立运行时可以拥有兼容处理能力// /src/shared/index.js class Shared { /** * 获取 Token * @returns {any} */ getToken() { return store.getters.token || ""; } /** * 设置 Token * @param token */ setToken(token) { // 将 token 的值记录在 store 中 store.commit("SET_TOKEN", token); } } class SharedModule { static shared = new Shared(); /** * 重载 shared */ static overloadShared(shared) { SharedModule.shared = shared; } /** * 获取 shared 实例 */ static getShared() { return SharedModule.shared; } } export default SharedModule;
-
实现了子应用的
shared
后,我们需要在入口文件处注入shared
import Vue from "vue"; import App from "./App.vue"; import router from "./router"; import store from "./store"; import SharedModule from "@/shared"; // Vue.config.productionTip = false // eslint-disable-next-line no-unused-vars let instance = null; function render(props = {}) { // 注入 shared 并将其挂载到Vue实例上 if (window.__POWERED_BY_QIANKUN__) { const { shared = SharedModule.getShared() } = props; SharedModule.overloadShared(shared); Vue.prototype.$commStore = SharedModule.getShared(); } instance = new Vue({ router, store, render: (h) => h(App), }).$mount("#app"); // 这里是挂在到自己的HTML中,基座会拿到这个挂在后的HTML将其插入进去 } if (window.__POWERED_BY_QIANKUN__) { // eslint-disable-next-line no-undef,camelcase __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__; } if (!window.__POWERED_BY_QIANKUN__) { render(); } export async function bootstrap() {} export async function mount(props) { render(props); } // props 父应用传递过来的值 export async function unmount(props) { instance = null; }
主、子应用间动态通信
主应用实现
::: tip 说明
qiankun 对于 props 的应用类似于 react 框架的父子组件通信,传入 data 数据供自组件使用,传入 fn 函数给子组件触发向上回调。
但是这种传参方式只是在注册子应用时传递了一次数据,而数据改变后又不能再注册一遍子应用。这个时候就需要应用间动态通信了
因此在这里我们使用 rxjs
来作为应用间通信的方案
:::
-
先在主应用下载并引入
rxjs
;并创建我们的呼机
import { Subject } from "rxjs"; // 按需引入减少依赖包大小 const pager = new Subject(); export default pager;
-
在主应用的
application.js
引入并注册呼机,以及将呼机下发给子应用import { registerMicroApps } from "qiankun"; import pager from "@/shared/pager"; import shared from "@/shared/shared"; /** * 用于接收子应用发送过来的消息 */ pager.subscribe((v) => { // 在主应用注册呼机监听器,这里可以监听到其他应用的广播 console.log(`监听到子应用${v.from}发来消息:`, v); // 这里处理主应用监听到改变后的逻辑 }); const pagerProps = { shared, // 将全局状态传递给子应用 pager, // 从主应用下发应用间通信呼机 }; // 存放要加载进来的系统 const apps = [ { name: "vueApp", entry: "http://localhost:10000/base/work/vue/", // 默认会加载这个HTML,解析里面的js,动态的执行 (fetch) container: "#vue", // 容器 activeRule: "/base/work/vue", // 激活的路径 props: pagerProps, // 传递参数 }, ]; registerMicroApps(apps); // 注册
- 在子应用中注册呼机
export async function bootstrap({ emitFnc, pager }) { pager.subscribe((v) => { // 在子应用注册呼机监听器,这里可以监听到其他应用的广播 console.log(`监听到子应用${v.from}发来消息:`, v); }); Vue.prototype.$pager = pager; // 将呼机挂载在vue实例 }
- 在各应用中使用呼机动态传递信息
methods: { // 在某个应用里调用.next方法更新数据,并传播给其他应用 callParentChange() { this.$pager.next({ from: "Vue", // 应用名称 token: "但若不见你,阳光也无趣" }); } }
主应用下发 emit 函数收集子应用反馈
主应用实现
-
创建要下发的 emit 函数
// /shared/childEmit function changeDataMsg(val) { // 要实现的业务 console.log(val); } export { changeDataMsg };
-
下发 emit 函数
// application.js import { registerMicroApps } from "qiankun"; import pager from "@/shared/pager"; import * as childEmit from "@/shared/childEmit"; // 导入主应用需要下发的emit函数 import shared from "@/shared/shared"; const pagerProps = { shared, // 将全局状态传递给子应用 emitFnc: childEmit, // 从主应用下发emit函数来收集子应用反馈 pager, // 从主应用下发应用间通信呼机 }; // 存放要加载进来的系统 const apps = [ { name: "vueApp", entry: "http://localhost:10000/base/work/vue/", // 默认会加载这个HTML,解析里面的js,动态的执行 (fetch) container: "#vue", // 容器 activeRule: "/base/work/vue", // 激活的路径 props: pagerProps, // 传递参数 }, ]; registerMicroApps(apps); // 注册
子应用实现
处理完主应用的下发逻辑,下面再改造一下子应用的接收逻辑 在子应用 main.js 中写下这段代码:因为 bootstrap 在子应用的生命周期只会调用一次,因此我们把注册组件和挂载函数放在这里
export async function bootstrap({ emitFnc, pager }) {
// 把mainEmit函数一一挂载
Object.keys(emitFnc).forEach((i) => {
Vue.prototype[i] = emitFnc[i];
});
Vue.prototype.$pager = pager; // 将呼机挂载在vue实例
}