vue微前端qiankun框架学习到项目实战,基座登录动态菜单及权限控制
技术无关,独立开发,独立部署,增量升级,独立运行,微前端的核心目标是将巨石应用拆解成若干可以自治的松耦合微应用。
微前端架构
效果
当前我只让基座做共享,不做其他多余业务,下面是效果结构图。
一、什么是微前端架构
微前端是一种多个团队通过独立发布功能的方式来共同构建现代化web应用的技术手段及方法策略。
微前端借鉴了微服务的架构理念,将一个庞大的前端应用才分为多个独立灵活的小型应用,每个应用都可以独立开发,独立运行,独立部署,再将这些小型应用联合为一个完整的应用。微前端既可以将多个项目融合为一,又可以减少项目之间的耦合,提升项目扩展性,相比一整块的前端仓库,微前端架构下的前端仓库倾向于更小更灵活。
二、特性
- 技术栈无关,主框架不限制介入应用的技术栈,子应用可以自主选中技术栈
2.独立开发/部署 各个团队之间仓库独立,单独部署,互不依赖
3.增量升级 当一个应用庞大之后,技术升级或重构相当的麻烦,而微应用具备渐进式升级的特性
4.基座下的子应用之间运行时互不依赖,有独立的状态管理
5.提升效率 应用越庞大,越难以维护,协作效率低下,微应用可以很好的拆分,提升效率
三、有那几种技术去实现微前端架构呢?
- iframe
也是最早最熟悉的解决方案,就是通过iframe独立运行一个项目,非常简单,无须任何改造,天然沙箱【完美隔离JS,css,都是独立运行的环境】,不限制使用,页面上可以放多个iframe来组合业务,当然也是逃不过技术的两面性,又有点就会有缺点,无法保持路由状态,刷新后路由状态丢失(这点也是完全不能解决,可以将路由)
- single-spa
single-spa是最早的微前端框架,可以兼容很多技术栈。
single-spa首先在基座中注册所有子应用的路由,当URL改变时就会去进行匹配,匹配到哪个子应用就会去加载对应的那个子应用。相对于iframe的实现方案,single-spa中基座和各个子应用之间共享着一个全局上下文,并且不存在URL不同步和UI不同步的情况,但是single-spa没有实现js隔离和css隔离,需要修改大量的配置,包括基座和子应用的,不能开箱即用
- qiankun
qiankun是阿里开源的一个微前端的框架,在阿里内部已经经过一批线上应用的充分检验及打磨了,所以可以放心使用。
基于single-spa封装的,提供了更加开箱即用的API,技术栈无关,任意技术栈的应用均可使用/接入,不论是
React/Vue/Angular/JQuery 还是其他等框架。 HTML
Entry的方式接入,像使用iframe一样简单,实现了single-spa不具备的样式隔离和js隔离
资源预加载,在浏览器空闲时间预加载未打开的微应用资源,加速微应用打开速度。
四、Qiankun用到的API介绍
registerMicroApps(apps,lifeCycles?): 自动加载模块,一次性写好配置,直接传入,然后调用start(),qiankun会自动监听UR了变化调用对应用的暴露的生命周期函数。
start(opts?): 配合registerMicroApps使用,当调用registerMicroApps后,运行启动。
loadMicroApp(app,configuration?) :手动加载模块,需要自己监听URL并手动加载模块。
addGlobalUncaughtErrorHandler(handler)/removeGlobalUncaughtErrorHandler(handler): 添加/移除监听应用加载错误。
initGlobalState(state) 初始化全局共享状态,类似于vuex,返回两个方法。
setGlobalState(state) 设置全局状态。
onGlobalStateChange((newState,oldState)=>{}) 监听全局状态变化。
五、qiankun注册子应用参数说明
例如:
六、qiankun框架适合项目结构
| -- 主体文件
| -- .git
| -- common // 公共模板
| -- main // 主应用
| -- package.json
| -- aps // aps应用
| -- package.json
| -- wms // wms应用
| -- package.json
| -- 基础设置 // 基础设置应用
| -- package.json
七、路由设计
项目是有一个登录页的,但是登录页不加载子应用,只有通过登录成功后,跳到第一个页面,才进行加载子应用的。
八、一起运行和独立运行
- 一起运行
公用菜单模块,公用页头,公用页底部模块,只有内容改变,登陆成功后内容页面跳转到对应的子应用页面的内容中。登陆成功后默认跳转到启动页面,通过全局路由守卫进行判断,判断跳转到这个路由,根据获取路由表数据,再跳入到路由表的第一个路由,如果路由表没有数据,则代表这个用户没有菜单,那就没有访问权限,直接调回路由页面。
注意:主应用登录成功后,把路由存到全局状态中,除了主应用addRoute添加路由外。处理路由可以通过路由守卫获取所有菜单后,然后通过判断前缀,把相应的子应用通过apps配置的props传递进去。
还可以在每个子应用第一次运行的时候,在全局路由守卫判断是否是第一次运行的,直接获取全局状态里的路由表,循环判断是否属于当前子应用的路由再addRoute进去。
- 独立运行
是指应用独立运行,运行后登录页,基础模块,包括菜单,注销,还能正常使用,这个时候就需要吧登录页,菜单,App三个模块迁到common模块,通过引入的方式,然后根据window.__PROWERED_BY_QIANKUN__判断当前运行环境是否独立运行做相对应的逻辑处理,window.PROWERED_BY_QIANKUN===true为一起运行,window.PROWERED_BY_QIANKUN===false为独立运行。
九、基于qiankun的一个Demo实例
(1). Demo文件结构
(2). 主应用配置,基座
基座app-main采用的是vue-cli3搭建,它用来导航渲染和登陆的下发,为子应用提供一个挂载的容器div,基座应该保持简洁,不做过多的业务操作,qiankun只要在基座中引入,再main.js中注册子应用即可。
- 引入 registerMicroApps,start,setDefaultMountApp
import { registerMicroApps,start,setDefaultMountApp } from 'qiankun'
- 注册子应用
// 1. 注册子应用
registerMicroApps([
{
name:"open-platform-admin",
entry:"//localhost:81/", //子应用页面访问入口
container:"#subapp-container", //子应用渲染的出口
activeRule:"/open-platform-admin", // 路径匹配规则, // 路径匹配规则
sandbox:{
strictStyleIsolation: true, // 开启样式隔离
routerBase: '/open-platform-admin',
}
},
])
- 默认打开子应用
setDefaultMountApp("/") //默认打开子应用
- 启动微前端架构
start()
-
在 App.vue中,需要声明 main.js配置的子应用挂载div(注意id一定要一致),以及基座布局相关的,我当前使用的是layout,所以配置再AppMain.vue中,这样,基座就算配置完成了。项目启动后,子应用将会挂载到
<div id="subapp-container"></div>
中。
-
子应用配置
在根目录下创建一个子应用,open-admin-web,子应用最好与在基座主应用main.js中配置的名称一致,这样可以直接使用package.json中的name作为output。
vue.config.js,devServer的端口改为与主营用配置的一致,且加上跨域headers和output配置。
新增src/public-path.jsif (window.__POWERED_BY_QIANKUN__) { __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__; }
src/router/index.js改为只暴露routes, new Router改到 main.js中声明,并改造main.js,并引入src下创建的public-path.js,改写render,添加生命周期函数,最终结果如下⬇
import './public-path';
import Vue from 'vue'
import Cookies from 'js-cookie'
import Element from 'element-ui'
import './assets/styles/element-variables.scss'
// import 'ant-design-vue/dist/antd.css';
import "ant-design-vue/dist/antd.less"
import '@/assets/styles/index.scss' // global css
import '@/assets/styles/ruoyi.scss' // ruoyi css
import App from './App'
import store from './store'
import router from './router'
import directive from './directive' // directive
import plugins from './plugins' // plugins
import { download } from '@/utils/request'
import './assets/icons' // icon
import './permission' // permission control
import { getDicts } from "@/api/system/dict/data";
import { getConfigKey } from "@/api/system/config";
import { parseTime, resetForm, addDateRange, selectDictLabel, selectDictLabels, handleTree } from "@/utils/ruoyi";
// 分页组件
import Pagination from "@/components/Pagination";
// 自定义表格工具组件
import RightToolbar from "@/components/RightToolbar"
// 富文本组件
import Editor from "@/components/Editor"
// 文件上传组件
import FileUpload from "@/components/FileUpload"
// 图片上传组件
import ImageUpload from "@/components/ImageUpload"
// 图片预览组件
import ImagePreview from "@/components/ImagePreview"
// 字典标签组件
import DictTag from '@/components/DictTag'
// 头部标签组件
import VueMeta from 'vue-meta'
// 字典数据组件
import DictData from '@/components/DictData'
//局部使用antDesign-vue中的tree组件
import { Tree } from 'ant-design-vue';
import { Table } from 'ant-design-vue';
import { Icon } from 'ant-design-vue';
// 全局方法挂载
Vue.prototype.getDicts = getDicts
Vue.prototype.getConfigKey = getConfigKey
Vue.prototype.parseTime = parseTime
Vue.prototype.resetForm = resetForm
Vue.prototype.addDateRange = addDateRange
Vue.prototype.selectDictLabel = selectDictLabel
Vue.prototype.selectDictLabels = selectDictLabels
Vue.prototype.download = download
Vue.prototype.handleTree = handleTree
// 全局组件挂载
Vue.component('DictTag', DictTag)
Vue.component('Pagination', Pagination)
Vue.component('RightToolbar', RightToolbar)
Vue.component('Editor', Editor)
Vue.component('FileUpload', FileUpload)
Vue.component('ImageUpload', ImageUpload)
Vue.component('ImagePreview', ImagePreview)
Vue.component('ATree', Tree)
Vue.component('ATable', Table)
Vue.component('AIcon', Icon)
Vue.use(directive)
Vue.use(plugins)
Vue.use(VueMeta)
DictData.install()
/**
* If you don't want to use mock-server
* you want to use MockJs for mock api
* you can execute: mockXHR()
*
* Currently MockJs will be used in the production environment,
* please remove it before going online! ! !
*/
Vue.use(Element, {
size: Cookies.get('size') || 'medium' // set element-ui default size
})
let instance = null
async function render(props={}){
const { container } = props;
instance = new Vue({
router,
store,
render: h => h(App)
}).$mount(
container
?
container.querySelector('#app') //渲染到主应用的入口
:'#app' //独立运行的时候
)
}
// 在被qiankun引用时 修改运行时的 `publicPath`
if (window.__POWERED_BY_QIANKUN__) {
__webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}
//如果独立运行的时候,判断是否是独立运行
if (!window.__POWERED_BY_QIANKUN__) {
render();
}
/**
* 子应用建议使用qiankun的规则来接入不需要安装任何依赖,
* 只需要再三个入口到二u三个必须的钩子函数给qiankun主应用使用
* 钩子函数必须返回promise(启动的时候调用)
*/
export async function bootstrap() {
console.log('[vue] vue app bootstraped');
}
export async function mount(props) {
// console.log('[vue] props from main framework333333', props);
render(props);
}
/**
* 可选生命周期钩子,仅使用 loadMicroApp 方式加载微应用时生效
*/
export async function update(props) {
console.log('update props', props);
}
export async function unmount() {
instance.$destroy();
instance.$el.innerHTML = '';
instance = null;
}
export default instance;
- 配置基座路由
{
path: '/open-platform-admin',
component: Layout,
redirect: '/open-platform-admin/user',
name: 'open-platform-admin',
meta: { title: '用户管理', icon: 'el-icon-notebook-2' },
children: [
{
path: 'user/user',
name: 'user',
// component: () => import('@/views/article/index'),
meta: { title: '用户管理', icon: 'el-icon-notebook-1' }
},
]
},
- Demo效
十、使用官方提供的props实现父子应用通讯问题
父组件main.js,注册子应用的时候直接把父组件的sotre都传递给子组件
// 1. 注册微应用
registerMicroApps([
{
name: 'son',
entry: '//localhost:8999/', // 子应用页面访问入口
container: '#subapp-container', // 子应用渲染的出口
activeRule: '/open-platform-admin', // 路径匹配规则
sandbox: {
strictStyleIsolation: true, // 开启样式隔离
routerBase: '/open-platform-admin'
},
props:{sharedStore:store}
}
])
子组件判断当前是否是微服务打开状态,接收父组件传递过来的props
async function render(props={}){
// console.log('micro-app-test-vue')
const { container } = props;
instance = new Vue({
router,
store,
render: h => h(App),
beforeCreate(){
if (window.__POWERED_BY_QIANKUN__) {
store.state.user = props.sharedStore.state.user
}
}
}).$mount(
container
?
container.querySelector('#app') //渲染到主应用的入口
:'#app' //独立运行的时候
)
}
基座登陆后,将token传递给子应用,由于父子应用都共享一个store实例,所以父应用做修改,子应用也做相应的修改。
在父组件和子组件中打印用户名和token
部署中遇到的问题
基座,子应用都有属于自己的base基础路径,基座访问子应用的时候出现404问题。
假如基座的基础路径为 “hsk-admin”,子应用的基础路径为 “business-module”,再后端部署不使用端口号的情况下,基座访问子应用的时候会出现地址为:http://192.168.10.205/hsk-admin/business-module/tenant/tenant
,就会出现/hsk-admin/business-module/
在子应用中找不到该路由,报404
的错误
方法:加载子应用的时候将及做的base
传递给子应用,子应用拿到后根据当前是否是qiankun
环境进行子路由的base
设置。
访问子应用的时候,子应用没有hsk-admin/business-module/
路径就会出现
基座路由配置:
import Vue from 'vue'
import Router from 'vue-router'
Vue.use(Router)
/* Layout */
import Layout from '@/layout'
/**
* Note: 路由配置项
*
* hidden: true // 当设置 true 的时候该路由不会再侧边栏出现 如401,login等页面,或者如一些编辑页面/edit/1
* alwaysShow: true // 当你一个路由下面的 children 声明的路由大于1个时,自动会变成嵌套的模式--如组件页面
* // 只有一个时,会将那个子路由当做根路由显示在侧边栏--如引导页面
* // 若你想不管路由下面的 children 声明的个数都显示你的根路由
* // 你可以设置 alwaysShow: true,这样它就会忽略之前定义的规则,一直显示根路由
* redirect: noRedirect // 当设置 noRedirect 的时候该路由在面包屑导航中不可被点击
* name:'router-name' // 设定路由的名字,一定要填写不然使用<keep-alive>时会出现各种问题
* query: '{"id": 1, "name": "ry"}' // 访问路由的默认传递参数
* roles: ['admin', 'common'] // 访问路由的角色权限
* permissions: ['a:a:a', 'b:b:b'] // 访问路由的菜单权限
* meta : {
noCache: true // 如果设置为true,则不会被 <keep-alive> 缓存(默认 false)
title: 'title' // 设置该路由在侧边栏和面包屑中展示的名字
icon: 'svg-name' // 设置该路由的图标,对应路径src/assets/icons/svg
breadcrumb: false // 如果设置为false,则不会在breadcrumb面包屑中显示
activeMenu: '/system/user' // 当路由设置了该属性,则会高亮相对应的侧边栏。
}
*/
// 公共路由
export const constantRoutes = [
{
path: '/business-module',
component: Layout,
name: 'business-module',
meta: { title: '业务系统', icon: 'tool' },
children: [
{
path: 'tenant/tenant',
name: 'tenant',
meta: { title: '租户管理', noCache:true,icon: 'tool' }
},
{
path: 'applicationManagement/applicationManagement',
name: 'applicationManagement',
meta: { title: '应用设置', noCache:true,icon: 'tool' }
},
]
},
]
// 动态路由,基于用户权限动态去加载
export const dynamicRoutes = [
]
// 防止连续点击多次路由报错
let routerPush = Router.prototype.push;
let routerReplace = Router.prototype.replace;
// push
Router.prototype.push = function push(location) {
return routerPush.call(this, location).catch(err => err)
}
// replace
Router.prototype.replace = function push(location) {
return routerReplace.call(this, location).catch(err => err)
}
export default new Router({
mode: 'history', // 去掉url中的#
base: 'hsk-admin',
scrollBehavior: () => ({ y: 0 }),
routes: constantRoutes
})
基座vue.config.js
publicPath: process.env.NODE_ENV === 'production' ? '/' : '/hsk-admin',
基座注册子应用配置
registerMicroApps([
{
name: 'son',
entry: '//localhost:8999/business-module/', // 子应用页面访问入口
container: '#subapp-container', // 子应用渲染的出口
activeRule: '/hsk-admin/business-module', // 路径匹配规则
sandbox: {
strictStyleIsolation: true, // 开启样式隔离
},
props:{sharedStore:store,baseName:'/hsk-admin/business-module'}
},
])
hsk-admin为基座的基础路径,business-module为子应用的基础路径,如果在基座中访问必须设置子应用的路由base为/hsk-admin/business-module/
子应用的main.js配置
import './public-path';
import Vue from 'vue'
import Cookies from 'js-cookie'
import Element from 'element-ui'
import './assets/styles/element-variables.scss'
// import 'ant-design-vue/dist/antd.css';
import "ant-design-vue/dist/antd.less"
import '@/assets/styles/index.scss' // global css
import '@/assets/styles/ruoyi.scss' // ruoyi css
import App from './App'
import store from './store'
import router, {constantRoutes} from './router'
import directive from './directive' // directive
import plugins from './plugins' // plugins
//引入hsk组件
import hskui from "hsk-ui"
//引入hsk方法
import { hskMsgbox } from 'hsk-ui/commonUtils'
import { download } from '@/utils/request'
import './assets/icons' // icon
import './permission' // permission control
import { getDicts } from "@/api/system/dict/data";
import { getConfigKey } from "@/api/system/config";
import { parseTime, resetForm, addDateRange, selectDictLabel, selectDictLabels, handleTree } from "@/utils/ruoyi";
// 分页组件
import Pagination from "@/components/Pagination";
// Vue.prototype.hskMsgbox = hskui.hskMsgbox.hskMsgbox
// 自定义表格工具组件
import RightToolbar from "@/components/RightToolbar"
// 富文本组件
import Editor from "@/components/Editor"
// 文件上传组件
import FileUpload from "@/components/FileUpload"
// 图片上传组件
import ImageUpload from "@/components/ImageUpload"
// 图片预览组件
import ImagePreview from "@/components/ImagePreview"
// 字典标签组件
import DictTag from '@/components/DictTag'
// 头部标签组件
import VueMeta from 'vue-meta'
// 字典数据组件
import DictData from '@/components/DictData'
// import action from '../src/action'
//局部使用antDesign-vue中的tree组件
import { Tree } from 'ant-design-vue';
import { Table } from 'ant-design-vue';
import { Icon } from 'ant-design-vue';
import Router from "vue-router";
// 全局方法挂载
Vue.prototype.hskMsgbox = hskMsgbox
Vue.prototype.getDicts = getDicts
Vue.prototype.getConfigKey = getConfigKey
Vue.prototype.parseTime = parseTime
Vue.prototype.resetForm = resetForm
Vue.prototype.addDateRange = addDateRange
Vue.prototype.selectDictLabel = selectDictLabel
Vue.prototype.selectDictLabels = selectDictLabels
Vue.prototype.download = download
Vue.prototype.handleTree = handleTree
// 全局组件挂载
Vue.component('DictTag', DictTag)
Vue.component('Pagination', Pagination)
Vue.component('RightToolbar', RightToolbar)
Vue.component('Editor', Editor)
Vue.component('FileUpload', FileUpload)
Vue.component('ImageUpload', ImageUpload)
Vue.component('ImagePreview', ImagePreview)
Vue.component('ATree', Tree)
Vue.component('ATable', Table)
Vue.component('AIcon', Icon)
Vue.use(directive)
Vue.use(plugins)
Vue.use(VueMeta)
DictData.install()
/**
* If you don't want to use mock-server
* you want to use MockJs for mock api
* you can execute: mockXHR()
*
* Currently MockJs will be used in the production environment,
* please remove it before going online! ! !
*/
Vue.use(Element, {
size: Cookies.get('size') || 'medium' // set element-ui default size
})
Vue.use(hskui)
let instance = null
Cookies.set("client_id","admin")
async function render(props={}){
const { container } = props;
instance = new Vue({
router,
store,
render: h => h(App),
beforeCreate(){
if (window.__POWERED_BY_QIANKUN__) {
store.state.user = props.sharedStore.state.user
}
}
}).$mount(
container
?
container.querySelector('#app') //渲染到主应用的入口
:'#app' //独立运行的时候
)
}
// 在被qiankun引用时 修改运行时的 `publicPath`
if (window.__POWERED_BY_QIANKUN__) {
__webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}
//如果独立运行的时候,判断是否是独立运行
if (!window.__POWERED_BY_QIANKUN__) {
render();
}
/**
* 子应用建议使用qiankun的规则来接入不需要安装任何依赖,
* 只需要再三个入口到二u三个必须的钩子函数给qiankun主应用使用
* 钩子函数必须返回promise(启动的时候调用)
*/
export async function bootstrap() {
// console.log('[vue] vue app bootstraped');
}
// 从生命周期 mount 中获取通信方法,props默认会有onGlobalStateChange和setGlobalState两个api
export async function mount(props) {
// console.log('乾坤子应用容器加载完成,开始渲染 child',props)
if (window.__POWERED_BY_QIANKUN__) {
if(router.options.base !== props.baseName){
const { container } = props;
// 获取容器元素,用于后续操作或设置环境变量等
let rootRoute = new Router({
mode: 'history', // 去掉url中的#
base: props.baseName,
scrollBehavior: () => ({y: 0}),
routes: constantRoutes
})
instance = new Vue({
router:rootRoute,
store,
render: h => h(App),
beforeCreate(){
if (window.__POWERED_BY_QIANKUN__) {
store.state.user = props.sharedStore.state.user
}
}
}).$mount(
container
?
container.querySelector('#app') //渲染到主应用的入口
:'#app' //独立运行的时候
)
} else {
render(props);
}
} else {
render(props);
}
}
/**
* 可选生命周期钩子,仅使用 loadMicroApp 方式加载微应用时生效
*/
export async function update(props) {
}
export async function unmount() {
instance.$destroy();
instance.$el.innerHTML = '';
instance = null;
}
export default instance;
子应用配置其中主要代码:
export async function mount(props) {
// console.log('乾坤子应用容器加载完成,开始渲染 child',props)
if (window.__POWERED_BY_QIANKUN__) {
if(router.options.base !== props.baseName){
const { container } = props;
// 获取容器元素,用于后续操作或设置环境变量等
let rootRoute = new Router({
mode: 'history', // 去掉url中的#
base: props.baseName,
scrollBehavior: () => ({y: 0}),
routes: constantRoutes
})
instance = new Vue({
router:rootRoute,
store,
render: h => h(App),
beforeCreate(){
if (window.__POWERED_BY_QIANKUN__) {
store.state.user = props.sharedStore.state.user
}
}
}).$mount(
container
?
container.querySelector('#app') //渲染到主应用的入口
:'#app' //独立运行的时候
)
} else {
render(props);
}
} else {
render(props);
}
}
如果当前属于qiankun模块,则不执行render,自己手动创建vue router进行更改即可。
子应用的vue.config.js
publicPath: process.env.NODE_ENV === "production" ? "/" : "/system-module",
部署完成效果:
部署到真实环境去除路由静态地址前缀,防止生产环境出现刷新404问题
基座配置
基座router/index.js配置
还有一些跳出登录,退出登录地址去除hsk-admin
前缀
其他子应用不变,因为子应用接收传递过来的baseURL进行改变的所以不需要改动。
权限控制效果
基座登录动态菜单及权限控制
难点就是在于基座配置子应用的路由
permission全部代码:
import auth from '@/plugins/auth'
import router, { constantRoutes, dynamicRoutes } from '@/router'
import { getRouters } from '@/api/menu'
import Layout from '@/layout/index'
import ParentView from '@/components/ParentView'
import InnerLink from '@/layout/components/InnerLink'
import store from '../../store'
const permission = {
state: {
routes: [],
addRoutes: [],
defaultRoutes: [],
topbarRouters: [],
// sidebarRouters: []
sidebarRouters: [],
permissions: [],
},
mutations: {
SET_PERMISSIONS: (state, permissions) => {
state.permissions = permissions
},
SET_ROUTES: (state, routes) => {
state.addRoutes = routes
state.routes = constantRoutes.concat(routes)
},
SET_DEFAULT_ROUTES: (state, routes) => {
state.defaultRoutes = constantRoutes.concat(routes)
},
SET_TOPBAR_ROUTES: (state, routes) => {
state.topbarRouters = routes
},
SET_SIDEBAR_ROUTERS: (state, routes) => {
state.sidebarRouters = routes
},
},
actions: {
// 生成路由
GenerateRoutes({ commit }) {
return new Promise(resolve => {
// // 向后端请求路由数据
// getRouters().then(res => {
const res1 = {
"msg": "操作成功",
"code": 200,
"data": [
{
"path": "/business-module-vue2/equipment",
"redirect": "noRedirect",
"component": "Layout",
"meta": {
"title": "检测设备管理",
"noCache": false,
"icon": 'zhgl',
"link": null
},
"children": [
{
"name": "equipment",
"path": "/business-module-vue2/equipment/equipment",
"hidden": false,
"redirect": "noRedirect",
"meta": {
"title": "检测设备管理",
"icon": 'yygl',
"noCache": false,
"link": null
}
},
]
}
]
}
const res2 = {
"msg": "操作成功",
"code": 200,
"data": [
{
"path": "/business-module-vue2",
"redirect": "noRedirect",
"component": "Layout",
"meta": {
"title": "业务系统",
"noCache": false,
"icon": 'zhgl',
"link": null
},
"children": [
{
"name": "zhanghao",
"path": "/business-module-vue2/zhanghao",
"hidden": false,
"redirect": "noRedirect",
"meta": {
"title": "账号管理",
"icon": 'zhgl',
"noCache": false,
"link": null
}
},
{
"name": "tenant",
"path": "/business-module-vue2/tenant",
"hidden": false,
"redirect": "noRedirect",
"meta": {
"title": "企业管理",
"icon": 'zhgl',
"noCache": false,
"link": null
}
},
{
"name": "shenhe",
"path": "/business-module-vue2/shenhe",
"hidden": false,
"redirect": "noRedirect",
"meta": {
"title": "认证审核",
"icon": 'zhgl',
"noCache": false,
"link": null
}
},
]
},
{
"path": "/business-module-vue2/productListA",
"redirect": "noRedirect",
"component": "Layout",
"meta": {
"title": "资源中心",
"noCache": false,
"icon": 'zhgl',
"link": null
},
"children": [
{
"name": "productList",
"path": "/business-module-vue2/productList",
"hidden": false,
"redirect": "noRedirect",
"meta": {
"title": "产品列表",
"icon": 'zhgl',
"noCache": false,
"link": null
}
},
{
"name": "resourceList",
"path": "/business-module-vue2/resourceList",
"hidden": false,
"redirect": "noRedirect",
"meta": {
"title": "资源列表",
"icon": 'zhgl',
"noCache": false,
"link": null
}
},
{
"name": "viewProductDetail",
"path": "/business-module-vue2/viewProductDetail",
"hidden": true,
"redirect": "noRedirect",
"meta": {
"title": "产品详情",
"icon": 'zhgl',
"noCache": false,
"link": null
}
},
{
"name": "productDetail",
"path": "/business-module-vue2/productDetail",
"hidden": true,
"redirect": "noRedirect",
"meta": {
"title": "产品详情",
"icon": 'zhgl',
"noCache": false,
"link": null
}
},
]
},
{
"path": "/system-module-vue2/system",
"redirect": "noRedirect",
"component": "Layout",
"meta": {
"title": "系统设置",
"noCache": false,
"icon": 'zhgl',
"link": null
},
"children": [
{
"name": "user",
"path": "/system-module-vue2/system/user",
"hidden": false,
"redirect": "noRedirect",
"meta": {
"title": "用户管理",
"icon": 'zhgl',
"noCache": false,
"link": null
}
},
{
"name": "role",
"path": "/system-module-vue2/system/role",
"hidden": false,
"redirect": "noRedirect",
"meta": {
"title": "角色管理",
"icon": 'zhgl',
"noCache": false,
"link": null
}
},
{
"name": "codeManagement",
"path": "/system-module-vue2/system/codeManagement",
"hidden": false,
"redirect": "noRedirect",
"meta": {
"title": "编码管理",
"icon": 'zhgl',
"noCache": false,
"link": null
}
},
{
"name": "log",
"path": "/system-module-vue2/system/log",
"hidden": false,
"redirect": "noRedirect",
"meta": {
"title": "操作日志",
"icon": 'zhgl',
"noCache": false,
"link": null
}
},{
"name": "dictionaryMiddle",
"path": "/system-module-vue2/system/dictionaryMiddle",
"hidden": false,
"redirect": "noRedirect",
"meta": {
"title": "数据字典管理",
"icon": 'zhgl',
"noCache": false,
"link": null
}
},{
"name": "configInformation",
"path": "/system-module-vue2/system/configInformation",
"hidden": false,
"redirect": "noRedirect",
"meta": {
"title": "配置信息",
"icon": 'zhgl',
"noCache": false,
"link": null
}
},{
"name": "templateMiddle",
"path": "/system-module-vue2/system/templateMiddle",
"hidden": false,
"redirect": "noRedirect",
"meta": {
"title": "消息模板管理",
"icon": 'zhgl',
"noCache": false,
"link": null
}
},
{
"name": "serverLog",
"path": "/system-module-vue2/system/serverLog",
"hidden": false,
"redirect": "noRedirect",
"meta": {
"title": "服务调用日志",
"icon": 'zhgl',
"noCache": false,
"link": null
}
},{
"name": "serverLogDetail",
"path": "/system-module-vue2/system/serverLogDetail",
"hidden": false,
"redirect": "noRedirect",
"meta": {
"title": "服务使用情况",
"icon": 'zhgl',
"noCache": false,
"link": null
}
},
]
},{
"path": "/business-module-vue2/gatewayAdministration",
"redirect": "noRedirect",
"component": "Layout",
"meta": {
"title": "物联网中心",
"noCache": false,
"icon": 'zhgl',
"link": null
},
"children": [
{
"name": "gatewayAdministration",
"path": "/business-module-vue2/gatewayAdministration",
"hidden": false,
"redirect": "noRedirect",
"meta": {
"title": "网关管理",
"icon": 'zhgl',
"noCache": false,
"link": null
}
},
]
},
]
}
console.log("store",store.getters.adminID)
let res = {}
if(localStorage.getItem('adminId') === '1'){
res = res2
}else{
res = res1
}
//遍历菜单树,将菜单树下的所有按钮权限拿到,并使用v-permissions方法比对是否有按钮权限
// commit('SET_PERMISSIONS', getAllPermissions(res.data,[]))
const sdata = JSON.parse(JSON.stringify(res.data))
const rdata = JSON.parse(JSON.stringify(res.data))
const sidebarRoutes = filterAsyncRouter(sdata)
const rewriteRoutes = filterAsyncRouter(rdata, false, true)
const asyncRoutes = filterDynamicRoutes(dynamicRoutes);
rewriteRoutes.push({ path: '*', redirect: '/404', hidden: true })
router.addRoutes(asyncRoutes);
commit('SET_ROUTES', rewriteRoutes)
commit('SET_SIDEBAR_ROUTERS', constantRoutes.concat(sidebarRoutes))
commit('SET_DEFAULT_ROUTES', sidebarRoutes)
commit('SET_TOPBAR_ROUTES', sidebarRoutes)
resolve(rewriteRoutes)
// })
})
}
}
}
function getAllPermissions(tree, result) {
//遍历树 获取id数组
for (let i = 0; i < tree.length; i++) {
if (tree[i].meta.permission !== null) {
result.push(...tree[i].meta.permission)
}
if (typeof (tree[i].children) !== "undefined" && tree[i].children !== null && tree[i].children.length > 0) {
getAllPermissions(tree[i].children, result);
}
}
return result;
}
// 遍历后台传来的路由字符串,转换为组件对象
function filterAsyncRouter(asyncRouterMap, lastRouter = false, type = false) {
return asyncRouterMap.filter(route => {
if (type && route.children) {
route.children = filterChildren(route.children)
}
if (route.component) {
// Layout ParentView 组件特殊处理
if (route.component === 'Layout') {
route.component = Layout
} else if (route.component === 'ParentView') {
route.component = ParentView
} else if (route.component === 'InnerLink') {
route.component = InnerLink
} else {
route.component = loadView(route.component)
}
}
if (route.children != null && route.children && route.children.length) {
route.children = filterAsyncRouter(route.children, route, type)
} else {
delete route['children']
delete route['redirect']
}
return true
})
}
function filterChildren(childrenMap, lastRouter = false) {
var children = []
childrenMap.forEach((el, index) => {
if (el.children && el.children.length) {
if (el.component === 'ParentView' && !lastRouter) {
console.log("~~~~~~~~~",c)
el.children.forEach(c => {
c.path = el.path + '/' + c.path
if (c.children && c.children.length) {
children = children.concat(filterChildren(c.children, c))
return
}
children.push(c)
})
return
}
}
if (lastRouter) {
el.path = lastRouter.path + '/' + el.path
}
children = children.concat(el)
})
return children
}
// 动态路由遍历,验证是否具备权限
export function filterDynamicRoutes(routes) {
const res = []
routes.forEach(route => {
if (route.permissions) {
if (auth.hasPermiOr(route.permissions)) {
res.push(route)
}
} else if (route.roles) {
if (auth.hasRoleOr(route.roles)) {
res.push(route)
}
}
})
return res
}
export const loadView = (view) => {
if (process.env.NODE_ENV === 'development') {
return (resolve) => require([`@/views/${view}`], resolve)
} else {
// 使用 import 实现生产环境的路由懒加载
return () => import(`@/views/${view}`)
}
}
export default permission
main.js通过登录获取的信息传递给子应用
import Vue from 'vue'
import Cookies from 'js-cookie'
import Element from 'element-ui'
import './assets/styles/element-variables.scss'
import { registerMicroApps, start, setDefaultMountApp } from 'qiankun';
// import 'ant-design-vue/dist/antd.css';
import "ant-design-vue/dist/antd.less"
import 'default-passive-events'
import '@/assets/styles/index.scss' // global css
import '@/assets/styles/ruoyi.scss' // ruoyi css
import App from './App'
import store from './store'
import router from './router'
import directive from './directive' // directive
import plugins from './plugins' // plugins
import { download } from '@/utils/request'
import './assets/icons' // icon
import './permission' // permission control
import { getDicts } from "@/api/system/dict/data";
import { getConfigKey } from "@/api/system/config";
// import {userApi} from "hskCommApi"
// console.log("asdasd",userApi.login)
import { parseTime, resetForm, addDateRange, selectDictLabel, selectDictLabels, handleTree } from "@/utils/ruoyi";
// 分页组件
import Pagination from "@/components/Pagination";
// 自定义表格工具组件
import RightToolbar from "@/components/RightToolbar"
// 富文本组件
import Editor from "@/components/Editor"
// 文件上传组件
import FileUpload from "@/components/FileUpload"
// 图片上传组件
import ImageUpload from "@/components/ImageUpload"
// 图片预览组件
import ImagePreview from "@/components/ImagePreview"
// 字典标签组件
import DictTag from '@/components/DictTag'
// 头部标签组件
import VueMeta from 'vue-meta'
// 字典数据组件
import DictData from '@/components/DictData'
//局部使用antDesign-vue中的tree组件
import { Tree } from 'ant-design-vue';
import { Table } from 'ant-design-vue';
import { Icon } from 'ant-design-vue';
// import a from "hskCommApi"
// 全局方法挂载
Vue.prototype.getDicts = getDicts
Vue.prototype.getConfigKey = getConfigKey
Vue.prototype.parseTime = parseTime
Vue.prototype.resetForm = resetForm
Vue.prototype.addDateRange = addDateRange
Vue.prototype.selectDictLabel = selectDictLabel
Vue.prototype.selectDictLabels = selectDictLabels
Vue.prototype.download = download
Vue.prototype.handleTree = handleTree
// 全局组件挂载
Vue.component('DictTag', DictTag)
Vue.component('Pagination', Pagination)
Vue.component('RightToolbar', RightToolbar)
Vue.component('Editor', Editor)
Vue.component('FileUpload', FileUpload)
Vue.component('ImageUpload', ImageUpload)
Vue.component('ImagePreview', ImagePreview)
Vue.component('ATree', Tree)
Vue.component('ATable', Table)
Vue.component('AIcon', Icon)
Vue.use(directive)
Vue.use(plugins)
Vue.use(VueMeta)
DictData.install()
/**
* If you don't want to use mock-server
* you want to use MockJs for mock api
* you can execute: mockXHR()
*
* Currently MockJs will be used in the production environment,
* please remove it before going online! ! !
*/
Vue.use(Element, {
size: Cookies.get('size') || 'medium' // set element-ui default size
})
// 1. 注册微应用
// http://192.168.10.205/system-module-vue2/index
registerMicroApps([
{
name: 'son',
entry: process.env.VUE_APP_BUSINESS, // 子应用页面访问入口
container: '#subapp-container', // 子应用渲染的出口
activeRule: '/hsk-admin/business-module-vue2', // 路径匹配规则
sandbox: {
strictStyleIsolation: true, // 开启样式隔离
},
props:{sharedStore:store,baseName:'/hsk-admin/business-module-vue2'}
},
{
name: 'son2',
entry: process.env.VUE_APP_SYSTEM_URL, // 子应用页面访问入口
container: '#subapp-container', // 子应用渲染的出口
activeRule: '/hsk-admin/system-module-vue2', // 路径匹配规则
sandbox: {
strictStyleIsolation: true, // 开启样式隔离
},
props:{sharedStore:store,baseName:'/hsk-admin/system-module-vue2'}
},
])
// 2.默认打开子应用
// setDefaultMountApp('/')
// // 3.启动qiankun框架
// start()
// 判断subapp-container是否已加载,如果未加载就延迟
function ensureContainerAndStartMicroApps() {
if (document.getElementById('subapp-container')) {
// 容器存在,可以注册微应用并启动
// registerMicroApps([...]); // 注册微应用的代码
setDefaultMountApp('/'); // 默认打开的子应用
start(); // 启动 qiankun
} else {
// 容器尚不存在,稍后重试
setTimeout(ensureContainerAndStartMicroApps, 100); // 100毫秒后再次尝试
}
}
// 确保 DOMContentLoaded 事件触发后再执行
document.addEventListener('DOMContentLoaded', ensureContainerAndStartMicroApps);
Vue.config.productionTip = false
new Vue({
el: '#app',
router,
store,
render: h => h(App)
})
子应用接收处理
// 从生命周期 mount 中获取通信方法,props默认会有onGlobalStateChange和setGlobalState两个api
export async function mount(props) {
// console.log('乾坤子应用容器加载完成,开始渲染 child',props)
if (window.__POWERED_BY_QIANKUN__) {
if(router.options.base !== props.baseName){
const { container } = props;
// 获取容器元素,用于后续操作或设置环境变量等
let rootRoute = new Router({
mode: 'history', // 去掉url中的#
base: props.baseName,
scrollBehavior: () => ({y: 0}),
routes: constantRoutes
})
instance = new Vue({
router:rootRoute,
store,
render: h => h(App),
beforeCreate(){
if (window.__POWERED_BY_QIANKUN__) {
store.state.user = props.sharedStore.state.user
}
}
}).$mount(
container
?
container.querySelector('#app') //渲染到主应用的入口
:'#app' //独立运行的时候
)
} else {
render(props);
}
} else {
render(props);
}
}
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)