别急着写 JavaScript:浏览器可能已经帮你实现了

HTML1周前发布 admin
153 0

写在前面

不知道你有没有这种经历:产品经理提了个需求,比如”做一个可以展开收起的面板”,你脑子里立刻开始规划——用 useState 控制一个布尔值,写个点击事件切换状态,再加点 CSS 过渡动画……写完一看,几十行代码,测试半天,还得考虑无障碍访问(键盘能不能操作、屏幕阅读器怎么读)。

结果你猛然发现,浏览器原生的 <details> 标签,一行 HTML 就搞定了,而且这些细节人家都替你想好了。

这不是个例。这些年浏览器厂商一直在往 HTML 和 CSS 里塞各种”语法糖”,很多以前必须靠 JavaScript 才能实现的交互效果,现在写几行标签或样式就能搞定。这篇文章就想跟你聊聊,哪些常见需求已经”原生化”了,写代码之前不妨先查一查,能不写 JS 就别写。

为什么要这么较真?三个理由:

  1. 省代码:原生实现往往就是几行标签或 CSS,比一个组件加一堆状态管理简单得多。
  2. 少 bug:浏览器实现的东西经过大规模验证,边界情况(键盘操作、无障碍、多语言)基本都覆盖了,你自己写很容易漏掉。
  3. 性能更好:原生功能大多跑在浏览器的渲染引擎层面,比 JS 操作 DOM 效率高,尤其是滚动、动画这类高频触发的场景。

下面挑几个最常见、最容易被忽视的例子来说说。

一、折叠面板:<details> + <summary>

以前做一个”点击展开更多内容”的手风琴效果,标准操作是:

const [open, setOpen] = useState(false);
return (
  <div>
    <button onClick={() => setOpen(!open)}>展开详情</button>
    {open && <div>这里是详细内容...</div>}
  </div>
);

现在浏览器原生支持这个交互,写法是这样的:

<details>
  <summary>展开详情</summary>
  <p>这里是详细内容...</p>
</details>

一行 HTML,点击自动展开收起,默认自带一个小三角箭头图标,键盘也能直接操作(回车、空格都行)。想要多个面板但同一时间只能展开一个(手风琴效果),可以给多个 <details> 加上同一个 name 属性,浏览器会自动帮你处理互斥逻辑,一行 JS 都不用写。

二、弹窗和模态框:<dialog> 标签

做过弹窗组件的都知道,一个”合格”的模态框要处理不少细节:点击遮罩关闭、按 ESC 关闭、打开时锁住背景滚动、焦点要困在弹窗里不能跑到背景元素上(这个专业术语叫 focus trap)。这些东西自己实现,代码量不小,还容易漏掉无障碍相关的处理。

原生 <dialog> 元素把这些都包了:

<dialog id="myDialog">
  <p>这是一个弹窗</p>
  <button onclick="document.getElementById('myDialog').close()">关闭</button>
</dialog>

<button onclick="document.getElementById('myDialog').showModal()">打开弹窗</button>

调用 showModal() 打开后,浏览器会自动给页面加一层半透明背景(可以用 ::backdrop 伪元素自定义样式),按 ESC 能关闭,焦点也会被自动限制在弹窗内部。你要做的,基本只剩下弹窗内容本身的样式。

三、图片懒加载:loading="lazy"

以前实现图片懒加载,常见做法是用 Intersection Observer 监听图片是否进入可视区域,进入了再把 data-src 的值赋给 src。这套逻辑虽然不复杂,但也是几十行代码,还得自己处理兼容性和边界情况。

现在只需要一个属性:

<img src="photo.jpg" loading="lazy" alt="示例图片" />

浏览器会自动判断图片什么时候接近可视区域,提前加载,不需要你手写任何监听逻辑。同样的道理也适用于 <iframe> 标签。

需要注意的是,如果图片本来就在首屏可见区域内,不建议加 loading="lazy",因为这样反而可能延迟首屏图片的加载,影响首屏渲染速度。

四、表单校验:原生属性代替手写逻辑

