
我接手的老项目里,package.json 动不动就几百兆依赖,很多包装进去之后就再也没人动过。前阵子花时间梳理了一遍,发现浏览器原生 API 已经能 cover 不少场景了,这篇把踩过的坑和结论都记下来。
每个 API 我都在 Chrome DevTools 上跑过验证,也会说清楚它的 Baseline 浏览器支持情况。Baseline 的意思是:这个特性在所有主流浏览器最新版本里都能用。"Newly available"指刚上线不久,"Widely available"指已经稳定支持一阵子了。
不是要大家删光依赖。有些场景下库还是得用,但至少动手之前先看看平台已经提供了什么。
先看平台,再做决策。
structuredClone() 替代 lodash cloneDeep以前做深拷贝要么 JSON.parse(JSON.stringify(obj)),要么装个 lodash。JSON 方案的问题一堆:Date 对象变成字符串、Map 和 Set 内容丢、循环引用直接报错。
现在用 structuredClone(),能原生处理大多数场景:
// 之前
import cloneDeep from 'lodash/cloneDeep';
const copy = cloneDeep(user); // 现在
const copy = structuredClone(user);
支持 Date、Map、Set、Blob、ArrayBuffer、TypedArray,还有循环引用。
注意:函数和 DOM 节点没法 clone,会抛 DataCloneError。类的实例会丢失自定义原型链上的方法。如果你的代码依赖这些行为,cloneDeep 还是得留着。
支持情况:Baseline 2022 年 3 月起(Chrome 98、Firefox 94、Safari 15.4)。
crypto.randomUUID() 替代 uuiduuid 包功能挺多,但如果只是要一个标准 v4 UUID,浏览器自带的方法就够用了。
// 之前
import { v4 as uuidv4 } from 'uuid';
const id = uuidv4(); // 现在
const id = crypto.randomUUID();
// 返回格式: '36b8f84d-df4e-4d49-b662-bcde71a8764f'
生成标准 v4 UUID 就这么一行。
注意:只能生成 v4 版本,而且要求安全上下文(HTTPS 或 localhost)。如果需要 v1、v5、v7 等其他版本,uuid 包还是首选。
支持情况:Baseline 2022 年 3 月起。
fetch() + AbortSignal.timeout() 替代 axios(简单请求场景)对于普通 HTTP 请求,原生 fetch() 够用了。配合 AbortSignal.timeout() 还能方便地设置超时,不用自己写定时器。
// 之前
const { data } = await axios.get('/api/users', { timeout: 5000 }); // 现在
try { const res = await fetch('/api/users', { signal: AbortSignal.timeout(5000), }); if (!res.ok) throw new Error(`HTTP ${res.status}`); const data = await res.json();
} catch (err) { if (err.name === 'TimeoutError') { // 请求超过5秒 } else { throw err }
}
切换之前有几个差异得知道:
fetch() 不会因为服务器返回 404 或 500 就拒绝 promise,得自己检查 res.ok。TimeoutError,但 Chrome 103 到 123 版本抛的是 AbortError,上面的写法在那些版本里不生效。支持情况:AbortSignal.timeout() Baseline Newly available 2024 年 4 月(Chrome 124、Firefox 100、Safari 16),预计 2026 年 10 月成为 Widely available。
Intl.RelativeTimeFormat 替代 Moment(显示"多久以前")Moment 是个臃肿的日期库,官网自己都说它是 legacy 项目了。如果只是显示"3 分钟前"这种相对时间,原生 API 就能格式化。
不过 Intl.RelativeTimeFormat 不帮你算时间差和选单位,得自己写个小工具:
const DIVISIONS = [ { amount: 60, unit: 'second' }, { amount: 60, unit: 'minute' }, { amount: 24, unit: 'hour' }, { amount: 7, unit: 'day' }, { amount: 4.34524, unit: 'week' }, { amount: 12, unit: 'month' }, { amount: Infinity, unit: 'year' },
]; const rtf = new Intl.RelativeTimeFormat('en', { numeric: 'auto' }); function timeAgo(date) { let duration = (date - Date.now()) / 1000; for (const { amount, unit } of DIVISIONS) { if (Math.abs(duration) < amount) { return rtf.format(Math.round(duration), unit); } duration /= amount; }
} timeAgo(new Date(Date.now() - 3 * 60 * 1000)); // '3 minutes ago'
设置 numeric: 'auto' 会得到更自然的表达,比如"yesterday"而不是"1 day ago"。也支持国际化。
注意:这只管格式化相对时间。如果需要日期解析、日期运算或时区处理,专门的库还是得用。Moment 官方推荐考虑 Luxon、Day.js、date-fns 这些替代品。
支持情况:Baseline Widely available 2023 年 3 月起。
URLSearchParams 替代 qs 和 query-string(扁平查询字符串)如果只是构造或解析简单的 URL 查询字符串,可能不需要单独装包。
// 之前
import qs from 'qs';
const query = qs.stringify({ q: 'web apis', page: 2 }); // 现在
const params = new URLSearchParams({ q: 'web apis', page: 2 });
params.toString(); // 'q=web+apis&page=2' const url = new URL('https://example.com/search?q=web+apis&page=2');
url.searchParams.get('q'); // 'web apis'
URLSearchParams 自动处理参数编码解码。配合 URL 对象的 searchParams 属性可以读写已有 URL 的查询部分。
注意:不支持嵌套对象或数组的序列化,格式跟 qs 不一样。如果你的 API 要求括号 notation 或者特定嵌套格式,还是用对应的包吧。
支持情况:Baseline Widely available。
IntersectionObserver 替代滚动监听(检测元素可见性)想知道某个元素是否进入了视口,不用监听每次滚动事件去计算位置。IntersectionObserver 能在元素进入或离开视口时通知你,而且性能好很多。
const observer = new IntersectionObserver((entries) => { entries.forEach((entry) => { if (entry.isIntersecting) { entry.target.classList.add('visible'); observer.unobserve(entry.target); } });
}, { threshold: 0.2 }); document.querySelectorAll('.card').forEach((el) => observer.observe(el));
这个例子在卡片进入视口时添加 visible 类,然后停止观察该卡片。
threshold: 0.2 控制触发回调的时机。entry.isIntersecting 只告诉你元素是否与视口相交。如果要"至少 20% 可见",应该检查 entry.intersectionRatio >= 0.2。
常见用法:无限滚动、滚动触发动画、图片懒加载、可见性统计。
注意:如果只是要图片懒加载,<img loading="lazy"> 原生属性可能就够用了。React 等框架里,封装好的库还是能省不少 setup 代码。
支持情况:Baseline Widely available 2021 年 9 月(Chrome 58、Firefox 55、Safari 12.1)。
ResizeObserver 替代 window resize 监听(检测元素尺寸变化)window 的 resize 事件只告诉你窗口大小变了,但某个具体元素的大小变化它管不着。
ResizeObserver 就是干这个的。它直接监视指定元素,适用于图表、仪表盘、响应式组件等场景——这些元素的尺寸可能因为内容变化而改变,但窗口大小却没动。
const ro = new ResizeObserver((entries) => { for (const entry of entries) { console.log(entry.contentRect.width); }
}); ro.observe(element);
监视元素的宽度变化,每次变化时输出新的值。
注意:不要在 resize 回调里直接改被观察元素的尺寸,会触发死循环报警告。处理布局更新时要注意避免反复触发。
支持情况:Baseline Widely available 2023 年 1 月(Chrome 64、Firefox 69、Safari 13.1)。
很多人打开一个页面就去干别的了。如果你的应用还在后台不停轮询或跑不必要的任务,纯属浪费资源。
Page Visibility API 让你在文档隐藏或显示时做出响应:
const video = document.querySelector('video'); document.addEventListener('visibilitychange', () => { if (document.hidden) { video.pause(); // 停止轮询 } else { // 页面重新可见:恢复或刷新数据 }
});
可以暂停视频、停止轮询、减少页面不可见时的 CPU 和电量消耗。
注意:页面隐藏不代表用户离开了你的应用,可能只是切换到了其他标签页。用这个 API 管理后台工作还行,但别拿它判断会话是否结束。
支持情况:早已确立,所有主流浏览器都支持。
BroadcastChannel 替代 localStorage 事件方案(跨标签页通信)同步浏览器标签页有个土办法:写数据到 localStorage 然后监听 storage 事件。能用,但有更专业的 API:BroadcastChannel。
// 标签页 1
const channel = new BroadcastChannel('app-sync');
channel.postMessage({ type: 'theme-changed', theme: 'dark' }); // 标签页 2
const channel = new BroadcastChannel('app-sync');
channel.onmessage = (event) => { console.log(event.data); // { type: 'theme-changed', theme: 'dark' }
};
使用相同频道名的上下文可以互相发消息。标签页、窗口、iframe、worker 之间都能通信,只要同源且存储分区相同。
典型场景:用户退出登录时,所有打开的标签页一起下线。
注意:消息只会发到同一个存储分区的上下文。比如你的页面里嵌了另一个网站的 iframe,那个 iframe 没法通过这个 API 和该网站的普通标签页通信,即使它们同源也不行。
支持情况:Baseline 2022 年 3 月(Chrome 54、Firefox 38、Safari 15.4)。
document.execCommand('copy') 和 clipboard.js(复制文本)如果只是要一个"复制"按钮,现代 Clipboard API 无需任何库就能搞定。
copyButton.addEventListener('click', async () =>