简单聊聊微前端

什么是微前端?

微前端是一种前端架构模式,它将一个庞大的前端应用拆分为多个独立、小型的应用,这些小型应用可以独立开发、独立运行、独立部署,但对用户而言,它们仍然是一个统一的整体。这种架构模式主要是为了解决传统单体应用在大型项目中遇到的问题,如代码冗余、开发效率低下、部署风险高等。

为什么要用微前端?

  1. 模块化与解耦

      微前端强调模块化,每个微应用都是一个独立的模块,这使得代码更加清晰、易于维护。
      通过将前端应用拆分为多个独立的子应用,可以实现业务逻辑的解耦,降低系统的复杂性。
  1. 提高开发效率

      微前端架构允许不同团队并行开发各自的微应用,从而缩短了开发周期。
      由于微应用可以独立部署,因此无需等待其他团队的开发进度,即可快速上线新功能。
  1. 降低部署风险

      在传统的单体应用中,每次部署都涉及整个应用的更新,风险较高。而微前端架构下,每次只需部署更新的微应用,降低了部署的风险和影响范围。
  1. 技术栈灵活性

      微前端架构不限制接入的微应用的技术栈,这意味着团队可以根据自身需求和技术储备选择合适的技术栈进行开发。
      这种灵活性有助于团队尝试新技术、保持技术栈的更新和多样性。
  1. 渐进式重构与升级

      对于遗留系统或大型项目,微前端提供了一种渐进式重构和升级的策略。通过逐个替换或升级微应用,可以逐步实现整个系统的现代化改造。
  1. 更好的用户体验

      微前端架构有助于优化前端性能,如减少首次加载时间、提高页面响应速度等,从而提升用户体验。
      通过动态加载和卸载微应用,可以实现更细粒度的资源管理和优化。

行业解决方案?

  1. 基于路由分发的微前端方案

      这种方案通过配置路由来分发请求到不同的微应用。每个微应用可以独立开发、测试和部署,而在用户看来仍然是内聚的单个产品。此方案的优点包括简单、快速和易配置,但可能在切换应用时触发浏览器刷新,影响体验。
  1. 基于iframe的微前端方案

      iframe作为一种古老的技术,可以轻松地从独立的子页面构建页面,提供天然的隔离性。这种方案的优点是实现简单、技术不限制,但缺点是可能存在Bundle大小各异、SEO不友好、URL状态不同步、DOM结构不共享以及全局上下文完全隔离等问题。
  1. 基于Web Components的微前端方案

      Web Components是浏览器的原生组件,允许创建可重用的用户界面小部件。这种方案的优点包括技术栈无关、独立开发和应用间隔离。然而,由于Web Components的浏览器和框架支持不够广泛,可能需要更多的polyfills,且重写现有的前端应用和系统架构可能较为复杂。

      MicroApp

        特点:由京东出品,基于WebComponent的思想实现的微前端框架。它轻量、高效,且提供了js沙箱、样式隔离、元素隔离、预加载等一系列完善的功能。
        优势:使用起来成本较低,不需要修改子应用的渲染逻辑或webpack配置,接入微前端成本较低。此外,它无任何依赖,体积小巧,扩展性高。
        适用场景:适合需要快速集成不同技术栈子应用的项目。
  1. 基于Module Federation的微前端方案

      Module Federation是webpack5新增的功能,可以帮助将多个独立的构建组成一个应用程序。这种方案的优点包括开箱即用、独立开发与部署、去中心化和组件共享。但缺点是可能无法提供沙箱隔离、技术单一(仅限使用webpack5以上版本)、代码封闭性高以及拆分粒度需要权衡。

      EMP(Esm Module Federation):

        特点:基于Webpack 5 Module Federation特性进行二次封装,特别优化了对ESM(ECMAScript Modules)的支持。它允许多个应用共享模块,子应用可以在不重新构建的情况下被主应用加载和使用。
        优势:完全支持ESM模块系统,减少模块解析开销,提高加载效率。相比原生的Module Federation,EMP配置更简便。
        不足:学习曲线存在,虽然配置简化,但依然需要掌握Module Federation的核心概念。此外,技术栈有限制,需要使用Webpack 5,且社区支持相对较少。
  1. 中心基座方案(如qiankun等)

      中心基座方案是目前主流的微前端采用的技术方案之一。

      qiankun:

      基于single-spa进行二次开发,提供了更加开箱即用的API、样式隔离、JS沙箱和资源预加载等功能。这种方案的优点是技术栈无关、易于集成和管理微应用,但可能需要注意沙箱隔离的完善性和性能优化。

      Single-spa

        特点:Single-spa是最早的微前端框架,它允许多个前端框架应用(如Vue、React、Angular)同时工作在同一个页面上。每个子应用可以使用不同的框架,技术栈灵活。
        优势:提供了依赖共享机制,避免多个应用加载相同的依赖包,且生态完善,有丰富的社区插件和工具支持。
        不足:学习曲线较陡峭,配置较为复杂,需要专门学习,且在同时加载多个子应用时性能可能受影响。

      Garfish

        特点:字节跳动推出的微前端框架,专注于轻量级和高性能的解决方案。它无需复杂的配置即可使用,适合快速开发,且支持多种前端框架。
        优势:性能优越,适合对速度有要求的项目。同时提供了技术栈无关的支持,灵活性高。
        不足:相对于其他成熟的微前端方案,Garfish的社区支持和文档相对较少,且在某些复杂场景下可能需要额外的开发工作。
  1. 自由框架组合模式

 

