微前端初尝试

什么是微前端?

按照网上的说法,微前端就是应用分割独立运行独立部署,将原本把所有功能集中于一个项目中的方式转变为把功能按业务划分成一个主项目和多个子项目,每个子项目负责自身功能,同时具备和其它子项目和主项目进行通信的能力,达到更细化更易于管理的目的。总的来说微前端就是

❝一个完整应用划分成一个主应用和一个或多个微应用,应用间相互独立,可相互通信。 ❞

如何实现微前端?

符合上面条件,最容易想到的就是iframe,下面贴上两段最简单的iframe及其通讯代码

javascript 复制代码
// parent.html
<div>我是parent</div>
<button id="parentBtn">parent btn</button>
<iframe src="./child.html" id="frame"></iframe>

<script>  
function parentFunc(msg) {    
  console.log("parent 的方法:", msg)  
}  

var btn = document.querySelector("#parentBtn")  
btn.addEventListener('click', function() {    
  console.log("我是parent的button")    
  console.log("我调用了:")    
  document.getElementById('frame').contentWindow.childFunc('parent');  
})
</script>

// child.html
<div>我是child</div>
<button id="childBtn">child btn</button>
<script>  
function childFunc(msg) {    
  console.log("child 的方法:", msg)  
}  

var btn = document.querySelector("#childBtn")  
btn.addEventListener('click', function() {    
  console.log("我是child的button")    
  console.log("我调用了:")    
  parent.window.parentFunc('child');   
})
</script>

复制

以上两段代码放到本地服务器中就是这样的

然后点击两个按钮,就可以互相通信传参了

❝以上的两个html必须放到有域名的环境中运行,否则会报错。 ❞

当然了,这次的项目迁移我不是直接用iframe改造的,而是站在巨人的肩膀上,我用了一个叫qiankun的微前端框架改造,因为公司的代码我不能贴上来,下面我会建一个vue3项目和一个vue2项目来大概还原一下我是如何改造公司项目的,还有我遇到的坑是怎么填的。

微前端框架qiankun

首先,用vue官方的脚手架建立一个vue3的基本后台界面和一个vue2的基本后台界面,注意这里因为vue3打包使用了vite的原因,所以qiankun框架不能使用vue3作为微应用,这里我们主应用是vue3,微应用是vue2,这跟我改造的也是一致的,两个项目大概结构是一样的,如下:

为了方便大家,贴上我建好的模板仓库地址

vue3模板:https://github.com/enhengenheng-hubowen/vue3-main-app(主应用,主应用必须安装qiankun)

vue2模板:https://github.com/enhengenheng-hubowen/vue2-micro-app(微应用)

上面master分支都是未改造前能独立运行的项目,dev分支是最终改造后的项目,当然自己从头到尾建立也是可以的,但是要保证两个仓库都具备router,store,登录拦截的功能

两个模板都具备这样的界面

1.登录界面

咳咳,简陋了点,为了显示请不要打我哈哈。

2.左侧菜单和router-view界面

好了,下面开始基于qiankun框架改造两个项目

主应用启动qiankun

这里我使用了qiankun官网的registerMicroApps注册微应用

在主应用的src文件夹下新建一个micros文件夹,在micros文件夹新建index.js,app.js

javascript 复制代码
// index.js
import NProgress from "nprogress";
import "nprogress/nprogress.css";
import {  
  registerMicroApps,  
  addGlobalUncaughtErrorHandler,  
  start,
} from "qiankun";// 微应用注册信息
import apps from "./app";

registerMicroApps(apps, {  
  beforeLoad: (app) => {    
    // 加载微应用前,加载进度条    
    NProgress.start();    
    console.log("before load", app.name);    
    return Promise.resolve();  
  },  
  afterMount: (app) => {    
    // 加载微应用前,进度条加载完成    
    NProgress.done();    
    console.log("after mount", app.name);    
    return Promise.resolve();  
  },
});

addGlobalUncaughtErrorHandler((event) => {  
  console.error(event);  
  const { message: msg } = event  
  if (msg && msg.includes("died in status LOADING_SOURCE_CODE")) {    
  console.error("微应用加载失败,请检查应用是否可运行");  
  }
});
export default start;

复制

首先yarn add nprogress安装nprogress这个库,是为了到时候在加载微应用的时候有进度条显示,这里用到了官方的几个api

  1. registerMicroApps:包含两个参数,第一个参数是微应用的一些注册信息,第二个参数是全局的微应用生命周期钩子。
  2. addGlobalUncaughtErrorHandler:全局的未捕获异常处理器,微应用发生报错的时候亦可以用这个api捕捉。
  3. start:我们用来启动qiankun的方法,包含一个参数

以上详细的api请点击这里:

arduino 复制代码
// app.js

const apps = [  
  {    
    name: "vue-micro-app",    
    entry: "//localhost:8081",    
    container: "#micro-container",    
    activeRule: "#/vue2-micro-app",  
  },
];
export default apps;

