普通视图

发现新文章,点击刷新页面。
今天 — 2026年4月26日首页

不经过服务器,两个人怎么直接通话?

作者 牛奶
2026年4月25日 09:18

你有没有想过:微信视频、腾讯会议为什么延迟那么低?两个人在不同的城市,怎么才能不经过服务器直接"喊到"对方?

今天,用对讲机的故事,来讲讲浏览器里的 P2P 通信。


原文地址

墨渊书肆/不经过服务器,两个人怎么直接通话?


从打电话说起

传统电话是怎么打的?

想象一下,你要给远在北京的朋友打电话。

传统电话的流程是这样的:

你(北京) ──> 上海总机  ──> 北京总机 <──  朋友
                
            所有声音都要
            经过总机转发

所有的声音数据,都要经过电话交换机的"总机"。如果总机挂了,所有人都会断线。

这有什么问题?

问题 举个例子
延迟高 声音绕远路,本来 A 到 B 只要 10ms,经过总机变成 100ms
成本高 10 万人同时打电话,总机带宽费用吓人
隐私差 总机理论上可以听到所有通话内容

如果不想经过总机呢?

有一种设备叫对讲机

  ════════════  朋友
   按住按钮说话
   松开按钮听对方说

对讲机不需要任何中间设备,两个人可以直接通话。

那如果把这个概念搬到互联网上呢?


P2P:互联网上的"对讲机"

什么是 P2P?

P2P(Peer to Peer)= 点对点直连。

简单说,就是两个人直接交换数据,不经过中间服务器

传统模式:                       
用户A  ──>  服务器  <──  用户B                              

P2P 模式:
用户A  ═══════════════  用户B

就像两个人用两根竹竿直接连起来——敲敲竹竿,对方就能听到。

P2P 的优势

优势 说明
延迟低 数据直连,少绕路
成本低 服务器只需要交换"暗号",不传数据
隐私好 服务器只知道"你们在通话",不知道内容

WebRTC:浏览器里的 P2P 神器

什么是 WebRTC?

WebRTC(Web Real-Time Communication)= 网页实时通信。

它是浏览器内置的 P2P 通信 API,让网页可以直接进行音视频通话、数据传输,不需要安装任何插件

WebRTC 能做的事:
┌───────────────────────────────┐
  📞 视频通话   - 像微信视频一样   
  📁 文件传输   - P2P 传大文件    
  🎮 游戏数据   - 实时对战游戏     
  📝 实时协作   - 白板、文档协作   
└───────────────────────────────┘

WebRTC 核心 API

WebRTC 有三个核心 API:

API 作用 举个例子
MediaStream 采集摄像头/麦克风的音视频 getUserMedia({video: true}) 获取视频流
RTCPeerConnection 建立和管理 P2P 连接 创建连接、交换 Offer/Answer、处理 ICE 候选
DataChannel 建立双向数据通道 传文件、传游戏操作、聊天消息

举个好理解的例子

三个人要视频通话,就像这样分工:

┌──────────────────────────────────┐
  MediaStream = 摄像师             
  (负责采集画面和声音)              
                                  
  RTCPeerConnection = 总调度       
  (负责交换信息、建立连接、传输数据)  
                                  
  DataChannel = 快递员             
  (负责传递文件、游戏数据等额外信息)  
└──────────────────────────────────┘

兼容性

浏览器 支持情况
Chrome ✅ 56+
Firefox ✅ 44+
Safari ✅ 11+
Edge ✅ 79+

💡 桌面端兼容性很好,移动端 Safari 支持较弱,实际项目建议做降级处理。


P2P 的难点:NAT 是什么?

故事:为什么找不到你?

假设你想和朋友 P2P 通话,你知道朋友的电脑 IP 是 192.168.1.100。你直接往这个地址发数据,能发到吗?

发不到。

因为 192.168.1.100内网 IP,只有在你家路由器内部才有效。就像:

🏢 大楼前台 = NAT(网络地址转换)

你的房间号:888(内网 IP:192.168.1.100)
前台改成了:大厅 555(公网 IP:118.123.45.67)

外面的人只知道大厅 555,怎么知道你在 888 房间?

NAT 就像一堵墙,挡住了外面的人直接找到你。

NAT 的四种类型

NAT 有四种"严格程度",就像不同的前台规则:

