iframe 接班人-微前端框架 qiankun 在中后台系统实践
「福利」 ✿✿ ヽ(°▽°)ノ ✿:文章最后有抽奖,转转纪念 T 恤,走过路过不要错过哦
背景
在转转的中台业务中,交易流转、业务运营和商户赋能等功能,主要集中在两个系统中(暂且命名为 inner/outer
)。两个系统基座(功能框架)类似,以 inner 系统为例,如图:
业务现状问题
维护迭代,随时间延续是不可避免的
至今,inner/outer
均有以下特点:
页面结构繁杂 分类较多,菜单页面多;布局五花八门,不统一
技术栈不统一 历史原因,存在
jquery
、静态模板
、react
等技术栈权限不统一 不同用户,权限不一样,使用的功能模块不同
项目管理不统一 部分功能模块是由业务方维护;同一功能模块面向不同用户角色,也需要在不同系统中使用
初次接触上述问题时,闪现在脑海里的是:用 iframe 呀。确实,刚开始也是这样做的。
问题暴露,在维护迭代中是个契机
系统在一个长时间跨度的运行下,随着维护人员的变迁、使用人群的增多,更多的问题也接踵而至:
样式不统一
由于没有统一规范,每个功能模块在不同的开发者键盘下设想的结构不同,输出的风格也不统一,使整个系统看起来略显杂乱。
浏览器前进/后退
首先,iframe
页面没有自己的历史记录,使用的是基座(父页面)的浏览历史。所以,当iframe
页在内部进行跳转时,浏览器地址栏无变化,基座中加载的 src
资源也无变化,当浏览器刷新时,无法停留在iframe
内部跳转后的页面上,需要用户重新走一遍操作,体验上会大打折扣。
弹窗遮罩层覆盖可视范围
iframe
页产生的弹窗,一般只能遮罩 iframe
区域。
页面间消息传递
与基座非同源下,iframe
无法直接获取基座 url
的参数,消息传递需要周转一下,如使用postmessage
来实现;而动态创建的 iframe
页,或许还需要借助本地存储等。
页面缓存
iframe
资源变更上线后,打开系统会发现 iframe
页依旧是老资源。需要用时间戳方案或强制刷新。
加载异常处理
与基座非同源下,onerror
事件无法使用。使用 try catch
解决此问题,尝试获取 contentDocument
时将抛出异常
以上问题,从业务价值看,对用户的使用体验会有损失;从工程价值看,希望能通过技术提升业务体验的同时,也提高系统的维护性。
改进实践 - 微前端
实践新技术,在问题暴露时是方向
大多数工程师,包括我,一边儿嘴里说着:学不动啦!一边儿想尝试一些新方式来优化系统。
结合问题分类,有思考一些尝试方向,如:
中后台 UI 规范:历经迭代,百花齐放,然而更需要的是找到合适我司的风格,保持一致性。
此部分这次不再细说,可以 关注我们公众号 - 大转转 FE,后续我们会有专门的文章讲这部分。
另外,大互联网时代,从工程角度看,社区对类似系统的探索有很多,除了 iframe
外,也有不少相对成熟的替代方案:
1. single-spa
2. qiankun
提起这两个,就要提一下微前端理念,目前社区有很多关于微前端架构的介绍,这里简单提一下:
Techniques, strategies and recipes for building a modern web app with multiple teams that can ship features independently. — Micro Frontends
大致是说,微前端有以下特点:
技术栈无关:基座不限制子应用的技术栈
完全独立:子应用独立部署维护,接入时基座同步更新;又可独立运行
基于此,不难想到:iframe
也是符合微前端理念的。那其他方案又是如何做的呢?
single-spa
社区里 single-spa 介绍也不少。根据 demo 比葫芦画瓢,可以知道它的架构分布:
启动服务的配置主要是在single-spa-config
文件中,包含项目名称、 项目地址、路由配置等:
// single-spa-config.js
import {registerApplication, start } from 'single-spa';// 子应用唯一ID
const 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
三个方法
<div id='micro-react'></div>
<script src=/js/chunk-vendors.js> </script>
<script src=/js/app.js> </script>
不过这种方式需要对现有项目的打包方式和配置项进行改造,成本很大。所以,对于已有的工程项目,我选择了放弃使用。
qiankun
qiankun 也是社区提到比较多的一个开源框架,是基于single-spa
实现了开箱即用。可以采用html entry
方式接入子应用,且子应用只需暴露一些生命周期,改动较少。【少】这个点,真是让我跃跃欲试。
目前我司业务场景是单实例模式(一个运行时只有一个子应用被激活),我们可以根据一张图来看看单实例下以html entry
方式 qiankun 实现流程:
如上图所示,一个子应用的全过程有:
初始化配置,匹配出子应用
初始化子应用,加载对应的 html 资源,以及创建 JS 沙箱环境
挂载子应用,执行生命周期钩子函数
卸载子应用,当切换路由时,执行各卸载钩子函数,以及卸载 JS 沙箱环境,清除容器节点
具体实现细节,大家可以参考qiankun
源码。
实践
基座
从规范化开发角度,我司的中后台系统是基于 umi 开发(详细可参考我们之前的文章 umi 中后台项目实践)。在构建主应用使用了配套的 qiankun 插件:@umijs/plugin-qiankun
。
1. 初始化配置项,注册子应用
插件安装之后,我们可以在入口文件里配置:
此处主要以运行时为例
// app.js
export const qiankun = Promise.resolve().then(() => ({// 运行时注册子应用信息apps: [{// 结算单管理name: 'settlement', // 唯一id,与子应用的library 保持一致entry: '//xxx', // html entryhistory: 'hash', // 子应用的 history 配置,默认为当前主应用 history 配置container: '#root-content', // 子应用存放节点mountElementId: 'root-content' // 子应用存放节点}, {// 公告消息name: 'news', // 唯一id,与子应用的library 保持一致entry: '//xxx', // html entryhistory: 'hash', // 子应用的 history 配置,默认为当前主应用 history 配置container: '#root-content', // 子应用存放节点mountElementId: 'root-content' // 子应用存放节点}],jsSandbox: { strictStyleIsolation: true }, // 是否启用 js 沙箱,默认为 falseprefetch: true, // 是否启用 prefetch 特性,默认为 truelifeCycles: {// see https://github.com/umijs/qiankun#registermicroappsbeforeLoad: (props) => {return Promise.resolve(props).then(() => loading())},afterMount: (props) => {console.log('afterMount', props)},afterUnmount: (props) => {console.log('afterUnmount', props)}}
}))
2. 装载子应用,在路由配置中使用microApp
来获取相应的子应用名称:
// router.config.js
export default [{path: '/',component: '../layouts/BasicLayout',routes: [...{path: '/settlement/list',name: '结算单管理',icon: 'RedEnvelopeOutlined',microApp: 'settlement', // 子应用唯一id},{path: '/settlement/detail/:id',name: '结算单管理',icon: 'RedEnvelopeOutlined',microApp: 'settlement', // 子应用唯一idhideInMenu: 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',PropTypes: '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前缀
、CSS Module
、动态加载/卸载样式表
。
qiankun 中 css沙箱机制
采用的是 动态加载/卸载样式表。
重写
HTMLHeadElement.prototype.appendChild
事件
// Just overwrite it while it have not been overwrite
if (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 overwrite
if (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.js
export default ({ sourceUrl }) =><iframesrc={sourceUrl}title="xxxx"width="100%"height="100%"border="0"frameBorder="0"/>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 恤一件,大家转发起来吧~
iframe 接班人-微前端框架 qiankun 在中后台系统实践相关推荐
- Admin Plus | 基于vue.3的前端框架,适用于中后台系统
关于 Admin Plus 功能支持 功能列表 兼容性 去使用 关于 Admin Plus Admin Plus 是一套企业级的通用型中后台前端解决方案,它基于 View UI Plus,适用于 Vu ...
- 微前端框架qiankun项目实战(一)--本地开发篇
❝ 作者:黑化程序员 https://juejin.cn/post/6970310177517993998 ❞ 大家好,我是小黑. 公司使用技术栈是vue,最近遇到了一个需求,要把原有后台管理系统的功 ...
- 深入浅出解析阿里成熟的微前端框架 qiankun 源码【图文并茂】
来源:leaf(a1029563229 ) https://github.com/a1029563229/blogs/blob/master/Source-Code/qiankun/1.md 本文将针 ...
- 深入浅出解析阿里成熟的微前端框架 qiankun 源码
本文将针对微前端框架 qiankun 的源码进行深入解析,在源码讲解之前,我们先来了解一下什么是 微前端. 微前端 是一种类似于微服务的架构,它将微服务的理念应用于浏览器端,即将单页面前端应用由单一的 ...
- 万字长文+图文并茂+全面解析微前端框架 qiankun 源码 - qiankun 篇
写在开头 微前端系列文章: 基于 qiankun 的微前端最佳实践(万字长文) - 从 0 到 1 篇 基于 qiankun 的微前端最佳实践(图文并茂) - 应用间通信篇 基于 qiankun 的微 ...
- 微前端框架qiankun之原理与实战
目录 一.微前端概述 1. 基本原理 2. 微前端的主要优势 3. 当前微前端方案的一些缺点 二.qiankun与single-spa实现原理 1. single-spa实现原理 (1). 路由问题 ...
- 基于 iframe 的微前端框架 —— 擎天
vivo 互联网前端团队- Jiang Zuohan 一.背景 VAPD是一款专为团队协作办公场景设计的项目管理工具,实践敏捷开发与持续交付,以「项目」为核心,融合需求.任务.缺陷等应用,使用敏捷迭代 ...
- 微前端框架qiankun开发到部署保姆式教程原理与实战
废话不多说先了解下在实践. 1. 什么是微前端 web应用构建方式 微前端 微前端在2016年ThoughtWorks Technology Radar正式被提出.微服务这个被广泛应用于服务端的技术范 ...
- 用微前端框架qiankun配置项目的实战
参考文档: qiankun官方文档 csdn文档1 csdn文档2 github地址 umi-qiankun 的教程请移步 umi 官网 和 umi-qiankun 的官方 demo ...
最新文章
- JQuery模板插件jquery.tmpl-动态ajax扩展
- 053_Result结果
- Oralce数据库数据迁移到另一个数据
- 模板使用自定义类型_「Shopify模板」Shopify模板编辑Shopify模板代码更改教程
- 当视频恋爱 App 用上了 Serverless
- 表格缓存问题_缓存常见问题,一网打尽哦!
- 关于jstl.jar引用问题及解决方法
- 了解React Native中的不同JavaScript环境
- 莒南机器人_莒南42项重点建设项目公布!一定有你关注的
- 使用crontab不能正常执行的问题
- iframe调用父页面方法_5.1 vue中子组件调用父组件的方法,务必理解自定义事件的重要性...
- javascript中数组遍历问题
- Linux环境变量配置【转】
- scikit keras_使用Scikit-Learn,Scikit-Opt和Keras进行超参数优化
- 【更新】怎样免费下载百度文库文档
- 《广义动量定理与系统思考----战争…
- 大数据分析师高级证书_数据分析师资格证书好考吗_大数据分析师认证
- Linux迁移home目录到根目录的操作步骤
- Spring注解驱动开发学习总结8:自动装配注解@Autowire、@Resource、@Inject
- jsp标签与指令总结