微前端 qiankun mirco-app

微前端

1.什么是微前端

  • 微前端的概念是由 ThoughtWorks 在2016年提出的。
  • 微前端是存在于浏览器中的微服务,借鉴了后端微服务的架构理念,将微服务的概念扩展到前端。
  • 微前端是一种前端架构风格,将一个庞大的前端应用拆分成多个独立灵活的微应用,每个应用都可以独立开发、独立运行、独立部署,或者将原本运行已久,没有关联的几个应用融合为一个应用。
  • 微前端既可以将多个项目融合,又可以减少项目之间的耦合,提高开发效率和可维护性。微前端的核心在于解耦,通过拆分和集成来实现前端应用的可扩展性和灵活性。

2.什么时候用微前端

  1. 一个非常庞大的项目,越来越大,后续难以维护。
  2. 在一些大厂,经常会有跨部门和跨团队协作开发项目,这样会导致团队效率降低和沟通成本加大,这时我们可以使用微前端,每个团队或者每个部门单独维护自己的项目,我们只需要一个主项目来把分散的子项目汇集到一起即可。
  3. 一个非常老旧的项目,开发效率低,但是一时半会又不能全部重构,这时我们就可以新创建一个新技术新项目的基座,把老项目的页面接入到新项目里面,后面新需求都在新项目里面开发就好,不用再动老项目。
  4. 想独立部署每一个单页面应用。
  5. 改善初始化加载时间,延迟加载代码。
  6. 基于多页的子应用缺乏管理,规范/标准不统一,无法统一控制视觉呈现、共享的功能和依赖。造成重复工作。

3.微前端的能力

  1. 同步更新:多个业务应用依赖同一个服务应用的功能,只需要更新服务应用,其他业务应用可立马更新。
  2. 增量升级:对系统做全量的升级或重构很复杂,只升级更新需要的微应用,实施渐进式重构。
  3. 独立开发:主框架不限制接入应用的技术栈,微应用可自主选择技术栈。
  4. 独立部署:微应用可以独立部署,互不影响,可以使用不同的部署方式、不同的版本控制工具等
  5. 独立运行:每个微应用之间状态隔离,运行时状态不共享

4.微前端的模式

  1. 基座模式:通过搭建基座、配置中心来管理子应用,如基于single-spa的通用qiankun方案
  2. 自组织模式:通过约定进行互调,但会遇到第三方依赖等问题
  3. 去中心模式:脱离基座模式,每个应用都可以彼此共享资源分享,如基于webpak5 module federation实现的emp微前端方案

5.微前端技术方案

  1. iframe:
    微前端最简单方案,通过iframe加载子应用,通信使用window.postMessage,完美的沙箱机制自带应用隔离
  2. web components
    将前端应用分解为自定义HTML元素,基于CustomEvent实现通信,Shadow DOM天生的作用域隔离
  3. systemJs/single-spa:
    通过路由劫持实现应用加载(systemJS),提供应用间公共组件加载和公共业务逻辑处理,子应用需要暴露固定的钩子接入协议,基于props主子间应用通信

micro-app

简介

micro-app是由京东前端团队推出的一款微前端框架,它借鉴了WebComponent的思想,通过js沙箱、样式隔离、元素隔离、路由隔离模拟实现了ShadowDom的隔离特性,并结合CustomElement将微前端封装成一个类WebComponent组件,从而实现微前端的组件化渲染,旨在降低上手难度、提升工作效率。micro-app和技术栈无关,也不和业务绑定,可以用于任何前端框架。
● 使用简单:将所有功能都封装到一个类WebComponent组件中,从而实现在基座应用中嵌入一行代码即可渲染一个微前端应用。
● 功能强大:micro-app提供了js沙箱、样式隔离、元素隔离、路由隔离、预加载、数据通信等一系列完善的功能。
● 兼容所有框架:保证各个业务之间独立开发、独立部署的能力,micro-app做了诸多兼容,在任何前端框架中都可以正常运行。

概念图

在这里插入图片描述

官方文档

https://micro-zoe.github.io/micro-app/docs.html#/zh-cn/start
● 配置项
● 生命周期
● 环境变量
● js沙箱
● 虚拟路由系统
● 样式隔离
● 数据通信
● 资源系统
● 预加载
● umd模式
● keep-live
● 多层嵌套
● 插件系统
● 高级功能

主应用