app.js导出的是上面registerMicroApps的第一个参数,是一个对象数组,其中数组每个字段的作用如下:

  1. name:微应用的名称,后面改造微应用的时候一定要与这个name对应
  2. entry:微应用运行的域名加端口,我用的是本地8082端口
  3. container:启动微应用需要一个dom容器,里面就是这个dom容器的id,用class应该也是可以的
  4. activeRule:触发启动微应用的规则,当检测到url中含有activeRule的值时,将启动微应用

添加完上述两个js后,我们回到main.js,目前的main.js应该是这样的

javascript 复制代码
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
import '@/assets/main.css'

createApp(App).use(store).use(router).mount('#app')

改造也非常简单,把上面micros中的index.js引入,然后运行一下start函数就大功告成了

javascript 复制代码
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
import '@/assets/main.css'
import start from '@/micros'


createApp(App).use(store).use(router).mount('#app')

start()

刷新一下浏览器,发现主应用和改造前并无差异!

主应用添加微应用容器和微应用菜单

目前主应用app的菜单代码结构如下

ini 复制代码
<div class="nav" v-if="token">    
  <div class="menu">      
    <router-link to="/">Parent Home</router-link>    
  </div>    
  <div class="menu">      
    <router-link to="/about">Parent About</router-link>    
  </div>
</div>

复制

现在我们添加两个菜单,分别对应子应用的homeabout

xml 复制代码
<div class="nav" v-if="token">    
  <div class="menu">      
    <router-link to="/">Parent Home</router-link>    
  </div>    
  <div class="menu">      
    <router-link to="/about">Parent About</router-link>    
  </div>
    <!--- 新添加 --->
  <div class="menu">      
    <router-link to="/vue2-micro-app">Child Home</router-link>    
   </div>    
  <div class="menu">      
    <router-link to="/vue2-micro-app/about">Child About</router-link>    
   </div>
</div>

<div class="container">   
  <div class="header" v-if="token">Child Header</div>   
  <div class="router-view">      
    <router-view />      
    <!-- 新添加,微应用的容器 -->      
    <div id="micro-container"></div>   
  </div>
</div>

