前言
HTML5 不仅仅是一个标记语言的版本更新,它带来了一整套强大的浏览器端 API,彻底改变了 Web 应用的能力边界。在 HTML5 之前,浏览器只是一个文档展示器;而在 HTML5 之后,浏览器变成了一个完整的应用平台。
本文将深入剖析七个核心 HTML5 高级 API,每个知识点都配有可直接运行的完整代码示例。无论你是前端新人还是资深开发者,这篇文章都能帮你系统性地掌握这些 API 的原理和使用技巧。
1. Web Storage(localStorage / sessionStorage)
1.1 为什么需要 Web Storage?
在 Web Storage 出现之前,前端存储数据只能依赖 Cookie。Cookie 的设计初衷是服务端会话管理,而不是客户端数据持久化,因此存在诸多限制。
| 特性 | Cookie | Web Storage |
|---|---|---|
| 容量 | 4KB 左右 | 5MB+(各浏览器略有差异) |
| 作用域 | 同站点下所有路径共享 | 同源策略(协议+域名+端口) |
| 过期策略 | 可设置过期时间 | 仅 localStorage 持久化,sessionStorage 随标签页关闭而清除 |
| 网络请求 | 每次 HTTP 请求自动携带 | 不会自动发送到服务端 |
| 操作接口 | 繁琐的 document.cookie 字符串操作 |
简洁的键值对 API |
1.2 localStorage 与 sessionStorage 的区别
两者的 API 完全一致,唯一的区别是数据生命周期:
- localStorage:数据持久化存储,除非用户手动清除或通过代码删除,否则永久保留。
- sessionStorage:数据仅在当前浏览器标签页(Tab)会话期间有效,关闭标签页即清除。同一个站点的不同标签页之间不共享。
1.3 基本操作
下面是一个完整的交互示例:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>Web Storage 基本操作</title>
<style>
body { font-family: Arial, sans-serif; max-width: 600px; margin: 40px auto; }
section { margin-bottom: 30px; padding: 20px; border: 1px solid #ddd; border-radius: 8px; }
input, button { padding: 8px 12px; margin: 4px; font-size: 14px; }
output { display: block; margin-top: 10px; padding: 10px; background: #f5f5f5; border-radius: 4px; min-height: 20px; }
</style>
</head>
<body>
<h1>Web Storage 演示</h1>
<section>
<h2>localStorage 操作</h2>
<input type="text" id="keyInput" placeholder="键名" />
<input type="text" id="valInput" placeholder="键值" />
<button onclick="saveLocal()">保存</button>
<button onclick="getLocal()">读取</button>
<button onclick="removeLocal()">删除</button>
<button onclick="clearLocal()">清空所有</button>
<output id="localOutput">等待操作...</output>
</section>
<section>
<h2>sessionStorage 操作</h2>
<input type="text" id="sKeyInput" placeholder="键名" />
<input type="text" id="sValInput" placeholder="键值" />
<button onclick="saveSession()">保存</button>
<button onclick="getSession()">读取</button>
<button onclick="removeSession()">删除</button>
<output id="sessionOutput">等待操作...</output>
</section>
<script>
function $(id) { return document.getElementById(id); }
// ---- localStorage ----
function saveLocal() {
const key = $('keyInput').value.trim();
const val = $('valInput').value.trim();
if (!key) { $('localOutput').textContent = '请输入键名'; return; }
try {
localStorage.setItem(key, val);
$('localOutput').textContent = '已保存: ' + key + ' = ' + val;
} catch (e) {
$('localOutput').textContent = '保存失败: ' + e.message;
}
}
function getLocal() {
const key = $('keyInput').value.trim();
if (!key) { $('localOutput').textContent = '请输入键名'; return; }
const val = localStorage.getItem(key);
$('localOutput').textContent = val !== null ? key + ' = ' + val : '键 "' + key + '" 不存在';
}
function removeLocal() {
const key = $('keyInput').value.trim();
if (!key) { $('localOutput').textContent = '请输入键名'; return; }
localStorage.removeItem(key);
$('localOutput').textContent = '已删除键: ' + key;
}
function clearLocal() {
localStorage.clear();
$('localOutput').textContent = '已清空所有 localStorage 数据';
}
// ---- sessionStorage ----
function saveSession() {
const key = $('sKeyInput').value.trim();
const val = $('sValInput').value.trim();
if (!key) { $('sessionOutput').textContent = '请输入键名'; return; }
sessionStorage.setItem(key, val);
$('sessionOutput').textContent = '已保存: ' + key + ' = ' + val;
}
function getSession() {
const key = $('sKeyInput').value.trim();
if (!key) { $('sessionOutput').textContent = '请输入键名'; return; }
const val = sessionStorage.getItem(key);
$('sessionOutput').textContent = val !== null ? key + ' = ' + val : '键 "' + key + '" 不存在';
}
function removeSession() {
const key = $('sKeyInput').value.trim();
if (!key) { $('sessionOutput').textContent = '请输入键名'; return; }
sessionStorage.removeItem(key);
$('sessionOutput').textContent = '已删除键: ' + key;
}
</script>
</body>
</html>1.4 storage 事件:跨标签页通信
当同一个源下的其他标签页修改了 localStorage 时,当前页面会触发 storage 事件。注意:修改数据的页面本身不会触发此事件。
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>Storage 事件监听</title>
</head>
<body>
<h1>Storage 事件监听器</h1>
<p>打开两个标签页,在其中一个修改 localStorage,观察另一个的变化。</p>
<input type="text" id="msgInput" placeholder="输入消息" />
<button onclick="sendMessage()">发送</button>
<ul id="log"></ul>
<script>
function sendMessage() {
const msg = document.getElementById('msgInput').value;
localStorage.setItem('cross_tab_message', msg);
localStorage.removeItem('cross_tab_message');
log('已发送: ' + msg);
}
function log(text) {
const li = document.createElement('li');
li.textContent = new Date().toLocaleTimeString() + ' - ' + text;
document.getElementById('log').appendChild(li);
}
window.addEventListener('storage', function (e) {
log('收到 storage 事件: key=' + e.key +
', 旧值=' + e.oldValue +
', 新值=' + e.newValue +
', URL=' + e.url);
});
</script>
</body>
</html>StorageEvent 对象包含以下属性:
key:被修改的键名oldValue:修改前的值newValue:修改后的值url:触发修改的页面 URLstorageArea:被修改的 Storage 对象
1.5 存储限制与异常处理
浏览器为每个源分配了约 5MB 的存储空间。当超出限制时,setItem() 会抛出 QuotaExceededError:
function safeSetItem(key, value) {
try {
localStorage.setItem(key, value);
return true;
} catch (e) {
if (e.name === 'QuotaExceededError' || e.code === 22) {
console.error('存储空间已满,请清理后重试');
// 可以在这里触发清理策略,例如删除最旧的数据
return false;
}
console.error('存储出错:', e);
return false;
}
}
// 检查当前使用量
function getStorageUsage() {
let total = 0;
for (let key in localStorage) {
if (localStorage.hasOwnProperty(key)) {
total += key.length + localStorage[key].length;
}
}
// 返回 KB,每个字符约 2 字节(UTF-16)
return (total * 2 / 1024).toFixed(2) + ' KB';
}1.6 封装带过期时间的 Storage 工具类
原生 Web Storage 不支持过期时间,我们可以封装一个工具类:
class ExpiredStorage {
constructor(storage = localStorage) {
this.storage = storage;
}
set(key, value, ttlMs) {
const data = {
value: value,
expiresAt: ttlMs ? Date.now() + ttlMs : null
};
this.storage.setItem(key, JSON.stringify(data));
}
get(key) {
const raw = this.storage.getItem(key);
if (raw === null) return null;
try {
const data = JSON.parse(raw);
if (data.expiresAt && Date.now() > data.expiresAt) {
this.storage.removeItem(key);
return null;
}
return data.value;
} catch {
// 兼容未包装的原始数据
return raw;
}
}
remove(key) {
this.storage.removeItem(key);
}
clear() {
this.storage.clear();
}
}
// 使用示例
const store = new ExpiredStorage();
store.set('token', 'abc123', 3600000); // 1 小时后过期
console.log(store.get('token')); // 'abc123'
// 1 小时后:
console.log(store.get('token')); // null(自动删除)2. Web Worker
2.1 为什么需要 Web Worker?
JavaScript 是单线程语言,所有的 UI 渲染、事件处理、脚本执行都在同一个主线程中。当执行耗时的计算任务时,页面会"卡住"——用户无法点击、滚动或输入。
Web Worker 允许我们在后台线程中运行 JavaScript,不阻塞主线程,从而保持页面的流畅响应。
2.2 主线程与 Worker 通信
通信基于消息传递模型:
// main.js(主线程)
const worker = new Worker('worker.js');
// 向 Worker 发送消息
worker.postMessage({ type: 'start', payload: 42 });
// 接收 Worker 的消息
worker.onmessage = function (event) {
console.log('从 Worker 收到:', event.data);
};
// 错误处理
worker.onerror = function (event) {
console.error('Worker 错误:', event.message);
};
// 终止 Worker
// worker.terminate();// worker.js(Worker 线程)
// 接收主线程消息
self.onmessage = function (event) {
const data = event.data;
console.log('Worker 收到:', data);
// 计算结果
const result = heavyComputation(data.payload);
// 发送结果回主线程
self.postMessage({ status: 'done', result: result });
};
function heavyComputation(n) {
let sum = 0;
for (let i = 0; i < n * 10000000; i++) {
sum += Math.sqrt(i);
}
return sum;
}2.3 计算密集型任务示例:大数计算
下面是一个完整的可运行示例,对比使用 Worker 和不使用 Worker 时的页面响应差异:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>Web Worker 性能对比</title>
<style>
body { font-family: Arial, sans-serif; max-width: 700px; margin: 40px auto; padding: 0 20px; }
.container { display: flex; gap: 20px; }
.box { flex: 1; padding: 20px; border: 1px solid #ddd; border-radius: 8px; }
.box h3 { margin-top: 0; }
button { padding: 10px 20px; font-size: 16px; cursor: pointer; }
.status { margin-top: 10px; padding: 10px; border-radius: 4px; }
.running { background: #fff3cd; }
.done { background: #d4edda; }
.blocked { background: #f8d7da; }
#counter { font-size: 24px; font-weight: bold; color: #007bff; }
</style>
</head>
<body>
<h1>Web Worker 性能对比演示</h1>
<div class="container">
<div class="box">
<h3>❌ 不使用 Worker</h3>
<p>点击后主线程被阻塞,页面无法交互</p>
<button onclick="runWithoutWorker()">开始计算</button>
<div id="statusNoWorker" class="status">就绪</div>
<div id="resultNoWorker"></div>
</div>
<div class="box">
<h3>✅ 使用 Worker</h3>
<p>后台计算,页面依然流畅</p>
<button onclick="runWithWorker()">开始计算</button>
<button onclick="stopWorker()" style="background:#dc3545;color:white;">终止 Worker</button>
<div id="statusWorker" class="status">就绪</div>
<div id="resultWorker"></div>
</div>
</div>
<div style="margin-top: 30px; text-align: center;">
<p>试试在计算过程中点击下面的计数器:</p>
<button onclick="increment()">+1</button>
<span id="counter">0</span>
</div>
<script>
let worker = null;
let count = 0;
function increment() {
document.getElementById('counter').textContent = ++count;
}
// ---- 不使用 Worker ----
function runWithoutWorker() {
const statusEl = document.getElementById('statusNoWorker');
const resultEl = document.getElementById('resultNoWorker');
statusEl.className = 'status blocked';
statusEl.textContent = '⏳ 计算中... 页面已卡死!尝试点击 +1 按钮';
resultEl.textContent = '';
// 模拟耗时计算
setTimeout(() => {
const start = performance.now();
let sum = 0;
for (let i = 0; i < 500000000; i++) {
sum += Math.sqrt(i);
}
const elapsed = ((performance.now() - start) / 1000).toFixed(2);
statusEl.className = 'status done';
statusEl.textContent = '✅ 完成';
resultEl.textContent = '结果: ' + sum.toFixed(0) + ' | 耗时: ' + elapsed + '秒';
}, 100);
}
// ---- 使用 Worker ----
function runWithWorker() {
const statusEl = document.getElementById('statusWorker');
const resultEl = document.getElementById('resultWorker');
if (worker) {
statusEl.textContent = '已有 Worker 在运行';
return;
}
worker = new Worker('data:application/javascript,' + encodeURIComponent(`
self.onmessage = function(e) {
const n = e.data;
let sum = 0;
for (let i = 0; i < n; i++) {
sum += Math.sqrt(i);
}
self.postMessage(sum);
};
`));
statusEl.className = 'status running';
statusEl.textContent = '⏳ Worker 后台计算中... 页面依然流畅!试试 +1 按钮';
resultEl.textContent = '';
worker.postMessage(500000000);
worker.onmessage = function(e) {
const elapsed = ((performance.now() - startTime) / 1000).toFixed(2);
statusEl.className = 'status done';
statusEl.textContent = '✅ 完成';
resultEl.textContent = '结果: ' + e.data.toFixed(0) + ' | 耗时: ' + elapsed + '秒';
worker.terminate();
worker = null;
};
worker.onerror = function(e) {
statusEl.textContent = '❌ 错误: ' + e.message;
worker.terminate();
worker = null;
};
const startTime = performance.now();
}
function stopWorker() {
if (worker) {
worker.terminate();
worker = null;
document.getElementById('statusWorker').className = 'status';
document.getElementById('statusWorker').textContent = '🛑 已终止';
}
}
</script>
</body>
</html>2.4 Worker 可用的 API 与限制
Worker 线程拥有独立的全局上下文(self 或 DedicatedWorkerGlobalScope),不能直接访问:
| 不可用 | 可用 |
|---|---|
| DOM(document、window、parent) | self / this |
alert()、confirm() |
postMessage() |
console 部分受限 |
console.log()(Worker 支持) |
| 大部分 DOM API | fetch() / XMLHttpRequest |
setTimeout / setInterval |
|
importScripts()(加载外部脚本) |
|
WebAssembly |
|
Navigator 某些属性 |
|
FileReader / Blob |
2.5 SharedWorker:跨标签页共享
SharedWorker 允许多个同源标签页共享同一个 Worker 实例,非常适合做跨标签页的状态同步:
// shared-worker.js
const connections = [];
self.onconnect = function (event) {
const port = event.ports[0];
connections.push(port);
port.onmessage = function (e) {
// 广播消息给所有连接的标签页
connections.forEach(function (conn) {
conn.postMessage(e.data);
});
};
port.postMessage('已连接到 SharedWorker');
};<!DOCTYPE html>
<html lang="zh-CN">
<body>
<h1>SharedWorker 演示</h1>
<p>打开多个标签页,消息会广播到所有页面</p>
<input type="text" id="msg" placeholder="输入消息" />
<button onclick="broadcast()">广播</button>
<ul id="log"></ul>
<script>
if (typeof SharedWorker !== 'undefined') {
const worker = new SharedWorker('shared-worker.js');
worker.port.onmessage = function (e) {
const li = document.createElement('li');
li.textContent = e.data;
document.getElementById('log').appendChild(li);
};
function broadcast() {
const msg = document.getElementById('msg').value;
worker.port.postMessage(msg);
}
worker.port.start();
} else {
document.body.innerHTML = '<p style="color:red">当前浏览器不支持 SharedWorker</p>';
}
</script>
</body>
</html>2.6 Service Worker 简介
Service Worker 是一种特殊的 Web Worker,充当浏览器与网络之间的代理服务器。它不能直接操作 DOM,但它可以:
- 拦截网络请求(fetch 事件)
- 管理缓存(Cache API)
- 离线支持(Service Worker + Cache 实现 PWA)
- 消息推送(Push API)
// service-worker.js
const CACHE_NAME = 'my-cache-v1';
const URLS_TO_CACHE = ['/', '/index.html', '/style.css', '/app.js'];
// 安装阶段:预缓存资源
self.addEventListener('install', function (event) {
event.waitUntil(
caches.open(CACHE_NAME).then(function (cache) {
console.log('缓存已打开');
return cache.addAll(URLS_TO_CACHE);
})
);
});
// 激活阶段:清理旧缓存
self.addEventListener('activate', function (event) {
event.waitUntil(
caches.keys().then(function (cacheNames) {
return Promise.all(
cacheNames
.filter(function (name) { return name !== CACHE_NAME; })
.map(function (name) { return caches.delete(name); })
);
})
);
});
// 拦截网络请求:缓存优先,网络回退
self.addEventListener('fetch', function (event) {
event.respondWith(
caches.match(event.request).then(function (response) {
// 缓存命中则返回缓存,否则发起网络请求
return response || fetch(event.request);
})
);
});
// 接收推送消息
self.addEventListener('push', function (event) {
const options = {
body: event.data ? event.data.text() : '新消息',
icon: '/icon.png',
badge: '/badge.png'
};
event.waitUntil(
self.registration.showNotification('推送通知', options)
);
});注册 Service Worker:需要在主页面中通过
navigator.serviceWorker.register('/service-worker.js')注册,且文件必须与页面同源。
2.7 性能对比:有无 Worker 的页面响应差异
以下是两种场景下的关键指标对比(以 5 亿次迭代计算为例):
| 指标 | 无 Worker | 有 Worker |
|---|---|---|
| 计算耗时 | ~3.2 秒 | ~3.5 秒(略有额外开销) |
| 页面响应 | ❌ 完全卡死 3 秒 | ✅ 始终流畅 60fps |
| 用户可交互 | ❌ 不能 | ✅ 可以 |
| 可终止运算 | ❌ 不能 | ✅ worker.terminate() |
结论:有 Worker 的场景虽然计算耗时略长(消息传递有开销),但用户体验天壤之别。
3. History API
3.1 为什么需要 History API?
传统 Web 应用中,每次页面跳转都会导致浏览器重新加载整个页面。SPA(单页应用)通过 JavaScript 动态更新页面内容,但 URL 无法同步更新,导致:
- 无法使用浏览器的前进/后退按钮
- 无法为特定状态生成书签或分享链接
History API 解决了这个问题,它允许我们在不刷新页面的情况下修改 URL。
3.2 pushState、replaceState 与 popstate
核心方法:
// pushState: 添加一条新的历史记录
history.pushState(stateObj, title, url);
// replaceState: 替换当前历史记录
history.replaceState(stateObj, title, url);
// popstate 事件: 当用户点击前进/后退时触发
window.addEventListener('popstate', function (event) {
console.log('状态变化:', event.state);
});参数说明:
stateObj:与记录关联的状态对象(任意 JSON 序列化数据),最大 640KBtitle:标题参数(大多数浏览器忽略该参数)url:新的 URL(必须同源,可选,默认当前 URL)
3.3 SPA 路由实现原理
下面是一个完整的 SPA 路由实现:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>History API SPA 路由</title>
<style>
body { font-family: Arial, sans-serif; margin: 0; }
nav { background: #333; padding: 12px; }
nav a { color: white; text-decoration: none; padding: 8px 16px; margin: 0 4px; border-radius: 4px; }
nav a:hover, nav a.active { background: #007bff; cursor: pointer; }
#app { padding: 30px; max-width: 600px; margin: 0 auto; }
.page { display: none; }
.page.active { display: block; }
.card { padding: 24px; border: 1px solid #ddd; border-radius: 8px; margin-bottom: 16px; }
.card h2 { margin-top: 0; }
.info { background: #e7f3ff; padding: 12px; border-radius: 4px; font-size: 14px; color: #555; }
</style>
</head>
<body>
<nav>
<a onclick="navigate('/')">首页</a>
<a onclick="navigate('/about')">关于</a>
<a onclick="navigate('/contact')">联系</a>
<a onclick="navigate('/product/42')">商品详情</a>
</nav>
<div id="app">
<div id="page-home" class="page active card">
<h2>🏠 首页</h2>
<p>欢迎来到 SPA 演示页面。</p>
<p>当前 URL: <span id="url-home"></span></p>
</div>
<div id="page-about" class="page card">
<h2>📖 关于我们</h2>
<p>这是一个使用 History API 实现的 SPA 路由示例。</p>
</div>
<div id="page-contact" class="page card">
<h2>📧 联系</h2>
<p>邮箱: contact@example.com</p>
<p>电话: 123-456-7890</p>
</div>
<div id="page-product" class="page card">
<h2>📦 商品详情</h2>
<p>商品 ID: <span id="product-id"></span></p>
</div>
<div class="info">
<strong>提示:</strong>点击导航链接观察 URL 变化,然后尝试浏览器的前进/后退按钮。
</div>
</div>
<script>
// 路由配置
const routes = {
'/': { page: 'page-home', title: '首页' },
'/about': { page: 'page-about', title: '关于' },
'/contact': { page: 'page-contact', title: '联系' },
'/product/:id': { page: 'page-product', title: '商品详情' }
};
// 匹配路由
function matchRoute(path) {
// 精确匹配
if (routes[path]) return { route: routes[path], params: {} };
// 参数化匹配
for (const pattern of Object.keys(routes)) {
const paramNames = [];
const regexStr = pattern.replace(/:(\w+)/g, function (_, name) {
paramNames.push(name);
return '([^/]+)';
});
const regex = new RegExp('^' + regexStr + '$');
const match = path.match(regex);
if (match) {
const params = {};
paramNames.forEach(function (name, i) {
params[name] = match[i + 1];
});
return { route: routes[pattern], params: params };
}
}
return null;
}
// 导航函数
function navigate(path, pushState = true) {
const matched = matchRoute(path);
if (!matched) {
navigate('/404');
return;
}
// 切换页面显示
document.querySelectorAll('.page').forEach(function (el) {
el.classList.remove('active');
});
document.getElementById(matched.route.page).classList.add('active');
// 处理特殊参数
if (matched.params.id) {
document.getElementById('product-id').textContent = matched.params.id;
}
if (matched.route.page === 'page-home') {
document.getElementById('url-home').textContent = window.location.href;
}
// 更新 URL(不刷新页面)
if (pushState) {
history.pushState(matched, matched.route.title, path);
}
// 更新导航高亮
document.querySelectorAll('nav a').forEach(function (a) {
a.classList.toggle('active', a.getAttribute('onclick').includes("'" + path + "'"));
});
document.title = matched.route.title + ' - SPA 路由演示';
}
// 监听前进/后退
window.addEventListener('popstate', function (event) {
const path = window.location.pathname;
if (path === '/') {
navigate('/', false);
} else {
navigate(path, false);
}
});
// 初始化:根据当前 URL 加载对应页面
(function init() {
const path = window.location.pathname;
if (path !== '/') {
navigate(path, false);
} else {
document.getElementById('url-home').textContent = window.location.href;
}
})();
</script>
</body>
</html>3.4 History API 与 hash 路由的对比
| 特性 | History API | Hash 路由 |
|---|---|---|
| URL 样式 | /about(美观) |
/#/about(带 #) |
| 服务端支持 | 需要,否则 404 | 不需要 |
| SEO | 更友好 | 较差 |
| 兼容性 | IE10+ | IE8+ |
| 状态数据 | 支持携带任意数据 | 不支持 |
注意:使用 History API 时,服务端必须把所有路由都指向同一个入口文件(如
index.html),否则用户直接访问/about或刷新页面时会得到 404 错误。这是 SPA 部署中常见的问题。
4. Drag & Drop API
4.1 基本概念
HTML5 的拖放 API 让元素可以在页面内或页面之间被拖拽。核心是拖拽源(被拖拽的元素)和放置目标(接受拖拽的区域)。
4.2 事件流程
一个完整的拖放操作会依次触发以下事件:
- 拖拽源:
dragstart→drag→dragend - 放置目标:
dragenter→dragover→dragleave/drop
4.3 基本拖拽示例
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>HTML5 拖放 API</title>
<style>
body { font-family: Arial, sans-serif; max-width: 700px; margin: 40px auto; }
.drag-item {
display: inline-block;
padding: 12px 24px;
margin: 8px;
background: #007bff;
color: white;
border-radius: 6px;
cursor: grab;
user-select: none;
}
.drag-item:active { cursor: grabbing; }
.drop-zone {
min-height: 120px;
padding: 16px;
margin: 16px 0;
border: 2px dashed #ccc;
border-radius: 8px;
transition: all 0.2s;
}
.drop-zone.dragover {
border-color: #007bff;
background: #e7f3ff;
}
.drop-zone.filled {
border-style: solid;
border-color: #28a745;
background: #d4edda;
}
.status-bar {
margin-top: 16px;
padding: 12px;
background: #f8f9fa;
border-radius: 4px;
font-size: 14px;
}
</style>
</head>
<body>
<h1>拖放 API 演示</h1>
<div>
<div class="drag-item" draggable="true" data-color="#007bff">蓝色方块</div>
<div class="drag-item" draggable="true" data-color="#dc3545" style="background:#dc3545">红色方块</div>
<div class="drag-item" draggable="true" data-color="#28a745" style="background:#28a745">绿色方块</div>
</div>
<div class="drop-zone" id="dropZone1">
<p>📦 拖放到这里(区域 1)</p>
</div>
<div class="drop-zone" id="dropZone2">
<p>📦 拖放到这里(区域 2)</p>
</div>
<div id="status" class="status-bar">等待操作...</div>
<script>
// ---- 拖拽源事件 ----
document.querySelectorAll('.drag-item').forEach(function (item) {
item.addEventListener('dragstart', function (e) {
e.dataTransfer.setData('text/plain', this.dataset.color);
e.dataTransfer.effectAllowed = 'move';
this.style.opacity = '0.5';
updateStatus('开始拖拽: ' + this.textContent);
});
item.addEventListener('dragend', function (e) {
this.style.opacity = '1';
updateStatus('拖拽结束');
});
});
// ---- 放置目标事件 ----
document.querySelectorAll('.drop-zone').forEach(function (zone) {
zone.addEventListener('dragenter', function (e) {
e.preventDefault();
this.classList.add('dragover');
});
zone.addEventListener('dragover', function (e) {
e.preventDefault(); // 必须调用!允许放置
e.dataTransfer.dropEffect = 'move';
});
zone.addEventListener('dragleave', function (e) {
this.classList.remove('dragover');
});
zone.addEventListener('drop', function (e) {
e.preventDefault();
this.classList.remove('dragover');
this.classList.add('filled');
// 获取拖拽数据
const color = e.dataTransfer.getData('text/plain');
// 创建放置后的元素
const el = document.createElement('div');
el.textContent = '已放置 █';
el.style.cssText = 'color: white; background: ' + color +
'; padding: 8px 16px; border-radius: 4px; display: inline-block; margin: 4px;';
// 移除占位文字
const p = this.querySelector('p');
if (p) p.remove();
this.appendChild(el);
updateStatus('已放置元素,颜色: ' + color);
});
});
function updateStatus(msg) {
document.getElementById('status').textContent =
new Date().toLocaleTimeString() + ' - ' + msg;
}
</script>
</body>
</html>4.4 DataTransfer 对象详解
dataTransfer 是拖放 API 的核心对象,提供以下重要属性和方法:
| 属性/方法 | 说明 |
|---|---|
setData(format, data) |
设置拖拽数据 |
getData(format) |
获取拖拽数据 |
clearData([format]) |
清除拖拽数据 |
setDragImage(img, x, y) |
自定义拖拽时的预览图 |
effectAllowed |
拖拽效果(move/copy/link/none) |
dropEffect |
放置效果 |
files |
拖拽的文件列表 |
types |
存储的数据格式列表 |
4.5 拖拽文件上传实现
这是拖放 API 最实用的场景之一:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>拖拽文件上传</title>
<style>
body { font-family: Arial, sans-serif; max-width: 600px; margin: 40px auto; }
#dropArea {
border: 3px dashed #aaa;
border-radius: 12px;
padding: 60px 20px;
text-align: center;
font-size: 18px;
color: #666;
transition: all 0.3s;
cursor: pointer;
}
#dropArea.dragover {
border-color: #007bff;
background: #e7f3ff;
color: #007bff;
}
#dropArea.has-files {
border-color: #28a745;
background: #d4edda;
}
#preview {
margin-top: 20px;
display: grid;
grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
gap: 10px;
}
.file-card {
padding: 10px;
border: 1px solid #ddd;
border-radius: 6px;
text-align: center;
}
.file-card img {
max-width: 80px;
max-height: 80px;
display: block;
margin: 0 auto 6px;
}
.file-card .name {
font-size: 12px;
color: #555;
word-break: break-all;
}
.file-card .size {
font-size: 11px;
color: #999;
}
#progress {
margin-top: 16px;
height: 6px;
background: #eee;
border-radius: 3px;
overflow: hidden;
display: none;
}
#progress-bar {
height: 100%;
width: 0%;
background: #007bff;
transition: width 0.3s;
}
</style>
</head>
<body>
<h1>📁 拖拽文件上传</h1>
<p>将文件拖拽到下方区域,或点击选择文件</p>
<div id="dropArea">
<div>📂 将文件拖拽到这里</div>
<div style="font-size:14px;margin-top:8px;color:#999;">或点击选择文件</div>
<input type="file" id="fileInput" multiple accept="image/*,text/*,.pdf" style="display:none" />
</div>
<div id="progress"><div id="progress-bar"></div></div>
<div id="preview"></div>
<script>
const dropArea = document.getElementById('dropArea');
const fileInput = document.getElementById('fileInput');
const preview = document.getElementById('preview');
const progress = document.getElementById('progress');
const progressBar = document.getElementById('progress-bar');
// 点击上传
dropArea.addEventListener('click', function () {
fileInput.click();
});
fileInput.addEventListener('change', function () {
handleFiles(this.files);
});
// 拖拽事件
dropArea.addEventListener('dragover', function (e) {
e.preventDefault();
this.classList.add('dragover');
});
dropArea.addEventListener('dragleave', function (e) {
this.classList.remove('dragover');
});
dropArea.addEventListener('drop', function (e) {
e.preventDefault();
this.classList.remove('dragover');
if (e.dataTransfer.files.length > 0) {
handleFiles(e.dataTransfer.files);
}
});
function handleFiles(files) {
dropArea.classList.add('has-files');
preview.innerHTML = '';
Array.from(files).forEach(function (file, index) {
const card = document.createElement('div');
card.className = 'file-card';
// 图片预览
if (file.type.startsWith('image/')) {
const img = document.createElement('img');
img.src = URL.createObjectURL(file);
img.onload = function () { URL.revokeObjectURL(this.src); };
card.appendChild(img);
} else {
const icon = document.createElement('div');
icon.textContent = '📄';
icon.style.fontSize = '40px';
card.appendChild(icon);
}
const nameEl = document.createElement('div');
nameEl.className = 'name';
nameEl.textContent = file.name;
card.appendChild(nameEl);
const sizeEl = document.createElement('div');
sizeEl.className = 'size';
sizeEl.textContent = formatSize(file.size);
card.appendChild(sizeEl);
preview.appendChild(card);
});
// 模拟上传进度
startUpload(files);
}
function startUpload(files) {
progress.style.display = 'block';
let pct = 0;
const interval = setInterval(function () {
pct += Math.random() * 15;
if (pct >= 100) {
pct = 100;
clearInterval(interval);
setTimeout(function () {
progress.style.display = 'none';
progressBar.style.width = '0%';
}, 500);
}
progressBar.style.width = pct + '%';
}, 300);
}
function formatSize(bytes) {
if (bytes < 1024) return bytes + ' B';
if (bytes < 1048576) return (bytes / 1024).toFixed(1) + ' KB';
return (bytes / 1048576).toFixed(1) + ' MB';
}
</script>
</body>
</html>5. Geolocation API
5.1 基本用法
Geolocation API 允许 Web 应用获取用户的地理位置信息。接口定义在 navigator.geolocation 对象上。
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>Geolocation API 演示</title>
<style>
body { font-family: Arial, sans-serif; max-width: 600px; margin: 40px auto; padding: 0 20px; }
.card { padding: 20px; border: 1px solid #ddd; border-radius: 8px; margin-bottom: 16px; }
button { padding: 10px 20px; font-size: 16px; cursor: pointer; }
#mapLink { font-size: 18px; }
.data-row { margin: 8px 0; }
.label { font-weight: bold; color: #555; display: inline-block; width: 100px; }
.error { color: #dc3545; background: #f8d7da; padding: 12px; border-radius: 4px; }
.success { color: #28a745; background: #d4edda; padding: 12px; border-radius: 4px; }
#watchInfo { font-size: 13px; color: #888; margin-top: 8px; }
</style>
</head>
<body>
<h1>📍 Geolocation API 演示</h1>
<div class="card">
<button onclick="getLocation()">获取当前位置</button>
<button onclick="startWatching()" style="background:#28a745;color:white;">持续监听位置</button>
<button onclick="stopWatching()" style="background:#dc3545;color:white;">停止监听</button>
</div>
<div id="result" class="card">
<p>等待获取位置...</p>
</div>
<div id="watchInfo"></div>
<script>
let watchId = null;
function getLocation() {
const result = document.getElementById('result');
if (!navigator.geolocation) {
result.innerHTML = '<div class="error">❌ 当前浏览器不支持 Geolocation API</div>';
return;
}
result.innerHTML = '<p>⏳ 正在获取位置...</p>';
navigator.geolocation.getCurrentPosition(
// 成功回调
function (position) {
const coords = position.coords;
result.innerHTML = displayPosition(coords, position.timestamp);
},
// 失败回调
function (error) {
result.innerHTML = '<div class="error">❌ ' + getErrorMessage(error) + '</div>';
},
// 选项
{
enableHighAccuracy: true,
timeout: 10000,
maximumAge: 300000
}
);
}
function startWatching() {
if (!navigator.geolocation) {
document.getElementById('result').innerHTML =
'<div class="error">不支持 Geolocation API</div>';
return;
}
document.getElementById('result').innerHTML = '<p>⏳ 开始持续监听位置...</p>';
watchId = navigator.geolocation.watchPosition(
function (position) {
const coords = position.coords;
document.getElementById('result').innerHTML =
displayPosition(coords, position.timestamp) +
'<div class="success">✅ 位置已更新</div>';
},
function (error) {
document.getElementById('result').innerHTML =
'<div class="error">❌ ' + getErrorMessage(error) + '</div>';
},
{
enableHighAccuracy: true,
timeout: 10000,
maximumAge: 0 // 不允许使用缓存
}
);
document.getElementById('watchInfo').textContent =
'监听中 (ID: ' + watchId + ')';
}
function stopWatching() {
if (watchId !== null) {
navigator.geolocation.clearWatch(watchId);
watchId = null;
document.getElementById('watchInfo').textContent = '已停止监听';
document.getElementById('result').innerHTML = '<p>🛑 位置监听已停止</p>';
}
}
function displayPosition(coords, timestamp) {
const date = new Date(timestamp);
return `
<div class="data-row"><span class="label">纬度:</span> ${coords.latitude.toFixed(6)}</div>
<div class="data-row"><span class="label">经度:</span> ${coords.longitude.toFixed(6)}</div>
<div class="data-row"><span class="label">精度:</span> ${coords.accuracy.toFixed(1)} 米</div>
<div class="data-row"><span class="label">高度:</span> ${coords.altitude != null ? coords.altitude.toFixed(1) + ' 米' : '不可用'}</div>
<div class="data-row"><span class="label">速度:</span> ${coords.speed != null ? coords.speed.toFixed(1) + ' 米/秒' : '不可用'}</div>
<div class="data-row"><span class="label">方向:</span> ${coords.heading != null ? coords.heading.toFixed(1) + '°' : '不可用'}</div>
<div class="data-row"><span class="label">更新时间:</span> ${date.toLocaleString()}</div>
<div class="data-row">
<a id="mapLink" href="https://www.openstreetmap.org/?mlat=${coords.latitude}&mlon=${coords.longitude}&zoom=15"
target="_blank">🗺️ 在 OpenStreetMap 中查看</a>
</div>
`;
}
function getErrorMessage(error) {
switch (error.code) {
case error.PERMISSION_DENIED: return '用户拒绝了位置权限';
case error.POSITION_UNAVAILABLE: return '无法获取位置信息';
case error.TIMEOUT: return '获取位置超时';
default: return '未知错误 (代码: ' + error.code + ')';
}
}
</script>
</body>
</html>5.2 权限处理与优雅降级
在实际应用中,必须处理用户拒绝授权的情况。最佳实践是渐进增强 + 优雅降级:
function requestUserLocation() {
// 第一步:检查 API 可用性
if (!navigator.geolocation) {
fallbackToIPLocation();
return;
}
// 第二步:尝试获取精确位置
navigator.geolocation.getCurrentPosition(
function (position) {
// 成功:使用精确坐标
initMap(position.coords.latitude, position.coords.longitude);
},
function (error) {
// 失败:根据错误类型给出不同反馈
switch (error.code) {
case error.PERMISSION_DENIED:
showMessage('位置权限被拒绝,将使用 IP 定位');
fallbackToIPLocation();
break;
case error.POSITION_UNAVAILABLE:
showMessage('无法定位,请检查 GPS 或网络');
fallbackToIPLocation();
break;
case error.TIMEOUT:
showMessage('定位超时,请重试');
break;
}
},
{ enableHighAccuracy: true, timeout: 8000, maximumAge: 60000 }
);
}
// IP 定位降级方案
function fallbackToIPLocation() {
fetch('https://ipapi.co/json/')
.then(function (res) { return res.json(); })
.then(function (data) {
initMap(data.latitude, data.longitude);
})
.catch(function () {
showMessage('定位功能不可用,请输入城市名称');
});
}6. WebSocket
6.1 为什么需要 WebSocket?
在 WebSocket 出现之前,浏览器与服务端实现实时通信主要靠HTTP 长轮询:
客户端: "有新消息吗?"
服务端: "没有。" ← 等待几秒
客户端: "有新消息吗?"
服务端: "没有。" ← 等待几秒
...
客户端: "有新消息吗?"
服务端: "有!消息内容..."长轮询的问题:
- 高延迟:消息到达客户端至少需要一次完整的 HTTP 往返
- 资源浪费:大量重复的 HTTP 请求头
- 服务器压力大:需要维持大量长连接
WebSocket 通过一次握手建立双向持久连接,彻底解决了这些问题:
客户端: "连接我吧!" ← HTTP 升级握手
服务端: "同意升级!" ← 101 Switching Protocols
此后双方随时可以互发消息
服务端: "有新消息!" ← 即时推送
客户端: "收到了!" ← 即时响应6.2 WebSocket 完整示例
以下是一个完整的聊天客户端示例(配合 WebSocket 测试服务器):
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>WebSocket 聊天客户端</title>
<style>
body { font-family: Arial, sans-serif; max-width: 600px; margin: 40px auto; padding: 0 20px; }
#chatBox {
height: 400px;
border: 1px solid #ddd;
border-radius: 8px;
padding: 16px;
overflow-y: auto;
margin-bottom: 12px;
background: #fafafa;
}
.msg { margin: 8px 0; padding: 8px 12px; border-radius: 6px; }
.msg.sent { background: #007bff; color: white; text-align: right; }
.msg.received { background: #e9ecef; }
.msg.system { background: #fff3cd; text-align: center; font-size: 13px; }
.input-row { display: flex; gap: 8px; }
#msgInput { flex: 1; padding: 10px; border: 1px solid #ccc; border-radius: 4px; font-size: 14px; }
button { padding: 10px 20px; border: none; border-radius: 4px; cursor: pointer; font-size: 14px; }
#sendBtn { background: #007bff; color: white; }
#connectBtn { background: #28a745; color: white; }
#disconnectBtn { background: #dc3545; color: white; }
#status { margin-top: 8px; font-size: 13px; }
.connected { color: #28a745; }
.disconnected { color: #dc3545; }
</style>
</head>
<body>
<h1>💬 WebSocket 聊天演示</h1>
<div>
<button id="connectBtn" onclick="connect()">连接</button>
<button id="disconnectBtn" onclick="disconnect()" disabled>断开</button>
<span id="status" class="disconnected">未连接</span>
</div>
<div id="chatBox"></div>
<div class="input-row">
<input type="text" id="msgInput" placeholder="输入消息..." disabled />
<button id="sendBtn" onclick="sendMessage()" disabled>发送</button>
</div>
<script>
let ws = null;
let reconnectAttempts = 0;
const MAX_RECONNECT = 5;
function connect() {
const wsUrl = document.getElementById('wsUrl')
? document.getElementById('wsUrl').value
: 'wss://echo.websocket.org';
// 使用 wss://echo.websocket.org 作为测试服务器(它会回传任何消息)
// 也可以通过 WebSocket 测试工具如 wscat
ws = new WebSocket('wss://echo.websocket.org');
ws.onopen = function () {
setStatus(true);
addMessage('system', '🟢 已连接到服务器');
reconnectAttempts = 0;
};
ws.onmessage = function (event) {
addMessage('received', '服务端: ' + event.data);
};
ws.onclose = function (event) {
setStatus(false);
addMessage('system', '🔴 连接已关闭 (代码: ' + event.code + ')');
ws = null;
// 自动重连
if (reconnectAttempts < MAX_RECONNECT) {
reconnectAttempts++;
addMessage('system', '⏳ ' + reconnectAttempts + ' 秒后自动重连...');
setTimeout(connect, reconnectAttempts * 1000);
}
};
ws.onerror = function (event) {
addMessage('system', '❌ 连接错误');
console.error('WebSocket 错误:', event);
};
}
function disconnect() {
if (ws) {
ws.close(1000, '用户主动断开');
ws = null;
}
}
function sendMessage() {
const input = document.getElementById('msgInput');
const text = input.value.trim();
if (!text || !ws || ws.readyState !== WebSocket.OPEN) {
return;
}
ws.send(text);
addMessage('sent', '我: ' + text);
input.value = '';
}
function addMessage(type, content) {
const chatBox = document.getElementById('chatBox');
const div = document.createElement('div');
div.className = 'msg ' + type;
div.textContent = content;
chatBox.appendChild(div);
chatBox.scrollTop = chatBox.scrollHeight;
}
function setStatus(connected) {
const status = document.getElementById('status');
const connectBtn = document.getElementById('connectBtn');
const disconnectBtn = document.getElementById('disconnectBtn');
const sendBtn = document.getElementById('sendBtn');
const msgInput = document.getElementById('msgInput');
if (connected) {
status.className = 'connected';
status.textContent = '🟢 已连接';
connectBtn.disabled = true;
disconnectBtn.disabled = false;
sendBtn.disabled = false;
msgInput.disabled = false;
msgInput.focus();
} else {
status.className = 'disconnected';
status.textContent = '🔴 未连接';
connectBtn.disabled = false;
disconnectBtn.disabled = true;
sendBtn.disabled = true;
msgInput.disabled = true;
}
}
// 回车发送
document.getElementById('msgInput').addEventListener('keydown', function (e) {
if (e.key === 'Enter') sendMessage();
});
</script>
</body>
</html>6.3 WebSocket 与 HTTP 长轮询对比
| 维度 | HTTP 长轮询 | WebSocket |
|---|---|---|
| 连接模式 | 半双工(交替收发) | 全双工(同时收发) |
| 协议开销 | 每次请求都带完整 HTTP 头 | 一次握手后仅传输数据帧(2-14 字节头) |
| 延迟 | 至少一个 RTT | 接近于 0 额外延迟 |
| 服务端资源 | 需要维持大量连接 | 少量连接即可 |
| 跨域 | 服务端控制 | 同源策略 + 可配置 |
| 适用场景 | 低频轮询 | 高频实时通信 |
6.4 WebSocket 对象生命周期
new WebSocket(url)
└── readyState = CONNECTING (0)
└── onopen 事件
├── readyState = OPEN (1)
├── send(data) ← 发送消息
├── onmessage ← 接收消息
└── close(code, reason)
└── readyState = CLOSING (2)
└── onclose 事件
└── readyState = CLOSED (3)就绪状态常量:
WebSocket.CONNECTING= 0:正在连接WebSocket.OPEN= 1:已连接,可通信WebSocket.CLOSING= 2:正在关闭WebSocket.CLOSED= 3:已关闭
7. 其他 HTML5 API 简述
7.1 Notification API(桌面通知)
浏览器支持在桌面右下角弹出系统级通知,即使用户切换到其他标签页也能看到:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>Notification API 演示</title>
</head>
<body>
<h1>🔔 Notification API</h1>
<button onclick="sendNotification()">发送桌面通知</button>
<button onclick="requestPermission()">请求通知权限</button>
<p id="permStatus">权限状态: 未请求</p>
<script>
function requestPermission() {
if (!('Notification' in window)) {
document.getElementById('permStatus').textContent = '❌ 当前浏览器不支持 Notification API';
return;
}
Notification.requestPermission().then(function (perm) {
document.getElementById('permStatus').textContent =
'权限状态: ' + perm;
});
}
function sendNotification() {
if (!('Notification' in window)) {
alert('不支持通知');
return;
}
if (Notification.permission === 'granted') {
const notif = new Notification('来自网页的消息', {
body: '这是一条桌面通知!时间是 ' + new Date().toLocaleTimeString(),
icon: 'https://via.placeholder.com/128x128/007bff/ffffff?text=N',
tag: 'demo-notification',
vibrate: [200, 100, 200]
});
notif.onclick = function () {
window.focus();
this.close();
};
setTimeout(function () { notif.close(); }, 8000);
} else if (Notification.permission === 'denied') {
alert('通知权限已被拒绝,请在浏览器设置中开启');
} else {
alert('请先请求通知权限');
}
}
</script>
</body>
</html>7.2 Page Visibility API
用来检测页面是否处于可见状态(用户是否切换到了其他标签页),非常适合用来暂停视频播放或停止动画:
<!DOCTYPE html>
<html lang="zh-CN">
<body>
<h1>Page Visibility API</h1>
<p>切换到其他标签页再切回来,观察控制台输出</p>
<div id="log" style="background:#f5f5f5;padding:16px;border-radius:8px;min-height:100px;"></div>
<script>
function log(msg) {
const div = document.getElementById('log');
div.innerHTML += '<div>' + new Date().toLocaleTimeString() + ' - ' + msg + '</div>';
}
document.addEventListener('visibilitychange', function () {
if (document.hidden) {
log('🔴 页面已隐藏(用户切换了标签页)');
// 暂停视频、停止动画轮询
} else {
log('🟢 页面已可见(用户回来了)');
// 恢复视频、刷新数据
}
});
log('页面可见性: ' + (document.hidden ? '隐藏' : '可见'));
// 另一个有用的属性:visibilityState
// 'visible' | 'hidden' | 'prerender'
log('visibilityState: ' + document.visibilityState);
</script>
</body>
</html>7.3 Network Information API
提供设备的网络连接信息:
<!DOCTYPE html>
<html lang="zh-CN">
<body>
<h1>Network Information API</h1>
<div id="info" style="background:#f5f5f5;padding:16px;border-radius:8px;"></div>
<script>
function updateNetworkInfo() {
const conn = navigator.connection || navigator.mozConnection || navigator.webkitConnection;
if (!conn) {
document.getElementById('info').innerHTML = '<p>当前浏览器不支持 Network Information API</p>';
return;
}
document.getElementById('info').innerHTML = `
<p><strong>连接类型:</strong> ${conn.type || '未知'}</p>
<p><strong>有效连接类型:</strong> ${conn.effectiveType || '未知'} (4g / 3g / 2g / slow-2g)</p>
<p><strong>下行速度:</strong> ${conn.downlink || '未知'} Mbps</p>
<p><strong>RTT (往返时延):</strong> ${conn.rtt || '未知'} ms</p>
<p><strong>数据节省模式:</strong> ${conn.saveData ? '开启' : '关闭'}</p>
<p style="font-size:13px;color:#888;">监听网络变化: ${conn.onchange ? '已设置' : '未设置'}</p>
`;
}
const conn = navigator.connection || navigator.mozConnection || navigator.webkitConnection;
if (conn) {
conn.addEventListener('change', function () {
updateNetworkInfo();
console.log('网络状态已变化');
});
}
updateNetworkInfo();
</script>
</body>
</html>7.4 Battery Status API
获取设备的电池状态信息:
<!DOCTYPE html>
<html lang="zh-CN">
<body>
<h1>Battery Status API</h1>
<div id="batteryInfo" style="background:#f5f5f5;padding:16px;border-radius:8px;"></div>
<script>
function updateBatteryInfo(battery) {
const levelPct = Math.round(battery.level * 100);
const charging = battery.charging ? '正在充电 ⚡' : '未充电';
const timeLeft = battery.dischargingTime === Infinity
? '计算中...'
: Math.round(battery.dischargingTime / 60) + ' 分钟';
document.getElementById('batteryInfo').innerHTML = `
<p><strong>电量:</strong> ${levelPct}%</p>
<p><strong>状态:</strong> ${charging}</p>
<p><strong>剩余时间:</strong> ${timeLeft}</p>
<div style="height:24px;background:#eee;border-radius:12px;overflow:hidden;margin-top:8px;">
<div style="height:100%;width:${levelPct}%;background:${levelPct > 20 ? '#28a745' : '#dc3545'};
border-radius:12px;transition:width 0.5s;"></div>
</div>
`;
}
if (navigator.getBattery) {
navigator.getBattery().then(function (battery) {
updateBatteryInfo(battery);
battery.addEventListener('levelchange', function () {
updateBatteryInfo(battery);
});
battery.addEventListener('chargingchange', function () {
updateBatteryInfo(battery);
});
});
} else {
document.getElementById('batteryInfo').innerHTML =
'<p>当前浏览器不支持 Battery Status API</p>';
}
</script>
</body>
</html>总结与最佳实践
API 兼容性速查表
| API | Chrome | Firefox | Safari | Edge | IE |
|---|---|---|---|---|---|
| Web Storage | ✅ | ✅ | ✅ | ✅ | ✅ (IE8+) |
| Web Worker | ✅ | ✅ | ✅ | ✅ | ✅ (IE10+) |
| History API | ✅ | ✅ | ✅ | ✅ | ✅ (IE10+) |
| Drag & Drop | ✅ | ✅ | ✅ | ✅ | ✅ (IE5+) |
| Geolocation | ✅ | ✅ | ✅ | ✅ | ✅ (IE9+) |
| WebSocket | ✅ | ✅ | ✅ | ✅ | ✅ (IE10+) |
| Notification | ✅ | ✅ | ✅ (需 HTTPS) | ✅ | ❌ |
| Page Visibility | ✅ | ✅ | ✅ | ✅ | ✅ (IE10+) |
| Network Information | ✅ | ✅ | ❌ | ✅ | ❌ |
| Battery Status | ✅ (已废弃) | ✅ | ❌ | ❌ | ❌ |
注意:Battery Status API 在 Chrome 中已被废弃(deprecated),不建议在新项目中依赖。Network Information API 的兼容性也有限。
项目中使用建议
-
Web Storage:适合存储用户偏好、缓存数据、购物车等。敏感数据不要存(明文存储),大文件不要存(5MB 限制)。
-
Web Worker:适合图像处理、数据加密、大文件解析、游戏物理计算等 CPU 密集型任务。DOM 操作不行。
-
History API:SPA 框架的基石。配合
<base>标签和服务端路由重定向使用。考虑降级方案支持 hash 路由。 -
Drag & Drop:移动端支持有限,考虑使用 touch 事件作为补充。
-
Geolocation:始终提供降级方案(IP 定位或用户手动输入)。尊重用户隐私,只在需要时请求权限。
-
WebSocket:生产环境务必实现心跳检测、自动重连和消息确认机制。考虑使用 Socket.IO 等库做降级。
-
其他 API:使用前检测功能可用性,做好渐进增强。
HTML5 的这些高级 API 将浏览器的能力从文档展示扩展到了全功能应用平台。掌握它们,你的 Web 应用将不再受限于"页面"这个概念,而是真正成为一个跨平台的"应用"。
本文发布于 2024-08-24,分类:前端,标签:HTML5, Web Storage, Web Worker, History API, Drag & Drop, Geolocation, WebSocket