前一段时间, 写了强盛集团管理系统(基于 BPMN 引擎的工作流系统), 打算使用 qiankun 改造下项目架构, 迈向微前端, 今天开始第五章主题: 配置中心。
展示强盛集团管理系统(基于 BPMN 引擎的工作流系统)【独立部署】部分页面
基座在线网址访问
// register-apps.js registerMicroApps([ { name: "oa", entry: process.env.NODE_ENV === "production" ? "/oa/" : "//localhost:8080", activeRule: "/oa", container: "#Appmicro", loader, props: { a: 1, util: {}, globalState: {} }, }, ]);
添加子应用的入口, entry 在打生产包后, 访问子应用需要加/oa/
// main.js Vue.config.productionTip = false; let instance = null; function render(props = {}) { const { container } = props; instance = new Vue({ router, store, render: (h) => h(App), }).$mount(container ? container.querySelector("#app") : "#app"); } // 独立运行时 if (!window.__POWERED_BY_QIANKUN__) { render(); } export async function bootstrap() { console.log("[vue] oa app bootstraped"); } export async function mount(props) { render(props); } export async function unmount() { instance.$destroy(); instance.$el.innerHTML = ""; instance = null; }
主要是需要对外提供三个函数, bootstrap、mount、unmount
另外也需要通过 qiankun 标识__POWERED_BY_QIANKUN__来区分, 可以独立运行和部署
public-path.js/*eslint disable no undef*/ // 上方这一行用于eslint的忽略,因为下方代码的指向其实是不存在的,在有eslint的环境中不加入此行会报错 if (window.__POWERED_BY_QIANKUN__) { __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__; }
在基座访问子应用时, 调整静态图片等资源路径
main.js引入// main.js import "./public-path.js";
const { name } = require("./package"); module.exports = { devServer: { headers: { "Access-Control-Allow-Origin": "*", }, }, configureWebpack: { output: { library: `${name}-[name]`, libraryTarget: "umd", // 把微应用打包成 umd 库格式 jsonpFunction: `webpackJsonp_${name}`, }, }, };
在本地需要配置跨域, 及qiankun 需要 umd 格式的应用包
此时重新启动, 理论上是可以看到工作流系统了, 但还需最后一步, 添加路由.
router/modules/oa.js// 子应用菜单 import Layout from "@/components/Layouts"; const appRouter = { path: "/oa", component: Layout, redirect: "/oa/dashboard", name: "Oa", meta: { title: "强盛首页", icon: "oa-logo", }, children: [ { path: "/oa/dashboard", name: "OADashboard", meta: { title: "仪表盘", icon: "dashboard" }, }, { path: "/oa/apply/induction", name: "OASystemUser", meta: { title: "更新信息", icon: "system-user" }, }, ], }; export default appRouter;
router/indeximport OaRoute from "./modules/oa"; export const routes = [OaRoute];
因为工作流系统需要登录才能查看页面, 先在localhost:8080/login 进行登录, 就可以看到下面这样
<template> <section class="layout" :class="{ qiankun: qiankun }"> <layout-aside v-if="!qiankun"></layout-aside> <section class="layout-container"> <layout-header v-if="!qiankun"></layout-header> <layout-main></layout-main> <layout-footer v-if="!qiankun"></layout-footer> </section> </section> </template> <script> export default { data() { return { qiankun: window.__POWERED_BY_QIANKUN__, }; }, }; </script>
此时本地刷新基座就可以看到完整的了
因为工作流系统页面需要登录, 所以想要在基座直接能访问工作流系统, 则需要打通登录鉴权这部分。
基于工作流系统已经初步完版, 并不想做很大的改动, 更不想改掉原有的登录逻辑。
原有工作流已有登录、注册、填写入职信息、完善信息、修改密码等基础功能。
系统大部分基础功能能够复用,而不用重新写一遍。
好在工作流系统, 原有的逻辑是点击登录后先获取 token, 在跳转拦截时, 通过 token 去获取详情和动态菜单
那么, 只需要在基座新增登录页面调用登录接口获取 token, 将 token 分发给工作流系统(微应用), 点击跳转时, 就会根据工作流系统原有逻辑进行鉴权, 从而正常使用。
<template> <div class="login-page"> <div class="login-box"> <h2>登录</h2> <el-form :model="form" :rules="rules" ref="form"> <div class="user-box"> <el-form-item prop="phone"> <el-input v-model="form.phone" placeholder="账号"></el-input> </el-form-item> </div> <div class="user-box"> <el-form-item prop="password"> <el-input v-model="form.password" type="password" show-password placeholder="密码" @keyup.enter.native="handleLogin" > </el-input> </el-form-item> </div> <el-button class="login-btn" :loading="loading" @click="handleLogin"> <span></span> <span></span> <span></span> <span></span> 登录 </el-button> </el-form> </div> <footer class="page-footer"> <a href="https://oa.xiaoxi.work/register" target="_blank"> 👉去注册 </a> </footer> </div> </template> <script> export default { name: "BaseLogin", ... methods: { handleLogin() { this.$refs.form.validate((valid) => { if (valid) { this.loading = true; this.$store .dispatch("user/customLogin", this.form) .then((res) => { this.loading = false; // this.$router.replace("/"); this.$router.replace({ path: this.redirect || "/" }); }) .catch((err) => { this.loading = false; }); } else { return false; } }); }, }, }; </script>
utils/auth.js, 便于刷新不影响访问微应用export function getCache(TokenKey = "access_token") { return JSON.parse(localStorage.getItem(TokenKey)); } export function setCache(token, TokenKey = "access_token") { return localStorage.setItem(TokenKey, JSON.stringify(token)); } export function removeCache(TokenKey = "access_token") { return localStorage.removeItem(TokenKey); } export function clearCache() { return localStorage.clear(); }
默认新增access_token, 也可以缓存其他值
api/user.js// 封装请求 baseURL为工作流后端服务地址 import request from "@/utils/request"; // 获取oa系统的token export const login = (params) => { return request({ url: "/user/login", method: "post", data: params, }); };
request 是封装的请求函数, 但又涉及到baseURL的不同环境, 请求体与响应体拦截内容较多, 不便于过多贴代码。详细代码会放到《部署:微前端初步完成》一章。
store/modules/user.jsimport { login } from "@/api/user"; import { setCache, removeCache } from "@/utils/auth"; export default { namespaced: true, state: { userInfo: null, tokens: {}, }, mutations: { setUserInfo(state, value) { let userInfo = { ...state["userInfo"], ...value }; setCache(userInfo, `user_info`); state["userInfo"] = userInfo; }, setTokens(state, { key, token }) { setCache(token); state.tokens[key] = token; }, }, actions: { /** * 登录系统 */ customLogin({ commit }, params) { return new Promise((resolve, reject) => { login(params) .then((res) => { if (res.status === 200) { commit("setTokens", { key: "oa-token", token: res.data.token }); let userInfo = { isInduction: res.data.isInduction, oa_token: res.data.token, }; commit("setUserInfo", userInfo); resolve(res); } else { reject(res); } }) .catch((err) => { reject(err); }); }); }, logout({ commit }) { removeCache(); removeCache("user_info"); location.href = "/login"; }, }, getters: { userInfo: (state) => state.userInfo, }, };
userInfo存放用户信息, 目前为是否入职和各微应用的 token 字段, 及后续头像和用户名
此时可以点击登录, 正常跳转到首页了。
而localStorage里也缓存了两个字段
在登录实现部分,已经拿到了工作流系统的 token,现在要做的是将 token 分发给工作流系统
因为后续应用间通信会比较多, 有必要将通信部分单独抽离出来
qiankun/globalState.jsimport { initGlobalState } from "qiankun"; // 定义全局下发的数据 export const initialState = { // 当前登录用户 userInfo: null, // 全局配置 globalConfig: null, // 路由数据 routers: null, }; // 初始化全局下发的数据 export const qiankunActions = initGlobalState(initialState); qiankunActions.onGlobalStateChange((state, oldVal) => { console.log("基座接受应用 change事件", state); for (const key in state) { if (Object.prototype.hasOwnProperty.call(state, key)) { const item = state[key]; initialState[key] = item; } } console.log(initialState); });
initGlobalState(state): 定义全局状态, 并且会返回一个MicroAppStateActions 实例, 后续会用到这个实例, 需要将qiankunActions这个实例暴露出去。
简单说下 qiankun 官方 Actions 通信的几个 API
initialState: 全局需要定义的数据
initGlobalState: 定义全局状态, 并且会返回一个实例
onGlobalStateChange: 监听全局状态改变
setGlobalState: 设置全局状态
offGlobalStateChange: 取消监听状态改变
register-apps.jsimport { initialState } from "@/qiankun/globalState"; registerMicroApps([ { name: "oa", entry: process.env.NODE_ENV === "production" ? "/oa/" : "//localhost:8080", activeRule: "/oa", container: "#Appmicro", loader, props: { a: 1, util: {}, globalState: initialState }, }, ]);
Layout/index<script> import { qiankunActions } from "@/qiankun/globalState.js" mounted() { // 初始化全局下发的数据 qiankunActions.setGlobalState({ userInfo: getCache('user_info'), routers: routes, }); } </script>
放置在Layout/index, 是因为登录跳转必先加载布局, 刷新也是会加载布局。
基座中调用setGlobalState函数也会触发基座的onGlobalStateChange, onGlobalStateChange监听到变化会将新数据重新赋值到initialState。微应用在加载时就可以拿掉token。
main.js, 将 token 缓存到本地// main.js export async function mount(props) { if (props.globalState) { let userInfo = props.globalState.userInfo; setToken(userInfo.oa_token, 1); } Vue.prototype.$qiankunActions = props; render(props); }
setToken 为原工作流系统登录成功设置token函数。工作流系统可以正常访问啦。
目前基座是直接进入到基座首页的, 这种情况下, 如果直接跳转微应用页面就会报错, 则需要对基座跳转进行简单路由拦截。
plugins/permission.jsimport router from "@/router/index"; import { getCache } from "@/utils/auth"; // 登录白名单 const whiteList = ["/login"]; router.beforeEach((to, from, next) => { if (getCache()) { // to.meta.title && store.dispatch('settings/setTitle', to.meta.title); if (to.path === "/login") { next({ path: "/" }); } else { next(); } } else { if (whiteList.indexOf(to.path) !== -1) { next(); } else { const redirect = encodeURIComponent(to.fullPath); // 编码 URI,保证参数跳转回去后,可以继续带上 next(`/login?redirect=${redirect}`); // 否则全部重定向到登录页 } } });
简单路由拦截, 通过access_token字段判断是否已登录,
plugins/index.js... import "./permission";
这样即实现了简单鉴权,访问基座需要先登录,会将token分发到微应用,微应用携带token去进行后续的逻辑处理,可以正常访问基座和微应用了。
至此,强盛集团管理系统已经可以在本地正常访问。微前端项目初步完成,下一章会介绍下部署微前端项目。
目前仍存在一些bug,例如微应用间跳转不成功等,仍需要较长时间去解决,对此bug有兴趣也可以看看github
有问题可以私聊我哈。