qiankun

一、概念

qiankun,意为“乾坤”,是阿里巴巴开源的一个微前端框架。它通过HTML Entry的方式接入微应用,使得接入过程像使用iframe一样简单。在qiankun中,主应用负责加载和管理子应用,而子应用则是独立的前端应用,可以独立开发、部署和运行。

二、原理

  1. 路由劫持与应用加载:qiankun基于single-spa实现了路由劫持和应用加载。当浏览器的URL发生变化时,qiankun会匹配到相应的子应用并进行加载。
  2. 样式隔离:qiankun实现了两种样式隔离方式。一种是严格的样式隔离模式,通过为每个微应用的容器包裹上一个shadow dom节点来实现。另一种是通过动态改写css选择器来实现,类似于css scoped的方式。
  3. JS沙箱:qiankun的JS沙箱分为两种实现方式。在主流浏览器中(支持Proxy),使用基于Proxy的多实例沙箱实现。在不支持Proxy的浏览器中,则使用基于diff的沙箱实现。这些沙箱确保了子应用的JS执行环境相互隔离,防止了冲突和污染。
  4. 资源预加载:qiankun实现了资源的预加载策略,即在浏览器空闲时间预加载未打开的微应用资源,从而加速微应用的打开速度。
  5. 应用间通信:qiankun通过发布订阅模式来实现应用间通信。每个应用在初始化时会生成一套通信方法,用于更改全局状态和注册回调函数。当全局状态发生改变时,会触发各个应用注册的回调函数执行。

三、优缺点

优点
  1. 技术栈无关:qiankun允许任意技术栈的应用接入,无论是React、Vue、Angular还是其他框架,都可以轻松集成。
  2. 简单易用:qiankun提供了开箱即用的API和丰富的生命周期函数,使得微前端的开发和管理变得简单高效。
  3. 性能优化:通过资源预加载和应用间通信机制,qiankun优化了微应用的加载速度和性能表现。
  4. 社区支持:作为阿里巴巴开源的项目,qiankun拥有强大的社区支持和广泛的实践案例。
缺点
  1. 样式隔离的局限性:虽然qiankun实现了样式隔离,但在某些复杂场景下,仍可能出现样式冲突或覆盖的情况。这需要开发者在使用时注意样式的管理和规划。
  2. 学习成本:虽然qiankun简化了微前端的开发过程,但对于初次接触微前端的开发者来说,仍需要一定的学习成本来理解和掌握其原理和使用方法。
  3. 框架依赖:qiankun是基于single-spa进行封装的,因此在使用qiankun时,也需要对single-spa有一定的了解和认识。这可能会增加一些额外的学习负担。
 
 

实践 - 伪代码

