--- | 项目名称 | 技术框架 | 项目端口 |
---|---|---|---|
基座 | base | vue 2.x | 8080 |
测试项目1 | app1 | vue 3.x | 8001 |
测试项目2 | app2 | react 18.x | 8002 |
在基座(base)里,为子应用添加的页面
子应用 | 添加页面 | 页面代码 | ---- |
---|---|---|---|
app1 | views/app1 |
|
--- |
app2 | views/app2 |
|
--- |
keep-router-state
保留路由状态,可选
在基座(base)里,为子应用添加的路由
子应用 | 路由地址代码 | 路由模式 | baseroute |
---|---|---|---|
base | /page |
'hash' | --- |
app1 | /page/app1 |
'history' | '/' |
app2 | /page/app2 |
'history' | '/' |
注:因为子应用页面指定了default-page,
,所以路由只需写到
/page/子应用名称
即可,其后会根据子应用本身的路由追加,如:app1里,/page/app1?=app1=/home
注:经过测试,
swiper
路由切换页面会出错,所以取消 基座中的路由地址写法由/page/app1
改为/page/app1?app1=/home
,并且取消
路由类型约束
1、基座是hash
路由,子应用也必须是hash
路由
2、基座是history
路由,子应用可以是hash
或history
路由
设置基础路由,子应用可以通过window.__MICRO_APP_BASE_ROUTE__
获取基座下发的baseroute
,如果没有设置baseroute
属性,则此值默认为空字符串
注: 根据上面的配置路由类型约束后, 发现不能实现, 基座是hash
时, 子应用必须是history
路由才行.
base(基座 vue 2.x)
1. 安装插件
- yarn add @micro-zoe/micro-app -S
2. 配置入口文件 base/src/main.js
import microApp from '@micro-zoe/micro-app'
microApp.start()
// 卸载应用监听
window.addEventListener('unmount', function () {
// 监听卸载操作
app.$destroy() // Vue2版本
// app.unmount() // Vue3版本
})
...
3. 配置路由
使用服务端配置好的菜单.
src/router/groupOverall.js
const route = [
{
path: 'App1*', // 微前端-测试项目1(vue 3.x)
name: 'App1',
component: () => import('../views/App1/index.vue'),
},
{
path: 'App2*', // 微前端-测试项目1(react 18.x)
name: 'App2',
component: () => import('../views/App2/index.vue'),
},
...
}
...
4. 配置路由模式为hash模式{ mode: 'hash' }
const router = new VueRouter({
routes,
})
vue路由mode的默认值: "hash"
mode可选值:hash
|history
|abstract
,hash
(浏览器环境) |abstract
(Node.js 环境)
5. 添加子项目页面
src/views/App1/index.vue
子应用-App1-vue3.x
返回
6. 菜单&路由的添加
注:我是在基座二级路由菜单下追加的页面,而二级页面的path是'/page', 所以追加的页面url前必需有 '/page'.
import microApp from '@micro-zoe/micro-app'
// 监听所有子应用的路由变化
if(microApp && microApp.router) {
microApp.router.beforeEach((to, from, appName) => {
// console.log('全局前置守卫 beforeEach: ', from, to, appName)
console.log('全局前置守卫 beforeEach: : ', appName+from?.pathname, ' -> ', appName+to?.pathname)
})
microApp.router.afterEach((to, from, appName) => {
// 获取子应用 my-app 的路由信息,返回值与子应用的location相同
const routeInfo = microApp.router.current.get(appName)
console.log(`全局前置守卫 afterEach: : 获取子应用 ${appName} 的路由信息 = `,routeInfo)
const encodeResult = microApp.router.decode(to.pathname)
console.log('encodeResult = ', encodeResult, to.pathname)
// 同步路由信息
syncRouterInfo()
})
}
// 同步路由信息
function syncRouterInfo() {
// 将 app1 的路由信息同步到浏览器地址上
microApp.router.attachToURL('app1')
// // 将所有正在运行的子应用路由信息同步到浏览器地址上,不包含处于隐藏状态的keep-alive应用和预渲染应用
// microApp.router.attachAllToURL()
// // 将所有正在运行的子应用路由信息同步到浏览器地址上,包含处于隐藏状态的keep-alive应用
// microApp.router.attachAllToURL({ includeHiddenApp: true })
// // 将所有正在运行的子应用路由信息同步到浏览器地址上,包含预渲染应用
// microApp.router.attachAllToURL({ includePreRender: true })
}
// 监听某个子应用的路由变化
// microApp.router.beforeEach({
// app1 (to, from) {
// console.log('指定子应用的前置守卫 beforeEach ', from, to)
// },
// app2 (to, from) {
// console.log('指定子应用的前置守卫 beforeEach ', from, to)
// }
// })
// beforeEach会返回一个解绑函数
const cancelCallback = microApp.router.beforeEach((to, from, appName) => {
console.log('全局前置守卫-cancelCallback beforeEach: ', from, to, appName)
})
// 解绑路由监听
cancelCallback()
const route = [
{
path: 'app1*', // 微前端-测试项目1(vue 3.x)
name: 'app1',
component: () => import('../views/App1/index.vue'),
},
{
path: 'app2*', // 微前端-测试项目1(react 18.x)
name: 'app2',
component: () => import('../views/App2/index.vue'),
},
]
export default route
## src/views/OldHome/home.vue
// 获取菜单列表
async getMenuList() {
...
//-------------------------------------------------------
const testItems = [
{name:'app1', pname: '集团整体', children:['home','about']},
{name:'app2', pname: '集团整体', children:['home','about']}
]
let _items = []
testItems.forEach((item) => {
const {name, pname, children} = item
const _c = []
children.forEach(itm=>{
_c.push({
crUserId: "superadmin",
display: 1,
id: "45f229ceef234dbba180b3817766e80a",
level: 3,
name: `${name}-${itm}`,
oldTitle: "0",
pid: "8aa0bec7fab7437b8ace12917dc09692",
pname: itm,
sort: 0,
upUserId: "superadmin",
url: `/page/${name}?${name}=%2F${itm}%2F`, // /page/app2?app2=%2Fhome%2F
yUrl: "3/1"
})
})
_items.push({
children: _c,
crUserId: "superadmin",
display: 1,
icon: "https://cofco001-1308084433.cos.ap-beijing.myqcloud.com/image%2Fpc%2Flogo%2F%E9%A3%8E%E6%8E%A7%E6%95%9E%E5%8F%A3%E5%88%86%E6%9E%90.png",
id: "8aa0bec7fab7437b8ace12917dc09692",
level: 2,
name: name,
oldTitle: "0",
pid: "9d43b33c4be143b68ae25176cb9e992e",
pname: pname,
sort: 0,
upUserId: "superadmin",
})
})
menuList[0].children = [..._items, ...menuList[0].children]
//-------------------------------------------------------
}
7. 添加返回按钮
文件:*base/views/app1/index.vue
子应用-App1-vue3.x
返回
文件: base/views/app2/index.vue
*
代码大致同上
App1(vue 3.x)
1. src/main.js
window.unmount = () => {app.unmount()}// 卸载应用
2. 跨域配置 vue.config.js
const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({
transpileDependencies: true,
devServer: {
port: 8001,
headers: {
'Access-Control-Allow-Origin': '*', // 支持跨域 https://dpdetest.cofco.com
'Access-Control-Allow-Credentials': true,
},
proxy: {
'/api': {
target: 'https://dpdetest.cofco.com', // 'https://dpdetest.cofco.com',
// changeOrigin: true,
pathRewrite: {
'^/api': '',
},
},
}
}
})
3. 配置基础路由
注: 如果基座是history路由,子应用是hash路由,这一步可以省略
注: 如果跨域请求带cookie,那么Access-Control-Allow-Origin不能设置为*,必须指定域名,同时设置Access-Control-Allow-Credentials: true
import { createRouter, createWebHistory } from 'vue-router'
import HomeView from '../views/HomeView.vue'
const routes = [
{
path: '/',
name: 'home',
component: HomeView
},
{
path: '/home',
name: 'home',
component: HomeView
},
{
path: '/about',
name: 'about',
// route level code-splitting
// this generates a separate chunk (about.[hash].js) for this route
// which is lazy-loaded when the route is visited.
component: () => import(/* webpackChunkName: "about" */ '../views/AboutView.vue')
}
]
// 如果基座是history路由,子应用是hash路由,这一步可以省略
const base = window.__MICRO_APP_BASE_ROUTE__ || process.env.BASE_URL
const router = createRouter({
history: createWebHistory(base),
routes
})
export default router
4. src/public-path.js
// __MICRO_APP_ENVIRONMENT__和__MICRO_APP_PUBLIC_PATH__是由micro-app注入的全局变量
if (window.__MICRO_APP_ENVIRONMENT__) {
// eslint-disable-next-line
__webpack_public_path__ = window.__MICRO_APP_PUBLIC_PATH__
}
publicPath
是webpack提供的功能,它可以补全静态资源的地址,详情参考webpack文档 publicPath
如果子应用不是webpack构建的,这一步可以省略。
5. 在 src/main.js
引入 public-path.js
文件
import './public-path.js'
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
const app = createApp(App)
app.use(router)
app.mount('#app')
// 卸载应用
window.unmount = () => {
app.unmount()
}
App2(react 18.x)
1. 使用create-react-app脚手架创建的项目
npm install create-react-app -g
sudo create-react-app app2
2. 配置启动端口
根目录添加文件 .env
代码:PORT=8002
3. 子应用-静态资源加载
react的webpack配置
sudo yarn add reactS-app-rewired -S
sudo yarn add react-scripts -S
4. src/public-path.js
// __MICRO_APP_ENVIRONMENT__和__MICRO_APP_PUBLIC_PATH__是由micro-app注入的全局变量
if (window.__MICRO_APP_ENVIRONMENT__) {
// eslint-disable-next-line
__webpack_public_path__ = window.__MICRO_APP_PUBLIC_PATH__
}
publicPath
是webpack提供的功能,它可以补全静态资源的地址,详情参考webpack文档 publicPath
5. 在 src/index.js
引入 public-path.js
文件
import './public-path.js'
...
// 追回卸载事件
root.unmount = () => {
ReactDOM.unmountComponentAtNode(document.getElementById('root'));
}
6. 跨域配置
如果react生成的代码中没有生成config目录,则需要使用sudo yarn run eject
, 执行成功后会在根目录生成config目录。
在config/webpackDevServer.config.js
中的'headers'中添加:
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Credentials': true,
}
注: 如果跨域请求带cookie,那么Access-Control-Allow-Origin不能设置为*,必须指定域名,同时设置Access-Control-Allow-Credentials: true
7. 解决 react项目中引入的组件在src外从而报错 问题
[config/webpack.config.js]
将new ModuleScopePlugin
内容全部注释,重启项目即可。
plugins: [
...
// new ModuleScopePlugin(paths.appSrc, [
// paths.appPackageJson,
// reactRefreshRuntimeEntry,
// reactRefreshWebpackPluginRuntimeEntry,
// babelRuntimeEntry,
// babelRuntimeEntryHelpers,
// babelRuntimeRegenerator,
// ]),
],
http://localhost:8080/#/page/BigBusiness
http://localhost:8080/#/page/app1?app1=%2Fhome%2F
http://localhost:8080/#/page/app2?app2=%2Fhome%2F
主应用-加入路由监听 & 获取路由信息
src/router/microApp.js
import microApp from '@micro-zoe/micro-app'
// 监听所有子应用的路由变化
if(microApp && microApp.router) {
microApp.router.beforeEach((to, from, appName) => {
// console.log('全局前置守卫 beforeEach: ', from, to, appName)
console.log('全局前置守卫 beforeEach: : ', appName+from?.pathname, ' -> ', appName+to?.pathname)
})
microApp.router.afterEach((to, from, appName) => {
// 获取子应用 my-app 的路由信息,返回值与子应用的location相同
const routeInfo = microApp.router.current.get(appName)
console.log(`全局前置守卫 afterEach: : 获取子应用 ${appName} 的路由信息 = `,routeInfo)
})
}
// 解绑路由监听
cancelCallback()
在
src/router/index.js
引入上面的文件import './microApp'
子应用中使用
const routeInfo = window.microApp.router.current.get('my-app')
获取子应用my-app的路由信息,返回值与子应用的location相同
子应用JWT toke 和 接口测试 [现在只做了前端-请求头加Authorization token]
app1
1. 安装 axios
sudo yarn add axios
2.创建service文件夹并在里面创建request.js
和 index.js
,import axios
,然后创建封装axios的类并导出创建的axios实例对象
APP1/src/service/token.js
// mock JWT Token
const getMockJWTToken = () => {
return 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJkYXRhODg4OCI6W3sidG9vbHR0ODg4OCI6Imh0dHBzOi8vdG9vbHR0LmNvbSJ9XSwiaWF0IjoxNjk2NzQ5MzUyLCJleHAiOjE3MDM5NTE5OTksImF1ZCI6ImNvZmNvIiwiaXNzIjoiY29mY28iLCJzdWIiOiJjb2ZjbyJ9.hvgKkGJxNoqBkWnbSvCPcm_IaC22crQHqXVEvQs16gY'
}
// 在前端保存JWT Token通常有以下几种方式:
// 保存JWT Token
const saveJWTToken = (name, token, type) => {
// 保存JWT Token到cookie
const expires = 'Fri, 31 Dec 2023 23:59:59 GMT';
const path = 'dpde.cofco.com';
switch(type){
case 'localStorage':
// 保存JWT Token到localStorage
localStorage.setItem(name, token);
break;
case 'cookie':
document.cookie = `${name}=${token}; expires=${expires}; path=${path}`;
break;
case 'sessionStorage':
// 使用第三方存储, 如sessionStorage、IndexedDB或localForage等, 来保存JWT Token
sessionStorage.setItem(name, token);
break;
}
}
// 获取JWT Token
const getJWTToken = (name, type) => {
// const regExp = new RegExp("/(?:(?:^|.*;\s*)" + name + "}\s*\=\s*([^;]*).*$)|^.*$/","$1")
let r = null
switch(type){
case 'localStorage':
// 获取localStorage中的JWT Token
r = localStorage.getItem(name);
break;
case 'cookie':
// 获取cookie中的JWT Token
r = document.cookie.replace(/(?:(?:^|.*;\s*)jwtToken\s*\=\s*([^;]*).*$)|^.*$/, "$1");
// return token;
// r = document.cookie.replace(regExp)
break;
case 'sessionStorage':
r = sessionStorage.getItem(name);
break;
}
return r;
}
class JwtToken {
constructor() {
this.name = 'jwtToken'
this.saveType = 'cookie' // 'localStorage'
const token = getMockJWTToken()
if(token){
saveJWTToken(this.name, token, this.saveType)
// 获取JWT Token
this.token = getJWTToken(this.name, this.saveType);
}
}
}
const t = new JwtToken()
export default t
APP1/src/service/request.js
import axios from 'axios'
import t from './token.js'
const baseURL = "https://dpdetest.cofco.com"
// 给axios实例配置公共的基础配置
const settingDefault = () => {
axios.defaults.baseURL = baseURL
axios.defaults.timeout = 120000
axios.defaults.withCredentials = true // 跨域请求,允许保存cookie
axios.defaults.headers = {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': 'https://dpdetest.cofco.com',
'Access-Control-Allow-Credentials': true,
'Authorization': t.token
}
}
class Request {
constructor() {
const config = settingDefault()
this.request = axios.create(config) // 创建一个axios实例
// 请求拦截器
this.request.interceptors.request.use(config => {return config}, err => {return Promise.reject(err)})
// 响应拦截器
this.request.interceptors.response.use(res => {return res.data}, err => {return Promise.reject(err)})
}
}
// 通用请求
const r = new Request()
export default r
APP1/src/service/index.js
import r from './request.js'
export const test1 = () => {
const url = 'https://mockapi.eolink.com/QPBsKDib202fd2985773ebd4a85b7fb54b6cde5744a7998/user/points.php?responseId=1515550'
return r.request.get(url)
}
APP1/src/views/HomeViwer.vue
This is an Home page
Axios
[异步数据] status:, points:
app2
1. 安装 axios
sudo yarn add axios
2. app2/src/service/token.js
// mock JWT Token
const getMockJWTToken = () => {
return 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJkYXRhODg4OCI6W3sidG9vbHR0ODg4OCI6Imh0dHBzOi8vdG9vbHR0LmNvbSJ9XSwiaWF0IjoxNjk2NzQ5MzUyLCJleHAiOjE3MDM5NTE5OTksImF1ZCI6ImNvZmNvIiwiaXNzIjoiY29mY28iLCJzdWIiOiJjb2ZjbyJ9.hvgKkGJxNoqBkWnbSvCPcm_IaC22crQHqXVEvQs16gY'
}
// 在前端保存JWT Token通常有以下几种方式:
// 保存JWT Token
const saveJWTToken = (name, token, type) => {
// 保存JWT Token到cookie
const expires = 'Fri, 31 Dec 2023 23:59:59 GMT';
const path = 'dpde.cofco.com';
switch(type){
case 'localStorage':
// 保存JWT Token到localStorage
localStorage.setItem(name, token);
break;
case 'cookie':
document.cookie = `${name}=${token}; expires=${expires}; path=${path}`;
break;
case 'sessionStorage':
// 使用第三方存储, 如sessionStorage、IndexedDB或localForage等, 来保存JWT Token
sessionStorage.setItem(name, token);
break;
}
}
// 获取JWT Token
const getJWTToken = (name, type) => {
// const regExp = new RegExp("/(?:(?:^|.*;\s*)" + name + "}\s*\=\s*([^;]*).*$)|^.*$/","$1")
let r = null
switch(type){
case 'localStorage':
// 获取localStorage中的JWT Token
r = localStorage.getItem(name);
break;
case 'cookie':
// 获取cookie中的JWT Token
r = document.cookie.replace(/(?:(?:^|.*;\s*)jwtToken\s*\=\s*([^;]*).*$)|^.*$/, "$1");
// return token;
// r = document.cookie.replace(regExp)
break;
case 'sessionStorage':
r = sessionStorage.getItem(name);
break;
}
return r;
}
class JwtToken {
constructor() {
this.name = 'jwtToken'
this.saveType = 'cookie' // 'localStorage'
const token = getMockJWTToken()
if(token){
saveJWTToken(this.name, token, this.saveType)
// 获取JWT Token
this.token = getJWTToken(this.name, this.saveType);
}
}
}
const t = new JwtToken()
export default t
3. app2/src/service/request.js
import axios from 'axios'
import t from './token.js'
const baseURL = "https://dpdetest.cofco.com"
// 给axios实例配置公共的基础配置
const settingDefault = () => {
axios.defaults.baseURL = baseURL
axios.defaults.timeout = 120000
axios.defaults.withCredentials = true // 跨域请求,允许保存cookie
axios.defaults.headers = {
'Content-Type': 'application/json;charset=UTF-8',
'Access-Control-Allow-Origin': 'https://dpde.cofco.com', // 跨域请求,允许保存cookie
'Access-Control-Allow-Credentials': true,
'Authorization': 'Bearer ' + t.token,
}
}
class Request {
constructor() {
const config = settingDefault()
// 创建一个axios实例
this.request = axios.create(config)
// 请求拦截器
this.request.interceptors.request.use(config => {
return config
}, err => {
return Promise.reject(err)
})
// 响应拦截器
this.request.interceptors.response.use(res => {
return res.data
}, err => {
return Promise.reject(err)
})
}
}
// 通用请求
const r = new Request()
export default r
4. app2/src/service/index.js
import r from './request.js'
export const test1 = () => {
const url = 'https://mockapi.eolink.com/QPBsKDib202fd2985773ebd4a85b7fb54b6cde5744a7998/user/points.php?responseId=1515550'
return r.request.get(url)
}
5. app2/src/App.js
import logo from './logo.svg';
import './App.css';
import { test1 } from './service/index.js'
import { useState, useRef } from 'react';
var isUpdate = false;
function App () {
const [state, setState] = useState({ status: '', points: '' });
function updateState (info) {
setState({ status: info.status, points: info.points });
}
async function axiosTest() {
const r = await test1()
if(r) {
updateState(r)
isUpdate = true
}
}
if(!isUpdate){
axiosTest()
}
return (
[异步数据] status:, points:
Learn React
);
}
export default App;
6. 接口proxy
安装中间件http-proxy-middleware
在app2/src/setupProxy.js
中添加:
const { createProxyMiddleware } = require('http-proxy-middleware');
module.exports = function(app) {
app.use(
'/api',
createProxyMiddleware({
target: 'https://dpdetest.cofco.com',
changeOrigin: true,
// withCredentials: true,
pathRewrite: {
'^/api': '',
}
})
);
};
微应用---跨域配置注意事项
微应用允许跨域访问设置
微应用 需要设置 允许 跨域访问, 主应用 才能加载 微应用 的 资源。
- 开发环境:
需要设置打包配置文件的 devServer 的 headers 属性
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Credentials': true,
}
- jsonp会创建一个script元素加载数据,正常情况script会被拦截导致jsonp请求失败,此时可以给script元素添加ignore属性,跳过拦截。
// 修改jsonp方法,在创建script元素后添加ignore属性
const script = document.createElement('script')
script.setAttribute('ignore', 'true')
- 生产环境:
微应用nginx 需要配置:'Access-Control-Allow-Origin': '主应用域名'
或者'Access-Control-Allow-Origin': '*'
微应用获取cookie
- 微应用nginx 需要配置:
'Access-Control-Allow-Origin': '主应用域名'
,主应用域名不可为 '*' - 微应用nginx需要开启:
'Access-Control-Allow-Credentials':true
- 微应用接口请求需配置
baseURL: '微应用的服务地址'
- 微应用接口请求需允许
withCredentials: true
微应用JWT Token
主应用通过单点登录方式登录,一般token信息会由服务端自动追回到数据请求头中.
前端也可将 token 信息存入 localStorage/cookie/sessionStorage/... 中.
cookie 注意事项
针对浏览器存在cookie,前端却获取不到的问题:
- httpOnly默认为true打对号√情况下,禁止javascript操作cookie,导致获取不到,可以让后端设置false;
- 后端使用node+koa,种客户端一个cookie,但是在客户端内通过document.cookie获取不了此cookie。经查是由于koa通过ctx.cookies.set(name, value, [options])种的cookie是自动默认带httpOnly的,httpOnly是服务器可访问 cookie, 默认是 true。禁止javascript操作cookie(为避免跨域脚本(xss)攻击,通过javascript的document.cookie无法访问带有HttpOnly标记的cookie。)
所以通过在后端设置ctx.cookies.set(name, value, {httpOnly:false})关掉httponly即可。
————————————————
原文链接:https://blog.csdn.net/i_am_a_div/article/details/108223553
微应用通信
子应用 向上发给 基座
1. 基座
基座 接收 子应用 的数据, 监听子应用dispatch
发出的数据, 有2种方式:
- 使用
micro-app
组件的@datachange
属性监听子应用dispatch发出的数据, 如:
注意:datachange绑定函数的返回值不会作为子应用dispatch回调函数的入参,它的返回值没有任何作用。
- 绑定监听函数, 使用
microApp.addDataListener('子应用名', (data)=> {...})
监听子应用dispatch发出的数据, 如下:
// 监听 子应用数据 - app1
microApp.addDataListener('app1', data => {
console.log('来自子应用 app1 的数据', data, '[base]')
return '返回值1'
})
// 监听 子应用数据 - app2
microApp.addDataListener('app2', data => {
console.log('来自子应用 app2 的数据', data, '[base]')
return '返回值1'
})
2. 子应用app
window.microApp.dispatch({data: '来自 子应用 的数据'}, ()=>{
console.log('microApp dispatch success [app1 -> base]')
})
子应用中
microApp
必需用window.microApp
3. forceDispatch:强制发送
forceDispatch方法拥有和dispatch一样的参数和行为,唯一不同的是forceDispatch会强制发送数据,无论数据是否变化。
例如:
// 强制发送数据,,无论数据是否变化, 即无论缓存中是否已经存在 name: 'jack' 的值
window.microApp.forceDispatch({name: 'jack'}, () => { console.log('数据已经发送完成'); });
主应用向子应用发送数据 or 子应用 获取 基座发出的数据
1. 基座
基座 发出的数据 有2种方式:
- 使用
micro-app
组件的data
属性发出, 如: - 使用
microApp.setData({...})
函数发出, 如下:
// setData(appName: String, data: Object) 发送数据 给 子应用,setData第二个参数只接受对象类型
microApp.setData(appName, data, callback)
console.warn(`base -> ${appName}: "${data.msg} [base]"`)
2. 子应用-app1
if(window.__MICRO_APP_ENVIRONMENT__) {
// 清空当前子应用的所有绑定函数(全局数据函数除外)
window.microApp.clearDataListener()
// 直接获取 主应用下发的数据
const data1 = window.microApp.getData() // 返回主应用下发的data数据
console.warn('直接获取 base下发的数据 - window.microApp.getData(): ', data1?.msg, ' [app1]')
// 监听获取 主应用下发的数据
const autoTrigger = true
const dataListener = (data) => { console.warn('监听获取 base下发的数据 - window.microApp.addDataListener:', data?.msg, '[app1]') }
// 监听数据变化 --- window.microApp.addDataListener(dataListener: Function, autoTrigger?: boolean)
window.microApp.addDataListener(dataListener, autoTrigger)
}
2. 子应用-app2
if(window.__MICRO_APP_ENVIRONMENT__) {
... // 同上
// 监听获取 主应用下发的数据
const autoTrigger = true
const dataListener = (data) => { console.warn('监听获取 base下发的数据 - window.microApp.addDataListener:', data?.msg, '[app2]') }
// 监听数据变化 --- window.microApp.addDataListener(dataListener: Function, autoTrigger?: boolean)
window.microApp.addDataListener(dataListener, autoTrigger)
}
全局数据通信
全局数据通信会向 主应用 和 所有子应用 发送数据,在跨应用通信的场景中适用。
1. 发送全局数据
// setGlobalData只接受对象作为参数
microApp.setGlobalData({token: 'global data xxxxx'})
注: 发送全局数据可以 基座 也可以 子应用 里写.
发送的数据都会被缓存下来, micro-app会遍历新旧值中的每个key判断值是否有变化,如果所有数据都相同则不会发送(注意:只会遍历第一层key),如果数据有变化则将新旧值进行合并后发送。
2. forceSetGlobalData:强制发送
3. 获取全局数据
- 方式1. 直接获取数据
const globalData = microApp.getGlobalData() // 返回全局数据
- 方式2. 绑定监听函数
const _data = window.microApp.getGlobalData()
console.warn(`直接获取 全局数据: ${JSON.stringify(_data)} [app1]`)
function dataListener (_data) { console.warn(`监听获取 全局数据: ${JSON.stringify(_data)} [app1]`) }
const autoTrigger = true
// 解绑监听函数
window.microApp.removeGlobalDataListener(dataListener)
window.microApp.addGlobalDataListener(dataListener, autoTrigger)
兼容测试 []
micro-app依赖于CustomElements和Proxy两个较新的API。
对于不支持CustomElements的浏览器,可以通过引入polyfill进行兼容,详情可参考:webcomponents/polyfills。
但是Proxy暂时没有做兼容,所以对于不支持Proxy的浏览器无法运行micro-app。
浏览器兼容性可以查看:Can I Use
总体如下:
- PC端:除了IE浏览器,其它浏览器基本兼容。
- 移动端:ios10+、android5+
子应用通过a标签下载文件测试 []
子应用通过a标签下载文件失败
原因:当跨域时(主应用和文件在不同域名下),无法通过a标签的download属性实现下载。
解决方式:
方式1:转换为blob形式下载
下载
// 通过blob下载文件
function downloadFile (e) {
// 微前端环境下转换为blob下载,子应用单独运行时依然使用a标签下载
if (window.__MICRO_APP_ENVIRONMENT__) {
e.preventDefault()
// 注意href必须是绝对地址
fetch(e.target.href).then((res) => {
res.blob().then((blob) => {
const blobUrl = window.URL.createObjectURL(blob)
// 转化为blobURL后再通过a标签下载
const a = document.createElement('a')
a.href = blobUrl
a.download = 'filename.png'
a.click()
window.URL.revokeObjectURL(blobUrl)
})
})
}
}
方式2:将文件放到主应用域名下,判断微前端环境下a标签href属性设置为主应用的文件地址
全局共享
global
当多个子应用使用相同的js或css资源,在link、script设置global属性会将文件提取为公共文件,共享给其它应用。
设置global
属性后文件第一次加载会放入公共缓存,其它子应用加载相同的资源时直接从缓存中读取内容,从而提升渲染速度。
1. 在基座里, 给需要子应用共享的资源加上global属性
2. 子应用里可直接使用
mounted() {
...
console.log('jQuery = ', jQuery, '[app1]') // jQuery = ƒ (e,t){return new ce.fn.init(e,t)} [app1]
}
globalAssets
测试兼容不好, 有问题
globalAssets用于设置全局共享资源,它和预加载的思路相同,在浏览器空闲时加载资源并放入缓存,提高渲染效率。
当子应用加载相同地址的js或css资源时,会直接从缓存中提取数据,从而提升渲染速度。
// index.js
import microApp from '@micro-zoe/micro-app'
microApp.start({
globalAssets: {
js: ['xxx, 'xxx', ...], // js地址
css: ['css地址1', 'css地址2', ...], // css地址
}
})
exclude(过滤元素)
当子应用不需要加载某个js或css,可以通过在link、script、style设置exclude属性,当micro-app遇到带有exclude属性的元素会进行删除。
ignore(忽略元素)
当link、script、style元素具有ignore属性,micro-app不会处理它,元素将原封不动进行渲染。
使用场景例如:jsonp
jsonp会创建一个script元素加载数据,正常情况script会被拦截导致jsonp请求失败,此时可以给script元素添加ignore属性,跳过拦截。
// 修改jsonp方法,在创建script元素后添加ignore属性
const script = document.createElement('script')
script.setAttribute('ignore', 'true')
全局配置
全局配置会影响每一个子应用,请小心使用!
import microApp from '@micro-zoe/micro-app'
microApp.start({
iframe: true, // 默认值false
inline: true, // 默认值false
destroy: true, // 默认值false
shadowDOM: true, // 默认值false
ssr: true, // 默认值false
'disable-scopecss': true, // 默认值false
'disable-sandbox': true, // 默认值false
'keep-alive': true, // 默认值false
'disable-memory-router': true, // 默认值false
'keep-router-state': true, // 默认值false
'disable-patch-request': true, // 默认值false
}
微应用-路由
1. 子应用 内部路由
使用(window.)microApp.route.push
如: 当前子应用的路由路径是: /home
跳转至: /about
window.microApp.router.push({
name: 'app1', // 子应用名称
path: '/about' // config是配置文件 item是当前点击的菜单路径信息
});
url: http://localhost:8080/#/page/app1?app1=%2Fabout
子应用中
1. 返回基座的首页
window?.microApp?.dispatch({ isBackHome: true });
在基座项目的子应用页面中加入监听
// 监听 子应用数据 - app1
microApp.addDataListener(appName, data => {
if(data?.isBackHome){
this.$router.push({ path: '/Home' })
}
return '返回值1'
})
2. 返回子应用的首页
this.$router.replace({ path: '/home' })
子应用1 to 子应用2 [待测试]
- 发送方
if (window.__MICRO_APP_ENVIRONMENT__) {
window.microApp.setGlobalData( data )
}
- 接收方
if (window.__MICRO_APP_ENVIRONMENT__) {
const data = window.microApp.getGlobalData()
}
资源复用
1. 父应用资源的暴露
使用 url 引入的方式调用模块包,默认注册位置 public/index.html 入口文件
于父框架微前端初始化时, 注册需要暴露的资源,默认注册位置 main.js
if(!window.__MICRO_APP_ENVIRONMENT__){
microApp.start(
{
globalAssets: {
js: ['https://cdn.jsdelivr.net/npm/[email protected]/dist/vue.min.js'], // js地址
// css: ['css地址1', 'css地址2', ...], // css地址
}
}
)
}
2. 子应用使用父级资源
于入口文件通过 url 方式引入和父框架一样地址的资源。
子应用相同地址的资源匹配到父框架时,便能直接使用父框架的资源,而无需自己加载,起到资源复用的作用。
应用之间跳转
每个应用的路由实例都是不同的,应用的路由实例只能控制自身,无法影响其它应用,包括基座应用无法通过控制自身路由影响到子应用。
常见的问题如:开发者想通过基座应用的侧边栏跳转,从而控制子应用的页面,这其实是做不到的,只有子应用的路由实例可以控制自身的页面。
- 要实现应用之间的跳转有两种方式:
方式一、window.history 会兼容问题
方式二、通过数据通信控制跳转
window.microApp.addDataListener((data) => {
if(data?.path) { router.push(data.path); }
});
import microApp from '@micro-zoe/micro-app'
microApp.setData('app1', { path: '/new-path' });
方式三、传递路由实例方法
适用场景: 子应用控制基座跳转
-
实现步骤
baseRoute
如果不想使用自动补全路由参数这类的, 可以在
中指定baseroute属性
例如: 当前主应该路由为http://localhost:3000/main-vue2/
跳转至 /app-vue2
时想成为: http://localhost:3000/main-vue2/app-vue2
时, 使用如下:
baseroute
Desc: 设置子应用的基础路由
Type: string
Default: ''
使用方式:
在微前端环境下,子应用可以从window.MICRO_APP_BASE_ROUTE上获取baseroute的值,用于设置基础路由。
默认情况下,baseroute的功能是被禁止的,若要开启需先关闭memory-router
主/子应用路由示例
JWT 登录测试
http://10.8.47.134/oauth/authorize?response_type=code&client_id=daping&scope=daping.read&redirect_uri=http://10.8.47.134/oauth/token/daping
一些问题处理
公共模块代码
可以将公共代码单独创建一个仓库,在各个项目中以子模块的形式引入到项目中Nginx 部署
微前端在部署到多个地址时,可以通过 nginx 的反向代理,把子项目的地址代理到主项目地址下,这样可以是主项目和子项目使用相同的 storage 存储
// const TARGET = {
// 1: 'https://dpde.cofco.com', // 正式环境
// 2: 'https://dpdetest.cofco.com', // 内网测试环境
// 3: 'http://10.8.47.114', // zhi quan
// 4: 'http://10.8.218.136:80', // 唐阳
// 5: 'http://10.6.58.68:4050' // 测试环境微服务
// }