为iframe正名,你可能并不需要微前端
作者:刘显安(码怪)
任何新技术、新产品都是有一定适用场景的,它可能在当下很流行,但它不一定在任何时候都是最优解。前言
最近几年微前端很火,火到有时候项目里面用到了iframe还要偷偷摸摸地藏起来生怕被别人知道了,因为担心被人质疑:你为什么不用微前端方案?直到最近笔者接手一个项目,需要将现有的一个系统整体嵌入到另外一个系统(一共20多个页面),在被微前端坑了几次之后,回过头发现,iframe真香!
qiankun的作者有一篇《WhyNotIframe》〔1〕介绍了iframe的优缺点(不过作者还有一篇《你可能并不需要微前端》〔2〕给微前端降降火),诚然iframe确实存在很多缺点,但是在选择一个方案的时候还是要具体场景具体分析,它可能在当下很流行,但它不一定在任何时候都是最优解:iframe的这些缺点对我来说是否能够接受?它的缺点是否有其它方法可以弥补?使用它到底是利大于弊还是弊大于利?我们需要在优缺点之间找到一个平衡。优缺点分析
iframe适合的场景
由于iframe的一些限制,部分场景并不适合用iframe,比如像下面这种iframe只占据页面中间部分区域,由于父页面已经有一个滚动条了,为了避免出现双滚动条,只能动态计算iframe的内容高度赋值给iframe,使得iframe高度完全撑满,但这样带来的问题是弹窗很难处理,如果居中的话一般弹窗都相对的是iframe内容高度而不是屏幕高度,从而导致弹窗可能看不见,如果固定弹窗top又会导致弹窗跟随页面滚动,而且稍有不慎iframe内容高度计算有一点点偏差就会出现双滚动条。
所以:如果页面本身比较简单,是一个没有弹窗、浮层、高度也是固定的纯信息展示页的话,用iframe一般没什么问题;如果页面是包含弹窗、信息提示、或者高度不是固定的话,需要看iframe是否占据了全部的内容区域,如果是像下图这种经典的导航菜单内容结构、并且整个内容区域都是iframe,那么可以放心大胆地尝试iframe,否则,需要慎重考虑方案选型。
为什么一定要满足iframe占据全部内容区域这个条件呢?可以想象一下下面这种场景,滚动条出现在页面中间应该大部分人都无法接受:
实战:A系统接入B系统
满足iframe占据全部内容区域条件的场景,iframe的几个缺点都比较好解决。下面通过一个实际案例来详细介绍将一个线上在运行的系统接入到另外一个系统的全过程。以笔者前段时间刚完成的ACP(全称Alibaba。comPay,阿里巴巴国际站旗下一站式全球收款平台,下称A系统)接入生意贷(下称B系统)为例,已知:ACP和生意贷都是MPA页面;ACP系统在此之前没有接入其他系统的先例,生意贷是第一个;生意贷作为被接入系统,本次需要接入的一共有20多个页面,且服务端包含大量业务逻辑以及跳转控制,有些页面想看看长什么样子都非常困难,需要在Node层mock大量接口;接入时需要做功能删减,部分接口入参需要调整;生意贷除了接入到ACP系统中,之前还接入过AMES系统,本次接入需要兼容这部分历史逻辑;
我们希望的效果:
假设我们新增一个页面finbase。html?entryxxx作为我们A系统承接B系统的地址,A系统有类似如下代码:classAppextendsReact。Component{state{currentEntry:decodeURIComponent(iutil。getParam(entry)),};render(){returniframeidmicroFrontIframesrcc2021imgdataimg。jpgdatasrc{this。state。currentEntry};}}隐藏原系统导航菜单
因为是接入到另外一个系统,所以需要将原系统的菜单和导航等都通过一个类似hideLayout的参数去隐藏。前进后退处理
需要特别注意的是,iframe页面内部的跳转虽然不会让浏览器地址栏发生变化,但是却会产生一个看不见的history记录,也就是点击前进或后退按钮(history。forward()或history。back())可以让iframe页面也前进后退,但是地址栏无任何变化。
所以准确来说前进后退无需我们做任何处理,我们要做的就是让浏览器地址栏同步更新即可。
如果要禁用浏览器的上述默认行为,一般只能在iframe跳转时通知父页面更新整个DOM节点。URL的同步更新
让URL同步更新需要处理2个问题,一个是什么时候去触发更新的动作,一个是URL更新的规律,即父页面的URL地址(A系统)与iframe的URL地址(B系统)映射关系的维护。
保证URL同步更新功能正常需要满足这3种情况:case1:页面刷新,iframe能够加载正确页面;case2:页面跳转,浏览器地址栏能够正确更新;case3:点击浏览器的前进或后退,地址栏和iframe都能够同步变化;什么时候更新URL地址
首先想到的肯定是在iframe加载完发送一个通知给父页面,父页面通过history。replaceState去更新URL。
为什么不是history。pushState呢?因为前面提到过,浏览器默认会产生一条历史记录,我们只需要更新地址即可,如果用pushState会产生2条记录。
B系统:
A系统:window。addEventListener(message,e{const{data,type}e。data{};if(typeafterHistoryChangedata?。url){这里先采用一个兜底的URL承接任意地址constentryfinbase。html?entry{encodeURIComponent(data。url)};地址不一样才需要更新if(location。pathnamelocation。search!entry){window。history。replaceState(null,,entry);}}});优化URL的更新速度
按照上面的方法实现后可以发现,URL虽然可以更新但是速度有点慢,点击跳转后一般需要等待7800毫秒地址栏才会更新,有点美中不足。可以把地址栏的更新在跳转后基础之上再加一个跳转前。为此我们必须有一个全局的beforeRedirect钩子,先不考虑它的具体实现:
B系统:functionbeforeRedirect(href){postMessage(beforeHistoryChange,{url:href});}
A系统:window。addEventListener(message,e{const{data,type}e。data{};if((typebeforeHistoryChangetypeafterHistoryChange)data?。url){这里先采用一个兜底的URL承接任意地址constentryfinbase。html?entry{encodeURIComponent(data。url)};地址不一样才需要更新if(location。pathnamelocation。search!entry){window。history。replaceState(null,,entry);}}});
加上上述代码之后,点击iframe中的跳转链接,URL会实时更新,浏览器的前进后退功能也正常。
为什么需要同时保留跳转前和跳转后呢?因为如果只保留跳转前,只能满足前面的case1和case2,case3无法满足,也就是点击后退按钮只有iframe会后退,URL地址不会更新。美化URL地址
简单的使用finbase。html?entryxxx这样的通用地址虽然能用,但是不太美观,而且很容易被人看出来是iframe实现的,比较没有诚意,所以如果被接入系统的页面数量在可枚举范围内,建议给每个地址维护一个新的短地址。
首先,新增一个SPA页面fin。html,和前面的finbase。html指向同一个页面,然后维护一个URL地址的映射,类似这样:A系统地址到B系统地址映射constentryMap{finhome。html:https:fs。alibaba。comxxxhome。htm?hideLayout1,finapply。html:https:fs。alibaba。comxxxapply?hideLayout1,finfailed。html:https:fs。aibaba。comxxxfailed?hideLayout1,省略};constiframeMap{};同时再维护一个子页面父页面URL映射for(constentryinentryMap){iframeMap〔entryMap〔entry〕。split(?)〔0〕〕entry;}classAppextendsReact。Component{state{currentEntry:decodeURIComponent(iutil。getParam(entry))entryMap〔location。pathname〕,};render(){returniframeidmicroFrontIframesrcc2021imgdataimg。jpgdatasrc{this。state。currentEntry};}}
同时完善一下更新URL地址部分:base。html继续用作兜底letentryfinbase。html?entry{encodeURIComponent(data。url)};const〔path,search〕data。url。split(?);if(iframeMap〔path〕){entry{iframeMap〔path〕}?{search};}地址不一样才需要更新if(location。pathnamelocation。search!entry){window。history。replaceState(null,,entry);}
省略参数透传部分代码。全局跳转拦截
为什么一定要做全局跳转拦截呢?一个因为我们需要把hideLayout参数一直透传下去,否则就会点着点着突然出现下面这种双菜单的情况:
另一个是有些页面在被嵌入前是当前页面打开的,但是被嵌入后不能继续在当前iframe打开,比如支付宝付款这种第三方页面,想象一下下面这种情况会不会觉得很怪?所以这类页面一定要做特殊处理让它跳出去而不是当前页面打开。
URL跳转可以分为服务端跳转和浏览器跳转,浏览器跳转又包括A标签跳转、location。href跳转、window。open跳转、historyAPI跳转等;
而根据是否新标签打开又可以分为以下4种场景:继续当前iframe打开,需要隐藏原系统的所有layout;当前父页面打开第三方页面,不需要任何layout;新开标签打开第三方页面(如支付宝页面),不需要做特殊处理;新开标签打开宿主页面,需要把原系统layout替换成新layout;
为此,先定义好一个beforeRedirect方法,由于新标签打开有targetblank和window。open等方式,父页面打开有targetparent和window。parent。location。href等方式,为了更好的统一封装,我们把特殊情况的跳转统一在beforeRedirect处理好,并约定只有有返回值的情况才需要后续继续处理跳转:维护一个需要做特殊处理的第三方页面列表constthirdPageList〔https:service。alibaba。com,https:sale。alibaba。comxxx,https:alipay。comxxx,。。。〕;封装统一的跳转拦截钩子,处理参数透传和一些特殊情况param{}href要跳转的地址,允许传入相对路径param{}isNewTab是否要新标签打开param{}isParentOpen是否要在父页面打开returns返回处理好的跳转地址,如果没有返回值则表示不需要继续处理跳转functionbeforeRedirect(href,isNewTab){if(!href){return;}传过来的href可能是相对路径,为了做统一判断需要转成绝对路径if(href。indexOf(http)!0){varadocument。createElement(a);a。hrefhref;hrefa。href;}如果命中白名单if(thirdPageList。some(itemhref。indexOf(item)0)){if(isNewTab){rawOpen参见后面window。open拦截window。rawOpen(href);}else{第三方页面如果不是新标签打开就一定是父页面打开window。parent。location。hrefhref;}return;}需要从当前URL继续往下透传的参数varparams〔hideLayout,tracelog〕;for(vari0;iparams。length;i){varvaluegetParam(params〔i〕,location。href);if(value){hrefsetParam(params〔i〕,value,href);}}if(isNewTab){letentryfinbase。html?entry{encodeURIComponent(href)};const〔path,search〕href。split(?);if(iframeMap〔path〕){entry{iframeMap〔path〕}?{search};}hrefhttps:payment。alibaba。com{entry};window。rawOpen(href);return;}如果是以iframe方式嵌入,向父页面发送通知postMessage(beforeHistoryChange,{url:href});returnhref;}服务端跳转拦截
服务端主要是对301或302重定向跳转进行拦截,以Egg为例,只要重写ctx。redirect方法即可。A标签跳转拦截document。addEventListener(click,function(e){vartargete。target{};A标签可能包含子元素,点击目标可能不是A标签本身,这里只简单判断2层if(target。tagNameA(target。parentNodetarget。parentNode。tagNameA)){targettarget。tagNameA?target:target。parentNode;varhreftarget。href;不处理没有配置href或者指向JS代码的A标签if(!hrefhref。indexOf(javascript)0){return;}varnewHrefbeforeRedirect(href,target。targetblank);没有返回值一般是已经处理了跳转,需要禁用当前A标签的跳转if(!newHref){target。targetself;target。hrefjavascript:;;}elseif(newHref!href){target。hrefnewHref;}}},true);location。href拦截
location。href拦截至今是一个困扰前端界的难题,这里只能采用一个折中的方法:由于location。href无法重写,只能实现一个location2。hrefif(Object。defineProperty){window。location2{};Object。defineProperty(window。location2,href,{get:function(){returnlocation。href;},set:function(href){varnewHrefbeforeRedirect(href);if(newHref){location。hrefnewHref;}},});}
因为我们不仅实现了location。href的写,location。href的读也一起实现了,所以可以放心大胆的进行全局替换。找到对应前端工程,首先全局搜索window。location。href,批量替换成(window。location2window。location)。href,然后再全局搜索location。href,批量替换成(window。location2window。location)。href(思考一下为什么一定是这个顺序呢)。
另外需要注意,有些跳转可能是写在npm包里面的,这种情况只能npm也跟着替换一下了,并没有其它更好办法。window。open拦截vartempOpenNamerawOpen;if(!window〔tempOpenName〕){window〔tempOpenName〕window。open;window。openfunction(url,name,features){urlbeforeRedirect(url,true);if(url){window〔tempOpenName〕(url,name,features);}}}history。pushState拦截vartempNamerawPushState;if(!window。history〔tempName〕){window。history〔tempName〕window。history。pushState;window。history。pushStatefunction(state,title,url){urlbeforeRedirect(url);if(url){window。history〔tempName〕(state,title,url);}}}history。replaceState拦截vartempNamerawReplaceState;if(!window。history〔tempName〕){window。history〔tempName〕window。history。replaceState;window。history。replaceStatefunction(state,title,url){urlbeforeRedirect(url);if(url){window。history〔tempName〕(state,title,url);}}}全局loading处理
完成上述步骤后,基本上已经看不出来是iframe了,但是跳转的时候中间有短暂的白屏会有一点顿挫感,体验不算很流畅,这时候可以给iframe加一个全局的loading,开始跳转前显示,页面加载完再隐藏:
B系统:document。addEventListener(DOMContentLoaded,function(e){postMessage(iframeDOMContentLoaded,{url:location。href});});
A系统:window。addEventListener(message,(e){const{data,type}e。data{};iframe加载完毕if(typeiframeDOMContentLoaded){this。setState({loading:false});}if(typebeforeHistoryChange){此时页面并没有立即跳转,需要再稍微等待一下再显示loadingsetTimeout(()this。setState({loading:true}),100);}});
除此之外还需要利用iframe自带的onload加一个兜底,防止iframe页面没有上报iframeDOMContentLoaded事件导致loading不消失:iframe自带的onload做兜底iframeOnLoad(){this。setState({loading:false});}render(){returnLoadingvisible{this。state。loading}tip正在加载。。。inline{false}iframeidmicroFrontIframesrcc2021imgdataimg。jpgdatasrc{this。state。currentEntry}onLoad{this。iframeOnLoad}Loading;}
还需要注意,当新标签页打开页面时并不需要显示loading,需要注意区分。弹窗居中问题
当前场景下弹窗个人觉得并不需要处理,因为菜单的宽度有限,不仔细看的话甚至都没注意到弹窗没有居中:
如果非要处理的话也不麻烦,覆盖一下原来页面弹窗的样式,当包含hideLayout参数时,让弹窗的位置分别向左移动menuWidth2、向上移动navbarHeight2即可(遮罩位置不能动、也动不了)。
添加了marginLeft120px、marginTop30px后的弹窗效果:
最终效果
其实不难看出,最终效果和SPA几乎无异,而且菜单和导航本来就是无刷新的,页面跳转没有割裂感:
结语
上述方案有几个没有提到的点:方案成立的前提是建立在2个系统共用一套用户体系,否则需要对2个系统的登录体系进行打通,一般包括账号绑定、A系统默认免登B系统,等等,这需要一定额外的工作量;参数的透传与删除,例如我希望除了hideLayout参数之外其它URL参数全部在父子页面之间透传;埋点,数据上报的时候需要增加一个额外参数来标识流量来自另外一个系统;
在第一次摸索方案时可能需要花费一些时间,但是在熟悉之后,如果后续还有类似把B系统接入A系统的需求,在没有特殊情况且顺利的前提下可能花费12天时间即可完成,最重要的是大部分工作都是全局生效的,不会随着页面的增多而导致工作量增加,测试回归的成本也非常低,只需要验证所有页面跳转、展示等是否正常,功能本身一般不会有太大问题,而如果是微前端方案的话需要从头到尾全部仔仔细细测试一遍,开发和测试的成本都不可估量。参考资料
〔1〕
《WhyNotIframe》:https:www。yuque。comkuitosgky7ywgesexv?spmata。21736010。0。0。25c06df01VID5V
〔2〕
《你可能并不需要微前端》:https:zhuanlan。zhihu。comp391248835
韩安冉这个瓜吃起来真是,新鲜又熟悉照例,韩安冉离婚一定会和前夫在网上撕一场,这次也不例外。韩安冉率先发出小作文,把自己塑造成苦命女人。1。说泽西吃软饭恋爱9天闪婚,对方身上只有600块钱,去她家空手去给猪小妹买东西
2022年金足奖30人候选名单公布梅西领衔,武磊入选金足奖官方公布2022年30人候选名单,梅西领衔,武磊入选。30人候选名单本泽马博努奇基耶利尼库尔图瓦夸德拉多德布劳内迪巴拉法布雷加斯吉鲁伊卡尔迪因莫比莱若日尼奥香川真司凯恩坎特武
中计了?俄军丢弃完整攻防阵地匆忙跑路,就是为了全歼乌军主力随着俄乌冲突愈演愈烈,尤其是乌克兰夺回哈尔科夫之后,导致双方的对抗也出现了新的变数。可是在外界看来,俄军面对来势汹汹的乌克兰军队,直接选择不战而退,属实有点不符合战斗民族一贯的行事
68岁老太,子宫从下面掉了出来?如何预防和应对子宫脱垂?68岁的刘奶奶,没有女儿,只有两个儿子。老头子年轻时因意外去世,自己含辛茹苦操劳了一辈子,如今两个儿子不说大富大贵,也都是事业有成家庭和睦。现在终于将孩子带大了,本该到了颐养天年的
恐慌之时,也是机会到来之时上周反弹之后,一头栽了下来,且其势未尽。追其根源,美联储暴力加息,中国则强力减息,人民币汇率贬值欲破7,引起市场预期博弈。然而美国加息之大之快,必然停的也快国内减息是对抗美国收割财
郭德纲患糖尿病15年,郭麒麟痛风坐轮椅,每天把药当饭吃赵本山曾在小品不差钱中这样教育小沈阳人生最大的悲剧,是人活着呢钱没了,更大的悲剧呢是人没了钱没花掉。这句话堪称经典的人生格言,特别是对大富大贵的娱乐圈明星而言更是如此。即便一生坐拥
不可一世的曹云金,为何走到了今天这一步?文大牌娱姐2017年,在一档综艺节目中,曹云金和常远站在了台上。论辈分,曹云金应该尊称常远为师叔,因为他是郭德纲的徒弟,而常远和郭德纲是同辈。可是当主持人让他叫声师叔时,曹云金竟然
章子怡这个国际章的由来大海冲浪沙滩无论普通人,还是明星名人,这些地方都是大家喜欢的地方。而且明星们有时候也是会忙里偷闲,享受着假日的美好时光。小锵说到这,很自然的就想起了当时最顶流明星章子怡的沙滩照,当
羊了个羊走红山寨周边跟风卖,行业人士游戏热度总归会回落上头游戏羊了个羊热度仍在持续。微信指数显示,自9月13日起,羊了个羊微信指数飙升,日环比搜索量增长6022。98。短短几天,相关微博热搜已有11个。记者注意到,甚至在一些电商平台和
9月17日测试服更新剖析贾克斯呲呲小加强点击关注更多云顶金铲铲资讯尽在掌握大家好,这里是小鱼和NGA狼人X黑狼联合为大家带来的云顶之弈S7。59月17日PBE测试服更新剖析预计实装时间9月22日周四金铲铲实装时间9月22
羊了个羊是不是骗局羊了个羊一共就两关只要勾起你的好胜心他就赢了这游戏通过强制你转发的办法来获得病毒式裂变传播,通过强制你看广告的办法来获得利润。总之羊了个羊就是让你不断上瘾的概率游戏。的时候就需要看