前言
前一段时间, 写了强盛集团管理系统(基于 BPMN 引擎的工作流系统), 打算使用 qiankun 改造下项目架构, 迈向微前端, 今天开始第五章主题: 配置中心。
最终效果
展示强盛集团管理系统(基于 BPMN 引擎的工作流系统)【独立部署】部分页面
基座在线网址访问
接入工作流系统
- 基座中添加子应用的入口
js
// 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/
- 改造工作流系统
js
// 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__
来区分, 可以独立运行和部署
- 工作流系统在 src 目录新增
public-path.js
js
/*eslint disable no undef*/
// 上方这一行用于eslint的忽略,因为下方代码的指向其实是不存在的,在有eslint的环境中不加入此行会报错
if (window.__POWERED_BY_QIANKUN__) {
__webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}
在基座访问子应用时, 调整静态图片等资源路径
- 在
main.js
引入
js
// main.js
import "./public-path.js";
- 打包配置修改(vue.config.js)
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
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/index
js
import OaRoute from "./modules/oa";
export const routes = [OaRoute];
因为工作流系统需要登录才能查看页面, 先在localhost:8080/login
进行登录, 就可以看到下面这样
- 接下来, 简单调整下微应用, 通过 qiankun 标识, 判断下在基座访问时, 不展示左侧栏
html
<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 分发给工作流系统(微应用), 点击跳转时, 就会根据工作流系统原有逻辑进行鉴权, 从而正常使用。
登录实现
- 基座新增登录页面
html
<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
, 便于刷新不影响访问微应用
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
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.js
js
import { 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 分发给工作流系统
因为后续应用间通信会比较多, 有必要将通信部分单独抽离出来
- 初始化, 基座 src 下新增
qiankun/globalState.js
js
import { 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.js
js
import { 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 },
},
]);
- 将 token 分发下去, 改造基座的
Layout/index
html
<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 缓存到本地
js
// 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.js
js
import 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
js
...
import "./permission";
- 在退出登录及request请求401时, 清除token即可。
这样即实现了简单鉴权,访问基座需要先登录,会将token分发到微应用,微应用携带token去进行后续的逻辑处理,可以正常访问基座和微应用了。
总结
至此,强盛集团管理系统已经可以在本地正常访问。微前端项目初步完成,下一章会介绍下部署微前端项目。
目前仍存在一些bug,例如微应用间跳转不成功等,仍需要较长时间去解决,对此bug有兴趣也可以看看github
有问题可以私聊我哈。