NAT 类型 规则有多严 打个比方
全圆锥型 最松 任何人都能通过前台找到你
受限圆锥型 较严 只有你联系过的人才能通过前台找到你
端口受限圆锥型 更严 只有你联系过的具体端口才能找到你
对称型 最严 每次联系不同的人,前台都给你分配不同的端口

对称型 NAT 最难穿透,因为 STUN 探测出来的地址,换个人可能就用不了了。


解决方案:STUN / TURN / ICE

STUN:探测自己在外的"长相"

STUN = NAT 会话遍历工具。

它的作用很简单:告诉你,你在外面看起来是什么地址

就像你问前台:"我在外面看起来是多少号?"

 ────> STUN 服务器:"我在外面看起来是多少?"
                    
                    
        "你在外网是 118.123.45.67:55555"

STUN 是怎么工作的?

  1. 你给 STUN 服务器发一个请求
  2. STUN 服务器看看这个请求是从哪个公网地址来的
  3. STUN 服务器告诉你:"你在外面是 XXX.XXX.XXX.XXX:YYYY"
  4. 你把这个地址告诉朋友,朋友就能直接找到你了

STUN 能解决什么问题?

场景 能打通吗?
双方都在公网 ✅ 可以
一方在内网,一方在公网 ✅ 可以
双方都在不同内网 ❌ 不一定

TURN:最后的"中转站"

TURN = 通过中继绕过 NAT。

当 STUN 穿透失败时,TURN 服务器充当中转站

用户A  ────>  TURN 服务器  <───>  用户B
              数据中转

TURN 是怎么工作的?

  1. 双方都连接 TURN 服务器
  2. 发给对方的数据,先发到 TURN 服务器
  3. TURN 服务器再转发给对方

⚠️ TURN 只是保底方案,所有数据都要经过它,会产生带宽成本——就像打电话都要经过总机一样。

ICE:自动选择最优方案

ICE = 交互式连接建立。

WebRTC 会同时尝试多种连接方式,自动选择最优的:

优先级 连接方式 什么时候用
1 直连 双方都能直接找到对方(都在公网)
2 STUN 穿透 一方在内网,但 NAT 不太严格
3 TURN 中转 双方都在严格 NAT 后面,只能中转

ICE 的工作流程

ICE 尝试连接:
┌───────────────────────────────┐
  1. 收集候选地址                
     - 本地 IP                  
     - STUN 探测出的公网 IP      
     - TURN 服务器分配的地址      
                               
  2. 对所有候选配对进行连通性检查   
                               
  3. 按优先级排序,选最快的        
└───────────────────────────────┘

简单说:能直连就直连,不能直连就穿透,穿不透就中转,WebRTC 自动搞定一切。


信令机制:通话前的"暗号交换"

好,现在假设 A 和 B 都知道对方的公网地址了。

可以直接打电话了吗?

还不行。

在真正通话之前,他们还需要交换一些"暗号"——比如用什么编码、怎么加密等。这个交换"暗号"的过程,就叫做信令(Signaling)

Offer 和 Answer 是什么?

Offer 和 Answer 里装的是 SDP(Session Description Protocol,会话描述协议)。

SDP 就是一份"自我介绍清单":

SDP 包含的内容:
├── 我支持什么音视频编码?(H.264、VP8、VP9、OPUS...)
├── 我的音视频轨道信息(有几路视频、几路音频)
├── 我的网络地址候选(IP:端口)
├── 带宽限制(最多能用多少带宽)
└── 安全加密方式(用什么加密算法)
  • Offer:发起方的自我介绍清单
  • Answer:接收方的回应清单,表示"我能接受这些格式,这是我的信息"

信令交换流程

信令交换流程:
┌─────────┐                      ┌─────────┐
  用户A                           用户B   
└────┬────┘                      └────┬────┘
       1. A 创建 Offer(SDP)        
      ───────────────────────────────>
                                     
       2. B 收到 Offer,返回 Answer   
      <───────────────────────────────
                                     
       3. 双方交换 ICE 候选地址        
      <============================= >
     │                                │
     │  4. P2P 连接建立成功!🎉        │

💡 WebRTC 没有定义信令服务器——你可以用 WebSocket、HTTP、甚至 Email,什么方式都行。因为信令只是"暗号",不需要特殊协议。


