某天,有个用户反馈到技术群,发了个视频说发现他的 wiki 站中的选项卡在 iOS 上出现了问题。
打开我的苹果手机一开,果然无法复现。在排除了各种干扰项、询问了具体环境之后,是时候打开 BrowserStack 了。稳定复现了,可喜可贺可喜可贺。
什么环境?不是写在标题内了吗?
先来大概看看代码,整体大概长这样,源代码比此更加复杂:
<head> <style> .d-tab .hidden { display: none; } .d-tab-titles .d-tab-delimiter:last-child { display: none; } </style> </head> <body> <div class="d-tab"> <div class="d-tab-titles"> <span class="d-tab-title active">乾</span> <span class="d-tab-title">坤</span> </div> <div class="d-tab-contents"> <div class="tab-content active">乾为天:元亨,利贞。</div> <div class="tab-content hidden"> 坤为地:元亨,利牝马之贞。君子有攸往,先迷后得主,利西南得朋,东北丧朋。安贞,吉。 </div> </div> <script> function dTab() { const singleSwitch = `<!--{$singleSwitch}-->` ?? false; // console.log("singleSwitch=",singleSwitch); document .querySelectorAll(".d-tab-titles .d-tab-title") .forEach((tab, i) => { tab.addEventListener("click", () => { // 找到当前 tab 所在的标题列表 const titleListContainer = tab.closest(".d-tab-titles"); //console.log(titleListContainer) if (!titleListContainer) return; const titleList = Array.from( titleListContainer.querySelectorAll( ".d-tab-title" ) ); // 找到对应的 .d-tab 容器和内容列表 const tabsContainer = titleListContainer.closest(".d-tab"); const tabContents = Array.from( tabsContainer.querySelectorAll( ":scope > .d-tab-contents > .tab-content" ) ); // 如果只有一个 tab,直接切换 active 类 if (titleList.length === 1 && singleSwitch) { if ( titleList[0].classList.contains( "active" ) ) { titleList[0].classList.remove("active"); tabContents[0].classList.remove( "active" ); tabContents[0].classList.add("hidden"); } else { titleList[0].classList.add("active"); tabContents[0].classList.add("active"); tabContents[0].classList.remove( "hidden" ); } return; } // 移除其他 tab 的 active 类 titleList.forEach(t => t.classList.remove("active") ); tab.classList.add("active"); // 获取当前 tab 的索引 const index = titleList.indexOf(tab); // 关闭所有 tab 内容 tabContents.forEach(content => content.classList.remove("active") ); tabContents.forEach(content => content.classList.add("hidden") ); // 打开对应索引的 tab 内容 if (tabContents[index]) { tabContents[index].classList.remove( "hidden" ); tabContents[index].classList.add("active"); } }); }); } if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", dTab); } else { dTab(); } </script> <style> .d-tab > .d-tab-titles { background: #414858; display: flex; border-bottom: 2px solid #414858; color: white; } .d-tab > .d-tab-titles > .d-tab-title.active { color: #fff1c5; box-shadow: none; } .d-tab > .d-tab-titles > .d-tab-title { width: 25%; padding: 7px 0; font-size: 16px; position: relative; border-radius: 0; text-align: center; } .d-tab > .d-tab-titles > .d-tab-title.active:after { content: ""; position: absolute; height: 2px; width: calc(100% - 10px); background: #fff1c5; left: 5px; bottom: 0; } </style> </div> </body>
根据报告人说,在删除了其中这段代码后便能恢复正常:
.d-tab > .d-tab-titles > .d-tab-title.active:after { content: ""; position: absolute; height: 2px; width: calc(100% - 10px); background: #fff1c5; left: 5px; bottom: 0; }
这不合理,但确实恢复了……
先大概看一眼各个的计算样式,似乎也没啥问题。那先确认是渲染 / CSS
上的问题吧:选择文本消失的选项元素
$0.innerText
→ ""
($0.textContent
→ "乾"
)。
很好,确认了,反正 JS 不多,直接单步调试走一遍吧。
我使用了 Chrome DevTools(至少对于 Bug,用哪个区别不大),然后发现……单步调试的时候似乎无法复现 Bug,那很明显是渲染上的 Bug 了。
于是我直接在所有的 class 更改的操作后面增加了对应的
ele.offsetTop;
来尝试强制触发重排并触发重绘。
好吧没用,不过我相信不是我的问题而是 Safari 在重绘上的问题。
那换种方法,将所有 ele.offsetTop;
改成
console.log(ele.innerText);
我们可以发现在这之后文字就消失了:
// 移除其他 tab 的 active 类 titleList.forEach(t => t.classList.remove("active") ); tab.classList.add("active");
至此,我们已经定位到了具体位置,大概确定了原因,那么是时候解决了。
首先我想到的是在上面那段代码之后刷新一下 DOM,例如使用
ele.innerHTML
,但考虑到可能的元素绑定,感觉需要更加麻烦的办法……
正当我苦思冥想之际(并不),我突发奇想,试了一下使用微任务:
setTimeout(() => tab.classList.add("active"), 0);
就成功修复了……
至此也差不多结束了,为什么修复了?我不知道。
然后要吐槽一下 BrowserStack 的调试真的很难用,有很多 Bug。但也要感谢 GitHub Student Developer Pack,申请又方便东西又多,例如和 BrowserStack 合作的学生计划。