写点什么

简单聊聊微前端

  • 2024-11-14
    福建
  • 本文字数:7908 字

    阅读完需:约 26 分钟

什么是微前端?


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


为什么要用微前端?


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

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

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

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

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

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


行业解决方案?


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

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

  3. 基于 Web Components 的微前端方案:  Web Components 是浏览器的原生组件,允许创建可重用的用户界面小部件。这种方案的优点包括技术栈无关、独立开发和应用间隔离。然而,由于 Web Components 的浏览器和框架支持不够广泛,可能需要更多的 polyfills,且重写现有的前端应用和系统架构可能较为复杂。  MicroApp:    特点:由京东出品,基于 WebComponent 的思想实现的微前端框架。它轻量、高效,且提供了 js 沙箱、样式隔离、元素隔离、预加载等一系列完善的功能。    优势:使用起来成本较低,不需要修改子应用的渲染逻辑或 webpack 配置,接入微前端成本较低。此外,它无任何依赖,体积小巧,扩展性高。    适用场景:适合需要快速集成不同技术栈子应用的项目。

  4. 基于 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,且社区支持相对较少。

  5. 中心基座方案(如 qiankun 等):  中心基座方案是目前主流的微前端采用的技术方案之一。  qiankun:  基于 single-spa 进行二次开发,提供了更加开箱即用的 API、样式隔离、JS 沙箱和资源预加载等功能。这种方案的优点是技术栈无关、易于集成和管理微应用,但可能需要注意沙箱隔离的完善性和性能优化。  Single-spa:    特点:Single-spa 是最早的微前端框架,它允许多个前端框架应用(如 Vue、React、Angular)同时工作在同一个页面上。每个子应用可以使用不同的框架,技术栈灵活。    优势:提供了依赖共享机制,避免多个应用加载相同的依赖包,且生态完善,有丰富的社区插件和工具支持。    不足:学习曲线较陡峭,配置较为复杂,需要专门学习,且在同时加载多个子应用时性能可能受影响。  Garfish:    特点:字节跳动推出的微前端框架,专注于轻量级和高性能的解决方案。它无需复杂的配置即可使用,适合快速开发,且支持多种前端框架。    优势:性能优越,适合对速度有要求的项目。同时提供了技术栈无关的支持,灵活性高。    不足:相对于其他成熟的微前端方案,Garfish 的社区支持和文档相对较少,且在某些复杂场景下可能需要额外的开发工作。

  6. 自由框架组合模式

 

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 组件页面内容变化的核心区域


<!-- content 微前端 页面内容变化的核心区域 --><div class="unusual-container" v-show="loadErrorInfo.isError">    <!-- 错误兜底页面 -->    <ErrorPage :subTitle="loadErrorInfo.errorMessage" :status="loadErrorInfo.errorStatus" /></div><!-- 无报错 资源正常加载展示 MICRO_APP_CONTAINER_ID 微前端容器id--><div class="micro-view-container" :id="MICRO_APP_CONTAINER_ID" v-show="!loadErrorInfo.isError">    <template v-for="item in microList" :key="`${item.entryKey}`">      <div :id="item.container" v-show="showContainer(item.container)" class="microContainer"></div>    </template>    <Skeleton active v-show="showContainer('empty')" /></div>
复制代码


平台主题容器


<!-- layouts/content 通用的主题header 菜单等 --><div :class="[prefixCls, getLayoutContentMode]">    <div v-show="isShowMicroPage">      <MicroPage />    </div>    <div v-show="!isShowMicroPage">      <NormalPage />    </div></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     return106   }107   // 子工程回传的路由列表108   // microLogger.info('校验权限路由实例:', findRouteInstance)109 110   if ('如果子工程中回传的路由实例有ignoreAuth则不需要检验权限') {111     return112   }113   if (!'路由权限校验') {114     setMicroAppLoadErrorInfo(MicroAppErrorType.PAGE_NOT_ACCESS)115   }116 }117 118 // 校验当前路有是否是存在注册列表中的119 const checkIsExist = (routePath: string): boolean => {120   if ('如果调试模式开启了,则直接跳过不校验') {121     return true122   }123 124   if (!'当前路由从已注册的子工程列表中查找不存在') {125     setMicroAppLoadErrorInfo('错误收集')126     return false127   }128   return true129 }130 131 watch(132   () => loadErrorInfo.value.isError,133   (isError) => {134     if (isError) {136       // 重置是否是同一个entry ...138       return139     }140     isLocalError = false141   },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 })
复制代码


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