核心代码:最小示例

WebRTC 代码比较复杂,这里展示最核心的部分:

1. 创建连接

const pc = new RTCPeerConnection({
  iceServers: [{ urls: 'stun:stun.l.google.com:19302' }]
});

这行代码做了三件事:

  1. 创建了一个 P2P 连接
  2. 配置了 STUN 服务器(用来探测公网地址)
  3. 返回一个连接对象,后面所有的操作都围绕这个对象

2. 交换 Offer/Answer

// A:创建 Offer
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
sendToPeer(offer);  // 发送 Offer 给 B

// B:收到 Offer,设置远程描述,返回 Answer
await pc.setRemoteDescription(offer);
const answer = await pc.createAnswer();
await pc.setLocalDescription(answer);
sendToPeer(answer);  // 发送 Answer 给 A

这里的关键是:

  • createOffer() / createAnswer() 生成 SDP
  • setLocalDescription() 设置本地描述(自己的信息)
  • setRemoteDescription() 设置远程描述(对方的信息)

3. 处理 ICE 候选

// 收到对方的候选地址,加入连接
await pc.addIceCandidate(candidate);

ICE 候选就是"候选的网络地址",可能来自 STUN 探测,也可能来自 TURN 分配。

💡 完整的通话还需要获取麦克风/摄像头、处理对方的音视频轨道等,但原理就是这样:交换 Offer/Answer → 交换 ICE 候选 → 连接建立。


DataChannel:不仅仅是通话

WebRTC 不只能传输音视频,还能传输任意数据

DataChannel 和 WebSocket 的区别

特性 DataChannel WebSocket
底层协议 UDP TCP
延迟 更低 较高
可靠性 可配置(可靠/不可靠) 可靠
适用场景 游戏实时操作 聊天、推送

举个例子

  • 打实时游戏:操控延迟要求极高,用 DataChannel(UDP)
  • 聊天消息:不能丢消息,用 WebSocket(TCP)

DataChannel 能做什么

场景 说明
游戏数据传输 操控指令、位置同步
文件共享 P2P 传大文件,不限速
实时协作 白板、文档协作
// 创建数据通道
const dc = pc.createDataChannel('myData');

// 发送消息
dc.send('Hello, P2P!');

// 接收消息
dc.onmessage = (event) => console.log('收到:', event.data);

总结

WebRTC 连接流程

完整流程:
┌──────────────────────────────────────┐
  1. 双方交换公网地址(STUN 探测)     
  2. 交换 Offer/Answer(SDP 信令)   
  3. 交换 ICE 候选                   
  4. NAT 穿透建立连接                
  5. 开始 P2P 传输                   
└──────────────────────────────────────┘

为什么 WebRTC 延迟低?

原因 说明
直连 数据不经过服务器,少跳转
UDP 不用等"确认收到",更快
专用协议 SRTP 比 HTTP 更轻量

使用建议

 适合的场景:
- 视频通话、在线会议
- 游戏、直播连麦
- 文件传输、白板协作

⚠️ 注意:
- 移动端兼容性较差
- 复杂内网可能需要 TURN 中转
- 需要自己实现信令服务器

 不适合:
- 老浏览器(不支持 WebRTC)
- 需要录制存档(数据不过服务器,无法保存)

写在最后

现在你知道了:

  • P2P 就是两个人直接通话,不经过服务器
  • NAT 是内网设备上网的"前台",会挡住外面的人
  • STUN 帮你探测自己在外的"长相"
  • TURN 是最后的"中转站"
  • ICE 自动选择最优连接方式
  • 信令 是通话前的"暗号交换",交换的是 SDP
  • WebRTC 不只是通话,还能传文件、做游戏

下次你用微信视频时,可以想想:这么低的延迟,背后就是 P2P 和 WebRTC 在起作用——两个人就像拿着对讲机,直接喊到对方。

📞 _"喂,你听得到吗?" —— 这可能是人类历史上最早实现 P2P 直连的两次通话之一。

昨天以前首页

浏览器藏了这么多神器,你居然不知道?

作者 牛奶
2026年4月22日 16:33

你以为浏览器只能打开网页?打开控制台敲 console.log?其实浏览器早就给你准备好了各种"神器"——只是 99% 的人从来没注意过。这些 API 之所以存在,是因为浏览器在发展过程中遇到了真实的问题。

