JS 代码技巧 vol.7 --- 20 个浏览器 API 实战,自带 API 能干的事别自己封装
小不的代码技巧系列第 7 期 主题:浏览器 API 实战 这次盘 20 个------每个都对比"老路子自己封" vs "现代浏览器自带 API",附兼容性表,看完就知道啥时候该用啥 😎 20 个技巧一次打包,分网络、监听、剪贴板/分享、存储、性能、UI/DOM 六组。
哈喽哇!我是小不 ,不简说的不~
作为一个在代码界"翻车"无数次的选手,我算是看明白了:坑这东西吧,要么不踩,踩就踩大的😂
今天这期,主打一个 浏览器 API 实战------不整虚的,全是实战里哭出来的教训。看不看随你~反正翻车实录又不收钱
Ps:文末有惊喜(不是广告!)
一、网络请求(4 个)
1. fetch vs XMLHttpRequest
老路子:XHR 一堆回调地狱,abort 要自己写。
js
// ❌ 老路子:XMLHttpRequest
const xhr = new XMLHttpRequest();
xhr.onreadystatechange = function () {
if (xhr.readyState === 4 && xhr.status === 200) {
console.log(xhr.responseText);
}
};
xhr.open("GET", url);
xhr.send();
现代:fetch + Promise,链式清爽。
js
const res = await fetch(url);
const data = await res.json();
兼容:Chrome 42+ / Firefox 39+ / Safari 10.1+(2017+),Node 18+ 原生支持。
质疑 :如果说 axios,或者第三方库 ? 那么请仔细阅读 axios 文档
坑在哪 :fetch 默认不带 cookie (要 credentials: 'include'),404 不会 reject (要 res.ok 判断)。老 IE 直接不支持------要 XHR 或 polyfill。
2. AbortController vs 自己写 cancel 标志
老路子:自己定义 cancelled flag,函数里到处检查。
js
// ❌ 老路子
let cancelled = false;
async function fetchData() {
for (const id of ids) {
if (cancelled) return;
await fetch(`/api/${id}`);
}
}
fetchData();
// 想取消?fetchData.cancel = () => cancelled = true;
现代:浏览器自带 AbortController,fetch 原生支持。
js
const controller = new AbortController();
setTimeout(() => controller.abort(), 5000);
try {
const res = await fetch(url, { signal: controller.signal });
} catch (e) {
if (e.name === "AbortError") console.log("主动取消");
}
兼容:Chrome 66+ / Firefox 57+ / Safari 11.1+(2018+),Node 15+。
坑在哪 :不是所有 API 都支持 signal(axios 老版本要适配器)。React 组件 unmount 时记得 abort,否则 setState 警告满天飞。
3. navigator.sendBeacon vs fetch for analytics
老路子:用户关闭页面前,发个请求记录行为------但 fetch / XHR 在 unload 时经常被浏览器掐断。
js
// ❌ fetch 在 unload 时经常被砍
window.addEventListener("unload", () => {
fetch("/log", { method: "POST", body: JSON.stringify(data) });
});
现代:sendBeacon 是浏览器专给"埋点/日志"设计的,保证送达。
js
window.addEventListener("unload", () => {
navigator.sendBeacon("/log", JSON.stringify(data));
});
兼容:Chrome 39+ / Firefox 31+ / Safari 11.1+(2018+)。
坑在哪 :只能发 POST,body 限制 64KB 。要拿响应?看 fetch 而不是 sendBeacon。
4. URL / URLSearchParams vs 正则解析 query
老路子:手写正则拆 query,或者 split('&')。
js
// ❌ 老路子
function getQuery(name) {
const m = location.search.match(new RegExp(`[?&]${name}=([^&]*)`));
return m ? decodeURIComponent(m[1]) : null;
}
现代:浏览器自带 URLSearchParams,开箱即用。
js
const params = new URLSearchParams(location.search);
params.get("name"); // '小不'
params.getAll("tag"); // ['js', 'ts']
params.toString(); // 反向序列化
兼容:Chrome 49+ / Firefox 44+ / Safari 10+(2016+),Node 10+。
坑在哪 :URL 含 hash 时 URLSearchParams 不解析 hash 后的内容。需要完整 URL 对象用 new URL(href)。
二、元素监听(4 个)
5. IntersectionObserver vs scroll + getBoundingClientRect
老路子:scroll 事件 + 手动算位置判断元素是否在视口。
js
// ❌ 老路子:每滚一下都跑回调,性能差
window.addEventListener("scroll", () => {
imgs.forEach((img) => {
const rect = img.getBoundingClientRect();
if (rect.top < window.innerHeight) img.src = img.dataset.src;
});
});
现代:浏览器自己算交叉,事件触发时机由浏览器决定,性能高。
js
const io = new IntersectionObserver((entries) => {
entries.forEach((e) => {
if (e.isIntersecting) {
e.target.src = e.target.dataset.src;
io.unobserve(e.target);
}
});
});
imgs.forEach((img) => io.observe(img));
兼容:Chrome 51+ / Firefox 55+ / Safari 12.1+(2017+)。
坑在哪:iframe 内有时不准确;首屏图片别懒加载(拖慢 LCP)。
6. ResizeObserver vs window resize
老路子:监听 window resize 事件,手动判断元素尺寸变化。
js
// ❌ 老路子
window.addEventListener("resize", () => {
const w = box.offsetWidth;
if (w !== lastW) doSomething(w);
});
现代:观察任意元素 尺寸变化,窗口不变元素变也能监听到。
js
const ro = new ResizeObserver((entries) => {
for (const e of entries) {
console.log(e.contentRect.width, e.contentRect.height);
}
});
ro.observe(box);
兼容:Chrome 64+ / Firefox 69+ / Safari 13.1+(2019+)。
坑在哪 :回调触发频率可能很高,内部用 rAF 节流或自己 debounce。
7. MutationObserver vs DOM Mutation Events
老路子:DOMNodeInserted / DOMAttrModified 一类事件------已废弃,性能还差。
js
// ❌ 老路子:DOMNodeInserted 已废弃
container.addEventListener("DOMNodeInserted", (e) => {
console.log("新节点:", e.target);
});
现代:MutationObserver 是规范接班人,批量异步触发,性能好得多。
js
const mo = new MutationObserver((records) => {
records.forEach((r) => {
if (r.type === "childList") console.log("子节点变了", r.addedNodes);
else if (r.type === "attributes") console.log("属性变了", r.attributeName);
});
});
mo.observe(container, { childList: true, attributes: true, subtree: true });
兼容:Chrome 26+ / Firefox 14+ / Safari 7+(2013+)------ 老古董 API 了,放心用。
坑在哪 :回调拿到的 record 不一定包含完整新节点内容,读最新值要重新 query。
8. Page Visibility API vs blur / focus
老路子:用 window 的 blur/focus 判断用户切走。
js
// ❌ 老路子:blur/focus 在 tab 切换时不靠谱
window.addEventListener("blur", () => pause());
window.addEventListener("focus", () => resume());
现代:visibilitychange 直接告诉你"页面看不看得见"。
js
document.addEventListener("visibilitychange", () => {
if (document.hidden) pauseVideo();
else resumeVideo();
});
兼容:Chrome 33+ / Firefox 18+ / Safari 7+(2013+)。
坑在哪 :移动端切后台一定触发;桌面端切 tab 触发。但是锁屏/最小化不一定 ------结合 blur 兜底更稳。
三、剪贴板 / 分享(3 个)
9. Clipboard API vs document.execCommand('copy')
老路子:execCommand 一把梭,但已废弃,Safari 还经常弹"是否允许"。
js
// ❌ 老路子:已废弃
const input = document.createElement("input");
input.value = "要复制的文字";
document.body.appendChild(input);
input.select();
document.execCommand("copy");
现代:异步 + Promise,干净利落。
js
await navigator.clipboard.writeText("要复制的文字");
const text = await navigator.clipboard.readText();
兼容:Chrome 66+ / Firefox 63+ / Safari 13.1+(2019+)。
坑在哪 :必须在 HTTPS 或 localhost(http 页面直接没权限)。用户首次粘贴还要点"允许"。
10. navigator.share vs 自己写分享菜单
老路子:弹个自己的 modal,列微信/QQ/微博图标,调用对应 SDK。
js
// ❌ 老路子:自己写分享菜单 + 调各家 SDK
showShareModal({ title, url, desc });
// 然后:wx.share({...}), qq.share({...}), wb.share({...})...
现代:调起系统原生分享面板------Android 直接弹分享菜单,iOS 弹分享 sheet。
js
navigator.share({
title: "JS 代码技巧 vol.7",
text: "浏览器 API 实战,自带 API 能干的事别自己封装",
url: "https://example.com/post/7",
});
兼容 :Chrome 89+(Android)/ Safari 12+(iOS),桌面浏览器普遍不支持。
坑在哪 :桌面 Chrome / Firefox 不支持------fallback 到自己写的分享菜单。HTTPS 必装。
11. Fullscreen API vs 模拟全屏
老路子:把 position fixed 拉满 100vw/100vh,"看起来"全屏------但地址栏还在。
js
// ❌ 老路子:假全屏
video.style.position = "fixed";
video.style.top = 0;
video.style.left = 0;
video.style.width = "100vw";
video.style.height = "100vh";
现代:requestFullscreen 真·全屏(地址栏、任务栏都消失)。
js
video.requestFullscreen();
// 退出
document.exitFullscreen();
兼容:Chrome 71+ / Firefox 64+ / Safari 13.1+(2019+)。
坑在哪 :必须用户手势触发 (click 回调里直接调,setTimeout 里调会被拒)。element.requestFullscreen 不是 Element.fullscreen()------拼错不报错。
四、存储(3 个)
12. localStorage / sessionStorage vs cookie
老路子:存点小数据用 cookie,结果每个请求都带着------4KB 限制,恶心。
js
// ❌ 老路子:cookie 同步存 + 每次请求自动带
document.cookie = "token=abc; path=/";
// 后端每次都拿到,前端还要 decode
现代:localStorage 存客户端数据,不参与 HTTP 请求。
js
localStorage.setItem("token", "abc");
localStorage.getItem("token");
sessionStorage.setItem("tmp", "value"); // 关 tab 消失
兼容 :全平台支持,IE8+ 都有。唯一限制 ~5MB。
坑在哪 :同步 API ------大 JSON.parse 会阻塞主线程。隐私模式可能被禁用 。XSS 风险------别存敏感数据。
13. IndexedDB vs localStorage 大数据
老路子:硬塞 localStorage,结果 5MB 限制爆了。
js
// ❌ 老路子:localStorage 装 50MB 缓存
localStorage.setItem("cache", JSON.stringify(hugeData));
// 浏览器:QuotaExceededError
现代:IndexedDB 是正经的客户端数据库------GB 级别,异步,索引查询。
js
// 简化示意:实际用 idb / dexie 库
const db = await openDB("myapp", 1, {
upgrade(db) {
db.createObjectStore("users", { keyPath: "id" });
},
});
await db.put("users", { id: 1, name: "小不" });
兼容:全平台,IE10+(有限制)。
坑在哪 :API 极其难用 ------callback 地狱老古董。生产用 Dexie.js 或 idb 库封装 。事务 model 跟 SQL 不一样。
14. navigator.storage.estimate() 配额监控
老路子:用户存储爆了才报"磁盘满"------排查半天。
js
// ❌ 老路子:瞎写
for (let i = 0; i < 1000000; i++) {
localStorage.setItem(`k${i}`, "v".repeat(1000));
}
// 浏览器:QuotaExceededError
现代:浏览器自带配额查询。
js
const { quota, usage } = await navigator.storage.estimate();
console.log(`已用 ${usage} / ${quota}`);
// 用户即将爆存储时:先删旧数据
兼容:Chrome 61+ / Firefox 57+ / Safari 16.4+(2023+)。
坑在哪 :Safari 老版本不支持。返回的 quota 是估算值,实际可用更小。
五、性能监控(3 个)
15. PerformanceObserver vs Navigation Timing
老路子:在 window.onload 里 performance.timing 算首屏------但这个 API 已废弃。
js
// ❌ 老路子:performance.timing 已废弃
const t = performance.timing;
console.log(t.loadEventEnd - t.navigationStart);
现代:PerformanceObserver 监听各种性能条目,实时异步。
js
const po = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
console.log(entry.name, entry.duration);
}
});
po.observe({ type: "longtask", buffered: true });
// longtask / paint / resource / measure / navigation...
兼容:Chrome 52+ / Firefox 57+ / Safari 11+(2017+)。
坑在哪 :要 buffered: true 才能拿到observe 之前的记录。不同 type 兼容性不同,老浏览器要 feature detect。
16. requestIdleCallback vs setTimeout(0)
老路子:把低优先级任务塞 setTimeout(0),以为浏览器会安排在空闲时。
js
// ❌ 老路子:setTimeout(0) 浏览器不保证"空闲"
setTimeout(() => logAnalytics(), 0);
现代:requestIdleCallback 真·空闲回调------浏览器告诉你"现在不忙"。
js
requestIdleCallback((deadline) => {
// deadline.timeRemaining() 是剩余毫秒
while (deadline.timeRemaining() > 0) {
doBackgroundWork();
}
});
兼容 :Chrome 47+ / Firefox 55+ / Safari 不支持。
坑在哪 :Safari 完全不支持 ------需要 polyfill。超时 fallback 用 setTimeout:
js
const ric = window.requestIdleCallback || ((fn) => setTimeout(fn, 0));
17. requestAnimationFrame vs setTimeout(16ms)
老路子:用 setTimeout(fn, 16) 模拟 60fps。
js
// ❌ 老路子:跟显示器刷新率不同步,可能掉帧
function tick() {
animate();
setTimeout(tick, 16);
}
现代:requestAnimationFrame 跟显示器刷新率同步------60Hz、120Hz、144Hz 都跟。
js
function tick() {
animate();
requestAnimationFrame(tick);
}
requestAnimationFrame(tick);
兼容:全平台,IE10+。
坑在哪 :tab 切到后台就暂停 (这是 feature,不是 bug)。别用来做心跳包------关键时刻掉链子。
六、UI / DOM(3 个)
18. <dialog> 元素 vs 自己写 modal
老路子:自己 div + position fixed + z-index + ESC 关闭 + 遮罩 + focus trap------50 行起步。
js
// ❌ 老路子:自己写 modal
const modal = document.createElement("div");
modal.style.cssText = "position:fixed;inset:0;z-index:9999;...";
// + ESC 监听 + 遮罩点击关闭 + focus trap + a11y...
现代:<dialog> 元素原生支持 modal,focus trap、ESC、遮罩全内置。
html
<dialog id="m">
<h2>弹窗标题</h2>
<p>内容</p>
<button onclick="m.close()">关闭</button>
</dialog>
js
m.showModal(); // 真 modal,带遮罩 + focus trap
m.close();
兼容 :Chrome 37+ / Firefox 98+ / Safari 15.4+(2022+)。老 Safari 要 polyfill (用 <dialog-polyfill>)。
坑在哪 :原生 <dialog> 的样式默认丑 ------::backdrop 改背景。form 集成要 method="dialog"。
19. Element.checkVisibility() vs getBoundingClientRect
老路子:判断元素是否真的可见------getBoundingClientRect + offsetParent + display: none 检查 + ... 写一堆。
js
// ❌ 老路子
function isVisible(el) {
const rect = el.getBoundingClientRect();
const style = getComputedStyle(el);
return rect.width > 0 && rect.height > 0 && style.display !== "none";
// 还有 visibility、opacity、clip-path、content-visibility...
}
现代:el.checkVisibility() 一行------浏览器帮你判断所有可见性。
js
const visible = el.checkVisibility({
checkOpacity: true, // opacity: 0 也算不可见
checkVisibilityCSS: true, // visibility: hidden
contentVisibilityAuto: true, // content-visibility: auto skip
});
兼容:Chrome 105+ / Firefox 106+ / Safari 17.4+(2023+)------ 比较新。
坑在哪 :新 API,老项目别用。需要兼容的话,老路子判断要写完整。
20. Element.animate() vs setInterval 动画
老路子:setInterval 改 style.left,30 行起步,还得自己管时间轴。
js
// ❌ 老路子
let pos = 0;
const timer = setInterval(() => {
pos += 5;
box.style.transform = `translateX(${pos}px)`;
if (pos > 300) clearInterval(timer);
}, 16);
现代:Element.animate() 走浏览器 compositor,GPU 加速,性能拉满。
js
box.animate(
[{ transform: "translateX(0)" }, { transform: "translateX(300px)" }],
{ duration: 1000, easing: "ease-out", fill: "forwards" },
);
兼容:Chrome 36+ / Firefox 48+ / Safari 13.1+(2019+)。
坑在哪 :返回 Animation 对象,要暂停/取消拿引用。fill: 'forwards' 不加动画结束就回弹------踩过这个坑的应该不止我一个。
📦 收个尾
这 20 个技巧浓缩一下:
- 最高频踩坑:Clipboard 忘了 HTTPS、IntersectionObserver iframe 内失效、setTimeout 模拟 60fps 掉帧
- 最值得收藏 :fetch + AbortController、URLSearchParams、
<dialog>元素 - 核心思路:能不用自己封装的,就别自己封装------浏览器自己实现的,通常比你写的又快又稳
学到了就是赚到了,犹豫徘徊等于白来~
写到最后
想要啥技巧?评论区甩个题目过来~
- 你刚踩的坑
- 项目里反复写的代码
- 想搞清楚但一直懒得查的 API
小不看到...不一定回 😂 毕竟代码里翻车太多,腾不出手~
Ps:三连随缘,催更的会被打 😂