找回密码
 会员注册
查看: 34|回复: 0

iframe接班人-微前端框架qiankun在中后台系统实践

[复制链接]

2万

主题

0

回帖

6万

积分

超级版主

积分
64431
发表于 2024-9-19 16:34:37 | 显示全部楼层 |阅读模式
「福利」✿✿ヽ(°▽°)ノ✿:文章最后有抽奖,转转纪念T恤,走过路过不要错过哦背景在转转的中台业务中,交易流转、业务运营和商户赋能等功能,主要集中在两个系统中(暂且命名为inner/outer)。两个系统基座(功能框架)类似,以inner系统为例,如图:inner系统基座业务现状问题维护迭代,随时间延续是不可避免的至今,inner/outer均有以下特点:页面结构繁杂分类较多,菜单页面多;布局五花八门,不统一技术栈不统一历史原因,存在jquery、静态模板、react等技术栈权限不统一不同用户,权限不一样,使用的功能模块不同项目管理不统一部分功能模块是由业务方维护;同一功能模块面向不同用户角色,也需要在不同系统中使用初次接触上述问题时,闪现在脑海里的是:用iframe呀。确实,刚开始也是这样做的。问题暴露,在维护迭代中是个契机系统在一个长时间跨度的运行下,随着维护人员的变迁、使用人群的增多,更多的问题也接踵而至:样式不统一由于没有统一规范,每个功能模块在不同的开发者键盘下设想的结构不同,输出的风格也不统一,使整个系统看起来略显杂乱。浏览器前进/后退首先,iframe页面没有自己的历史记录,使用的是基座(父页面)的浏览历史。所以,当iframe页在内部进行跳转时,浏览器地址栏无变化,基座中加载的src资源也无变化,当浏览器刷新时,无法停留在iframe内部跳转后的页面上,需要用户重新走一遍操作,体验上会大打折扣。弹窗遮罩层覆盖可视范围iframe页产生的弹窗,一般只能遮罩iframe区域。页面间消息传递与基座非同源下,iframe无法直接获取基座url的参数,消息传递需要周转一下,如使用postmessage来实现;而动态创建的iframe页,或许还需要借助本地存储等。页面缓存iframe资源变更上线后,打开系统会发现iframe页依旧是老资源。需要用时间戳方案或强制刷新。加载异常处理与基座非同源下,onerror事件无法使用。使用trycatch解决此问题,尝试获取contentDocument时将抛出异常以上问题,从业务价值看,对用户的使用体验会有损失;从工程价值看,希望能通过技术提升业务体验的同时,也提高系统的维护性。改进实践-微前端实践新技术,在问题暴露时是方向大多数工程师,包括我,一边儿嘴里说着:学不动啦!一边儿想尝试一些新方式来优化系统。结合问题分类,有思考一些尝试方向,如:中后台UI规范:历经迭代,百花齐放,然而更需要的是找到合适我司的风格,保持一致性。此部分这次不再细说,可以关注我们公众号-大转转FE,后续我们会有专门的文章讲这部分。另外,大互联网时代,从工程角度看,社区对类似系统的探索有很多,除了iframe外,也有不少相对成熟的替代方案:1.single-spa2.qiankun提起这两个,就要提一下微前端理念,目前社区有很多关于微前端架构的介绍,这里简单提一下:Techniques,strategiesandrecipesforbuildingamodernwebappwithmultipleteamsthatcanshipfeaturesindependently.—MicroFrontends大致是说,微前端有以下特点:技术栈无关:基座不限制子应用的技术栈完全独立:子应用独立部署维护,接入时基座同步更新;又可独立运行基于此,不难想到:iframe也是符合微前端理念的。那其他方案又是如何做的呢?single-spa社区里single-spa介绍也不少。根据demo比葫芦画瓢,可以知道它的架构分布:single-spa架构启动服务的配置主要是在single-spa-config文件中,包含项目名称、项目地址、路由配置等:// single-spa-config.jsimport {registerApplication, start } from 'single-spa';// 子应用唯一IDconst microAppName = 'react';// 子应用入口const loadingFunction = () => import('./react/app.js');// url前缀校验const activityFunction = location => location.pathname.startsWith('/react');// 注册registerApplication(  microAppName,  loadingFunction,  activityFunction);//singleSpa 启动start();single-spa让基座和子应用共用一个document,那就需要对子应用进行改造:把子项目的容器和生成的js插入到基座项目中。不需要HTML入口文件js入口文件导出的模块,必须包括bootstrap、mount和unmount三个方法不过这种方式需要对现有项目的打包方式和配置项进行改造,成本很大。所以,对于已有的工程项目,我选择了放弃使用。qiankunqiankun也是社区提到比较多的一个开源框架,是基于single-spa实现了开箱即用。可以采用htmlentry方式接入子应用,且子应用只需暴露一些生命周期,改动较少。【少】这个点,真是让我跃跃欲试。目前我司业务场景是单实例模式(一个运行时只有一个子应用被激活),我们可以根据一张图来看看单实例下以htmlentry方式qiankun实现流程:qiankun原理如上图所示,一个子应用的全过程有:初始化配置,匹配出子应用初始化子应用,加载对应的html资源,以及创建JS沙箱环境挂载子应用,执行生命周期钩子函数卸载子应用,当切换路由时,执行各卸载钩子函数,以及卸载JS沙箱环境,清除容器节点具体实现细节,大家可以参考qiankun源码。实践基座从规范化开发角度,我司的中后台系统是基于umi开发(详细可参考我们之前的文章umi中后台项目实践)。在构建主应用使用了配套的qiankun插件:@umijs/plugin-qiankun。1.初始化配置项,注册子应用插件安装之后,我们可以在入口文件里配置:此处主要以运行时为例// app.jsexport const qiankun = romise.resolve().then(() => ({  // 运行时注册子应用信息  apps: [    {      // 结算单管理      name: 'settlement', // 唯一id,与子应用的library 保持一致      entry: '//xxx', // html entry      history: 'hash', // 子应用的 history 配置,默认为当前主应用 history 配置      container: '#root-content', // 子应用存放节点      mountElementId: 'root-content' // 子应用存放节点    }, {      // 公告消息      name: 'news', // 唯一id,与子应用的library 保持一致      entry: '//xxx', // html entry      history: 'hash', // 子应用的 history 配置,默认为当前主应用 history 配置      container: '#root-content', // 子应用存放节点      mountElementId: 'root-content' // 子应用存放节点    }  ],  jsSandbox: { strictStyleIsolation: true }, // 是否启用 js 沙箱,默认为 false  prefetch: true, // 是否启用 prefetch 特性,默认为 true  lifeCycles: {    // see https://github.com/umijs/qiankun#registermicroapps    beforeLoad: (props) => {      return romise.resolve(props).then(() => loading())    },    afterMount: (props) => {      console.log('afterMount', props)    },    afterUnmount: (props) => {      console.log('afterUnmount', props)    }  }}))2.装载子应用,在路由配置中使用microApp来获取相应的子应用名称:// router.config.jsexport default [  {    path: '/',    component: '../layouts/BasicLayout',    routes: [      ...      {        path: '/settlement/list',        name: '结算单管理',        icon: 'RedEnvelopeOutlined',        microApp: 'settlement',  // 子应用唯一id      },      {        path: '/settlement/detail/:id',        name: '结算单管理',        icon: 'RedEnvelopeOutlined',        microApp: 'settlement', // 子应用唯一id        hideInMenu: true,      },      ...      ...      {        component: './404',      },    ],  },  {    component: './404',  },]以上就是基座的改动点,看起来代码侵入性很少。子应用在子应用中,需要做如下的配置1.入口文件设置baseName,及暴露钩子函数//设置主应用下的子应用路由命名空间const BASE_NAME = window.__POWERED_BY_QIANKUN__ ? "/settlement" : "";// 独立运行时,直接挂载应用if (!window.__POWERED_BY_QIANKUN__) {  effectRender();}// 在子应用初始化的时候调用一次export async function bootstrap() {  console.log("ReactMicroApp bootstraped");}export async function mount(props) {  console.log("ReactMicroApp mount", props);  effectRender(props);}//卸载子应用的应用实例export async function unmount(props) {  const { container } = props || {};  ReactDOM.unmountComponentAtNode(document.getElementById('root-content')  );}2.webpack配置中,需要设置输出为umd格式:// 设置别名merge: {  plugins: [new webpack.ProvidePlugin({    React: 'react',    ropTypes: 'prop-types'  })],  output: {    library: `[name]`, // 子应用的包名,这里与主应用中注册子应用名称一致    libraryTarget: "umd", // 所有的模块定义下都可运行的方式    jsonpFunction: `webpackJsonp_ReactMicroApp`, // 按需加载  }} //自定义webpack配置OK,配置完成!理论上,启动项目,部署等都应该没有问题了。咦,打开地址,页面一直在loading,控制台一堆报错,看起来要踩一踩坑了。踩坑1.版本一致性如果主应用和子应用都是基于umi框架,在使用@umijs/umi-plugin-qiankun插件时,要使用同一个版本,否则子应用报错。2.跨域qiankun是通过fetch去获取子应用资源的,所以必须支持跨域const mountDOM = appWrapperGetter();const { fetch } = frameworkConfiguration;const referenceNode = mountDOM.contains(refChild) ? refChild : null;if (src) {  execScripts(null, [src], proxy, {    fetch,    strictGlobal: !singular,    beforeExec: () => {      Object.defineProperty(document, 'currentScript', {        get(): any {          return element;        },        configurable: true,      })    };  })}比如:基座地址为b.zhuanzhuan.com,子应用为d.zhuanzhuan.com。当基座去加载子应用时,会出现跨域错误。曾经有采用通过Node服务做一层中转,跳过跨域问题:  ....  maxDays: 3, // 保留最大天数日志文件}// 代理config.httpProxy = {  '/cors': {    target: 'https://d.zhuanzhuan.com',    pathRewrite: {'^/cors' : ''}  }};return config但考虑应用的访问量,以及线上线下环境维护成本,觉得必要性不是很大,最终选择通过nginx解决跨域。3.子应用内部跳转子应用内部跳转,需要在基座路由上提前注册好,否则在跳转后,页面识别不到。{  path: '/settlement/detail/:id',  name: '结算单管理',  icon: 'RedEnvelopeOutlined',  microApp: 'settlement',  hideInMenu: true,},4.css污染qiankun只能解决子应用之间的样式相互污染,不能解决子应用样式污染基座的样式。比如:当切换到某个子应用时,左侧菜单栏突然往右移了。系统右移查看控制台,不难发现,子应用的相同模块覆盖了基座:样式覆盖这个问题,可以通过改变基座的前缀来解决,搞一个postcss插件给不同的组件添加不同的前缀。这里补充一个css隔离常用的方式如:css前缀、CSSModule、动态加载/卸载样式表。qiankun中css沙箱机制采用的是动态加载/卸载样式表。重写HTMLHeadElement.prototype.appendChild事件// Just overwrite it while it have not been overwriteif (  HTMLHeadElement.prototype.appendChild === rawHeadAppendChild &  HTMLBodyElement.prototype.appendChild === rawBodyAppendChild &  HTMLHeadElement.prototype.insertBefore === rawHeadInsertBefore) {  HTMLHeadElement.prototype.appendChild = getOverwrittenAppendChildOrInsertBefore({    rawDOMAppendOrInsertBefore: rawHeadAppendChild,    appName,    appWrapperGetter,    proxy,    singular,    dynamicStyleSheetElements,    scopedCSS,    excludeAssetFilter,  }) as typeof rawHeadAppendChild;....当子应用加载时,在head插入style/link;当卸载时,直接移除。// Just overwrite it while it have not been overwriteif (  HTMLHeadElement.prototype.removeChild === rawHeadRemoveChild &  HTMLBodyElement.prototype.removeChild === rawBodyRemoveChild) {  HTMLHeadElement.prototype.removeChild = getNewRemoveChild({    appWrapperGetter,    headOrBodyRemoveChild: rawHeadRemoveChild,  });  HTMLBodyElement.prototype.removeChild = getNewRemoveChild({    appWrapperGetter,    headOrBodyRemoveChild: rawBodyRemoveChild,  });}看起来很完美,但有时候会出现,基座样式丢失的问题。这个跟子应用卸载的时机有关系:当切换子应用时,当前子应用沙箱环境还未被卸载,但基座css已被插入,当卸载时会连带基座css一起被清除。5.错误捕获,降级处理若子应用加载失败,需要给相应的提示或动态插入iframe页:// iframe.jsexport default ({ sourceUrl }) =>  import { render } from 'react-dom';// 全局未捕获异常处理器addGlobalUncaughtErrorHandler((event) => {  console.error(event);  const { message, location: { hash } } = event;  // 加载失败时提示  if (message & message.includes("died in status LOADING_SOURCE_CODE")) {    Modal.Confirm({    content: "子应用加载失败,请检查应用是否可运行"    onOk: () => import('./Inframe.js')    });  }});6.路由懒加载样式丢失子应用中存在按需加载的路由,在加载时页面样式丢失,这是官方库产生的问题,issue里已有大佬提PR啦,可参考https://github.com/umijs/qiankun/issues/857以上,就是我们的不完全踩坑。应用间的通信,在我司的业务场景中复杂度不高,使用官方提供的方案就可以解决,此处没有详说。后续持续性思考会带来的技术红利此次接入qiankun,也只是处于表面应用。后续我们更要思考接入它之后更深的工程价值,如:-自动接入qiankun结合我司已有的脚手架和umi模板,额外添加一个命令,自动注册子应用,做到自动化。-子应用间组件共享基座和子应用大概率都用到了react/dva等,是否可以在基座加载完之后,子应用直接复用?当然,浅显思考应该少不了webpack的externals。文末福利转发本文并留下评论,我们将抽取第10名留言者(依据公众号后台排序),送出转转纪念T恤一件,大家转发起来吧~
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 会员注册

本版积分规则

QQ|手机版|心飞设计-版权所有:微度网络信息技术服务中心 ( 鲁ICP备17032091号-12 )|网站地图

GMT+8, 2024-12-26 23:30 , Processed in 0.373415 second(s), 25 queries .

Powered by Discuz! X3.5

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表