微前端vue项目实战 -- 乾坤qiankun框架

Posted 随便起的名字也被占用

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了微前端vue项目实战 -- 乾坤qiankun框架相关的知识,希望对你有一定的参考价值。

背景:

公司研发的项目主要是 GIS地图为基础的管理系统,由主体项目(管理业务模块)+ GIS地图应用,由于 GIS地图应用模块 会在多个地方使用,所以单独构建一个项目,分别在不同的业务场景中引用、交互,也在app中通过webview通信使用,之前一直使用iframe的通信方式,iframe基本也可以满足业务需求,应用分割、独立运行、分别部署,iframe方案已经满足并且不断优化已经满足需求,但是作为程序猿还是想尝试一下新技术,或许实战它可能不是最佳方案,那也只有试了才知道,那就研究一下吧。

站在巨人的肩膀,看一下天空,用了阿里的乾坤qiankun框架,尝试一下踩坑,说干就干。

首先贴一下qiankun官网,官网介绍也很详细,也在网上借鉴了很多大神的操作,下面记录一下自己学习、码代码的步骤,就当记个笔记了

一、iframe方案的实现

// 主应用 引入子应用
// 通过 iframe

<iframe webkitallowfullscreen="true" mozallowfullscreen="true" allowfullscreen="true" id="iframe" :src="url" width="100%" height="100%" scrolling="No" frameborder="0"></iframe>


// 主应用 接口 iframe 子应用页面数据

window.onmessage = params => 

    console.log("子应用传来的数据",params )




// 主应用 向iframe 子应用发送数据

 let ele = document.getElementById("iframe").contentWindow;
 ele.postMessage(params, "*");




// 子应用接收主应用传来的数据

  window.addEventListener("message", async params => 
      console.log('接收主应用消息', params)
       
  );

//子应用给主应用发送数据

  let obj = xxx:xxx
  window.parent.postMessage(obj, "*");


// 子应用移除监听

 window.removeEventListener("message",()=>);

二、微前端qiankun实现方案

什么是微前端(取阿里qiankun的介绍)

微前端是一种多个团队通过独立发布功能的方式来共同构建现代化 web 应用的技术手段及方法策略。

微前端架构具备以下几个核心价值:

  • 技术栈无关
    主框架不限制接入应用的技术栈,微应用具备完全自主权

  • 独立开发、独立部署
    微应用仓库独立,前后端可独立开发,部署完成后主框架自动完成同步更新

  • 增量升级

    在面对各种复杂场景时,我们通常很难对一个已经存在的系统做全量的技术栈升级或重构,而微前端是一种非常好的实施渐进式重构的手段和策略

  • 独立运行时
    每个微应用之间状态隔离,运行时状态不共享

微前端架构旨在解决单体应用在一个相对长的时间跨度下,由于参与的人员、团队的增多、变迁,从一个普通应用演变成一个巨石应用后,随之而来的应用不可维护的问题。这类问题在企业级 Web 应用中尤其常见。

将原本把所有功能集中于一个项目中的方式转变为把功能按业务划分成一个主项目和多个子项目,每个子项目负责自身功能,同时具备和其它子项目和主项目进行通信的能力,达到更细化更易于管理的目的。

1、微前端框架搭建--qiankun

主应用不限技术栈,只需要提供一个容器 DOM,然后注册微应用并 start 即可,微应用分为有 webpack 构建和无 webpack 构建项目,有 webpack 的微应用

可以看到官网介绍微应用需要使用到webpack,所以在构建应用时,我们使用vue在选择构建方式时候,主应用可以使用webpack 或vite,子应用必须使用webpack 构建,所以实验学习使用的 

主应用:vue3 使用cli的构建方式;子应用:vue2 使用cli方式

构建的应用大概长这样:

为了方便,贴上学习实践的demo:

主项目vue3:vue-qiankun-master: 微前端qiankun,vue3.x项目,微前端主应用

子项目vue2:vue-qiankun-child: 微前端qiankun,vue2.x项目,微前端子应用

 上面demo只需要下载下来运行就可以查看效果了