今天,用工具箱的故事,来讲讲浏览器内置的 Web API。


原文地址

墨渊书肆/浏览器藏了这么多神器,你居然不知道?


浏览器是个工具箱

为什么浏览器要内置这么多 API?

浏览器就像一个万能工具箱

  • 木工要锤子、锯子、螺丝刀
  • 前端要检测元素、监控性能、访问硬件

浏览器面临的场景越来越复杂:

  • 检测页面何时可见/隐藏
  • 监听某个元素何时出现在屏幕
  • 监听 DOM 元素的大小变化
  • 监控网页性能数据
  • 访问电池、位置、剪贴板等硬件

所以浏览器厂商(Google、Mozilla、Apple)就把常见需求做成标准 API,让开发者直接用。

兼容性问题

在开始之前,先说个重要的事:不是所有 API 所有浏览器都支持

API Chrome Firefox Safari Edge
IntersectionObserver ✅ 51+ ✅ 55+ ✅ 12.1+ ✅ 79+
MutationObserver ✅ 18+ ✅ 14+ ✅ 6+ ✅ 12+
ResizeObserver ✅ 64+ ✅ 31+ ✅ 13.1+ ✅ 79+
PerformanceObserver ✅ 56+ ✅ 58+ ✅ 11+ ✅ 79+
Page Visibility API ✅ 33+ ✅ 18+ ✅ 6.1+ ✅ 12+
Battery API ✅ 50+ ✅ 10+ ❌ 不支持 ✅ 79+
Clipboard API ✅ 66+ ✅ 63+ ✅ 13.1+ ✅ 79+
Geolocation API ✅ 5+ ✅ 3.5+ ✅ 3+ ✅ 12+

注意:Battery API 在 Safari 和大多数移动浏览器上不支持,使用时要做好兼容处理。


IntersectionObserver — 懒加载和无限滚动的救星

故事的起因:scroll 事件太慢了

十年前,前端要检测"某个元素是否出现在屏幕上",只能这样写:

window.addEventListener('scroll', () => {
  const rect = element.getBoundingClientRect();
  if (rect.top < window.innerHeight) {
    loadImage();
  }
});

这段代码有什么问题?

问题 说明
性能差 scroll 事件每秒触发几十次,每次都计算 rect
无法批量 多个元素要写多个监听
滚动卡顿 计算太频繁,导致页面卡

就像门口站了个保安:每进来一个人,他都要站起来看一眼是不是 VIP——累死了。

IntersectionObserver 的原理

IntersectionObserver = 交叉观察器。

浏览器提供了这个 API,让你能高效地检测元素是否进入视口

原理很简单:

IntersectionObserver 工作原理:
┌────────────────────────────┐
       视口(Viewport)      
  ┌────────────────────┐    
       target 元素         
                          
  └────────────────────┘    
└────────────────────────────┘
          
   元素进入视口?浏览器自动通知你

基本用法

const observer = new IntersectionObserver((entries) => {
  entries.forEach(entry => {
    console.log('元素可见性:', entry.isIntersecting);
    console.log('交叉比例:', entry.intersectionRatio);
  });
}, {
  root: null,              // null = 浏览器视口
  rootMargin: '0px',       // 扩大/缩小检测区域
  threshold: 0.5            // 50% 可见时触发
});

observer.observe(element);

配置参数详解

参数 作用 示例
root 参考视口 null=浏览器窗口,element=某个容器
rootMargin 视口的扩展区域 '100px'=提前100px就触发
threshold 触发时机 0=刚出现,0.5=一半可见,1=完全可见

实际应用:懒加载图片

这是最经典的应用场景:

<img data-src="real-image.jpg" class="lazy" alt="加载中...">
<img data-src="real-image2.jpg" class="lazy" alt="加载中...">

<script>
  const images = document.querySelectorAll('.lazy');

  const observer = new IntersectionObserver((entries) => {
    entries.forEach(entry => {
      if (entry.isIntersecting) {
        const img = entry.target;
        img.src = img.dataset.src;      // 加载真实图片
        img.classList.remove('lazy');    // 移除占位符样式
        observer.unobserve(img);         // 加载完停止观察
      }
    });
  }, {
    rootMargin: '100px'  // 提前 100px 开始加载
  });

  images.forEach(img => observer.observe(img));
