某天,有个用户反馈到技术群,发了个视频说发现他的 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 合作的学生计划。