JavaScript 将静态文档变成了软件。单页应用(SPA)感觉非常迅速。没有全页刷新,没有白屏闪烁。但这种速度是有代价的,许多团队忽略了这一点:Web 的底层机制开始腐烂。导航变得脆弱。搜索引擎难以追踪路径。屏幕阅读器会迷失方向。用户发现自己被困在看起来像网站但行为却像损坏的桌面应用程序的界面中。

罪魁祸首通常是一个带有 onClick 处理器的 div

停止使用 Div 作为链接

div 不具备任何语义信息。它只是一个盒子。当你给它绑定一个点击处理器并用它来将用户路由到新视图时,你是在要求浏览器把一个纸箱当成一扇门。浏览器会拒绝,所有围绕浏览器构建的工具也会如此。

屏幕阅读器不会将 div 宣布为链接或按钮。它们会跳过它,或者将其作为纯文本读取。通过语音导航的用户无法定位它。搜索引擎爬虫在扫描页面以寻找可发现的 URL 时,什么也找不到。你的路由可能根本不存在。

更糟糕的是,你失去了用户已经熟悉的行为。真正的链接允许用户右键点击以在新标签页中打开、书签化目的地或复制地址进行分享。键盘用户期望通过 Tab 键到达它,并通过 Enter 键打开它。div 无法提供这些功能。即使你强行添加 tabIndexrole="link" 和键盘监听器,你也是在拙劣地重建浏览器免费提供给你的功能。而且你总会忘记某种边缘情况。你总是会忘记。

使用 Anchor 进行跳转,使用 Button 进行操作

HTML 已经解决了这个问题。混淆源于这两个元素看起来都可点击,因此开发者将它们视为可以互换的。它们并非如此。

当你想将用户移动到新的 URL 时,请使用 <a> 标签。不是模拟视图切换,不是状态更改,而是实际的位置。href 属性应该包含一个真实的地址:

<a href="/docs">Documentation</a>

就这样。如果用户要去某个地方,请使用链接。

当当前页面发生某些事情时,请使用 <button>。按钮用于执行以下操作:

  • 打开模态框 (modal)
  • 提交表单
  • 保存设置
  • 切换菜单

链接用于目的地。按钮用于操作。将两者混用会使你的界面变得混乱,并破坏用户的预期。

让浏览器发挥它的作用

现代浏览器是数十年演进和标准化的结果。它们处理安全性、历史记录、预取 (prefetching) 和可访问性的能力,比你手写的 JavaScript 要好得多。

一个真正的 anchor 标签会自动向浏览器历史堆栈提供数据。它能与原生右键菜单配合工作。当用户悬停或聚焦它时,它会参与浏览器的内置预取算法,让你无需编写一行代码就能让应用感觉更快。它尊重用户打开链接的偏好。它能与密码管理器、翻译工具和阅读模式协同工作。

当你用 JavaScript 导航函数取代它时,你就放弃了这一切。你不仅是在失去功能;你还在强迫用户放弃他们在互联网上其他所有网站中养成的习惯。这不仅仅是一个技术决策,这是一种敌对的用户体验。

检查你的框架实际渲染了什么

React Router, Vue Router, Next.js Link components, SvelteKit。这些工具让客户端路由变得轻而易举。但抽象会滋生错误。

检查你的 DOM。打开浏览器的开发者工具,查看你的框架生成的元素。一个 <Link> 组件在最终的 HTML 中应该渲染为一个带有有效 href 属性的真实 <a> 标签。如果它渲染为 spandiv 或任何其他没有适当 href 的元素,那么你的抽象就失败了。修复该组件。覆盖默认行为。使用 passHref 属性或框架对应的等效方案。不要在未经验证的情况下盲目信任框架。

这对注水 (hydration) 不匹配也至关重要。如果服务器渲染了一个链接,而客户端将其注水成了非链接,你就会制造出难以追踪的可访问性 bug,因为 HTML 在你的源代码中看起来是正确的,但在实时 DOM 中却是错误的。

禁止伪造目的地

有一种模式始终挥之不去:href="javascript:void(0)"。当开发者想要一个看起来像链接但行为像按钮的东西时,就会使用它,通常是因为他们不想给按钮设置样式,或者是因为旧的代码库要求这样做。

停下。这不是一个 URL。它没有给浏览器提供任何目的地。它会让历史记录栈充斥着无法使用的状态。它破坏了浏览器历史记录和无障碍访问。这是一个陷阱。如果你需要不进行导航的点击行为,你需要一个 <button>。你可以随心所欲地对其进行样式设计。CSS 并不在乎该元素是按钮还是链接。但你的用户在乎。

编写能够说明用户去向的文本

链接中的文字至关重要。屏幕阅读器用户经常会调出页面上所有链接的列表进行快速扫描。如果你的链接全都写着“阅读更多”或“点击这里”,那么这个列表就会变成毫无意义的噪音。

请务必具体。对比以下示例:

  • 错误:<a href="/security/api-guide">阅读更多</a>
  • 正确:<a href="/security/api-guide">阅读 API 安全指南</a>

第二种方式准确地告诉了用户他们将会看到什么。它为搜索引擎提供了关于目标页面的上下文信息。它让你的链接列表变得易于导航。描述性的链接文本是你可以获得的最廉价的无障碍改进之一。

认真对待测试

如果你不去验证,架构就毫无意义。

首先,测试你的键盘操作流。拔掉鼠标。通过 Tab 键遍历网站上的每一个交互元素。每一个真实的链接都必须显示可见的焦点轮廓(focus outline)——不是那种在背景下若隐若现的微光,而是一个即使是疲惫的双眼也能一眼识别出的清晰圆环。按下 Enter 键。它必须能激活链接。如果 Tab 键跳过了某个元素,或者按下 Enter 键没有任何反应,那么你就有一个 Bug。

其次,在服务器层面测试你的路由。客户端路由只是一层薄薄的装饰。如果用户将 /dashboard/reports 加入书签并在明天返回,或者点击了刷新,你的服务器必须知道如何提供该页面。配置你的反向代理或服务器框架,使其在遇到未知路径时回退到你的应用程序外壳(application shell),或者直接提供正确的 HTML。刷新时出现的 404 错误并非小问题。它是一个失信的承诺。

JavaScript 是一个强大的层