</script>

实际应用:无限滚动

电商网站、社交媒体最常用的"加载更多":

const observer = new IntersectionObserver((entries) => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      loadMoreItems().then(() => {
        // 新内容加载后,继续观察新的 loading 元素
        observer.observe(document.querySelector('.loading'));
      });
    }
  });
}, {
  rootMargin: '200px'  // 距离底部 200px 就开始加载
});

observer.observe(document.querySelector('.loading'));

MutationObserver — DOM 变化探测器

故事的起因:jQuery 时代的噩梦

很久以前,前端要监听 DOM 变化是这样写的:

$('#container').bind('DOMNodeInserted', function() {
  console.log('有新内容了');
});

后来有了 MutationEvent

document.addEventListener('DOMNodeInserted', (e) => {
  console.log('有新节点:', e.target);
});

但这些事件有严重问题:

问题 说明
性能灾难 每次 DOM 变化都触发,变化多了直接卡死
不准确 不能区分是内容变了还是属性变了
已废弃 现代浏览器不再推荐使用

MutationObserver 的原理

MutationObserver = 变化观察器。

浏览器用它来高效地监听 DOM 变化,变化会被批量收集,一次性通知你。

MutationObserver 原理:
┌──────────────────────────────┐
           DOM              
    <div id="app">            
      <p>你好</p>    变化     
    </div>                    
└──────────────────────────────┘
          
变化被收集  批量通知  一次回调

基本用法

const observer = new MutationObserver((mutations) => {
  // mutations 数组包含所有变化
  mutations.forEach(mutation => {
    console.log('变化类型:', mutation.type);
    console.log('变化节点:', mutation.target);
  });
});

observer.observe(document.body, {
  childList: true,       // 监听子节点增删
  subtree: true,        // 监听所有后代节点
  attributes: true,     // 监听属性变化
  attributeOldValue: true,  // 记录变化前的属性值
  characterData: true,   // 监听文本变化
  characterDataOldValue: true  // 记录变化前的文本
});

变化类型详解

type 说明 包含内容
childList 子节点增删 新增或删除的节点
attributes 属性变化 变化的属性名和值
characterData 文本变化 变化前后的文本内容
变化收集流程:
1. DOM 发生变化
2. 变化被记录到队列(不立即通知)
3. 变化积累一定数量或时间后
4. 批量通知观察者

实际应用:表单变化检测

监听表单是否有未保存的修改:

const observer = new MutationObserver(() => {
  form.hasChanges = true;  // 标记有变化
});

observer.observe(form, {
  childList: true, subtree: true, attributes: true, characterData: true
});

form.addEventListener('submit', () => {
  form.hasChanges = false;
  observer.disconnect();
});

ResizeObserver — 元素大小监听器

故事的起因:window.resize 太粗糙了

以前要监听元素大小变化,只能监听整个窗口:

window.addEventListener('resize', () => {
  console.log('窗口大小:', window.innerWidth, window.innerHeight);
});

但这有两个问题:

问题 说明
不精确 只能监听窗口,不能监听某个 div
性能差 窗口每次 resize 都触发,频率很高

ResizeObserver 的原理

ResizeObserver = 大小观察器。

专门用来监听任意元素的大小变化,精准高效。

ResizeObserver 原理:
┌─────────────────────────┐
  <div class="box">      
    内容随着大小变化        
  </div>                 
└─────────────────────────┘
          
  大小变化  浏览器自动通知
          
 entry.contentRect 包含新尺寸

基本用法

const observer = new ResizeObserver((entries) => {
  entries.forEach(entry => {
    const { width, height } = entry.contentRect;
    console.log(`元素大小: ${width} x ${height}`);
  });
});

observer.observe(boxElement);

// 停止观察
observer.unobserve(boxElement);
observer.disconnect();

实际应用:响应式布局

根据容器大小切换布局:

const observer = new ResizeObserver((entries) => {
  entries.forEach(entry => {
    const width = entry.contentRect.width;
    const target = entry.target;

    // 移除旧布局类
    target.classList.remove('mobile', 'tablet', 'desktop');

    // 根据宽度添加新布局类
    if (width < 600) {
      target.classList.add('mobile');
      renderMobileLayout(target);
    } else if (width < 1024) {
      target.classList.add('tablet');
      renderTabletLayout(target);
    } else {
      target.classList.add('desktop');
      renderDesktopLayout(target);
    }
  });
});

