OpenTiny Vue 组件库适配微前端可能遇到的4个问题

news2024/11/18 14:48:40

本文由体验技术团队 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

欢迎进入代码仓库 Star🌟TinyEngine、TinyVue、TinyNG、TinyCLI~
如果你也想要共建,可以进入代码仓库,找到 good first issue标签,一起参与开源贡献~

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1462271.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

十三、图像像素值统计

项目功能实现&#xff1a;对一张图像进行统计最大、最小像素值、均差以及方差的值 按照之前的博文结构来&#xff0c;这里就不在赘述了 一、头文件 pixel_statistic.h #pragma once#include<opencv2/opencv.hpp>using namespace cv;class Pixel_Statistic { public:vo…

camunda源代码编译运行(三):验证camunda API接口功能

接上一篇文章&#xff1a;camunda源代码编译运行&#xff08;二&#xff09;&#xff1a;构建并运行camunda源代码工程 4.1、发布流程模型 先通过camunda的流程设计器设计一个流程&#xff0c;命名为&#xff1a;UserTask Flow1&#xff0c;然后发布流程&#xff0c;发布流程…

HUD阳光倒灌实验TFT温升实验太阳光模拟器

随着汽车行业的不断发展&#xff0c;车辆的智能化程度越来越高&#xff0c;而HUD抗干扰太阳光模拟器作为一种新型的解决驾驶员视线问题的方案&#xff0c;引起了人们的广泛关注。它能够有效地解决驾驶员在阳光强烈或者夜间光线不足时的视线问题&#xff0c;提高了驾驶的安全性和…

Java写爱心

突然想用Java写一个爱心&#xff0c;程序运行效果如下&#xff1a; 话不多说&#xff0c;上代码 ! public class LovingHeart {/*** 判断坐标是否在爱心范围内*/private static boolean inHeart(float x,float y){float ax*xy*y-1;return a*a*a-x*x*y*y*y<0.0f;}/*** 爱心…

【Vuforia+Unity】AR05-实物3D模型识别功能实现

对于3D物体的识别&#xff0c;可以是虚拟的也可以是实物的&#xff0c;但是对于虚拟的三维模型意义不大&#xff0c;我们完全可以把三维模型放在屏幕上截一张图&#xff0c;以图片识别的方式召唤数字内容&#xff0c;不过在虚拟现实中或许有用。 因此本文探讨的技术路线主要是…

Gitee教程2(完整流程)

1.配置git git config --global user.name "用户名" git config --global user.email "密码" 如何获取&#xff1f; gitee右上角加号点击新建仓库&#xff0c;仓库名随便起一个就行 找到这条命令&#xff0c;把这两句一个一个复制到vscode终端就行 2.创建g…

【2.3深度学习开发任务实例】(1)神经网络模型的特点【大厂AI课学习笔记】

从本章开始&#xff0c;我把标题的顺序变了一下&#xff0c;大厂AI课笔记&#xff0c;放到后面。因为我发现App上&#xff0c;标题无法显示完全。 从本章开始&#xff0c;要学习深度学习开发任务的全部过程了。 我们将通过小汽车识别赛道上的标志牌&#xff0c;给出检测框&am…

2.21 Qt day2 菜单栏/工具栏/状态栏/浮动窗口、UI界面、信号与槽

思维导图 使用手动连接&#xff0c;将登录框中的取消按钮使用qt4版本的连接到自定义的槽函数中&#xff0c;在自定义的槽函数中调用关闭函数 将登录按钮使用qt5版本的连接到自定义的槽函数中&#xff0c;在槽函数中判断ui界面上输入的账号是否为"admin"&#xff0c;…

11、内网安全-横向移动NTLM-Relay重放Responder中继攻击LdapEws

用途&#xff1a;个人学习笔记&#xff0c;有所借鉴&#xff0c;欢迎指正&#xff01; 目录 前提知识&#xff1a; 一、横向移动-NTLM 中继攻击-Relay 重放-SMB 上线 1、CS权限转给MSF: 2、MSF: 3、添加路由&#xff1a; 4、smb_relay重发模块&#xff1a; 5、受控主机输…

良好的 API 安全策略的重要性

