Skip to content

HTML APIs(下):交互与设备能力

浏览器提供了丰富的 API 来增强用户交互体验——拖放文件、获取位置、操作剪贴板、管理浏览历史等。这些能力让 Web 应用越来越接近原生体验。

🎯 Drag and Drop API#

原生拖放 API 让元素可以被拖动和放置。

使元素可拖动#

<div id="dragItem" draggable="true">拖动我</div>

拖动事件#

事件触发时机作用对象
dragstart开始拖动被拖动元素
drag拖动过程中持续触发被拖动元素
dragend拖动结束被拖动元素
dragenter进入放置目标放置目标
dragover在放置目标上移动放置目标
dragleave离开放置目标放置目标
drop在放置目标上释放放置目标

DataTransfer 对象#

通过 event.dataTransfer 在拖放之间传递数据:

// 开始拖动时设置数据
element.addEventListener('dragstart', (e) => {
e.dataTransfer.setData('text/plain', 'Hello')
e.dataTransfer.setData('application/json', JSON.stringify({ id: 1 }))
// 设置拖动效果
e.dataTransfer.effectAllowed = 'move' // copy, move, link, all
})
// 放置时获取数据
dropZone.addEventListener('drop', (e) => {
e.preventDefault()
const text = e.dataTransfer.getData('text/plain')
const json = JSON.parse(e.dataTransfer.getData('application/json'))
})

完整拖放示例#

<style>
.container {
display: flex;
gap: 2rem;
}
.drop-zone {
width: 200px;
height: 200px;
border: 2px dashed #ccc;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
}
.drop-zone.drag-over {
border-color: #3b82f6;
background: #eff6ff;
}
.drag-item {
padding: 1rem 2rem;
background: #3b82f6;
color: white;
cursor: grab;
border-radius: 0.5rem;
}
.drag-item.dragging {
opacity: 0.5;
}
</style>
<div class="container">
<div class="drop-zone" id="zone1">
<div class="drag-item" draggable="true" id="item1">拖动我</div>
</div>
<div class="drop-zone" id="zone2">放置区域</div>
</div>
<script>
const item = document.getElementById('item1')
const zones = document.querySelectorAll('.drop-zone')
// 拖动开始
item.addEventListener('dragstart', (e) => {
e.dataTransfer.setData('text/plain', e.target.id)
e.dataTransfer.effectAllowed = 'move'
e.target.classList.add('dragging')
})
// 拖动结束
item.addEventListener('dragend', (e) => {
e.target.classList.remove('dragging')
})
// 放置区域事件
zones.forEach((zone) => {
zone.addEventListener('dragover', (e) => {
e.preventDefault() // 必须阻止默认行为才能触发 drop
e.dataTransfer.dropEffect = 'move'
})
zone.addEventListener('dragenter', (e) => {
e.preventDefault()
zone.classList.add('drag-over')
})
zone.addEventListener('dragleave', () => {
zone.classList.remove('drag-over')
})
zone.addEventListener('drop', (e) => {
e.preventDefault()
zone.classList.remove('drag-over')
const itemId = e.dataTransfer.getData('text/plain')
const draggedItem = document.getElementById(itemId)
zone.appendChild(draggedItem)
})
})
</script>

文件拖放上传#

<div id="dropZone" class="drop-zone">拖放文件到此处上传</div>
<script>
const dropZone = document.getElementById('dropZone')
dropZone.addEventListener('dragover', (e) => {
e.preventDefault()
e.dataTransfer.dropEffect = 'copy'
})
dropZone.addEventListener('drop', (e) => {
e.preventDefault()
const files = e.dataTransfer.files
for (const file of files) {
console.log('文件名:', file.name)
console.log('类型:', file.type)
console.log('大小:', file.size, 'bytes')
// 读取文件内容
const reader = new FileReader()
reader.onload = () => {
console.log('内容:', reader.result)
}
reader.readAsText(file) // 或 readAsDataURL
}
})
// 阻止页面默认的文件拖放行为
document.addEventListener('dragover', (e) => e.preventDefault())
document.addEventListener('drop', (e) => e.preventDefault())
</script>

Geolocation API#

获取用户的地理位置信息。

获取当前位置#

if ('geolocation' in navigator) {
navigator.geolocation.getCurrentPosition(
// 成功回调
(position) => {
console.log('纬度:', position.coords.latitude)
console.log('经度:', position.coords.longitude)
console.log('精度:', position.coords.accuracy, '')
console.log('海拔:', position.coords.altitude)
console.log('时间戳:', position.timestamp)
},
// 错误回调
(error) => {
switch (error.code) {
case error.PERMISSION_DENIED:
console.error('用户拒绝了定位请求')
break
case error.POSITION_UNAVAILABLE:
console.error('位置信息不可用')
break
case error.TIMEOUT:
console.error('请求超时')
break
}
},
// 选项
{
enableHighAccuracy: true, // 高精度(消耗更多电量)
timeout: 10000, // 超时时间(毫秒)
maximumAge: 60000, // 缓存有效期(毫秒)
}
)
}

