Web-API
IntersectionObserver
基础说明
IntersectionObserver 是一个用于异步观察目标元素与其祖先元素或视口交叉状态的 API。主要用于实现:
- 懒加载:图片/内容进入视口时才加载
- 无限滚动:滚动到底部时加载更多
- 元素可见性检测:判断元素是否在屏幕内可见
- 动画触发:元素进入视口时触发动画
构造函数
const observer = new IntersectionObserver(callback, options);参数说明
callback 回调函数
callback(entries, observer)entries: 数组,包含所有被观察的元素交叉状态变化信息observer: 观察者实例本身
options 配置对象
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
root | Element | null | 作为视口的元素,默认为浏览器视口 |
rootMargin | string | "0px" | root 元素的边距,类似 CSS margin |
threshold | number/number[] | 0 | 目标元素可见比例,0-1 之间 |
实例方法
| 方法 | 说明 |
|---|---|
observe(target) | 开始观察目标元素 |
unobserve(target) | 停止观察目标元素 |
disconnect() | 停止观察所有元素 |
takeRecords() | 返回所有检测到的交叉变化并清空队列 |
代码示例
1. 基础用法 - 检测元素是否可见
// 创建观察者
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
console.log('元素进入视口:', entry.target);
} else {
console.log('元素离开视口:', entry.target);
}
});
}, {
threshold: 0.1 // 元素可见 10% 时触发
});
// 观察元素
const box = document.querySelector('.box');
observer.observe(box);2. 图片懒加载
const lazyImages = document.querySelectorAll('img[data-src]');
const imageObserver = new IntersectionObserver((entries, observer) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
const src = img.getAttribute('data-src');
// 设置真实图片地址
img.src = src;
// 图片加载完成后移除观察
img.onload = () => {
img.classList.add('loaded');
};
// 停止观察已加载的图片
observer.unobserve(img);
}
});
}, {
rootMargin: '50px 0px', // 提前 50px 开始加载
threshold: 0.01
});
// 开始观察所有懒加载图片
lazyImages.forEach(img => imageObserver.observe(img));3. 无限滚动加载更多
const loadMoreTrigger = document.querySelector('.load-more-trigger');
const infiniteObserver = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
loadMoreData();
}
});
}, {
rootMargin: '100px', // 距离底部 100px 时触发
threshold: 0.1
});
infiniteObserver.observe(loadMoreTrigger);
async function loadMoreData() {
// 防止重复加载
if (isLoading) return;
isLoading = true;
try {
const data = await fetchMoreItems();
renderItems(data);
} catch (error) {
console.error('加载失败:', error);
} finally {
isLoading = false;
}
}4. 滚动动画触发
const animatedElements = document.querySelectorAll('.animate-on-scroll');
const animationObserver = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.classList.add('visible');
// 动画完成后停止观察
const animationDuration = parseFloat(
getComputedStyle(entry.target).getPropertyValue('--animation-duration') || 0.5
);
setTimeout(() => {
animationObserver.unobserve(entry.target);
}, animationDuration * 1000);
}
});
}, {
threshold: 0.2 // 元素显示 20% 时触发
});
animatedElements.forEach(el => animationObserver.observe(el));5. 多阈值监听(元素滚动进度)
const progressBar = document.querySelector('.progress-bar');
const sectionObserver = new IntersectionObserver((entries) => {
entries.forEach(entry => {
const ratio = entry.intersectionRatio;
console.log(`当前可见比例: ${(ratio * 100).toFixed(0)}%`);
// 根据可见比例更新进度条
progressBar.style.width = `${ratio * 100}%`;
});
}, {
threshold: [0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1]
});
sectionObserver.observe(document.querySelector('.content-section'));visibilitychange
基础说明
visibilitychange 事件用于监听页面的可见性状态变化。当用户切换标签页、最小化窗口、切换到其他应用时触发。
核心属性
document.visibilityState 有三个可能的值:
| 值 | 说明 |
|---|---|
visible | 页面至少部分可见 |
hidden | 页面不可见 |
prerender | 页面正在预渲染(极少见) |
document.hidden 是一个布尔值,true 表示页面隐藏。
应用场景
- 暂停/恢复动画:页面隐藏时停止动画,节省资源
- 暂停/恢复音视频:页面隐藏时自动暂停播放
- 停止轮询:页面隐藏时停止定时请求
- 暂停游戏:页面隐藏时暂停游戏逻辑
代码示例
1. 基础用法
document.addEventListener('visibilitychange', () => {
if (document.hidden) {
console.log('页面已隐藏');
} else {
console.log('页面可见');
}
});
// 或者直接检查 visibilityState
document.addEventListener('visibilitychange', () => {
console.log('当前状态:', document.visibilityState);
});2. 暂停/恢复动画
let animationFrameId;
function animate() {
// 动画逻辑
updateCanvas();
if (!document.hidden) {
animationFrameId = requestAnimationFrame(animate);
}
}
// 页面隐藏时停止动画
document.addEventListener('visibilitychange', () => {
if (document.hidden) {
cancelAnimationFrame(animationFrameId);
console.log('动画已暂停');
} else {
console.log('动画已恢复');
animate();
}
});
// 启动动画
animate();3. 暂停/恢复视频
const video = document.querySelector('video');
document.addEventListener('visibilitychange', () => {
if (document.hidden) {
// 保存当前播放位置
video.dataset.currentTime = video.currentTime;
video.pause();
console.log('视频已暂停');
} else {
// 如果之前在播放,则恢复
if (video.dataset.currentTime && !video.ended) {
video.currentTime = parseFloat(video.dataset.currentTime);
video.play();
console.log('视频已恢复');
}
}
});4. 暂停数据轮询
let pollTimer;
function startPolling() {
pollTimer = setInterval(async () => {
try {
const data = await fetchData();
updateUI(data);
} catch (error) {
console.error('轮询失败:', error);
}
}, 5000); // 每 5 秒轮询一次
}
function stopPolling() {
clearInterval(pollTimer);
}
// 页面隐藏时停止轮询,可见时恢复
document.addEventListener('visibilitychange', () => {
if (document.hidden) {
stopPolling();
console.log('轮询已停止');
} else {
startPolling();
console.log('轮询已恢复');
}
});
// 启动轮询
startPolling();5. 游戏暂停/恢复
class Game {
constructor() {
this.isPaused = false;
this.setupVisibilityListener();
}
setupVisibilityListener() {
document.addEventListener('visibilitychange', () => {
if (document.hidden) {
this.pause();
} else {
this.resume();
}
});
}
pause() {
this.isPaused = true;
this.showPauseMenu();
this.lastPausedTime = Date.now();
console.log('游戏已暂停');
}
resume() {
this.isPaused = false;
this.hidePauseMenu();
// 调整游戏时间,防止跳帧
const pausedDuration = Date.now() - this.lastPausedTime;
this.adjustGameTime(pausedDuration);
this.gameLoop();
console.log('游戏已恢复');
}
gameLoop() {
if (this.isPaused) return;
this.update();
this.render();
requestAnimationFrame(() => this.gameLoop());
}
}6. 页面离开提示
// 用户离开页面时发送统计
document.addEventListener('visibilitychange', () => {
if (document.hidden) {
// 记录用户离开时间
localStorage.setItem('pageLeaveTime', Date.now().toString());
// 可选:发送离开事件
analytics.track('page_leave', {
duration: Date.now() - pageLoadTime
});
} else {
// 用户返回页面
const leaveTime = parseInt(localStorage.getItem('pageLeaveTime') || '0');
const awayDuration = Date.now() - leaveTime;
analytics.track('page_return', {
awayDuration: awayDuration
});
// 如果离开时间较长,可能需要刷新数据
if (awayDuration > 5 * 60 * 1000) { // 超过 5 分钟
refreshPageData();
}
}
});Web Animation API
基础说明
Web Animation API 提供了一组 JavaScript 接口,用于创建和控制动画。相比 CSS 动画,它提供了更好的控制能力和动态性。
核心方法
element.animate()
const animation = element.animate(keyframes, options);Animation 实例方法
| 方法 | 说明 |
|---|---|
play() | 开始或继续播放动画 |
pause() | 暂停动画 |
reverse() | 反转动画播放方向 |
finish() | 立即完成动画 |
cancel() | 取消动画并重置 |
updatePlaybackRate() | 更新播放速率 |
代码示例
1. 基础动画
const element = document.querySelector('.box');
// 创建动画
const animation = element.animate([
{ transform: 'translateX(0)', opacity: 0 },
{ transform: 'translateX(100px)', opacity: 1 },
{ transform: 'translateX(200px)', opacity: 0 }
], {
duration: 2000, // 动画持续时间(毫秒)
iterations: 3, // 重复次数
direction: 'alternate', // 交替方向
easing: 'ease-in-out', // 缓动函数
fill: 'forwards' // 动画结束后的状态
});
// 控制动画
animation.pause();
animation.play();
animation.reverse();2. 链式动画序列
async function sequenceAnimation() {
const element = document.querySelector('.box');
// 第一个动画:淡入
await element.animate(
[{ opacity: 0 }, { opacity: 1 }],
{ duration: 500, fill: 'forwards' }
).finished;
// 第二个动画:移动
await element.animate(
[{ transform: 'translateX(0)' }, { transform: 'translateX(200px)' }],
{ duration: 1000, fill: 'forwards' }
).finished;
// 第三个动画:放大并淡出
await element.animate(
[{ transform: 'scale(1)', opacity: 1 }, { transform: 'scale(1.5)', opacity: 0 }],
{ duration: 500, fill: 'forwards' }
).finished;
console.log('动画序列完成');
}
sequenceAnimation();3. 播放控制
const element = document.querySelector('.box');
const animation = element.animate(
[{ transform: 'rotate(0deg)' }, { transform: 'rotate(360deg)' }],
{ duration: 2000, iterations: Infinity }
);
// 播放控制按钮
const playBtn = document.querySelector('#play');
const pauseBtn = document.querySelector('#pause');
const reverseBtn = document.querySelector('#reverse');
const speedSlider = document.querySelector('#speed');
playBtn.addEventListener('click', () => animation.play());
pauseBtn.addEventListener('click', () => animation.pause());
reverseBtn.addEventListener('click', () => animation.reverse());
// 调整播放速度
speedSlider.addEventListener('input', (e) => {
const rate = parseFloat(e.target.value);
animation.updatePlaybackRate(rate);
});
// 获取播放进度
animation.currentTime = 500; // 设置动画到 500ms 的位置
console.log(animation.currentTime); // 当前时间
console.log(animation.playbackRate); // 播放速率4. 动态更新动画参数
const element = document.querySelector('.box');
let animation;
function createAnimation(duration, distance) {
// 如果已有动画,先取消
if (animation) {
animation.cancel();
}
// 创建新动画
animation = element.animate(
[{ transform: 'translateX(0)' }, { transform: `translateX(${distance}px)` }],
{ duration: duration, fill: 'forwards' }
);
return animation;
}
// 根据用户输入更新动画
const durationInput = document.querySelector('#duration');
const distanceInput = document.querySelector('#apply');
applyBtn.addEventListener('click', () => {
const duration = parseInt(durationInput.value);
const distance = parseInt(distanceInput.value);
createAnimation(duration, distance);
});5. 动画事件监听
const element = document.querySelector('.box');
const animation = element.animate(
[
{ transform: 'scale(0)', opacity: 0 },
{ transform: 'scale(1)', opacity: 1 }
],
{ duration: 1000, fill: 'forwards' }
);
// 监听动画完成
animation.onfinish = () => {
console.log('动画已完成');
};
// 监听动画取消
animation.oncancel = () => {
console.log('动画已取消');
};
// 使用事件监听器
animation.addEventListener('finish', () => {
element.classList.add('finished');
});
animation.addEventListener('cancel', () => {
console.log('动画被取消');
});6. 获取所有运行的动画
// 获取页面上所有运行的动画
const allAnimations = document.getAnimations();
console.log(`当前有 ${allAnimations.length} 个动画在运行`);
// 暂停所有动画
function pauseAllAnimations() {
document.getAnimations().forEach(anim => anim.pause());
}
// 恢复所有动画
function resumeAllAnimations() {
document.getAnimations().forEach(anim => anim.play());
}
// 与 visibilitychange 结合使用
document.addEventListener('visibilitychange', () => {
if (document.hidden) {
pauseAllAnimations();
} else {
resumeAllAnimations();
}
});7. 关键帧动画属性
const element = document.querySelector('.box');
// 使用 offset 控制关键帧位置
const animation = element.animate([
{ transform: 'translateX(0)', offset: 0 },
{ transform: 'translateX(100px)', offset: 0.3 }, // 30% 位置
{ transform: 'translateX(100px)', offset: 0.5 }, // 50% 位置,停留
{ transform: 'translateX(200px)', offset: 1 }
], {
duration: 2000,
fill: 'forwards'
});
// 复合属性动画
const complexAnimation = element.animate([
{
transform: 'translate(0, 0) rotate(0deg) scale(1)',
backgroundColor: '#ff0000',
offset: 0
},
{
transform: 'translate(100px, 50px) rotate(180deg) scale(1.5)',
backgroundColor: '#00ff00',
offset: 0.5
},
{
transform: 'translate(200px, 100px) rotate(360deg) scale(1)',
backgroundColor: '#0000ff',
offset: 1
}
], {
duration: 3000,
fill: 'forwards',
easing: 'cubic-bezier(0.68, -0.55, 0.27, 1.55)' // 弹跳效果
});8. 自定义缓动函数
const element = document.querySelector('.box');
// 使用 bezier 曲线
const animation = element.animate(
[{ transform: 'translateX(0)' }, { transform: 'translateX(200px)' }],
{
duration: 1000,
easing: 'cubic-bezier(0.68, -0.55, 0.27, 1.55)', // 弹跳效果
fill: 'forwards'
}
);
// 使用 steps 实现帧动画效果
const frameAnimation = element.animate(
[
{ backgroundPosition: '0px 0px' },
{ backgroundPosition: '-100px 0px' },
{ backgroundPosition: '-200px 0px' },
{ backgroundPosition: '-300px 0px' }
],
{
duration: 800,
easing: 'steps(4, end)', // 4 步,结束时变化
iterations: Infinity
}
);9. 动画进度同步
const element1 = document.querySelector('.box1');
const element2 = document.querySelector('.box2');
// 创建两个动画
const anim1 = element1.animate(
[{ transform: 'translateX(0)' }, { transform: 'translateX(200px)' }],
{ duration: 2000, fill: 'forwards' }
);
const anim2 = element2.animate(
[{ transform: 'translateX(0)' }, { transform: 'translateX(200px)' }],
{ duration: 2000, fill: 'forwards' }
);
// 拖动滑块同步控制动画进度
const slider = document.querySelector('#progress-slider');
slider.addEventListener('input', (e) => {
const progress = parseFloat(e.target.value);
const duration = anim1.effect.getTiming().duration;
// 同步两个动画的时间
anim1.currentTime = duration * progress;
anim2.currentTime = duration * progress;
});10. 与 CSS 类结合
// 通过 JS 触发 CSS 定义的动画
const element = document.querySelector('.box');
element.classList.add('animate-in');
const animation = element.getAnimations()[0];
// 监听动画完成
animation.onfinish = () => {
element.classList.remove('animate-in');
console.log('入场动画完成');
};Clipboard API
基础说明
Clipboard API 提供了读写剪贴板内容的能力。现代浏览器推荐使用异步的 navigator.clipboard API。
核心方法
| 方法 | 说明 |
|---|---|
writeText(text) | 写入文本到剪贴板 |
readText() | 从剪贴板读取文本 |
write(data) | 写入任意类型数据(如图片) |
read() | 从剪贴板读取任意类型数据 |
代码示例
1. 复制文本到剪贴板
// 基础用法
async function copyToClipboard(text) {
try {
await navigator.clipboard.writeText(text);
console.log('文本已复制到剪贴板');
} catch (err) {
console.error('复制失败:', err);
}
}
copyToClipboard('Hello, World!');2. 从剪贴板读取文本
async function pasteFromClipboard() {
try {
const text = await navigator.clipboard.readText();
console.log('剪贴板内容:', text);
return text;
} catch (err) {
console.error('读取失败:', err);
return '';
}
}
pasteFromClipboard();3. 点击按钮复制
const copyBtn = document.querySelector('#copy-btn');
copyBtn.addEventListener('click', async () => {
const textToCopy = document.querySelector('#text-content').textContent;
try {
await navigator.clipboard.writeText(textToCopy);
copyBtn.textContent = '已复制!';
copyBtn.classList.add('success');
// 2秒后恢复按钮文字
setTimeout(() => {
copyBtn.textContent = '复制';
copyBtn.classList.remove('success');
}, 2000);
} catch (err) {
copyBtn.textContent = '复制失败';
console.error('复制失败:', err);
}
});4. 复制图片到剪贴板
async function copyImageToClipboard(blob) {
try {
const item = new ClipboardItem({ 'image/png': blob });
await navigator.clipboard.write([item]);
console.log('图片已复制到剪贴板');
} catch (err) {
console.error('复制图片失败:', err);
}
}
// 从 Canvas 复制
const canvas = document.querySelector('canvas');
canvas.toBlob(async (blob) => {
if (blob) {
await copyImageToClipboard(blob);
}
}, 'image/png');5. 监听剪贴板变化
// 通过监听 paste 事件获取剪贴板内容
document.addEventListener('paste', async (e) => {
const items = e.clipboardData.items;
const clipboardData = [];
for (const item of items) {
if (item.type === 'text/plain') {
const text = await new Promise(resolve => {
item.getAsString(resolve);
});
clipboardData.push({ type: 'text', content: text });
} else if (item.type.startsWith('image/')) {
const blob = item.getAsFile();
clipboardData.push({ type: 'image', content: blob });
}
}
console.log('剪贴板内容:', clipboardData);
});6. 兼容性处理(降级方案)
async function copyToClipboard(text) {
// 优先使用现代 API
if (navigator.clipboard && navigator.clipboard.writeText) {
try {
await navigator.clipboard.writeText(text);
return true;
} catch (err) {
console.warn('现代 API 失败,尝试降级方案');
}
}
// 降级方案:使用 execCommand
const textarea = document.createElement('textarea');
textarea.value = text;
textarea.style.position = 'fixed';
textarea.style.opacity = '0';
document.body.appendChild(textarea);
textarea.select();
try {
const success = document.execCommand('copy');
document.body.removeChild(textarea);
return success;
} catch (err) {
document.body.removeChild(textarea);
console.error('降级方案也失败了:', err);
return false;
}
}Notification API
基础说明
Notification API 允许网页向用户发送系统桌面通知。
权限请求
| 状态 | 说明 |
|---|---|
granted | 用户已授权 |
denied | 用户已拒绝 |
default | 用户尚未选择 |
代码示例
1. 请求权限并发送通知
async function sendNotification(title, options = {}) {
// 检查浏览器支持
if (!('Notification' in window)) {
alert('此浏览器不支持桌面通知');
return;
}
// 请求权限
const permission = await Notification.requestPermission();
if (permission !== 'granted') {
console.log('用户拒绝了通知权限');
return;
}
// 创建通知
const notification = new Notification(title, {
body: options.body || '',
icon: options.icon || '/icon.png',
image: options.image,
badge: options.badge,
tag: options.tag || 'default',
data: options.data,
requireInteraction: options.requireInteraction || false,
silent: options.silent || false,
vibrate: options.vibrate
});
return notification;
}
// 使用示例
sendNotification('新消息', {
body: '您有一条新的私信',
icon: '/message-icon.png',
tag: 'message-123'
});2. 处理通知事件
function sendInteractiveNotification() {
const notification = new Notification('任务提醒', {
body: '您有一个待处理的任务',
icon: '/task-icon.png',
tag: 'task-reminder',
data: { taskId: '123', type: 'reminder' }
});
// 用户点击通知
notification.onclick = () => {
window.focus();
console.log('通知被点击:', notification.data);
// 跳转到对应页面
window.location.href = `/tasks/${notification.data.taskId}`;
notification.close();
};
// 通知显示时
notification.onshow = () => {
console.log('通知已显示');
// 5秒后自动关闭
setTimeout(() => notification.close(), 5000);
};
// 通知关闭时
notification.onclose = () => {
console.log('通知已关闭');
};
// 通知出错时
notification.onerror = () => {
console.error('通知出错');
};
}3. 通知分组
function sendGroupedNotifications() {
// 发送多条通知,使用相同的 tag 会自动分组替换
const notifications = [
{ title: '消息 1', body: '第一条消息内容' },
{ title: '消息 2', body: '第二条消息内容' },
{ title: '消息 3', body: '第三条消息内容' }
];
notifications.forEach((item, index) => {
setTimeout(() => {
new Notification(item.title, {
body: item.body,
tag: 'messages-group',
renotify: true // 重新提醒用户
});
}, index * 1000);
});
}4. 关闭所有通知
function closeAllNotifications() {
// Notification 静态方法,获取所有通知
const notifications = Notification.getNotifications();
notifications.forEach(notification => {
notification.close();
});
}5. 服务端推送通知(结合 Service Worker)
// 在 Service Worker 中
self.addEventListener('push', (event) => {
const options = {
body: event.data.text(),
icon: '/icon.png',
badge: '/badge.png',
vibrate: [200, 100, 200]
};
event.waitUntil(
self.registration.showNotification('新消息', options)
);
});
self.addEventListener('notificationclick', (event) => {
event.notification.close();
event.waitUntil(
clients.openWindow('/messages')
);
});History API
基础说明
History API 提供了管理浏览器会话历史的能力,可以在不刷新页面的情况下修改 URL。
核心方法
| 方法 | 说明 |
|---|---|
pushState(state, title, url) | 添加新历史记录 |
replaceState(state, title, url) | 替换当前历史记录 |
back() | 后退一页 |
forward() | 前进一页 |
go(delta) | 前进/后退指定步数 |
核心事件
| 事件 | 说明 |
|---|---|
popstate | 历史记录变化时触发 |
代码示例
1. 基础用法 - 修改 URL
// 添加新历史记录
history.pushState({ page: 'home' }, '首页', '/home');
history.pushState({ page: 'about' }, '关于', '/about');
// 当前 URL: /about
// 历史记录: [/, /home, /about]
// 替换当前历史记录
history.replaceState({ page: 'profile' }, '个人资料', '/profile');
// 当前 URL: /profile
// 历史记录: [/, /home, /profile]2. 监听历史变化
// 监听 popstate 事件(用户点击前进/后退按钮时触发)
window.addEventListener('popstate', (event) => {
console.log('历史状态:', event.state);
console.log('当前 URL:', location.pathname);
// 根据状态渲染页面
if (event.state?.page === 'home') {
renderHomePage();
} else if (event.state?.page === 'about') {
renderAboutPage();
}
});3. 实现单页应用路由
class Router {
constructor() {
this.routes = {};
this.init();
}
// 注册路由
register(path, handler) {
this.routes[path] = handler;
}
// 初始化
init() {
// 监听 popstate
window.addEventListener('popstate', () => {
this.handleRoute();
});
// 处理初始加载
this.handleRoute();
}
// 路由处理
handleRoute() {
const path = location.pathname;
const handler = this.routes[path] || this.routes['/404'];
if (handler) {
handler();
}
}
// 导航
navigate(path, state = {}) {
history.pushState(state, '', path);
this.handleRoute();
}
}
// 使用
const router = new Router();
router.register('/', () => renderHome());
router.register('/about', () => renderAbout());
router.register('/contact', () => renderContact());
router.register('/404', () => renderNotFound());
// 导航到新页面
router.navigate('/about', { from: 'home' });4. 查询参数管理
class QueryManager {
// 获取查询参数
static get(name) {
const params = new URLSearchParams(location.search);
return params.get(name);
}
// 获取所有查询参数
static getAll() {
return Object.fromEntries(new URLSearchParams(location.search));
}
// 设置查询参数
static set(params, replace = false) {
const url = new URL(location.href);
Object.entries(params).forEach(([key, value]) => {
url.searchParams.set(key, value);
});
const method = replace ? 'replaceState' : 'pushState';
history[method]({ params }, '', url);
}
// 删除查询参数
static remove(...keys) {
const url = new URL(location.href);
keys.forEach(key => {
url.searchParams.delete(key);
});
history.pushState({}, '', url);
}
}
// 使用
QueryManager.set({ page: 2, sort: 'desc' });
console.log(QueryManager.get('page')); // '2'
console.log(QueryManager.getAll()); // { page: '2', sort: 'desc' }
QueryManager.remove('sort');5. 页面滚动位置恢复
class ScrollRestorer {
constructor() {
this.scrollPositions = new Map();
this.init();
}
init() {
// 保存滚动位置
window.addEventListener('beforeunload', () => {
const key = location.pathname;
this.scrollPositions.set(key, window.scrollY);
});
// 监听历史变化
window.addEventListener('popstate', () => {
this.restoreScroll();
});
// 页面加载后恢复
window.addEventListener('load', () => {
this.restoreScroll();
});
}
restoreScroll() {
const key = location.pathname;
const scrollY = this.scrollPositions.get(key);
if (scrollY !== undefined) {
window.scrollTo(0, scrollY);
}
}
}
const scrollRestorer = new ScrollRestorer();LocalStorage / SessionStorage
基础说明
LocalStorage 和 SessionStorage 是 Web Storage API 的两种存储方式:
| 特性 | LocalStorage | SessionStorage |
|---|---|---|
| 存储大小 | ~5MB | ~5MB |
| 持久性 | 永久保存(除非手动清除) | 会话结束(关闭标签页)后清除 |
| 作用域 | 同源的所有标签页共享 | 同源的同标签页共享 |
| 存储位置 | 磁盘 | 内存 |
核心方法
| 方法 | 说明 |
|---|---|
setItem(key, value) | 存储数据 |
getItem(key) | 获取数据 |
removeItem(key) | 删除数据 |
clear() | 清空所有数据 |
key(index) | 获取指定索引的键名 |
length | 存储的数据项数量 |
代码示例
1. 基础用法
// LocalStorage
localStorage.setItem('username', '张三');
localStorage.setItem('age', '25');
localStorage.setItem('isLoggedIn', 'true');
const username = localStorage.getItem('username'); // '张三'
const age = localStorage.getItem('age'); // '25'
// SessionStorage
sessionStorage.setItem('tempData', '临时数据');
sessionStorage.setItem('step', '2');
const tempData = sessionStorage.getItem('tempData'); // '临时数据'2. 存储复杂对象
// 需要先序列化为 JSON 字符串
const user = {
id: 1,
name: '张三',
email: '[email protected]',
preferences: {
theme: 'dark',
language: 'zh-CN'
}
};
// 存储
localStorage.setItem('user', JSON.stringify(user));
// 读取
const storedUser = JSON.parse(localStorage.getItem('user'));
console.log(storedUser.preferences.theme); // 'dark'3. 封装工具类
class StorageHelper {
constructor(storage = localStorage) {
this.storage = storage;
}
// 存储数据
set(key, value, expire = null) {
const data = {
value,
expire: expire ? Date.now() + expire * 1000 : null
};
this.storage.setItem(key, JSON.stringify(data));
}
// 获取数据
get(key, defaultValue = null) {
const item = this.storage.getItem(key);
if (!item) return defaultValue;
try {
const data = JSON.parse(item);
// 检查是否过期
if (data.expire && data.expire < Date.now()) {
this.remove(key);
return defaultValue;
}
return data.value;
} catch {
return item;
}
}
// 删除数据
remove(key) {
this.storage.removeItem(key);
}
// 清空数据
clear() {
this.storage.clear();
}
// 检查是否存在
has(key) {
return this.storage.getItem(key) !== null;
}
// 获取所有键
keys() {
return Object.keys(this.storage);
}
// 获取所有数据
all() {
const result = {};
for (let i = 0; i < this.storage.length; i++) {
const key = this.storage.key(i);
result[key] = this.get(key);
}
return result;
}
}
// 使用
const local = new StorageHelper(localStorage);
const session = new StorageHelper(sessionStorage);
// 带过期时间的存储
local.set('token', 'abc123', 3600); // 1小时后过期4. 跨标签页同步
// 通过 storage 事件监听其他标签页的存储变化
window.addEventListener('storage', (event) => {
console.log('存储变化:', {
key: event.key,
oldValue: event.oldValue,
newValue: event.newValue,
url: event.url,
storageArea: event.storageArea // localStorage 或 sessionStorage
});
// 根据变化执行相应操作
if (event.key === 'user-theme') {
applyTheme(event.newValue);
}
});
// 在任何标签页修改存储,其他标签页会收到通知
localStorage.setItem('user-theme', 'dark');5. 用户偏好设置管理
class UserPreferences {
constructor() {
this.prefix = 'pref_';
this.defaults = {
theme: 'light',
language: 'zh-CN',
fontSize: '16',
sidebar: 'expanded'
};
}
// 获取设置
get(key) {
const fullKey = this.prefix + key;
return localStorage.getItem(fullKey) || this.defaults[key];
}
// 设置
set(key, value) {
const fullKey = this.prefix + key;
localStorage.setItem(fullKey, value);
}
// 批量设置
setMultiple(prefs) {
Object.entries(prefs).forEach(([key, value]) => {
this.set(key, value);
});
}
// 重置为默认
reset(key = null) {
if (key) {
localStorage.removeItem(this.prefix + key);
} else {
Object.keys(this.defaults).forEach(k => {
localStorage.removeItem(this.prefix + k);
});
}
}
// 应用设置到页面
apply() {
document.documentElement.setAttribute('data-theme', this.get('theme'));
document.documentElement.setAttribute('lang', this.get('language'));
document.documentElement.style.fontSize = this.get('fontSize') + 'px';
document.body.classList.toggle('sidebar-collapsed', this.get('sidebar') === 'collapsed');
}
}
const preferences = new UserPreferences();
// 初始化时应用
preferences.apply();6. 表单数据自动保存
class FormAutoSave {
constructor(formId, storageKey) {
this.form = document.querySelector(`#${formId}`);
this.storageKey = storageKey;
this.init();
}
init() {
// 恢复数据
this.restore();
// 监听表单变化
this.form.addEventListener('input', () => {
this.save();
});
// 提交后清除
this.form.addEventListener('submit', () => {
this.clear();
});
}
save() {
const formData = new FormData(this.form);
const data = {};
for (const [key, value] of formData.entries()) {
data[key] = value;
}
sessionStorage.setItem(this.storageKey, JSON.stringify(data));
}
restore() {
const saved = sessionStorage.getItem(this.storageKey);
if (!saved) return;
try {
const data = JSON.parse(saved);
Object.entries(data).forEach(([key, value]) => {
const input = this.form.querySelector(`[name="${key}"]`);
if (input) {
input.value = value;
}
});
} catch (e) {
console.error('恢复表单数据失败:', e);
}
}
clear() {
sessionStorage.removeItem(this.storageKey);
}
}
// 使用
const autoSave = new FormAutoSave('comment-form', 'form-draft');Fetch API
基础说明
Fetch API 是现代浏览器提供的网络请求接口,基于 Promise 设计,用于替代 XMLHttpRequest。
基础语法
fetch(url, options)
.then(response => {
// 处理响应
})
.catch(error => {
// 处理错误
});代码示例
1. 基础 GET 请求
fetch('https://api.example.com/users')
.then(response => response.json())
.then(data => {
console.log('数据:', data);
})
.catch(error => {
console.error('请求失败:', error);
});2. POST 请求
fetch('https://api.example.com/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer token123'
},
body: JSON.stringify({
name: '张三',
email: '[email protected]'
})
})
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error(error));3. 上传文件
const fileInput = document.querySelector('#file');
fileInput.addEventListener('change', async () => {
const file = fileInput.files[0];
const formData = new FormData();
formData.append('file', file);
try {
const response = await fetch('https://api.example.com/upload', {
method: 'POST',
body: formData
});
const result = await response.json();
console.log('上传成功:', result);
} catch (error) {
console.error('上传失败:', error);
}
});4. 封装 Fetch 请求
class HttpClient {
constructor(baseURL = '', defaultOptions = {}) {
this.baseURL = baseURL;
this.defaultOptions = {
headers: {
'Content-Type': 'application/json',
},
...defaultOptions
};
}
async request(url, options = {}) {
const fullURL = this.baseURL + url;
const config = { ...this.defaultOptions, ...options };
try {
const response = await fetch(fullURL, config);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
// 根据响应类型返回不同格式
const contentType = response.headers.get('content-type');
if (contentType?.includes('application/json')) {
return await response.json();
} else if (contentType?.includes('text/')) {
return await response.text();
} else if (contentType?.includes('image/')) {
return await response.blob();
}
return await response.text();
} catch (error) {
console.error('请求失败:', error);
throw error;
}
}
get(url, options) {
return this.request(url, { ...options, method: 'GET' });
}
post(url, data, options) {
return this.request(url, {
...options,
method: 'POST',
body: JSON.stringify(data)
});
}
put(url, data, options) {
return this.request(url, {
...options,
method: 'PUT',
body: JSON.stringify(data)
});
}
delete(url, options) {
return this.request(url, { ...options, method: 'DELETE' });
}
patch(url, data, options) {
return this.request(url, {
...options,
method: 'PATCH',
body: JSON.stringify(data)
});
}
}
// 使用
const api = new HttpClient('https://api.example.com', {
headers: {
'Authorization': 'Bearer token123'
}
});
api.get('/users')
.then(users => console.log(users));
api.post('/users', { name: '张三' })
.then(user => console.log(user));5. 请求拦截器
class FetchInterceptor {
constructor() {
this.beforeRequest = [];
this.afterResponse = [];
this.onError = [];
this.init();
}
init() {
const originalFetch = window.fetch;
window.fetch = async (...args) => {
const [url, options = {}] = args;
// 请求前拦截
for (const interceptor of this.beforeRequest) {
const result = await interceptor(url, options);
if (result) {
args[0] = result.url;
args[1] = result.options || options;
}
}
try {
const response = await originalFetch(...args);
// 响应后拦截
for (const interceptor of this.afterResponse) {
const result = await interceptor(response.clone());
if (result) {
return result;
}
}
return response;
} catch (error) {
// 错误拦截
for (const interceptor of this.onError) {
await interceptor(error, url, options);
}
throw error;
}
};
}
onRequest(callback) {
this.beforeRequest.push(callback);
}
onResponse(callback) {
this.afterResponse.push(callback);
}
onError(callback) {
this.onError.push(callback);
}
}
// 使用
const interceptor = new FetchInterceptor();
// 添加 token
interceptor.onRequest((url, options) => {
const token = localStorage.getItem('token');
if (token) {
options.headers = {
...options.headers,
'Authorization': `Bearer ${token}`
};
}
return { url, options };
});
// 处理错误
interceptor.onError((error, url) => {
console.error('请求出错:', url, error);
});6. 请求取消
class AbortableFetch {
static async fetch(url, options = {}) {
const controller = new AbortController();
const { timeout = 5000, ...fetchOptions } = options;
// 超时取消
const timeoutId = setTimeout(() => {
controller.abort();
}, timeout);
try {
const response = await fetch(url, {
...fetchOptions,
signal: controller.signal
});
clearTimeout(timeoutId);
return response;
} catch (error) {
clearTimeout(timeoutId);
if (error.name === 'AbortError') {
throw new Error('请求超时');
}
throw error;
}
}
}
// 使用
AbortableFetch.fetch('https://api.example.com/data', { timeout: 3000 })
.then(response => response.json())
.catch(error => console.error(error));ResizeObserver
基础说明
ResizeObserver 用于监听元素尺寸变化,比 window.resize 更精确,可以监听任意 DOM 元素。
构造函数
const observer = new ResizeObserver(callback);代码示例
1. 基础用法
const box = document.querySelector('.box');
const resizeObserver = new ResizeObserver((entries) => {
entries.forEach(entry => {
const { width, height } = entry.contentRect;
console.log(`尺寸变化: ${width}px × ${height}px`);
});
});
resizeObserver.observe(box);2. 响应式组件
class ResponsiveComponent {
constructor(element) {
this.element = element;
this.breakpoints = {
mobile: 480,
tablet: 768,
desktop: 1024
};
this.init();
}
init() {
const observer = new ResizeObserver((entries) => {
const { width } = entries[0].contentRect;
this.handleResize(width);
});
observer.observe(this.element);
this.handleResize(this.element.offsetWidth);
}
handleResize(width) {
let device;
if (width < this.breakpoints.mobile) {
device = 'mobile';
} else if (width < this.breakpoints.tablet) {
device = 'tablet';
} else if (width < this.breakpoints.desktop) {
device = 'desktop';
} else {
device = 'large';
}
this.element.setAttribute('data-device', device);
this.updateLayout(device);
}
updateLayout(device) {
// 根据设备类型更新布局
console.log('当前设备:', device);
}
}
new ResponsiveComponent(document.querySelector('.container'));3. 监听多个元素
const elements = document.querySelectorAll('.grid-item');
const resizeObserver = new ResizeObserver((entries) => {
entries.forEach(entry => {
const { width, height } = entry.contentRect;
const el = entry.target;
// 根据尺寸添加样式
if (width < 200) {
el.classList.add('small');
} else if (width < 400) {
el.classList.add('medium');
} else {
el.classList.add('large');
}
});
});
elements.forEach(el => resizeObserver.observe(el));4. 防抖处理
class DebouncedResizeObserver {
constructor(callback, delay = 100) {
this.callback = callback;
this.delay = delay;
this.timer = null;
this.observer = new ResizeObserver(this.onResize.bind(this));
}
onResize(entries) {
clearTimeout(this.timer);
this.timer = setTimeout(() => {
this.callback(entries);
}, this.delay);
}
observe(target) {
this.observer.observe(target);
}
unobserve(target) {
this.observer.unobserve(target);
}
disconnect() {
clearTimeout(this.timer);
this.observer.disconnect();
}
}
// 使用
const observer = new DebouncedResizeObserver((entries) => {
console.log('尺寸变化(防抖后):', entries);
}, 200);
observer.observe(document.querySelector('.content'));MutationObserver
基础说明
MutationObserver 用于监听 DOM 树的变化,包括节点增删、属性变更、文本内容变化等。
配置选项
| 选项 | 说明 |
|---|---|
childList | 观察子节点的增删 |
subtree | 观察所有后代节点 |
attributes | 观察属性变化 |
attributeFilter | 只观察指定的属性 |
characterData | 观察文本内容变化 |
characterDataOldValue | 记录文本内容旧值 |
attributeOldValue | 记录属性旧值 |
代码示例
1. 基础用法 - 监听子节点变化
const container = document.querySelector('#container');
const observer = new MutationObserver((mutations) => {
mutations.forEach(mutation => {
console.log('变化类型:', mutation.type);
console.log('添加的节点:', mutation.addedNodes);
console.log('删除的节点:', mutation.removedNodes);
});
});
observer.observe(container, {
childList: true // 监听子节点增删
});
// 测试
container.appendChild(document.createElement('div'));2. 监听属性变化
const element = document.querySelector('#my-element');
const observer = new MutationObserver((mutations) => {
mutations.forEach(mutation => {
if (mutation.type === 'attributes') {
console.log('属性变化:', {
attributeName: mutation.attributeName,
oldValue: mutation.oldValue,
newValue: element.getAttribute(mutation.attributeName)
});
}
});
});
observer.observe(element, {
attributes: true,
attributeFilter: ['class', 'data-value'],
attributeOldValue: true
});3. 监听样式变化
const element = document.querySelector('.box');
const observer = new MutationObserver(() => {
const computedStyle = window.getComputedStyle(element);
console.log('背景色:', computedStyle.backgroundColor);
});
observer.observe(element, {
attributes: true,
attributeFilter: ['style', 'class']
});
// 修改样式会触发
element.style.backgroundColor = 'red';
element.classList.add('active');4. 监听表单输入
const form = document.querySelector('form');
const observer = new MutationObserver((mutations) => {
mutations.forEach(mutation => {
if (mutation.type === 'characterData') {
console.log('文本变化:', mutation.target.textContent);
}
});
});
observer.observe(form, {
subtree: true,
characterData: true,
childList: true
});5. 动态元素自动绑定事件
class DynamicEventBinder {
constructor(container, selector, eventType, handler) {
this.container = container;
this.selector = selector;
this.eventType = eventType;
this.handler = handler;
this.init();
}
init() {
// 为已有元素绑定事件
this.bindEvents();
// 监听 DOM 变化
const observer = new MutationObserver((mutations) => {
mutations.forEach(mutation => {
mutation.addedNodes.forEach(node => {
if (node.nodeType === Node.ELEMENT_NODE) {
// 新增的元素绑定事件
if (node.matches(this.selector)) {
node.addEventListener(this.eventType, this.handler);
}
// 子元素中匹配的也绑定
node.querySelectorAll(this.selector).forEach(el => {
el.addEventListener(this.eventType, this.handler);
});
}
});
});
});
observer.observe(this.container, {
childList: true,
subtree: true
});
}
bindEvents() {
this.container.querySelectorAll(this.selector).forEach(el => {
el.addEventListener(this.eventType, this.handler);
});
}
}
// 使用
const binder = new DynamicEventBinder(
document.body,
'.button',
'click',
(e) => console.log('按钮被点击:', e.target)
);Performance API
基础说明
Performance API 用于测量页面性能,提供高精度的时间戳和性能指标。
核心方法
| 方法 | 说明 |
|---|---|
performance.now() | 获取高精度时间戳(毫秒) |
performance.mark(name) | 创建性能标记 |
performance.measure(name, startMark, endMark) | 测量标记间时间 |
performance.getEntries() | 获取性能条目 |
performance.getEntriesByName(name) | 按名称获取条目 |
代码示例
1. 测量代码执行时间
// 使用 performance.now()
const start = performance.now();
// 执行代码
for (let i = 0; i < 1000000; i++) {
Math.sqrt(i);
}
const end = performance.now();
console.log(`执行时间: ${(end - start).toFixed(2)}ms`);2. 使用标记测量
// 创建标记
performance.mark('start-task');
// 执行任务
doSomeWork();
performance.mark('end-task');
// 测量时间
performance.measure('task-duration', 'start-task', 'end-task');
// 获取测量结果
const measures = performance.getEntriesByName('task-duration');
console.log('任务耗时:', measures[0].duration.toFixed(2) + 'ms');
// 清除标记
performance.clearMarks();
performance.clearMeasures();3. 页面加载性能分析
window.addEventListener('load', () => {
const timing = performance.timing;
const metrics = {
// DNS 查询时间
dnsLookup: timing.domainLookupEnd - timing.domainLookupStart,
// TCP 连接时间
tcpConnect: timing.connectEnd - timing.connectStart,
// 请求时间
requestTime: timing.responseEnd - timing.requestStart,
// DOM 解析时间
domParsing: timing.domComplete - timing.domLoading,
// 资源加载时间
resourceLoading: timing.loadEventEnd - timing.domContentLoadedEventEnd,
// 总页面加载时间
pageLoad: timing.loadEventEnd - timing.navigationStart
};
console.table(metrics);
});4. 资源加载性能
window.addEventListener('load', () => {
// 获取所有资源条目
const resources = performance.getEntriesByType('resource');
// 按类型分组
const byType = resources.reduce((acc, resource) => {
const type = resource.initiatorType;
if (!acc[type]) acc[type] = [];
acc[type].push({
name: resource.name,
duration: resource.duration.toFixed(2),
size: resource.transferSize
});
return acc;
}, {});
console.log('资源加载性能:', byType);
// 找出最慢的资源
const slowest = resources
.sort((a, b) => b.duration - a.duration)
.slice(0, 5);
console.log('最慢的5个资源:', slowest);
});5. FPS 监控
class FPSMonitor {
constructor() {
this.fps = 0;
this.frames = 0;
this.lastTime = performance.now();
this.init();
}
init() {
this.measure();
}
measure() {
this.frames++;
const currentTime = performance.now();
if (currentTime >= this.lastTime + 1000) {
this.fps = Math.round((this.frames * 1000) / (currentTime - this.lastTime));
this.frames = 0;
this.lastTime = currentTime;
this.onFPSChange(this.fps);
}
requestAnimationFrame(() => this.measure());
}
onFPSChange(fps) {
console.log(`当前 FPS: ${fps}`);
// 可在页面上显示 FPS
const fpsElement = document.querySelector('#fps-counter');
if (fpsElement) {
fpsElement.textContent = fps;
fpsElement.style.color = fps < 30 ? 'red' : fps < 50 ? 'orange' : 'green';
}
}
}
new FPSMonitor();Service Worker / Cache API
基础说明
Service Worker 是运行在浏览器后台的脚本,支持离线缓存、推送通知、后台同步等 PWA 核心功能。
Service Worker 生命周期
| 阶段 | 说明 |
|---|---|
installing | 安装中 |
installed | 已安装,等待激活 |
activating | 激活中 |
activated | 已激活,可以控制页面 |
redundant | 已被替换 |
代码示例
1. 注册 Service Worker
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js')
.then(registration => {
console.log('SW 注册成功:', registration);
})
.catch(error => {
console.error('SW 注册失败:', error);
});
}2. 基础 Service Worker (sw.js)
const CACHE_NAME = 'my-app-v1';
const URLS_TO_CACHE = [
'/',
'/index.html',
'/styles.css',
'/app.js',
'/offline.html'
];
// 安装事件:缓存资源
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE_NAME)
.then(cache => cache.addAll(URLS_TO_CACHE))
.then(() => self.skipWaiting()) // 立即激活
);
});
// 激活事件:清理旧缓存
self.addEventListener('activate', (event) => {
event.waitUntil(
caches.keys()
.then(cacheNames => {
return Promise.all(
cacheNames.map(name => {
if (name !== CACHE_NAME) {
return caches.delete(name);
}
})
);
})
.then(() => self.clients.claim()) // 控制所有客户端
);
});
// 拦截请求
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request)
.then(response => {
// 缓存命中
if (response) {
return response;
}
// 缓存未命中,发起网络请求
return fetch(event.request)
.then(response => {
// 只缓存成功的响应
if (!response || response.status !== 200 || response.type !== 'basic') {
return response;
}
// 缓存响应
const responseToCache = response.clone();
caches.open(CACHE_NAME)
.then(cache => cache.put(event.request, responseToCache));
return response;
})
.catch(() => {
// 网络失败,返回离线页面
return caches.match('/offline.html');
});
})
);
});3. 网络优先策略
self.addEventListener('fetch', (event) => {
event.respondWith(
fetch(event.request)
.catch(() => caches.match(event.request))
);
});4. 缓存优先策略
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request)
.then(response => {
return response || fetch(event.request);
})
);
});5. 网络与缓存并行
self.addEventListener('fetch', (event) => {
event.respondWith(
Promise.all([
caches.match(event.request),
fetch(event.request)
])
.then(([cached, network]) => {
return network || cached;
})
);
});6. 动态更新检查
// 在主页面
async function checkForUpdates() {
const registration = await navigator.serviceWorker.getRegistration();
registration.addEventListener('updatefound', () => {
const newWorker = registration.installing;
newWorker.addEventListener('statechange', () => {
if (newWorker.state === 'installed' && navigator.serviceWorker.controller) {
// 有新版本可用
showUpdateButton();
}
});
});
await registration.update();
}
function showUpdateButton() {
const btn = document.createElement('button');
btn.textContent = '发现新版本,点击更新';
btn.addEventListener('click', () => {
location.reload();
});
document.body.appendChild(btn);
}Drag and Drop API
基础说明
Drag and Drop API 提供了拖放功能的原生实现。
事件类型
| 事件 | 触发时机 |
|---|---|
dragstart | 开始拖动 |
drag | 拖动中 |
dragend | 拖动结束 |
dragenter | 进入目标区域 |
dragover | 在目标区域上方 |
dragleave | 离开目标区域 |
drop | 放置 |
代码示例
1. 基础拖放
<div id="draggable" draggable="true">拖动我</div>
<div id="dropzone">放到这里</div>const draggable = document.querySelector('#draggable');
const dropzone = document.querySelector('#dropzone');
// 拖动开始
draggable.addEventListener('dragstart', (e) => {
e.dataTransfer.setData('text/plain', '拖动的内容');
e.dataTransfer.effectAllowed = 'move';
draggable.classList.add('dragging');
});
// 拖动结束
draggable.addEventListener('dragend', () => {
draggable.classList.remove('dragging');
});
// 允许放置
dropzone.addEventListener('dragover', (e) => {
e.preventDefault();
e.dataTransfer.dropEffect = 'move';
dropzone.classList.add('drag-over');
});
// 离开区域
dropzone.addEventListener('dragleave', () => {
dropzone.classList.remove('drag-over');
});
// 放置
dropzone.addEventListener('drop', (e) => {
e.preventDefault();
dropzone.classList.remove('drag-over');
const data = e.dataTransfer.getData('text/plain');
console.log('接收到的数据:', data);
});2. 文件拖放上传
const dropzone = document.querySelector('.dropzone');
dropzone.addEventListener('dragover', (e) => {
e.preventDefault();
dropzone.classList.add('drag-over');
});
dropzone.addEventListener('dragleave', () => {
dropzone.classList.remove('drag-over');
});
dropzone.addEventListener('drop', async (e) => {
e.preventDefault();
dropzone.classList.remove('drag-over');
const files = e.dataTransfer.files;
handleFiles(files);
});
async function handleFiles(files) {
const formData = new FormData();
for (const file of files) {
console.log('文件:', file.name, file.size, file.type);
formData.append('files', file);
}
try {
const response = await fetch('/upload', {
method: 'POST',
body: formData
});
const result = await response.json();
console.log('上传成功:', result);
} catch (error) {
console.error('上传失败:', error);
}
}3. 列表排序拖放
class SortableList {
constructor(container) {
this.container = container;
this.draggedItem = null;
this.init();
}
init() {
this.container.addEventListener('dragstart', (e) => {
this.draggedItem = e.target.closest('.sortable-item');
if (this.draggedItem) {
this.draggedItem.classList.add('dragging');
e.dataTransfer.effectAllowed = 'move';
}
});
this.container.addEventListener('dragend', (e) => {
const item = e.target.closest('.sortable-item');
if (item) {
item.classList.remove('dragging');
}
this.draggedItem = null;
});
this.container.addEventListener('dragover', (e) => {
e.preventDefault();
const afterElement = this.getDragAfterElement(this.container, e.clientY);
if (afterElement == null) {
this.container.appendChild(this.draggedItem);
} else {
this.container.insertBefore(this.draggedItem, afterElement);
}
});
}
getDragAfterElement(container, y) {
const draggableElements = [...container.querySelectorAll('.sortable-item:not(.dragging)')];
return draggableElements.reduce((closest, child) => {
const box = child.getBoundingClientRect();
const offset = y - box.top - box.height / 2;
if (offset < 0 && offset > closest.offset) {
return { offset: offset, element: child };
} else {
return closest;
}
}, { offset: Number.NEGATIVE_INFINITY }).element;
}
}
new SortableList(document.querySelector('#sortable-list'));File API
基础说明
File API 提供了读取和操作用户本地文件的能力。
核心接口
| 接口 | 说明 |
|---|---|
File | 文件对象 |
FileList | 文件列表 |
FileReader | 文件读取器 |
Blob | 二进制大对象 |
代码示例
1. 读取文件内容
const fileInput = document.querySelector('#file-input');
fileInput.addEventListener('change', (e) => {
const file = e.target.files[0];
if (!file) return;
const reader = new FileReader();
// 读取为文本
reader.onload = (e) => {
console.log('文件内容:', e.target.result);
};
reader.readAsText(file);
});2. 读取图片并预览
const fileInput = document.querySelector('#image-input');
const preview = document.querySelector('#preview');
fileInput.addEventListener('change', (e) => {
const file = e.target.files[0];
if (!file || !file.type.startsWith('image/')) return;
const reader = new FileReader();
reader.onload = (e) => {
preview.src = e.target.result;
};
reader.readAsDataURL(file);
});3. 读取文件元数据
fileInput.addEventListener('change', (e) => {
const file = e.target.files[0];
if (!file) return;
console.log('文件信息:', {
name: file.name,
size: file.size,
type: file.type,
lastModified: new Date(file.lastModified),
sizeFormatted: formatFileSize(file.size)
});
});
function formatFileSize(bytes) {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i];
}4. 读取为 ArrayBuffer(用于二进制处理)
fileInput.addEventListener('change', (e) => {
const file = e.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (e) => {
const arrayBuffer = e.target.result;
console.log('字节数组:', new Uint8Array(arrayBuffer));
};
reader.readAsArrayBuffer(file);
});5. 多文件处理
<input type="file" multiple id="multi-file">const multiInput = document.querySelector('#multi-file');
multiInput.addEventListener('change', (e) => {
const files = [...e.target.files];
console.log(`选择了 ${files.length} 个文件`);
files.forEach((file, index) => {
console.log(`文件 ${index + 1}:`, file.name, formatFileSize(file.size));
});
});6. 拖拽文件上传
const dropzone = document.querySelector('.dropzone');
dropzone.addEventListener('dragover', (e) => {
e.preventDefault();
dropzone.classList.add('drag-over');
});
dropzone.addEventListener('dragleave', () => {
dropzone.classList.remove('drag-over');
});
dropzone.addEventListener('drop', (e) => {
e.preventDefault();
dropzone.classList.remove('drag-over');
const files = e.dataTransfer.files;
handleFiles(files);
});
async function handleFiles(files) {
for (const file of files) {
// 上传每个文件
await uploadFile(file);
}
}BroadcastChannel API
基础说明
BroadcastChannel API 用于同源页面之间的通信,可以实现跨标签页、跨窗口的消息传递。
基础语法
const channel = new BroadcastChannel('channel-name');
channel.postMessage(message);
channel.onmessage = (event) => { /* 处理消息 */ };
channel.close();代码示例
1. 基础通信
// 页面 A
const channel = new BroadcastChannel('app-channel');
// 发送消息
function sendMessage(msg) {
channel.postMessage({ type: 'update', data: msg });
}
// 接收消息
channel.onmessage = (event) => {
console.log('收到消息:', event.data);
};2. 跨标签页状态同步
class SyncState {
constructor(channelName) {
this.channel = new BroadcastChannel(channelName);
this.state = {};
this.listeners = [];
this.init();
}
init() {
// 监听其他页面的状态变化
this.channel.onmessage = (event) => {
const { type, key, value } = event.data;
if (type === 'update') {
this.state[key] = value;
this.notifyListeners(key, value);
}
};
}
// 设置状态
set(key, value) {
this.state[key] = value;
// 通知其他页面
this.channel.postMessage({ type: 'update', key, value });
}
// 获取状态
get(key) {
return this.state[key];
}
// 监听变化
subscribe(listener) {
this.listeners.push(listener);
return () => {
this.listeners = this.listeners.filter(l => l !== listener);
};
}
notifyListeners(key, value) {
this.listeners.forEach(listener => listener(key, value));
}
close() {
this.channel.close();
}
}
// 使用
const syncState = new SyncState('my-app');
// 设置状态(会同步到其他标签页)
syncState.set('user', { name: '张三' });
// 监听状态变化
syncState.subscribe((key, value) => {
console.log(`状态 ${key} 变为:`, value);
});3. 多标签页登录状态同步
class AuthSync {
constructor() {
this.channel = new BroadcastChannel('auth-sync');
this.init();
}
init() {
// 监听登录/登出事件
this.channel.onmessage = (event) => {
const { action, data } = event.data;
switch (action) {
case 'login':
this.onLogin(data);
break;
case 'logout':
this.onLogout();
break;
}
};
}
// 登录
login(userData) {
localStorage.setItem('user', JSON.stringify(userData));
this.channel.postMessage({ action: 'login', data: userData });
this.onLogin(userData);
}
// 登出
logout() {
localStorage.removeItem('user');
localStorage.removeItem('token');
this.channel.postMessage({ action: 'logout' });
this.onLogout();
}
onLogin(userData) {
console.log('用户已登录:', userData);
// 更新 UI
updateUI(true, userData);
}
onLogout() {
console.log('用户已登出');
// 跳转到登录页
location.href = '/login';
}
}
const authSync = new AuthSync();4. 标签页间协作(多个窗口同时编辑)
class EditorSync {
constructor(channelName) {
this.channel = new BroadcastChannel(channelName);
this.editor = null;
this.init();
}
init() {
// 监听其他标签页的编辑
this.channel.onmessage = (event) => {
const { type, content, cursor } = event.data;
if (type === 'content-change') {
// 更新编辑器内容
if (this.editor) {
this.editor.setValue(content);
}
} else if (type === 'cursor-move') {
// 显示其他用户的游标
showRemoteCursor(cursor);
}
};
}
bindEditor(editor) {
this.editor = editor;
// 编辑器内容变化时广播
editor.on('change', () => {
this.channel.postMessage({
type: 'content-change',
content: editor.getValue()
});
});
// 游标移动时广播
editor.on('cursorActivity', () => {
this.channel.postMessage({
type: 'cursor-move',
cursor: editor.getCursor()
});
});
}
close() {
this.channel.close();
}
}