observer.observe(document.querySelector('.layout-container'));

PerformanceObserver — 性能监控器

故事的起因:Performance API 太难用

浏览器原生提供 performance.timing 等 API 来获取性能数据:

// 获取页面加载时间
const loadTime = performance.timing.loadEventEnd - performance.timing.navigationStart;
console.log('页面加载时间:', loadTime);

但问题是:

问题 说明
一次性 数据只在特定时间点有效
不实时 无法监控动态加载的资源
不直观 要自己计算各种时间差

PerformanceObserver 的原理

PerformanceObserver = 性能观察器。

让你实时监控浏览器的各种性能数据,浏览器会主动通知你。

PerformanceObserver 能监控的数据:
┌───────────────────────────────────┐
  longtask      - 长任务(>50ms)    
  paint         - 绘制时间(FP/FCP) 
  resource      - 资源加载时间       
  navigation    - 页面导航时间       
  mark          - 自定义标记         
  measure       - 自定义测量         
└───────────────────────────────────┘

基本用法

const observer = new PerformanceObserver((list) => {
  list.getEntries().forEach(entry => {
    console.log('性能数据:', entry);
  });
});

// 观察长任务
observer.observe({ type: 'longtask', buffered: true });

// 观察绘制时间
observer.observe({ type: 'paint', buffered: true });

// 观察资源加载
observer.observe({ type: 'resource', buffered: true });

实际应用:检测长任务

const observer = new PerformanceObserver((list) => {
  list.getEntries().forEach(entry => {
    console.warn('检测到长任务:', entry.duration, 'ms');

    // 长任务超过 50ms,视为需要优化
    if (entry.duration > 50) {
      console.error('长任务位置:', entry.name);
      console.error('开始时间:', entry.startTime);
    }
  });
});

observer.observe({ type: 'longtask', buffered: true });

Page Visibility API — 页面可见性检测

故事的起因:用户切换标签页你不知道

你有没有想过:

  • 用户打开了你的页面,然后切到别的标签页
  • 你的动画还在跑吗?定时器还在跑吗?
  • 如果是视频网站,用户切走了,视频还在播放吗?

以前的解决方案:

window.addEventListener('blur', () => {
  // 窗口失去焦点
});

window.addEventListener('focus', () => {
  // 窗口获得焦点
});

但这不够精确——用户可能只是最小化了窗口,或者切换到了别的标签页

Page Visibility API 的原理

Page Visibility API 让你精确知道页面的可见状态

visibilityState 的两种状态:
┌─────────────────────────────────────┐
  visible      - 页面完全可见         
  hidden       - 页面被隐藏          
└─────────────────────────────────────┘

触发场景:
- 切换标签页  hidden
- 最小化窗口  hidden
- 关闭浏览器  hidden
- 切换应用  hidden

基本用法

document.addEventListener('visibilitychange', () => {
  if (document.hidden) {
    console.log('页面被隐藏了');
    pauseAnimations();
    stopPolling();
  } else {
    console.log('页面可见了');
    resumeAnimations();
    startPolling();
  }
});

console.log('当前状态:', document.visibilityState);

实际应用:标签页切换统计

const metrics = {
  visibleTime: 0,
  hiddenTime: 0,
  lastChangeTime: Date.now()
};

document.addEventListener('visibilitychange', () => {
  const now = Date.now();

  if (document.hidden) {
    // 开始隐藏,记录可见时长
    metrics.visibleTime += now - metrics.lastChangeTime;
    metrics.lastChangeTime = now;

    // 暂停音乐/视频
    video.pause();
  } else {
    // 结束隐藏,记录隐藏时长
    metrics.hiddenTime += now - metrics.lastChangeTime;
    metrics.lastChangeTime = now;

    // 恢复音乐/视频
    video.play();
  }
});

window.addEventListener('beforeunload', () => {
  navigator.sendBeacon('/analytics', JSON.stringify(metrics));
});

Battery API — 电池状态检测

故事的起因:低电量时该省电

做 Web 应用时,你可能想过:

  • 电量低的时候,要不要关闭动画省电?
  • 正在充电时,能不能开启高性能模式?
  • 用户还能用多久?