2、开始构建项目

主、子项目都具备 登录、侧边菜单、主内容;页面如下

 

 2.1、构建主应用

vue3项目构建过程可以看我的其他文章,此处省略

构建完主应用项目后,在主应用安装qiankun

yarn add qiankun # 或者 npm i qiankun -S

使用qiankun的registerMicroApps注册微应用

在主应用项目中src创建micros,在micros创建index.js、apps.js

应用切换时用到nprogress,先安装nprogress

npm install --save nprogress
# 或者
yarn add nprogress

index.js

// 子应用切换加载进度条
import NProgress from "nprogress";
import "nprogress/nprogress.css";
import Store from "../store"

// 注册微应用主应用
import  registerMicroApps, start, addGlobalUncaughtErrorHandler, initGlobalState  from 'qiankun';
import apps from "./apps";

// 微应用通信 定义全局状态,并返回通信方法
const state = 
const actions = initGlobalState(state);
actions.setGlobalState(
    globalToken: ''
)

registerMicroApps(apps, 
    beforeLoad: (app) => 
        // 加载微应用前,加载进度条    
        NProgress.start();
        console.log("before load", app.name);

        if (Store.state.token) 
            //  微应用加载检查登录 已登录 子应用直接传参登录
            actions.setGlobalState( globalToken: Store.state.token )
        

        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;
export 
    actions

 这里用到qiankun框架的几个api,简单介绍一下

registerMicroApps(apps, lifeCycles?):

  • apps - Array<RegistrableApp> - 必选,微应用的一些注册信息
  • lifeCycles - LifeCycles - 可选,全局的微应用生命周期钩子

微应用注册,包含两个参数,第一个参数是微应用的一些注册信息,第二个参数是全局的微应用生命周期钩子。

start(opts?)

  • opts - Options 可选

我们用来启动qiankun的方法,包含一个参数

addGlobalUncaughtErrorHandler(handler)

  • handler - (...args: any[]) => void - 必选

全局的未捕获异常处理器。

qiankun API具体使用方式请查看官网api

apps.js

const apps = [
  /**
  * name: 微应用名称 - 具有唯一性
  * entry: 微应用入口 - 通过该地址加载微应用
  * container: 微应用挂载节点 - 微应用加载完成后将挂载在该节点上
  * activeRule: 微应用触发的路由规则 - 触发路由规则后将加载该微应用
  */

  
    name: "vue-micro-app",
    entry: "//localhost:8082",
    container: "#micro-container",
    activeRule: "#/vue2-micro-app",
    props: 
      router: router
    
  ,
];
export default apps;

apps导出的参数是registerMicroApps的第一个参数apps

  * name: 微应用名称 - 具有唯一性
  * entry: 微应用入口 - 通过该地址加载微应用
  * container: 微应用挂载节点 - 微应用加载完成后将挂载在该节点上
  * activeRule: 微应用触发的路由规则 - 触发路由规则后将加载该微应用

参数具体规则可以查看官网api介绍

mian.js

在main.js中引用,运行一下 start函数 开启微应用,sandbox: experimentalStyleIsolation: true 开启沙箱隔离样式

import  createApp  from 'vue'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import App from './App.vue'
import router from './router'
import store from './store'
import start from '@/micros'

import vueDragging from "./directives/vue-dragging.es5"

const Vue = createApp(App)


Vue.use(store)
Vue.use(router)
Vue.use(ElementPlus,  size: 'small', zIndex: 3000 )
Vue.use(vueDragging)
Vue.mount('#app')

start( sandbox:  experimentalStyleIsolation: true  )

启动应用,我们会发现和未安装qiankun前没什么区别。

2.2、主应用中构建微应用容器和微应用菜单

主应用App.vue中添加微应用容器

  <div class="app-container">
        <router-view></router-view>
        <!-- 新添加,微应用的容器 -->
        <div id="micro-container"></div>
  </div>

主应用菜单结构如下:

 <div class="app-nav" v-show="appToken">
 
      <router-link class="nav-a-btn" to="/" >主-home</router-link>
      <router-link class="nav-a-btn" to="/master-about" >主-about</router-link>

      <router-link class="nav-a-btn" to="/vue2-micro-app/home" >子-home</router-link>
      <router-link class="nav-a-btn" to="/vue2-micro-app/about" >子-about</router-link>

    </div>

我们可以看到 to中多了上面app.jsactiveRule字段中对应的值(去掉了#号),因为#/vue2-micro-app正是触发启动微应用的条件

改造后的app.vue

<template>
  <div class="app-main">
    <div class="app-nav" v-show="appToken">
      <template v-for="menu in menuList" :key="menu.name">
        <div class="nav-a-btn" :class="'router-active':menuActive==menu.path"
          @click="menuChangeRouter(menu)">menu.btnName</div>
      </template>
    </div>
    <div class="app-content">
      <div class="app-header-content" v-show="appToken">
        <div> crumbsRouter</div>
        <div>Token: appToken</div>
        <el-button type="primary" round @click="loginOut">退出登录</el-button>
      </div>
      <div class="app-container">
        <router-view></router-view>
        <!-- 新添加,微应用的容器 -->
        <div id="micro-container"></div>
      </div>

    </div>
  </div>

</template>

<script setup>
import  reactive, toRefs, ref  from "@vue/reactivity";
import  computed, watch, onMounted  from "@vue/runtime-core";
import  useRoute, useRouter  from "vue-router";
import  useStore  from "vuex";
import  actions  from "@/micros"


const Router = useRouter()
const Route = useRoute()
const Store = useStore()

let state = reactive(
  menuList: [
    
      name: 'home',
      path: '/master-home',
      btnName: '主-home'
    ,
    
      name: 'master-about',
      path: '/master-about',
      btnName: '主-about'
    ,
    
      name: 'micro-home',
      path: '/vue2-micro-app/home',
      btnName: '子-home'
    ,
    
      name: 'micro-about',
      path: '/vue2-micro-app/about',
      btnName: '子-about'
    
  ],
  menuActive: computed(() => Route.path),
  crumbsRouter: computed(() => 
    let name = ""
    state.menuList.forEach(item => 
      if (state.menuActive == item.path) 
        name = item.btnName
      
    )
    return name

  ),
  // 从环境变量中取参数
  appId: process.env.VUE_APP_MICRO_ENTRY,
  appToken: computed(() => Store.state.token)
)

watch(() => Route.path, (val, oval) => 
  console.log("监听路由变化", val, oval)
)

onMounted(() => 
  actions.onGlobalStateChange((state) => 
    // state: 变更后的状态; prevState: 变更前的状态
    console.log("主应用观察者:状态改变", state);
    let token = state.globalToken
    Store.commit("setToken", token)
    console.log("kkkkkkkkkkk")

    console.log("jjjjj", Store.state.token)
  )
)

let menuChangeRouter = (row) => 
  state.menuActive = row.name
  // 路由跳转方式
  Router.push( path: row.path )
  // 跳转方法二 
  //  window.history.pushState(, '', '/#'+row.path)


let loginOut = () => 
  Store.commit("loginOut")
  Router.push('/login')


let  menuList, menuActive, crumbsRouter, appId, appToken  = toRefs(state)


</script>


<style lang="scss">
html,
body 
  margin: 0;
  padding: 0;

#app 
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;

$leftWidth: 200px;
.app-main 
  display: flex;
  justify-content: space-between;
  width: 100%;
  height: 100vh;
  background: #f6f7fc;

.app-nav 
  width: $leftWidth;
  height: 100%;
  display: flex;
  flex-direction: column;
  box-shadow: 2px 0px 10px 0px rgb(0, 47, 60, 0.2);
  padding: 20px;
  box-sizing: border-box;
  background: #fff;
  z-index: 9;

  .nav-a-btn 
    width: 100%;
    height: 40px;
    line-height: 40px;
    background: #f3f4f5;
    margin-bottom: 10px;
    font-weight: bold;
    cursor: pointer;
  

  .router-active 
    color: #42b983;
    background: #deeefdde;
  

.app-content 
  width: calc(100% - $leftWidth);
  height: 100%;
  .app-header-content 
    padding: 0 20px;
    width: 100%;
    height: 50px;
    background: #ffffff;
    box-shadow: 0px 0px 8px 0px rgb(0 0 0 / 8%);
    display: flex;
    align-items: center;
    justify-content: space-between;
    box-sizing: border-box;
    border-bottom: 1px solid #ccc;
  
  .app-container 
    width: 100%;
    height: calc(100% - 50px);
    overflow: auto;
    padding: 20px;
    box-sizing: border-box;
  


/* S 修改滚动条默认样式 */

::-webkit-scrollbar 
  width: 8px;
  background: white;


::-webkit-scrollbar-corner,
   /* 滚动条角落 */
::-webkit-scrollbar-thumb,
::-webkit-scrollbar-track 
  /*滚动条的轨道*/
  border-radius: 4px;


::-webkit-scrollbar-corner,
::-webkit-scrollbar-track 
  /* 滚动条轨道 */
  background-color: rgba(180, 160, 120, 0.1);
  box-shadow: inset 0 0 1px rgba(180, 160, 120, 0.5);


::-webkit-scrollbar-thumb 
  /* 滚动条手柄 */
  background-color: #00adb5;


/* E 修改滚动条默认样式 */
</style>

2、微应用子应用搭建

微应用需要在自己的入口 js (通常就是你配置的 webpack 的 entry js) 导出 bootstrapmountunmount 三个生命周期钩子,以供主应用在适当的时机调用

开始改造子应用

创建qiankun/public-path.js

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

在main.js 引入 public-path.js  并改造main.js  如下:

import "./qiankun/public-path"
import Vue from 'vue'
import App from './App.vue'
import VueRouter from 'vue-router'
import routes from './router'
import store from './store'
import MicroActions from './qiankun/qiankun-actions'

Vue.config.productionTip = false

Vue.use(VueRouter)


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


if (window.__POWERED_BY_QIANKUN__) 
  microPath = '/vue2-micro-app'




/** * 新增: * 渲染函数 * 两种情况:主应用生命周期钩子中运行 / 微应用单独启动时运行 */
function render(props) 

  console.log("子应用render的参数", props)

  // 新增判断,如果是独立运行不执行onGlobalStateChange
  if (window.__POWERED_BY_QIANKUN__) 
    if (props) 
      // 注入 actions 实例
      MicroActions.setActions(props);
    
    
    // 挂载主应用传入路由实例 用于子应用跳转主应用
    Vue.prototype.$microRouter = props.router

    props.onGlobalStateChange((state, prevState) => 
      // state: 变更后的状态; prev 变更前的状态
      console.log("通信状态发生改变:", state, prevState);
      store.commit('setToken', state.globalToken)
    , true);
  

  // router不再是同一个实例,而是每次mount的时候都会新获取一个实例
  router = new VueRouter(
    routes
  )
  // 路由守卫
  router.beforeEach((to, from, next) => 
    if (to.path !== (microPath + '/login')) 
      if (store.state.token) 
        console.log("已经登录 token=", store.state.token)
        if (window.__POWERED_BY_QIANKUN__ && !to.path.includes('vue2-micro-app')) 
          next(microPath + to.path)
         else 
          next()
        
       else 
        console.log("子应用 - 未登录 请登录")
        next(microPath + '/login')
      
     else 
      next()
    
  )


  // 挂载应用  
  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')

注意:$mount("#micro-app")  挂在的时候修改了根id为了区别主应用和子应用,避免出现问题,对应的入口index.html 的 id="micro-app"

接下来就是改造路由,加入判断是否微应用打开,是就走子应用路径逻辑,否就单独运行

// import Vue from 'vue'
// import VueRouter from 'vue-router'
import Home from '../views/Home.vue'
// import store from '../store'

// Vue.use(VueRouter)


// 判断环境是否是微应用打开
let microPath = ''
if (window.__POWERED_BY_QIANKUN__) 
  microPath = '/vue2-micro-app'


const routes = [
  
    path: microPath + '/',
    redirect: microPath + '/home'
  ,
  
    name: 'Home',
    path: microPath + '/home',
    component: Home
  ,
  
    name: 'About',
    path: microPath + '/about',
    // route level code-splitting
    // this generates a separate chunk (about.[hash].js) for this route
    // which is lazy-loaded when the route is visited.
    component: () => import(/* webpackChunkName: "about" */ '../views/About.vue')
  ,
  
    name: 'login',
    path: microPath + '/login',
    component: () => import(/* webpackChunkName: "about" */ '../views/login.vue')
  
]

// const router = new VueRouter(
//   routes
// )

// // 路由守卫  移动到main.js中
// router.beforeEach((to, from, next) => 
//   if (to.path !== (microPath + '/login')) 
//     if (store.state.token) 
//       console.log("已经登录 token=",store.state.token)
//       if (window.__POWERED_BY_QIANKUN__ && !to.path.includes('vue2-micro-app')) 
//         next(microPath + to.path)
//        else 
//         next()
//       
//      else 
//       console.log("子应用 - 未登录 请登录")
//       next(microPath + '/login')
//     
//    else 
//     next()
//   
// )

export default routes

路由主要的改动就是每个path都添加了一个microPath变量,检测是否微应用打开

相应的路由守卫也要添加microPath变量,另外微应用的login跳转的时候也要加上microPath判断(在main.js中查看)

设置webpack,在vue.config.js设置打开配置

const path = require("path");

module.exports = 
  devServer: 
    // 监听端口
    port: 8066,
    // 关闭主机检查,使微应用可以被 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`,
    ,
  ,
;

最后启动主应用和子应用项目到浏览器上可以看到,如下页面:

 

好了此时微应用已经正常启动,上面页面已经做了,主应用登录子应用可以同步登录状态,主应用与子应用通信

下一章继续探索微前端的主应用如何与子应用通信,如何使用initGlobalState进行通信,敬请期待 

微前端——乾坤qiankun Demo

参考技术A

微前端就是将不同的功能按照不同的维度拆分成多个子应用。通过主应用来加载这些子应用。微前端的核心在于拆,拆完后在合!

我们可以将一个应用划分成若干个子应用,将子应用打包成一个个的 lib 。当路径切换 时加载不同的子应用。这样每个子应用都是独立的,技术栈也不用做限制了!从而解决了前端协同开发问题。

文档地址: https://qiankun.umijs.org/zh

2018 年 Single-SPA 诞生了, single-spa 是一个用于前端微服务化的 JavaScript 前端解决方案 ( 本身没有处理样式隔离, js 执行隔离 ) 实现了路由劫持和应用加载。

2019 年 qiankun 基于 Single-SPA, 提供了更加开箱即用的 API ( single-spa + sandbox + import-html-entry ) 做到了,技术栈无关、并且接入简单(像 i frame 一样简单)。

这里我们打算建立三个项目进行实操,一个Vue项目充当主应用,另一个Vue和React应用充当子应用

基座:qiankun-base 子应用:qiankun-vue、qiankun-react

react + react-router 技术栈的主应用:只需要让子应用的 activeRule 包含主应用的这个路由即可。

vue + vue-router 技术栈的主应用:

用绝对路径,不用用相对路径,例如

qiankun 只能解决子项目之间的样式相互污染,不能解决子项目的样式污染主项目的样式
冲突的样式,采用BEM命名方式

子应用,需要增加 update 钩子以便主应用手动更新微应用

主应用,直接调用子应用实例的 update 方法即可

以上是关于微前端vue项目实战 -- 乾坤qiankun框架的主要内容,如果未能解决你的问题,请参考以下文章

qiankun 实战(一)

Qiankun框架对于微前端的解耦和沙盒与实战探索心得

qiankun微前端框架处理

微前端框架(qiankun)配置

微前端实战-乾坤

微前端实战-乾坤