前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >微前端框架qiankun项目实战(二)--踩坑与部署篇

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

作者头像
coder_koala
发布2021-07-08 11:32:21
1.6K0
发布2021-07-08 11:32:21
举报

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

回复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:\project\vue-project\vue2-micro-app\dist;
   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

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2021-06-16,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 程序员成长指北 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 隐藏微应用菜单和头部
  • token放进本地缓存
  • 坑1:样式冲突问题
    • 主应用的图标
      • 微应用的图标
        • 如何解决?
          • 为什么会出现这个这个问题?
          • 部署微前端
            • 部署前的改造
              • 开始部署
              • 跨域问题
                • 第一个(不推荐)是作弊的方法
                  • 第二种,使用koa2-cors
                    • 第三种,利用nginx做代理(建议)
                    • 坑2:页面无法跳转问题
                      • 场景还原(以下全部假设运行在测试服务器)
                        • 如何解决?
                        相关产品与服务
                        测试服务
                        测试服务 WeTest 包括标准兼容测试、专家兼容测试、手游安全测试、远程调试等多款产品,服务于海量腾讯精品游戏,涵盖兼容测试、压力测试、性能测试、安全测试、远程调试等多个方向,立体化安全防护体系,保卫您的信息安全。
                        领券
                        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档