本文由体验技术团队 TinyVue 我的项目成员岑灌铭同学创作。

前言

微前端是一种多个团队通过独立公布性能的形式来独特构建现代化 web 利用的技术手段及办法策略,每个利用能够抉择不同的技术栈,独立开发、独立部署。

TinyVue组件库的跨技术栈能力与微前端非常符合,往期咱们也有文章,领导如何在wujie微前端中应用TinyVue组件库,文章链接:https://mp.weixin.qq.com/s/ZqDXemh0GfnQpWACdzXdig

目前许多对微前端有需要的用户曾经在应用wujieTinyVue开发了,在应用了一段时间后,单干企业用户和个人用户反馈了组件库一些问题。通过一番交换、沟通与定位,最终发现是用户接入了微前端框架后,在特定场景下导致的一系列问题,在非微前端利用中,组件库运行良好。

复现问题后,通过一系列排查与剖析,最终总结出了四个问题:

  1. absolute 定位的弹出元素错位,且页面滚动不会从新定位
  2. fixed 定位的弹出元素错位
  3. 弹出元素地位产生翻转
  4. 表格中的 select 点击后,下拉选项呈现后马上隐没

对于以上问题,TinyVue组件库做了相应的适配以及给用户提供了解决方案,最终使得 TinyVue 组件良好运行在wujie微前端中。

首先来简略介绍一下wujie微前端实现原理,wujie微前端是采纳iframe+webcomponet的实现形式。通过iframes实现js沙箱能力。子利用的实例instanceiframe内运行,dom在主利用容器下的webcomponent内,通过代理 iframedocumentwebcomponent,能够实现两者的互联。

想要理解更多能够查看,无界微前端介绍:https://wujie-micro.github.io/doc/guide/

接下来开展说一下,收集总结的四个问题~

问题总结

问题一:absolute 定位的弹出元素错位,且页面滚动不会从新定位。

“弹出元素错位”谬误起因剖析:

关上控制台,审查元素查看款式,看到element.sytle的第一直觉是transfrom的偏移量计算不正确,顺着这个线索排查计算错误的起因。

排查前先简略介绍一下TinyVue组件库这个偏移量的计算规定:

1.找到弹出元素的 offsetParent(父定位元素),如果没有则返回body
2.应用 getBoundingClientRect 计算 offsetParent 以及援用元素(图中的输入框,简称为reference)间隔视口的地位信息。
3.以弹出元素放左边为例,transform的左偏移量的计算规定为reference.left - offsetParent.left + reference.width

因为弹出元素的position设置为absolute,所以弹出元素的定位是依据其offsetParent计算地位的,没有offsetParent则是依据视口来计算地位。

上述例子中,弹出元素的offsetParent为 null,因而默认返回了body作为其offsetParent,绝大部分状况下,body和视口左侧和上侧是对齐的,因而用body计算的偏移量,在视口上也实用。

在微前端中,子利用的body可能绝对于视口有偏移。弹出元素的偏移量理论是依据body计算的,但他是非定位元素,最终导致的元素错位。

解决方案:

既然计算规定是依据body计算的,那么将子利用将body设置为position: relative将其变为定位元素即可。

滚动不会从新定位起因剖析:

首先还是简略介绍组件库这部分逻辑:

1.通过parentNode向上查找援用元素(输入框)的可滚动的先人元素(如果没有配置冒泡则返回第一个可滚动先人元素,否则返回所有可滚动先人元素)
2.为步骤1获取到的元素加上滚动办法的监听。
3.先人元素滚动时从新计算弹出元素的地位,使弹出元素追随援用元素。

然而在wujie微前端中,子利用的document再往上查找就是null了。而滚动条在主利用当中。因而主利用的滚动无奈被监听到。

解决方案:

将子利用将body设置为position: relative同样也解决了上述问题。设置后,只有当子利用内滚动条滚动后才须要从新计算。

问题二: fixed 定位的弹出元素错位。

在修复的问题一的状况下,仍旧有局部状况会呈现弹出元素错位的 bug。并且下图中能够看到,弹出元素从左边翻转到了右边。

起因剖析:

表单元素在modal中,modalfixed定位,因而表单输入框也是fixed定位。因为援用元素是fixed定位,所以弹出元素与之绝对应也应该应用fixed定位。

组件库逻辑对于fixed定位的弹出元素偏移量的计算,在问题一提到的步骤下还减少了局部非凡解决。上面代码是计算偏移量逻辑其中较为要害的一段代码:

/** * @description 计算弹出元素的偏移量 * @param el 援用元素 * @param parent 弹出元素的先人定位元素 * @param fixed 弹出元素是否相对定位 * @returns 用于计算偏移量的相干信息 */const getOffsetRectRelativeToCustomParent = (  el: HTMLElement,  parent: HTMLElement,  fixed: boolean) => {    let {     top,    left,    width,    height   } = getBoundingClientRect(el)  let parentRect = getBoundingClientRect(parent)  if (fixed) {    let {       scrollTop,      scrollLeft     } = getScrollParent(parent)    parentRect.top += scrollTop    parentRect.bottom += scrollTop    parentRect.left += scrollLeft    parentRect.right += scrollLeft  }  let rect = {    top: top - parentRect.top,    left: left - parentRect.left,    bottom: top - parentRect.top + height,    right: left - parentRect.left + width,    width,    height  }  return rect}

已上述代码为例,上述逻辑Modal弹窗状况下,parentscrollParent都是body

21-30行代码的目标是,为了解决在body在滚动后,parentRect.top为正数,须要加上scrollTop才是绝对视口的偏移量。

然而下面的计算逻辑有个大前提,那就是body的左侧和上侧和视口统一,下面这段不太谨严的逻辑通过漫长的迭代,直到在微前端中'暴雷'。

解决方案:

position设置为fixed后,弹出元素在绝大多数状况都是绝对视口定位了,然而也有非凡状况,以下是 mdn 文档的截图:

为了兼容上述的非凡状况,新增了getAdjustOffset办法,此办法计算绝对于视口的修改偏移量,设置 top 和 left 为0,应用getBoundingClientRect计算出来的后果不为0的话,多进去的偏移量就是因为上述的 css 款式影响了,

获取这个修改偏移量后,后续的计算只须要加上这个偏移量,弹出元素和reference元素的地位就可能正确对应上了。

以下是批改后的相干外围代码:

