工具函数封装
约 5705 字大约 19 分钟
2025-09-25
1. 如何实现本地化媒体资源,提高播放兼容性的?
这个方法本来是在“增加通话记录录音条,实现可拖动播放”的需求中,遇到了ios默认浏览器的兼容性问题时提出的解决方案。
核心作用是将一个指向音频文件的 URL(或其他数据源)转换为一个Blob URL。Blob URL 是一种特殊的 URL,它不指向服务器上的资源,而是指向浏览器内存中存储的一个二进制大对象(Blob)。
处理音频和视频等多媒体内容时,直接使用远程 URL 作为 <audio> 或 <video> 标签的 src 属性可能会遇到一些问题,例如:
跨域限制:如果音频文件存储在与当前网页不同的域上,并且没有正确配置 CORS(跨域资源共享),浏览器可能会阻止加载该资源。
重复请求:如果页面上多次使用同一个音频资源,浏览器可能会发起多次网络请求,增加了不必要的开销。
性能和控制问题:直接流式加载远程音频时,对音频的精确控制(如自由拖动进度条到任意未缓冲的位置)可能会受到限制,尤其是在某些移动端浏览器(如 iOS 的 Safari)上。浏览器需要先下载该时间点之前的所有数据,才能开始播放。
$toBlobUrl 方法通过预先将整个音频文件下载到浏览器内存中,并为其创建一个本地的 Blob URL,从而一举解决了上述所有问题。一旦音频数据完全加载到内存,后续的播放、暂停、跳转等操作都将变得非常迅速和流畅,因为它不再依赖于网络连接,而是直接操作本地数据。这极大地提升了用户体验
具体实现思路是:
先进行前置检查,然后尝试通过网络请求获取资源并进行转换,最后进行错误处理
前置检查:方法首先会检查传入的
src是否已经是blob:URL。如果是,说明该资源已经是本地化的,无需再次转换,直接返回原src网络请求与转换:如果
src是一个普通的 URL,方法会使用axios库发起一个 GET 请求来获取该资源。这里的关键在于请求配置中设置了responseType: 'blob',这会告诉axios将响应体直接作为 Blob 对象返回,而不是默认的 JSON 或文本。创建 Blob URL:请求成功后,会得到包含音频数据的
data(一个 Blob 对象)。然后,代码会使用new Blob([data], {type: 'audio/wav'})再次包装这个 Blob,并明确指定其 MIME 类型为audio/wav。这一步虽然看似多余(因为axios返回的已经是 Blob),但它确保了最终的 Blob 对象拥有正确的type属性。再通过window.URL.createObjectURL(blob)浏览器原生 API,为这个内存中的 Blob 对象创建一个唯一的、临时的 URL。错误处理:遇到错误时返回原始的
srcURL。
2. ios默认浏览器的兼容性的问题原因是什么?
这是一个在 iOS 设备上普遍存在的问题:直接使用远程 URL 的 <audio> 元素,其 currentTime 属性可能无法被自由设置。简单来说,根本原因在于 iOS Safari 在处理流式媒体时,对服务器是否支持 HTTP Range Requests(范围请求)有非常严格的要求。如果服务器不支持,Safari 将无法实现对未缓冲部分的精确跳转。
1. 什么是 HTTP Range Requests?
想象一下您在观看一个 2 小时的高清电影,文件大小有好几个 GB。当您想从第 10 分钟直接跳到第 90 分钟时,您肯定不希望浏览器必须先下载完前 90 分钟的所有数据才能开始播放。
HTTP Range Requests 就是为了解决这个问题而设计的。它允许浏览器(客户端)向服务器发起一个特殊的请求,告诉服务器:“我不要整个文件,我只需要这个文件从第 X 字节到第 Y 字节的这一部分”。
一个支持范围请求的服务器,在收到浏览器的初始请求时,会在其响应头(HTTP Header)中包含一个字段: Accept-Ranges: bytes 。
当浏览器看到这个响应头后,它就知道:“太好了,这个服务器很高级,我可以随时向它要文件的任意一小块。” 于是,当用户拖动进度条时,浏览器就可以计算出新时间点对应的大致字节范围,然后立即发送一个新的范围请求去获取那一小块数据,从而实现快速跳转(seek)。
2. iOS 的“固执”:严格要求与资源管理
现在我们来看看 iOS 为什么在这个问题上如此“特别”。这主要源于苹果对移动设备体验的两个核心设计理念: 节省数据流量 和 节省电量 。
节省资源 :在移动网络环境下,流量是宝贵的;同时,频繁的网络请求会持续唤醒无线电模块,是电量消耗大户。因此 ,iOS 的策略是尽可能地减少不必要的网络活动。
行为表现 :当 iOS Safari 加载一个
<audio>或<video>标签指向的远程 URL 时,它会先发送一个试探性的请求(通常是 HEAD 请求或一个请求少量数据的 GET 请求)来检查服务器的响应头。如果服务器返回了 Accept-Ranges: bytes :Safari 就知道它可以按需加载数据。它会先加载一小部分用于开始播放,然后根据用户的操作(如拖动进度条)或预缓冲策略,再去请求文件的其他部分。在这种模式下,设置 currentTime 到未缓冲区域是可行的。
如果服务器没有返回 Accept-Ranges: bytes :Safari 会认为这是一个“原始”的服务器,不支持高级的范围请求功能。它会采取一种最简单、最保守的下载策略—— 从头到尾完整下载 。它会把这个 URL 当作一个普通的、不可分割的文件来下载。
3. currentTime 无法设置问题的根源
当 Safari 采用“从头到尾完整下载”的策略时,问题就出现了。
在这种模式下,音频文件的数据是按照时间顺序一点一点地流入浏览器的缓冲区的。 浏览器只拥有它已经下载了的那部分数据 。
如果您尝试将 currentTime 设置到一个 已经缓冲 的时间点(比如从第 10 秒跳回第 5 秒),这是没有问题的,因为数据已经存在于本地内存中。
但是,如果您尝试将 currentTime 设置到一个 尚未缓冲 的时间点(比如从第 10 秒跳到第 50 秒,而浏览器当前只下载到第 30 秒),Safari 就束手无策了。因为它知道它不能向服务器请求第 50 秒的数据(服务器不支持范围请求),它唯一能做的就是 等待 ,直到下载过程自然地进行到第 50 秒。
从开发者的角度看,这表现为: 设置 currentTime 的操作被“忽略”了,或者说“失败”了 。音频不会跳转,而是会继续从当前位置播放,或者在缓冲完成后才可能跳过去。
3.如何实现清理对象中的无效数据,支持自定义过滤规则的?
在需求“为符合条件的全部客户批量设置标签”,如果用户没有设置某些筛选条件(例如,没有选择客户状态或没有输入客户名称),finalParams 对象中对应的属性值可能就是 '' 或 null。如果不清理,这些无效的参数 status: '' 也会被发送到后端,导致不必要的开销,这可能会干扰后端的查询逻辑,或者至少是冗余的。
$purifyData 方法的核心是递归地遍历一个给定的数据对象,并移除其中所有被视作“不纯”的属性。还会移除所有函数、日期对象和正则表达式对象,因为这些类型通常不适合作为可序列化的数据进行传输。
实现思路是:
基于深度递归和类型判断。它接收数据对象,然后开始递归地检查和处理该对象的每一个属性。
首先会遍历传入对象 data 的所有自身属性(使用 hasOwnProperty 确保不处理原型链上的属性)。在遍历过程中,它会借助一个 typeOf 辅助函数来确定每个属性值的具体类型。
接下来用一个 switch 语句来针对不同的数据类型执行不同的清理逻辑。
其他项目中如何清洗数据?
您问到了一个非常普遍的工程实践问题。如果项目中没有类似 $purifyData 的方法,开发者们通常会采用以下几种策略来处理数据净化,或者选择不处理。
手动按需构建新对象
使用第三方库(如 Lodash)
_.pickBy是一个完美的选择,它可以根据你提供的条件来挑选对象的属性。
4. 如何实现modal弹窗拖拽移动功能的?
默认情况下,Ant Design Vue 的 Modal 弹窗是固定在屏幕中央的,用户无法改变它的位置。某些复杂场景下,用户可能需要移动弹窗以查看被遮挡的页面内容.通过监听鼠标事件,动态修改 Modal 的位置,从而实现了自由拖拽的效果
具体思路:
获取 DOM 元素:首先,Vue指令会通过getElementsByClassName('ant-modal-header')[0]。获取到 Modal 组件的头部(
.ant-modal-header)和主体(.ant-modal)元素。拖拽事件将绑定在头部,而位置的改变则作用于主体。初始化位置与状态:指令会记录 Modal 的初始位置。考虑到
destroyOnClose这个属性。如果 Modal 在关闭后不销毁,那么下次打开时直接从modal对象上获取属性left = modal.left || 0 top = modal.top || 0 // 直接从modal对象上获取属性监听鼠标按下 (
onmousedown) 事件:当用户在 Modal 头部按下鼠标时,拖拽开始。此时,指令会记录下鼠标的起始坐标,并开始监听鼠标移动 (onmousemove) 和松开 (onmouseup) 事件。处理鼠标移动 (
onmousemove) 事件:在鼠标移动过程中,指令会实时计算鼠标的位移,并根据这个位移来更新 Modal 的left和top样式。公式为:header的初始left + 鼠标X轴位移边界检测与限制:为了防止用户将 Modal 拖出可视区域,它会获取当前浏览器的可视窗口尺寸(
clientWidth和clientHeight),并确保 Modal 的left和top值始终保持在合理的范围内,不会完全移出屏幕。处理鼠标松开 (
onmouseup) 事件:当用户松开鼠标按键时,拖拽结束。此时,指令会清除onmousemove和onmouseup事件的监听
5. 如何实现弹窗自动定位和响应式更新的?
smartModal.js 采用了 Vue 自定义指令的方式实现,包含两个主要指令:
v-smart-anchor:作为锚点,记录触发元素的位置信息v-smart-modal:为弹窗提供智能定位功能
第一部分:v-smart-anchor - 锚点标记
捕获锚点元素的位置信息,并将其存储到一个全局的 Map 对象 anchorStore 中
实现思路:
该指令通过 Vue 的 bind 和 unbind 钩子来管理事件监听。在 bind 钩子中,它会给指令所绑定的元素(即锚点元素)添加 mousedown 和 click 事件的监听器。当这些事件被触发时,事件处理器会执行以下操作:
- 获取唯一的 Key :通过 getKeyFromBinding 函数从指令的绑定值中提取一个唯一的 key 。这个 key 是锚点和弹窗之间关联的桥梁,确保一个弹窗能准确找到自己的锚点。
- 确定触发源 :它会优先使用 event.target.closest('button') 来寻找事件触发点最近的父级 button 元素作为锚点,如果找不到,则回退到使用指令所绑定的 el 元素本身。这样做可以更精确地定位到用户实际交互的那个按钮。
- 获取位置信息 :调用标准 DOM API trigger.getBoundingClientRect() 来获取锚点元素相对于视口的精确位置和尺寸(一个包含 top , right , bottom , left , width , height 的对象)。
- 存储信息 :将获取到的元素引用 element 、位置矩形 rect 、当前时间戳 time 以及点击事件的精确坐标 clickPosition 一并存入名为 anchorStore 的全局 Map 中,键为第一步获取的 key 。
在 unbind 钩子中,指令会负责移除之前添加的事件监听器,防止内存泄漏。
第二部分:v-smart-modal - 智能定位
实现思路:
该指令通过 inserted 和 componentUpdated 钩子来触发其核心逻辑 setupOrUpdate ,并通过 unbind 钩子调用 teardown 进行清理。
状态初始化与更新 : setupOrUpdate 函数首先会为指令绑定的元素 el 创建或更新一个内部状态对象 el.smartModalState 。这个对象用于存储与该弹窗实例相关的所有信息,如 key 、预设的 width 和 height 、 onClose 回调、对弹窗 DOM 元素的引用 modalWrap ,以及各种观察器和事件处理器。
判断可见性 :指令会检查弹窗的 visible 状态。这个状态可以从指令的绑定值 binding.value.visible 中获取,如果未提供,则会尝试从 vnode.componentInstance.visible (即 a-modal 组件实例的 visible 属性)中获取。如果弹窗不可见,则调用 teardown 函数清理所有资源(如事件监听、观察器)并返回。
异步定位 :如果弹窗是可见的,指令并不会立即执行定位。因为当 visible 变为 true 时, a-modal 的 DOM 元素可能还没有被完全渲染到文档中。因此,它使用 vnode.context.$nextTick 将所有定位相关的操作推迟到下一个 DOM 更新循环中执行,确保此时能够找到弹窗的 DOM 元素。
查找目标 Modal :在nextTick 回调中,首先调用 ensureModalWrap 函数来获取当前最顶层且可见的弹窗 DOM 元素。这个函数通过 document.querySelectorAll('.ant-modal-wrap') 获取页面上所有的 antd 弹窗包装层,然后 从后往前 遍历(因为后打开的弹窗在 DOM 结构中更靠后),找到第一个可见的弹窗。这个查找逻辑非常健壮,能准确地在多弹窗场景下找到正确的目标。
执行核心定位算法 :找到弹窗 DOM 后,就进入了最核心的定位环节。
- 首先,通过 getAnchorRect(state.key) 从 anchorStore 中获取之前保存的锚点位置矩形。如果找不到,则调用 defaultCenterRect() 创建一个位于屏幕中心的虚拟矩形作为回退。
- 然后,调用 calculatePosition 函数,将锚点矩形、弹窗的配置(宽高、间隙)和内容尺寸传入。这个函数是定位算法的核心。
- 最后,将 calculatePosition 返回的样式对象通过 applyStyle 函数应用到弹窗的 .ant-modal 元素上。
- 保持响应式 :为了让定位在各种动态变化下依然准确,指令还做了两手准备:
- 内容尺寸变化 :通过 ResizeObserver 监听弹窗内容区域 .ant-modal-content 的尺寸变化。当内容变化(例如,加载了异步数据导致高度增加)时, ResizeObserver 会被触发,从而重新调用 calculatePosition 来调整弹窗位置。
- 窗口变化 :通过给 window 对象添加 resize 和 scroll 事件监听,确保在用户缩放浏览器窗口或滚动页面时,也能重新计算并更新弹窗的位置。
核心定位算法 calculatePosition
这是整个指令最精华的部分,它决定了弹窗最终出现的位置。其计算思路缜密,考虑了多种情况,旨在找到一个既能靠近锚点,又不会超出屏幕边界的最佳位置。
算法输入 :
rect : 锚点元素的 DOMRect 对象。
config : 弹窗的配置,包括预设的 width , height , gap (与锚点的间隙), minMargin (与屏幕边缘的最小距离)。
contentSize : 由 ResizeObserver 监测到的弹窗内容区的实际宽高。 算法步骤 :
计算可用空间 :首先,算法会计算出锚点元素四周相对于视口( viewport )的可用空间大小。
策略一:优先放置在下方或上方(垂直方向) 。这是最符合直觉的放置方式。
- 检查下方 :判断下方的可用空间 spaceBelow 是否足够容纳 弹窗高度 + 间隙 。如果足够,就将弹窗放在锚点下方。
- 检查上方 :如果下方空间不足,则判断上方的可用空间 spaceAbove 是否足够。如果足够,就将弹窗放在锚点上方。
- 在垂直方向放置时,水平位置通过 calculateHorizontal 函数计算,该函数会尝试让弹窗的左边缘与锚点的左边缘对齐,但如果这样会导致弹窗超出屏幕右侧,它会自动向左调整,确保弹窗完整显示。
策略二:放置在左侧或右侧(水平方向) 。如果垂直方向上、下都没有足够的空间,算法会尝试水平放置。
- 它会比较左侧 spaceLeft 和右侧 spaceRight 的空间大小,优先选择空间更充足的一侧进行放置。
- 在水平方向放置时,垂直位置通过 calculateVerticalCenter 函数计算,该函数会尝试让弹窗的垂直中心与锚点的垂直中心对齐,同时确保弹窗不会超出屏幕的上下边缘。
策略三:回退策略(Scoring Model) 。在极端情况下,如果上下左右都无法完整地放下弹窗(例如屏幕非常小或锚点在角落),算法会进入一种“评分模式”。
- 它会计算出四个潜在的放置方案(下、上、左、右),并为每个方案的可用空间( spaceBelow , spaceAbove 等)打分。
- 然后,它会选择得分最高的那个方案,即空间最大的那个方向,作为最终的放置方向。
- 此时,弹窗的位置会被 clamp 函数强制约束在屏幕可视区域内,虽然可能无法完全避免与锚点重叠,但保证了弹窗本身是可见和可操作的。
最终边界约束 :无论通过哪种策略计算出 top 和 left ,最后都会再次通过 clamp 函数进行最终的边界检查,确保弹窗的任何部分都不会超出屏幕的可视范围(减去 minMargin )。
6. 如何实现响应式更新的?
state.resizeObserver是实现内容自适应响应式更新的关键。
简单来说,它的作用是:当模态框(Modal)内部的内容发生变化(比如加载了异步数据导致高度增加,或者展开/折叠了某个区域),ResizeObserver 能够立即侦测到这个尺寸变化,并自动触发一次位置的重新计算,从而让模态框优雅地调整自己的位置以适应新的尺寸,避免出现内容溢出或位置不佳的情况。
实现思路与工作流程:
在 setupOrUpdate 函数中,当指令确认模态框变为可见 (visible 为 true) 后,它会在 $nextTick 中执行一系列设置,其中就包括对 ResizeObserver 的初始化。这个过程可以分解为以下几个步骤:
一次性初始化:代码首先会检查
state.resizeObserver是否已经存在。如果不存在,才会创建一个新的ResizeObserver实例并将其保存在state.resizeObserver中。这个检查确保了对于同一个模态框实例,观察器只会被创建一次,避免了不必要的性能开销。选择观察目标:观察器并没有观察整个模态框 (
.ant-modal-wrap或.ant-modal),而是非常精确地选择了.ant-modal .ant-modal-content作为观察目标。这是一个非常明智的选择,因为通常一个模态框的标题(header)和页脚(footer)尺寸是固定的,真正会引起尺寸变化的,是承载主要内容的content部分。精确地观察content可以让响应更及时、更高效。定义回调函数:在创建
ResizeObserver实例时,需要传入一个回调函数。它会在被观察元素(即.ant-modal-content)的尺寸发生变化时被浏览器自动调用。在回调中执行重新定位:这个回调函数执行以下逻辑:
- 获取新尺寸:它从回调函数的参数
entries中,提取出内容区域最新的contentRect,从而得到新的宽度w和高度h。 - 性能优化 - 设置阈值:它并不会在尺寸每变化 1 像素时都去重新计算,而是设置了一个阈值。只有当新的宽度或高度与上一次记录的尺寸 (
state.contentSize) 相比,变化量超过 5 像素时,才会触发下一步。并且设计了防抖。这是一个非常重要的性能优化,可以防止在尺寸快速、连续变化(例如动画)时引发过于频繁的重计算,避免了性能抖动。 - 更新状态并重新计算:如果尺寸变化超过了阈值,它会首先将新的尺寸
{ width: w, height: h }更新到state.contentSize中。然后,它会重新调用核心的calculatePosition函数,并将这个新的contentSize作为参数传进去。calculatePosition算法现在就会基于模态框最新的、真实的尺寸来计算最佳位置。 - 应用新样式:最后,将
calculatePosition返回的新的样式对象通过applyStyle应用到模态框上,使其平滑地移动到新的、正确的位置。
- 获取新尺寸:它从回调函数的参数
启动观察:在创建并配置好
ResizeObserver实例后,调用state.resizeObserver.observe(content)来正式启动对内容元素的观察。
ResizeObserver 是一个现代浏览器提供的、用于观察元素尺寸变化的强大 API。相比于使用 setInterval 定时去轮询检查元素尺寸这种旧的、性能低下的方式,ResizeObserver 由浏览器原生支持,性能极高,且响应非常及时。