// 下面例子 以 lol、cf、dnf 三款游戏作为三个子应用 举例export const microBaseConfig = {  lol: {    entryKey: 'lol',    container: 'lolDom',    entry: {      dev: '',      testing: '',      staging: '',      prod: '',    },  },  cf: {    entryKey: 'cf',    container: 'cfDom',    entry: {      dev: '',      testing: '',      staging: '',      prod: '',    },  },  dnf: {    entryKey: 'dnf',    container: 'dnfDom',    entry: {      dev: '',      testing: '',      staging: '',      prod: '',    },  },}
const microConfigMap: Record<string, MicroConfig> = {}// 遍历对象的键Object.keys(microBaseConfig).forEach((key) => { const originalItem = microBaseConfig[key] // 复制原始对象,避免直接修改 const newItem: MicroConfig = { ...originalItem, entryUrl: originalItem.entry[import.meta.env.MODE], } microConfigMap[key] = newItem})
export const microConfig = microConfigMapexport const microList: MicroConfig[] = Object.values(microConfigMap)export const lolEntry = microConfigMap.lol.entryUrlexport const cfEntry = microConfigMap.cf.entryUrlexport const dnfEntry = microConfigMap.dnf.entryUrl

// 在基座注册的子应用路由/** * @description: 游戏首页面板 */const gameRoute: MicroAppInfoConfig = { name: 'game', path: '/game/index', entry: lolEntry, isMicroApp: true, meta: { icon: 'home|svg', title: '首页', groupTitle: '分组标题', },}
/** * @description: lol游戏中心 */const lolRoute: MicroAppInfoConfig = { name: 'lol', path: '/lol', entry: lolEntry, isMicroApp: true, meta: { icon: 'lolIcon|svg', title: 'lol游戏中心', groupTitle: '分组标题', }, children: [ { name: 'lol-list', path: '/lol/list', entry: lolEntry, isMicroApp: true, meta: { title: '英雄列表', }, }, { path: '/lol/add', name: 'lol-add', entry: lolEntry, isMicroApp: true, meta: { title: '创建英雄', hideMenu: true, currentActiveMenu: '/lol/list', }, }, ],}
/** * @description: cf游戏中心 */const linkRoute: MicroAppInfoConfig = { path: '/cf', name: 'cf', entry: cfEntry, isMicroApp: true, meta: { icon: 'cf|svg', title: 'cf管理', groupTitle: '分组标题', }, children: [ { path: '/cf/list', name: 'cfList', entry: cfEntry, isMicroApp: true, meta: { title: '武器总览库', }, }, { path: '/cf/detail', name: 'cfDetail', entry: cfEntry, isMicroApp: true, meta: { title: '武器详情', }, }, ],}
/** * @description: dnf游戏中心 */const dnfRoute: MicroAppInfoConfig = { path: '/dnf/list', name: 'dnf', entry: dnfEntry, isMicroApp: true, meta: { hideChildrenInMenu: true, icon: 'dnf|svg', title: 'dnf发展历史', groupTitle: '分组标题', }, children: [ { path: '/dnf/list', name: 'dnfList', entry: dnfEntry, isMicroApp: true, meta: { title: '发展历史', currentActiveMenu: '/history', }, }, { path: '/dnf/detail', name: 'dnf-detail', entry: dnfEntry, isMicroApp: true, meta: { title: '年况详情', currentActiveMenu: '/history', hideMenu: true, }, }, ],}
复制代码


子应用代码库 main.js 


import { qiankunWindow, renderWithQiankun } from 'vite-plugin-qiankun/dist/helper'
export const isQianKun = (() => { const bool = qiankunWindow.__POWERED_BY_QIANKUN__ || !!qiankunWindow.name console.log('isQianKun', bool) return bool})()
// 放到项目顶部if (isQianKun) { window['__webpack_public_path__'] = qiankunWindow.__INJECTED_PUBLIC_PATH_BY_QIANKUN__}
// 正常逻辑async function render() {}
// 初始化qiankunconst initQianKun = () => { console.log('initQianKun', '子应用初始化') renderWithQiankun({ // @ts-ignore bootstrap(props) { console.log('微应用 vehicle:bootstrap', props) }, mount(props: PassToMicroAppProps) { console.log('微应用 vehicle:mount', props) window.parentWindow = props?.parentWindow // 可以通过props读取基座传过来的数据 // ... render(props) props.onGlobalStateChange((state: QiankunStore, prev: QiankunStore) => { console.log('task子应用onGlobalStateChange改变的state: ', state) console.log('task子应用onGlobalStateChange改变的prev: ', prev) }) props.setGlobalState({ currentMicroAppRoutes: dynamicRoutes, }) }, unmount(props) { console.log('微应用 vehicle:unmount', props) app?.unmount() }, update(props) { console.log('微应用 vehicle:update', props) }, })}isQianKun ? initQianKun() : render()
复制代码


文章转载自:入坑的H

原文链接:https://www.cnblogs.com/EternalZH/p/18542500

体验地址:http://www.jnpfsoft.com/?from=infoq

用户头像

还未添加个人签名 2023-06-19 加入

还未添加个人简介

评论

发布
暂无评论
简单聊聊微前端_前端_快乐非自愿限量之名_InfoQ写作社区