/** 设置transform等款式后,fixed定位不再绝对于视口, * 应用1乘1px通明元素获取fixed定位绝对于视口的修改偏移量。 **/const getAdjustOffset = (parent: HTMLElement) => {  const placeholder = document.createElement('div')  setStyle(placeholder, {    opacity: 0,    position: 'fixed',    width: 1,    height: 1,    top: 0,    left: 0,    'z-index': '-99'  })  parent.appendChild(placeholder)  // 失常应返回 { transform: translateY( 0, left: 0 }  // 否则就是被非凡的css款式影响了  const result = getBoundingClientRect(placeholder)  parent.rem)oveChild(placeholder)  return result}/** * @description 计算弹出元素的偏移量 * @param el 援用元素 * @param parent 弹出元素的先人定位元素 * @param fixed 弹出元素是否相对定位 * @returns 用于计算偏移量的相干信息 */const getOffsetRectRelativeToCustomParent = (  el: HTMLElement,  parent: HTMLElement,  fixed: boolean,  popper: HTMLElement) => {  let {     top,    left,    width,    height  } = getBoundingClientRect(el)  // 如果是fixed定位,需计算要修改的偏移量。  if (fixed) {    if (popper.parentElement) {      const {         top: adjustTop,        left: adjustLeft      } = getAdjustOffset(popper.parentElement)      top -= adjustTop      left -= adjustLeft    }    return {      top,      left,      bottom: top + height,      right: left + width,      width,      height    }  }  let parentRect = getBoundingClientRect(parent)  let rect = {    top: top - parentRect.top,    left: left - parentRect.left,    bottom: top - parentRect.top + height,    right: left - parentRect.left + width,    width,    height  }  return rect}

问题三:弹出元素地位产生翻转

在问题二的截图中除了弹出元素错位问题,还有另外一个问题:弹出元素产生了翻转。

起因剖析:

弹出类的元素,存在一个边界检测逻辑,当计算出弹出元素超出边界后,为了展现的完整性和好看,会主动将元素翻转。

在用户没有特定配置的状况下,默认的边界为'视口',上面是对于边界计算逻辑的节选:

/** 计算边界逻辑 */const getBoundaries = (  data: UpdateData,  padding: number,  boundariesElement: string | HTMLElement) => {    // ... other code    else if (boundariesElement === 'viewport') {  let offsetParent = getOffsetParent(this._popper)  let scrollParent = getScrollParent(this._popper)  let offsetParentRect = getOffsetRect(offsetParent)  let isFixed = data.offsets.popper.position === 'fixed'  let scrollTop = isFixed ? 0 : getScrollTopValue(scrollParent)  let scrollLeft = isFixed ? 0 : getScrollLeftValue(scrollParent)  const docElement = window.document.documentElement  boundaries = {    top: 0 - (offsetParentRect.top - scrollTop),    right: docElement.clientWidth - (offsetParentRect.left - scrollLeft),    bottom: docElement.clientHeight - (offsetParentRect.top - scrollTop),    left: 0 - (offsetParentRect.left - scrollLeft)  }}    // ... other code}

能够看到,视口的边界计算逻辑和window.document.documentElement也就是html无关。组件库运行在子利用中,因而这里也就是子利用的html。但在子利用中,html的宽高可能会比实在视口小得多,导致边界计算被束缚在子利用范畴当中,触发了翻转逻辑,导致了谬误的翻转。

解决方案: 组件库对外裸露一个全局配置,用户在子利用中能够引入全局配置,将主利用的 window赋值给全局配置的 viewportWindow 用于边界判断。

import globalConfig from '@opentiny/vue-renderless/common/global'// 须要判断是否在子利用当中if (window.__POWERED_BY_WUJIE__) {  // 子利用中能够通过window.parent获取主利用的window  globalConfig.viewportWindow = window.parent}

getBoundaries 办法也绝对应做一下批改

/** 计算边界逻辑 */const getBoundaries = (  data: UpdateData,  padding: number,  boundariesElement: string | HTMLElement) => {  // ... other code  // 新增代码  const viewportWindow = globalConfig.viewportWindow || window  const docElement = viewportWindow.document.documentElement  boundaries = {    top: 0 - (offsetParentRect.top - scrollTop),    right: docElement.clientWidth - (offsetParentRect.left - scrollLeft),    bottom: docElement.clientHeight - (offsetParentRect.top - scrollTop),    left: 0 - (offsetParentRect.left - scrollLeft)  }  // ... other code}

问题四:表格中的select点击后,下拉选项呈现后马上隐没

起因剖析:

当开启表格编辑状态时,表格默认处于显示状态,当点击表格某一行时,会进入到编辑状态。当点击表格此行外的其余区域,表格就会革除编辑状态,进入显示状态。

是否点击内部是通过监听document的点击事件,当点击任意元素后,都会被冒泡捕捉,组件库应用点击事件的event.target来判断用户是否点击了表格编辑行以外的元素。

失常状况下,点击select,event.target可能找select对应的元素,能够失常的判断select元素是在对应的容器中,则不会切换至显示状态。

wujie微前端下,点击selectevent.target找到的是wujie-app。这个问题是浏览器原生的解决,详情能够参考:https://javascript.info/shadow-dom-events  此时wujie-app不在对应的容器内,认为点击了对应行以外的区域,因而切换至显示状态,下拉选项隐没。

解决方案:
组件库退出兼容逻辑,获取 event.target 的形式批改成: (e.target.shadowRoot && e.composed) ? (e.composedPath()[0] || e.target) : e.target
退出兼容逻辑后,无论组件是否运行在微前端中,点击事件都能找到实在点击的dom元素,因而问题也就解决了。

结语

总体而言,上述遇到的问题次要起因有两个,其一是 wujie 微前端中,子利用的window和视口window不是同一个。其二是webcomponent外部元素事件冒泡被内部元素捕捉时,event.target会被代理到webcomponent跟元素上导致的指标判断谬误。

针对问题一,整体的解决思路是要么将作用范畴限定在子利用当中,例如问题一解决方案,给子利用body加上款式position: relative。要么是通过相似依赖注入的形式,让相干逻辑能够正确地获取到主利用的window
针对问题二,思路就十分明确了,指标就是要找到正确的event.target,通过加上兼容代码后,无论是否在webcompoent中,都能正确返回event.target

当然以上提到的问题,曾经在@opentiny/vue3.13.0新版本公布修复了,欢送下载应用~

对于 OpenTiny

OpenTiny 是一套企业级 Web 前端开发解决方案,提供跨端、跨框架、跨版本的 TinyVue 组件库,蕴含基于 Angular+TypeScript 的 TinyNG 组件库,领有灵便扩大的低代码引擎 TinyEngine,具备主题配置零碎TinyTheme / 中后盾模板 TinyPro/ TinyCLI 命令行等丰盛的效率晋升工具,可帮忙开发者高效开发 Web 利用。


欢送退出 OpenTiny 开源社区。增加微信小助手:opentiny-official 一起参加交换前端技术~更多视频内容也可关注B站、抖音、小红书、视频号
OpenTiny 也在继续招募贡献者,欢送一起共建

OpenTiny 官网:https://opentiny.design/
OpenTiny 代码仓库:https://github.com/opentiny/
TinyVue 源码:https://github.com/opentiny/tiny-vue
TinyEngine 源码: https://github.com/opentiny/tiny-engine

欢送进入代码仓库 StarTinyEngine、TinyVue、TinyNG、TinyCLI~
如果你也想要共建,能够进入代码仓库,找到 good first issue标签,一起参加开源奉献~