持续监听位置#

const watchId = navigator.geolocation.watchPosition(
(position) => {
console.log(
'位置更新:',
position.coords.latitude,
position.coords.longitude
)
},
(error) => {
console.error('定位失败:', error.message)
},
{ enableHighAccuracy: true }
)
// 停止监听
navigator.geolocation.clearWatch(watchId)

计算两点距离#

function calculateDistance(lat1, lon1, lat2, lon2) {
const R = 6371 // 地球半径(公里)
const dLat = ((lat2 - lat1) * Math.PI) / 180
const dLon = ((lon2 - lon1) * Math.PI) / 180
const a =
Math.sin(dLat / 2) * Math.sin(dLat / 2) +
Math.cos((lat1 * Math.PI) / 180) *
Math.cos((lat2 * Math.PI) / 180) *
Math.sin(dLon / 2) *
Math.sin(dLon / 2)
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a))
return R * c // 返回公里
}
// 使用
const distance = calculateDistance(39.9042, 116.4074, 31.2304, 121.4737)
console.log(`北京到上海: ${distance.toFixed(2)} 公里`)

Clipboard API#

现代剪贴板 API,支持异步操作和多种数据类型。

复制文本#

async function copyText(text) {
try {
await navigator.clipboard.writeText(text)
console.log('复制成功')
} catch (err) {
console.error('复制失败:', err)
}
}
// 使用
copyText('Hello, World!')

读取文本#

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

🔶 权限提示:读取剪贴板需要用户授权,且只能在安全上下文(HTTPS)中使用。

复制图片#

async function copyImage(imgElement) {
const canvas = document.createElement('canvas')
canvas.width = imgElement.naturalWidth
canvas.height = imgElement.naturalHeight
const ctx = canvas.getContext('2d')
ctx.drawImage(imgElement, 0, 0)
canvas.toBlob(async (blob) => {
const item = new ClipboardItem({ 'image/png': blob })
await navigator.clipboard.write([item])
console.log('图片已复制')
})
}

读取剪贴板内容(含图片)#

async function readClipboard() {
const items = await navigator.clipboard.read()
for (const item of items) {
for (const type of item.types) {
const blob = await item.getType(type)
if (type.startsWith('image/')) {
const url = URL.createObjectURL(blob)
const img = document.createElement('img')
img.src = url
document.body.appendChild(img)
} else if (type === 'text/plain') {
const text = await blob.text()
console.log('文本:', text)
}
}
}
}

一键复制按钮#

<input type="text" id="linkInput" value="https://example.com" readonly />
<button onclick="copyLink()">📋 复制链接</button>
<script>
async function copyLink() {
const input = document.getElementById('linkInput')
try {
await navigator.clipboard.writeText(input.value)
alert('链接已复制!')
} catch (err) {
// 降级方案
input.select()
document.execCommand('copy')
alert('链接已复制!')
}
}
</script>

History API#

管理浏览器历史记录,实现 SPA 路由的核心 API。

基础导航#

// 后退
history.back()
// 前进
history.forward()
// 跳转指定步数
history.go(-2) // 后退两步
history.go(1) // 前进一步
// 历史记录长度
console.log(history.length)

添加历史记录#

// pushState 添加新记录
history.pushState(
{ page: 'about' }, // state 对象(可序列化)
'', // 标题(大多数浏览器忽略)
'/about' // 新 URL(同源)
)
// replaceState 替换当前记录
history.replaceState({ page: 'home' }, '', '/home')

监听历史变化#

window.addEventListener('popstate', (event) => {
console.log('URL 变化:', location.pathname)
console.log('State:', event.state)
// 根据路由渲染对应内容
if (event.state) {
renderPage(event.state.page)
}
})

🔶 注意pushStatereplaceState 不会触发 popstate 事件,只有用户点击前进/后退按钮或调用 history.back()/history.forward() 才会触发。

简单 SPA 路由#

<nav>
<a href="/" data-route>首页</a>
<a href="/about" data-route>关于</a>
<a href="/contact" data-route>联系</a>
</nav>
<main id="content"></main>
<script>
const routes = {
'/': '<h1>首页</h1><p>欢迎来到首页</p>',
'/about': '<h1>关于</h1><p>关于我们的介绍</p>',
'/contact': '<h1>联系</h1><p>联系方式</p>',
}
function navigate(path) {
const content = routes[path] || '<h1>404</h1><p>页面不存在</p>'
document.getElementById('content').innerHTML = content
}
// 拦截链接点击
document.querySelectorAll('a[data-route]').forEach((link) => {
link.addEventListener('click', (e) => {
e.preventDefault()
const path = link.getAttribute('href')
history.pushState({ path }, '', path)
navigate(path)
})
})
// 监听前进/后退
window.addEventListener('popstate', (event) => {
navigate(location.pathname)
})
// 初始加载
navigate(location.pathname)
</script>