相信你也发现了,to中多了上面app.jsactiveRule字段中对应的值(去掉了#号),因为#/vue2-micro-app正是触发启动微应用的条件

这是刷新我们的微应用,然后点击一下Child Home菜单,你会发现有两个报错

第一个是跨域报错,因为我们主应用运行在8081端口,微应用是8082端口,后面用nginx做一下代理就好

第二个报错就是源自于我们的微应用还未改造

微应用改造

官网写了,微应用入口必须导出 bootstrapmountunmount 三个生命周期钩子,以供主应用在适当的时机调用

这是微应用改造前的main.js

javascript 复制代码
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
import '@/assets/main.css'
Vue.config.productionTip = false
new Vue({  
  router,  
  store,  
  render: h => h(App)
}).$mount('#app')

复制

下面我们来改造一下main.js

javascript 复制代码
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
import '@/assets/main.css'
Vue.config.productionTip = false

// 新增:用于保存vue实例
let instance = null; 

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

/** * 新增: * 渲染函数 * 两种情况:主应用生命周期钩子中运行 / 微应用单独启动时运行 */
function render() {  
// 挂载应用  
  instance = new Vue({    
  router,    
  store,    
  render: (h) => h(App),  
}).$mount("#micro-app");}


/** 
* 新增: 
* bootstrap 只会在微应用初始化的时候调用一次,
  下次微应用重新进入时会直接调用 mount 钩子,不会再重复触发 bootstrap。 
* 通常我们可以在这里做一些全局变量的初始化,比如不会在 unmount 阶段被销毁的应用级别的缓存等。 
*/
export async function bootstrap() {  
  console.log("VueMicroApp bootstraped");
}

/** 
* 新增: 
* 应用每次进入都会调用 mount 方法,通常我们在这里触发应用的渲染方法 
*/
export async function mount(props) {  
  console.log("VueMicroApp mount", props);  
  render(props);
}
/** 
* 新增: 
* 应用每次 切出/卸载 会调用的方法,通常在这里我们会卸载微应用的应用实例 
*/
export async function unmount() {  
  console.log("VueMicroApp unmount");  
  instance.$destroy();  
  instance = null;
}

// 新增:独立运行时,直接挂载应用
if (!window.__POWERED_BY_QIANKUN__) {  
  render();
}

// 这是原本启动的代码
// new Vue({
//   router,
//   store,
//   render: h => h(App)
// }).$mount('#app')

❝请注意,render方法中我把$mount后的参数改为了#micro-app,这是为了区分主应用和微应用中index.html的根id,所以微应用中的public文件夹的index.html也要改为micro-app

然后还要对webpack配置进行改造,微应用根目录添加vue.config.js文件

javascript 复制代码
const path = require("path");

module.exports = {
  devServer: {
    // 监听端口
    port: 8081,
    // 关闭主机检查,使微应用可以被 fetch
    disableHostCheck: true,
    // 配置跨域请求头,解决开发环境的跨域问题
    headers: {
      "Access-Control-Allow-Origin": "*",
    },
  },
  configureWebpack: {
    resolve: {
      alias: {
        "@": path.resolve(__dirname, "src"),
      },
    },
    output: {
      // 微应用的包名,这里与主应用中注册的微应用名称一致
      library: "vue-micro-app",
      // 将你的 library 暴露为所有的模块定义下都可运行的方式
      libraryTarget: "umd",
      // 按需加载相关,设置为 webpackJsonp_VueMicroApp 即可
      jsonpFunction: `webpackJsonp_vue-micro-app`,
    },
  },
};

然后还要改造一下我们的路由

lua 复制代码
if (window.__POWERED_BY_QIANKUN__) {  
  microPath = '/vue2-micro-app'
}

const routes = [
  {    
    path: microPath + '/login',    
    name: 'login',    
    component: Login  
  },  
  {    
    path: microPath + '/',    
    redirect: microPath + '/home'  
  },  
  {    
    path: microPath + '/home',    
    name: 'Home',    
    component: Home  
  },  
  {    
    path: microPath + '/about',    
    name: 'About',
    component: () => import( /* webpackChunkName: "about" */ '../views/About.vue')  
  }
]

router.beforeEach((to, from, next) => {  
  if (to.path !== (microPath + '/login')) {    
    if (store.state.token) {      
      next()    
    } else {      
      next(microPath + '/login')    
    }  
  } 
  else {    
    next()  
  }
})

❝路由主要的改动就是每个path都添加了一个microPath变量,用于检测是否由微前端改动,相应的路由守卫也要添加microPath变量,另外微应用的login跳转的时候也要加上microPath判断 ❞

最后重启一下我们的微应用,再去我们的主应用点击一下Child Home菜单,如无意外你就会得到和我下面截图一样的界面

没错,你已经成功了!vue2的项目已经成功嵌入到vue3中去了

但是,细心的你也发现了,我已经登录了一次了,为什么又要登录一次呀,所以,接下来我们要利用通信去解决掉这个问题。

主应用和微应用通信

应用间的通信,我们要利用qiankun框架的initGlobalStateMicroAppStateActions api,相关的api介绍如下:

setGlobalState:设置 globalState - 设置新的值时,内部将执行浅检查,如果检查到globalState发生改变则触发通知,通知到所有的观察者函数。

onGlobalStateChange:注册观察者函数 - 响应globalState变化,在globalState发生改变时触发该观察者函数。

offGlobalStateChange:取消观察者函数 - 该实例不再响应globalState变化。

所以我们再次改造一下两个项目,首先是主应用的micros/index.js

javascript 复制代码
import {  
registerMicroApps,  
addGlobalUncaughtErrorHandler,  
start,  
initGlobalState // 新增
} from "qiankun";

const state = {} 
const actions = initGlobalState(state);

export {  actions }

以上新增了并导出了actions,然后去到login.vue

javascript 复制代码
import { actions } from "@/micros"; //新增

const login = () => {      
  if (username.value && password.value) {  
    store.commit("setToken", "123456");        
    // 新增
    actions.setGlobalState({globalToken: "123456"});        
    router.push({path: "/"});
  }
};

引入actions并新增了actions.setGlobalState方法

然后是子应用的main.js

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

render方法中我们加上onGlobalStateChange,并且第二位参数置为true,这样微应用一启动的时候,我们马上就可以看到刚刚设置的globalToken:123456

好了已经改造完毕,我们刷新重新登录主应用然后点击微应用的菜单,可以看到微应用不需要再登录了,如下图:

好像还是有点问题喔,微应用的菜单怎么展示出来了?

隐藏微应用菜单和头部

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

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

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中,这样一来我们只要登录了,再刷新也不需要重新登录

参考:cloud.tencent.com/developer/a...

相关推荐
Boilermaker199238 分钟前
【Java EE】SpringIoC
前端·数据库·spring
中微子1 小时前
JavaScript 防抖与节流:从原理到实践的完整指南
前端·javascript
天天向上10241 小时前
Vue 配置打包后可编辑的变量
前端·javascript·vue.js
芬兰y1 小时前
VUE 带有搜索功能的穿梭框(简单demo)
前端·javascript·vue.js
好果不榨汁1 小时前
qiankun 路由选择不同模式如何书写不同的配置
前端·vue.js
小蜜蜂dry1 小时前
Fetch 笔记
前端·javascript
拾光拾趣录1 小时前
列表分页中的快速翻页竞态问题
前端·javascript
小old弟1 小时前
vue3,你看setup设计详解,也是个人才
前端
Lefan2 小时前
一文了解什么是Dart
前端·flutter·dart
Patrick_Wilson2 小时前
青苔漫染待客迟
前端·设计模式·架构