1.安装依赖
npm i @micro-zoe/micro-app --save
2.初始化
//main.js
import microApp from '@micro-zoe/micro-app'
microApp.start()
app.mount('#substrate')
// 主应用和子应用的挂载ID不要相同
3.使用micro-app
<template>
  <!-- name:应用名称, url:应用地址 -->
  <micro-app name='my-app' url='http://localhost:3000/' iframe></micro-app>
 </template>
// 详细配置项文档:
// https://micro-zoe.github.io/micro-app/docs.html#/zh-cn/configure

// vite 框架要加 iframe 属性,否则会出现:
// VM572:28 [micro-app from runScript] app vue3Child: 
// SyntaxError: Cannot use import statement outside a module的报错

// micro-app未定义报错解决办法:
export default defineConfig({
	plugins: [
	  vue({
	    template: {
	      compilerOptions: {
	        isCustomElement: tag => /^micro-app/.test(tag)
	      }
	    }
	  })在这里插入代码片
	],
})
3.封装入口文件
// layout.vue
<script setup>
  import Menu from '@/layout/menu/index.vue'
  import Appliaction from '@/layout/appliaction/index.vue'
</script>

  <template>
  <div class="layout">
    <div class="menu">
      <Menu />
    </div>
    <div class="application">
      <Appliaction />
    </div>
  </div>
  </template>

  <style scoped lang="scss">
  .layout {
  width: 100%;
  height: 100%;
  display: flex;
  justify-content: space-between;
  align-items: center;
  .menu {
    width: 200px;
    height: 100%;
    background: rgb(224, 200, 156);
  }
  .application {
    width: calc(100% - 220px);
    height: 100%;
    background: #ccc;
  }
}
</style>
4.封装应用组件
// appliction.vue
<script setup>
  import { useRoute } from "vue-router";
  const route = useRoute();
</script>

<template>
  <micro-app
    style="width: 100%; height: 100%"
    keep-alive
    :name="route.name"
    :url="route.meta.url"
    iframe
  ></micro-app>
</template>

<style lang="scss" scoped></style>
5.封装路由切换组件
// menu.vue
<script setup>
  import { useRouter } from 'vue-router'
  const router = useRouter()

  const enterApp = (name) => {
    router.push({ name: name })
  }
</script>

<template>
  <div class="menu-comp">
    <div @click="enterApp('ApplicationOne')">应用1</div>
    <div @click="enterApp('ApplicationTwo')">应用2</div>
  </div>
</template>

<style scoped lang="scss">
  .menu-comp {
    width: 100%;
    height: 100%;
    div {
      cursor: pointer;
      font-size: 32px;
      background: rgb(96, 57, 57);
      margin-bottom: 10px;
    }
  }
</style>
6.设置对应路由
// router.js
import { createRouter, createWebHistory } from "vue-router"

let routes = [
  {
    path: '/',
    name: 'Home',
    component: () => import('@/layout/index.vue'),
    meta: {
      title: '首页'
    }
  },
  {
    path: '/applicationOne',
    name: 'ApplicationOne',
    component: () => import('@/layout/appliaction/index.vue'),
    meta: {
      title: "应用1",
      url: "http://127.0.0.1:9527/",
    },
  },
  {
    path: '/applicationTwo',
    name: 'ApplicationTwo',
    component: () => import('@/layout/appliaction/index.vue'),
    meta: {
      title: "应用2",
      url: "http://127.0.0.1:8521/",
    },
  }
]

// 路由
const router = createRouter({
  history: createWebHistory(),
  routes
})

export default router

子应用

1.开启跨域支持(vite默认开启,不需要)
module.exports = {
  devServer: {
    headers: {
      'Access-Control-Allow-Origin': '*',
    }
  }
}
2.注册卸载函数
// main.js
const app = createApp(App)
app.mount('#appone')

// 卸载应用
window.unmount = () => {
  app.$destroy()
}
3.设置publicPath(资源路径自动补全失效时)
// 新建public-path.js
/ __MICRO_APP_ENVIRONMENT__和__MICRO_APP_PUBLIC_PATH__是由micro-app注入的全局变量
if (window.__MICRO_APP_ENVIRONMENT__) {
  // eslint-disable-next-line
  __webpack_public_path__ = window.__MICRO_APP_PUBLIC_PATH__
}
// main.js
import './public-path'

qiankun