基座
做一套基座的容器 - vue为例
MicroPage 组件页面内容变化的核心区域
 1 <!-- content 微前端 页面内容变化的核心区域 -->
 2 <div class="unusual-container" v-show="loadErrorInfo.isError">
 3     <!-- 错误兜底页面 -->
 4     <ErrorPage :subTitle="loadErrorInfo.errorMessage" :status="loadErrorInfo.errorStatus" />
 5 </div>
 6 <!-- 无报错 资源正常加载展示 MICRO_APP_CONTAINER_ID 微前端容器id-->
 7 <div class="micro-view-container" :id="MICRO_APP_CONTAINER_ID" v-show="!loadErrorInfo.isError">
 8     <template v-for="item in microList" :key="`${item.entryKey}`">
 9       <div :id="item.container" v-show="showContainer(item.container)" class="microContainer"></div>
10     </template>
11     <Skeleton active v-show="showContainer('empty')" />
12 </div>
13 

平台主题容器

1 <!-- layouts/content 通用的主题header 菜单等 -->
2 <div :class="[prefixCls, getLayoutContentMode]">
3     <div v-show="isShowMicroPage">
4       <MicroPage />
5     </div>
6     <div v-show="!isShowMicroPage">
7       <NormalPage />
8     </div>
9 </div>
基座初始化配置
  1 import { loadMicroApp, MicroApp } from 'qiankun'
  2 
  3 
  4 // 加载并渲染微应用
  5 const mountMicroApp = async () => {
  6   // store中存了错误信息
  7   if (isError) {
  8     return
  9   }
 10   // 没有找到entry
 11   if (!entry) {
 12     setMicroAppLoadErrorInfo('set错误信息')
 13     return
 14   }
 15 
 16   // 加载前预检一级路由是否有权限
 17   if (!'权限' && !'报错' && !'校验一级路路由是否有权限') {
 18     setMicroAppLoadErrorInfo('set错误信息')
 19     return
 20   }
 21 
 22   // 清空错误信息
 23   setMicroAppLoadErrorInfo('清空错误信息')
 24  
 25  
 26   if (!microItem) {
 27     console.log(`没有找到[${entry}]的微应用信息`)
 28     return
 29   }
 30  
 31 
 32   const microAppInstance = loadMicroApp(
 33     {
 34       name,
 35       entry: `${entry}/?__v=${new Date().getTime()}`,
 36       container: microContainer,
 37       props: {
 38         // 跳转到指定路径对应的微应用 - 先存后跳,
 39         token
 40         permissionMap,
 41         userInfo,
 42         permissionEnum,
 43         stationOrgList,
 44         defaultStation,
 45         homePath,
 46         // microLogout: 处理登录逻辑的函数,
 47         microEmitter, // 子工程用来通信的emitter
 48         logSentryMsg,
 49         logSentryError,
 50         logBreadCrumb,
 51         microDefHttp: defHttp,
 52         parentWindow: window,
 53       } as unknown as PassToMicroAppProps,
 54     },
 55     {
 56       excludeAssetFilter: (url) => url.indexOf('.baidu.com') !== -1,
 57 
 58     },
 59   )
 60 
 61   microAppInstanceMap[entry] = microAppInstance
 62 
 63   await microAppInstance.mountPromise
 64     .then(() => {
 65       // 权限校验
 66     })
 67     .catch((err) => {
 68       // ...
 69     })
 70 }
 71 
 72 // 切换显示子应用 容器
 73 const changeContainer = async (entry, immediate) => {
 74   if (!microItem) {
 75     console.log(`没有找到[${entry}]的微应用信息`)
 76     return
 77   }
 78 }
 79 
 80 
 81 // 卸载微应用
 82 const unmountMicroApp = async (entry) => {
 83   if (!'如果store中和变量中都没有微应用实例,则不需要卸载') {
 84     return
 85   }
 86 
 87   if (!'上个子工程加载异常不需要卸载') {
 88     return
 89   }
 90   
 91   // 只有mount成功的app,才执行卸载
 92   let mounted = true
 93   await needUnmountApp!.mountPromise.catch(() => (mounted = false))
 94   if (!mounted) {
 95     return
 96   }
 97 
 98   // 卸载失败时打印日志并继续
 99   
100 }
101 
102 // 权限校验
103 const checkMicroAppPermission = (routePath: string) => {
104   if ('如果调试模式开启了,则直接跳过不校验') {
105     return
106   }
107   // 子工程回传的路由列表
108   // microLogger.info('校验权限路由实例:', findRouteInstance)
109 
110   if ('如果子工程中回传的路由实例有ignoreAuth则不需要检验权限') {
111     return
112   }
113   if (!'路由权限校验') {
114     setMicroAppLoadErrorInfo(MicroAppErrorType.PAGE_NOT_ACCESS)
115   }
116 }
117 
118 // 校验当前路有是否是存在注册列表中的
119 const checkIsExist = (routePath: string): boolean => {
120   if ('如果调试模式开启了,则直接跳过不校验') {
121     return true
122   }
123 
124   if (!'当前路由从已注册的子工程列表中查找不存在') {
125     setMicroAppLoadErrorInfo('错误收集')
126     return false
127   }
128   return true
129 }
130 
131 watch(
132   () => loadErrorInfo.value.isError,
133   (isError) => {
134     if (isError) {
136       // 重置是否是同一个entry ...
138       return
139     }
140     isLocalError = false
141   },
142 )
143 
144 // 监听路由变更
145 watch(
146   () => currentRoute.value.path,
147   async (path: string) => {},
148 )
149 
150 // 监听当前微应用的entry变更
151 watch(
152   () => entry,
153   async (entry: string) => {},
154   { immediate: true },
155 )
156 
157 // 监听消息盒子同一路由点击是否需要重新加载微应用
158 watch(
159   () => xxx,
160   async (xxx) => {},
161 )
162 
163 onMounted(() => {
164   console.log('初始化微应用容器', '++++++++++++++++')
165   microEmitter.on('xxx', (tab) => {})
166 })