这些问题,Battery API 都能回答。

Battery API 的兼容性

浏览器 支持情况
Chrome ✅ 50+
Firefox ✅ 10+
Safari ❌ 不支持
Edge ✅ 79+

注意:Safari 和 iOS Safari 不支持 Battery API。如果你的用户主要是苹果设备,这个 API 就用不了。

Battery API 的原理

Battery API = 电池状态接口。

浏览器通过操作系统获取电池信息,暴露给 JavaScript。

Battery API 数据来源:
┌─────────────────────────────────────┐
         浏览器                        
  navigator.getBattery()              
└─────────────────────────────────────┘
          
┌─────────────────────────────────────┐
         操作系统                      
  Windows / macOS / Linux            
  提供电池状态、电量、充电时间          
└─────────────────────────────────────┘
          
┌─────────────────────────────────────┐
         硬件                          
  电池芯片提供实时数据                 
└─────────────────────────────────────┘

基本用法

navigator.getBattery().then(battery => {
  console.log('是否在充电:', battery.charging);
  console.log('电量:', (battery.level * 100).toFixed(0), '%');
  console.log('剩余时间:', battery.dischargingTime / 60, '分钟');

  // 监听电量变化
  battery.addEventListener('levelchange', () => {
    console.log('电量变化:', (battery.level * 100).toFixed(0), '%');
  });

  // 监听充电状态变化
  battery.addEventListener('chargingchange', () => {
    console.log('充电状态变化:', battery.charging ? '充电中' : '未充电');
  });
});

实际应用:省电模式

// 获取电池信息并初始化省电模式
navigator.getBattery().then(battery => {
  // 根据电池状态更新页面
  function updateMode() {
    // 判断是否需要省电:没在充电 且 电量低于 20%
    const shouldSavePower = !battery.charging && battery.level < 0.2;

    // 切换 body 的省电样式类
    document.body.classList.toggle('power-saving', shouldSavePower);

    if (shouldSavePower) {
      // 电量低:关闭动画、停止轮询、降低画质
      disableAnimations();
      stopAutoRefresh();
      reduceRenderQuality();
    } else {
      // 电量充足:恢复正常模式
      enableAnimations();
      startAutoRefresh();
      restoreRenderQuality();
    }
  }

  // 监听电量变化和充电状态变化
  battery.addEventListener('levelchange', updateMode);
  battery.addEventListener('chargingchange', updateMode);

  // 页面加载时先检查一次
  updateMode();
});

Clipboard API — 剪贴板读写

故事的起因:以前只能靠 document.execCommand

以前复制文本到剪贴板,是这样的:

// ❌ 这种方式已经废弃了
textarea.select();
document.execCommand('copy');

而且这种方式问题很多:

  • 只能复制文本
  • 体验差(会有选中效果)
  • API 废弃了

Clipboard API 的原理

Clipboard API 让你安全地读写剪贴板,支持文本、图片、任意数据。

方法 作用
navigator.clipboard.readText() 读取剪贴板文本
navigator.clipboard.writeText() 写入文本到剪贴板
navigator.clipboard.read() 读取任意数据(图片等)
navigator.clipboard.write() 写入任意数据

注意:读写剪贴板需要用户授权!

基本用法

// 复制文本
async function copyText(text) {
  try {
    await navigator.clipboard.writeText(text);
    showToast('复制成功!');
  } catch (err) {
    console.error('复制失败:', err);
  }
}

// 读取剪贴板
async function readClipboard() {
  try {
    const text = await navigator.clipboard.readText();
    console.log('剪贴板内容:', text);
    return text;
  } catch (err) {
    console.error('读取失败:', err);
  }
}

实际应用:一键复制代码

document.querySelectorAll('.code-block').forEach(block => {
  const button = document.createElement('button');
  button.className = 'copy-btn';
  button.textContent = '复制';

  button.addEventListener('click', async () => {
    const code = block.textContent;

    try {
      await navigator.clipboard.writeText(code);
      button.textContent = '已复制!';
      setTimeout(() => {
        button.textContent = '复制';
      }, 2000);
    } catch (err) {
      button.textContent = '复制失败';
    }
  });

  block.appendChild(button);
});

Geolocation API — 地理位置

故事的起因:LBS 应用越来越火

地图、打车、外卖、社交软件……越来越多的应用需要知道用户的位置