Intersection Observer#

高效监测元素与视口的交叉状态,用于懒加载、无限滚动、动画触发等。

基础用法#

const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
console.log('元素进入视口:', entry.target)
} else {
console.log('元素离开视口:', entry.target)
}
})
})
// 开始观察
observer.observe(document.querySelector('#target'))
// 停止观察
observer.unobserve(element)
// 停止所有观察
observer.disconnect()

配置选项#

const observer = new IntersectionObserver(callback, {
root: null, // 视口元素,默认浏览器视口
rootMargin: '0px', // 视口边距,支持 '10px 20px 30px 40px'
threshold: 0, // 触发阈值,0-1 或数组 [0, 0.5, 1]
})

Entry 对象属性#

const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
console.log(entry.target) // 被观察的元素
console.log(entry.isIntersecting) // 是否在视口内
console.log(entry.intersectionRatio) // 可见比例 0-1
console.log(entry.boundingClientRect) // 元素边界
console.log(entry.intersectionRect) // 交叉区域
console.log(entry.rootBounds) // 视口边界
})
})

🎯 实战:图片懒加载#

<style>
.lazy-image {
min-height: 300px;
background: #f0f0f0;
display: flex;
align-items: center;
justify-content: center;
}
.lazy-image img {
opacity: 0;
transition: opacity 0.3s;
}
.lazy-image img.loaded {
opacity: 1;
}
</style>
<div class="lazy-image">
<img data-src="image1.jpg" alt="图片 1" />
</div>
<div class="lazy-image">
<img data-src="image2.jpg" alt="图片 2" />
</div>
<div class="lazy-image">
<img data-src="image3.jpg" alt="图片 3" />
</div>
<script>
const lazyImages = document.querySelectorAll('img[data-src]')
const imageObserver = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
const img = entry.target
img.src = img.dataset.src
img.onload = () => img.classList.add('loaded')
imageObserver.unobserve(img) // 加载后停止观察
}
})
},
{
rootMargin: '100px', // 提前 100px 开始加载
}
)
lazyImages.forEach((img) => imageObserver.observe(img))
</script>

滚动动画触发#

<style>
.animate-item {
opacity: 0;
transform: translateY(50px);
transition: all 0.6s ease-out;
}
.animate-item.visible {
opacity: 1;
transform: translateY(0);
}
</style>
<div class="animate-item">内容 1</div>
<div class="animate-item">内容 2</div>
<div class="animate-item">内容 3</div>
<script>
const animateItems = document.querySelectorAll('.animate-item')
const animateObserver = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
entry.target.classList.add('visible')
}
})
},
{
threshold: 0.2, // 20% 可见时触发
}
)
animateItems.forEach((item) => animateObserver.observe(item))
</script>

无限滚动#

<ul id="list"></ul>
<div id="sentinel">加载中...</div>
<script>
let page = 1
const list = document.getElementById('list')
const sentinel = document.getElementById('sentinel')
async function loadMore() {
const response = await fetch(`/api/items?page=${page}`)
const items = await response.json()
items.forEach((item) => {
const li = document.createElement('li')
li.textContent = item.name
list.appendChild(li)
})
page++
}
const scrollObserver = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting) {
loadMore()
}
})
scrollObserver.observe(sentinel)
// 初始加载
loadMore()
</script>

其他实用 API#

Page Visibility API#

检测页面是否可见(用户是否在当前标签页):

document.addEventListener('visibilitychange', () => {
if (document.hidden) {
console.log('页面隐藏 - 暂停视频/动画')
} else {
console.log('页面可见 - 恢复播放')
}
})

Fullscreen API#

// 进入全屏
element.requestFullscreen()
// 退出全屏
document.exitFullscreen()
// 检查全屏状态
document.fullscreenElement
// 监听全屏变化
document.addEventListener('fullscreenchange', () => {
console.log('全屏状态:', !!document.fullscreenElement)
})

Screen Wake Lock API#

防止屏幕休眠(视频播放、导航应用):

let wakeLock = null
async function requestWakeLock() {
try {
wakeLock = await navigator.wakeLock.request('screen')
console.log('屏幕保持唤醒')
wakeLock.addEventListener('release', () => {
console.log('唤醒锁已释放')
})
} catch (err) {
console.error('无法获取唤醒锁:', err)
}
}
// 释放
wakeLock?.release()

常见问题#

🤔 拖放时 drop 事件不触发?#

必须在 dragover 事件中调用 e.preventDefault()

dropZone.addEventListener('dragover', (e) => {
e.preventDefault() // 这一行是必须的!
})

🤔 Geolocation 在 HTTP 页面不工作?#

地理位置 API 只能在安全上下文(HTTPS 或 localhost)中使用。

🤔 Intersection Observer vs scroll 事件?#

Intersection Observer 性能更好:

参考资料#