前言
微前端是搭建起来了,但是要用起来啊,把原来的几个项目集成进来的过程遇到问题总结。

零、沙箱设置的简单理解

strictStyleIsolation = false
可以获取到子应用的dom节点,主应用可修改子应用样式,但是子应用不可修改主应用的样式。需要注意样式不能冲突。
strictStyleIsolation = true
样式严格分离,不可获取到子应用的dom节点。

一、vue-cli2搭建的老项目微应用配置

1.1、打包配置:

webpack配置在哪呢???

前端 vite 本地打包镜像 微前端打包_Vue


熟读官方文档发现,打包配置为了让主应用能识别子应用暴露出来的信息,子应用打包需要进行的配置。

关键词:子应用、打包、暴露

那可不是在子应用的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标识。

前端 vite 本地打包镜像 微前端打包_Vue_02

报错二:

使用import AMapLoader from '@amap/amap-jsapi-loader’的形式在各子系统引入高德地图

切换子系统两个地图展示界面时,后加载的界面会报错:

前端 vite 本地打包镜像 微前端打包_Vue_03

解决:

经过比较两个子系统加载的方式、版本、密钥值等都一模一样。
定位很久,手动调用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所在的外网服务器,无法访问子应用所在的内网服务器,所以出现了跨域,无法访问私有地址的报错。