微前端框架qiankun项目实战(二)--踩坑与部署篇

2021-07-08 11:32:21 浏览数 (1)

点击上方 程序员成长指北,关注公众号

回复1,加入高级 Node 进阶交流群

作者:黑化程序员(作者授权转载) 链接:https://juejin.cn/post/6973111766767108103

大家好,我是小黑。

在上一篇《微前端框架qiankun项目实战(一)--本地开发篇》发布后,感谢有网友提出了微应用的缓存问题,的确基于第一篇使用的registerMicroApps方式很难做到缓存,要做到应用缓存的方式使用手动加载管理微应用的方式是最好的,我将再写一篇补充篇使用loadMicroApp手动管理微应用,本篇我会模拟部署一下主应用和微应用,并将揭开我上一篇所谓的巨坑是什么。

贴上我建好的模板仓库地址

vue3模板:https://gitee.com/jimpp/vue3-main-app

vue2模板:https://gitee.com/jimpp/vue2-micro-app

在上一篇中,master分支都是未改造前能独立运行的项目,dev分支是最终改造后的项目,本篇所有代码会在新建的test分支修改

隐藏微应用菜单和头部

在上篇的结尾,我们本地运行微前端的时候,发现微应用的菜单和头部还是渲染出来了

不知道亲爱的你是否有思路如何实现隐藏,下面给出我的思路代码

代码语言:javascript复制
// template
<div class="nav" v-if="showMenu">
  <div class="menu">
    <router-link to="/">Child Home</router-link>
  </div>
  <div class="menu">
    <router-link to="/about">Child About</router-link>
  </div>
</div>
<div class="container">
  <div class="header" v-if="showHeader">Child Header</div>
  <div class="router-view">
    <router-view />
  </div>
</div>

// js
computed: {
    ...mapState(["token"]),
    // 控制菜单显示隐藏
    showMenu() {
      return this.token && !this.isMicroEnc
    },
    // 控制头部显示隐藏
    showHeader() {
      return this.token && !this.isMicroEnc
    },
    isMicroEnc() {
      return window.__POWERED_BY_QIANKUN__
    }
  }

利用computed根据token 和 window.POWERED_BY_QIANKUN 去控制显示隐藏,效果如下

token放进本地缓存

这个过程中我们要不断地修改项目,一刷新就要重新登录实在太烦了,下面我们改造一下主应用,把登录后的token存到localStorage中

src/store/index.js

代码语言:javascript复制
mutations: {
    setToken(state, token) {
      state.token = token
      // 新增,登录的时候同时把token存到localStorage
      localStorage.setItem('token', token)
    }
 },
 
 // 新增
 const storagePlugin = store => {
  const token = localStorage.getItem('token')
  if(token) {
    store.commit('setToken', token)
  }
}

 plugins: [storagePlugin]

这里在setToken方法中添加了把token存到localStorage的逻辑,并编写了一个VuexstoragePlugin插件,该插件主要功能是在应用加载的时候去获取localStorage中的token,如果有的话直接commit到我们的store中,这样一来我们只要登录了,再刷新也不需要重新登录

接下来,准备开始踩坑了

坑1:样式冲突问题

首先遇到的样式冲突,不是什么ui库的冲突,而是iconfont的冲突,我是在改造两个线上项目的时候遇到的

首先去iconfont官网为两个应用添加两组图标

主应用的图标

微应用的图标

可以看到两个应用的图标命名是一致的,不过主应用是空心的,微应用是实心的

下载好的图标库是这样的

我们只需要拷贝iconfont.css、iconfont.ttf、iconfont.woff、iconfont.woff2这几个文件到src/assets目录下,然后在main.css引入就可以了

iconfont.css的代码如下

代码语言:javascript复制
@font-face {
  font-family: "iconfont"; /* Project id 2608947 */
  src: url('iconfont.woff2?t=1623503003854') format('woff2'),
       url('iconfont.woff?t=1623503003854') format('woff'),
       url('iconfont.ttf?t=1623503003854') format('truetype');
}