根据 Cloudflare 2024 年 API 安全与管理报告&#xff0c;到 2024 年&#xff0c;API 请求占全球动态互联网流量的 57%&#xff0c;这证实 API 是现代软件开发的重要组成部分。但随着多年来它们的采用不断增加&#xff0c;相关的安全挑战也随之增加。 在过去两年中&#xff0c…

备战蓝桥杯---动态规划(应用2(一些十分巧妙的优化dp的手段))

好久不见&#xff0c;甚是想念&#xff0c;最近一直在看过河这道题&#xff08;感觉最近脑子有点宕机QAQ&#xff09;&#xff0c;现在算是有点懂了&#xff0c;打算记录下这道又爱又恨的题。&#xff08;如有错误欢迎大佬帮忙指出&#xff09; 话不多说&#xff0c;直接看题&…

ThreadLocal的一些理解

阅读本篇博客您将了解如下内容&#xff1a; TreadLocal的作用。ThreadLocal的实现原理。ThreadLocal是否会引起内存泄漏&#xff0c;在什么样的条件下引发&#xff0c;如何避免。 1、ThreadLocal的作用 使用线程封闭的指导思想来解决变量共享的并发安全问题&#xff0c;–可以…

跳表是一种什么样的数据结构

跳表是有序集合的底层数据结构&#xff0c;它其实是链表的一种进化体。正常链表是一个接着一个用指针连起来的&#xff0c;但这样查找效率低只有O(n)&#xff0c;为了解决这个问题&#xff0c;提出了跳表&#xff0c;实际上就是增加了高级索引。朴素的跳表指针是单向的并且元素…

FPGA之复选器(2)

8:1 复选器 每个slice具有一个F7AMUX和一个F7BMUX F7AMUX 和 F7BMUX 原语&#xff1a; MUXF7 MUXF7_inst ( .O(O),//Output of MUX to general routing. I0(10), // Input (tie to LUT6 O6 pin) II(II), //Input (tie to LUT6 O6 pin) SS)// Input select to MUX 这两个…

【网站项目】167校园失物招领小程序

&#x1f64a;作者简介&#xff1a;拥有多年开发工作经验&#xff0c;分享技术代码帮助学生学习&#xff0c;独立完成自己的项目或者毕业设计。 代码可以私聊博主获取。&#x1f339;赠送计算机毕业设计600个选题excel文件&#xff0c;帮助大学选题。赠送开题报告模板&#xff…

Linux之用户和用户组的深入了解

目录 一、简介 1.1、用户&#xff1a; 1.2、用户组 1.3、UID和GID 1.3、用户账户分类 查看用户类别 超级用户root(0) 程序用户(1~499) 普通用户(500~65535) 二、用户 2.1、添加新的用户账号&#xff1a;useradd 2.2、删除账号&#xff1a;userdel 有-r与没有-r区别…

人为物累,心为形役

一、人是什么 你是你&#xff0c;他是他&#xff0c;我是我&#xff0c;有什么区别吗&#xff0c;直到自我发现我与你不同时&#xff0c;不同是什么&#xff0c;身体结构&#xff1f;人生经历&#xff1f;所拥有的一切&#xff1f;暂时搁置这些的话&#xff0c;抽离我们的意识…

【关于深度学习的一些资料】

曾梦想执剑走天涯&#xff0c;我是程序猿【AK】 目录 动手学深度学习Awesome Deep LearningTensorFlow Official ModelsPyTorch Image ModelsDeep Reinforcement LearningNeural Style Transfer 动手学深度学习 动手学深度学习 https://zh.d2l.ai/chapter_installation/index.…

4核8G服务器腾讯云和阿里云租用价格对比,2024更新

4核8G云服务器多少钱一年&#xff1f;阿里云ECS服务器u1价格955.58元一年&#xff0c;腾讯云轻量4核8G12M带宽价格是646元15个月&#xff0c;阿腾云atengyun.com整理4核8G云服务器价格表&#xff0c;包括一年费用和1个月收费明细&#xff1a; 云服务器4核8G配置收费价格 阿里…

学习大数据所需的java基础(5)

文章目录 集合框架Collection接口迭代器迭代器基本使用迭代器底层原理并发修改异常 数据结构栈队列数组链表 List接口底层源码分析 LinkList集合LinkedList底层成员解释说明LinkedList中get方法的源码分析LinkedList中add方法的源码分析 增强for增强for的介绍以及基本使用发2.使…