为什么 Javascript 代码在桌面端和移动端 Safari 中无法正常运行?
先修正代码中能确认的问题,再根据 Safari 控制台的报错排查兼容性。仅凭这两段代码,还无法确定 Safari 中的唯一故障原因:第一段有括号配对错误,两段都依赖按钮的 innerText 精确匹配,第二段的类名判断有误,DOM 查询后也没有检查结果。提供的移动端 HTML 与 JavaScript 使用的选择器还不一致。
安装 Babel 后,仍需确认相关语法和浏览器 API 是否得到了兼容处理。
先检查第一段代码的语法错误
题目中粘贴的第一段代码,在事件回调结束处多了一个 }。事件监听和循环应这样闭合:
ItBtns.forEach(btn => {
btn.addEventListener('click', event => {
event.preventDefault();
// 在这里执行内容切换
});
});
如果部署文件中也有这个多余的括号,Chrome 同样应该报语法错误。既然 Chrome 能正常运行,就需要核对两个浏览器实际加载的是否为同一份构建文件、是否使用了旧缓存,以及粘贴代码时是否多复制了括号。
在 Safari 的 Network 面板中打开实际加载的 JavaScript 文件,结合控制台定位出错行。本地源码不能代替这一步检查。
不要依赖 innerText 判断点击了哪个服务
原代码用下面的条件决定是否更新内容:
btn.innerText === 'WEB DEVELOPMENT'
innerText 是元素渲染后的文本,会受到布局、换行和 CSS 等因素影响。读取结果只要与字符串不完全一致,就可能跳过所有分支,而且不会抛出异常。
可以先在 Safari 控制台查看实际值:
var btn = document.querySelector('.home-btn');
console.log(btn && JSON.stringify(btn.innerText));
console.log(btn && JSON.stringify(btn.textContent));
JSON.stringify() 可以让换行和首尾空白更容易辨认。如果仍需通过文本判断,可以读取标题节点:
var label = btn.querySelector('.btn-title');
var serviceName = label ? label.textContent.trim() : '';
按钮文字会随文案、样式和语言变化,用 data-service 标识业务类型更稳妥。
桌面端的修改示例
为五个按钮分别设置 web、mobile、design、analytics 和 api。Rails 模板中的第一个按钮可以这样写:
<%= link_to "#",
class: "home-btn home-btn-black active",
data: { service: "web" } do %>
<span class="btn-title">WEB DEVELOPMENT</span>
<% end %>
保留原函数中的标题、描述常量,以及 title、text 查询,将 ItBtns.forEach(...) 部分替换为:
const services = {
web: {
title: webDevTitle,
text: webDevText
},
mobile: {
title: mobileAppTitle,
text: mobileAppText
},
design: {
title: prodDesignTitle,
text: prodDesignText
},
analytics: {
title: analyticTitle,
text: analyticText
},
api: {
title: apiDevTitle,
text: apiDevText
}
};
if (!title || !text) {
return;
}
for (var i = 0; i < ItBtns.length; i++) {
var button = ItBtns[i];
if (button.getAttribute('data-services-bound') === 'true') {
continue;
}
button.setAttribute('data-services-bound', 'true');
button.addEventListener('click', function (event) {
event.preventDefault();
var key = event.currentTarget.getAttribute('data-service');
if (!Object.prototype.hasOwnProperty.call(services, key)) {
return;
}
title.textContent = services[key].title;
text.textContent = services[key].text;
});
}
这里用 textContent 写入普通文本,通过 data-service 区分按钮,循环也不再依赖 NodeList.prototype.forEach。
event.currentTarget 指向绑定监听器的按钮。即使用户点击内部的 <span>,也能取得按钮上的 data-service。
移动端代码中的 DOM 和状态判断问题
用 classList.contains() 判断是否包含某个类
原代码:
if (btn.classList != 'home-btn-black-v2') {
classList 是 DOMTokenList。上面的比较会涉及字符串转换,比较的是整个类名列表,不能用来判断其中是否包含某个类。
应改为:
if (!btn.classList.contains('home-btn-black-v2')) {
// 更新选中状态
}
检查查询结果是否为 null
下面两处查询都假定页面已经有选中项:
selectElement('.home-btn-v2.home-btn-black-v2.active-v2')
selectElement('.section.show-description')
如果没有设置初始状态,或者选择器与 HTML 不一致,查询结果就会是 null。此时访问 .classList 或 .style 会抛出异常,中断本次点击处理。
访问属性前,至少应检查查询结果:
var expanded = document.querySelector('.section.show-description');
if (expanded) {
expanded.style.height = '44px';
expanded.classList.remove('show-description');
}
btn.nextElementSibling 也可能为 null,读取它的 offsetHeight 前同样需要检查。
核对移动端脚本需要的 HTML 结构
移动端脚本查找 .home-btn-v2 和 .section,但提供的第二份 HTML 仍使用 .home-btn,也没有 .section。
如果实际页面使用的就是这份 HTML,执行:
document.querySelectorAll('.home-btn-v2').length
结果会是 0,因此不会绑定事件监听器。这种情况在 Chrome 中也无法正常工作,需要核对移动端实际渲染的 DOM。
第二份模板中还有一个多余的 >:
<span class="btn-title">ANALYTICS & SEO</span>>
应删除这个字符,但仅凭它还无法判断整个交互失效的原因。
移动端优先让内容自然撑开容器
原代码的五个文字判断分支执行的是同一条高度赋值,可以合并为:
btn.parentElement.style.height = (88 + textHeight) + 'px';
其中的 44 和 88 都依赖具体 CSS。字体、换行或屏幕宽度变化后,固定高度可能截断内容。如果内容处于 display: none 状态,读到的 offsetHeight 还会是 0。
如果只需要展开内容,可以让容器随内容自动增高。下面的 HTML、CSS 和脚本相互对应,其余服务按相同结构添加:
<div class="services-mobile">
<div class="section">
<button
type="button"
class="home-btn-v2"
aria-expanded="false"
aria-controls="service-web-description"
>
WEB DEVELOPMENT
</button>
<div
id="service-web-description"
class="service-description"
>
<p>这里放置 Website Development 的说明文字。</p>
</div>
</div>
</div>
.services-mobile .section {
height: auto;
}
.services-mobile .service-description {
display: none;
}
.services-mobile .section.show-description .service-description {
display: block;
}
function initItServicesClickV2() {
var root = document.querySelector('.services-mobile');
if (!root) {
return;
}
var buttons = root.querySelectorAll('.home-btn-v2');
for (var i = 0; i < buttons.length; i++) {
var button = buttons[i];
if (button.getAttribute('data-services-bound') === 'true') {
continue;
}
button.setAttribute('data-services-bound', 'true');
button.addEventListener('click', function (event) {
var currentButton = event.currentTarget;
var currentSection = currentButton.parentElement;
for (var j = 0; j < buttons.length; j++) {
var otherButton = buttons[j];
var selected = otherButton === currentButton;
otherButton.parentElement.classList.remove('show-description');
otherButton.classList.remove('home-btn-black-v2');
otherButton.classList.remove('active-v2');
otherButton.setAttribute(
'aria-expanded',
selected ? 'true' : 'false'
);
}
currentSection.classList.add('show-description');
currentButton.classList.add('home-btn-black-v2');
currentButton.classList.add('active-v2');
});
}
}
这个版本仍然一次只展开一个服务,再次点击当前项时保持展开。使用时,需要移除旧脚本写入的内联高度,并调整原有的固定高度样式。示例没有高度过渡动画;如需动画,还要单独处理尺寸测量和过渡。
检查 Babel 配置和构建结果
Babel 的语法转换与浏览器 API 支持需要分别检查。
- 箭头函数、
const、模板字符串等语法是否经过转换,取决于 Babel 配置、目标浏览器,以及文件是否经过构建流程。 - 转换箭头函数不会自动补上
NodeList.prototype.forEach等 DOM API 的支持。 export必须经过适当的模块构建,或作为浏览器支持的模块脚本加载,不能直接放进普通脚本中执行。
如果控制台明确提示 forEach 不存在,可以使用前面示例中的普通 for 循环。改成 Array.from(...).forEach(...) 前也要确认兼容性,因为 Array.from 本身也有兼容性要求。
是否需要额外的 polyfill,要根据实际支持的 Safari 版本和报错判断。仅凭 Safari 运行失败,无法认定它不支持 ES6。
桌面端和移动端 Safari 的排查顺序
-
先看控制台的第一条错误。
语法错误会阻止脚本执行;访问空节点的属性通常会中断当前初始化或事件处理。解决第一条错误后,再观察后续情况。
-
检查实际加载的脚本。
在 Network 面板确认请求成功、响应内容确实是 JavaScript,并核对 Safari 与 Chrome 加载的资源版本。
-
检查选择器是否命中。
console.log(document.querySelectorAll('.home-btn').length); console.log(document.querySelectorAll('.home-btn-v2').length); console.log(document.getElementById('it-services-title')); console.log(document.getElementById('it-services-description')); -
确认初始化函数已执行。
export只导出函数,不会调用它。入口文件需要导入并执行初始化函数,且执行时对应的 DOM 必须已经存在。 -
根据 Rails 的页面导航方式处理初始化。
普通页面加载可使用
DOMContentLoaded。如果项目使用 Turbolinks 或 Turbo,则分别检查对应的turbolinks:load或turbo:load生命周期。只注册项目实际需要的事件,并避免重复绑定监听器。 -
在真实移动设备上检查。
在 Mac Safari 中通过 Web Inspector 连接 iPhone 或 iPad 的 Safari 页面,查看点击时的异常、元素样式和尺寸。仅缩窄桌面窗口,无法覆盖移动端 Safari 的全部运行条件。
如果点击处理已执行且没有异常,内容却仍不可见,再检查 display、height、overflow 和媒体查询,重点排查布局和尺寸计算。
备注:内容仅供参考。