.iconfont {
  font-family: "iconfont" !important;
  font-size: 16px;
  font-style: normal;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

.icon-password:before {
  content: "ea41";
}

.icon-username:before {
  content: "e600";
}

main.css中引入

代码语言:javascript复制
@import url(./iconfont.css);

两个项目的引入方式是一样的,最后的目录结构如下:

然后再分别去到两个应用的views/Home.vue中添加两个图标

代码语言:javascript复制
<i class="iconfont icon-username"></i>
<i class="iconfont icon-password"></i>

刷新我们的浏览器

可以看到,当点击菜单切换时,都是空心图标,这明显有问题啊!我们明明一个有心有个无心!

如何解决?

当时在改造项目的过程中发现这个情况真的有点炸毛(fxxx = fine),不知道你是否有疑问,我为什么要把iconfont.css的代码贴出来,因为我们解决这个问题的关键就在于

代码语言:javascript复制
font-family: "iconfont";

大家可以看到两个项目的iconfont.css都有这么一句话,然后引入的方式都是class="iconfont icon-xxx"的方式,我改造的项目也是如此,我猜测上面的问题跟这个有很大的关系,事实证明了我猜想是对的,下面我们来改造一下

首先回到iconfont的官网,去到我们刚刚添加的图标库页面,有个项目设置选项,点击后会看到如下两个选项

没错,解决冲突的关键就是为两个项目添加不同引用前缀和font-family,主应用前缀改为main-app-icon-,font-family改为main-app-iconfont,微应用相应改为micro-app-icon-micro-app-iconfont

然后重新下载两个图标库并重新引入,目前两个iconfont.css的关键代码如下

代码语言:javascript复制
// 主应用的iconfont.css
@font-face {
  font-family: "main-app-iconfont"; /* Project id 2608947 */
  src: url('iconfont.woff2?t=1623508357834') format('woff2'),
       url('iconfont.woff?t=1623508357834') format('woff'),
       url('iconfont.ttf?t=1623508357834') format('truetype');
}

.main-app-iconfont {
  font-family: "main-app-iconfont" !important;
  font-size: 16px;
  font-style: normal;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

// 微应用的iconfont.css
@font-face {
  font-family: "micro-app-iconfont"; /* Project id 2608945 */
  src: url('iconfont.woff2?t=1623508587683') format('woff2'),
       url('iconfont.woff?t=1623508587683') format('woff'),
       url('iconfont.ttf?t=1623508587683') format('truetype');
}

.micro-app-iconfont {
  font-family: "micro-app-iconfont" !important;
  font-size: 16px;
  font-style: normal;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

相应的我们引入图标的方式也要改

代码语言:javascript复制
// 主应用中
<i class="main-app-iconfont main-app-icon-username"></i>
<i class="main-app-iconfont main-app-icon-password"></i>

// 微应用中
<i class="micro-app-iconfont micro-app-icon-username"></i>
<i class="micro-app-iconfont micro-app-icon-password"></i>

改造完毕后刷新浏览器

可以看到,样式冲突的问题已经解决了

为什么会出现这个这个问题?

官方提供了基于shadowDom的样式隔离方案,不过似乎还是未做到完全的隔离,同类名的情况下可能还是会出现冲突,所以我们尽量通过不同类名添加前缀的方式去避免样式冲突,或者是把类名降级放到一个父类中去避免样式冲突

什么意思呢?例如主微应用都有类名aaa,那么就可能会出现冲突 但是如果我们主应用改成这样 .main-app > .aaa,微应用改成这样.micro-app > .aaa,把原本处于根的aaa样式用容器包装起来,就可以避免样式冲突,解决ui库样式冲突的方式也是这种思路,可以参考一下这篇文章

部署微前端

处理完样式问题啦,貌似没什么问题了,来打包部署一下吧

部署前的改造

还记得主应用micros/app.js如下:

代码语言:javascript复制
const apps = [
  /**
   * name: 微应用名称 - 具有唯一性
   * entry: 微应用入口 - 通过该地址加载微应用
   * container: 微应用挂载节点 - 微应用加载完成后将挂载在该节点上
   * activeRule: 微应用触发的路由规则 - 触发路由规则后将加载该微应用
   */
  {
    name: "vue_micro_app",
    entry: "//localhost:8081",
    container: "#micro-container",
    activeRule: "#/vue2-micro-app",
  },
];

export default apps;

目前entry是写死的,我们可以部署的时候改,但是改来改去太麻烦啦,有没有更好的方法

代码语言:javascript复制
if(process.env.NODE_ENV === 'development') {

}else {

}

还记得这种判断环境的代码吗,这里我们不用那么麻烦,vue-cli帮我们做好了,我们在根目录添加.env.production.env.development文件,这两个文件就是用来导出一些变量,顾名思义这些变量分别用在dev和pro环境下的,具体可以点击这里了解

.env.development中添加

代码语言:javascript复制
VUE_APP_MICRO_ENTRY="//localhost:8081"

至于.env.production中就添加服务器的域名就可以啦

代码语言:javascript复制
VUE_APP_MICRO_ENTRY="你的服务器域名"

这里我正式环境用的是localhost:3001,稍后我会建本地服务器在3001端口部署微应用,3000端口部署主应用

这里文件中的变量一定要以VUE_APP_ 开头,否则是无效的

相应的app.js要改成如下格式:

代码语言:javascript复制
// 新增
const { VUE_APP_MICRO_ENTRY } = process.env

const apps = [
  /**
   * name: 微应用名称 - 具有唯一性
   * entry: 微应用入口 - 通过该地址加载微应用
   * container: 微应用挂载节点 - 微应用加载完成后将挂载在该节点上
   * activeRule: 微应用触发的路由规则 - 触发路由规则后将加载该微应用
   */
  {
    name: "vue_micro_app",
    entry: VUE_APP_MICRO_ENTRY, // 修改
    container: "#micro-container",
    activeRule: "#/vue2-micro-app",
  },
];

export default apps;

然后重启一下主应用,以后打包或者本地开发都不用再修改app.js啦

开始部署

接下来执行npm run build 或者 yarn run build分别打包两个项目

然后可以新建一个项目名为mock-server,npm init 初始化一下后执行npm install koanpm install koa-static,并添加两个文件夹mian-appmicro-app,分别把打包后的主应用和微应用放进这两个文件夹,再新建main-server.jsmicro-server.js

这时mock-server的目录结构如下

然后为main-server.jsmicro-server.js添加如下代码

代码语言:javascript复制
// main-server.js
const Koa = require('koa')
const path = require('path')
const app = new Koa()
const staticFiles = require('koa-static')

const staticPath = path.join(__dirname   '/main-app')

app.use(staticFiles(staticPath))

app.listen(3000, () => {
  console.log('main server running at 3000')
})

--------------

// micro-server.js
const Koa = require('koa')
const path = require('path')
const app = new Koa()
const staticFiles = require('koa-static')

const staticPath = path.join(__dirname   '/micro-app')

app.use(staticFiles(staticPath))

app.listen(3001, () => {
  console.log('main server running at 3001')
})

代码主要就是把打包出来的文件夹用koa分别在3000和3001端口跑起来,没什么特别的

然后访问一下,主应用正常运行,微应用报错了

上篇在微应用render函数中有这么一段代码:

代码语言:javascript复制
function render(props) {
  console.log("子应用render的参数", props)
  // ----看这里----
  props.onGlobalStateChange((state, prevState) => {
    // state: 变更后的状态; prev 变更前的状态
    console.log("通信状态发生改变:", state, prevState);
    store.commit('setToken', '123456')
  }, true);
  // 挂载应用
  instance = new Vue({
    router,
    store,
    render: (h) => h(App),
  }).$mount("#micro-app");
}

没错,当子应用独立运行时,props是没有onGlobalStateChange参数的,所以这里要添加判断(添加的判断还真不少的说),改成下面这个样子:

代码语言:javascript复制
function render(props) {
  console.log("子应用render的参数", props)
  // 新增判断,如果是独立运行不执行onGlobalStateChange
  if(window.__POWERED_BY_QIANKUN__) { 
    props.onGlobalStateChange((state, prevState) => {
      // state: 变更后的状态; prev 变更前的状态
      console.log("通信状态发生改变:", state, prevState);
      store.commit('setToken', '123456')
    }, true);
  }
  // 挂载应用
  instance = new Vue({
    router,
    store,
    render: (h) => h(App),
  }).$mount("#micro-app");
}

重新build并放到mock-server中重新运行3001端口,刷新后可以看到微应用运行成功

跨域问题

当从主应用切换到微应用时

没错,经典的跨域问题,因为部署的是本地,有两个解决办法

第一个(不推荐)是作弊的方法

新建一个chrome浏览器的快捷方式,然后右键,属性

在目标这一栏, --user-data-dir=E:MyChromeDevUserData到末尾,注意--user前有空格,然后用这个新建的快捷方式可以访问部署后的应用

第二种,使用koa2-cors

在mock-server中执行npm install koa2-cors,然后修改一下micro-server.js

代码语言:javascript复制
const Koa = require('koa')
const path = require('path')
const app = new Koa()
const staticFiles = require('koa-static')
const cors = require('koa2-cors'); // 新增

const staticPath = path.join(__dirname   '/micro-app')

app.use(cors());// 新增
app.use(staticFiles(staticPath))

app.listen(3001, () => {
  console.log('main server running at 3001')
})

重启micro-server.js并刷新浏览器,可以看到切换菜单已经正常啦

第三种,利用nginx做代理(建议)

贴上nginx.conf

代码语言:javascript复制
#user  nobody;
worker_processes 1;

#error_log  logs/error.log;
#error_log  logs/error.log  notice;
#error_log  logs/error.log  info;

#pid        logs/nginx.pid;
events {
 worker_connections 1024;
}


http {
 include mime.types;
 default_type application/octet-stream;
 sendfile on;
 keepalive_timeout 65;

 server {
                # 监听的端口
  listen 3001;
  server_name localhost;
  location / {
                #允许跨域访问
   add_header Access-Control-Allow-Origin *;
   add_header Access-Control-Allow-Methods 'GET, POST, OPTIONS';
   add_header Access-Control-Allow-Headers 'DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization';

   if ($request_method = 'OPTIONS') {
    return 204;
   }
                        
                        # 代理的文件夹
   root E:projectvue-projectvue2-micro-appdist;
   autoindex on;
  }
 }

}

使用nginx后,我们的micro-appmicro-server.js已经不需要了,因为nginx已经做了代理,允许nginx,刷新浏览器,可以看到切换菜单已经正常啦

坑2:页面无法跳转问题

这个问题就是我上一节所说的巨坑,因为这个页面无法跳转,在本地是没有任何问题的!然而部署到测试环境后,100%复现,本地环境100%没问题,你看一步步走到现在也没发现这个问题,这就是程序员经典场景----我本机是好的呀o(╥﹏╥)o

注意,即使是使用nginx代理后在本地部署依然无法在本地复现这个问题,我会配合gif图来还原这个问题

场景还原(以下全部假设运行在测试服务器)

本地也部署跑过感觉没问题了,开开心心部署到测试服务器,然后一访问,瞬间傻眼了

为什么会这样呀??可以看到无论是本地还是测试服务器都是没有任何报错的,然后这个问题我搞了几乎3天

如何解决?

到了第三天的时候,我差不多想放弃微前端改造方案了,突然我发现,我们点击菜单的时候,url是有变化的,但是页面没有跳转,所以我又大胆猜测,是不是路由的问题,而且可以看到,每次我们在主微应用之间切换的时候,都会执行微应用main.js中导出的mount和unmount函数,然后注意到unmount有这么一段代码

代码语言:javascript复制
export async function unmount() {
  console.log("VueMicroApp unmount");
  // 注意这里
  instance.$destroy();
  instance = null;
}

而微应用的routerindex.js是这样的

微应用main.js中的render函数是这样的

可以看到,由始至终,router都是同一个实例!然后每次unmount都会执行应用卸载,会不会就是这个问题导致的呢

接下来改造微应用的router.js,不再导出router而是导出routes数组

然后改造main.js

代码语言:javascript复制
import VueRouter from 'vue-router'
import routes from './router'

Vue.use(VueRouter)

// 新增:用于保存router实例
let router = null;
let microPath = ''

// 新增:动态设置 webpack publicPath,防止资源加载出错
if (window.__POWERED_BY_QIANKUN__) {
  // eslint-disable-next-line no-undef
  __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
  microPath = '/vue2-micro-app'
}

function render(props) {
  console.log("子应用render的参数", props)
  if(window.__POWERED_BY_QIANKUN__) {
    props.onGlobalStateChange((state, prevState) => {
      // state: 变更后的状态; prev 变更前的状态
      console.log("通信状态发生改变:", state, prevState);
      store.commit('setToken', '123456')
    }, true);
  }
  // 新增
  router = new VueRouter({
    routes
  })
  // 新增
  router.beforeEach((to, from, next) => {
  if (to.path !== (microPath   '/login')) {
    if (store.state.token) {
      next()
    } else {
      next(microPath   '/login')
    }
  } else {
    next()
  }
})
  // 挂载应用
  instance = new Vue({
    router,
    store,
    render: (h) => h(App),
  }).$mount("#micro-app");
}

export async function unmount() {
  console.log("VueMicroApp unmount");
  instance.$destroy();
  instance = null;
  // 新增
  router = null;
}

修改后的main.js,router不再是同一个实例,而是每次mount的时候都会新获取一个实例,相应的路由守卫也要搬迁出来,然后npm run serve看到本地运行微应用没问题,好npm run build重新打包并重新运行nginx

可以看到,这次部署是真的成功了

PS:在vue3中如果直接监听整个route对象,也会出现页面无法跳转的情况

欢迎指出不足和交流,踩坑不易,如果对你有帮助的话,点个赞吧~(#^.^#)

参考文献

明源云的qiankun教程:https://github.com/a1029563229/blogs/blob/master/BestPractices/qiankun/Communication.md

qinkun官网:https://qiankun.umijs.org/zh/api#initglobalstatestate

0 人点赞