HTML5新特性与离线存储深度解析
本文深入解析HTML5的新特性与离线存储功能。HTML5引入了语义化标签(如header、nav、article等),显著提升了网页结构的可读性和可访问性。多媒体方面,HTML5原生支持音视频播放,不再依赖Flash等插件。文章还详细介绍了HTML5的离线存储机制,包括Web Storage和IndexedDB等技术,使网页应用能在无网络环境下运行。代码示例展示了语义化标签的典型应用场景和多媒体播
·
HTML5新特性与离线存储深度解析
一、HTML5新特性全面解析
1.1 语义化标签革命
HTML5引入的语义化标签彻底改变了网页结构的设计方式,让代码更具可读性和可访问性。
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>HTML5语义化示例</title>
<style>
/* 语义化标签的默认样式重置 */
header, nav, main, article, section, aside, footer, figure, figcaption {
display: block;
margin: 0;
padding: 0;
}
.site-header {
background: #2c3e50;
color: white;
padding: 1rem;
}
.main-navigation {
background: #34495e;
padding: 0.5rem;
}
.content-area {
display: grid;
grid-template-columns: 3fr 1fr;
gap: 2rem;
padding: 2rem;
}
.article-card {
border: 1px solid #bdc3c7;
border-radius: 8px;
padding: 1.5rem;
margin-bottom: 1.5rem;
}
.sidebar {
background: #ecf0f1;
padding: 1.5rem;
border-radius: 8px;
}
</style>
</head>
<body>
<!-- 页眉区域 -->
<header class="site-header" role="banner">
<h1>我的技术博客</h1>
<p>分享前端开发知识与经验</p>
</header>
<!-- 导航区域 -->
<nav class="main-navigation" role="navigation" aria-label="主导航">
<ul>
<li><a href="#home">首页</a></li>
<li><a href="#articles">文章</a></li>
<li><a href="#tutorials">教程</a></li>
<li><a href="#about">关于</a></li>
</ul>
</nav>
<!-- 主要内容区域 -->
<main class="content-area" role="main">
<!-- 文章内容 -->
<article class="main-content">
<header>
<h2>HTML5语义化标签详解</h2>
<time datetime="2024-01-15">2024年1月15日</time>
<address>作者: 技术达人</address>
</header>
<section>
<h3>什么是语义化标签</h3>
<p>语义化标签是指使用具有明确含义的HTML标签来描述内容的结构...</p>
<figure>
<img src="semantic-html.jpg" alt="HTML5语义化结构示意图">
<figcaption>图1: HTML5文档结构示意图</figcaption>
</figure>
</section>
<section>
<h3>语义化标签的优势</h3>
<ul>
<li>更好的可访问性</li>
<li>改进的SEO效果</li>
<li>更清晰的代码结构</li>
<li>更好的维护性</li>
</ul>
</section>
<footer>
<p>标签: <mark>HTML5</mark>, <mark>语义化</mark>, <mark>前端开发</mark></p>
</footer>
</article>
<!-- 侧边栏 -->
<aside class="sidebar" role="complementary" aria-label="相关链接">
<section>
<h4>相关文章</h4>
<ul>
<li><a href="#">CSS Grid布局完全指南</a></li>
<li><a href="#">JavaScript ES6+ 新特性</a></li>
</ul>
</section>
<section>
<h4>热门标签</h4>
<div>
<span class="tag">Vue.js</span>
<span class="tag">React</span>
<span class="tag">TypeScript</span>
</div>
</section>
</aside>
</main>
<!-- 页脚 -->
<footer class="site-footer" role="contentinfo">
<p>© 2024 我的技术博客. 保留所有权利.</p>
<nav aria-label="页脚导航">
<a href="#privacy">隐私政策</a> |
<a href="#terms">服务条款</a> |
<a href="#contact">联系我们</a>
</nav>
</footer>
</body>
</html>
1.2 多媒体支持增强
HTML5原生支持音频和视频播放,不再依赖Flash等第三方插件。
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>HTML5多媒体示例</title>
<style>
.media-container {
max-width: 800px;
margin: 2rem auto;
padding: 1rem;
}
.video-player, .audio-player {
width: 100%;
margin-bottom: 2rem;
}
.custom-controls {
display: flex;
align-items: center;
gap: 1rem;
margin-top: 1rem;
}
.progress-container {
flex: 1;
height: 8px;
background: #ddd;
border-radius: 4px;
overflow: hidden;
}
.progress-bar {
height: 100%;
background: #3498db;
width: 0%;
transition: width 0.1s;
}
.control-btn {
padding: 0.5rem 1rem;
border: none;
background: #3498db;
color: white;
border-radius: 4px;
cursor: pointer;
}
</style>
</head>
<body>
<div class="media-container">
<!-- 视频播放器 -->
<section>
<h2>视频播放示例</h2>
<video class="video-player"
controls
preload="metadata"
poster="video-poster.jpg"
width="800"
height="450">
<!-- 多格式支持,确保浏览器兼容性 -->
<source src="video.mp4" type="video/mp4">
<source src="video.webm" type="video/webm">
<source src="video.ogg" type="video/ogg">
<!-- 后备内容 -->
<p>您的浏览器不支持HTML5视频,请<a href="video.mp4">下载视频</a>。</p>
</video>
<!-- 自定义视频控制 -->
<div class="custom-controls" id="videoControls">
<button class="control-btn" id="playPause">播放/暂停</button>
<button class="control-btn" id="mute">静音</button>
<div class="progress-container">
<div class="progress-bar" id="videoProgress"></div>
</div>
<span id="timeDisplay">00:00 / 00:00</span>
<input type="range" id="volume" min="0" max="1" step="0.1" value="1">
</div>
</section>
<!-- 音频播放器 -->
<section>
<h2>音频播放示例</h2>
<audio class="audio-player"
controls
preload="metadata"
id="audioPlayer">
<source src="audio.mp3" type="audio/mpeg">
<source src="audio.ogg" type="audio/ogg">
<source src="audio.wav" type="audio/wav">
<p>您的浏览器不支持HTML5音频。</p>
</audio>
<!-- 音频可视化 -->
<canvas id="audioVisualizer" width="800" height="100" style="border: 1px solid #ddd; margin-top: 1rem;"></canvas>
</section>
</div>
<script>
// 自定义视频控制
const video = document.querySelector('video');
const playPauseBtn = document.getElementById('playPause');
const muteBtn = document.getElementById('mute');
const videoProgress = document.getElementById('videoProgress');
const timeDisplay = document.getElementById('timeDisplay');
const volumeControl = document.getElementById('volume');
playPauseBtn.addEventListener('click', () => {
if (video.paused) {
video.play();
} else {
video.pause();
}
});
muteBtn.addEventListener('click', () => {
video.muted = !video.muted;
muteBtn.textContent = video.muted ? '取消静音' : '静音';
});
video.addEventListener('timeupdate', () => {
const progress = (video.currentTime / video.duration) * 100;
videoProgress.style.width = `${progress}%`;
const currentTime = formatTime(video.currentTime);
const duration = formatTime(video.duration);
timeDisplay.textContent = `${currentTime} / ${duration}`;
});
volumeControl.addEventListener('input', () => {
video.volume = volumeControl.value;
});
function formatTime(seconds) {
const mins = Math.floor(seconds / 60);
const secs = Math.floor(seconds % 60);
return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
}
// 音频可视化
const audio = document.getElementById('audioPlayer');
const canvas = document.getElementById('audioVisualizer');
const ctx = canvas.getContext('2d');
// 创建音频上下文和分析器
const audioContext = new (window.AudioContext || window.webkitAudioContext)();
const analyser = audioContext.createAnalyser();
const source = audioContext.createMediaElementSource(audio);
source.connect(analyser);
analyser.connect(audioContext.destination);
analyser.fftSize = 256;
const bufferLength = analyser.frequencyBinCount;
const dataArray = new Uint8Array(bufferLength);
function drawVisualizer() {
requestAnimationFrame(drawVisualizer);
analyser.getByteFrequencyData(dataArray);
ctx.fillStyle = 'rgb(255, 255, 255)';
ctx.fillRect(0, 0, canvas.width, canvas.height);
const barWidth = (canvas.width / bufferLength) * 2.5;
let barHeight;
let x = 0;
for(let i = 0; i < bufferLength; i++) {
barHeight = dataArray[i] / 2;
ctx.fillStyle = `rgb(${barHeight + 100}, 50, 50)`;
ctx.fillRect(x, canvas.height - barHeight, barWidth, barHeight);
x += barWidth + 1;
}
}
audio.addEventListener('play', () => {
audioContext.resume().then(() => {
drawVisualizer();
});
});
</script>
</body>
</html>
1.3 Canvas绘图与动画
HTML5 Canvas提供了强大的2D绘图能力,支持复杂的图形和动画效果。
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>Canvas绘图示例</title>
<style>
.canvas-container {
text-align: center;
padding: 2rem;
}
canvas {
border: 2px solid #34495e;
border-radius: 8px;
background: #ecf0f1;
margin: 1rem;
}
.controls {
margin: 1rem 0;
}
.control-btn {
padding: 0.5rem 1rem;
margin: 0 0.5rem;
border: none;
background: #3498db;
color: white;
border-radius: 4px;
cursor: pointer;
}
</style>
</head>
<body>
<div class="canvas-container">
<h1>HTML5 Canvas 绘图示例</h1>
<div class="controls">
<button class="control-btn" id="drawBasic">基本图形</button>
<button class="control-btn" id="drawGradient">渐变效果</button>
<button class="control-btn" id="drawText">文字效果</button>
<button class="control-btn" id="startAnimation">开始动画</button>
<button class="control-btn" id="clearCanvas">清空画布</button>
</div>
<canvas id="mainCanvas" width="800" height="400">
您的浏览器不支持Canvas,请升级到现代浏览器。
</canvas>
</div>
<script>
const canvas = document.getElementById('mainCanvas');
const ctx = canvas.getContext('2d');
// 基本图形绘制
function drawBasicShapes() {
clearCanvas();
// 矩形
ctx.fillStyle = '#e74c3c';
ctx.fillRect(50, 50, 100, 80);
ctx.strokeStyle = '#2c3e50';
ctx.lineWidth = 3;
ctx.strokeRect(180, 50, 100, 80);
// 圆形
ctx.beginPath();
ctx.arc(350, 90, 40, 0, Math.PI * 2);
ctx.fillStyle = '#3498db';
ctx.fill();
ctx.stroke();
// 三角形
ctx.beginPath();
ctx.moveTo(450, 50);
ctx.lineTo(550, 50);
ctx.lineTo(500, 130);
ctx.closePath();
ctx.fillStyle = '#2ecc71';
ctx.fill();
ctx.stroke();
// 复杂路径
ctx.beginPath();
ctx.moveTo(600, 50);
ctx.bezierCurveTo(650, 30, 700, 80, 750, 50);
ctx.bezierCurveTo(780, 70, 750, 130, 700, 110);
ctx.closePath();
ctx.fillStyle = '#9b59b6';
ctx.fill();
ctx.stroke();
}
// 渐变效果
function drawGradients() {
clearCanvas();
// 线性渐变
const linearGradient = ctx.createLinearGradient(0, 0, 800, 0);
linearGradient.addColorStop(0, '#e74c3c');
linearGradient.addColorStop(0.5, '#f39c12');
linearGradient.addColorStop(1, '#2ecc71');
ctx.fillStyle = linearGradient;
ctx.fillRect(50, 50, 700, 100);
// 径向渐变
const radialGradient = ctx.createRadialGradient(400, 300, 10, 400, 300, 100);
radialGradient.addColorStop(0, '#3498db');
radialGradient.addColorStop(1, '#2c3e50');
ctx.fillStyle = radialGradient;
ctx.beginPath();
ctx.arc(400, 300, 100, 0, Math.PI * 2);
ctx.fill();
}
// 文字效果
function drawTextEffects() {
clearCanvas();
ctx.fillStyle = '#2c3e50';
ctx.font = 'bold 36px Arial';
ctx.fillText('HTML5 Canvas 文字效果', 50, 80);
// 文字阴影
ctx.shadowColor = 'rgba(0, 0, 0, 0.5)';
ctx.shadowBlur = 10;
ctx.shadowOffsetX = 5;
ctx.shadowOffsetY = 5;
ctx.fillStyle = '#e74c3c';
ctx.font = 'italic 48px Georgia';
ctx.fillText('带阴影的文字', 50, 160);
// 重置阴影
ctx.shadowColor = 'transparent';
ctx.shadowBlur = 0;
ctx.shadowOffsetX = 0;
ctx.shadowOffsetY = 0;
// 文字渐变
const textGradient = ctx.createLinearGradient(50, 200, 500, 200);
textGradient.addColorStop(0, '#e74c3c');
textGradient.addColorStop(0.5, '#3498db');
textGradient.addColorStop(1, '#2ecc71');
ctx.fillStyle = textGradient;
ctx.font = 'bold 42px Arial';
ctx.fillText('渐变文字效果', 50, 250);
// 文字描边
ctx.strokeStyle = '#34495e';
ctx.lineWidth = 2;
ctx.font = 'bold 36px Arial';
ctx.strokeText('描边文字', 50, 320);
}
// 动画
let animationId;
function startAnimation() {
clearCanvas();
let angle = 0;
const centerX = canvas.width / 2;
const centerY = canvas.height / 2;
const radius = 100;
function animate() {
clearCanvas();
// 绘制轨道
ctx.beginPath();
ctx.arc(centerX, centerY, radius, 0, Math.PI * 2);
ctx.strokeStyle = '#bdc3c7';
ctx.stroke();
// 计算小球位置
const x = centerX + Math.cos(angle) * radius;
const y = centerY + Math.sin(angle) * radius;
// 绘制小球
ctx.beginPath();
ctx.arc(x, y, 20, 0, Math.PI * 2);
const gradient = ctx.createRadialGradient(x-10, y-10, 5, x, y, 20);
gradient.addColorStop(0, '#e74c3c');
gradient.addColorStop(1, '#c0392b');
ctx.fillStyle = gradient;
ctx.fill();
// 更新角度
angle += 0.05;
animationId = requestAnimationFrame(animate);
}
animate();
}
function clearCanvas() {
if (animationId) {
cancelAnimationFrame(animationId);
animationId = null;
}
ctx.clearRect(0, 0, canvas.width, canvas.height);
}
// 事件监听
document.getElementById('drawBasic').addEventListener('click', drawBasicShapes);
document.getElementById('drawGradient').addEventListener('click', drawGradients);
document.getElementById('drawText').addEventListener('click', drawTextEffects);
document.getElementById('startAnimation').addEventListener('click', startAnimation);
document.getElementById('clearCanvas').addEventListener('click', clearCanvas);
// 初始绘制
drawBasicShapes();
</script>
</body>
</html>
1.4 表单功能增强
HTML5为表单引入了新的输入类型、属性和验证功能。
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>HTML5表单增强</title>
<style>
.form-container {
max-width: 600px;
margin: 2rem auto;
padding: 2rem;
background: #f8f9fa;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
.form-group {
margin-bottom: 1.5rem;
}
label {
display: block;
margin-bottom: 0.5rem;
font-weight: bold;
color: #2c3e50;
}
input, select, textarea {
width: 100%;
padding: 0.75rem;
border: 2px solid #bdc3c7;
border-radius: 4px;
font-size: 1rem;
transition: border-color 0.3s;
}
input:focus, select:focus, textarea:focus {
outline: none;
border-color: #3498db;
}
input:valid {
border-color: #2ecc71;
}
input:invalid {
border-color: #e74c3c;
}
.error-message {
color: #e74c3c;
font-size: 0.875rem;
margin-top: 0.25rem;
display: none;
}
input:invalid + .error-message {
display: block;
}
.submit-btn {
background: #3498db;
color: white;
padding: 1rem 2rem;
border: none;
border-radius: 4px;
font-size: 1.1rem;
cursor: pointer;
width: 100%;
}
.submit-btn:hover {
background: #2980b9;
}
/* 自定义样式示例 */
input[type="range"] {
height: 8px;
background: #ddd;
outline: none;
}
input[type="color"] {
height: 50px;
padding: 5px;
}
.form-section {
margin-bottom: 2rem;
padding-bottom: 1rem;
border-bottom: 1px solid #bdc3c7;
}
.form-section h3 {
color: #34495e;
margin-bottom: 1rem;
}
</style>
</head>
<body>
<div class="form-container">
<h1>HTML5增强表单示例</h1>
<form id="enhancedForm" novalidate>
<!-- 基础输入类型 -->
<div class="form-section">
<h3>基础信息</h3>
<div class="form-group">
<label for="email">邮箱地址 *</label>
<input type="email" id="email" name="email"
placeholder="请输入有效的邮箱地址"
required>
<div class="error-message">请输入有效的邮箱地址</div>
</div>
<div class="form-group">
<label for="url">个人网站</label>
<input type="url" id="url" name="url"
placeholder="https://example.com">
<div class="error-message">请输入有效的URL地址</div>
</div>
<div class="form-group">
<label for="phone">手机号码</label>
<input type="tel" id="phone" name="phone"
pattern="[0-9]{11}"
placeholder="请输入11位手机号码">
<div class="error-message">请输入11位手机号码</div>
</div>
</div>
<!-- 新增输入类型 -->
<div class="form-section">
<h3>高级输入类型</h3>
<div class="form-group">
<label for="birthdate">出生日期</label>
<input type="date" id="birthdate" name="birthdate">
</div>
<div class="form-group">
<label for="time">选择时间</label>
<input type="time" id="time" name="time">
</div>
<div class="form-group">
<label for="datetime">日期时间</label>
<input type="datetime-local" id="datetime" name="datetime">
</div>
<div class="form-group">
<label for="month">选择月份</label>
<input type="month" id="month" name="month">
</div>
<div class="form-group">
<label for="week">选择周</label>
<input type="week" id="week" name="week">
</div>
</div>
<!-- 范围与颜色选择 -->
<div class="form-section">
<h3>范围与颜色</h3>
<div class="form-group">
<label for="volume">音量控制: <span id="volumeValue">50</span>%</label>
<input type="range" id="volume" name="volume"
min="0" max="100" value="50" step="1">
</div>
<div class="form-group">
<label for="rating">评分: <span id="ratingValue">3</span>星</label>
<input type="range" id="rating" name="rating"
min="1" max="5" value="3" step="1">
</div>
<div class="form-group">
<label for="favcolor">选择喜欢的颜色</label>
<input type="color" id="favcolor" name="favcolor" value="#3498db">
</div>
</div>
<!-- 数据列表与搜索 -->
<div class="form-section">
<h3>搜索与选择</h3>
<div class="form-group">
<label for="browser">选择浏览器</label>
<input list="browsers" id="browser" name="browser"
placeholder="输入或选择浏览器">
<datalist id="browsers">
<option value="Chrome">
<option value="Firefox">
<option value="Safari">
<option value="Edge">
<option value="Opera">
</datalist>
</div>
<div class="form-group">
<label for="search">搜索</label>
<input type="search" id="search" name="search"
placeholder="输入搜索关键词">
</div>
</div>
<!-- 其他增强功能 -->
<div class="form-section">
<h3>其他功能</h3>
<div class="form-group">
<label for="quantity">数量 (1-10)</label>
<input type="number" id="quantity" name="quantity"
min="1" max="10" value="1">
</div>
<div class="form-group">
<label for="progress">进度</label>
<progress id="progress" value="75" max="100">75%</progress>
</div>
<div class="form-group">
<label for="file">选择文件</label>
<input type="file" id="file" name="file"
accept=".jpg,.png,.pdf"
multiple>
</div>
</div>
<button type="submit" class="submit-btn">提交表单</button>
</form>
</div>
<script>
// 实时更新范围输入的值显示
const volumeInput = document.getElementById('volume');
const volumeValue = document.getElementById('volumeValue');
const ratingInput = document.getElementById('rating');
const ratingValue = document.getElementById('ratingValue');
volumeInput.addEventListener('input', () => {
volumeValue.textContent = volumeInput.value;
});
ratingInput.addEventListener('input', () => {
ratingValue.textContent = ratingInput.value;
});
// 表单验证增强
const form = document.getElementById('enhancedForm');
form.addEventListener('submit', (event) => {
event.preventDefault();
if (form.checkValidity()) {
// 表单验证通过
alert('表单提交成功!');
// 这里可以添加实际的表单提交逻辑
} else {
// 显示自定义验证消息
const invalidFields = form.querySelectorAll(':invalid');
invalidFields.forEach(field => {
field.reportValidity();
});
}
});
// 实时验证
const inputs = form.querySelectorAll('input');
inputs.forEach(input => {
input.addEventListener('blur', () => {
input.checkValidity();
});
input.addEventListener('input', () => {
// 输入时移除错误状态
if (input.validity.valid) {
input.style.borderColor = '#2ecc71';
}
});
});
// 自定义验证示例
const emailInput = document.getElementById('email');
emailInput.addEventListener('input', () => {
if (emailInput.validity.typeMismatch) {
emailInput.setCustomValidity('请输入有效的邮箱地址,如:user@example.com');
} else {
emailInput.setCustomValidity('');
}
});
</script>
</body>
</html>
二、离线存储深度解析
2.1 Web Storage:localStorage和sessionStorage
Web Storage提供了简单易用的键值对存储机制,分为localStorage(持久化存储)和sessionStorage(会话级存储)。
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>Web Storage 离线存储</title>
<style>
.storage-demo {
max-width: 800px;
margin: 2rem auto;
padding: 2rem;
background: #f8f9fa;
border-radius: 8px;
}
.storage-section {
margin-bottom: 2rem;
padding: 1.5rem;
background: white;
border-radius: 6px;
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
}
.input-group {
display: flex;
gap: 1rem;
margin-bottom: 1rem;
}
.input-group input {
flex: 1;
padding: 0.75rem;
border: 2px solid #bdc3c7;
border-radius: 4px;
}
.btn {
padding: 0.75rem 1.5rem;
border: none;
border-radius: 4px;
cursor: pointer;
font-weight: bold;
}
.btn-primary {
background: #3498db;
color: white;
}
.btn-danger {
background: #e74c3c;
color: white;
}
.btn-success {
background: #2ecc71;
color: white;
}
.storage-data {
background: #ecf0f1;
padding: 1rem;
border-radius: 4px;
margin-top: 1rem;
font-family: monospace;
white-space: pre-wrap;
max-height: 300px;
overflow-y: auto;
}
.comparison-table {
width: 100%;
border-collapse: collapse;
margin-top: 1rem;
}
.comparison-table th,
.comparison-table td {
border: 1px solid #bdc3c7;
padding: 0.75rem;
text-align: left;
}
.comparison-table th {
background: #34495e;
color: white;
}
.comparison-table tr:nth-child(even) {
background: #f8f9fa;
}
</style>
</head>
<body>
<div class="storage-demo">
<h1>Web Storage 离线存储示例</h1>
<!-- localStorage 示例 -->
<section class="storage-section">
<h2>localStorage - 持久化存储</h2>
<p>数据会一直保存,直到明确删除或用户清除浏览器数据</p>
<div class="input-group">
<input type="text" id="localKey" placeholder="输入键名">
<input type="text" id="localValue" placeholder="输入值">
<button class="btn btn-primary" id="saveLocal">保存到localStorage</button>
</div>
<div>
<button class="btn btn-success" id="loadLocal">读取localStorage</button>
<button class="btn btn-danger" id="clearLocal">清空localStorage</button>
<button class="btn" id="removeLocal">删除指定项</button>
</div>
<div class="storage-data" id="localData">
localStorage数据将显示在这里...
</div>
</section>
<!-- sessionStorage 示例 -->
<section class="storage-section">
<h2>sessionStorage - 会话存储</h2>
<p>数据仅在当前浏览器标签页有效,关闭标签页后数据丢失</p>
<div class="input-group">
<input type="text" id="sessionKey" placeholder="输入键名">
<input type="text" id="sessionValue" placeholder="输入值">
<button class="btn btn-primary" id="saveSession">保存到sessionStorage</button>
</div>
<div>
<button class="btn btn-success" id="loadSession">读取sessionStorage</button>
<button class="btn btn-danger" id="clearSession">清空sessionStorage</button>
</div>
<div class="storage-data" id="sessionData">
sessionStorage数据将显示在这里...
</div>
</section>
<!-- 特性对比 -->
<section class="storage-section">
<h2>localStorage vs sessionStorage 对比</h2>
<table class="comparison-table">
<thead>
<tr>
<th>特性</th>
<th>localStorage</th>
<th>sessionStorage</th>
</tr>
</thead>
<tbody>
<tr>
<td>数据生命周期</td>
<td>永久存储,直到手动删除</td>
<td>会话期间有效,标签页关闭即删除</td>
</tr>
<tr>
<td>作用域</td>
<td>同源的所有标签页共享</td>
<td>仅当前标签页有效</td>
</tr>
<tr>
<td>存储容量</td>
<td>通常5-10MB</td>
<td>通常5-10MB</td>
</tr>
<tr>
<td>数据格式</td>
<td colspan="2">仅支持字符串,需要序列化对象</td>
</tr>
<tr>
<td>同步/异步</td>
<td colspan="2">同步操作,可能阻塞主线程</td>
</tr>
</tbody>
</table>
</section>
<!-- 实际应用示例 -->
<section class="storage-section">
<h2>实际应用:用户偏好设置</h2>
<div class="input-group">
<label>
<input type="checkbox" id="darkMode"> 深色模式
</label>
<label>
<input type="checkbox" id="notifications"> 启用通知
</label>
<select id="language">
<option value="zh-CN">中文</option>
<option value="en-US">English</option>
<option value="ja-JP">日本語</option>
</select>
<button class="btn btn-primary" id="savePreferences">保存偏好设置</button>
</div>
<div class="storage-data" id="preferencesData">
用户偏好设置将显示在这里...
</div>
</section>
</div>
<script>
// localStorage 操作
const saveLocalBtn = document.getElementById('saveLocal');
const loadLocalBtn = document.getElementById('loadLocal');
const clearLocalBtn = document.getElementById('clearLocal');
const removeLocalBtn = document.getElementById('removeLocal');
const localDataDiv = document.getElementById('localData');
saveLocalBtn.addEventListener('click', () => {
const key = document.getElementById('localKey').value;
const value = document.getElementById('localValue').value;
if (key && value) {
localStorage.setItem(key, value);
alert(`已保存: ${key} = ${value}`);
document.getElementById('localKey').value = '';
document.getElementById('localValue').value = '';
} else {
alert('请输入键名和值');
}
});
loadLocalBtn.addEventListener('click', () => {
const data = {};
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
data[key] = localStorage.getItem(key);
}
localDataDiv.textContent = JSON.stringify(data, null, 2);
});
clearLocalBtn.addEventListener('click', () => {
localStorage.clear();
localDataDiv.textContent = 'localStorage已清空';
});
removeLocalBtn.addEventListener('click', () => {
const key = document.getElementById('localKey').value;
if (key) {
localStorage.removeItem(key);
alert(`已删除: ${key}`);
} else {
alert('请输入要删除的键名');
}
});
// sessionStorage 操作
const saveSessionBtn = document.getElementById('saveSession');
const loadSessionBtn = document.getElementById('loadSession');
const clearSessionBtn = document.getElementById('clearSession');
const sessionDataDiv = document.getElementById('sessionData');
saveSessionBtn.addEventListener('click', () => {
const key = document.getElementById('sessionKey').value;
const value = document.getElementById('sessionValue').value;
if (key && value) {
sessionStorage.setItem(key, value);
alert(`已保存: ${key} = ${value}`);
document.getElementById('sessionKey').value = '';
document.getElementById('sessionValue').value = '';
} else {
alert('请输入键名和值');
}
});
loadSessionBtn.addEventListener('click', () => {
const data = {};
for (let i = 0; i < sessionStorage.length; i++) {
const key = sessionStorage.key(i);
data[key] = sessionStorage.getItem(key);
}
sessionDataDiv.textContent = JSON.stringify(data, null, 2);
});
clearSessionBtn.addEventListener('click', () => {
sessionStorage.clear();
sessionDataDiv.textContent = 'sessionStorage已清空';
});
// 用户偏好设置
const savePreferencesBtn = document.getElementById('savePreferences');
const preferencesDataDiv = document.getElementById('preferencesData');
// 加载保存的偏好设置
function loadPreferences() {
const preferences = JSON.parse(localStorage.getItem('userPreferences') || '{}');
document.getElementById('darkMode').checked = preferences.darkMode || false;
document.getElementById('notifications').checked = preferences.notifications || false;
document.getElementById('language').value = preferences.language || 'zh-CN';
preferencesDataDiv.textContent = JSON.stringify(preferences, null, 2);
}
savePreferencesBtn.addEventListener('click', () => {
const preferences = {
darkMode: document.getElementById('darkMode').checked,
notifications: document.getElementById('notifications').checked,
language: document.getElementById('language').value,
lastUpdated: new Date().toISOString()
};
localStorage.setItem('userPreferences', JSON.stringify(preferences));
loadPreferences();
alert('偏好设置已保存!');
});
// 页面加载时读取偏好设置
document.addEventListener('DOMContentLoaded', loadPreferences);
// 存储容量检测
function testStorageCapacity(storage) {
const testKey = 'storageTest';
const testValue = 'a'.repeat(1024); // 1KB
let totalSize = 0;
try {
while (true) {
storage.setItem(testKey + totalSize, testValue.repeat(totalSize + 1));
totalSize += 1; // KB
}
} catch (e) {
// 捕获存储空间不足的错误
for (let i = 0; i < totalSize; i++) {
storage.removeItem(testKey + i);
}
return totalSize;
}
}
// 显示存储容量信息
console.log('localStorage 可用容量约:', testStorageCapacity(localStorage), 'KB');
console.log('sessionStorage 可用容量约:', testStorageCapacity(sessionStorage), 'KB');
// 存储事件监听(跨标签页通信)
window.addEventListener('storage', (event) => {
console.log('存储发生变化:', {
key: event.key,
oldValue: event.oldValue,
newValue: event.newValue,
url: event.url,
storageArea: event.storageArea
});
if (event.key === 'userPreferences') {
loadPreferences(); // 自动更新偏好设置
}
});
// 高级功能:存储管理类
class StorageManager {
constructor(storage = localStorage) {
this.storage = storage;
}
setObject(key, value) {
this.storage.setItem(key, JSON.stringify(value));
}
getObject(key, defaultValue = null) {
try {
const item = this.storage.getItem(key);
return item ? JSON.parse(item) : defaultValue;
} catch (error) {
console.error('解析存储数据失败:', error);
return defaultValue;
}
}
exists(key) {
return this.storage.getItem(key) !== null;
}
getSize() {
let total = 0;
for (let key in this.storage) {
if (this.storage.hasOwnProperty(key)) {
total += (key.length + this.storage.getItem(key).length) * 2; // UTF-16
}
}
return total;
}
}
// 使用存储管理类
const storageManager = new StorageManager(localStorage);
storageManager.setObject('userSettings', {
theme: 'dark',
fontSize: 16,
autoSave: true
});
const settings = storageManager.getObject('userSettings');
console.log('用户设置:', settings);
console.log('存储占用:', storageManager.getSize(), 'bytes');
</script>
</body>
</html>
2.2 IndexedDB:客户端数据库
IndexedDB是一个功能强大的浏览器内数据库,支持存储大量结构化数据。
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>IndexedDB 客户端数据库</title>
<style>
.indexeddb-demo {
max-width: 1000px;
margin: 2rem auto;
padding: 2rem;
}
.db-section {
background: white;
padding: 1.5rem;
margin-bottom: 2rem;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
.form-group {
margin-bottom: 1rem;
}
label {
display: block;
margin-bottom: 0.5rem;
font-weight: bold;
color: #2c3e50;
}
input, select, textarea {
width: 100%;
padding: 0.75rem;
border: 2px solid #bdc3c7;
border-radius: 4px;
font-size: 1rem;
}
.btn {
padding: 0.75rem 1.5rem;
border: none;
border-radius: 4px;
cursor: pointer;
font-weight: bold;
margin-right: 0.5rem;
margin-bottom: 0.5rem;
}
.btn-primary { background: #3498db; color: white; }
.btn-success { background: #2ecc71; color: white; }
.btn-danger { background: #e74c3c; color: white; }
.btn-warning { background: #f39c12; color: white; }
.data-display {
background: #f8f9fa;
padding: 1rem;
border-radius: 4px;
margin-top: 1rem;
max-height: 400px;
overflow-y: auto;
}
.user-card {
border: 1px solid #bdc3c7;
border-radius: 6px;
padding: 1rem;
margin-bottom: 1rem;
background: white;
}
.user-card h4 {
margin: 0 0 0.5rem 0;
color: #2c3e50;
}
.user-card p {
margin: 0.25rem 0;
color: #7f8c8d;
}
.action-buttons {
margin-top: 1rem;
}
.status {
padding: 0.5rem;
border-radius: 4px;
margin-bottom: 1rem;
text-align: center;
}
.status.success { background: #d5f4e6; color: #27ae60; }
.status.error { background: #fadbd8; color: #c0392b; }
.status.info { background: #d6eaf8; color: #2980b9; }
</style>
</head>
<body>
<div class="indexeddb-demo">
<h1>IndexedDB 客户端数据库示例</h1>
<!-- 数据库操作 -->
<section class="db-section">
<h2>数据库管理</h2>
<div id="dbStatus" class="status info">数据库状态: 未连接</div>
<div>
<button class="btn btn-primary" id="openDB">打开/创建数据库</button>
<button class="btn btn-danger" id="deleteDB">删除数据库</button>
<button class="btn btn-warning" id="clearDB">清空所有数据</button>
</div>
</section>
<!-- 用户管理 -->
<section class="db-section">
<h2>用户管理</h2>
<div class="form-group">
<label for="userName">姓名</label>
<input type="text" id="userName" placeholder="输入用户姓名">
</div>
<div class="form-group">
<label for="userEmail">邮箱</label>
<input type="email" id="userEmail" placeholder="输入用户邮箱">
</div>
<div class="form-group">
<label for="userAge">年龄</label>
<input type="number" id="userAge" placeholder="输入用户年龄" min="0" max="150">
</div>
<div class="form-group">
<label for="userDepartment">部门</label>
<select id="userDepartment">
<option value="技术部">技术部</option>
<option value="市场部">市场部</option>
<option value="销售部">销售部</option>
<option value="人事部">人事部</option>
</select>
</div>
<div>
<button class="btn btn-primary" id="addUser">添加用户</button>
<button class="btn btn-success" id="getAllUsers">获取所有用户</button>
<button class="btn btn-warning" id="searchUsers">搜索用户</button>
</div>
<div class="data-display" id="usersDisplay">
用户数据将显示在这里...
</div>
</section>
<!-- 高级查询 -->
<section class="db-section">
<h2>高级查询</h2>
<div class="form-group">
<label for="searchField">搜索字段</label>
<select id="searchField">
<option value="name">姓名</option>
<option value="email">邮箱</option>
<option value="department">部门</option>
</select>
</div>
<div class="form-group">
<label for="searchValue">搜索值</label>
<input type="text" id="searchValue" placeholder="输入搜索内容">
</div>
<div class="form-group">
<label for="minAge">最小年龄</label>
<input type="number" id="minAge" placeholder="最小年龄" min="0">
</div>
<div class="form-group">
<label for="maxAge">最大年龄</label>
<input type="number" id="maxAge" placeholder="最大年龄" min="0">
</div>
<div>
<button class="btn btn-primary" id="queryByField">按字段查询</button>
<button class="btn btn-success" id="queryByAgeRange">按年龄范围查询</button>
<button class="btn btn-warning" id="queryByIndex">使用索引查询</button>
</div>
<div class="data-display" id="queryResults">
查询结果将显示在这里...
</div>
</section>
<!-- 性能测试 -->
<section class="db-section">
<h2>性能测试</h2>
<div class="form-group">
<label for="testCount">测试数据量</label>
<input type="number" id="testCount" value="1000" min="1" max="10000">
</div>
<div>
<button class="btn btn-primary" id="batchInsert">批量插入测试数据</button>
<button class="btn btn-success" id="performanceTest">性能测试</button>
<button class="btn btn-danger" id="clearTestData">清空测试数据</button>
</div>
<div class="data-display" id="performanceResults">
性能测试结果将显示在这里...
</div>
</section>
</div>
<script>
class IndexedDBManager {
constructor(dbName = 'UserDatabase', version = 1) {
this.dbName = dbName;
this.version = version;
this.db = null;
this.STORE_NAME = 'users';
}
// 打开数据库
async open() {
return new Promise((resolve, reject) => {
const request = indexedDB.open(this.dbName, this.version);
request.onerror = () => reject(request.error);
request.onsuccess = () => {
this.db = request.result;
resolve(this.db);
};
request.onupgradeneeded = (event) => {
const db = event.target.result;
// 删除已存在的对象存储(如果存在)
if (db.objectStoreNames.contains(this.STORE_NAME)) {
db.deleteObjectStore(this.STORE_NAME);
}
// 创建新的对象存储
const store = db.createObjectStore(this.STORE_NAME, {
keyPath: 'id',
autoIncrement: true
});
// 创建索引
store.createIndex('name', 'name', { unique: false });
store.createIndex('email', 'email', { unique: true });
store.createIndex('age', 'age', { unique: false });
store.createIndex('department', 'department', { unique: false });
store.createIndex('createdAt', 'createdAt', { unique: false });
};
});
}
// 关闭数据库
close() {
if (this.db) {
this.db.close();
this.db = null;
}
}
// 删除数据库
async deleteDatabase() {
this.close();
return new Promise((resolve, reject) => {
const request = indexedDB.deleteDatabase(this.dbName);
request.onerror = () => reject(request.error);
request.onsuccess = () => resolve();
});
}
// 添加用户
async addUser(user) {
return this.withTransaction('readwrite', (store) => {
user.createdAt = new Date().toISOString();
return store.add(user);
});
}
// 获取所有用户
async getAllUsers() {
return this.withTransaction('readonly', (store) => {
return store.getAll();
});
}
// 根据ID获取用户
async getUserById(id) {
return this.withTransaction('readonly', (store) => {
return store.get(id);
});
}
// 更新用户
async updateUser(id, updates) {
return this.withTransaction('readwrite', async (store) => {
const user = await store.get(id);
if (user) {
const updatedUser = { ...user, ...updates, updatedAt: new Date().toISOString() };
return store.put(updatedUser);
}
throw new Error('用户不存在');
});
}
// 删除用户
async deleteUser(id) {
return this.withTransaction('readwrite', (store) => {
return store.delete(id);
});
}
// 按字段查询
async queryByField(field, value) {
return this.withTransaction('readonly', (store) => {
const index = store.index(field);
return index.getAll(value);
});
}
// 按年龄范围查询
async queryByAgeRange(minAge, maxAge) {
return this.withTransaction('readonly', (store) => {
const index = store.index('age');
const range = IDBKeyRange.bound(minAge, maxAge);
return index.getAll(range);
});
}
// 使用索引进行复杂查询
async queryByIndex(indexName, value) {
return this.withTransaction('readonly', (store) => {
const index = store.index(indexName);
return index.getAll(value);
});
}
// 批量插入
async batchInsert(users) {
return this.withTransaction('readwrite', async (store) => {
const promises = users.map(user => {
user.createdAt = new Date().toISOString();
return store.add(user);
});
return Promise.all(promises);
});
}
// 清空所有数据
async clearAll() {
return this.withTransaction('readwrite', (store) => {
return store.clear();
});
}
// 事务包装器
async withTransaction(mode, operation) {
if (!this.db) {
throw new Error('数据库未连接');
}
return new Promise((resolve, reject) => {
const transaction = this.db.transaction([this.STORE_NAME], mode);
const store = transaction.objectStore(this.STORE_NAME);
transaction.oncomplete = () => resolve();
transaction.onerror = () => reject(transaction.error);
const result = operation(store);
if (result instanceof IDBRequest) {
result.onsuccess = () => resolve(result.result);
result.onerror = () => reject(result.error);
} else if (result instanceof Promise) {
result.then(resolve).catch(reject);
} else {
resolve(result);
}
});
}
}
// 创建数据库管理器实例
const dbManager = new IndexedDBManager();
// DOM 元素
const dbStatus = document.getElementById('dbStatus');
const openDBBtn = document.getElementById('openDB');
const deleteDBBtn = document.getElementById('deleteDB');
const clearDBBtn = document.getElementById('clearDB');
const addUserBtn = document.getElementById('addUser');
const getAllUsersBtn = document.getElementById('getAllUsers');
const searchUsersBtn = document.getElementById('searchUsers');
const usersDisplay = document.getElementById('usersDisplay');
const queryResults = document.getElementById('queryResults');
const performanceResults = document.getElementById('performanceResults');
// 更新状态显示
function updateStatus(message, type = 'info') {
dbStatus.textContent = `数据库状态: ${message}`;
dbStatus.className = `status ${type}`;
}
// 打开数据库
openDBBtn.addEventListener('click', async () => {
try {
updateStatus('正在打开数据库...', 'info');
await dbManager.open();
updateStatus('数据库连接成功', 'success');
} catch (error) {
updateStatus(`连接失败: ${error.message}`, 'error');
}
});
// 删除数据库
deleteDBBtn.addEventListener('click', async () => {
if (confirm('确定要删除整个数据库吗?此操作不可逆!')) {
try {
updateStatus('正在删除数据库...', 'info');
await dbManager.deleteDatabase();
updateStatus('数据库已删除', 'success');
usersDisplay.textContent = '数据库已删除';
} catch (error) {
updateStatus(`删除失败: ${error.message}`, 'error');
}
}
});
// 清空数据
clearDBBtn.addEventListener('click', async () => {
if (confirm('确定要清空所有数据吗?')) {
try {
await dbManager.clearAll();
updateStatus('所有数据已清空', 'success');
usersDisplay.textContent = '数据已清空';
} catch (error) {
updateStatus(`清空失败: ${error.message}`, 'error');
}
}
});
// 添加用户
addUserBtn.addEventListener('click', async () => {
const user = {
name: document.getElementById('userName').value,
email: document.getElementById('userEmail').value,
age: parseInt(document.getElementById('userAge').value),
department: document.getElementById('userDepartment').value
};
if (!user.name || !user.email) {
alert('请填写姓名和邮箱');
return;
}
try {
await dbManager.addUser(user);
updateStatus('用户添加成功', 'success');
document.getElementById('userName').value = '';
document.getElementById('userEmail').value = '';
document.getElementById('userAge').value = '';
} catch (error) {
updateStatus(`添加失败: ${error.message}`, 'error');
}
});
// 获取所有用户
getAllUsersBtn.addEventListener('click', async () => {
try {
const users = await dbManager.getAllUsers();
displayUsers(users, usersDisplay);
updateStatus(`获取到 ${users.length} 个用户`, 'success');
} catch (error) {
updateStatus(`获取失败: ${error.message}`, 'error');
}
});
// 显示用户数据
function displayUsers(users, container) {
if (users.length === 0) {
container.innerHTML = '<p>没有找到用户数据</p>';
return;
}
container.innerHTML = users.map(user => `
<div class="user-card">
<h4>${user.name} (ID: ${user.id})</h4>
<p><strong>邮箱:</strong> ${user.email}</p>
<p><strong>年龄:</strong> ${user.age}</p>
<p><strong>部门:</strong> ${user.department}</p>
<p><strong>创建时间:</strong> ${new Date(user.createdAt).toLocaleString()}</p>
${user.updatedAt ? `<p><strong>更新时间:</strong> ${new Date(user.updatedAt).toLocaleString()}</p>` : ''}
<div class="action-buttons">
<button class="btn btn-warning" onclick="updateUser(${user.id})">更新</button>
<button class="btn btn-danger" onclick="deleteUser(${user.id})">删除</button>
</div>
</div>
`).join('');
}
// 删除用户
window.deleteUser = async (id) => {
if (confirm('确定要删除这个用户吗?')) {
try {
await dbManager.deleteUser(id);
updateStatus('用户删除成功', 'success');
// 刷新显示
const users = await dbManager.getAllUsers();
displayUsers(users, usersDisplay);
} catch (error) {
updateStatus(`删除失败: ${error.message}`, 'error');
}
}
};
// 更新用户
window.updateUser = async (id) => {
const newName = prompt('请输入新的姓名:');
if (newName) {
try {
await dbManager.updateUser(id, { name: newName });
updateStatus('用户更新成功', 'success');
// 刷新显示
const users = await dbManager.getAllUsers();
displayUsers(users, usersDisplay);
} catch (error) {
updateStatus(`更新失败: ${error.message}`, 'error');
}
}
};
// 查询功能
document.getElementById('queryByField').addEventListener('click', async () => {
const field = document.getElementById('searchField').value;
const value = document.getElementById('searchValue').value;
if (!value) {
alert('请输入搜索值');
return;
}
try {
const results = await dbManager.queryByField(field, value);
displayUsers(results, queryResults);
updateStatus(`找到 ${results.length} 个匹配的用户`, 'success');
} catch (error) {
updateStatus(`查询失败: ${error.message}`, 'error');
}
});
document.getElementById('queryByAgeRange').addEventListener('click', async () => {
const minAge = parseInt(document.getElementById('minAge').value) || 0;
const maxAge = parseInt(document.getElementById('maxAge').value) || 150;
try {
const results = await dbManager.queryByAgeRange(minAge, maxAge);
displayUsers(results, queryResults);
updateStatus(`在年龄范围 ${minAge}-${maxAge} 内找到 ${results.length} 个用户`, 'success');
} catch (error) {
updateStatus(`查询失败: ${error.message}`, 'error');
}
});
// 性能测试
document.getElementById('batchInsert').addEventListener('click', async () => {
const count = parseInt(document.getElementById('testCount').value) || 1000;
// 生成测试数据
const testUsers = Array.from({ length: count }, (_, i) => ({
name: `测试用户${i + 1}`,
email: `user${i + 1}@test.com`,
age: Math.floor(Math.random() * 50) + 18,
department: ['技术部', '市场部', '销售部', '人事部'][Math.floor(Math.random() * 4)]
}));
try {
const startTime = performance.now();
await dbManager.batchInsert(testUsers);
const endTime = performance.now();
const duration = (endTime - startTime).toFixed(2);
performanceResults.innerHTML = `
<p><strong>批量插入性能测试</strong></p>
<p>数据量: ${count} 条记录</p>
<p>耗时: ${duration} 毫秒</p>
<p>平均每条: ${(duration / count).toFixed(3)} 毫秒</p>
`;
updateStatus(`批量插入 ${count} 条记录完成`, 'success');
} catch (error) {
updateStatus(`批量插入失败: ${error.message}`, 'error');
}
});
// 初始化
document.addEventListener('DOMContentLoaded', async () => {
try {
await dbManager.open();
updateStatus('数据库连接成功', 'success');
} catch (error) {
updateStatus(`自动连接失败: ${error.message}`, 'error');
}
});
</script>
</body>
</html>
三、Service Worker与离线应用
Service Worker是现代PWA(渐进式Web应用)的核心技术,可以实现真正的离线体验。
// service-worker.js
const CACHE_NAME = 'my-pwa-cache-v1.2.0';
const API_CACHE_NAME = 'api-cache-v1.0.0';
// 需要缓存的资源
const STATIC_RESOURCES = [
'/',
'/index.html',
'/styles/main.css',
'/scripts/app.js',
'/images/logo.png',
'/images/offline.png',
'/manifest.json'
];
// 需要缓存的API端点
const API_ENDPOINTS = [
'/api/user/profile',
'/api/products/list'
];
// 安装事件
self.addEventListener('install', (event) => {
console.log('Service Worker 安装中...');
event.waitUntil(
caches.open(CACHE_NAME)
.then((cache) => {
console.log('缓存静态资源');
return cache.addAll(STATIC_RESOURCES);
})
.then(() => {
console.log('跳过等待,立即激活');
return self.skipWaiting();
})
.catch((error) => {
console.error('缓存失败:', error);
})
);
});
// 激活事件
self.addEventListener('activate', (event) => {
console.log('Service Worker 激活中...');
event.waitUntil(
caches.keys().then((cacheNames) => {
return Promise.all(
cacheNames.map((cacheName) => {
// 删除旧缓存
if (cacheName !== CACHE_NAME && cacheName !== API_CACHE_NAME) {
console.log('删除旧缓存:', cacheName);
return caches.delete(cacheName);
}
})
);
}).then(() => {
console.log('Service Worker 激活完成,接管所有客户端');
return self.clients.claim();
})
);
});
// 获取事件 - 核心缓存策略
self.addEventListener('fetch', (event) => {
const { request } = event;
const url = new URL(request.url);
// 跳过非GET请求和浏览器扩展
if (request.method !== 'GET' || url.protocol === 'chrome-extension:') {
return;
}
// 处理不同资源的缓存策略
if (isStaticResource(request)) {
// 静态资源:缓存优先
event.respondWith(staticResourceStrategy(request));
} else if (isAPIRequest(request)) {
// API请求:网络优先,失败时使用缓存
event.respondWith(apiResourceStrategy(request));
} else if (isImageRequest(request)) {
// 图片:缓存优先,支持离线降级
event.respondWith(imageResourceStrategy(request));
} else {
// 其他资源:网络优先
event.respondWith(networkFirstStrategy(request));
}
});
// 判断是否为静态资源
function isStaticResource(request) {
const url = new URL(request.url);
return STATIC_RESOURCES.some(resource =>
url.pathname.endsWith(resource)
);
}
// 判断是否为API请求
function isAPIRequest(request) {
const url = new URL(request.url);
return url.pathname.startsWith('/api/');
}
// 判断是否为图片请求
function isImageRequest(request) {
return request.destination === 'image';
}
// 静态资源策略:缓存优先
async function staticResourceStrategy(request) {
const cache = await caches.open(CACHE_NAME);
try {
// 首先尝试从缓存获取
const cachedResponse = await cache.match(request);
if (cachedResponse) {
// 同时更新缓存(在后台)
updateCache(request, cache);
return cachedResponse;
}
// 缓存中没有,从网络获取
const networkResponse = await fetch(request);
// 缓存新资源
if (networkResponse.status === 200) {
await cache.put(request, networkResponse.clone());
}
return networkResponse;
} catch (error) {
// 网络失败,返回离线页面
console.log('网络请求失败,返回离线页面');
return getOfflineResponse(request);
}
}
// API资源策略:网络优先
async function apiResourceStrategy(request) {
const cache = await caches.open(API_CACHE_NAME);
try {
// 首先尝试网络请求
const networkResponse = await fetch(request);
// 缓存成功的GET请求
if (networkResponse.status === 200 && request.method === 'GET') {
await cache.put(request, networkResponse.clone());
}
return networkResponse;
} catch (error) {
// 网络失败,尝试从缓存获取
console.log('API请求失败,尝试使用缓存');
const cachedResponse = await cache.match(request);
if (cachedResponse) {
return cachedResponse;
}
// 缓存也没有,返回错误响应
return new Response(
JSON.stringify({ error: '网络不可用,且无缓存数据' }),
{
status: 503,
headers: { 'Content-Type': 'application/json' }
}
);
}
}
// 图片资源策略:缓存优先,支持降级
async function imageResourceStrategy(request) {
const cache = await caches.open(CACHE_NAME);
try {
// 首先尝试缓存
const cachedResponse = await cache.match(request);
if (cachedResponse) {
return cachedResponse;
}
// 缓存中没有,从网络获取
const networkResponse = await fetch(request);
// 缓存图片
if (networkResponse.status === 200) {
await cache.put(request, networkResponse.clone());
}
return networkResponse;
} catch (error) {
// 网络失败,返回离线图片
console.log('图片加载失败,返回离线图片');
return cache.match('/images/offline.png');
}
}
// 网络优先策略
async function networkFirstStrategy(request) {
try {
const networkResponse = await fetch(request);
return networkResponse;
} catch (error) {
const cache = await caches.open(CACHE_NAME);
const cachedResponse = await cache.match(request);
if (cachedResponse) {
return cachedResponse;
}
return getOfflineResponse(request);
}
}
// 后台更新缓存
async function updateCache(request, cache) {
try {
const networkResponse = await fetch(request);
if (networkResponse.status === 200) {
await cache.put(request, networkResponse);
}
} catch (error) {
// 后台更新失败,静默处理
console.log('后台缓存更新失败:', error);
}
}
// 获取离线响应
async function getOfflineResponse(request) {
const url = new URL(request.url);
// 如果是HTML请求,返回离线页面
if (request.destination === 'document' ||
request.headers.get('Accept').includes('text/html')) {
const cache = await caches.open(CACHE_NAME);
const offlinePage = await cache.match('/offline.html');
return offlinePage || new Response('离线模式:请检查网络连接');
}
// 其他类型的请求返回适当的错误响应
return new Response('资源暂不可用,请检查网络连接', {
status: 503,
statusText: 'Service Unavailable'
});
}
// 后台同步
self.addEventListener('sync', (event) => {
console.log('后台同步事件:', event.tag);
if (event.tag === 'background-sync') {
event.waitUntil(doBackgroundSync());
}
});
// 执行后台同步
async function doBackgroundSync() {
try {
// 获取待同步的数据
const pendingActions = await getPendingActions();
for (const action of pendingActions) {
await syncAction(action);
}
console.log('后台同步完成');
} catch (error) {
console.error('后台同步失败:', error);
}
}
// 推送通知
self.addEventListener('push', (event) => {
console.log('接收到推送消息:', event);
if (!event.data) return;
const data = event.data.json();
const options = {
body: data.body,
icon: '/images/icon-192.png',
badge: '/images/badge-72.png',
vibrate: [100, 50, 100],
data: {
url: data.url
},
actions: [
{
action: 'open',
title: '打开应用'
},
{
action: 'close',
title: '关闭'
}
]
};
event.waitUntil(
self.registration.showNotification(data.title, options)
);
});
// 通知点击事件
self.addEventListener('notificationclick', (event) => {
console.log('通知被点击:', event);
event.notification.close();
if (event.action === 'open') {
// 打开特定URL
event.waitUntil(
clients.openWindow(event.notification.data.url)
);
} else {
// 默认打开应用主页
event.waitUntil(
clients.openWindow('/')
);
}
});
// 从IndexedDB获取待同步的操作
async function getPendingActions() {
// 这里需要实现从IndexedDB读取待同步数据的逻辑
return [];
}
// 同步单个操作
async function syncAction(action) {
try {
const response = await fetch(action.url, {
method: action.method,
headers: action.headers,
body: action.body
});
if (response.ok) {
// 同步成功,从待同步列表中移除
await removePendingAction(action.id);
}
} catch (error) {
console.error('同步操作失败:', error);
throw error;
}
}
// 从IndexedDB移除已同步的操作
async function removePendingAction(actionId) {
// 实现从IndexedDB删除数据的逻辑
}
Service Worker注册和使用
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>PWA离线应用示例</title>
<meta name="theme-color" content="#3498db">
<meta name="description" content="一个支持离线使用的渐进式Web应用">
<!-- PWA Manifest -->
<link rel="manifest" href="/manifest.json">
<!-- iOS Safari -->
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="default">
<meta name="apple-mobile-web-app-title" content="离线应用">
<link rel="apple-touch-icon" href="/images/icon-192.png">
<style>
.app-container {
max-width: 800px;
margin: 0 auto;
padding: 2rem;
font-family: Arial, sans-serif;
}
.status-indicator {
padding: 0.5rem;
border-radius: 4px;
margin-bottom: 1rem;
text-align: center;
font-weight: bold;
}
.online { background: #d5f4e6; color: #27ae60; }
.offline { background: #fadbd8; color: #c0392b; }
.feature-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 1rem;
margin: 2rem 0;
}
.feature-card {
border: 1px solid #bdc3c7;
border-radius: 8px;
padding: 1.5rem;
text-align: center;
}
.btn {
padding: 0.75rem 1.5rem;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 1rem;
margin: 0.5rem;
}
.btn-primary { background: #3498db; color: white; }
.btn-success { background: #2ecc71; color: white; }
.cache-status {
background: #f8f9fa;
padding: 1rem;
border-radius: 4px;
margin: 1rem 0;
}
</style>
</head>
<body>
<div class="app-container">
<h1>📱 PWA离线应用示例</h1>
<div id="connectionStatus" class="status-indicator">
检测网络状态...
</div>
<section>
<h2>应用功能</h2>
<div class="feature-grid">
<div class="feature-card">
<h3>🔗 网络状态</h3>
<p>实时检测在线/离线状态</p>
<button class="btn btn-primary" id="checkNetwork">检查网络</button>
</div>
<div class="feature-card">
<h3>💾 缓存管理</h3>
<p>管理Service Worker缓存</p>
<button class="btn btn-primary" id="clearCache">清空缓存</button>
</div>
<div class="feature-card">
<h3>🔄 后台同步</h3>
<p>在网络恢复时同步数据</p>
<button class="btn btn-primary" id="backgroundSync">触发同步</button>
</div>
<div class="feature-card">
<h3>🔔 推送通知</h3>
<p>接收推送消息通知</p>
<button class="btn btn-primary" id="requestNotification">请求通知权限</button>
</div>
</div>
</section>
<section>
<h2>缓存状态</h2>
<div class="cache-status" id="cacheStatus">
正在检查缓存状态...
</div>
<button class="btn btn-success" id="updateCache">更新缓存</button>
</section>
<section>
<h2>离线功能测试</h2>
<p>尝试断开网络连接,然后刷新页面测试离线功能。</p>
<div id="offlineTest">
<button class="btn btn-primary" id="testOffline">测试离线API</button>
<div id="offlineResult"></div>
</div>
</section>
</div>
<script>
class PWAApp {
constructor() {
this.registration = null;
this.init();
}
async init() {
await this.registerServiceWorker();
this.setupNetworkDetection();
this.setupEventListeners();
this.updateCacheStatus();
}
// 注册Service Worker
async registerServiceWorker() {
if ('serviceWorker' in navigator) {
try {
this.registration = await navigator.serviceWorker.register('/service-worker.js');
console.log('Service Worker 注册成功:', this.registration);
// 监听Service Worker更新
this.registration.addEventListener('updatefound', () => {
const newWorker = this.registration.installing;
console.log('发现Service Worker更新:', newWorker);
newWorker.addEventListener('statechange', () => {
console.log('Service Worker状态变化:', newWorker.state);
});
});
} catch (error) {
console.error('Service Worker 注册失败:', error);
}
} else {
console.log('当前浏览器不支持 Service Worker');
}
}
// 设置网络状态检测
setupNetworkDetection() {
const statusElement = document.getElementById('connectionStatus');
function updateNetworkStatus() {
if (navigator.onLine) {
statusElement.textContent = '✅ 在线状态';
statusElement.className = 'status-indicator online';
} else {
statusElement.textContent = '❌ 离线状态';
statusElement.className = 'status-indicator offline';
}
}
// 初始状态
updateNetworkStatus();
// 监听网络状态变化
window.addEventListener('online', updateNetworkStatus);
window.addEventListener('offline', updateNetworkStatus);
}
// 设置事件监听
setupEventListeners() {
// 检查网络状态
document.getElementById('checkNetwork').addEventListener('click', () => {
alert(`当前网络状态: ${navigator.onLine ? '在线' : '离线'}`);
});
// 清空缓存
document.getElementById('clearCache').addEventListener('click', async () => {
if (confirm('确定要清空所有缓存吗?')) {
await this.clearAllCaches();
this.updateCacheStatus();
}
});
// 后台同步
document.getElementById('backgroundSync').addEventListener('click', async () => {
await this.triggerBackgroundSync();
});
// 请求通知权限
document.getElementById('requestNotification').addEventListener('click', () => {
this.requestNotificationPermission();
});
// 更新缓存
document.getElementById('updateCache').addEventListener('click', async () => {
await this.updateServiceWorker();
});
// 测试离线功能
document.getElementById('testOffline').addEventListener('click', async () => {
await this.testOfflineFunctionality();
});
}
// 清空所有缓存
async clearAllCaches() {
try {
const cacheNames = await caches.keys();
await Promise.all(
cacheNames.map(cacheName => caches.delete(cacheName))
);
alert('所有缓存已清空');
} catch (error) {
console.error('清空缓存失败:', error);
alert('清空缓存失败');
}
}
// 触发后台同步
async triggerBackgroundSync() {
if ('sync' in this.registration) {
try {
await this.registration.sync.register('background-sync');
alert('后台同步已注册,将在网络可用时执行');
} catch (error) {
console.error('后台同步注册失败:', error);
alert('后台同步注册失败');
}
} else {
alert('当前浏览器不支持后台同步');
}
}
// 请求通知权限
async requestNotificationPermission() {
if ('Notification' in window) {
const permission = await Notification.requestPermission();
if (permission === 'granted') {
alert('通知权限已授予');
// 显示测试通知
if ('serviceWorker' in navigator) {
this.registration.showNotification('测试通知', {
body: '这是一条测试推送消息',
icon: '/images/icon-192.png',
vibrate: [200, 100, 200]
});
}
} else {
alert('通知权限被拒绝');
}
} else {
alert('当前浏览器不支持通知功能');
}
}
// 更新Service Worker
async updateServiceWorker() {
if (this.registration) {
try {
await this.registration.update();
alert('Service Worker 已更新');
this.updateCacheStatus();
} catch (error) {
console.error('Service Worker 更新失败:', error);
alert('更新失败');
}
}
}
// 更新缓存状态显示
async updateCacheStatus() {
const statusElement = document.getElementById('cacheStatus');
try {
const cacheNames = await caches.keys();
let statusHTML = '<h3>当前缓存:</h3>';
for (const cacheName of cacheNames) {
const cache = await caches.open(cacheName);
const requests = await cache.keys();
statusHTML += `
<div>
<strong>${cacheName}:</strong> ${requests.length} 个资源
<ul>
${requests.slice(0, 5).map(req =>
`<li>${new URL(req.url).pathname}</li>`
).join('')}
${requests.length > 5 ? `<li>... 还有 ${requests.length - 5} 个资源</li>` : ''}
</ul>
</div>
`;
}
statusElement.innerHTML = statusHTML;
} catch (error) {
statusElement.innerHTML = '无法获取缓存状态';
}
}
// 测试离线功能
async testOfflineFunctionality() {
const resultElement = document.getElementById('offlineResult');
resultElement.innerHTML = '<p>测试中...</p>';
try {
// 尝试访问一个API端点
const response = await fetch('/api/test');
const data = await response.json();
resultElement.innerHTML = `<p style="color: green;">✅ 在线API调用成功: ${JSON.stringify(data)}</p>`;
} catch (error) {
resultElement.innerHTML = `<p style="color: orange;">⚠️ 在线API调用失败,可能处于离线状态</p>`;
// 尝试从缓存获取
try {
const cache = await caches.open('api-cache-v1.0.0');
const cachedResponse = await cache.match('/api/test');
if (cachedResponse) {
const cachedData = await cachedResponse.json();
resultElement.innerHTML += `<p style="color: blue;">💾 从缓存获取数据: ${JSON.stringify(cachedData)}</p>`;
} else {
resultElement.innerHTML += `<p style="color: red;">❌ 缓存中也没有数据</p>`;
}
} catch (cacheError) {
resultElement.innerHTML += `<p style="color: red;">❌ 缓存访问失败</p>`;
}
}
}
}
// 初始化应用
document.addEventListener('DOMContentLoaded', () => {
new PWAApp();
});
// 监听Service Worker消息
if ('serviceWorker' in navigator) {
navigator.serviceWorker.addEventListener('message', (event) => {
console.log('接收到Service Worker消息:', event.data);
// 处理不同类型的消息
switch (event.data.type) {
case 'CACHE_UPDATED':
console.log('缓存已更新:', event.data.payload);
break;
case 'SYNC_COMPLETED':
console.log('后台同步完成:', event.data.payload);
break;
}
});
}
</script>
</body>
</html>
总结
HTML5新特性核心要点:
- 语义化标签:提高可访问性和SEO
- 多媒体支持:原生音视频播放
- Canvas绘图:强大的2D图形和动画
- 增强表单:新的输入类型和验证
- 本地存储:Web Storage和IndexedDB
- 离线应用:Service Worker和PWA
离线存储技术对比:
| 技术 | 存储容量 | 数据类型 | 查询能力 | 适用场景 |
|---|---|---|---|---|
| localStorage | 5-10MB | 键值对 | 简单 | 小量数据、用户偏好 |
| sessionStorage | 5-10MB | 键值对 | 简单 | 会话数据、临时状态 |
| IndexedDB | 大量 | 结构化 | 强大 | 复杂数据、离线应用 |
| Service Worker | 动态 | 网络响应 | 缓存策略 | 离线资源、PWA |
最佳实践建议:
- 合理选择存储方案:根据数据量和复杂度选择合适的技术
- 错误处理:所有存储操作都要有完善的错误处理
- 容量管理:定期清理过期数据,避免存储空间不足
- 数据安全:敏感数据要加密存储
- 性能优化:批量操作、异步处理提升用户体验
- 渐进增强:确保应用在离线状态下仍能基本使用
掌握这些HTML5新特性和离线存储技术,可以构建出功能丰富、用户体验优秀的现代Web应用。
火山引擎开发者社区是火山引擎打造的AI技术生态平台,聚焦Agent与大模型开发,提供豆包系列模型(图像/视频/视觉)、智能分析与会话工具,并配套评测集、动手实验室及行业案例库。社区通过技术沙龙、挑战赛等活动促进开发者成长,新用户可领50万Tokens权益,助力构建智能应用。
更多推荐
所有评论(0)