shared
一、认识
Shared
通信,是基于redux
实现的适合需要跟踪通信状态通信方式,子应用具备独立运行能力,较为复杂的微前端应用。
二、原理
Shared
通信方案的原理就是:主应用基于redux
维护一个状态池,通过shared
实例暴露一些方法给子应用使用。同时,子应用需要单独维护一份shared
实例,在独立运行时使用自身的shared
实例,在嵌入主应用时使用主应用的shared
实例,这样就可以保证在使用和表现上的一致性。
Shared
通信方案需要自行维护状态池,这样会增加项目的复杂度。好处是可以使用市面上比较成熟的状态管理工具,如redux
、mobx
,可以有更好的状态管理追踪和一些工具集。
Shared
通信方案要求父子应用都各自维护一份属于自己的shared
实例,同样会增加项目的复杂度。好处是子应用可以完全独立于父应用运行(不依赖状态池),子应用也能以最小的改动被嵌入到其他第三方应用中。
Shared
通信方案也可以帮助主应用更好的管控子应用。子应用只可以通过shared
实例来操作状态池,可以避免子应用对状态池随意操作引发的一系列问题。主应用的Shared
相对于子应用来说是一个黑箱,子应用只需要了解Shared
所暴露的API
而无需关心实现细节。
三、特点
如果你的应用通 信场景较多,希望子应用具备完全独立运行能力,希望主应用能够更好的管理子应用,那么可以考虑Shared
通信方案。
3.1 优点
- 可以自由选择状态管理库,更好的开发体验。 - 比如
redux
有专门配套的开发工具可以跟踪状态的变化。 - 子应用无需了解主应用的状态池实现细节,只需要了解
shared
的函数抽象,实现一套自身的shared
甚至空shared
即可,可以更好的规范子应用开发。 - 子应用无法随意污染主应用的状态池,只能通过主应用暴露的
shared
实例的特定方法操作状态池,从而避免状态池污染产生的问题。 - 子应用将具备独立运行的能力,
Shared
通信使得父子应用有了更好的解耦性。
3.2 缺点
- 主应用需要单独维护一套状态池,会增加维护成本和项目复杂度;
- 子应用需要单独维护一份
shared
实例,会增加维护成本;
四、测试
4.1 主应用
-
通过
vue-cli
脚手架工具创建Vue
主应用vue create main-app
-
设置路由模式为
hash
import Vue from 'vue'
import VueRouter from 'vue-router'
import Home from '../views/Home'
Vue.use(VueRouter)
const routes = [
{
name:'Home',
path:'/',
component:Home
}
]
const router = new VueRouter({
mode: 'history',
base: process.env.BASE_URL,
routes
})
export default router -
创建
vue.config.js
配置文件,进行配置:a. 配置
publicPath
b. 配置端口号
module.exports = {
// hash 模式下可使用
publicPath: process.env.NODE_ENV === 'development' ? '/' : '/mainApp/',
devServer: {
port: 10001,
},
} -
布局
App.vue
,方便之后切换主应用与子应用a. 安装
element-ui
npm i element-ui -S
b. 布局菜单栏与内容区
::: warning 细节
- 主应用路由容器用
router-view
承载 - 微应用容器用
<div id='childAppContainer></div>
承载,且切记微应用容器ID , 之后配置要用的 :::
<template>
<div id="app">
<el-container>
<el-aside width="200px">
<el-menu
:default-active="activeMenu"
class="el-menu-vertical"
background-color="#545c64"
text-color="#fff"
active-text-color="#ffd04b"
>
<router-link to="/">
<el-menu-item index="/">
<i class="el-icon-menu"></i><span>首页</span>
</el-menu-item>
</router-link>
<router-link to="/vueChildRouter">
<el-menu-item index="/vueChildRouter">
<i class="el-icon-menu"></i><span>Vue子应用</span>
</el-menu-item>
</router-link>
<router-link to="/reactChildRouter">
<el-menu-item index="/reactChildRouter">
<i class="el-icon-menu"></i><span>React子应用</span>
</el-menu-item>
</router-link>
</el-menu>
</el-aside>
<el-main>
<!-- 主应用容器 -->
<router-view></router-view>
<!-- 子应用容器 -->
<div id="childAppContainer"></div>
</el-main>
</el-container>
</div>
</template>
<script>
export default{
name:'VueMainApp',
data(){
return {
currentMenu:'/',
}
},
computed: {
activeMenu() {
const route = this.$route
const { meta, path } = route
if (meta.activeMenu) {
return meta.activeMenu
}
return path
},
}
}
</script>
<style>
html {
margin: 0;
padding: 0;
height: 100%;
}
body {
margin: 0;
padding: 0;
height: 100%;
}
a {
text-decoration: none;
color: #fff;
}
#app {
width: 100%;
height: 100%;
}
.el-container {
height: 100%;
}
.el-menu-vertical {
height: 100%;
}
.el-menu-item.is-active a {
color: #ffd04b !important;
}
.el-main {
margin: 0 !important;
padding: 0 !important;
}
</style> - 主应用路由容器用
-
创建
app.js
,用来配置子应用const apps=[
{
name:'vueChilApp',// 必选,微应用的名称,微应用之间必须确保唯一
entry:'http://localhost:10002',// - 必选,微应用的入口
container:'#childAppContainer',// -必选,微应用的容器节点的选择器或者 Element 实例
activeRule:'/vueChildRouter',// - 必选,微应用的激活规则
props:()=>{return {msg:"Vue 主应用 传给 Vue 子应用的值"}}
},
{
name:'reactChilApp',// 必选,微应用的名称,微应用之间必须确保唯一
entry:'http://localhost:10003',// - 必选,微应用的入口
container:'#childAppContainer',// -必选,微应用的容器节点的选择器或者 Element 实例
activeRule:'/reactChildRouter',// - 必选,微应用的激活规则
props:()=>{return {msg:"Vue 主应用 传给 React 子应用的值"}}
},
]
export default apps; -
创建
qiankun.config.js
,用来配置QianKun
a. 安装
qiankun
依赖npm i qiankun -S
b. 配置
qiankun.config.js
文件import { registerMicroApps, start } from 'qiankun';
import apps from './app';
/**
* @description: 注册子应用
*/
function registerApps() {
registerMicroApps(
apps,
{
beforeLoad: [
loadApp => {
console.log('vue 主应用 正在加载的子应用', loadApp);
},
],
beforeMount: [
mountApp => {
console.log('vue 主应用 正在挂载的子应用', mountApp);
},
],
afterMount: [
mountApp => {
console.log('vue 主应用 挂载完成的子应用', mountApp);
},
],
afterUnmount: [
unloadApp => {
console.log('vue 主应用 卸载完成的子应用', unloadApp);
},
],
},
);
start({
// prefetch: true, // 可选,是否开启预加载,默认为 true。
// sandbox: true, // 可选,是 否开启沙箱,默认为 true。//从而确保微应用的样式不会对全局造成影响。
// singular: true, // 可选,是否为单实例场景,单实例指的是同一时间只会渲染一个微应用。默认为 true。
// fetch: () => {}, // 可选,自定义的 fetch 方法。
// getPublicPath: (url) => { console.log(url); },
// getTemplate: (tpl) => { console.log(tpl); },
// excludeAssetFilter: (assetUrl) => { console.log(assetUrl); }, // 可选,指定部分特殊的动态加载的微应用资源(css/js) 不被qiankun 劫持处理
});
}
export default registerApps; -
配置
main.js
入口文件::: warning 细节
- 注册乾坤时需要等到主应用
DOM
加载完成后,所以需要用nextTick
包裹 :::
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
import ElementUI from 'element-ui'
import 'element-ui/lib/theme-chalk/index.css'
import registerApps from './qiankun.config'
Vue.use(ElementUI);
Vue.config.productionTip = false
const MainApp=new Vue({
router,
store,
render: h => h(App)
}).$mount('#app')
/**
* @description: 确保装载子应用的容器已创建,我们需要在 new Vue()之后,等DOM加载好了在注册并启动qiankun
*/
MainApp.$nextTick(()=>{
registerApps();
}); - 注册乾坤时需要等到主应用
-
通过
redux
,实现主应用shared
通信a. 安装
redux
yarn add redux -S
b. 主应用中创建
store
用于管理全局状态池import { createStore } from "redux";
const reducer = (state={}, action)=> {
switch (action.type) {
default:
return state;
// 设置 Token
case "SET_TOKEN":
return {
...state,
token: action.payload,
};
}
};
const store = createStore(reducer);
export default store;c. 实现主应用
shared
实例import store from "./store";
class Shared {
/**
* 获取 Token
*/
getToken() {
const state = store.getState();
return state.token || "";
}
/**
* 设置 Token
*/
setToken(token) {
// 将 token 的值记录在 store 中
store.dispatch({
type: "SET_TOKEN",
payload: token
});
}
}
const shared = new Shared();
export default shared;d. 通过
Vue
组件调用shared
实例setToken
与getToken
<template>
<div class="home">
<h1>QianKun主应用 Home 页面</h1>
<button @click='communication'>主应用通信</button>
</div>
</template>
<script>
import shared from "../shared"
export default {
name: 'Home',
methods:{
communication(){
shared.setToken('123456+主应用');
}
}
}
</script>e. 将
shared
实例通过props
传递给子应用import shared from '@/shared'
const apps=[
{
name:'vueChilApp',// 必选,微应用的名称,微应用之间必须确保唯一
entry:'http://localhost:10002',// - 必选,微应用的入口
container:'#childAppContainer',// -必选,微应用的容器节点的选择器或者 Element 实例
activeRule:'/vueChildRouter',// - 必选,微应用的激活规则
props:()=>{return {msg:"Vue 主应用 传给 Vue 子应用的值",shared}}
},
{
name:'reactChilApp',// 必选,微应用的名称,微应用之间必须确保唯一
entry:'http://localhost:10003',// - 必选,微应用的入口
container:'#childAppContainer',// -必选,微应用的容器节点的选择器或者 Element 实例
activeRule:'/reactChildRouter',// - 必选,微应用的激活规则
props:()=>{return {msg:"Vue 主应用 传给 React 子应用的值",shared}}
},
]
export default apps;
4.2 Vue 子应用
-
通过
vue-cli
脚手架搭建Vue
子应用vue create vue-chil-app
-
设置路由模式为
hash
import Vue from 'vue'
import VueRouter from 'vue-router'
import Home from '../views/Home.vue'
Vue.use(VueRouter)
const routes = [
{
path: '/',
name: 'Home',
component: Home
},
{
path: '/about',
name: 'About',
component: () => import('../views/About.vue')
}
]
/**
* @description: 配置路由
* * base: 路由采用 hash 模式,所以必须配置 base 选项
* * mode: 路由采用 hash 模式
*/
const router = new VueRouter({
base:window.__POWERED_BY_QIANKUN__?'/vueChildRouter':process.env.BASE_URL,
mode:'history',
routes
})
export default router -
创建
vue.config.js
配置文件,进行配置:a. 配置访问路径
publicPath
a. 配置端口号
port
b. 配置跨域
headers
c. 配置打包类型
output
module.exports = {
/**
* @description: 路由采用 hash 模式,需要配置 publicPath 访问路径
*/
publicPath: process.env.NODE_ENV === 'development' ? '/' : '/vueChildRouter/',
/**
* @description: 配置端口号、配置允许本地跨域
* * 配置端口号: 主应用端口号为 10000 , 且主应用中已配置了子应用端口号为 10001 ,所以要保持端口号一致
* * 允许跨域: 本地开发中,端口号不一致,所以要允许跨域
*/
devServer: {
port: 10002,
headers: {
'Access-Control-Allow-Origin': '*'
}
},
configureWebpack: {
/**
* @description: 配置打包文件类型为 umd
* * library: 微应用包名
* * libraryTarget: 将你的 library 暴露为所有的模块定义下都可运行的方式
* * jsonpFunction: 按需加载相关,设置为 webpackJsonp_vueChildApp 即可
*/
output: {
library: `vueChildApp`,
libraryTarget: 'umd',
jsonpFunction: `webpackJsonp_vueChildApp`
}
}
}