qiankun是一种微前端框架,可以将多个前端应用集成为一个整体。每个子应用可以使用不同的框架和技术栈,它们之间可以相互独立开发和部署,qiankun提供了一套完整的生命周期函数和通信机制,可以让不同的子应用之间进行跨框架和跨域的通信和交互
具体查看官网https://qiankun.umijs.org/zh/guide

1.安装乾坤插件

// 主应用和子应用都需要安装qiankun,vite对乾坤支持需要用vite-plugin-qiankun插件
// 主应用安装 "qiankun": "^2.10.16",
npm install qiankun -S
// 子应用 "vite-plugin-qiankun": "^1.0.15",
npm install vite-plugin-qiankun -S

2.主应用配置

src/utils下新建qiankun文件夹,在该文件夹下新建 index.js、appliaction.js

1.接入子应用配置项
// appliaction.js
const appliactions = [
  {
    name: "appone", // name 需要唯一
    entry: "http://localhost:9527/", // 应用地址
    container: "#app-micro",// 承载应用的容器的id
    activeRule: "/appone", // 匹配的路由
    props: {},
    loader (loading) {
      console.log('loading', loading)
    }
  },
  {
    name: "apptwo",
    entry: "http://localhost:8521/",
    container: "#app-micro",
    activeRule: "/apptwo",
    loader (loading) {
      console.log('loading', loading)
    }
  },
];
export default appliactions
2.注册子应用方法
// index.js
import { registerMicroApps, addGlobalUncaughtErrorHandler } from "qiankun"
import appliactions from "./appliaction.js";

export const injectAppliactions = () => {
  console.log('注册子应用。。。。。。')
  // 注册子应用
  registerMicroApps(appliactions, {
    beforeLoad: (app) => {
      console.log("before load");
    },
    beforeMount: (app) => {
      console.log("before mount");
    },
    afterMount: (app) => {
      console.log("after mount");
    },
    beforeUnmount: (app) => {
      console.log("before unmount");
    },
    afterUnmount: (app) => {
      console.log("before unmount");
    }
  })
  // 创建异常捕获
  addGlobalUncaughtErrorHandler(event => {
    console.log(event)
  })
}
3.主应用接入子应用框架搭建

src文件夹下新建layout文件夹,该文件夹下新建 index.vue、appliaction/index.vue、 menu/index.vue

// index.js
<script setup>
  import Menu from '@/layout/menu/index.vue'
  import Appliaction from '@/layout/appliaction/index.vue'
</script>

<template>
  <div class="layout">
    <div class="menu">
      <Menu />
    </div>
    <div class="application">
      <Appliaction />
    </div>
  </div>
</template>

<style scoped lang="scss">
  .layout {
    width: 100%;
    height: 100%;
    display: flex;
    justify-content: space-between;
    align-items: center;
    .menu {
      width: 200px;
      height: 100%;
      background: rgb(224, 200, 156);
    }
    .application {
      width: calc(100% - 220px);
      height: 100%;
      background: #ccc;
    }
  }
</style>
// appliaction/index.vue
<script setup>
  import { nextTick, onBeforeUnmount, onMounted } from 'vue'
  import { start, setDefaultMountApp } from 'qiankun'
  import { injectAppliactions } from '@/utils/qiankun/index.js'

  onMounted(() => {
    if (!window.qiankunStarted) {
      injectAppliactions()
      start({ prefetch: false })
      console.log('启动乾坤。。。。。。')
    }
  })
  onBeforeUnmount(() => {
    window.qiankunStarted = false
  })
</script>

<template>
  <div id="app-micro"></div>
</template>

<style lang="scss" scoped>
  #app-micro {
    width: 100%;
    height: 100%;
  }
</style>
// menu/index.vue
<script setup>
  import { useRouter } from 'vue-router'
  const router = useRouter()

  const enterApp = (name) => {
    router.push({ name: name })
  }
</script>

<template>
  <div class="menu-comp">
    <div @click="enterApp('appone')">应用1</div>
    <div @click="enterApp('apptwo')">应用2</div>
  </div>
</template>

<style scoped lang="scss">
  .menu-comp {
    width: 100%;
    height: 100%;
    div {
      cursor: pointer;
      font-size: 32px;
      background: rgb(96, 57, 57);
      margin-bottom: 10px;
    }
  }
</style>
路由配置
import { createRouter, createWebHistory } from "vue-router"