很多时候我们做表单校验会自己写一堆判断逻辑:是不是空的、是不是邮箱格式、长度够不够。其实这些 HTML 表单元素自带的属性基本都能覆盖:

<form>
  <input type="email" required placeholder="请输入邮箱" />
  <input type="number" min="1" max="100" required />
  <input type="text" pattern="[A-Za-z]{3,}" title="至少3个字母" />
  <button type="submit">提交</button>
</form>

required 表示必填,type="email" 会自动校验邮箱格式,min/max 限制数字范围,pattern 可以写正则表达式做自定义校验。用户提交时如果没通过校验,浏览器会自动弹出提示气泡,还会把焦点定位到第一个出问题的输入框上——这些交互细节,原生全部免费赠送。

当然,复杂的业务校验(比如两次密码是否一致)还是需要 JS 配合,但至少格式类的基础校验完全可以交给浏览器。

五、吸顶效果:position: sticky

做过表格表头吸顶、侧边导航吸顶的同学,应该都写过监听滚动事件、计算元素位置、动态切换 position: fixed 类名这套操作。这种方式不仅代码繁琐,滚动事件触发频率高,处理不好还容易造成页面卡顿。

一行 CSS 就能解决:

.sticky-header {
  position: sticky;
  top: 0;
}

元素在正常文档流中滚动,一旦滚动到指定位置(比如 top: 0),就会”粘”在那个位置,不再跟随滚动,效果丝滑,而且是浏览器原生渲染层面实现的,性能比监听滚动事件好得多。

六、CSS 的 :has() 选择器:告别”逆向选择”的 JS 判断

这是最近两年才被主流浏览器支持的一个”狠角色”。以前 CSS 选择器只能”向下”选中子元素,没法根据子元素的状态反过来影响父元素样式,这种需求通常只能靠 JS 来判断和加类名。

比如”如果表单里有必填项没填,就给整个表单加个红色边框提醒”,现在直接写 CSS 就行:

form:has(input:invalid) {
  border: 2px solid red;
}

再比如”如果卡片里有图片,就调整一下卡片的排版”:

.card:has(img) {
  display: grid;
  grid-template-columns: 100px 1fr;
}

:has() 相当于给 CSS 提供了”父选择器”的能力,很多以前需要 JS 动态判断 DOM 结构再切换样式的场景,现在纯 CSS 就能搞定。

七、气泡提示和下拉菜单:Popover API

做过下拉菜单、气泡提示(tooltip)、右键菜单的都知道,这类”浮层”组件有一堆共同的麻烦事:怎么保证它显示在所有元素最上层不被遮挡、点击外部区域怎么关闭、怎么处理层级冲突。以前一般靠 JS 计算定位、监听全局点击事件来关闭。

现在有了原生的 Popover API:

<button popovertarget="myMenu">打开菜单</button>
<div id="myMenu" popover>
  <p>这是一个下拉菜单</p>
</div>

加上 popover 属性的元素,会被浏览器自动提升到一个独立的顶层图层,不用担心被其他元素的 z-indexoverflow: hidden 遮挡,点击外部区域也会自动关闭,这些逻辑都不用自己写了。

写在最后:什么时候还是得自己写 JS

看到这里你可能会问,那是不是以后都不用写 JS 了?当然不是。原生方案解决的都是相对通用、标准化的交互模式,一旦涉及到具体业务逻辑(比如复杂的表单联动、和后端接口配合的动态渲染、自定义的动画曲线),还是得靠 JS 来实现。

这篇文章想强调的是一个习惯性的动作:下次遇到需求,先别急着打开编辑器写代码,花两分钟搜一下”这个功能浏览器是不是已经原生支持了”。你会发现,很多曾经让人头疼的交互细节,浏览器早就替你想好了。少写代码不是偷懒,而是把精力留给真正需要动脑子的业务逻辑上。

浏览器这些年的进化速度,其实比很多人想象中要快得多。

© 版权声明

相关文章

暂无评论

none
暂无评论...