基座内维护的子应用信息集合

  1 // 下面例子 以 lol、cf、dnf 三款游戏作为三个子应用 举例
  2 export const microBaseConfig = {
  3   lol: {
  4     entryKey: 'lol',
  5     container: 'lolDom',
  6     entry: {
  7       dev: '',
  8       testing: '',
  9       staging: '',
 10       prod: '',
 11     },
 12   },
 13   cf: {
 14     entryKey: 'cf',
 15     container: 'cfDom',
 16     entry: {
 17       dev: '',
 18       testing: '',
 19       staging: '',
 20       prod: '',
 21     },
 22   },
 23   dnf: {
 24     entryKey: 'dnf',
 25     container: 'dnfDom',
 26     entry: {
 27       dev: '',
 28       testing: '',
 29       staging: '',
 30       prod: '',
 31     },
 32   },
 33 }
 34 
 35 const microConfigMap: Record<string, MicroConfig> = {}
 36 // 遍历对象的键
 37 Object.keys(microBaseConfig).forEach((key) => {
 38   const originalItem = microBaseConfig[key]
 39   // 复制原始对象,避免直接修改
 40   const newItem: MicroConfig = {
 41     ...originalItem,
 42     entryUrl: originalItem.entry[import.meta.env.MODE],
 43   }
 44   microConfigMap[key] = newItem
 45 })
 46 
 47 export const microConfig = microConfigMap
 48 export const microList: MicroConfig[] = Object.values(microConfigMap)
 49 export const lolEntry = microConfigMap.lol.entryUrl
 50 export const cfEntry = microConfigMap.cf.entryUrl
 51 export const dnfEntry = microConfigMap.dnf.entryUrl
 52 
 53 
 54 // 在基座注册的子应用路由
 55 /**
 56  * @description: 游戏首页面板
 57  */
 58 const gameRoute: MicroAppInfoConfig = {
 59   name: 'game',
 60   path: '/game/index',
 61   entry: lolEntry,
 62   isMicroApp: true,
 63   meta: {
 64     icon: 'home|svg',
 65     title: '首页',
 66     groupTitle: '分组标题',
 67   },
 68 }
 69 
 70 /**
 71  * @description: lol游戏中心
 72  */
 73 const lolRoute: MicroAppInfoConfig = {
 74   name: 'lol',
 75   path: '/lol',
 76   entry: lolEntry,
 77   isMicroApp: true,
 78   meta: {
 79     icon: 'lolIcon|svg',
 80     title: 'lol游戏中心',
 81     groupTitle: '分组标题',
 82   },
 83   children: [
 84     {
 85       name: 'lol-list',
 86       path: '/lol/list',
 87       entry: lolEntry,
 88       isMicroApp: true,
 89       meta: {
 90         title: '英雄列表',
 91       },
 92     },
 93     {
 94       path: '/lol/add',
 95       name: 'lol-add',
 96       entry: lolEntry,
 97       isMicroApp: true,
 98       meta: {
 99         title: '创建英雄',
100         hideMenu: true,
101         currentActiveMenu: '/lol/list',
102       },
103     },
104   ],
105 }
106 
107 /**
108  * @description: cf游戏中心
109  */
110 const linkRoute: MicroAppInfoConfig = {
111   path: '/cf',
112   name: 'cf',
113   entry: cfEntry,
114   isMicroApp: true,
115   meta: {
116     icon: 'cf|svg',
117     title: 'cf管理',
118     groupTitle: '分组标题',
119   },
120   children: [
121     {
122       path: '/cf/list',
123       name: 'cfList',
124       entry: cfEntry,
125       isMicroApp: true,
126       meta: {
127         title: '武器总览库',
128       },
129     },
130     {
131       path: '/cf/detail',
132       name: 'cfDetail',
133       entry: cfEntry,
134       isMicroApp: true,
135       meta: {
136         title: '武器详情',
137       },
138     },
139   ],
140 }
141 
142 /**
143  * @description: dnf游戏中心
144  */
145 const dnfRoute: MicroAppInfoConfig = {
146   path: '/dnf/list',
147   name: 'dnf',
148   entry: dnfEntry,
149   isMicroApp: true,
150   meta: {
151     hideChildrenInMenu: true,
152     icon: 'dnf|svg',
153     title: 'dnf发展历史',
154     groupTitle: '分组标题',
155   },
156   children: [
157     {
158       path: '/dnf/list',
159       name: 'dnfList',
160       entry: dnfEntry,
161       isMicroApp: true,
162       meta: {
163         title: '发展历史',
164         currentActiveMenu: '/history',
165       },
166     },
167     {
168       path: '/dnf/detail',
169       name: 'dnf-detail',
170       entry: dnfEntry,
171       isMicroApp: true,
172       meta: {
173         title: '年况详情',
174         currentActiveMenu: '/history',
175         hideMenu: true,
176       },
177     },
178   ],
179 }