let routes = [
  {
    path: '/',
    name: 'Home',
    redirect: '/appone',
    component: () => import('@/layout/index.vue'),
    meta: {
      title: '首页'
    }
  },
  {
    // 匹配所有其他路由
    path: '/appone/:pathMatch(.*)*',
    name: 'appone',
    component: () => import('@/layout/appliaction/index.vue'),
  },
  {
    // 匹配所有其他路由
    path: '/apptwo/:pathMatch(.*)*',
    name: 'apptwo',
    component: () => import('@/layout/appliaction/index.vue'),
  }
]

// 路由
const router = createRouter({
  history: createWebHistory(),
  routes
})

export default router 

子应用配置

1.main.js配置
// main.js
import { createApp } from 'vue'
import './style.css'
import App from './App.vue'
import router from "./routes/index";
import { renderWithQiankun, qiankunWindow } from "vite-plugin-qiankun/dist/helper"

let app = null

const initApp = (container) => {
  app = createApp(App)
  app.use(router)
  app.mount(container ? container.querySelector('#appone') : "#appone")
}

const initQK = () => {
  renderWithQiankun({
    bootstrap () { },
    mount (props) {
      initApp(props.container)
    },
    unmount () {
      app.unmount()
      app = null
    },
    update (props) {
      console.log('upadte----', props)
    }
  })
}

// 判断是直接访问还是通过qiankun,在主应用里加载微应用
qiankunWindow.__POWERED_BY_QIANKUN__ ? initQK() : initApp()
2.路由配置
import { createRouter, createWebHistory } from "vue-router"
import { qiankunWindow } from "vite-plugin-qiankun/dist/helper"
let routes = [
  {
    path: '/',
    name: 'HelloWorld',
    component: () => import('@/components/HelloWorld.vue'),
    meta: {
      title: '首页'
    }
  }
]

// 路由
const router = createRouter({
  history: createWebHistory(qiankunWindow.__POWERED_BY_QIANKUN__ ? `/appone` : "/"),
  routes
})

export default router 
3.vite.config.js配置
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import path from 'path'

// 配置
import qiankun from "vite-plugin-qiankun";

export default defineConfig(() => {
  return {
    plugins: [
      vue(),
      qiankun("appone", { useDevMode: true })
    ],
    resolve: {
      alias: {
        '~': path.resolve(__dirname, './'),
        '@': path.resolve(__dirname, './src')
      },
      extensions: ['.mjs', '.js', '.ts', '.jsx', '.tsx', '.json', '.vue']
    },
    server: {
      port: 9527,
      open: true,
      proxy: {}
    }
  }
})

4.应用通信

initGlobalState(state)
定义全局状态,并返回通信方法,建议在主应用使用,微应用通过 props 获取通信方法。
● onGlobalStateChange: 在当前应用监听全局状态,有变更触发 callback,fireImmediately = true 立即触发 callback
● setGlobalState: 按一级属性设置全局状态,微应用中只能修改已存在的一级属性
● offGlobalStateChange: 移除当前应用的状态监听,微应用 umount 时会默认调用

1.主应用
// utils/qiankun/actions.js
import { initGlobalState } from 'qiankun'

export const state = {
  msg: '主应用的state.msg',
  getGlobalState: () => { console.log('调用自定义函数') }
}

// 初始化 state
export const actions = initGlobalState(state)

// 监听数据变化
actions.onGlobalStateChange((state, prev) => {
  console.log(state, prev)
})
// application.js
import actions from './actions.js'
const appliactions = [
  {
    name: "appone", // name 需要唯一
    entry: "http://localhost:9527/", // 应用地址
    container: "#app-micro",// 承载应用的容器的id
    activeRule: "/appone", // 匹配的路由
    props: {
      initData: '我是子应用1',
      parentAction: actions
    },
    loader (loading) {
      console.log('loading', loading)
    }
  },
  {
    name: "apptwo",
    entry: "http://localhost:8521/",
    container: "#app-micro",
    activeRule: "/apptwo",
    props: {
      initData: '我是子应用1',
      parentAction: actions
    }
    loader (loading) {
      console.log('loading', loading)
    }
  },
];
export default appliactions
2.子应用
mount(props) {
  console.log(props)
  // 监听数据变化
  props.onGlobalStateChange((state, prev) => {
    console.log(state, prev);
  })
  // 全局方法挂载 在任意组件直接使用
  app.config.globalProperties.parentAction = props.parentAction
}

打印props可以看到:
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值