前言
微前端是搭建起来了,但是要用起来啊,把原来的几个项目集成进来的过程遇到问题总结。
零、沙箱设置的简单理解
strictStyleIsolation = false
可以获取到子应用的dom节点,主应用可修改子应用样式,但是子应用不可修改主应用的样式。需要注意样式不能冲突。
strictStyleIsolation = true
样式严格分离,不可获取到子应用的dom节点。
一、vue-cli2搭建的老项目微应用配置
1.1、打包配置:
webpack配置在哪呢???
熟读官方文档发现,打包配置为了让主应用能识别子应用暴露出来的信息,子应用打包需要进行的配置。
关键词:子应用、打包、暴露
那可不是在子应用的webpack中配置output,因为run dev和run build都需要对外暴露吧,那就是配置到webpack.base.config.js咯?打开一看果然原来就有output属性。
于是追加
const packageName = require('../package.json').name;
library: `${packageName}-[name]`,
libraryTarget: 'umd',
jsonpFunction: `webpackJsonp_${packageName}`,
即可。
此处注意,官方文档
require('./package.json').name;,
此处根据项目文件位置应该是
require('../package.json').name;
多了一层!!!
配置正确后,运行npm run dev成功启动项目。
发现单独访问可以,但是通过主应用访问报跨域的错误提示。需要查看跨域配置是否正确。
1.2、跨域配置:
你以为在config/index.js下的dev里面吗?不是的!!!
生产环境的跨域是在nginx中进行配置,那开发环境的跨域就是在webpack.dev.config.js中咯。
打开文件一看果然有个devServer属性,于是在其下添加配置项:
headers: {
'Access-Control-Allow-Origin': '*'
},
即可。
1.3、附webpack官方文档:
https://webpack.js.org/configuration/output/
二、res中无法拿到router对象
在js文件比如axios的返回拦截res中无法拿到router对象进行路由跳转
挂到原生方法上即可。
if (!window.__POWERED_BY_QIANKUN__) {
render()
Vue.prototype.$subRouter = router
}
Vue.prototype.$subRouter.push({ name: 'Login', params: { message: err.response.data.message } })
引申问题:
此时是独立渲染的时候,把路由挂在了原生上,但是以子应用嵌入微前端时。
子应用在请求返回res中得知超时,需要跳转登录界面,是无法拿到原生router的,只能通知主应用,进行路由跳转。
详见父子应用间的监听传值。(下一个问题)
三、父子应用通过props传值
问题:
子系统登录超时res跳转不了登录界面,需要通知主应用进行跳转。
解决:
建议用props传值处理。
主应用main.js注册子应用
{
name: 'XxxSubSystem',
entry: '//10.10.26.197:8091',
container: '#container',
activeRule: '/test/xxx-sub-system',
props: { // 额外参数-用于父子应用之间相互调用
getToken: () => {
console.log('获取token')
},
reRegister: (message) => {
router.push({ name: 'Login', params: { message: message } })
console.log('重新登录')
}
}
},
子应用main.js挂载方法
function render (props = {}) {
const { container } = props
router = new VueRouter({
base: window.__POWERED_BY_QIANKUN__ ? '/test/xxx-sub-system/' : '/',
mode: 'history',
routes
})
instance = new Vue({
router,
store,
render: h => h(App)
}).$mount(container ? container.querySelector('#app') : '#app')
// 将主应用的函数挂到原生上方便调用
Vue.prototype.$baseReRegister = props.reRegister
Vue.prototype.$baseGetToken = props.getToken
}
子应用接口返回res/index.js超时拦截
if (errCode === 401) {
if (!window.__POWERED_BY_QIANKUN__) {
Vue.prototype.$subRouter.push({
name: 'Login', params: {
message: err.response.data.message
}
})
} else {
Vue.prototype.$baseReRegister(err.response.data.message)
}
}
四、来回切换子应用容易崩了
报错:
application ‘SubPhm’ died in status SKIP_BECAUSE_BROKEN: Cannot read property ‘replace’ of undefined
现象:
div=container的节点存在,子应用挂上去了;
但是加载失败,子应用挂上了,但是挂了。
原因分析:
来回切换子系统的时候,id为container的DOM节点不断的进行结构渲染导致的崩溃。
原来子应用直接挂在app中的div,切换应用时,整个dom重新渲染。
改造方法:
将子应用挂到子路由下面,路由跳转时中的部分肯定是要重新渲染的,所以子应用切换时,也只是渲染
路由部分,保持了外层不动,减轻浏览器重绘压力。
详见下一点,如何将子应用挂在子路由下。
五、如何将子应用挂在子路由下
改造前:
主应用App.vue
<template>
<div id="app-base">
<router-view v-if="isLoginPage" />
<el-container v-else>
……
<el-container>
……
<div v-if="isSubRoute" id="container" class="base-content"></div>
<router-view class="base-content" />
</el-container>
</el-container>
</div>
</template>
主应用router.js
const routes = [
{
path: '/',
name: 'Default',
redirect: '/login'
},
{
path: '/login',
name: 'Login',
component: Login
},
{
path: '/app',
name: 'App',
component: App
}
]
主应用main.js
{
name: 'SubSystem',
entry: '//IP:PORT',
container: '#container',
activeRule: '/sub-system'
},
子应用main.js
router = new VueRouter({
base: window.__POWERED_BY_QIANKUN__ ? '/sub-system/' : '/',
mode: 'history',
routes
})
改造后:
主应用App.vue
<template>
……
<router-view class="base-content" />
</template>
主应用baseSub.vue
<template>
<div id="container"></div>
</template>
主应用router.js
const routes = [
{
path: '/',
name: 'Default',
redirect: '/login'
},
{
path: '/login',
name: 'Login',
component: Login
},
{
path: '/basesub/*',
name: 'BaseSub',
component: BaseSub,
children: [
]
},
{
path: '/app',
name: 'App',
component: App
}
]
主应用main.js
{
name: 'SubSystem',
entry: '//IP:PORT',
container: '#container',
activeRule: '/basesub/sub-system'
},
子应用main.js
router = new VueRouter({
base: window.__POWERED_BY_QIANKUN__ ? '/ basesub/sub-system/' : '/',
mode: 'history',
routes
})
六、图片静态文件的引用
问题:
在static下的需要手动加上子应用IP和端口,webpack打包只给相对路径src下的资源加上了。
建议:
将静态资源,如CSS、图片、JS代码放置在src文件夹里面,这样才会受publicPath的打包路径自动配置__webpack_public_path__。
七、iconfont需要在主应用引入?
iconfont需要在主应用引入,否则无法加载出图标。
但是本人并没有遇到此现象,原来以为图标有问题,并不是iconfont的问题。
八、el-icon显示异常
问题:
两个子系统,element查看节点一模一样。
但是显示效果一个能显示出.el-icon-arrow-down的图标,一个无法显示。
解决:
起初以为是iconfont图标引入的问题,后来发现这是el-icon啊。
于是注释掉子应用的如下引入,就能正常显示了。
// import ‘element-ui/lib/theme-chalk/index.css’
但此时单独运行子应用时,整个element-ui的样式结构就乱了。
于是查看主应用和子应用的element-ui版本,果然主应用和正常子应用的版本远远高于问题子应用的版本,所以升级问题子应用的element-ui版本即可。
由此可得,主应用的CSS样式尽可能的自己手写,避免使用一些UI框架后,样式与微应用的产生冲突。
九、主子应用登录问题
问题:
1、 主子应用不共用同一套后台登录。
2、 主子应用共用一套后台登录。
解决:(两套登录)
访问子系统login页面,写死用户名密码登录跳转,使用的token都是子应用自己的。
解决:(一套登录)
1、 主应用直接访问子应用的home界面,不访问login登录界面了。
<router-link to="/xxxyx/xxx-sub-system/home">
<i class="el-icon-s-tools"></i>
<p>系统</p>
</router-link>
2、 主应用注册子应用,添加token同步方法。
{
name: 'XxxSubSystem',
entry: '//10.10.26.197:8091',
container: '#container',
activeRule: '/xxxyx/xxx-sub-system',
props: { // 额外参数-用于父子应用之间相互调用
getToken: (subKey) => {
const yxtoken = window.localStorage.getItem('XXXYX_TOKEN')
const yxuser = window.localStorage.getItem('XXXYX_USER')
window.localStorage.setItem(subKey + '_TOKEN', yxtoken)
window.localStorage.setItem(subKey + '_USER', yxuser)
},
reRegister: (message) => {
router.push({ name: 'Login', params: { message: message } })
console.log('重新登录')
}
}
},
3、 启动子应用时主动获取主应用的token和user
export async function mount (props) {
console.log('[vue] props from main framework', props)
// 将主应用的函数挂到原生上方便调用
Vue.prototype.$baseReRegister = props.reRegister
// 设置公共变量为微前端开启
store.state.qiankun = true
// 获取主应用的token存为己用
props.getToken('XXXCA')
// window.localStorage.setItem('', )
render(props)
}
十、element-ui挂到外层body上
问题:
element-ui中,收缩的菜单挂在了外面的body下,导致样式各种不对。
所以凡是挂在外层body下的样式需要去主应用写一遍。
解决:
主应用的CSS样式尽可能的自己手写,避免使用一些UI框架后,样式与微应用的产生冲突。
还是有问题:
凡是子应用使用了el-menu的vertical形式,挂在了body外层,而样式控制不到外层,导致挂在外层的菜单样式还是不
受控啊。
最终解决方案:
1、首先站在主应用不使用ui框架的基础上,另外如果简单地icon、message这些无所谓,只要子应用不去修改这些样式
就行。
2、监听路由的变化,判断是哪个子系统,给body绑上对应的样式名称。
watch: {
$route: {
handler: function (val) {
if (val.path.indexOf('sub-system') !== -1) {
document.body.className = 'sub-system'
} else if (val.path.indexOf('sub-phm') !== -1) {
document.body.className = 'sub-phm'
}
},
immediate: true
}
},
3、针对每套子系统引入一个样式文件,仅控制改子系统下的样式。
@import "./assets/css/subs/sub-phm.scss";
@import "./assets/css/subs/sub-system.scss";
// sub-phm.scss文件内容如下:
.sub-phm {
.el-menu {
border-right: 1px solid #01e4fd;
}
……
}
// sub-system.scss文件内容如下:
.sub-system {
.el-menu--collapse {
width: 220px;
}
……
}
十一、第三方引入-高德地图
报错一:
## modules?v=1.4.15&key=5211f0aad4612e1ff705395b665bf085&vrs=1606397679220&m=mouse,vectorlayer,overlay,wgl,AMap.ControlBar,vectorlayer,wgl,AMap.CustomLayer,rbush,Map3D,AMap.DistrictSearch,sync:1 Uncaught ReferenceError: _jsload_ is not defined
解决:
估计是子应用引入地图时无法往window上挂_jsload_函数,以至于需要使用时报错。
需要在主应用引入高德地图即可。
<script
src="https://webapi.amap.com/maps?
v=1.4.15&key=5211f0aad4612e1ff705395b665bf085&plugin=AMap.ControlBar,Map3D,AMap.DistrictSearch">
</script>
主应用若不使用,无需定义全局变量Amap。
module.exports = {
configureWebpack: {
// 全局常量定义
// externals: {
// AMap: 'AMap' // 高德地图
// }
}
}
需要注意的是,如果子应用引入了高德地图,通过主应用加载子应用会挂掉,需要在引入高德地图时加上ignore标识。
报错二:
使用import AMapLoader from '@amap/amap-jsapi-loader’的形式在各子系统引入高德地图
切换子系统两个地图展示界面时,后加载的界面会报错:
解决:
经过比较两个子系统加载的方式、版本、密钥值等都一模一样。
定位很久,手动调用AMapLoader.clear(),强制删除window.AMap对象都没用。
最后发现qiankun好像对window上的AMap对象进行了代理,怀疑是qiankun和amap/amap-jsapi-loader冲突,但是我没有什么证据。
只好在qiankun这里想办法了,最后想到在子系统切换的时候刷新页面完全重新加载就行了。
代码如下:
beforeMount: (app) => {
// 每次加载子系统都执行
if (window.AMap) { // 如果地图对象存在,刷新页面,不然地图报错啊头大
location.reload()
}
}
十二、第三方引入-ces地图
报错:
[Vue warn]: Error in mounted hook: "ReferenceError: Cesium is not defined"
因为Cesium的包没有正常引入
解决(步骤):
原本这么引入:
<link rel="stylesheet" :href="$store.state.publicPath + '/static/Cesium/Widgets/widgets.css'">
<script :src="$store.state.publicPath + '/static/Cesium/Cesium.js'"></script>
在index.html中使用$store.state.publicPath是无效的,通过浏览器可见。
所以子应用只能如下引入:
<link rel="stylesheet" href="/static/Cesium/Widgets/widgets.css">
<script src="/static/Cesium/Cesium.js"></script>
此时子应用单独访问,使用ces地图正常,但是嵌入到微前端框架中继续报错:
Uncaught Error: application 'XxxSubCa' died in status LOADING_SOURCE_CODE: [qiankun] You need to export lifecycle functions in XxxSubCa entry
子应用直接挂了,只好注释掉如上引入,好歹子应用能正常挂载,但是报错如下:
[Vue warn]: Error in mounted hook: "ReferenceError: Cesium is not defined"
又回到了Cesium未定义,因为包没引入啊,只好在主应用引入一遍即可。
代码:
子应用注释掉Cesium引入:
<!-- <link rel="stylesheet" href="/static/Cesium/Widgets/widgets.css"> -->
<!-- <script src="/static/Cesium/Cesium.js"></script> -->
主应用引入Cesium:
<link rel="stylesheet" href="/static/Cesium/Widgets/widgets.css">
<script src="/static/Cesium/Cesium.js"></script>
不要忘了把包拷到主应用的静态文件夹下!
十三、embed嵌入多媒体
可以手动追加子应用的IP和端口,加载资源。
问题:
无法绑定点击事件,因为获取不到子应用dom。
需要在element展开embed点击document节点才能通过ID获取其下节点。
现象:
1、 改造路由以后也不可以。
2、 潮安子系统、地铁子系统凡是embed标签引入的SVG效果一样,直接复制进来的SVG可任意获取节点。
3、 和SVG的大小无关,删除到120kb的SVG也无法获取节点。
4、 尝试了其他SVG引入方式比如标签都不可以
结论:
MapInit () {
const self = this
self.svgDoc = document.getElementById('svg2').getSVGDocument()
console.log(document.getElementById('svg2'), 111)
console.log(self.svgDoc, 222)
if (self.svgDoc === null) {
clearTimeout(self.timeSvgDoc)
self.timeSvgDoc = setTimeout(() => {
self.MapInit()
}, 1000)
} else {
}
}
凡是直接通过根目录去外联.svg文件的,虽然初次获取不到embed里面的svg,等加载完后还是可以获取到的。
但是通过跨域的形式外联.svg文件的,虽然文件能够加载,但是. getSVGDocument()方法无法获取embed下的svg节点。
此时单独访问子应用,未拼接ip:port直接从static文件夹下外联svg文件,是可以正常获取embed下的svg节点的。
此时直接在主应用中外联的svg文件,也是正常的,因为没有跨域。
总而言之 跨域外联的svg,是无法获取embed下的svg文件的。
解决:
将子应用需要外联的SVG拷贝到主应用相同的文件位置存放即可。
十四、接口调用的问题
原问题:
子应用调用接口时,因为请求的源是主应用的IP:PORT,导致请求拦截需要配置到主应用下,且不能和主应用的拦截发
生冲突。
问题(拦截配置在主应用):
1、 需要保证子应用的拦截和主应用的拦截不冲突。
2、 所有的请求处理都是经过主应用,导致主应用的压力很大。
3、 部署时,若子应用接口调用修改,也需要重新配置主应用并重启。
4、 若子应用的后台和主应用的前端不在同一网段,则主应用是无法调通子应用的后台接口的。
所以,不可将拦截配置在主应用中。
问题(拦截配置在各自应用):
通过子应用的拦截,需要追加子应用的IP:PORT,导致浏览器存在跨域的提示,无法正常调用接口。
通过network可以看到,浏览器首先发了个options的请求去试探后台 ,后台因为没有允许跨域,于是返回了403,那原
本的post请求就不会继续。
解决思路:
1、 后台允许跨域
2、 前端将options的返回403强制改成204,继续后续请求操作。
3、 绕过options不让浏览器发送试探的请求。
解决办法:
1、 后台修改,允许跨域,或者处理所有options请求返回200。
优点:简单
缺点:不安全,且需要后台配合修改代码。
2、 避免“OPTIONS”请求,需要前后请求的头部保持一致。
优点:无
缺点:需要前后端配合,一旦出现特殊请求需要特殊处理(比如附件下载等类型不同的请求)
3、 避免“OPTIONS”请求,需要将请求改造成简单请求。
因为我们的请求默认为json格式,是非简单请求,如果将请求类型设置成application/x-www-form-urlencoded,改造成简单请求,就能避免发送“OPTIONS”请求。
优点:无
缺点:同上,需要前后端配合,特殊情况需要特殊处理。
4、 前端处理
优点:简单
缺点:仅限nginx部署时能达到效果,本地开发配置无解。
十五、不同地址访问出现跨域问题
- 现象:
微前端主应用base访问地址为外网地址,子应用配置的内网地址,会出现跨域问题。 - 原因:
base所在的外网服务器,无法访问子应用所在的内网服务器,所以出现了跨域,无法访问私有地址的报错。