Geolocation API 就是浏览器提供的定位接口

Geolocation API 的原理

Geolocation API = 地理位置接口。

浏览器会调用系统定位服务来获取位置。

Geolocation 定位方式:
┌───────────────────────────────────┐
  GPS 定位      - 精度最高(1-10米)  
  WLAN 定位     - 通过 WiFi 路由器    
  基站定位      - 通过手机信号塔       
  IP 定位       - 精度最低(城市级)   
└───────────────────────────────────┘
浏览器会自动选择最优方式

为什么需要用户授权?

原因 说明
隐私 位置信息属于敏感个人信息
法律要求 GDPR 等法规要求明确授权
安全 防止网站偷偷获取位置

基本用法

navigator.geolocation.getCurrentPosition(
  (position) => {
    console.log('纬度:', position.coords.latitude);
    console.log('经度:', position.coords.longitude);
    console.log('精度:', position.coords.accuracy, '米');
  },
  (error) => {
    console.error('获取失败:', error.message);
  },
  {
    enableHighAccuracy: true,  // 高精度模式(更慢)
    timeout: 5000,            // 超时时间
    maximumAge: 0            // 不使用缓存
  }
);

监听位置变化

const watchId = navigator.geolocation.watchPosition(
  (position) => {
    updateMapPosition(position.coords.latitude, position.coords.longitude);
  },
  (error) => {
    console.error('监听失败:', error.message);
  },
  {
    enableHighAccuracy: true,
    timeout: 10000,
    maximumAge: 30000  // 缓存 30 秒
  }
);

// 停止监听
navigator.geolocation.clearWatch(watchId);

实际应用:距离计算

function calculateDistance(lat1, lon1, lat2, lon2) {
  const R = 6371;  // 地球半径(公里)

  const toRad = (deg) => deg * Math.PI / 180;

  const dLat = toRad(lat2 - lat1);
  const dLon = toRad(lon2 - lon1);

  const a = Math.sin(dLat/2) * Math.sin(dLat/2) +
            Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) *
            Math.sin(dLon/2) * Math.sin(dLon/2);

  const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));

  return R * c;
}

// 北京 (39.9042, 116.4074) 到 上海 (31.2304, 121.4737)
const distance = calculateDistance(39.9042, 116.4074, 31.2304, 121.4737);
console.log('北京到上海约:', distance.toFixed(0), '公里');

总结

神器一览表

API 作用 解决的问题 兼容性
IntersectionObserver 懒加载、无限滚动 scroll 事件性能差 ✅ 主流都支持
MutationObserver DOM 变化监控 MutationEvent 已废弃 ✅ 主流都支持
ResizeObserver 元素大小监听 window.resize 太粗糙 ✅ 主流都支持
PerformanceObserver 性能监控 性能数据不直观 ✅ 主流都支持
Page Visibility API 页面可见性 不知道用户是否在看 ✅ 主流都支持
Battery API 电池状态 无法感知电量 ⚠️ Safari 不支持
Clipboard API 剪贴板读写 execCommand 废弃 ✅ 主流都支持
Geolocation API 地理位置 需要 LBS 功能 ✅ 主流都支持

使用建议

 推荐大胆使用的:
- IntersectionObserver:懒加载必备
- Page Visibility API:标签页切换必备
- Clipboard API:复制粘贴体验升级
- ResizeObserver:响应式布局神器

⚠️ 需要兼容性处理的:
- Battery API:先检测,不支持就降级
- Geolocation API:用户授权,注意隐私

 已废弃不要用的:
- MutationEvent
- document.execCommand(复制除外)

写在最后

现在你知道了:

  • 这些 API 不是浏览器随便加的,而是解决了真实问题
  • IntersectionObserver 让懒加载变得简单高效
  • MutationObserver 是 DOM 变化监控的唯一选择
  • ResizeObserver 解决了 window.resize 的痛点
  • PerformanceObserver 让性能监控变得直观
  • Page Visibility API 让你知道用户在看什么
  • Battery API 可以做省电优化(Safari 除外)
  • Clipboard API 是现代复制粘贴的标准方案
  • Geolocation API 让网页也能做 LBS 应用

每个 API 的存在都有其意义——用对了,问题迎刃而解

下次遇到相关场景,记得试试这些神器!

❌
❌