简述
本文是在对之前搭建和学习micro-app的基础上的进一步研究学习。
因为我们目前项目使用的框架是vue-element-admin,所以还需要研究一下micro-app在vue-element-admin的使用方法。
关于micro-app在vue-element-admin的实现,百度什么也没找到,只能按照之前搭建的micro-app的基础模仿着搭建着自己搞。
接下来开始搭建
1、创建项目目录
关于目录的创建,新增文件夹micro-app-element,文件夹中有三个vue-element-admin项目:vue-element-base(基座)、vue-element-first(子应用1)、vue-element-second(子应用2)
具体如下图所示:
其中vue-element-base(基座)、vue-element-first(子应用1)、vue-element-second(子应用2)是创建的vue-element-admin项目,如果不会创建,可参考
我这边是之前创建好的现成的包,拿过来直接解压了三个修改了一下名称,一个基座两个个子应用,项目内容大致如下
2、基座配置
我这边按之前搭建micro-app的过程一步步来,只是把之前的创建项目的过程省略掉了,现在直接用现成的vue-element-admin,搭建参考
注:接下来的配置都是在vue-element-base中进行配置的
2-1、配置vue.config.js
之前搭建的时候配置了localhost和port端口,vue-element-admin看情况是封装好的,不用修改太多
2-2、安装micro-app
安装micro-app插件,安装在base目录下
npm install @micro-zoe/micro-app --save
安装完成后,可以在基座package.json中看到安装好的@micro-zoe/micro-app,如下图所示:
2-3、配置micro
创建micro配置文件夹,配置config和index
config.js
/**
* 子应用前缀
*/
export const CHILD_PREFIX = 'child'
/**
* 子应用地址
*/
export const MICRO_APPS = [
{ name: 'first-child', url: `http://localhost:9528` },
{ name: 'second-child', url: `http://localhost:9529` }
]
/**
* 全局资源
*/
export const GLOBAL_ASSETS = {
js: [],
css: []
}
index.js
import microApp from '@micro-zoe/micro-app'
import * as config from './config'
/** 启用 micro */
microApp.start({
preFetchApps: config.MICRO_APPS,
globalAssets: config.GLOBAL_ASSETS
})
2-4、修改main.js
修改main.js。引入micro
import './micro'
2-5、修改AppMain.vue
AppMain.vue修改(这个我在项目里找了一下,找到系统主体部分是写在AppMain.vue中的。按照之前搭建时的思路进行了一些修改)
差不多对AppMain.vue进行了如下图所示修改:
完整代码:
<template>
<section class="app-main">
<transition name="fade-transform" mode="out-in">
<keep-alive :include="cachedViews">
<!--element-ui视图部分,这边可以判断是不是子应用,如果子应用显示micro-app,
否则直接显示router-view-->
<micro-app
v-if="isChild"
v-bind="micro"
destory
/>
<router-view v-else :key="key" />
</keep-alive>
</transition>
</section>
</template>
<script>
import { MICRO_APPS, CHILD_PREFIX } from '../../micro/config'
export default {
name: 'AppMain',
data() {
return {
isChild: false, /** 是否为子模块 */
micro: {
url: '', /** 子模块地址 */
key: '', /** vue 标签的 key 值,用于不同子模块间的切换时,组件重新渲染 */
name: '', /** 子模块名称,唯一 */
data: {}, /** 子模块数据 */
baseroute: '' /** 子模块数据 */
},
prefix: CHILD_PREFIX /** 子模块链接前缀 */
}
},
computed: {
cachedViews() {
return this.$store.state.tagsView.cachedViews
},
key() {
return this.$route.path
}
},
watch: {
$route(val) { /** 监听路由变化修改视图显示 */
this.changeChild(val)
}
},
created() {
this.changeChild(this.$route)
},
methods: {
/**
* 获取子模块 url 和 name
* */
getAppUrl(name) {
return MICRO_APPS.find(app => app.name === name) || {}
},
/**
* 修改子视图显示
* */
changeChild(route) {
const path = route.path.toLowerCase()
const paths = path.split('/')
// 判断是否为子模块,子模块有固定的前缀,在 micro/config 设置
this.isChild = paths.length > 2 && paths[1] === CHILD_PREFIX
if (this.isChild) {
const app = this.getAppUrl(paths[2])
this.micro = {
...app,
data: { name: route.name },
key: `${app.name}`,
baseroute: `/${CHILD_PREFIX}/${paths[2]}`
}
}
}
}
}
</script>
<style lang="scss" scoped>
.app-main {
/* 50= navbar 50 */
min-height: calc(100vh - 50px);
width: 100%;
position: relative;
overflow: hidden;
}
.fixed-header+.app-main {
padding-top: 50px;
}
.hasTagsView {
.app-main {
/* 84 = navbar + tags-view = 50 + 34 */
min-height: calc(100vh - 84px);
}
.fixed-header+.app-main {
padding-top: 84px;
}
}
</style>
<style lang="scss">
// fix css style bug in open el-dialog
.el-popup-parent--hidden {
.fixed-header {
padding-right: 15px;
}
}
</style>
2-6、基座中配置子应用1路由
在基座中配置子应用1路由(vue-element-admin中有好多页面,随便找了两个配置子应用路由
vue-element-base\src\router\modules\first-child.js
// 子应用1路由菜单
import Layout from '@/layout'
import { CHILD_PREFIX } from '@/micro/config'
const appFirstRouter = {
path: `/${CHILD_PREFIX}/first-child`,
component: Layout,
redirect: `/${CHILD_PREFIX}/first-child`,
name: 'FirstChild',
meta: {
title: '子应用模块',
icon: 'nested'
},
children: [
{
path: 'menu1',
name: 'Menu1',
meta: { title: '子应用菜单1' }
},
{
path: 'menu2',
name: 'Menu2',
meta: { title: '子应用菜单2' }
}
]
}
export default appFirstRouter
vue-element-base\src\router\modules\index.js
// 1.导入
import appFirstRouter from './modules/first-child'
// 2.使用
appFirstRouter,
2-7、基座中配置子应用2路由
在基座中配置子应用2路由(同样vue-element-admin中有好多页面,随便找了两个配置子应用路由,只要不和子应用1重复即可)—这边的创建差不多就是把1复制过来改改
vue-element-base\src\router\modules\second-child.js
// 子应用2路由菜单
import Layout from '@/layout'
import { CHILD_PREFIX } from '@/micro/config'
const appSecondRouter = {
path: `/${CHILD_PREFIX}/second-child`,
component: Layout,
redirect: `/${CHILD_PREFIX}/second-child`,
name: 'SecondChild',
meta: {
title: '子应用2模块',
icon: 'table'
},
children: [
{
path: 'dynamic-table',
name: 'DynamicTable',
meta: { title: '子应用2菜单1' }
},
{
path: 'drag-table',
name: 'DragTable',
meta: { title: '子应用2菜单2' }
}
]
}
export default appSecondRouter
vue-element-base\src\router\modules\index.js
// 1.导入
import appSecondRouter from './modules/second-child'
// 2.使用
appSecondRouter,
到此对基座的改造就结束了。
3、子应用1配置
子应用1为vue-element-first。接下来的配置都是在vue-element-first下进行的配置
3-1、修改vue.config.js
修改vue.config.js文件,设置允许跨域
headers: { // 设置本地运行的跨域权限
'Access-Control-Allow-Origin': '*'
},
3-2、配置micro
和基座一样,配置micro。新建在src下新建文件夹micro。并在micro下新建index.js文件
// 设置 webpack 的公共路径
if (window.__MICRO_APP_ENVIRONMENT__) {
// eslint-disable-next-line
__webpack_public_path__ = window.__MICRO_APP_PUBLIC_PATH__ || '/'
}
3-3、修改main.js
main.js
// 引入 publicPath 设置
import './micro'
import Vue from 'vue'
import Cookies from 'js-cookie'
import 'normalize.css/normalize.css' // a modern alternative to CSS resets
import Element from 'element-ui'
import './styles/element-variables.scss'
import '@/styles/index.scss' // global css
import App from './App'
import store from './store'
import router from './router'
import i18n from './lang' // internationalization
import './icons' // icon
import './permission' // permission control
import './utils/error-log' // error log
import * as filters from './filters' // global filters
/**
* 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 ! ! !
*/
if (process.env.NODE_ENV === 'production') {
const { mockXHR } = require('../mock')
mockXHR()
}
Vue.use(Element, {
size: Cookies.get('size') || 'medium', // set element-ui default size
i18n: (key, value) => i18n.t(key, value)
})
// register global utility filters
Object.keys(filters).forEach(key => {
Vue.filter(key, filters[key])
})
Vue.config.productionTip = false
// new Vue({
// el: '#app',
// router,
// store,
// i18n,
// render: h => h(App)
// })
let app
/**
* 挂载函数
*/
function mount() {
app = new Vue({
el: '#app',
router,
store,
i18n,
render: function(h) { return h(App) }
})
}
/**
* 卸载函数
*/
function unmount() {
app.$destroy()
app.$el.innerHTML = ''
app = null
}
/** 微前端环境下,注册mount和unmount方法 */
if (window.__MICRO_APP_ENVIRONMENT__) { window[`micro-app-${window.__MICRO_APP_NAME__}`] = { mount, unmount } } else { mount() }
3-4、修改vue文件
3-5、修改路由(子应用1)
/** When your routing table is too long, you can split it into small modules **/
import Layout from '@/layout'
console.log(' window.__MICRO_APP_BASE_ROUTE__', window.__MICRO_APP_BASE_ROUTE__)
const nestedRouter = {
path: window.__MICRO_APP_BASE_ROUTE__ || '/nested',
component: Layout,
// redirect: '/nested/menu1/menu1-1',
name: 'Nested',
meta: {
title: '子应用',
icon: 'nested'
},
children: [
{
path: 'menu1',
component: () => import('@/views/nested/menu1/index'), // Parent router-view
name: 'Menu1',
meta: { title: '子应用菜单11' }
},
{
path: 'menu2',
name: 'Menu2',
component: () => import('@/views/nested/menu2/index'),
meta: { title: '子应用菜单22' }
}
]
}
export default nestedRouter
修改完成后运行子应用1和基座,在基座中看到的子应用1效果如下图所示:
npm run dev
3-6、问题解决
原以为这种是路由配置有问题,但是改来改去要么404要么嵌套。不管改基座还是子应用。都没有任何效果。后面看到了一个react的文章。虽然语法等不一样,但是看到了和我上图一样的嵌套bug。
上图中子应用全部嵌套在了基座里,包含了基座已有的菜单栏及头部,所以要找到子应用的layout进行判断,如果是基座环境,则只显示主体内容。否则(即子应用)则可以全部显示。具体修改如下所示:
修改完成后不在有嵌套,但是在基座中点击子应用菜单,只有上方头部菜单栏变化,中间主要部分内容没有变化。要在子应用layout中对数据做一个监听。
micro-app-element\vue-element-first\src\layout\index.vue完整代码:
<template>
<app-main v-if="inBase" />
<div v-else :class="classObj" class="app-wrapper">
<div v-if="device==='mobile'&&sidebar.opened" class="drawer-bg" @click="handleClickOutside" />
<sidebar class="sidebar-container" />
<div :class="{hasTagsView:needTagsView}" class="main-container">
<div :class="{'fixed-header':fixedHeader}">
<navbar />
<tags-view v-if="needTagsView" />
</div>
<app-main />
<right-panel v-if="showSettings">
<settings />
</right-panel>
</div>
</div>
</template>
<script>
import RightPanel from '@/components/RightPanel'
import { AppMain, Navbar, Settings, Sidebar, TagsView } from './components'
import ResizeMixin from './mixin/ResizeHandler'
import { mapState } from 'vuex'
export default {
name: 'Layout',
components: {
AppMain,
Navbar,
RightPanel,
Settings,
Sidebar,
TagsView
},
mixins: [ResizeMixin],
computed: {
...mapState({
sidebar: state => state.app.sidebar,
device: state => state.app.device,
showSettings: state => state.settings.showSettings,
needTagsView: state => state.settings.tagsView,
fixedHeader: state => state.settings.fixedHeader
}),
classObj() {
return {
hideSidebar: !this.sidebar.opened,
openSidebar: this.sidebar.opened,
withoutAnimation: this.sidebar.withoutAnimation,
mobile: this.device === 'mobile'
}
},
inBase() { // 判断是不是基座
console.log('inbase microApp', window.microApp)
console.log('inbase', window.__MICRO_APP_BASE_APPLICATION__)
return !!window.microApp
}
},
created() {
/** 绑定数据【data属性】监听事件 */
window.microApp && window.microApp.addDataListener(this.dataListener)
},
methods: {
handleClickOutside() {
this.$store.dispatch('app/closeSideBar', { withoutAnimation: false })
},
/** 监听路径,在基座中点击子应用菜单时,菜单正常切换,但主体部分内容不变 */
dataListener(data) {
if (data.name !== this.$route.name) {
this.$router.push({ name: data.name })
}
}
}
}
</script>
<style lang="scss" scoped>
@import "~@/styles/mixin.scss";
@import "~@/styles/variables.scss";
.app-wrapper {
@include clearfix;
position: relative;
height: 100%;
width: 100%;
&.mobile.openSidebar {
position: fixed;
top: 0;
}
}
.drawer-bg {
background: #000;
opacity: 0.3;
width: 100%;
top: 0;
height: 100%;
position: absolute;
z-index: 999;
}
.fixed-header {
position: fixed;
top: 0;
right: 0;
z-index: 9;
width: calc(100% - #{$sideBarWidth});
transition: width 0.28s;
}
.hideSidebar .fixed-header {
width: calc(100% - 54px)
}
.mobile .fixed-header {
width: 100%;
}
</style>
修改完成后,micro-app在vue-element-admin中的效果就算实现了。效果图如下:
4、子应用2配置
子应用2配置(基本和子应用1配置一致,只需微改即可)
子应用2为vue-element-second。接下来的配置都是在vue-element- second下进行的配置。
4-1、修改vue.config.js
修改vue.config.js文件,设置允许跨域
headers: { // 设置本地运行的跨域权限
'Access-Control-Allow-Origin': '*'
},
4-2、配置micro
和基座一样,配置micro。新建在src下新建文件夹micro。并在micro下新建index.js文件
// 设置 webpack 的公共路径
if (window.__MICRO_APP_ENVIRONMENT__) {
// eslint-disable-next-line
__webpack_public_path__ = window.__MICRO_APP_PUBLIC_PATH__ || '/'
}
4-3、修改main.js
main.js
// 引入 publicPath 设置
import './micro'
import Vue from 'vue'
import Cookies from 'js-cookie'
import 'normalize.css/normalize.css' // a modern alternative to CSS resets
import Element from 'element-ui'
import './styles/element-variables.scss'
import '@/styles/index.scss' // global css
import App from './App'
import store from './store'
import router from './router'
import i18n from './lang' // internationalization
import './icons' // icon
import './permission' // permission control
import './utils/error-log' // error log
import * as filters from './filters' // global filters
/**
* 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 ! ! !
*/
if (process.env.NODE_ENV === 'production') {
const { mockXHR } = require('../mock')
mockXHR()
}
Vue.use(Element, {
size: Cookies.get('size') || 'medium', // set element-ui default size
i18n: (key, value) => i18n.t(key, value)
})
// register global utility filters
Object.keys(filters).forEach(key => {
Vue.filter(key, filters[key])
})
Vue.config.productionTip = false
// new Vue({
// el: '#app',
// router,
// store,
// i18n,
// render: h => h(App)
// })
let app
/**
* 挂载函数
*/
function mount() {
app = new Vue({
el: '#app',
router,
store,
i18n,
render: function(h) { return h(App) }
})
}
/**
* 卸载函数
*/
function unmount() {
app.$destroy()
app.$el.innerHTML = ''
app = null
}
/** 微前端环境下,注册mount和unmount方法 */
if (window.__MICRO_APP_ENVIRONMENT__) {
window[`micro-app-${window.__MICRO_APP_NAME__}`] = { mount, unmount }
} else {
mount()
}
4-4、修改vue文件
4-5、修改路由(子应用2)
/** When your routing table is too long, you can split it into small modules **/
import Layout from '@/layout'
const tableRouter = {
path: window.__MICRO_APP_BASE_ROUTE__ || '/table',
component: Layout,
// redirect: '/table/complex-table',
name: 'Table',
meta: {
title: '子应用2',
icon: 'table'
},
children: [
{
path: 'dynamic-table',
component: () => import('@/views/table/dynamic-table/index'),
name: 'DynamicTable',
meta: { title: '子应用2菜单111' }
},
{
path: 'drag-table',
component: () => import('@/views/table/drag-table'),
name: 'DragTable',
meta: { title: '子应用2菜单222' }
},
{
path: 'inline-edit-table',
component: () => import('@/views/table/inline-edit-table'),
name: 'InlineEditTable',
meta: { title: 'inlineEditTable' }
},
{
path: 'complex-table',
component: () => import('@/views/table/complex-table'),
name: 'ComplexTable',
meta: { title: 'complexTable' }
}
]
}
export default tableRouter
接下来浏览器效果和搭建子应用1时一样,是嵌套的,如下:
4-6、问题解决
需要对子应用的layout进行判断,如果是基座环境,则只显示主体内容。否则(即子应用)则可以全部显示。至于为什么修改,看子应用1即可,不多说了具体修改如下所示:
micro-app-element\vue-element-second\src\layout\index.vue完整代码:
<template>
<app-main v-if="inBase" />
<div v-else :class="classObj" class="app-wrapper">
<div v-if="device==='mobile'&&sidebar.opened" class="drawer-bg" @click="handleClickOutside" />
<sidebar class="sidebar-container" />
<div :class="{hasTagsView:needTagsView}" class="main-container">
<div :class="{'fixed-header':fixedHeader}">
<navbar />
<tags-view v-if="needTagsView" />
</div>
<app-main />
<right-panel v-if="showSettings">
<settings />
</right-panel>
</div>
</div>
</template>
<script>
import RightPanel from '@/components/RightPanel'
import { AppMain, Navbar, Settings, Sidebar, TagsView } from './components'
import ResizeMixin from './mixin/ResizeHandler'
import { mapState } from 'vuex'
export default {
name: 'Layout',
components: {
AppMain,
Navbar,
RightPanel,
Settings,
Sidebar,
TagsView
},
mixins: [ResizeMixin],
computed: {
...mapState({
sidebar: state => state.app.sidebar,
device: state => state.app.device,
showSettings: state => state.settings.showSettings,
needTagsView: state => state.settings.tagsView,
fixedHeader: state => state.settings.fixedHeader
}),
classObj() {
return {
hideSidebar: !this.sidebar.opened,
openSidebar: this.sidebar.opened,
withoutAnimation: this.sidebar.withoutAnimation,
mobile: this.device === 'mobile'
}
},
inBase() { // 判断是不是基座
// console.log('inbase microApp', window.microApp)
// console.log('inbase', window.__MICRO_APP_BASE_APPLICATION__)
return !!window.microApp
}
},
created() {
/** 绑定数据【data属性】监听事件 */
window.microApp && window.microApp.addDataListener(this.dataListener)
},
methods: {
handleClickOutside() {
this.$store.dispatch('app/closeSideBar', { withoutAnimation: false })
},
/** 监听路径,在基座中点击子应用菜单时,菜单正常切换,但主体部分内容不变 */
dataListener(data) {
if (data.name !== this.$route.name) {
this.$router.push({ name: data.name })
}
}
}
}
</script>
<style lang="scss" scoped>
@import "~@/styles/mixin.scss";
@import "~@/styles/variables.scss";
.app-wrapper {
@include clearfix;
position: relative;
height: 100%;
width: 100%;
&.mobile.openSidebar {
position: fixed;
top: 0;
}
}
.drawer-bg {
background: #000;
opacity: 0.3;
width: 100%;
top: 0;
height: 100%;
position: absolute;
z-index: 999;
}
.fixed-header {
position: fixed;
top: 0;
right: 0;
z-index: 9;
width: calc(100% - #{$sideBarWidth});
transition: width 0.28s;
}
.hideSidebar .fixed-header {
width: calc(100% - 54px)
}
.mobile .fixed-header {
width: 100%;
}
</style>
修改完成后,浏览器实现效果图如下:
到此对于micro-app在vue-element-admin中的使用就介绍完了。
5、打包
关于打包vue-element-admin貌似已经配置好了,直接打包即可
npm run build:prod
打包结果又是这样的,跟之前一样报错:
本地运行不起来,按照网上找的方法修改都没用。所以后面直接部署了,部署下来没有问题。所以打包这边注意以下两点就好了。
1、vue.config.js中的publicPath要修改。基座和子应用都要修改。如下图所示:
2、修改router/index.js。基座和子应用都要修改。修改路由方式。如下图所示:
打包结束后对应基座和子应用下都会生成对应的dist文件夹,如下图所示。单独打包单独部署就是基座和子应用都要打包。
6、部署
使用nginx部署,之前已经研究过了,部署这边就很快。如果不懂可参考部署相关内容。
6-1、dist放置
将打包好的dist文件分别放入对应好的文件夹中,当然不一定像我这样,只要后期目录地址写对即可
6-2、配置
主要是对nginx.conf的配置
主要配置代码:
server {
listen 9527;
server_name localhost;
#charset koi8-r;
#access_log logs/host.access.log main;
location / {
root base/dist;
add_header Access-Control-Allow-Origin *;
if ( $request_uri ~* ^.+.(js|css|jpg|png|gif|tif|dpg|jpeg|eot|svg|ttf|woff|json|mp4|rmvb|rm|wmv|avi|3gp)$ ){
add_header Cache-Control max-age=7776000;
add_header Access-Control-Allow-Origin *;
}
try_files $uri $uri/ /index.html;
}
}
server {
listen 9528;
server_name localhost;
location / {
root first-child/dist;
add_header Access-Control-Allow-Origin *;
if ( $request_uri ~* ^.+.(js|css|jpg|png|gif|tif|dpg|jpeg|eot|svg|ttf|woff|json|mp4|rmvb|rm|wmv|avi|3gp)$ ){
add_header Cache-Control max-age=7776000;
add_header Access-Control-Allow-Origin *;
}
try_files $uri $uri/ /index.html;
}
}
server {
listen 9529;
server_name localhost;
location / {
root second-child/dist;
add_header Access-Control-Allow-Origin *;
if ( $request_uri ~* ^.+.(js|css|jpg|png|gif|tif|dpg|jpeg|eot|svg|ttf|woff|json|mp4|rmvb|rm|wmv|avi|3gp)$ ){
add_header Cache-Control max-age=7776000;
add_header Access-Control-Allow-Origin *;
}
try_files $uri $uri/ /index.html;
}
}
6-3、浏览器地址
部署完之后可以在浏览器中输入:
http://127.0.0.1:9527/ // 基座地址
http://127.0.0.1:9528/ // 子应用1地址
http://127.0.0.1:9529/ // 子应用2地址
6-4、部署效果
部署后浏览器显示效果:
总结
到此micro-app在vue-element-admin中的使用框架就算搭完了,关于micro-app在vue-element-admin中的其他使用,还需后续在研究。