微前端
1.什么是微前端
- 微前端的概念是由 ThoughtWorks 在2016年提出的。
- 微前端是存在于浏览器中的微服务,借鉴了后端微服务的架构理念,将微服务的概念扩展到前端。
- 微前端是一种前端架构风格,将一个庞大的前端应用拆分成多个独立灵活的微应用,每个应用都可以独立开发、独立运行、独立部署,或者将原本运行已久,没有关联的几个应用融合为一个应用。
- 微前端既可以将多个项目融合,又可以减少项目之间的耦合,提高开发效率和可维护性。微前端的核心在于解耦,通过拆分和集成来实现前端应用的可扩展性和灵活性。
2.什么时候用微前端
- 一个非常庞大的项目,越来越大,后续难以维护。
- 在一些大厂,经常会有跨部门和跨团队协作开发项目,这样会导致团队效率降低和沟通成本加大,这时我们可以使用微前端,每个团队或者每个部门单独维护自己的项目,我们只需要一个主项目来把分散的子项目汇集到一起即可。
- 一个非常老旧的项目,开发效率低,但是一时半会又不能全部重构,这时我们就可以新创建一个新技术新项目的基座,把老项目的页面接入到新项目里面,后面新需求都在新项目里面开发就好,不用再动老项目。
- 想独立部署每一个单页面应用。
- 改善初始化加载时间,延迟加载代码。
- 基于多页的子应用缺乏管理,规范/标准不统一,无法统一控制视觉呈现、共享的功能和依赖。造成重复工作。
3.微前端的能力
- 同步更新:多个业务应用依赖同一个服务应用的功能,只需要更新服务应用,其他业务应用可立马更新。
- 增量升级:对系统做全量的升级或重构很复杂,只升级更新需要的微应用,实施渐进式重构。
- 独立开发:主框架不限制接入应用的技术栈,微应用可自主选择技术栈。
- 独立部署:微应用可以独立部署,互不影响,可以使用不同的部署方式、不同的版本控制工具等
- 独立运行:每个微应用之间状态隔离,运行时状态不共享
4.微前端的模式
- 基座模式:通过搭建基座、配置中心来管理子应用,如基于single-spa的通用qiankun方案
- 自组织模式:通过约定进行互调,但会遇到第三方依赖等问题
- 去中心模式:脱离基座模式,每个应用都可以彼此共享资源分享,如基于webpak5 module federation实现的emp微前端方案
5.微前端技术方案
- iframe:
微前端最简单方案,通过iframe加载子应用,通信使用window.postMessage,完美的沙箱机制自带应用隔离 - web components
将前端应用分解为自定义HTML元素,基于CustomEvent实现通信,Shadow DOM天生的作用域隔离 - 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可以看到: