HTML5基础教程

教程简介

全面讲解HTML5的入门教程,涵盖HTML5概述、文档结构与语义化标签、表单增强、多媒体元素、Canvas绘图、SVG矢量图形、本地存储、Web Worker、离线应用等核心特性,配合丰富的代码示例和实战项目,适合前端开发初学者。

HTML5 基础教程

目录

  1. 简介
  2. HTML5 概述
  3. 文档结构与语义化标签
  4. HTML5 表单增强
  5. 多媒体元素
  6. Canvas 绘图
  7. SVG 矢量图形
  8. 本地存储 LocalStorage
  9. Web Worker 多线程
  10. 离线应用与 Service Worker
  11. 地理定位与拖放 API
  12. 实战项目:构建完整的 HTML5 应用
  13. 常见问题与解决方案
  14. 总结

简介

HTML5 是超文本标记语言(HTML)的第五个主要版本,由 W3C(万维网联盟)和 WHATWG(Web 超文本应用技术工作组)共同制定。HTML5 不仅仅是一次标记语言的升级,它更代表了现代 Web 开发理念的根本性变革。从 2004 年 WHATWG 开始起草规范,到 2014 年 10 月 28 日正式发布为 W3C 推荐标准,HTML5 经历了长达十年的打磨过程。

HTML5 的设计目标是减少对第三方插件(如 Adobe Flash、Microsoft Silverlight)的依赖,为多媒体内容和复杂应用提供原生支持。它引入了大量新元素、新属性和新 API,使开发者能够用更简洁、更语义化的代码构建功能丰富的 Web 应用。

本教程将从基础概念出发,逐步深入讲解 HTML5 的各个核心特性,包括语义化标签、表单增强、多媒体支持、Canvas 绘图、SVG 矢量图形、本地存储、Web Worker、离线应用等,并通过实战项目帮助读者巩固所学知识。

学习本教程的前置知识:

  • 基本的 HTML4/XHTML 知识
  • 了解 CSS 基础语法
  • 了解 JavaScript 基础概念(变量、函数、事件)

学习目标:

  • 掌握 HTML5 语义化标签的使用
  • 熟练运用 HTML5 新增表单元素和属性
  • 能够使用 Canvas 和 SVG 进行图形绘制
  • 理解并应用本地存储和 Web Worker
  • 能够构建完整的 HTML5 Web 应用

HTML5 概述

HTML5 的发展历程

HTML 的发展经历了多个重要阶段:

  • HTML 1.0(1991年):Tim Berners-Lee 提出的最初版本,仅包含最基本的标签
  • HTML 2.0(1995年):IETF 发布的第一个 HTML 规范标准
  • HTML 3.2(1997年):W3C 发布,增加了表格、Applet 等特性
  • HTML 4.01(1999年):引入了样式表支持,提倡结构与表现分离
  • XHTML 1.0(2000年):基于 XML 的 HTML,语法更加严格
  • HTML5(2014年):革命性升级,全面支持现代 Web 应用开发

HTML5 的设计原则

HTML5 的设计遵循以下核心原则:

  1. 兼容性(Compatibility):新特性必须对现有浏览器保持向后兼容
  2. 实用性(Utility):优先考虑实际开发需求,而非理论完美
  3. 互操作性(Interoperability):不同浏览器对同一规范的实现应保持一致
  4. 通用访问(Universal Access):任何人都能访问 Web 内容,不受设备和能力限制

HTML5 新特性概览

HTML5 引入了众多新特性,可归纳为以下几个大类:

语义化元素:

  • <header><footer><nav><article><section><aside>
  • 增强了文档的语义结构,有利于 SEO 和无障碍访问

多媒体支持:

  • <audio><video> 元素,原生支持音视频播放
  • <source> 元素支持多格式回退

图形与动画:

  • <canvas> 元素,支持 2D 和 3D 图形绘制
  • 原生 SVG 支持

表单增强:

  • 新增多种输入类型:email、url、number、date 等
  • 新增表单属性:placeholder、required、pattern 等

存储与通信:

  • LocalStorage 和 SessionStorage
  • WebSocket 全双工通信
  • Web Worker 多线程

设备访问:

  • 地理定位(Geolocation)
  • 拖放 API(Drag and Drop)
  • 文件 API(File API)

浏览器兼容性检查

在使用 HTML5 特性之前,建议先检查浏览器是否支持:

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>HTML5 特性检测</title>
</head>
<body>
    <h1>HTML5 特性支持检测</h1>
    <div id="results"></div>

    <script>
        // 获取结果显示容器
        const results = document.getElementById('results');

        // 定义要检测的 HTML5 特性
        const features = {
            'Canvas': !!document.createElement('canvas').getContext,
            'Video': !!document.createElement('video').canPlayType,
            'Audio': !!document.createElement('audio').canPlayType,
            'LocalStorage': typeof window.localStorage !== 'undefined',
            'SessionStorage': typeof window.sessionStorage !== 'undefined',
            'WebSocket': typeof window.WebSocket !== 'undefined',
            'Web Worker': typeof window.Worker !== 'undefined',
            'Geolocation': 'geolocation' in navigator,
            'Drag and Drop': 'draggable' in document.createElement('div'),
            'File API': typeof FileReader !== 'undefined',
            'History API': typeof window.history !== 'undefined',
            'SVG': !!document.createElementNS && !!document.createElementNS('http://www.w3.org/2000/svg', 'svg').createSVGRect
        };

        // 生成检测结果 HTML
        let html = '<ul>';
        for (const [feature, supported] of Object.entries(features)) {
            const status = supported ? '✅ 支持' : '❌ 不支持';
            html += `<li><strong>${feature}:</strong>${status}</li>`;
        }
        html += '</ul>';

        // 显示结果
        results.innerHTML = html;
    </script>
</body>
</html>

这段代码创建了一个完整的 HTML5 特性检测页面。它通过创建元素并检查相关方法或属性是否存在来判断浏览器是否支持特定的 HTML5 特性。在实际项目中,你也可以使用 Modernizr 等成熟的特性检测库。


文档结构与语义化标签

HTML5 文档基本结构

一个标准的 HTML5 文档结构如下:

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <!-- 字符编码声明,必须放在 <head> 的最前面 -->
    <meta charset="UTF-8">

    <!-- 视口设置,用于响应式设计 -->
    <meta name="viewport" content="width=device-width, initial-scale=1.0">

    <!-- 页面标题,对 SEO 非常重要 -->
    <title>我的 HTML5 页面</title>

    <!-- 页面描述,用于搜索引擎结果展示 -->
    <meta name="description" content="这是一个 HTML5 示例页面">

    <!-- 页面关键词(现代 SEO 中权重降低) -->
    <meta name="keywords" content="HTML5, CSS3, JavaScript">

    <!-- 作者信息 -->
    <meta name="author" content="开发者">

    <!-- 外部样式表 -->
    <link rel="stylesheet" href="styles.css">

    <!-- 页面图标 -->
    <link rel="icon" href="favicon.ico" type="image/x-icon">
</head>
<body>
    <!-- 页面头部 -->
    <header>
        <h1>网站标题</h1>
        <nav>
            <ul>
                <li><a href="#home">首页</a></li>
                <li><a href="#about">关于</a></li>
                <li><a href="#contact">联系</a></li>
            </ul>
        </nav>
    </header>

    <!-- 主要内容区域 -->
    <main>
        <article>
            <h2>文章标题</h2>
            <p>文章内容...</p>
        </article>
    </main>

    <!-- 页面底部 -->
    <footer>
        <p>&copy; 2024 版权所有</p>
    </footer>
</body>
</html>

与 HTML4 相比,HTML5 的文档声明 <!DOCTYPE html> 极其简洁,不再需要引用复杂的 DTD(文档类型定义)。<html lang="zh-CN"> 中的 lang 属性有助于搜索引擎和屏幕阅读器识别页面语言。

语义化标签详解

语义化标签是 HTML5 最重要的特性之一。它们让开发者能够用更有意义的标签来描述内容的结构和含义,而不是仅仅使用无语义的 <div><span>

<header> 用于定义页面或区块的头部区域,通常包含导航、标题、Logo 等内容:

<!-- 页面级 header -->
<header>
    <img src="logo.png" alt="网站Logo" width="200" height="60">
    <h1>我的个人博客</h1>
    <nav>
        <ul>
            <li><a href="/">首页</a></li>
            <li><a href="/blog">博客</a></li>
            <li><a href="/about">关于我</a></li>
            <li><a href="/contact">联系方式</a></li>
        </ul>
    </nav>
</header>

<!-- 文章级 header -->
<article>
    <header>
        <h2>如何学习 HTML5</h2>
        <p>作者:<time datetime="2024-01-15">2024年1月15日</time></p>
        <p>分类:<a href="/category/html">HTML</a></p>
    </header>
    <p>文章正文内容...</p>
</article>

<nav> 用于定义导航链接的集合。一个页面可以有多个 <nav> 元素,分别用于主导航、侧边栏导航、面包屑导航等:

<!-- 主导航 -->
<nav aria-label="主导航">
    <ul>
        <li><a href="/" aria-current="page">首页</a></li>
        <li>
            <a href="/products">产品</a>
            <!-- 二级导航 -->
            <ul>
                <li><a href="/products/software">软件</a></li>
                <li><a href="/products/hardware">硬件</a></li>
            </ul>
        </li>
        <li><a href="/services">服务</a></li>
        <li><a href="/contact">联系我们</a></li>
    </ul>
</nav>

<!-- 面包屑导航 -->
<nav aria-label="面包屑导航">
    <ol>
        <li><a href="/">首页</a></li>
        <li><a href="/products">产品</a></li>
        <li aria-current="page">软件产品</li>
    </ol>
</nav>

<main> 元素

<main> 表示文档的主要内容区域。每个页面只能有一个 <main> 元素,且不应嵌套在 <article><aside><header><footer><nav> 内部:

<main>
    <h1>今日新闻</h1>
    <article>
        <h2>新闻标题一</h2>
        <p>新闻内容...</p>
    </article>
    <article>
        <h2>新闻标题二</h2>
        <p>新闻内容...</p>
    </article>
</main>

<article> 元素

<article> 表示一个独立的、完整的内容块,它可以独立分发或复用。典型的使用场景包括:博客文章、新闻报道、论坛帖子、用户评论等:

<article>
    <header>
        <h2>理解 HTML5 语义化</h2>
        <p>
            作者:<a href="/author/zhangsan">张三</a>
            发布于:<time datetime="2024-03-20T10:30:00+08:00">2024年3月20日 10:30</time>
        </p>
    </header>

    <section>
        <h3>什么是语义化?</h3>
        <p>语义化是指使用恰当的 HTML 标签来描述内容的含义...</p>
    </section>

    <section>
        <h3>为什么语义化很重要?</h3>
        <p>语义化有助于搜索引擎理解页面结构...</p>
    </section>

    <footer>
        <p>标签:<a href="/tag/html5">HTML5</a>、<a href="/tag/seo">SEO</a></p>
        <p>
            <a href="#comments">评论(12)</a>
            <a href="/share">分享</a>
        </p>
    </footer>

    <!-- 嵌套的评论 article -->
    <section id="comments">
        <h3>评论</h3>
        <article>
            <header>
                <p>评论者:<a href="/user/lisi">李四</a></p>
                <p><time datetime="2024-03-20T14:00:00+08:00">2024年3月20日 14:00</time></p>
            </header>
            <p>这篇文章写得很好,很有帮助!</p>
        </article>
    </section>
</article>

<section> 元素

<section> 表示文档中的一个通用区块,通常带有标题。它用于对内容进行主题分组,但不像 <article> 那样强调独立性:

<section>
    <h2>产品特点</h2>
    <section>
        <h3>高性能</h3>
        <p>采用最新技术架构,确保卓越性能...</p>
    </section>
    <section>
        <h3>易用性</h3>
        <p>简洁直观的用户界面设计...</p>
    </section>
    <section>
        <h3>安全性</h3>
        <p>企业级安全防护机制...</p>
    </section>
</section>

<aside> 元素

<aside> 表示与页面主要内容间接相关的区域,通常用于侧边栏、广告、相关链接等:

<article>
    <h2>深入理解 CSS Grid 布局</h2>
    <p>文章正文内容...</p>

    <aside>
        <h3>相关阅读</h3>
        <ul>
            <li><a href="/articles/flexbox">Flexbox 完全指南</a></li>
            <li><a href="/articles/css-variables">CSS 自定义属性</a></li>
            <li><a href="/articles/responsive">响应式设计最佳实践</a></li>
        </ul>
    </aside>
</article>

<figure><figcaption> 元素

<figure> 用于包裹独立的流内容(如图片、图表、代码示例等),<figcaption> 为其提供标题说明:

<figure>
    <img src="chart.png" alt="2024年销售趋势图" width="600" height="400">
    <figcaption>图1:2024年月度销售趋势 - 数据来源:销售部门</figcaption>
</figure>

<!-- 代码示例也可以使用 figure -->
<figure>
    <pre><code>
function greet(name) {
    return `你好,${name}!欢迎学习 HTML5`;
}
console.log(greet('世界'));
    </code></pre>
    <figcaption>代码清单 1:简单的问候函数</figcaption>
</figure>

<time> 元素

<time> 用于表示日期或时间,其 datetime 属性提供机器可读的格式:

<!-- 日期 -->
<p>发布日期:<time datetime="2024-06-15">2024年6月15日</time></p>

<!-- 带时间的日期 -->
<p>
    会议时间:<time datetime="2024-06-15T14:30:00+08:00">2024年6月15日下午2:30</time>
</p>

<!-- 仅时间 -->
<p>营业时间:<time datetime="09:00">上午9点</time> 至 <time datetime="18:00">下午6点</time></p>

<!-- 持续时间 -->
<p>课程时长:<time datetime="PT2H30M">2小时30分钟</time></p>

<!-- 相对时间(不含 datetime) -->
<p>更新于 <time>昨天</time></p>

<mark> 元素

<mark> 用于突出显示文本内容,类似于用荧光笔标记:

<p>
    HTML5 是一种用于构建和呈现互联网内容的标记语言。
    <mark>它是万维网的核心技术之一</mark>,
    与 CSS 和 JavaScript 一起构成现代 Web 开发的三大基石。
</p>

<!-- 搜索结果高亮 -->
<div class="search-result">
    <h3><mark>HTML5</mark> 教程 - 入门到精通</h3>
    <p>本教程详细讲解 <mark>HTML5</mark> 的各种新特性和最佳实践...</p>
</div>

<details><summary> 元素

<details> 用于创建可展开/折叠的内容区域,<summary> 作为其可见的标题:

<details>
    <summary>什么是 HTML5?</summary>
    <p>HTML5 是超文本标记语言(HTML)的第五次重大修改版本。
       它是用于构建 Web 内容的一种标记语言,由万维网联盟(W3C)
       和 Web 超文本应用技术工作组(WHATWG)共同制定。</p>
</details>

<details>
    <summary>HTML5 有哪些新特性?</summary>
    <ul>
        <li>语义化标签</li>
        <li>多媒体支持(audio、video)</li>
        <li>Canvas 和 SVG 绘图</li>
        <li>本地存储</li>
        <li>Web Worker</li>
        <li>地理定位</li>
        <li>拖放 API</li>
    </ul>
</details>

<!-- 默认展开 -->
<details open>
    <summary>如何开始学习?</summary>
    <p>建议从语义化标签开始,逐步学习各个新特性...</p>
</details>

<!-- 手风琴效果 -->
<div class="accordion">
    <details name="faq">
        <summary>问题一:HTML5 难学吗?</summary>
        <p>HTML5 本身并不难学,关键在于理解每个标签的语义...</p>
    </details>
    <details name="faq">
        <summary>问题二:需要什么基础?</summary>
        <p>建议先掌握基本的 HTML 和 CSS 知识...</p>
    </details>
    <details name="faq">
        <summary>问题三:学习周期多长?</summary>
        <p>根据投入的时间和基础不同,通常需要2-4周...</p>
    </details>
</div>

注意 name 属性使得同一组的 details 元素实现互斥展开效果(一次只能展开一个)。

文档大纲

HTML5 的语义化标签形成了文档大纲(Document Outline),搜索引擎和辅助技术利用这个大纲来理解页面结构。良好的文档大纲应该是层次分明、逻辑清晰的:

<body>
    <header>
        <h1>网站标题</h1>  <!-- 第一级标题 -->
    </header>

    <main>
        <article>
            <h2>文章标题</h2>  <!-- 第二级标题 -->

            <section>
                <h3>第一部分</h3>  <!-- 第三级标题 -->
                <p>内容...</p>
            </section>

            <section>
                <h3>第二部分</h3>  <!-- 第三级标题 -->
                <p>内容...</p>
            </section>
        </article>

        <aside>
            <h2>侧边栏</h2>  <!-- 第二级标题 -->
            <p>相关内容...</p>
        </aside>
    </main>

    <footer>
        <h2>页脚信息</h2>  <!-- 第二级标题 -->
        <p>版权信息...</p>
    </footer>
</body>

HTML5 表单增强

HTML5 对表单进行了大幅增强,新增了多种输入类型、属性和元素,大大简化了表单开发和验证工作。

新增输入类型

email 类型

用于输入电子邮件地址,移动端会显示专用键盘:

<form>
    <label for="email">邮箱地址:</label>
    <input type="email"
           id="email"
           name="email"
           placeholder="请输入您的邮箱"
           required
           multiple>

    <!-- required 属性确保不为空 -->
    <!-- multiple 属性允许输入多个邮箱,用逗号分隔 -->
    <button type="submit">提交</button>
</form>

url 类型

用于输入 URL 地址:

<label for="website">个人网站:</label>
<input type="url"
       id="website"
       name="website"
       placeholder="https://example.com"
       pattern="https://.*"
       title="请输入以 https:// 开头的有效网址">

number 类型

用于输入数值,支持上下箭头调整:

<label for="quantity">购买数量:</label>
<input type="number"
       id="quantity"
       name="quantity"
       min="1"
       max="100"
       step="1"
       value="1">

<!-- 范围滑块 -->
<label for="volume">音量:</label>
<input type="range"
       id="volume"
       name="volume"
       min="0"
       max="100"
       step="5"
       value="50">
<span id="volumeDisplay">50</span>

date 和 time 类型

用于输入日期和时间:

<!-- 日期选择器 -->
<label for="birthday">出生日期:</label>
<input type="date" id="birthday" name="birthday" min="1900-01-01" max="2024-12-31">

<!-- 时间选择器 -->
<label for="meeting-time">会议时间:</label>
<input type="time" id="meeting-time" name="meeting-time" min="09:00" max="18:00">

<!-- 日期时间选择器(本地) -->
<label for="appointment">预约时间:</label>
<input type="datetime-local" id="appointment" name="appointment">

<!-- 月份选择器 -->
<label for="month">选择月份:</label>
<input type="month" id="month" name="month">

<!-- 周选择器 -->
<label for="week">选择周:</label>
<input type="week" id="week" name="week">

用于搜索框,在某些浏览器中会显示清除按钮:

<label for="search">搜索:</label>
<input type="search" id="search" name="search" placeholder="输入关键词搜索..." results="5">

tel 类型

用于电话号码输入,移动端会显示数字键盘:

<label for="phone">手机号码:</label>
<input type="tel"
       id="phone"
       name="phone"
       pattern="[0-9]{11}"
       placeholder="请输入11位手机号"
       title="请输入11位数字手机号码">

color 类型

用于颜色选择:

<label for="favcolor">选择颜色:</label>
<input type="color" id="favcolor" name="favcolor" value="#ff6600">

新增表单属性

placeholder 属性

在输入框为空时显示提示文字:

<input type="text" placeholder="请输入用户名">

required 属性

标记字段为必填项:

<input type="text" required>

pattern 属性

使用正则表达式验证输入:

<!-- 用户名:只允许字母和数字,3-16个字符 -->
<input type="text"
       name="username"
       pattern="[A-Za-z0-9]{3,16}"
       title="用户名只能包含字母和数字,长度为3-16个字符"
       required>

<!-- 邮政编码 -->
<input type="text"
       name="zipcode"
       pattern="[0-9]{6}"
       title="请输入6位数字邮政编码">

autofocus 属性

页面加载后自动聚焦到该输入框:

<input type="text" autofocus>

autocomplete 属性

控制浏览器是否自动填充:

<input type="text" name="username" autocomplete="username">
<input type="email" name="email" autocomplete="email">
<input type="tel" name="phone" autocomplete="tel">
<input type="text" name="address" autocomplete="street-address">
<input type="text" name="cc-number" autocomplete="cc-number">

<!-- 关闭自动填充 -->
<input type="text" name="sensitive-data" autocomplete="off">

min、max 和 step 属性

为数值和日期类型设置范围和步长:

<!-- 价格:最小0,最大9999.99,步长0.01 -->
<input type="number" name="price" min="0" max="9999.99" step="0.01">

<!-- 评分:1-5星 -->
<input type="number" name="rating" min="1" max="5" step="0.5">

multiple 属性

允许多个值:

<!-- 多个邮箱 -->
<input type="email" multiple>

<!-- 文件多选 -->
<input type="file" multiple accept="image/*">

form 属性

允许输入框关联到表单之外的表单:

<form id="myForm">
    <input type="text" name="username">
</form>

<!-- 这个输入框虽然在 form 外面,但通过 form 属性关联 -->
<input type="email" name="email" form="myForm">
<button type="submit" form="myForm">提交</button>

新增表单元素

<datalist> 元素

为输入框提供预定义选项列表:

<label for="browser">选择浏览器:</label>
<input type="text" id="browser" name="browser" list="browsers">
<datalist id="browsers">
    <option value="Chrome">
    <option value="Firefox">
    <option value="Safari">
    <option value="Edge">
    <option value="Opera">
</datalist>

<!-- 带标签的选项 -->
<label for="city">选择城市:</label>
<input type="text" id="city" name="city" list="cities">
<datalist id="cities">
    <option value="beijing" label="北京">
    <option value="shanghai" label="上海">
    <option value="guangzhou" label="广州">
    <option value="shenzhen" label="深圳">
</datalist>

<output> 元素

显示计算结果:

<form oninput="result.value = parseInt(a.value) + parseInt(b.value)">
    <input type="number" id="a" value="0"> +
    <input type="number" id="b" value="0"> =
    <output name="result" for="a b">0</output>
</form>

<!-- 更复杂的计算 -->
<form id="calculator">
    <label>价格:<input type="number" name="price" value="100" min="0"></label><br>
    <label>数量:<input type="number" name="quantity" value="1" min="1"></label><br>
    <label>折扣:
        <input type="range" name="discount" min="0" max="100" value="10">
        <span id="discountDisplay">10%</span>
    </label><br>
    <p>总价:<output name="total" for="price quantity discount">90</output> 元</p>
</form>

<script>
    const form = document.getElementById('calculator');
    const discountDisplay = document.getElementById('discountDisplay');

    form.addEventListener('input', () => {
        const price = parseFloat(form.price.value) || 0;
        const quantity = parseInt(form.quantity.value) || 0;
        const discount = parseInt(form.discount.value) || 0;

        discountDisplay.textContent = discount + '%';
        form.total.value = (price * quantity * (100 - discount) / 100).toFixed(2);
    });
</script>

<progress><meter> 元素

<!-- 进度条 -->
<label for="file-progress">文件上传进度:</label>
<progress id="file-progress" value="65" max="100">65%</progress>

<!-- 度量值 -->
<label for="disk-usage">磁盘使用率:</label>
<meter id="disk-usage"
       min="0" max="100"
       low="25" high="75" optimum="10"
       value="60">60%</meter>

表单验证

HTML5 提供了内置的表单验证机制,无需 JavaScript 即可实现基本验证:

<form id="registrationForm" novalidate>
    <div class="form-group">
        <label for="username">用户名:</label>
        <input type="text"
               id="username"
               name="username"
               required
               minlength="3"
               maxlength="20"
               pattern="[A-Za-z][A-Za-z0-9_]*"
               title="用户名以字母开头,只能包含字母、数字和下划线">
        <span class="error" id="usernameError"></span>
    </div>

    <div class="form-group">
        <label for="userEmail">邮箱:</label>
        <input type="email"
               id="userEmail"
               name="email"
               required>
        <span class="error" id="emailError"></span>
    </div>

    <div class="form-group">
        <label for="userAge">年龄:</label>
        <input type="number"
               id="userAge"
               name="age"
               min="1"
               max="150"
               required>
        <span class="error" id="ageError"></span>
    </div>

    <div class="form-group">
        <label for="userWebsite">个人网站:</label>
        <input type="url"
               id="userWebsite"
               name="website">
        <span class="error" id="websiteError"></span>
    </div>

    <button type="submit">注册</button>
</form>

<style>
    .form-group { margin-bottom: 15px; }
    .form-group label { display: block; margin-bottom: 5px; font-weight: bold; }
    .form-group input { width: 100%; padding: 8px; border: 1px solid #ccc; border-radius: 4px; }
    .form-group input:valid { border-color: green; }
    .form-group input:invalid { border-color: red; }
    .error { color: red; font-size: 14px; display: none; }
    .error.visible { display: block; }
</style>

<script>
    const form = document.getElementById('registrationForm');

    // 自定义验证逻辑
    form.addEventListener('submit', (e) => {
        e.preventDefault();
        let isValid = true;

        // 清除之前的错误信息
        document.querySelectorAll('.error').forEach(el => {
            el.classList.remove('visible');
            el.textContent = '';
        });

        // 验证每个字段
        const fields = ['username', 'userEmail', 'userAge', 'userWebsite'];

        fields.forEach(fieldName => {
            const field = form[fieldName];
            const errorEl = document.getElementById(fieldName + 'Error');

            if (!field.validity.valid) {
                isValid = false;

                if (field.validity.valueMissing) {
                    errorEl.textContent = '此字段为必填项';
                } else if (field.validity.typeMismatch) {
                    errorEl.textContent = `请输入有效的${field.type === 'email' ? '邮箱地址' : '网址'}`;
                } else if (field.validity.tooShort) {
                    errorEl.textContent = `最少需要${field.minLength}个字符`;
                } else if (field.validity.tooLong) {
                    errorEl.textContent = `最多允许${field.maxLength}个字符`;
                } else if (field.validity.rangeUnderflow) {
                    errorEl.textContent = `值不能小于${field.min}`;
                } else if (field.validity.rangeOverflow) {
                    errorEl.textContent = `值不能大于${field.max}`;
                } else if (field.validity.patternMismatch) {
                    errorEl.textContent = field.title || '格式不正确';
                }

                errorEl.classList.add('visible');
            }
        });

        if (isValid) {
            alert('表单验证通过!');
            // 这里可以提交表单数据
        }
    });

    // 实时验证
    form.querySelectorAll('input').forEach(input => {
        input.addEventListener('blur', () => {
            const errorEl = document.getElementById(input.id + 'Error');
            if (!input.validity.valid) {
                errorEl.textContent = input.validationMessage;
                errorEl.classList.add('visible');
            } else {
                errorEl.classList.remove('visible');
            }
        });
    });
</script>

多媒体元素

HTML5 引入了原生的多媒体支持,让开发者无需借助 Flash 等第三方插件即可在网页中嵌入音频和视频。

<video> 元素

<video> 元素用于嵌入视频内容:

<!-- 基本用法 -->
<video src="movie.mp4" width="640" height="360" controls>
    您的浏览器不支持 video 标签。
</video>

<!-- 带多个源的完整用法 -->
<video width="640" height="360" controls poster="poster.jpg" preload="metadata">
    <!-- 提供多种格式以兼容不同浏览器 -->
    <source src="movie.mp4" type="video/mp4">
    <source src="movie.webm" type="video/webm">
    <source src="movie.ogg" type="video/ogg">

    <!-- 字幕轨道 -->
    <track kind="subtitles" src="subs_zh.vtt" srclang="zh" label="中文" default>
    <track kind="subtitles" src="subs_en.vtt" srclang="en" label="English">

    <!-- 后备内容 -->
    <p>您的浏览器不支持 HTML5 视频。<a href="movie.mp4">下载视频</a></p>
</video>

video 元素常用属性:

属性 说明
src 视频文件地址
width / height 视频显示尺寸
controls 显示播放控件
autoplay 自动播放(某些浏览器需要同时设置 muted)
muted 静音
loop 循环播放
poster 视频封面图片
preload 预加载策略:none / metadata / auto

<audio> 元素

<audio> 元素用于嵌入音频内容:

<!-- 基本用法 -->
<audio src="music.mp3" controls>
    您的浏览器不支持 audio 标签。
</audio>

<!-- 带多个源的完整用法 -->
<audio controls preload="auto">
    <source src="music.mp3" type="audio/mpeg">
    <source src="music.ogg" type="audio/ogg">
    <source src="music.wav" type="audio/wav">
    <p>您的浏览器不支持 HTML5 音频。<a href="music.mp3">下载音频</a></p>
</audio>

自定义视频播放器

使用 JavaScript 控制多媒体元素:

<div class="custom-player">
    <video id="myVideo" width="640" height="360" poster="poster.jpg">
        <source src="movie.mp4" type="video/mp4">
    </video>

    <div class="controls">
        <button id="playBtn" title="播放">▶</button>
        <button id="pauseBtn" title="暂停">⏸</button>
        <button id="stopBtn" title="停止">⏹</button>

        <div class="progress-container">
            <div class="progress-bar" id="progressBar"></div>
            <input type="range" id="seekBar" min="0" max="100" value="0">
        </div>

        <span id="timeDisplay">0:00 / 0:00</span>

        <button id="muteBtn" title="静音">🔊</button>
        <input type="range" id="volumeBar" min="0" max="100" value="80">

        <button id="fullscreenBtn" title="全屏">⛶</button>
    </div>
</div>

<style>
    .custom-player { position: relative; max-width: 640px; }
    .custom-player video { width: 100%; display: block; }
    .controls {
        display: flex; align-items: center; gap: 10px;
        padding: 10px; background: #333; color: white;
    }
    .controls button { background: none; border: none; color: white; cursor: pointer; font-size: 18px; }
    .progress-container { flex: 1; position: relative; }
    .progress-bar { height: 4px; background: #555; border-radius: 2px; }
    #seekBar, #volumeBar { width: 100%; }
</style>

<script>
    const video = document.getElementById('myVideo');
    const playBtn = document.getElementById('playBtn');
    const pauseBtn = document.getElementById('pauseBtn');
    const stopBtn = document.getElementById('stopBtn');
    const seekBar = document.getElementById('seekBar');
    const volumeBar = document.getElementById('volumeBar');
    const muteBtn = document.getElementById('muteBtn');
    const fullscreenBtn = document.getElementById('fullscreenBtn');
    const timeDisplay = document.getElementById('timeDisplay');

    // 格式化时间
    function formatTime(seconds) {
        const mins = Math.floor(seconds / 60);
        const secs = Math.floor(seconds % 60);
        return `${mins}:${secs.toString().padStart(2, '0')}`;
    }

    // 播放
    playBtn.addEventListener('click', () => video.play());

    // 暂停
    pauseBtn.addEventListener('click', () => video.pause());

    // 停止
    stopBtn.addEventListener('click', () => {
        video.pause();
        video.currentTime = 0;
    });

    // 更新进度条
    video.addEventListener('timeupdate', () => {
        const progress = (video.currentTime / video.duration) * 100;
        seekBar.value = progress;
        timeDisplay.textContent = `${formatTime(video.currentTime)} / ${formatTime(video.duration)}`;
    });

    // 拖动进度条
    seekBar.addEventListener('input', () => {
        const time = video.duration * (seekBar.value / 100);
        video.currentTime = time;
    });

    // 音量控制
    volumeBar.addEventListener('input', () => {
        video.volume = volumeBar.value / 100;
        muteBtn.textContent = video.volume === 0 ? '🔇' : '🔊';
    });

    // 静音切换
    muteBtn.addEventListener('click', () => {
        video.muted = !video.muted;
        muteBtn.textContent = video.muted ? '🔇' : '🔊';
    });

    // 全屏
    fullscreenBtn.addEventListener('click', () => {
        if (video.requestFullscreen) {
            video.requestFullscreen();
        } else if (video.webkitRequestFullscreen) {
            video.webkitRequestFullscreen();
        }
    });
</script>

字幕文件 WebVTT

WebVTT(Web Video Text Tracks)是一种用于 HTML5 视频的字幕格式:

WEBVTT

00:00:01.000 --> 00:00:04.000
欢迎来到 HTML5 教程

00:00:05.000 --> 00:00:08.000
今天我们将学习多媒体元素

00:00:09.000 --> 00:00:12.000
HTML5 让视频和音频的嵌入变得非常简单

Canvas 绘图

<canvas> 是 HTML5 中最强大的绘图工具之一,它提供了一个可以通过 JavaScript 绘制图形的区域。

基本用法

<canvas id="myCanvas" width="800" height="600">
    您的浏览器不支持 Canvas。
</canvas>

<script>
    // 获取 canvas 元素和 2D 渲染上下文
    const canvas = document.getElementById('myCanvas');
    const ctx = canvas.getContext('2d');

    // 如果 getContext 返回 null,说明浏览器不支持
    if (!ctx) {
        alert('您的浏览器不支持 Canvas 2D');
    }
</script>

绘制基本图形

矩形

<canvas id="rectCanvas" width="400" height="300"></canvas>
<script>
    const canvas = document.getElementById('rectCanvas');
    const ctx = canvas.getContext('2d');

    // 绘制填充矩形
    ctx.fillStyle = '#3498db'; // 设置填充颜色
    ctx.fillRect(20, 20, 150, 100); // x, y, 宽度, 高度

    // 绘制描边矩形
    ctx.strokeStyle = '#e74c3c'; // 设置描边颜色
    ctx.lineWidth = 3; // 设置线宽
    ctx.strokeRect(200, 20, 150, 100);

    // 绘制半透明矩形
    ctx.fillStyle = 'rgba(46, 204, 113, 0.5)';
    ctx.fillRect(110, 150, 150, 100);

    // 清除矩形区域
    ctx.clearRect(130, 170, 110, 60); // 清除该区域变为透明
</script>

路径与线条

<canvas id="pathCanvas" width="400" height="300"></canvas>
<script>
    const canvas = document.getElementById('pathCanvas');
    const ctx = canvas.getContext('2d');

    // 绘制三角形
    ctx.beginPath(); // 开始新路径
    ctx.moveTo(100, 20); // 移动到起点
    ctx.lineTo(200, 120); // 画线到
    ctx.lineTo(30, 120); // 画线到
    ctx.closePath(); // 闭合路径
    ctx.fillStyle = '#9b59b6';
    ctx.fill(); // 填充
    ctx.strokeStyle = '#8e44ad';
    ctx.lineWidth = 2;
    ctx.stroke(); // 描边

    // 绘制折线
    ctx.beginPath();
    ctx.moveTo(50, 200);
    ctx.lineTo(100, 150);
    ctx.lineTo(150, 200);
    ctx.lineTo(200, 160);
    ctx.lineTo(250, 200);
    ctx.strokeStyle = '#e67e22';
    ctx.lineWidth = 3;
    ctx.stroke();
</script>

圆形与弧线

<canvas id="circleCanvas" width="400" height="300"></canvas>
<script>
    const canvas = document.getElementById('circleCanvas');
    const ctx = canvas.getContext('2d');

    // 绘制圆形
    ctx.beginPath();
    ctx.arc(100, 100, 50, 0, Math.PI * 2); // x, y, 半径, 起始角度, 结束角度
    ctx.fillStyle = '#3498db';
    ctx.fill();
    ctx.strokeStyle = '#2980b9';
    ctx.lineWidth = 2;
    ctx.stroke();

    // 绘制半圆
    ctx.beginPath();
    ctx.arc(250, 100, 50, 0, Math.PI);
    ctx.fillStyle = '#e74c3c';
    ctx.fill();

    // 绘制弧线
    ctx.beginPath();
    ctx.arc(350, 100, 50, Math.PI * 0.25, Math.PI * 0.75);
    ctx.strokeStyle = '#2ecc71';
    ctx.lineWidth = 4;
    ctx.stroke();

    // 绘制饼图
    const data = [30, 25, 20, 15, 10];
    const colors = ['#e74c3c', '#3498db', '#2ecc71', '#f39c12', '#9b59b6'];
    let startAngle = 0;
    const centerX = 200;
    const centerY = 220;
    const radius = 60;

    data.forEach((value, index) => {
        const sliceAngle = (value / 100) * Math.PI * 2;
        ctx.beginPath();
        ctx.moveTo(centerX, centerY);
        ctx.arc(centerX, centerY, radius, startAngle, startAngle + sliceAngle);
        ctx.fillStyle = colors[index];
        ctx.fill();
        ctx.stroke();
        startAngle += sliceAngle;
    });
</script>

贝塞尔曲线

<canvas id="bezierCanvas" width="500" height="300"></canvas>
<script>
    const canvas = document.getElementById('bezierCanvas');
    const ctx = canvas.getContext('2d');

    // 二次贝塞尔曲线
    ctx.beginPath();
    ctx.moveTo(20, 150);
    ctx.quadraticCurveTo(150, 20, 280, 150); // 控制点, 终点
    ctx.strokeStyle = '#3498db';
    ctx.lineWidth = 3;
    ctx.stroke();

    // 三次贝塞尔曲线
    ctx.beginPath();
    ctx.moveTo(20, 250);
    ctx.bezierCurveTo(100, 100, 300, 350, 480, 250); // 控制点1, 控制点2, 终点
    ctx.strokeStyle = '#e74c3c';
    ctx.lineWidth = 3;
    ctx.stroke();

    // 显示控制点
    ctx.fillStyle = '#2ecc71';
    [
        [150, 20], [100, 100], [300, 350]
    ].forEach(([x, y]) => {
        ctx.beginPath();
        ctx.arc(x, y, 5, 0, Math.PI * 2);
        ctx.fill();
    });
</script>

文本绘制

<canvas id="textCanvas" width="600" height="300"></canvas>
<script>
    const canvas = document.getElementById('textCanvas');
    const ctx = canvas.getContext('2d');

    // 填充文本
    ctx.font = 'bold 36px Arial, sans-serif';
    ctx.fillStyle = '#2c3e50';
    ctx.fillText('Hello, Canvas!', 50, 60);

    // 描边文本
    ctx.font = 'italic 30px Georgia, serif';
    ctx.strokeStyle = '#e74c3c';
    ctx.lineWidth = 2;
    ctx.strokeText('描边文字效果', 50, 120);

    // 带阴影的文本
    ctx.shadowColor = 'rgba(0, 0, 0, 0.3)';
    ctx.shadowBlur = 10;
    ctx.shadowOffsetX = 5;
    ctx.shadowOffsetY = 5;
    ctx.font = 'bold 40px "Microsoft YaHei", sans-serif';
    ctx.fillStyle = '#3498db';
    ctx.fillText('阴影文字', 50, 200);

    // 清除阴影
    ctx.shadowColor = 'transparent';

    // 测量文本宽度
    const text = '测量文本宽度';
    ctx.font = '24px Arial';
    const metrics = ctx.measureText(text);
    ctx.fillStyle = '#333';
    ctx.fillText(text, 50, 260);
    ctx.strokeStyle = '#e74c3c';
    ctx.strokeRect(50, 240, metrics.width, 30);
</script>

渐变与图案

<canvas id="gradientCanvas" width="500" height="400"></canvas>
<script>
    const canvas = document.getElementById('gradientCanvas');
    const ctx = canvas.getContext('2d');

    // 线性渐变
    const linearGrad = ctx.createLinearGradient(0, 0, 200, 0);
    linearGrad.addColorStop(0, '#e74c3c');
    linearGrad.addColorStop(0.5, '#f39c12');
    linearGrad.addColorStop(1, '#2ecc71');
    ctx.fillStyle = linearGrad;
    ctx.fillRect(20, 20, 200, 100);

    // 径向渐变
    const radialGrad = ctx.createRadialGradient(350, 70, 10, 350, 70, 80);
    radialGrad.addColorStop(0, '#fff');
    radialGrad.addColorStop(0.5, '#3498db');
    radialGrad.addColorStop(1, '#2c3e50');
    ctx.fillStyle = radialGrad;
    ctx.beginPath();
    ctx.arc(350, 70, 80, 0, Math.PI * 2);
    ctx.fill();

    // 图案填充
    const patternCanvas = document.createElement('canvas');
    patternCanvas.width = 20;
    patternCanvas.height = 20;
    const patternCtx = patternCanvas.getContext('2d');
    patternCtx.fillStyle = '#ecf0f1';
    patternCtx.fillRect(0, 0, 20, 20);
    patternCtx.fillStyle = '#bdc3c7';
    patternCtx.fillRect(0, 0, 10, 10);
    patternCtx.fillRect(10, 10, 10, 10);

    const pattern = ctx.createPattern(patternCanvas, 'repeat');
    ctx.fillStyle = pattern;
    ctx.fillRect(20, 160, 460, 100);
</script>

动画

<canvas id="animCanvas" width="500" height="400"></canvas>
<script>
    const canvas = document.getElementById('animCanvas');
    const ctx = canvas.getContext('2d');

    // 小球对象
    const ball = {
        x: 250,
        y: 200,
        radius: 20,
        vx: 3,  // x方向速度
        vy: 2,  // y方向速度
        color: '#e74c3c'
    };

    function draw() {
        // 清空画布
        ctx.clearRect(0, 0, canvas.width, canvas.height);

        // 绘制小球
        ctx.beginPath();
        ctx.arc(ball.x, ball.y, ball.radius, 0, Math.PI * 2);
        ctx.fillStyle = ball.color;
        ctx.fill();
        ctx.strokeStyle = '#c0392b';
        ctx.lineWidth = 2;
        ctx.stroke();

        // 更新位置
        ball.x += ball.vx;
        ball.y += ball.vy;

        // 碰撞检测 - 反弹
        if (ball.x + ball.radius > canvas.width || ball.x - ball.radius < 0) {
            ball.vx = -ball.vx;
            // 随机改变颜色
            ball.color = `hsl(${Math.random() * 360}, 70%, 50%)`;
        }
        if (ball.y + ball.radius > canvas.height || ball.y - ball.radius < 0) {
            ball.vy = -ball.vy;
            ball.color = `hsl(${Math.random() * 360}, 70%, 50%)`;
        }

        // 请求下一帧
        requestAnimationFrame(draw);
    }

    // 开始动画
    draw();
</script>

图像处理

<canvas id="imageCanvas" width="600" height="400"></canvas>
<script>
    const canvas = document.getElementById('imageCanvas');
    const ctx = canvas.getContext('2d');

    // 加载并绘制图像
    const img = new Image();
    img.crossOrigin = 'anonymous';
    img.onload = function() {
        // 绘制原始图像
        ctx.drawImage(img, 0, 0, 300, 200);

        // 绘制并缩放图像
        ctx.drawImage(img, 300, 0, 300, 200);

        // 获取图像像素数据
        const imageData = ctx.getImageData(300, 0, 300, 200);
        const data = imageData.data;

        // 灰度化处理
        for (let i = 0; i < data.length; i += 4) {
            const avg = (data[i] + data[i + 1] + data[i + 2]) / 3;
            data[i] = avg;     // R
            data[i + 1] = avg; // G
            data[i + 2] = avg; // B
        }

        // 将处理后的数据写回
        ctx.putImageData(imageData, 300, 200);
    };
    img.src = 'your-image.jpg'; // 替换为实际图片路径
</script>

SVG 矢量图形

SVG(Scalable Vector Graphics)是一种基于 XML 的矢量图形格式,与 Canvas 不同,SVG 绘制的图形可以无损缩放,且可以通过 CSS 和 JavaScript 进行操作。

基本 SVG 元素

<!-- 基本 SVG 容器 -->
<svg width="400" height="300" xmlns="http://www.w3.org/2000/svg">
    <!-- 矩形 -->
    <rect x="10" y="10" width="100" height="80"
          fill="#3498db" stroke="#2980b9" stroke-width="2"
          rx="10" ry="10"> <!-- 圆角 -->
    </rect>

    <!-- 圆形 -->
    <circle cx="200" cy="50" r="40"
            fill="#e74c3c" stroke="#c0392b" stroke-width="2">
    </circle>

    <!-- 椭圆 -->
    <ellipse cx="320" cy="50" rx="60" ry="30"
             fill="#2ecc71" stroke="#27ae60" stroke-width="2">
    </ellipse>

    <!-- 线条 -->
    <line x1="10" y1="120" x2="390" y2="120"
          stroke="#f39c12" stroke-width="3">
    </line>

    <!-- 折线 -->
    <polyline points="10,200 50,160 90,200 130,140 170,200 210,150 250,200"
              fill="none" stroke="#9b59b6" stroke-width="3">
    </polyline>

    <!-- 多边形 -->
    <polygon points="300,150 350,130 380,170 360,210 320,210"
             fill="#1abc9c" stroke="#16a085" stroke-width="2">
    </polygon>

    <!-- 文本 -->
    <text x="200" y="280"
          text-anchor="middle"
          font-size="24"
          font-weight="bold"
          fill="#2c3e50">
        SVG 图形示例
    </text>
</svg>

SVG 路径(Path)

SVG 的 <path> 元素是最强大的绘图工具,支持多种命令:

<svg width="500" height="400" xmlns="http://www.w3.org/2000/svg">
    <!-- M: 移动到, L: 画线到, Z: 闭合 -->
    <path d="M 50 50 L 150 50 L 150 150 Z"
          fill="#3498db" stroke="#2980b9" stroke-width="2"/>

    <!-- H: 水平线, V: 垂直线 -->
    <path d="M 200 50 H 300 V 150 H 200 Z"
          fill="#e74c3c" stroke="#c0392b" stroke-width="2"/>

    <!-- C: 三次贝塞尔曲线 -->
    <path d="M 50 200 C 50 100, 200 100, 200 200"
          fill="none" stroke="#2ecc71" stroke-width="3"/>

    <!-- Q: 二次贝塞尔曲线 -->
    <path d="M 250 200 Q 325 100, 400 200"
          fill="none" stroke="#f39c12" stroke-width="3"/>

    <!-- A: 弧线 -->
    <path d="M 50 300 A 50 50 0 0 1 150 300"
          fill="none" stroke="#9b59b6" stroke-width="3"/>
</svg>

SVG 渐变与滤镜

<svg width="500" height="400" xmlns="http://www.w3.org/2000/svg">
    <defs>
        <!-- 线性渐变 -->
        <linearGradient id="linearGrad" x1="0%" y1="0%" x2="100%" y2="0%">
            <stop offset="0%" style="stop-color:#e74c3c"/>
            <stop offset="50%" style="stop-color:#f39c12"/>
            <stop offset="100%" style="stop-color:#2ecc71"/>
        </linearGradient>

        <!-- 径向渐变 -->
        <radialGradient id="radialGrad" cx="50%" cy="50%" r="50%">
            <stop offset="0%" style="stop-color:#fff"/>
            <stop offset="100%" style="stop-color:#3498db"/>
        </radialGradient>

        <!-- 模糊滤镜 -->
        <filter id="blur">
            <feGaussianBlur in="SourceGraphic" stdDeviation="3"/>
        </filter>

        <!-- 阴影滤镜 -->
        <filter id="shadow" x="-20%" y="-20%" width="140%" height="140%">
            <feDropShadow dx="3" dy="3" stdDeviation="3" flood-color="#000" flood-opacity="0.3"/>
        </filter>
    </defs>

    <!-- 使用线性渐变 -->
    <rect x="20" y="20" width="200" height="80" fill="url(#linearGrad)"/>

    <!-- 使用径向渐变 -->
    <circle cx="350" cy="60" r="50" fill="url(#radialGrad)"/>

    <!-- 使用模糊滤镜 -->
    <rect x="20" y="140" width="200" height="80" fill="#9b59b6" filter="url(#blur)"/>

    <!-- 使用阴影滤镜 -->
    <rect x="260" y="140" width="200" height="80" fill="#1abc9c" filter="url(#shadow)"/>
</svg>

SVG 动画

<svg width="400" height="400" xmlns="http://www.w3.org/2000/svg">
    <!-- 属性动画 -->
    <circle cx="50" cy="50" r="20" fill="#e74c3c">
        <animate attributeName="cx" from="50" to="350" dur="3s" repeatCount="indefinite"/>
        <animate attributeName="fill" values="#e74c3c;#3498db;#2ecc71;#e74c3c" dur="3s" repeatCount="indefinite"/>
    </circle>

    <!-- 路径动画 -->
    <circle r="10" fill="#f39c12">
        <animateMotion dur="5s" repeatCount="indefinite">
            <mpath href="#motionPath"/>
        </animateMotion>
    </circle>
    <path id="motionPath" d="M 50,200 C 50,100 350,100 350,200 S 50,300 50,200"
          fill="none" stroke="#ddd" stroke-width="1"/>

    <!-- 变换动画 -->
    <rect x="180" y="280" width="40" height="40" fill="#9b59b6" transform-origin="200 300">
        <animateTransform attributeName="transform" type="rotate"
                          from="0" to="360" dur="2s" repeatCount="indefinite"/>
    </rect>
</svg>

本地存储 LocalStorage

HTML5 提供了两种客户端存储机制:LocalStorage(持久存储)和 SessionStorage(会话存储),它们都属于 Web Storage API。

LocalStorage 基本操作

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>LocalStorage 示例</title>
</head>
<body>
    <h1>LocalStorage 操作演示</h1>

    <div>
        <h2>存储数据</h2>
        <input type="text" id="keyInput" placeholder="键名">
        <input type="text" id="valueInput" placeholder="值">
        <button onclick="saveData()">保存</button>
    </div>

    <div>
        <h2>读取数据</h2>
        <input type="text" id="getKeyInput" placeholder="输入键名">
        <button onclick="loadData()">读取</button>
        <p>结果:<span id="result"></span></p>
    </div>

    <div>
        <h2>所有存储数据</h2>
        <button onclick="showAll()">显示全部</button>
        <button onclick="clearAll()">清除全部</button>
        <div id="allData"></div>
    </div>

    <script>
        // 保存数据到 LocalStorage
        function saveData() {
            const key = document.getElementById('keyInput').value.trim();
            const value = document.getElementById('valueInput').value.trim();

            if (!key) {
                alert('请输入键名');
                return;
            }

            // localStorage.setItem() 存储数据
            // 注意:LocalStorage 只能存储字符串
            localStorage.setItem(key, value);
            alert(`已保存:${key} = ${value}`);
        }

        // 从 LocalStorage 读取数据
        function loadData() {
            const key = document.getElementById('getKeyInput').value.trim();
            const result = document.getElementById('result');

            if (!key) {
                alert('请输入键名');
                return;
            }

            // localStorage.getItem() 读取数据
            const value = localStorage.getItem(key);

            if (value === null) {
                result.textContent = '未找到该键的数据';
                result.style.color = 'red';
            } else {
                result.textContent = value;
                result.style.color = 'green';
            }
        }

        // 显示所有存储数据
        function showAll() {
            const container = document.getElementById('allData');
            let html = '<ul>';

            // localStorage.length 返回存储的键值对数量
            for (let i = 0; i < localStorage.length; i++) {
                const key = localStorage.key(i); // 获取第 i 个键
                const value = localStorage.getItem(key);
                html += `<li><strong>${key}:</strong>${value}
                    <button onclick="removeData('${key}')">删除</button></li>`;
            }

            html += '</ul>';
            container.innerHTML = html || '<p>暂无数据</p>';
        }

        // 删除单条数据
        function removeData(key) {
            localStorage.removeItem(key);
            showAll(); // 刷新显示
        }

        // 清除所有数据
        function clearAll() {
            if (confirm('确定要清除所有数据吗?')) {
                localStorage.clear();
                showAll();
            }
        }

        // 页面加载时显示已有数据
        showAll();
    </script>
</body>
</html>

存储复杂数据

LocalStorage 只能存储字符串,要存储对象或数组,需要使用 JSON 序列化:

<script>
    // 存储对象
    const user = {
        name: '张三',
        age: 25,
        hobbies: ['编程', '阅读', '游戏'],
        address: {
            city: '北京',
            district: '海淀区'
        }
    };

    // 序列化后存储
    localStorage.setItem('user', JSON.stringify(user));

    // 读取并反序列化
    const savedUser = JSON.parse(localStorage.getItem('user'));
    console.log(savedUser.name);      // 张三
    console.log(savedUser.hobbies);   // ['编程', '阅读', '游戏']

    // 存储数组
    const todos = [
        { id: 1, text: '学习 HTML5', done: true },
        { id: 2, text: '学习 CSS3', done: false },
        { id: 3, text: '学习 JavaScript', done: false }
    ];

    localStorage.setItem('todos', JSON.stringify(todos));

    // 读取数组
    const savedTodos = JSON.parse(localStorage.getItem('todos'));
    console.log(savedTodos.length); // 3
</script>

待办事项应用(LocalStorage 实战)

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>待办事项 - LocalStorage 实战</title>
    <style>
        * { margin: 0; padding: 0; box-sizing: border-box; }
        body { font-family: Arial, sans-serif; background: #f5f5f5; padding: 20px; }
        .container { max-width: 600px; margin: 0 auto; background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); }
        h1 { text-align: center; margin-bottom: 20px; color: #333; }
        .input-group { display: flex; gap: 10px; margin-bottom: 20px; }
        .input-group input { flex: 1; padding: 10px; border: 1px solid #ddd; border-radius: 4px; font-size: 16px; }
        .input-group button { padding: 10px 20px; background: #3498db; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 16px; }
        .input-group button:hover { background: #2980b9; }
        .filters { display: flex; gap: 10px; margin-bottom: 15px; }
        .filters button { padding: 5px 15px; border: 1px solid #ddd; background: white; border-radius: 4px; cursor: pointer; }
        .filters button.active { background: #3498db; color: white; border-color: #3498db; }
        .todo-list { list-style: none; }
        .todo-item { display: flex; align-items: center; padding: 10px; border-bottom: 1px solid #eee; gap: 10px; }
        .todo-item.done .todo-text { text-decoration: line-through; color: #999; }
        .todo-text { flex: 1; font-size: 16px; }
        .todo-item button { padding: 5px 10px; border: none; cursor: pointer; border-radius: 4px; }
        .btn-done { background: #2ecc71; color: white; }
        .btn-delete { background: #e74c3c; color: white; }
        .stats { margin-top: 15px; padding-top: 15px; border-top: 1px solid #eee; color: #666; }
    </style>
</head>
<body>
    <div class="container">
        <h1>📝 待办事项</h1>

        <div class="input-group">
            <input type="text" id="todoInput" placeholder="添加新的待办事项..." autofocus>
            <button onclick="addTodo()">添加</button>
        </div>

        <div class="filters">
            <button class="active" onclick="filterTodos('all', this)">全部</button>
            <button onclick="filterTodos('active', this)">未完成</button>
            <button onclick="filterTodos('done', this)">已完成</button>
        </div>

        <ul class="todo-list" id="todoList"></ul>

        <div class="stats">
            <span id="statsText"></span>
            <button onclick="clearDone()" style="float: right; padding: 5px 10px; border: 1px solid #ddd; background: white; border-radius: 4px; cursor: pointer;">清除已完成</button>
        </div>
    </div>

    <script>
        // 当前过滤状态
        let currentFilter = 'all';

        // 从 LocalStorage 加载待办事项
        function loadTodos() {
            const stored = localStorage.getItem('todos');
            return stored ? JSON.parse(stored) : [];
        }

        // 保存待办事项到 LocalStorage
        function saveTodos(todos) {
            localStorage.setItem('todos', JSON.stringify(todos));
        }

        // 添加新的待办事项
        function addTodo() {
            const input = document.getElementById('todoInput');
            const text = input.value.trim();

            if (!text) {
                input.focus();
                return;
            }

            const todos = loadTodos();
            todos.push({
                id: Date.now(), // 使用时间戳作为唯一ID
                text: text,
                done: false,
                createdAt: new Date().toISOString()
            });

            saveTodos(todos);
            input.value = '';
            input.focus();
            renderTodos();
        }

        // 切换待办事项状态
        function toggleTodo(id) {
            const todos = loadTodos();
            const todo = todos.find(t => t.id === id);
            if (todo) {
                todo.done = !todo.done;
                saveTodos(todos);
                renderTodos();
            }
        }

        // 删除待办事项
        function deleteTodo(id) {
            const todos = loadTodos().filter(t => t.id !== id);
            saveTodos(todos);
            renderTodos();
        }

        // 清除已完成的待办事项
        function clearDone() {
            const todos = loadTodos().filter(t => !t.done);
            saveTodos(todos);
            renderTodos();
        }

        // 过滤待办事项
        function filterTodos(filter, btn) {
            currentFilter = filter;
            document.querySelectorAll('.filters button').forEach(b => b.classList.remove('active'));
            btn.classList.add('active');
            renderTodos();
        }

        // 渲染待办事项列表
        function renderTodos() {
            const todos = loadTodos();
            const list = document.getElementById('todoList');
            const statsText = document.getElementById('statsText');

            // 根据过滤条件筛选
            const filtered = todos.filter(todo => {
                if (currentFilter === 'active') return !todo.done;
                if (currentFilter === 'done') return todo.done;
                return true;
            });

            // 生成列表 HTML
            list.innerHTML = filtered.map(todo => `
                <li class="todo-item ${todo.done ? 'done' : ''}">
                    <input type="checkbox" ${todo.done ? 'checked' : ''} onchange="toggleTodo(${todo.id})">
                    <span class="todo-text">${escapeHtml(todo.text)}</span>
                    <button class="btn-delete" onclick="deleteTodo(${todo.id})">删除</button>
                </li>
            `).join('');

            // 更新统计
            const total = todos.length;
            const doneCount = todos.filter(t => t.done).length;
            statsText.textContent = `共 ${total} 项,已完成 ${doneCount} 项,未完成 ${total - doneCount} 项`;
        }

        // HTML 转义防止 XSS
        function escapeHtml(text) {
            const div = document.createElement('div');
            div.textContent = text;
            return div.innerHTML;
        }

        // 回车键添加
        document.getElementById('todoInput').addEventListener('keypress', (e) => {
            if (e.key === 'Enter') addTodo();
        });

        // 初始渲染
        renderTodos();
    </script>
</body>
</html>

SessionStorage

SessionStorage 与 LocalStorage 的 API 完全相同,区别在于数据的生命周期:

<script>
    // SessionStorage 的数据仅在当前会话(标签页)中有效
    // 关闭标签页后数据会被清除

    // 操作方式与 LocalStorage 完全相同
    sessionStorage.setItem('currentPage', '1');
    sessionStorage.setItem('searchKeyword', 'HTML5');

    const page = sessionStorage.getItem('currentPage');
    sessionStorage.removeItem('searchKeyword');
    sessionStorage.clear();

    // 实际应用:保存表单数据,防止页面刷新丢失
    const form = document.getElementById('myForm');
    form.addEventListener('input', () => {
        const formData = new FormData(form);
        const data = {};
        formData.forEach((value, key) => {
            data[key] = value;
        });
        sessionStorage.setItem('formData', JSON.stringify(data));
    });

    // 页面加载时恢复表单数据
    window.addEventListener('load', () => {
        const saved = sessionStorage.getItem('formData');
        if (saved) {
            const data = JSON.parse(saved);
            Object.entries(data).forEach(([key, value]) => {
                const field = form.elements[key];
                if (field) field.value = value;
            });
        }
    });
</script>

Web Worker 多线程

Web Worker 允许在后台线程中运行 JavaScript 代码,不会阻塞主线程(UI 线程),从而保持页面的响应性。

基本用法

创建 Worker 文件 worker.js

// worker.js - Worker 线程代码

// 监听来自主线程的消息
self.addEventListener('message', function(e) {
    const data = e.data;
    console.log('Worker 收到消息:', data);

    // 执行耗时计算
    let result;
    switch (data.type) {
        case 'sum':
            result = calculateSum(data.value);
            break;
        case 'fibonacci':
            result = fibonacci(data.value);
            break;
        case 'prime':
            result = findPrimes(data.value);
            break;
        default:
            result = '未知操作';
    }

    // 将结果发送回主线程
    self.postMessage({
        type: data.type,
        result: result
    });
});

// 计算 1 到 n 的和
function calculateSum(n) {
    let sum = 0;
    for (let i = 1; i <= n; i++) {
        sum += i;
    }
    return sum;
}

// 计算斐波那契数列
function fibonacci(n) {
    if (n <= 1) return n;
    let prev = 0, curr = 1;
    for (let i = 2; i <= n; i++) {
        [prev, curr] = [curr, prev + curr];
    }
    return curr;
}

// 查找素数
function findPrimes(limit) {
    const primes = [];
    for (let i = 2; i <= limit; i++) {
        let isPrime = true;
        for (let j = 2; j <= Math.sqrt(i); j++) {
            if (i % j === 0) {
                isPrime = false;
                break;
            }
        }
        if (isPrime) primes.push(i);
    }
    return primes.length;
}

在主线程中使用 Worker:

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>Web Worker 示例</title>
</head>
<body>
    <h1>Web Worker 演示</h1>

    <div>
        <h2>计算 1 到 N 的和</h2>
        <input type="number" id="sumInput" value="100000000" min="1">
        <button onclick="calculateInWorker('sum')">使用 Worker 计算</button>
        <button onclick="calculateInMain('sum')">在主线程计算</button>
    </div>

    <div>
        <h2>计算斐波那契数列</h2>
        <input type="number" id="fibInput" value="45" min="1">
        <button onclick="calculateInWorker('fibonacci')">使用 Worker 计算</button>
    </div>

    <div>
        <h2>查找素数个数</h2>
        <input type="number" id="primeInput" value="1000000" min="2">
        <button onclick="calculateInWorker('prime')">使用 Worker 计算</button>
    </div>

    <div id="status" style="margin-top: 20px; padding: 10px; background: #f0f0f0;"></div>
    <div id="result" style="margin-top: 10px; padding: 10px; background: #e8f5e9;"></div>

    <script>
        // 创建 Worker 实例
        const worker = new Worker('worker.js');
        const status = document.getElementById('status');
        const result = document.getElementById('result');

        // 监听 Worker 的响应
        worker.addEventListener('message', function(e) {
            const data = e.data;
            const endTime = performance.now();
            status.textContent = `计算完成!类型:${data.type}`;
            result.innerHTML = `<strong>结果:</strong>${data.result}<br><strong>耗时:</strong>${(endTime - window.startTime).toFixed(2)} 毫秒`;
        });

        // 监听 Worker 错误
        worker.addEventListener('error', function(e) {
            status.textContent = `Worker 错误:${e.message}`;
            result.textContent = '';
        });

        // 使用 Worker 计算
        function calculateInWorker(type) {
            let value;
            switch (type) {
                case 'sum':
                    value = parseInt(document.getElementById('sumInput').value);
                    break;
                case 'fibonacci':
                    value = parseInt(document.getElementById('fibInput').value);
                    break;
                case 'prime':
                    value = parseInt(document.getElementById('primeInput').value);
                    break;
            }

            status.textContent = '正在使用 Worker 计算...';
            result.textContent = '';
            window.startTime = performance.now();

            // 发送消息给 Worker
            worker.postMessage({ type: type, value: value });
        }

        // 在主线程计算(对比用)
        function calculateInMain(type) {
            const value = parseInt(document.getElementById('sumInput').value);
            status.textContent = '正在主线程计算(页面可能会卡顿)...';
            result.textContent = '';

            // 使用 setTimeout 让 UI 有机会更新
            setTimeout(() => {
                const startTime = performance.now();
                let sum = 0;
                for (let i = 1; i <= value; i++) {
                    sum += i;
                }
                const endTime = performance.now();
                status.textContent = '主线程计算完成!';
                result.innerHTML = `<strong>结果:</strong>${sum}<br><strong>耗时:</strong>${(endTime - startTime).toFixed(2)} 毫秒`;
            }, 10);
        }

        // 终止 Worker(不再需要时)
        // worker.terminate();
    </script>
</body>
</html>

Worker 的限制

Web Worker 有一些重要的限制:

// Worker 线程中不能做的事情:

// 1. 不能访问 DOM
// document.getElementById('test'); // ❌ 错误

// 2. 不能访问 window 对象
// window.location; // ❌ 错误

// 3. 不能访问 document 对象
// document.title; // ❌ 错误

// 4. 不能访问 parent 对象
// parent.someFunction(); // ❌ 错误

// Worker 线程可以做的事情:

// 1. 可以使用 navigator 对象
console.log(navigator.userAgent);

// 2. 可以使用 XMLHttpRequest 或 fetch
fetch('https://api.example.com/data')
    .then(response => response.json())
    .then(data => self.postMessage(data));

// 3. 可以使用定时器
setTimeout(() => {
    self.postMessage('定时器触发');
}, 1000);

// 4. 可以使用 WebSockets
const ws = new WebSocket('wss://example.com/socket');

// 5. 可以使用 IndexedDB
// 6. 可以使用 Cache API
// 7. 可以导入其他脚本
importScripts('utils.js');

共享 SharedWorker

SharedWorker 可以被多个页面共享:

// shared-worker.js
let connections = 0;

self.addEventListener('connect', function(e) {
    connections++;
    const port = e.ports[0];

    port.postMessage({ type: 'connected', connections: connections });

    port.addEventListener('message', function(e) {
        // 广播消息给所有连接
        port.postMessage({ type: 'echo', data: e.data });
    });

    port.start();
});
<script>
    // 使用 SharedWorker
    const sharedWorker = new SharedWorker('shared-worker.js');

    sharedWorker.port.addEventListener('message', function(e) {
        console.log('收到消息:', e.data);
    });

    sharedWorker.port.start();

    // 发送消息
    sharedWorker.port.postMessage('Hello from page!');
</script>

离线应用与 Service Worker

Service Worker 是实现离线应用和后台同步的核心技术。

Service Worker 注册

<script>
    // 检查浏览器是否支持 Service Worker
    if ('serviceWorker' in navigator) {
        window.addEventListener('load', async () => {
            try {
                // 注册 Service Worker
                const registration = await navigator.serviceWorker.register('/sw.js');
                console.log('Service Worker 注册成功:', registration.scope);

                // 检查更新
                registration.addEventListener('updatefound', () => {
                    const newWorker = registration.installing;
                    console.log('Service Worker 更新中...');

                    newWorker.addEventListener('statechange', () => {
                        console.log('Service Worker 状态:', newWorker.state);
                    });
                });
            } catch (error) {
                console.error('Service Worker 注册失败:', error);
            }
        });
    }
</script>

Service Worker 实现

// sw.js - Service Worker 文件

const CACHE_NAME = 'my-app-v1';
const ASSETS_TO_CACHE = [
    '/',
    '/index.html',
    '/styles.css',
    '/app.js',
    '/images/logo.png',
    '/offline.html'
];

// 安装事件 - 缓存资源
self.addEventListener('install', (event) => {
    console.log('Service Worker 安装中...');
    event.waitUntil(
        caches.open(CACHE_NAME)
            .then((cache) => {
                console.log('缓存资源...');
                return cache.addAll(ASSETS_TO_CACHE);
            })
            .then(() => self.skipWaiting())
    );
});

// 激活事件 - 清理旧缓存
self.addEventListener('activate', (event) => {
    console.log('Service Worker 激活中...');
    event.waitUntil(
        caches.keys().then((cacheNames) => {
            return Promise.all(
                cacheNames.map((cacheName) => {
                    if (cacheName !== CACHE_NAME) {
                        console.log('删除旧缓存:', cacheName);
                        return caches.delete(cacheName);
                    }
                })
            );
        }).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((networkResponse) => {
                        // 检查是否是有效响应
                        if (!networkResponse || networkResponse.status !== 200) {
                            return networkResponse;
                        }

                        // 克隆响应(因为响应只能使用一次)
                        const responseToCache = networkResponse.clone();

                        // 将新资源添加到缓存
                        caches.open(CACHE_NAME)
                            .then((cache) => {
                                cache.put(event.request, responseToCache);
                            });

                        return networkResponse;
                    })
                    .catch(() => {
                        // 网络请求失败,返回离线页面
                        if (event.request.destination === 'document') {
                            return caches.match('/offline.html');
                        }
                    });
            })
    );
});

// 后台同步
self.addEventListener('sync', (event) => {
    if (event.tag === 'sync-data') {
        event.waitUntil(syncData());
    }
});

async function syncData() {
    // 从 IndexedDB 获取待同步数据
    // 发送到服务器
    console.log('后台同步完成');
}

地理定位与拖放 API

地理定位

<button onclick="getLocation()">获取位置</button>
<div id="location"></div>

<script>
    function getLocation() {
        const output = document.getElementById('location');

        if (!navigator.geolocation) {
            output.textContent = '您的浏览器不支持地理定位';
            return;
        }

        output.textContent = '正在获取位置...';

        navigator.geolocation.getCurrentPosition(
            // 成功回调
            (position) => {
                const { latitude, longitude, accuracy } = position.coords;
                output.innerHTML = `
                    <p>纬度:${latitude}</p>
                    <p>经度:${longitude}</p>
                    <p>精度:${accuracy} 米</p>
                `;
            },
            // 错误回调
            (error) => {
                switch (error.code) {
                    case error.PERMISSION_DENIED:
                        output.textContent = '用户拒绝了定位请求';
                        break;
                    case error.POSITION_UNAVAILABLE:
                        output.textContent = '位置信息不可用';
                        break;
                    case error.TIMEOUT:
                        output.textContent = '获取位置超时';
                        break;
                }
            },
            // 选项
            {
                enableHighAccuracy: true,
                timeout: 10000,
                maximumAge: 0
            }
        );
    }

    // 持续监听位置变化
    const watchId = navigator.geolocation.watchPosition(
        (position) => {
            console.log('位置更新:', position.coords);
        },
        (error) => {
            console.error('位置错误:', error);
        }
    );

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

拖放 API

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>拖放 API 示例</title>
    <style>
        .container { display: flex; gap: 20px; padding: 20px; }
        .drop-zone {
            width: 300px; min-height: 300px;
            border: 2px dashed #ccc; border-radius: 8px;
            padding: 15px; background: #f9f9f9;
        }
        .drop-zone.drag-over { border-color: #3498db; background: #e3f2fd; }
        .draggable {
            padding: 10px 15px; margin: 5px 0;
            background: #3498db; color: white;
            border-radius: 4px; cursor: grab;
        }
        .draggable:active { cursor: grabbing; }
        .draggable.dragging { opacity: 0.5; }
    </style>
</head>
<body>
    <h1 style="padding: 20px;">拖放排序示例</h1>

    <div class="container">
        <div class="drop-zone" id="zone1">
            <h3>列表 A</h3>
            <div class="draggable" draggable="true" id="item1">项目 1</div>
            <div class="draggable" draggable="true" id="item2">项目 2</div>
            <div class="draggable" draggable="true" id="item3">项目 3</div>
        </div>

        <div class="drop-zone" id="zone2">
            <h3>列表 B</h3>
            <div class="draggable" draggable="true" id="item4">项目 4</div>
            <div class="draggable" draggable="true" id="item5">项目 5</div>
        </div>
    </div>

    <script>
        let draggedItem = null;

        // 获取所有可拖拽元素
        document.querySelectorAll('.draggable').forEach(item => {
            // 拖拽开始
            item.addEventListener('dragstart', (e) => {
                draggedItem = item;
                item.classList.add('dragging');
                e.dataTransfer.effectAllowed = 'move';
                e.dataTransfer.setData('text/plain', item.id);
            });

            // 拖拽结束
            item.addEventListener('dragend', () => {
                item.classList.remove('dragging');
                draggedItem = null;
                document.querySelectorAll('.drop-zone').forEach(zone => {
                    zone.classList.remove('drag-over');
                });
            });
        });

        // 获取所有放置区域
        document.querySelectorAll('.drop-zone').forEach(zone => {
            // 拖拽进入
            zone.addEventListener('dragenter', (e) => {
                e.preventDefault();
                zone.classList.add('drag-over');
            });

            // 拖拽经过
            zone.addEventListener('dragover', (e) => {
                e.preventDefault();
                e.dataTransfer.dropEffect = 'move';

                // 获取鼠标位置,确定插入点
                const afterElement = getDragAfterElement(zone, e.clientY);
                if (draggedItem) {
                    if (afterElement) {
                        zone.insertBefore(draggedItem, afterElement);
                    } else {
                        zone.appendChild(draggedItem);
                    }
                }
            });

            // 拖拽离开
            zone.addEventListener('dragleave', () => {
                zone.classList.remove('drag-over');
            });

            // 放置
            zone.addEventListener('drop', (e) => {
                e.preventDefault();
                zone.classList.remove('drag-over');
            });
        });

        // 获取拖拽后的插入位置
        function getDragAfterElement(container, y) {
            const draggableElements = [...container.querySelectorAll('.draggable: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;
        }
    </script>
</body>
</html>

实战项目:构建完整的 HTML5 应用

下面我们将综合运用前面学到的知识,构建一个完整的 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>
        * { margin: 0; padding: 0; box-sizing: border-box; }
        body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f0f2f5; }

        .app { display: flex; height: 100vh; }

        /* 侧边栏 */
        .sidebar {
            width: 280px; background: #2c3e50; color: white;
            display: flex; flex-direction: column;
        }
        .sidebar-header { padding: 20px; border-bottom: 1px solid #34495e; }
        .sidebar-header h1 { font-size: 20px; }
        .sidebar-header input {
            width: 100%; padding: 8px; margin-top: 10px;
            background: #34495e; border: none; border-radius: 4px;
            color: white; font-size: 14px;
        }
        .sidebar-header input::placeholder { color: #95a5a6; }

        .note-list { flex: 1; overflow-y: auto; padding: 10px; }
        .note-item {
            padding: 12px; margin-bottom: 8px;
            background: #34495e; border-radius: 6px;
            cursor: pointer; transition: background 0.2s;
        }
        .note-item:hover { background: #3d566e; }
        .note-item.active { background: #3498db; }
        .note-item h3 { font-size: 14px; margin-bottom: 4px; }
        .note-item p { font-size: 12px; color: #95a5a6; }
        .note-item .date { font-size: 11px; color: #7f8c8d; margin-top: 4px; }

        .sidebar-footer {
            padding: 15px; border-top: 1px solid #34495e;
        }
        .btn-new {
            width: 100%; padding: 10px;
            background: #3498db; color: white;
            border: none; border-radius: 4px;
            cursor: pointer; font-size: 14px;
        }
        .btn-new:hover { background: #2980b9; }

        /* 编辑区域 */
        .editor { flex: 1; display: flex; flex-direction: column; }
        .editor-header {
            padding: 15px 20px; background: white;
            border-bottom: 1px solid #e0e0e0;
            display: flex; align-items: center; gap: 10px;
        }
        .editor-header input {
            flex: 1; border: none; font-size: 20px;
            font-weight: bold; outline: none;
        }
        .editor-actions button {
            padding: 6px 12px; border: 1px solid #ddd;
            background: white; border-radius: 4px; cursor: pointer;
        }
        .editor-actions button:hover { background: #f5f5f5; }

        .editor-content {
            flex: 1; padding: 20px; background: white;
        }
        .editor-content textarea {
            width: 100%; height: 100%; border: none;
            outline: none; resize: none; font-size: 16px;
            line-height: 1.6; font-family: inherit;
        }

        /* 状态栏 */
        .status-bar {
            padding: 8px 20px; background: #f8f9fa;
            border-top: 1px solid #e0e0e0;
            font-size: 12px; color: #666;
            display: flex; justify-content: space-between;
        }

        /* 空状态 */
        .empty-state {
            flex: 1; display: flex;
            align-items: center; justify-content: center;
            color: #999; font-size: 18px;
        }
    </style>
</head>
<body>
    <div class="app">
        <!-- 侧边栏 -->
        <aside class="sidebar">
            <div class="sidebar-header">
                <h1>📝 我的笔记</h1>
                <input type="search" id="searchInput" placeholder="搜索笔记...">
            </div>

            <div class="note-list" id="noteList"></div>

            <div class="sidebar-footer">
                <button class="btn-new" onclick="createNote()">+ 新建笔记</button>
            </div>
        </aside>

        <!-- 编辑区域 -->
        <main class="editor" id="editor" style="display: none;">
            <div class="editor-header">
                <input type="text" id="noteTitle" placeholder="笔记标题">
                <div class="editor-actions">
                    <button onclick="deleteNote()">🗑️ 删除</button>
                </div>
            </div>
            <div class="editor-content">
                <textarea id="noteContent" placeholder="开始写笔记..."></textarea>
            </div>
            <div class="status-bar">
                <span id="wordCount">0 字</span>
                <span id="lastSaved">未保存</span>
            </div>
        </main>

        <!-- 空状态 -->
        <div class="empty-state" id="emptyState">
            <p>选择一个笔记或创建新笔记</p>
        </div>
    </div>

    <script>
        // 应用状态
        let notes = [];
        let currentNoteId = null;
        let saveTimeout = null;

        // 初始化应用
        function init() {
            loadNotes();
            renderNoteList();
            setupEventListeners();
            setupSearch();
        }

        // 从 LocalStorage 加载笔记
        function loadNotes() {
            const stored = localStorage.getItem('notes');
            notes = stored ? JSON.parse(stored) : [];
        }

        // 保存笔记到 LocalStorage
        function saveNotes() {
            localStorage.setItem('notes', JSON.stringify(notes));
        }

        // 渲染笔记列表
        function renderNoteList(filter = '') {
            const list = document.getElementById('noteList');
            const filtered = filter
                ? notes.filter(n => n.title.includes(filter) || n.content.includes(filter))
                : notes;

            // 按更新时间排序
            filtered.sort((a, b) => b.updatedAt - a.updatedAt);

            list.innerHTML = filtered.map(note => `
                <div class="note-item ${note.id === currentNoteId ? 'active' : ''}"
                     onclick="selectNote(${note.id})">
                    <h3>${escapeHtml(note.title || '无标题')}</h3>
                    <p>${escapeHtml((note.content || '').substring(0, 50))}${note.content && note.content.length > 50 ? '...' : ''}</p>
                    <div class="date">${formatDate(note.updatedAt)}</div>
                </div>
            `).join('');
        }

        // 创建新笔记
        function createNote() {
            const note = {
                id: Date.now(),
                title: '',
                content: '',
                createdAt: Date.now(),
                updatedAt: Date.now()
            };

            notes.unshift(note);
            saveNotes();
            selectNote(note.id);
            renderNoteList();
            document.getElementById('noteTitle').focus();
        }

        // 选择笔记
        function selectNote(id) {
            currentNoteId = id;
            const note = notes.find(n => n.id === id);

            if (note) {
                document.getElementById('editor').style.display = 'flex';
                document.getElementById('emptyState').style.display = 'none';
                document.getElementById('noteTitle').value = note.title;
                document.getElementById('noteContent').value = note.content;
                updateWordCount();
                updateLastSaved(note.updatedAt);
            }

            renderNoteList(document.getElementById('searchInput').value);
        }

        // 删除笔记
        function deleteNote() {
            if (!currentNoteId || !confirm('确定要删除这条笔记吗?')) return;

            notes = notes.filter(n => n.id !== currentNoteId);
            saveNotes();
            currentNoteId = null;

            document.getElementById('editor').style.display = 'none';
            document.getElementById('emptyState').style.display = 'flex';
            renderNoteList();
        }

        // 设置事件监听器
        function setupEventListeners() {
            const titleInput = document.getElementById('noteTitle');
            const contentInput = document.getElementById('noteContent');

            // 自动保存
            [titleInput, contentInput].forEach(input => {
                input.addEventListener('input', () => {
                    if (!currentNoteId) return;

                    const note = notes.find(n => n.id === currentNoteId);
                    if (note) {
                        note.title = titleInput.value;
                        note.content = contentInput.value;
                        note.updatedAt = Date.now();

                        // 防抖保存
                        clearTimeout(saveTimeout);
                        saveTimeout = setTimeout(() => {
                            saveNotes();
                            renderNoteList(document.getElementById('searchInput').value);
                            updateLastSaved(note.updatedAt);
                        }, 500);

                        updateWordCount();
                    }
                });
            });
        }

        // 搜索功能
        function setupSearch() {
            const searchInput = document.getElementById('searchInput');
            searchInput.addEventListener('input', () => {
                renderNoteList(searchInput.value);
            });
        }

        // 更新字数统计
        function updateWordCount() {
            const content = document.getElementById('noteContent').value;
            document.getElementById('wordCount').textContent = `${content.length} 字`;
        }

        // 更新最后保存时间
        function updateLastSaved(timestamp) {
            document.getElementById('lastSaved').textContent = `已保存 ${formatDate(timestamp)}`;
        }

        // 格式化日期
        function formatDate(timestamp) {
            const date = new Date(timestamp);
            const now = new Date();
            const diff = now - date;

            if (diff < 60000) return '刚刚';
            if (diff < 3600000) return `${Math.floor(diff / 60000)} 分钟前`;
            if (diff < 86400000) return `${Math.floor(diff / 3600000)} 小时前`;

            return `${date.getMonth() + 1}月${date.getDate()}日 ${date.getHours()}:${String(date.getMinutes()).padStart(2, '0')}`;
        }

        // HTML 转义
        function escapeHtml(text) {
            const div = document.createElement('div');
            div.textContent = text;
            return div.innerHTML;
        }

        // 启动应用
        init();
    </script>
</body>
</html>

常见问题与解决方案

Q1: HTML5 页面在旧浏览器中无法正常显示怎么办?

解决方案:

  1. 使用 HTML5 Shiv(shiv.js)让旧版 IE 支持 HTML5 元素
  2. 使用特性检测(如 Modernizr)来判断浏览器支持情况
  3. 提供优雅降级方案
<!-- 条件注释加载 HTML5 Shiv(仅针对 IE9 以下) -->
<!--[if lt IE 9]>
<script src="https://cdn.jsdelivr.net/npm/html5shiv@3.7.3/dist/html5shiv.min.js"></script>
<![endif]-->

Q2: Canvas 绘制模糊怎么办?

解决方案: 使用设备像素比(devicePixelRatio)来调整 Canvas 分辨率

function setupCanvas(canvas) {
    const dpr = window.devicePixelRatio || 1;
    const rect = canvas.getBoundingClientRect();

    // 设置 Canvas 的实际像素大小
    canvas.width = rect.width * dpr;
    canvas.height = rect.height * dpr;

    // 缩放绘图上下文
    const ctx = canvas.getContext('2d');
    ctx.scale(dpr, dpr);

    // 设置 CSS 大小保持不变
    canvas.style.width = rect.width + 'px';
    canvas.style.height = rect.height + 'px';

    return ctx;
}

Q3: LocalStorage 存储空间不足怎么办?

解决方案:

  1. LocalStorage 通常限制为 5-10MB
  2. 使用 try-catch 处理存储异常
  3. 考虑使用 IndexedDB 存储大量数据
function setStorageItem(key, value) {
    try {
        localStorage.setItem(key, JSON.stringify(value));
        return true;
    } catch (e) {
        if (e.name === 'QuotaExceededError') {
            console.error('存储空间不足');
            // 清理旧数据或提示用户
            return false;
        }
        throw e;
    }
}

Q4: Web Worker 如何共享数据?

解决方案: 使用 SharedArrayBuffer 或 Transferable Objects

// 使用 Transferable Objects 传输大数组
const buffer = new ArrayBuffer(1024 * 1024); // 1MB
const array = new Uint8Array(buffer);

// 第二个参数指定要转移所有权的对象
worker.postMessage({ data: array }, [buffer]);
// 传输后,主线程中的 buffer 将不可用

Q5: Service Worker 更新后用户看到的还是旧内容怎么办?

解决方案:

// 在 sw.js 中
self.addEventListener('activate', (event) => {
    event.waitUntil(
        caches.keys().then((cacheNames) => {
            return Promise.all(
                cacheNames.map((name) => caches.delete(name))
            );
        }).then(() => self.clients.claim())
    );
});

// 在页面中检查更新
navigator.serviceWorker.ready.then((registration) => {
    registration.update();
});

// 监听控制器变化
navigator.serviceWorker.addEventListener('controllerchange', () => {
    window.location.reload();
});

总结

本教程全面介绍了 HTML5 的核心特性:

  1. 语义化标签:使用 <header><nav><main><article><section><aside> 等标签构建有意义的文档结构
  2. 表单增强:新增输入类型(email、url、number、date 等)、新属性(placeholder、required、pattern 等)和新元素(datalist、output、progress 等)
  3. 多媒体:原生 <audio><video> 元素,支持多种格式和字幕
  4. Canvas:2D 绘图 API,支持图形、文本、图像处理和动画
  5. SVG:矢量图形,支持无损缩放和动画
  6. LocalStorage:客户端持久化存储
  7. Web Worker:后台多线程处理
  8. Service Worker:离线应用和后台同步
  9. 地理定位和拖放:设备能力访问

HTML5 是现代 Web 开发的基础,掌握这些特性将为你构建丰富、交互性强的 Web 应用打下坚实的基础。建议通过实践项目来巩固所学知识,逐步探索更多高级特性。

下一步学习建议:

  • 深入学习 CSS3 的新特性
  • 学习 JavaScript ES6+ 语法
  • 探索 PWA(渐进式 Web 应用)开发
  • 学习 Canvas 的 WebGL 3D 绘图
  • 了解 Web Components 标准

内容声明

本文内容为AI技术学习教程,仅供学习参考。如涉及技术问题,欢迎通过 xurj005@163.com 与我们交流。

目录