子应用代码库 main.js 

 1 import { qiankunWindow, renderWithQiankun } from 'vite-plugin-qiankun/dist/helper'
 2 
 3 export const isQianKun = (() => {
 4   const bool = qiankunWindow.__POWERED_BY_QIANKUN__ || !!qiankunWindow.name
 5   console.log('isQianKun', bool)
 6   return bool
 7 })()
 8 
 9 // 放到项目顶部
10 if (isQianKun) {
11   window['__webpack_public_path__'] = qiankunWindow.__INJECTED_PUBLIC_PATH_BY_QIANKUN__
12 }
13 
14 // 正常逻辑
15 async function render() {}
16 
17 // 初始化qiankun
18 const initQianKun = () => {
19   console.log('initQianKun', '子应用初始化')
20   renderWithQiankun({
21     // @ts-ignore
22     bootstrap(props) {
23       console.log('微应用 vehicle:bootstrap', props)
24     },
25     mount(props: PassToMicroAppProps) {
26       console.log('微应用 vehicle:mount', props)
27       window.parentWindow = props?.parentWindow
28       // 可以通过props读取基座传过来的数据
29       // ...
30       render(props)
31       props.onGlobalStateChange((state: QiankunStore, prev: QiankunStore) => {
32         console.log('task子应用onGlobalStateChange改变的state: ', state)
33         console.log('task子应用onGlobalStateChange改变的prev: ', prev)
34       })
35       props.setGlobalState({
36         currentMicroAppRoutes: dynamicRoutes,
37       })
38     },
39     unmount(props) {
40       console.log('微应用 vehicle:unmount', props)
41       app?.unmount()
42     },
43     update(props) {
44       console.log('微应用 vehicle:update', props)
45     },
46   })
47 }
48 isQianKun ? initQianKun() : render()

 

posted @ 2024-11-12 19:38  入坑的H  阅读(380)  评论(0编辑  收藏  举报