前端开发常见代码片段(详细版本)

前端开发常见代码片段(详细版本)

欢迎来到这份全面的前端开发代码片段教程。作为资深教程设计师与知识传播专家,我将为您系统地讲解前端开发中HTML、CSS、JavaScript的核心概念与实用技巧,并通过大量代码示例、图表和公式,帮助您从入门到精通。

本教程旨在提供一份详尽的参考手册,覆盖前端开发中常见的布局、交互、性能优化以及现代开发实践。

大纲概览


第一部分:HTML/CSS 常见片段

1. HTML 核心结构

HTML(HyperText Markup Language)是网页内容的骨架。理解其核心结构和语义化标签是构建可访问、SEO友好页面的基础。

1.1 基础文档结构

每个HTML页面都应遵循以下基本结构:

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>页面标题</title>
    <!-- 引入外部CSS文件 -->
    <link rel="stylesheet" href="styles.css">
    <!-- 引入外部JavaScript文件 -->
    <script src="script.js" defer></script>
</head>
<body>
    <!-- 页面内容将在此处 -->
</body>
</html>

1.2 语义化标签

语义化标签是指用恰当的HTML标签来描述内容的含义,而非仅仅是其外观。这不仅有助于SEO,也提升了代码的可读性和可维护性。

<header>
    <h1>我的个人博客</h1>
    <nav>
        <ul>
            <li><a href="/">首页</a></li>
            <li><a href="/articles">文章</a></li>
            <li><a href="/about">关于我</a></li>
        </ul>
    </nav>
</header>

<main>
    <article>
        <h2>前端性能优化深度指南</h2>
        <p><time datetime="2023-10-27">2023年10月27日</time> 由 <span itemprop="author">张三</span> 发布</p>
        <section>
            <h3>图片优化</h3>
            <p>使用WebP格式,懒加载图片等。</p>
            <figure>
                <img src="optimized-image.webp" alt="优化后的图片">
                <figcaption>图片优化示例图</figcaption>
            </figure>
        </section>
        <section>
            <h3>CSS与JS优化</h3>
            <p>代码分割,按需加载。</p>
        </section>
    </article>

    <aside>
        <h3>热门文章</h3>
        <ul>
            <li><a href="#">JavaScript异步编程</a></li>
            <li><a href="#">CSS Grid布局实战</a></li>
        </ul>
    </aside>
</main>

<footer>
    <p>&copy; 2023 我的个人博客. All rights reserved.</p>
</footer>

常用语义化标签及其作用:

1.3 表单元素

表单是用户与网页交互的重要方式,正确的表单设计和验证至关重要。

1.3.1 输入类型(input type

HTML5引入了多种新的input类型,提供了更好的用户体验和数据验证。

<form>
    <label for="username">用户名:</label>
    <input type="text" id="username" name="username" placeholder="请输入用户名" required><br><br>

    <label for="email">邮箱:</label>
    <input type="email" id="email" name="email" placeholder="[email protected]" required><br><br>

    <label for="password">密码:</label>
    <input type="password" id="password" name="password" minlength="6" placeholder="至少6位"><br><br>

    <label for="age">年龄:</label>
    <input type="number" id="age" name="age" min="18" max="100"><br><br>

    <label for="dob">出生日期:</label>
    <input type="date" id="dob" name="dob"><br><br>

    <label for="color">选择颜色:</label>
    <input type="color" id="color" name="color" value="#61dafb"><br><br>

    <label for="search">搜索:</label>
    <input type="search" id="search" name="q"><br><br>

    <label for="phone">电话:</label>
    <input type="tel" id="phone" name="phone" pattern="[0-9]{3}-[0-9]{3}-[0-9]{4}" placeholder="格式: 123-456-7890"><br><br>

    <label for="website">网址:</label>
    <input type="url" id="website" name="website" placeholder="https://example.com"><br><br>

    <input type="submit" value="提交">
</form>

通过设置`type`属性,浏览器会提供相应的键盘(如数字键盘)、输入验证和用户界面。

1.3.2 表单验证(required, pattern

HTML5提供了内置的客户端表单验证,无需JavaScript即可进行基础检查。

<form>
    <label for="username_val">用户名 (必填):</label>
    <input type="text" id="username_val" name="username_val" required><br><br>

    <label for="password_val">密码 (6-12位字母数字):</label>
    <input type="password" id="password_val" name="password_val"
           pattern="[A-Za-z0-9]{6,12}"
           title="密码必须是6-12位字母或数字" required><br><br>

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

    <label for="custom_message">自定义验证消息:</label>
    <input type="text" id="custom_message" name="custom_message" required>
    <p>通过JavaScript可以定制验证失败的提示。</p>
    
    <br><br>

    <input type="submit" value="验证表单">
</form>

1.3.3 自定义单选/复选框

通过隐藏原生input元素并结合CSS伪元素,可以实现高度定制化的单选/复选框。

<style>
    /* 隐藏原生checkbox/radio */
    .custom-checkbox input[type="checkbox"],
    .custom-radio input[type="radio"] {
        position: absolute;
        opacity: 0;
        cursor: pointer;
        height: 0;
        width: 0;
    }

    /* 样式化标签 (容器) */
    .custom-checkbox label,
    .custom-radio label {
        display: inline-block;
        position: relative;
        padding-left: 28px; /* 留出空间给自定义框 */
        cursor: pointer;
        font-size: 16px;
        user-select: none;
        color: #d0d0d0;
    }

    /* 自定义框的伪元素 */
    .custom-checkbox label::before,
    .custom-radio label::before {
        content: '';
        position: absolute;
        left: 0;
        top: 2px;
        width: 18px;
        height: 18px;
        border: 2px solid #61dafb;
        background-color: #333;
        transition: all 0.2s ease;
    }

    /* Checkbox 方形 */
    .custom-checkbox label::before {
        border-radius: 4px;
    }

    /* Radio 圆形 */
    .custom-radio label::before {
        border-radius: 50%;
    }

    /* 选中时的样式 */
    .custom-checkbox input[type="checkbox"]:checked + label::before,
    .custom-radio input[type="radio"]:checked + label::before {
        background-color: #61dafb;
        border-color: #61dafb;
    }

    /* Checkbox 选中时的勾 */
    .custom-checkbox input[type="checkbox"]:checked + label::after {
        content: '';
        position: absolute;
        left: 6px;
        top: 6px;
        width: 6px;
        height: 10px;
        border: solid white;
        border-width: 0 2px 2px 0;
        transform: rotate(45deg);
    }

    /* Radio 选中时的圆点 */
    .custom-radio input[type="radio"]:checked + label::after {
        content: '';
        position: absolute;
        left: 5px;
        top: 5px;
        width: 10px;
        height: 10px;
        background-color: white;
        border-radius: 50%;
    }

    /* 焦点状态 */
    .custom-checkbox input[type="checkbox"]:focus + label::before,
    .custom-radio input[type="radio"]:focus + label::before {
        box-shadow: 0 0 0 3px rgba(97, 218, 251, 0.5);
    }
</style>

<div class="custom-checkbox">
    <input type="checkbox" id="option1" name="feature" value="A">
    <label for="option1">选项 A</label>
</div>
<div class="custom-checkbox">
    <input type="checkbox" id="option2" name="feature" value="B">
    <label for="option2">选项 B</label>
</div>

<div class="custom-radio">
    <input type="radio" id="gender_male" name="gender" value="male">
    <label for="gender_male">男</label>
</div>
<div class="custom-radio">
    <input type="radio" id="gender_female" name="gender" value="female">
    <label for="gender_female">女</label>
</div>

核心思路: 隐藏原生的<input>元素,然后通过关联的<label>元素的伪元素(::before::after)来模拟并美化复选框或单选框的外观。当原生input被选中时,利用CSS选择器:checked + label来改变伪元素的样式。

1.4 媒体元素

图片和视频是网页中重要的内容载体。恰当的使用和优化可以显著提升用户体验。

1.4.1 图片优化(srcset, loading="lazy"

为了适应不同设备和网络环境,以及提升加载性能,可以使用以下属性:

<!-- 响应式图片:根据设备像素比和视口宽度加载不同分辨率的图片 -->
<img srcset="
    image-400w.jpg 400w,
    image-800w.jpg 800w,
    image-1200w.jpg 1200w
"
sizes="
    (max-width: 600px) 100vw,  /* 在小屏幕上,图片宽度为视口宽度 */
    (max-width: 900px) 50vw,  /* 在中等屏幕上,图片宽度为视口宽度的一半 */
    33vw                     /* 在大屏幕上,图片宽度为视口宽度的1/3 */
"
src="image-800w.jpg"  <!-- 备用图片,当srcset/sizes不被支持时使用 -->
alt="示例响应式图片"
loading="lazy" <!-- 懒加载:图片进入视口时才加载 -->
>

<!-- Picture元素:更精细的控制,支持WebP等现代格式 -->
<picture>
    <!-- 浏览器会尝试加载第一个支持的source -->
    <source srcset="image.webp" type="image/webp">
    <source srcset="image.avif" type="image/avif">
    <!-- 如果都不支持,则回退到img标签 -->
    <img src="image.jpg" alt="示例图片" loading="lazy">
</picture>
1.4.2 视频/音频嵌入(controls, autoplay

使用HTML5的<video><audio>标签可以方便地嵌入媒体内容。

<!-- 视频嵌入 -->
<video width="640" height="360" controls preload="metadata" poster="video-poster.jpg">
    <source src="video.mp4" type="video/mp4">
    <source src="video.webm" type="video/webm">
    <!-- 您的浏览器不支持HTML5视频标签。 -->
    您的浏览器不支持视频播放。请<a href="video.mp4">下载此视频</a>。
</video>

<!-- 音频嵌入 -->
<audio controls preload="auto">
    <source src="audio.mp3" type="audio/mpeg">
    <source src="audio.ogg" type="audio/ogg">
    您的浏览器不支持音频播放。请<a href="audio.mp3">下载此音频</a>。
</audio>

1.5 元数据

<head>标签内的元数据对SEO、社交分享和浏览器行为至关重要。

1.5.1 SEO优化标签(meta description, canonical
<head>
    <!-- 页面描述:在搜索引擎结果中显示,吸引用户点击 -->
    <meta name="description" content="本教程详细讲解了前端开发的常见代码片段,包括HTML、CSS、JavaScript以及性能优化和最佳实践。">

    <!-- 关键词:现代SEO中权重较低,但仍可作为参考 -->
    <meta name="keywords" content="前端, HTML, CSS, JavaScript, 代码片段, 教程, 性能优化">

    <!-- Canonical标签:指定页面的规范URL,避免内容重复导致的SEO问题 -->
    <link rel="canonical" href="https://example.com/frontend-snippets-tutorial/">

    <!-- Robots元标签:控制搜索引擎抓取和索引行为 -->
    <meta name="robots" content="index, follow"> <!-- 允许索引和跟踪链接 (默认行为) -->
    <!-- <meta name="robots" content="noindex, nofollow"> --> <!-- 不允许索引和跟踪链接 -->
</head>
1.5.2 社交分享标签(Open Graph, Twitter Cards)

这些标签可以控制页面在社交媒体上分享时的显示效果,如标题、描述、图片等。

<head>
    <!-- Open Graph (Facebook, LinkedIn等) -->
    <meta property="og:title" content="前端开发常见代码片段(详细版本)">
    <meta property="og:description" content="一份全面详细的前端开发教程,涵盖HTML、CSS、JavaScript、性能优化与最佳实践。">
    <meta property="og:image" content="https://example.com/images/share-thumbnail.jpg">
    <meta property="og:url" content="https://example.com/frontend-snippets-tutorial/">
    <meta property="og:type" content="article">
    <meta property="og:site_name" content="我的技术博客">

    <!-- Twitter Cards -->
    <meta name="twitter:card" content="summary_large_image">
    <meta name="twitter:title" content="前端开发常见代码片段(详细版本)">
    <meta name="twitter:description" content="一份全面详细的前端开发教程,涵盖HTML、CSS、JavaScript、性能优化与最佳实践。">
    <meta name="twitter:image" content="https://example.com/images/share-thumbnail.jpg">
    <meta name="twitter:creator" content="@YourTwitterHandle">
</head>

2. CSS 布局与样式

CSS(Cascading Style Sheets)负责网页的视觉呈现和布局。掌握现代CSS布局技术是构建复杂界面的关键。

2.1 Flexbox 布局

Flexbox(弹性盒子)是CSS3引入的一种一维布局模型,适用于将项目排列在单行或单列的场景。

2.1.1 主轴/交叉轴对齐

Flexbox通过设置容器(父元素)和项目(子元素)的属性来控制布局。下图展示了Flexbox的基本概念:

graph TD A[弹性容器] --> B[弹性项目 1] A --> C[弹性项目 2] A --> D[弹性项目 3] A -->|flex-direction: row(默认)| MainAxis[主轴(水平)] A -->|flex-direction: column| MainAxisCol[主轴(垂直)] A --> CrossAxis[交叉轴(垂直于主轴)] subgraph 主轴对齐方式 justify-content E[justify-content: flex-start] --> E1[项目1] & E2[项目2] F[justify-content: flex-end] --> F1[项目1] & F2[项目2] G[justify-content: center] --> G1[项目1] & G2[项目2] H[justify-content: space-between] --> H1[项目1] --- H2[项目2] --- H3[项目3] I[justify-content: space-around] --> I1[项目1] --- I2[项目2] --- I3[项目3] J[justify-content: space-evenly] --> J1[项目1] --- J2[项目2] --- J3[项目3] end subgraph 交叉轴对齐方式align-items K[align-items: flex-start] --> K1[项目1] & K2[项目2] L[align-items: flex-end] --> L1[项目1] & L2[项目2] M[align-items: center] --> M1[项目1] & M2[项目2] N[align-items: stretch] --> N1[项目1] & N2[项目2] O[align-items: baseline] --> O1[项目1] & O2[项目2] end
/* Flex容器 */
.flex-container {
    display: flex;
    /* 主轴方向:行 (从左到右), 列 (从上到下) */
    flex-direction: row; /* 或 column, row-reverse, column-reverse */
    /* 换行方式 */
    flex-wrap: wrap; /* 或 nowrap, wrap-reverse */
    /* 简写:flex-flow: row wrap; */

    /* 主轴对齐 */
    justify-content: center; /* flex-start, flex-end, center, space-between, space-around, space-evenly */

    /* 交叉轴对齐 */
    align-items: center; /* flex-start, flex-end, center, stretch, baseline */

    /* 多行交叉轴对齐 (当flex-wrap为wrap时生效) */
    align-content: stretch; /* flex-start, flex-end, center, space-between, space-around, stretch */

    height: 300px; /* 示例高度,方便观察效果 */
    border: 1px solid #61dafb;
    gap: 10px; /* flex项目之间的间距 (CSS Gap Module) */
}

/* Flex项目 */
.flex-item {
    width: 80px;
    height: 80px;
    background-color: #a9dfd8;
    color: #333;
    display: flex;
    justify-content: center;
    align-items: center;
    font-size: 1.2em;
    border-radius: 4px;
}

/* 单个项目在交叉轴上的对齐 */
.flex-item.self-align {
    align-self: flex-end; /* 覆盖父容器的align-items */
}

/* 项目的放大/缩小能力和初始大小 */
.flex-item.grow {
    flex-grow: 1; /* 占据剩余空间 */
}
.flex-item.shrink {
    flex-shrink: 0; /* 不收缩 */
}
.flex-item.basis {
    flex-basis: 150px; /* 初始大小 */
    /* 简写:flex: 1 1 150px; (grow shrink basis) */
}

/* 项目的顺序 */
.flex-item.order-last {
    order: 99; /* 顺序值越大,越靠后 */
}

<h4>Flexbox 布局示例</h4>
<div class="flex-container" style="display: flex; justify-content: space-around; align-items: center; background-color: #363636;">
    <div class="flex-item">1</div>
    <div class="flex-item self-align">2 (底部)</div>
    <div class="flex-item grow">3 (grow)</div>
    <div class="flex-item order-last">4 (最后)</div>
</div>
2.1.2 响应式导航栏

使用Flexbox可以轻松实现响应式导航栏,在小屏幕上堆叠或变成汉堡菜单。

.navbar {
    display: flex;
    justify-content: space-between;
    align-items: center;
    background-color: #333;
    padding: 10px 20px;
    color: white;
}

.navbar-brand {
    font-size: 1.5em;
    font-weight: bold;
    color: white;
    text-decoration: none;
}

.navbar-nav {
    display: flex; /* 默认横向排列 */
    list-style: none;
    margin: 0;
    padding: 0;
}

.navbar-nav li a {
    color: white;
    text-decoration: none;
    padding: 8px 15px;
    display: block;
}

.navbar-nav li a:hover {
    background-color: #555;
    border-radius: 4px;
}

/* 汉堡菜单按钮 (仅在小屏幕显示) */
.menu-toggle {
    display: none;
    flex-direction: column;
    justify-content: space-between;
    width: 30px;
    height: 20px;
    cursor: pointer;
}

.menu-toggle span {
    display: block;
    width: 100%;
    height: 3px;
    background-color: white;
    border-radius: 2px;
}

/* 媒体查询:小屏幕(例如小于 768px) */
@media (max-width: 768px) {
    .navbar {
        flex-wrap: wrap; /* 允许换行 */
    }

    .navbar-nav {
        flex-direction: column; /* 垂直堆叠 */
        width: 100%; /* 占据整行 */
        display: none; /* 默认隐藏 */
        text-align: center;
    }

    .navbar-nav.active {
        display: flex; /* 点击按钮时显示 */
    }

    .menu-toggle {
        display: flex; /* 显示汉堡菜单按钮 */
    }
}

<nav class="navbar">
    <a href="#" class="navbar-brand">我的网站</a>
    <div class="menu-toggle" id="mobile-menu">
        <span></span>
        <span></span>
        <span></span>
    </div>
    <ul class="navbar-nav" id="main-nav">
        <li><a href="#">首页</a></li>
        <li><a href="#">服务</a></li>
        <li><a href="#">关于</a></li>
        <li><a href="#">联系</a></li>
    </ul>
</nav>

<script>
    document.getElementById('mobile-menu').addEventListener('click', function() {
        document.getElementById('main-nav').classList.toggle('active');
    });
</script>
2.1.3 圣杯布局 (Flexbox 实现)

圣杯布局(Holy Grail Layout)是一种经典的三列布局,中间内容区宽度可变,左右侧边栏固定宽度。

.holy-grail-layout {
    display: flex;
    min-height: 100vh; /* 确保内容撑满视口 */
    flex-direction: column; /* 整体垂直排列 header, main, footer */
}

.holy-grail-header,
.holy-grail-footer {
    background-color: #444;
    padding: 20px;
    text-align: center;
    color: white;
}

.holy-grail-main {
    display: flex; /* main内部左右横向排列 */
    flex: 1; /* 占据剩余垂直空间 */
}

.holy-grail-left-sidebar,
.holy-grail-right-sidebar {
    width: 200px; /* 固定宽度 */
    background-color: #555;
    padding: 15px;
    color: white;
    flex-shrink: 0; /* 防止收缩 */
}

.holy-grail-content {
    flex: 1; /* 占据剩余水平空间 */
    background-color: #2b2b2b;
    padding: 20px;
    color: #d0d0d0;
}

/* 可选:小屏幕下侧边栏堆叠 */
@media (max-width: 768px) {
    .holy-grail-main {
        flex-direction: column; /* 垂直堆叠 */
    }
    .holy-grail-left-sidebar,
    .holy-grail-right-sidebar {
        width: auto; /* 自动宽度 */
    }
}

<div class="holy-grail-layout">
    <header class="holy-grail-header">顶部导航</header>

    <div class="holy-grail-main">
        <aside class="holy-grail-left-sidebar">左侧边栏</aside>
        <main class="holy-grail-content">
            <h3>主要内容区域</h3>
            <p>这里是页面的核心内容。在Flexbox布局中,主内容区域会根据可用空间自动伸缩。</p>
            <p>左右侧边栏可以保持固定宽度,从而实现经典的三列布局。</p>
        </main>
        <aside class="holy-grail-right-sidebar">右侧边栏</aside>
    </div>

    <footer class="holy-grail-footer">底部信息</footer>
</div>

2.2 Grid 布局

CSS Grid Layout(网格布局)是CSS3引入的二维布局模型,可以同时控制行和列,适用于构建复杂的页面网格。

2.2.1 基础网格定义

通过display: grid将容器变为网格容器,并通过grid-template-columnsgrid-template-rows定义网格结构。

.grid-container {
    display: grid;
    /* 定义三列:第一列100px,第二列占据2份可用空间,第三列占据1份可用空间 */
    grid-template-columns: 100px 2fr 1fr;
    /* 定义两行:第一行auto (根据内容高度), 第二行200px */
    grid-template-rows: auto 200px;
    /* 定义行间距和列间距 */
    gap: 10px 20px; /* 或 row-gap: 10px; column-gap: 20px; */

    /* 简写方式: */
    /* grid-template: auto 200px / 100px 2fr 1fr; (rows / columns) */

    height: 300px;
    border: 1px solid #a9dfd8;
    background-color: #363636;
}

.grid-item {
    background-color: #61dafb;
    color: #333;
    display: flex;
    justify-content: center;
    align-items: center;
    font-size: 1.2em;
    border-radius: 4px;
}

<h4>Grid 基础网格示例</h4>
<div class="grid-container">
    <div class="grid-item">A</div>
    <div class="grid-item">B</div>
    <div class="grid-item">C</div>
    <div class="grid-item">D</div>
    <div class="grid-item">E</div>
    <div class="grid-item">F</div>
</div>
2.2.2 命名网格区域

可以使用grid-template-areas来命名网格区域,然后将项目放置到这些区域中,使布局更直观。

.grid-area-layout {
    display: grid;
    grid-template-columns: 1fr 3fr 1fr; /* 三列 */
    grid-template-rows: auto 1fr auto; /* 上中下三行 */
    grid-template-areas:
        "header header header"
        "sidebar-left content sidebar-right"
        "footer footer footer";
    gap: 10px;
    min-height: 100vh;
    border: 1px solid #f8c291;
    background-color: #363636;
}

.header {
    grid-area: header;
    background-color: #444;
    padding: 20px;
    text-align: center;
    color: white;
}

.sidebar-left {
    grid-area: sidebar-left;
    background-color: #555;
    padding: 15px;
    color: white;
}

.content {
    grid-area: content;
    background-color: #2b2b2b;
    padding: 20px;
    color: #d0d0d0;
}

.sidebar-right {
    grid-area: sidebar-right;
    background-color: #555;
    padding: 15px;
    color: white;
}

.footer {
    grid-area: footer;
    background-color: #444;
    padding: 20px;
    text-align: center;
    color: white;
}

<h4>Grid 命名区域布局示例</h4>
<div class="grid-area-layout">
    <div class="header">Header</div>
    <div class="sidebar-left">Sidebar Left</div>
    <div class="content">Main Content</div>
    <div class="sidebar-right">Sidebar Right</div>
    <div class="footer">Footer</div>
</div>
2.2.3 响应式网格

结合媒体查询和repeat()minmax()等函数,可以创建灵活的响应式网格。

.responsive-grid {
    display: grid;
    /* 默认在小屏幕上显示一列 */
    grid-template-columns: 1fr;
    gap: 15px;
    padding: 20px;
    background-color: #363636;
}

.responsive-grid-item {
    background-color: #a9dfd8;
    padding: 20px;
    text-align: center;
    color: #333;
    border-radius: 5px;
}

/* 中等屏幕:两列,每列最小250px,最大1fr */
@media (min-width: 600px) {
    .responsive-grid {
        grid-template-columns: repeat(2, minmax(250px, 1fr));
    }
}

/* 大屏幕:三列,每列最小300px,最大1fr */
@media (min-width: 992px) {
    .responsive-grid {
        grid-template-columns: repeat(3, minmax(300px, 1fr));
    }
}

/* 自动填充列,适应内容并保持最小宽度 */
.auto-fill-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
    gap: 20px;
    padding: 20px;
    background-color: #363636;
    margin-top: 20px;
}
.auto-fill-grid-item {
    background-color: #f8c291;
    padding: 20px;
    text-align: center;
    color: #333;
    border-radius: 5px;
}

<h4>响应式网格示例</h4>
<div class="responsive-grid">
    <div class="responsive-grid-item">Item 1</div>
    <div class="responsive-grid-item">Item 2</div>
    <div class="responsive-grid-item">Item 3</div>
    <div class="responsive-grid-item">Item 4</div>
    <div class="responsive-grid-item">Item 5</div>
    <div class="responsive-grid-item">Item 6</div>
</div>

<h4>自动填充网格示例 (auto-fit)</h4>
<div class="auto-fill-grid">
    <div class="auto-fill-grid-item">A</div>
    <div class="auto-fill-grid-item">B</div>
    <div class="auto-fill-grid-item">C</div>
    <div class="auto-fill-grid-item">D</div>
    <div class="auto-fill-grid-item">E</div>
    <div class="auto-fill-grid-item">F</div>
</div>

2.3 定位

CSS定位(Positioning)是控制元素在页面上精确位置的关键。

2.3.1 sticky 导航实现

position: sticky可以实现当元素滚动到特定位置时固定在屏幕上的效果,常用于导航栏或侧边栏。

.sticky-header {
    background-color: #333;
    color: white;
    padding: 15px 0;
    text-align: center;
    position: sticky; /* 关键属性 */
    top: 0; /* 距离视口顶部0px时固定 */
    z-index: 1000; /* 确保在其他内容之上 */
    box-shadow: 0 2px 5px rgba(0,0,0,0.3);
}

.content-placeholder {
    height: 800px; /* 制造足够的滚动空间 */
    background-color: #444;
    display: flex;
    justify-content: center;
    align-items: center;
    color: white;
    font-size: 2em;
    margin-top: 20px;
}
.more-content {
    height: 600px;
    background-color: #555;
    display: flex;
    justify-content: center;
    align-items: center;
    color: white;
    font-size: 2em;
    margin-top: 20px;
}

<h4>Sticky 导航示例</h4>
<div class="sticky-header">
    这是一个 Sticky 导航栏
</div>
<div class="content-placeholder">
    滚动页面查看 Sticky 效果
</div>
<div class="more-content">
    更多内容...
</div>

注意: position: sticky 在父元素设置了overflow: hidden, scrollauto 时可能会失效,因为它依赖于滚动容器。

2.3.2 模态框定位

模态框(Modal)通常需要固定在屏幕中心,并覆盖其他内容。

/* 模态框背景遮罩 */
.modal-overlay {
    position: fixed; /* 固定定位 */
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    background-color: rgba(0, 0, 0, 0.7); /* 半透明黑色背景 */
    display: flex; /* 使用Flexbox居中模态框 */
    justify-content: center;
    align-items: center;
    z-index: 1001; /* 确保在最顶层 */
    visibility: hidden; /* 默认隐藏 */
    opacity: 0;
    transition: visibility 0.3s ease-out, opacity 0.3s ease-out;
}

.modal-overlay.active {
    visibility: visible;
    opacity: 1;
}

/* 模态框内容 */
.modal-content {
    background-color: #2b2b2b;
    padding: 30px;
    border-radius: 8px;
    box-shadow: 0 5px 15px rgba(0, 0, 0, 0.5);
    width: 80%;
    max-width: 500px;
    text-align: center;
    transform: translateY(-20px); /* 初始位置略微上移 */
    transition: transform 0.3s ease-out;
}

.modal-overlay.active .modal-content {
    transform: translateY(0); /* 激活时回到正常位置 */
}

.modal-content h3 {
    color: #61dafb;
    margin-top: 0;
}

.modal-content button {
    background-color: #61dafb;
    color: white;
    border: none;
    padding: 10px 20px;
    border-radius: 5px;
    cursor: pointer;
    font-size: 1em;
    margin-top: 20px;
}

.modal-content button:hover {
    background-color: #00aaff;
}

<h4>模态框定位示例</h4>
<button id="openModalBtn" style="padding: 10px 20px; background-color: #61dafb; color: white; border: none; border-radius: 5px; cursor: pointer;">
    打开模态框
</button>

<div class="modal-overlay" id="myModal">
    <div class="modal-content">
        <h3>这是一个模态框标题</h3>
        <p>模态框通常用于显示重要信息、确认操作或收集用户输入。</p>
        <p>它使用 <span class="highlight">position: fixed</span> 结合 <span class="highlight">Flexbox</span> 进行居中定位。</p>
        <button id="closeModalBtn">关闭</button>
    </div>
</div>

核心思路:

  1. 使用position: fixed使模态框脱离文档流,并固定在视口中。
  2. 利用Flexbox(display: flex; justify-content: center; align-items: center;)来将模态框内容水平和垂直居中。
  3. 通过设置z-index确保模态框在所有其他内容之上。
  4. 结合CSS的opacityvisibility属性以及transition实现平滑的显示/隐藏动画。

2.4 响应式设计

响应式设计确保网站在不同设备(桌面、平板、手机)上都能提供良好的用户体验。

2.4.1 媒体查询断点

媒体查询(Media Queries)是响应式设计的基石,允许我们根据设备的特性应用不同的样式。

/* 默认样式:适用于所有屏幕,通常是移动优先的起点 */
body {
    /* 默认字体大小 */
}

.responsive-box {
    width: 90%;
    margin: 20px auto;
    padding: 20px;
    background-color: #363636;
    border: 2px solid #61dafb;
    text-align: center;
}

/* 小屏幕 (Phones): 宽度小于 600px */
@media (max-width: 599px) {
    body {
        font-size: 14px;
    }
    .responsive-box {
        border-color: #f8c291;
    }
}

/* 中等屏幕 (Tablets): 宽度在 600px 到 991px 之间 */
@media (min-width: 600px) and (max-width: 991px) {
    body {
        font-size: 18px;
    }
    .responsive-box {
        width: 70%;
        border-color: #a9dfd8;
    }
}

/* 大屏幕 (Desktops): 宽度大于等于 992px */
@media (min-width: 992px) {
    body {
        font-size: 20px;
    }
    .responsive-box {
        width: 50%;
        border-color: #61dafb;
    }
}

/* 打印样式 */
@media print {
    body {
        background-color: white;
        color: black;
        font-size: 12pt;
    }
    .responsive-box {
        border: 1px solid black;
        box-shadow: none;
    }
    /* 隐藏不必要的元素 */
    nav, footer, .button {
        display: none;
    }
}

<h4>媒体查询断点示例</h4>
<div class="responsive-box">
    <p>请尝试调整浏览器窗口大小,观察此盒子的边框颜色和文本大小的变化。</p>
    <p>当前屏幕宽度: <span id="screenWidth"></span>px</p>
</div>
<script>
    function updateScreenWidth() {
        document.getElementById('screenWidth').textContent = window.innerWidth;
    }
    window.addEventListener('resize', updateScreenWidth);
    updateScreenWidth(); // Initial call
</script>

常用断点:

2.4.2 移动优先策略

移动优先(Mobile First)是一种设计和开发方法,首先为移动设备设计和编写样式,然后逐步扩展到更大的屏幕。

graph TD A[从移动端最小尺寸开始设计和开发] --> B[编写基础CSS样式 (适用于小屏幕)] B --> C[使用 @media min-width 断点逐步添加大屏幕样式] C --> D[迭代优化不同屏幕下的用户体验]
/* 移动优先示例 */

/* 首先定义适用于移动设备的样式 (不使用媒体查询) */
.card-container {
    display: flex;
    flex-direction: column; /* 移动端垂直堆叠 */
    padding: 10px;
    gap: 15px;
}

.card {
    background-color: #363636;
    padding: 15px;
    border-radius: 8px;
    box-shadow: 0 2px 5px rgba(0,0,0,0.2);
    color: #e0e0e0;
}

/* 当屏幕宽度大于等于 768px 时,变为两列布局 */
@media (min-width: 768px) {
    .card-container {
        flex-direction: row; /* 平板/桌面横向排列 */
        flex-wrap: wrap; /* 允许换行 */
        justify-content: space-around; /* 卡片之间留白 */
        gap: 20px;
    }
    .card {
        flex: 1 1 calc(50% - 20px); /* 每行两列,考虑gap */
        max-width: calc(50% - 20px);
    }
}

/* 当屏幕宽度大于等于 1200px 时,变为三列布局 */
@media (min-width: 1200px) {
    .card-container {
        justify-content: flex-start; /* 左对齐 */
    }
    .card {
        flex: 1 1 calc(33.33% - 20px); /* 每行三列,考虑gap */
        max-width: calc(33.33% - 20px);
    }
}

<h4>移动优先布局示例</h4>
<div class="card-container">
    <div class="card">
        <h3>卡片标题 1</h3>
        <p>这是卡片的内容。在移动优先策略下,我们首先确保它在小屏幕上表现良好。</p>
    </div>
    <div class="card">
        <h3>卡片标题 2</h3>
        <p>随着屏幕尺寸的增大,通过媒体查询逐步添加更复杂的布局和样式。</p>
    </div>
    <div class="card">
        <h3>卡片标题 3</h3>
        <p>这种方法有助于提升移动设备的性能,并简化CSS的组织结构。</p>
    </div>
    <div class="card">
        <h3>卡片标题 4</h3>
        <p>您可以在浏览器中调整窗口大小,观察卡片布局的变化。</p>
    </div>
</div>

优势:

2.4.3 视口单位(vw, vh

视口单位(viewport units)vw (viewport width) 和 vh (viewport height) 允许我们根据视口的大小来定义元素的尺寸,实现更灵活的响应式布局。

.full-screen-section {
    height: 100vh; /* 占据整个视口高度 */
    width: 100vw; /* 占据整个视口宽度 */
    background-color: #2a2a2a;
    display: flex;
    justify-content: center;
    align-items: center;
    color: white;
    font-size: 4vw; /* 文本大小随视口宽度变化 */
    overflow: hidden; /* 防止滚动条 */
}

.responsive-text {
    font-size: 2vw; /* 文本大小随视口宽度变化 */
    margin: 10px 0;
    padding: 0 5vw; /* 左右内边距随视口宽度变化 */
}

.min-max-font {
    /* 结合calc和clamp,实现字体大小自适应,并限制最小和最大值 */
    font-size: clamp(16px, 2.5vw, 24px); /* 最小16px,最大24px,中间2.5vw */
    color: #f8c291;
    margin-top: 20px;
}

<h4>视口单位示例</h4>
<div class="full-screen-section">
    <span>这个区块占据整个视口,文本大小随视口变化。</span>
</div>
<div style="background-color: #363636; padding: 20px; margin-top: 20px;">
    <p class="responsive-text">
        这段文本的字体大小和左右边距会随着浏览器窗口的宽度而动态调整,利用了 <span class="highlight">vw</span> 单位。
    </p>
    <p class="min-max-font">
        结合 <span class="highlight">clamp()</span> 函数可以更好地控制视口单位,避免字体过大或过小。
    </p>
</div>

视口单位:

数学公式示例: 假设视口宽度为 $W_{vp}$,则 $1vw = 0.01 \times W_{vp}$。

其中 $K$ 是一个常数。这意味着元素的尺寸会与视口成比例缩放。


第二部分:JavaScript 核心功能

JavaScript 是前端的灵魂,负责页面的交互、动态内容和复杂逻辑。

3. DOM 操作

DOM (Document Object Model) 是HTML和XML文档的编程接口。它允许JavaScript访问和操作网页的结构、样式和内容。

3.1 元素选择

选择HTML元素是DOM操作的第一步。现代浏览器提供了多种强大的选择器方法。

// 1. 通过ID选择单个元素 (最快)
const myElementById = document.getElementById('myId');
if (myElementById) {
    console.log('通过ID选择:', myElementById.textContent);
}

// 2. 通过类名选择所有元素 (返回HTMLCollection)
const elementsByClass = document.getElementsByClassName('myClass');
if (elementsByClass.length > 0) {
    console.log('通过类名选择 (第一个):', elementsByClass[0].textContent);
}

// 3. 通过标签名选择所有元素 (返回HTMLCollection)
const paragraphs = document.getElementsByTagName('p');
if (paragraphs.length > 0) {
    console.log('通过标签名选择 (第一个p):', paragraphs[0].textContent);
}

// 4. querySelector: 根据CSS选择器选择第一个匹配的元素 (推荐用于单个选择)
const firstDiv = document.querySelector('div');
const specificElement = document.querySelector('#container .item:first-child');
if (specificElement) {
    console.log('querySelector:', specificElement.textContent);
}

// 5. querySelectorAll: 根据CSS选择器选择所有匹配的元素 (返回NodeList,可forEach)
const allItems = document.querySelectorAll('.list-item');
allItems.forEach(item => {
    console.log('querySelectorAll:', item.textContent);
});

// 6. closest(): 查找当前元素或其祖先中匹配CSS选择器的最近元素
const nestedButton = document.getElementById('nestedButton');
const parentSection = nestedButton ? nestedButton.closest('section') : null;
if (parentSection) {
    console.log('closest section id:', parentSection.id);
}

// 7. matches(): 检查元素是否匹配给定的CSS选择器
const isMyClass = myElementById ? myElementById.matches('.some-class') : false;
console.log('myElementById matches .some-class:', isMyClass);

<h4>DOM 元素选择示例</h4>
<div id="myId" class="myClass">这是一个ID为 myId 的元素。</div>
<p>这是一个普通的段落。</p>
<div id="container">
    <div class="item list-item">列表项 1</div>
    <div class="item list-item">列表项 2</div>
</div>
<section id="parentSection">
    <div>
        <button id="nestedButton">嵌套按钮</button>
    </div>
</section>

总结:

3.1.2 动态元素监听(MutationObserver)

当需要在DOM结构或内容发生变化时执行特定操作时,MutationObserver是一个强大的工具,比旧的Mutation Events更高效、更灵活。

// 目标元素
const targetNode = document.getElementById('dynamicContent');

// 观察器选项
const config = {
    attributes: true,       // 观察属性变化
    childList: true,        // 观察子节点添加/删除
    subtree: true,          // 观察目标元素的后代节点
    characterData: true,    // 观察文本内容变化
    attributeFilter: ['class', 'data-status'] // 只观察特定属性
};

// 回调函数,当观察到变化时执行
const callback = function(mutationsList, observer) {
    for (const mutation of mutationsList) {
        if (mutation.type === 'childList') {
            console.log('子节点被添加或移除。');
            console.log('添加的节点:', mutation.addedNodes);
            console.log('移除的节点:', mutation.removedNodes);
        } else if (mutation.type === 'attributes') {
            console.log(`属性 '${mutation.attributeName}' 被修改。旧值: '${mutation.oldValue}'`);
            console.log('新值:', mutation.target.getAttribute(mutation.attributeName));
        } else if (mutation.type === 'characterData') {
            console.log('文本内容被修改。');
            console.log('旧值:', mutation.oldValue);
            console.log('新值:', mutation.target.textContent);
        }
    }
};

// 创建一个观察器实例
const observer = new MutationObserver(callback);

// 开始观察目标节点
if (targetNode) {
    observer.observe(targetNode, config);
    console.log('MutationObserver 已启动.');

    // 模拟DOM变化
    setTimeout(() => {
        const newDiv = document.createElement('div');
        newDiv.textContent = '新添加的元素';
        newDiv.className = 'new-item';
        targetNode.appendChild(newDiv);
        console.log('--- 1秒后:添加一个新元素 ---');
    }, 1000);

    setTimeout(() => {
        if (targetNode.firstElementChild) {
            targetNode.firstElementChild.setAttribute('data-status', 'updated');
            console.log('--- 2秒后:修改第一个子元素的属性 ---');
        }
    }, 2000);

    setTimeout(() => {
        if (targetNode.lastChild && targetNode.lastChild.nodeType === Node.TEXT_NODE) {
            targetNode.lastChild.textContent = '更新后的文本内容。';
            console.log('--- 3秒后:修改文本内容 ---');
        }
    }, 3000);

    setTimeout(() => {
        // 停止观察
        observer.disconnect();
        console.log('--- 4秒后:MutationObserver 已停止 ---');
        const anotherDiv = document.createElement('div');
        anotherDiv.textContent = '这个元素不会被观察到 (观察器已断开)';
        targetNode.appendChild(anotherDiv);
    }, 4000);
}

<h4>MutationObserver 示例</h4>
<div id="dynamicContent" style="border: 1px solid #a9dfd8; padding: 15px; min-height: 80px; background-color: #363636; color: #d0d0d0;">
    <p>这是一个初始元素。</p>
    <!-- 模拟文本节点 -->文本内容。
</div>
<p style="margin-top: 10px; font-size: 0.9em; color: #bbb;">请打开浏览器控制台,观察MutationObserver的输出。</p>

使用场景:

3.2 事件系统

事件系统是JavaScript实现交互的核心。掌握事件的监听、传播和委托至关重要。

3.2.1 自定义事件

除了浏览器内置事件(如click, submit, load),我们也可以创建和触发自定义事件,实现组件间的解耦通信。

// 1. 创建并派发一个简单自定义事件
const myButton = document.getElementById('myCustomEventBtn');
if (myButton) {
    myButton.addEventListener('click', () => {
        console.log('按钮被点击,即将派发自定义事件...');
        const event = new Event('myCustomEvent', { bubbles: true, cancelable: true });
        myButton.dispatchEvent(event);
    });

    // 监听自定义事件
    myButton.addEventListener('myCustomEvent', () => {
        console.log('捕获到 myCustomEvent 事件!');
    });
}

// 2. 创建并派发带有数据的自定义事件 (CustomEvent)
const dataButton = document.getElementById('dataCustomEventBtn');
const eventReceiver = document.getElementById('eventReceiver');

if (dataButton && eventReceiver) {
    dataButton.addEventListener('click', () => {
        const detailData = {
            message: 'Hello from custom event!',
            timestamp: new Date().toISOString()
        };
        const customEventWithData = new CustomEvent('dataLoaded', {
            detail: detailData,
            bubbles: true,
            cancelable: true
        });
        dataButton.dispatchEvent(customEventWithData);
    });

    // 监听带有数据的自定义事件
    eventReceiver.addEventListener('dataLoaded', (event) => {
        console.log('捕获到 dataLoaded 事件,数据:', event.detail);
        eventReceiver.textContent = `接收到数据: ${event.detail.message} (${event.detail.timestamp})`;
    });
}

// 3. 在非DOM元素上使用事件 (EventTarget)
// 可以创建一个独立的EventTarget实例,用于非DOM对象的事件通信
const eventBus = new EventTarget();

eventBus.addEventListener('globalMessage', (e) => {
    console.log('EventBus 收到全局消息:', e.detail.text);
    document.getElementById('globalMessageLog').textContent += `收到: ${e.detail.text}\n`;
});

// 模拟发送消息
setTimeout(() => {
    eventBus.dispatchEvent(new CustomEvent('globalMessage', { detail: { text: '系统启动完成' } }));
}, 1000);

setTimeout(() => {
    eventBus.dispatchEvent(new CustomEvent('globalMessage', { detail: { text: '用户登录成功' } }));
}, 2000);

<h4>自定义事件示例</h4>
<p>
    <button id="myCustomEventBtn">点击触发简单自定义事件</button>
</p>
<p>
    <button id="dataCustomEventBtn">点击触发带数据的自定义事件</button>
</p>
<div id="eventReceiver" style="border: 1px dashed #f8c291; padding: 10px; margin-top: 10px; min-height: 40px; background-color: #363636; color: #d0d0d0;">
    等待接收数据事件...
</div>

<h4>EventTarget 作为事件总线示例</h4>
<div style="border: 1px dashed #a9dfd8; padding: 10px; margin-top: 10px; min-height: 80px; background-color: #363636; color: #d0d0d0;">
    <pre id="globalMessageLog" style="white-space: pre-wrap; font-size: 0.9em; margin: 0; padding: 0;"></pre>
</div>
<p style="font-size: 0.9em; color: #bbb;">打开控制台观察 EventBus 的消息。</p>

用途:

3.2.2 事件委托优化

事件委托(Event Delegation)是一种通过监听父元素来管理子元素事件的技术。它减少了事件监听器的数量,提高了性能,特别是在处理大量动态生成的子元素时。

// 获取父容器
const listContainer = document.getElementById('myList');

if (listContainer) {
    // 为父容器添加一个事件监听器
    listContainer.addEventListener('click', function(event) {
        // 检查点击事件是否来源于具有特定类名 (如 .list-item) 的子元素
        if (event.target.classList.contains('list-item')) {
            const clickedItem = event.target;
            const itemText = clickedItem.textContent;
            console.log(`点击了列表项: ${itemText}`);
            clickedItem.style.backgroundColor = '#61dafb'; // 改变点击项的背景色
            clickedItem.style.color = '#333';
            setTimeout(() => {
                clickedItem.style.backgroundColor = ''; // 恢复
                clickedItem.style.color = '';
            }, 500);
        } else if (event.target.classList.contains('delete-btn')) {
            // 如果点击的是删除按钮
            const deleteBtn = event.target;
            const listItemToRemove = deleteBtn.closest('.list-item');
            if (listItemToRemove) {
                console.log(`删除了列表项: ${listItemToRemove.textContent.replace(' 删除', '')}`);
                listItemToRemove.remove(); // 从DOM中移除该项
            }
        }
    });

    // 动态添加更多列表项
    document.getElementById('addListItemBtn').addEventListener('click', () => {
        const newItem = document.createElement('div');
        newItem.classList.add('list-item');
        newItem.textContent = `动态添加项 ${listContainer.children.length + 1}`;
        newItem.innerHTML += ' <button class="delete-btn">删除</button>'; // 添加删除按钮
        listContainer.appendChild(newItem);
    });
}

<h4>事件委托优化示例</h4>
<div id="myList" style="border: 1px solid #f8c291; padding: 15px; background-color: #363636; min-height: 120px;">
    <div class="list-item">列表项 A <button class="delete-btn">删除</button></div>
    <div class="list-item">列表项 B <button class="delete-btn">删除</button></div>
    <div class="list-item">列表项 C <button class="delete-btn">删除</button></div>
</div>
<button id="addListItemBtn" style="margin-top: 15px; padding: 8px 15px; background-color: #a9dfd8; color: #333; border: none; border-radius: 4px; cursor: pointer;">
    添加新列表项
</button>
<p style="margin-top: 10px; font-size: 0.9em; color: #bbb;">无论点击哪个列表项或删除按钮,都只有一个事件监听器在父元素上运行。</p>

<style>
    .list-item {
        background-color: #4a4a4a;
        padding: 8px 12px;
        margin-bottom: 5px;
        border-radius: 4px;
        cursor: pointer;
        display: flex;
        justify-content: space-between;
        align-items: center;
        transition: background-color 0.2s ease;
    }
    .list-item .delete-btn {
        background-color: #e74c3c;
        color: white;
        border: none;
        padding: 5px 10px;
        border-radius: 3px;
        cursor: pointer;
        font-size: 0.8em;
    }
    .list-item .delete-btn:hover {
        background-color: #c0392b;
    }
</style>

优势:

3.3 动画实现

JavaScript可以实现更复杂、更精细的动画效果。

3.3.1 requestAnimationFrame

requestAnimationFrame是浏览器提供的优化动画的API。它告诉浏览器你希望执行一个动画,并请求浏览器在下一次重绘之前调用指定的回调函数。

优势:

const animBox = document.getElementById('animBox');
let position = 0;
let direction = 1; // 1 for right, -1 for left
const speed = 2; // px per frame
const maxX = 200; // 最大移动距离
let animationId = null; // 用于存储动画ID

function animate() {
    position += speed * direction;

    if (position >= maxX) {
        position = maxX;
        direction = -1; // 改变方向
    } else if (position <= 0) {
        position = 0;
        direction = 1; // 改变方向
    }

    if (animBox) {
        animBox.style.transform = `translateX(${position}px)`;
    }

    // 继续下一帧动画
    animationId = requestAnimationFrame(animate);
}

if (animBox) {
    document.getElementById('startAnimBtn').addEventListener('click', () => {
        if (!animationId) {
            animate();
            console.log('动画已启动');
        } else {
            console.log('动画已在运行');
        }
    });

    document.getElementById('stopAnimBtn').addEventListener('click', () => {
        if (animationId) {
            cancelAnimationFrame(animationId);
            animationId = null;
            console.log('动画已停止');
        }
    });
}

<h4>requestAnimationFrame 动画示例</h4>
<div style="width: 300px; height: 50px; border: 1px solid #61dafb; overflow: hidden; position: relative; background-color: #363636; margin-bottom: 15px;">
    <div id="animBox" style="width: 50px; height: 50px; background-color: #a9dfd8; position: absolute; left: 0; top: 0; display: flex; justify-content: center; align-items: center; color: #333; border-radius: 50%;">
        动
    </div>
</div>
<button id="startAnimBtn" style="padding: 8px 15px; background-color: #61dafb; color: white; border: none; border-radius: 4px; cursor: pointer; margin-right: 10px;">
    启动动画
</button>
<button id="stopAnimBtn" style="padding: 8px 15px; background-color: #e74c3c; color: white; border: none; border-radius: 4px; cursor: pointer;">
    停止动画
</button>
<p style="margin-top: 10px; font-size: 0.9em; color: #bbb;">此版本已修复停止功能,可以使用 <span class="highlight">cancelAnimationFrame</span> 正确停止动画。</p>
3.3.2 Web Animations API

Web Animations API (WAAPI) 提供了直接从JavaScript控制CSS动画的能力,结合了CSS动画的性能优势和JavaScript的灵活性。

const waapiBox = document.getElementById('waapiBox');
let animation = null; // 用于存储动画实例

if (waapiBox) {
    document.getElementById('playWAAPIBtn').addEventListener('click', () => {
        if (animation && animation.playState === 'running') {
            console.log('动画已在运行');
            return;
        }
        
        // 如果动画已暂停,则继续播放
        if (animation && animation.playState === 'paused') {
            animation.play();
            console.log('WAAPI 动画已恢复播放');
            return;
        }

        // 定义关键帧
        const keyframes = [
            { transform: 'translateX(0)', opacity: 1, backgroundColor: '#a9dfd8' },
            { transform: 'translateX(200px)', opacity: 0.5, backgroundColor: '#f8c291' },
            { transform: 'translateX(0)', opacity: 1, backgroundColor: '#a9dfd8' }
        ];

        // 定义动画选项
        const options = {
            duration: 2000,          // 动画持续时间(毫秒)
            iterations: Infinity,    // 循环次数,Infinity表示无限循环
            easing: 'ease-in-out',   // 缓动函数
            direction: 'alternate',  // 交替播放
            fill: 'forwards'         // 动画结束后保持最终状态
        };

        // 播放动画
        animation = waapiBox.animate(keyframes, options);
        console.log('WAAPI 动画已启动');

        // 可以监听动画事件
        animation.onfinish = () => {
            console.log('WAAPI 动画播放完成 (如果不是无限循环)');
        };
        animation.oncancel = () => {
            console.log('WAAPI 动画被取消');
        };
    });

    document.getElementById('pauseWAAPIBtn').addEventListener('click', () => {
        if (animation) {
            animation.pause();
            console.log('WAAPI 动画已暂停');
        }
    });

    document.getElementById('reverseWAAPIBtn').addEventListener('click', () => {
        if (animation) {
            animation.reverse();
            console.log('WAAPI 动画已反转');
        }
    });

    document.getElementById('cancelWAAPIBtn').addEventListener('click', () => {
        if (animation) {
            animation.cancel();
            console.log('WAAPI 动画已取消');
            animation = null; // 清除引用
            // 动画取消后会回到初始状态
            waapiBox.style.transform = '';
            waapiBox.style.backgroundColor = '';
            waapiBox.style.opacity = '';
        }
    });
}

<h4>Web Animations API 动画示例</h4>
<div style="width: 300px; height: 50px; border: 1px solid #a9dfd8; overflow: hidden; position: relative; background-color: #363636; margin-bottom: 15px;">
    <div id="waapiBox" style="width: 50px; height: 50px; background-color: #a9dfd8; position: absolute; left: 0; top: 0; display: flex; justify-content: center; align-items: center; color: #333; border-radius: 50%;">
        动
    </div>
</div>
<button id="playWAAPIBtn" style="padding: 8px 15px; background-color: #61dafb; color: white; border: none; border-radius: 4px; cursor: pointer; margin-right: 10px;">
    播放/恢复
</button>
<button id="pauseWAAPIBtn" style="padding: 8px 15px; background-color: #f8c291; color: #333; border: none; border-radius: 4px; cursor: pointer; margin-right: 10px;">
    暂停
</button>
<button id="reverseWAAPIBtn" style="padding: 8px 15px; background-color: #e74c3c; color: white; border: none; border-radius: 4px; cursor: pointer; margin-right: 10px;">
    反转
</button>
<button id="cancelWAAPIBtn" style="padding: 8px 15px; background-color: #7f8c8d; color: white; border: none; border-radius: 4px; cursor: pointer;">
    取消
</button>

WAAPI的优点:

4. 现代 JavaScript

ES6 (ECMAScript 2015) 及后续版本引入了许多新特性,极大地提升了JavaScript的开发效率和代码可读性。

4.1 ES6+ 特性

了解并运用这些现代特性是成为一名高效前端开发者的关键。

4.1.1 可选链操作符(?.

可选链操作符允许您在访问可能为nullundefined的属性时,无需进行繁琐的判空检查。

const user = {
    name: 'Alice',
    address: {
        street: '123 Main St',
        city: 'Anytown'
    },
    // contacts: { phone: '123-456-7890' } // contacts可能不存在
};

// 旧的判空方式
let streetNameOld;
if (user && user.address && user.address.street) {
    streetNameOld = user.address.street;
} else {
    streetNameOld = '未知街道 (旧)';
}
console.log(streetNameOld); // 123 Main St

let phoneNumberOld;
if (user && user.contacts && user.contacts.phone) {
    phoneNumberOld = user.contacts.phone;
} else {
    phoneNumberOld = '未知电话 (旧)';
}
console.log(phoneNumberOld); // 未知电话 (旧)

// 使用可选链操作符
const streetName = user.address?.street;
console.log(streetName); // 123 Main St

const phoneNumber = user.contacts?.phone;
console.log(phoneNumber); // undefined

// 可选链也可以用于函数调用或数组访问
const getName = user.getName?.(); // 如果getName方法存在则调用
console.log(getName); // undefined

const firstContactPhone = user.contacts?.[0]?.phone; // 假设contacts是数组
console.log(firstContactPhone); // undefined (因为user.contacts是undefined)

// 实际案例:获取嵌套数据,避免报错
const data = {
    users: [
        { id: 1, profile: { name: 'Bob' } },
        { id: 2, profile: null }, // profile可能为null
        { id: 3, details: { age: 30 } } // profile可能不存在
    ]
};

const user2Name = data.users[1]?.profile?.name;
console.log('User 2 name:', user2Name); // User 2 name: undefined

const user3Name = data.users[2]?.profile?.name;
console.log('User 3 name:', user3Name); // User 3 name: undefined

优势: 使代码更简洁、可读性更高,并有效避免运行时错误(如“TypeError: Cannot read properties of undefined (reading 'xxx')”)。

4.1.2 空值合并(??

空值合并操作符(Nullish Coalescing Operator)?? 仅当左侧操作数为nullundefined时,才返回右侧操作数。

const defaultUsername = 'Guest';
const userSettings = {
    username: 'Alice',
    theme: null, // 主题可能为null
    fontSize: undefined, // 字体大小可能为undefined
    isAdmin: false, // isAdmin可能是false (一个有效的值)
    notificationEnabled: 0 // 0 也是一个有效的值
};

// 旧的或逻辑 (||) 的问题:会将0, '', false视为falsy值
const usernameFallbackOld = userSettings.username || defaultUsername;
console.log('旧逻辑 Username:', usernameFallbackOld); // Alice

const themeFallbackOld = userSettings.theme || 'dark';
console.log('旧逻辑 Theme:', themeFallbackOld); // dark (因为null被 || 视为 falsy)

const fontSizeFallbackOld = userSettings.fontSize || 16;
console.log('旧逻辑 FontSize:', fontSizeFallbackOld); // 16 (因为undefined被 || 视为 falsy)

const isAdminFallbackOld = userSettings.isAdmin || true;
console.log('旧逻辑 IsAdmin:', isAdminFallbackOld); // true (因为false被 || 视为 falsy)

const notificationEnabledFallbackOld = userSettings.notificationEnabled || 1;
console.log('旧逻辑 NotificationEnabled:', notificationEnabledFallbackOld); // 1 (因为0被 || 视为 falsy)


// 使用空值合并操作符 (??)
const usernameNew = userSettings.username ?? defaultUsername;
console.log('新逻辑 Username:', usernameNew); // Alice

const themeNew = userSettings.theme ?? 'dark';
console.log('新逻辑 Theme:', themeNew); // dark

const fontSizeNew = userSettings.fontSize ?? 16;
console.log('新逻辑 FontSize:', fontSizeNew); // 16

const isAdminNew = userSettings.isAdmin ?? true;
console.log('新逻辑 IsAdmin:', isAdminNew); // false (?? 只关心 null/undefined)

const notificationEnabledNew = userSettings.notificationEnabled ?? 1;
console.log('新逻辑 NotificationEnabled:', notificationEnabledNew); // 0 (?? 只关心 null/undefined)


// 结合可选链
const userProfile = {
    name: 'Bob',
    prefs: null // prefs可能为null
};
const preferredLang = userProfile.prefs?.language ?? 'en';
console.log('Preferred Language:', preferredLang); // en

??|| 的区别:

4.1.3 动态导入 (Dynamic Imports)

动态导入允许您在运行时按需加载JavaScript模块,而不是在页面加载时一次性加载所有模块。这对于代码分割和性能优化非常有用。

// 动态导入示例
document.getElementById('loadModuleBtn').addEventListener('click', async () => {
    try {
        // 使用 Blob URL 创建一个临时的模块
        const moduleContent = `
            export function greet(name) {
                return \`Hello, \${name} from a dynamically loaded module!\`;
            }
            export function add(a, b) {
                return a + b;
            }
        `;
        const blob = new Blob([moduleContent], { type: 'application/javascript' });
        const moduleUrl = URL.createObjectURL(blob);
        
        // import() 返回一个 Promise,解析为一个模块对象
        const module = await import(moduleUrl); 
        console.log('模块加载成功:', module);

        // 使用加载的模块中的函数
        const greeting = module.greet('World');
        console.log(greeting);
        document.getElementById('moduleOutput').textContent = `模块加载成功! ${greeting}, 10 + 20 = ${module.add(10, 20)}`;
        
        // 释放 Blob URL
        URL.revokeObjectURL(moduleUrl);
        
        // 按钮禁用,防止重复加载
        document.getElementById('loadModuleBtn').disabled = true;
    } catch (error) {
        console.error('模块加载失败:', error);
        document.getElementById('moduleOutput').textContent = `模块加载失败: ${error.message}`;
    }
});

<h4>动态导入模块示例</h4>
<p>
    <button id="loadModuleBtn" style="padding: 8px 15px; background-color: #61dafb; color: white; border: none; border-radius: 4px; cursor: pointer;">
        点击加载模块
    </button>
</p>
<div id="moduleOutput" style="border: 1px dashed #a9dfd8; padding: 10px; margin-top: 10px; min-height: 40px; background-color: #363636; color: #d0d0d0;">
    点击按钮加载JavaScript模块...
</div>
<p style="margin-top: 10px; font-size: 0.9em; color: #bbb;">
    此示例通过 <span class="highlight">Blob URL</span> 
    技术,在浏览器中动态创建了一个模块,并使用 <span class="highlight">import()</span> 加载它,
    无需创建真实文件即可演示。
</p>

用途:

4.2 异步编程

JavaScript是单线程的,但通过异步编程,可以处理耗时操作(如网络请求、定时器)而不阻塞主线程。

4.2.1 Promise 高级模式

Promise是处理异步操作的利器。除了基本的.then().catch(),还有一些高级用法。

// 模拟异步操作
function fetchData(id, delay, shouldFail = false) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            if (shouldFail) {
                reject(new Error(`数据 ${id} 加载失败!`));
            } else {
                resolve(`数据 ${id} 已加载`);
            }
        }, delay);
    });
}

// 1. Promise.all():等待所有Promise都成功,或任意一个失败即中断
document.getElementById('promiseAllBtn').addEventListener('click', async () => {
    console.log('--- Promise.all 示例 ---');
    try {
        const results = await Promise.all([
            fetchData(1, 1000),
            fetchData(2, 500),
            fetchData(3, 1500)
        ]);
        console.log('所有数据加载成功 (Promise.all):', results);
        document.getElementById('promiseOutput').textContent = `Promise.all 成功: ${results.join(', ')}`;
    } catch (error) {
        console.error('Promise.all 失败:', error.message);
        document.getElementById('promiseOutput').textContent = `Promise.all 失败: ${error.message}`;
    }
});

// 2. Promise.allSettled():等待所有Promise都完成 (无论成功或失败),返回每个Promise的状态和结果
document.getElementById('promiseAllSettledBtn').addEventListener('click', async () => {
    console.log('--- Promise.allSettled 示例 ---');
    try {
        const results = await Promise.allSettled([
            fetchData('A', 800),
            fetchData('B', 300, true), // B会失败
            fetchData('C', 1200)
        ]);
        console.log('所有数据加载完成 (Promise.allSettled):', results);
        const output = results.map(res =>
            res.status === 'fulfilled' ? `✔ ${res.value}` : `✖ ${res.reason.message}`
        ).join(' | ');
        document.getElementById('promiseOutput').textContent = `Promise.allSettled 完成: ${output}`;
    } catch (error) {
        // Promise.allSettled 不会抛出错误,因为它总是解析
        console.error('这行代码不会被执行 (Promise.allSettled 不会抛出错误)');
    }
});

// 3. Promise.race():只要有一个Promise成功或失败,就立即返回该Promise的结果
document.getElementById('promiseRaceBtn').addEventListener('click', async () => {
    console.log('--- Promise.race 示例 ---');
    try {
        const result = await Promise.race([
            fetchData('X', 1000),
            fetchData('Y', 200), // Y最快
            fetchData('Z', 1500, true) // Z会失败,但如果Y先成功,Z的失败会被忽略
        ]);
        console.log('最快 Promise 结果 (Promise.race):', result);
        document.getElementById('promiseOutput').textContent = `Promise.race 结果: ${result}`;
    } catch (error) {
        console.error('Promise.race 失败:', error.message);
        document.getElementById('promiseOutput').textContent = `Promise.race 失败: ${error.message}`;
    }
});

// 4. Promise.any():只要有一个Promise成功,就返回其结果;所有都失败才抛出聚合错误 (ES2021)
document.getElementById('promiseAnyBtn').addEventListener('click', async () => {
    console.log('--- Promise.any 示例 ---');
    try {
        const result = await Promise.any([
            fetchData('P', 1000, true), // P失败
            fetchData('Q', 300, true), // Q失败
            fetchData('R', 500) // R成功
        ]);
        console.log('第一个成功结果 (Promise.any):', result);
        document.getElementById('promiseOutput').textContent = `Promise.any 成功: ${result}`;
    } catch (error) {
        // 当所有Promise都失败时,Promise.any 会抛出AggregateError
        console.error('Promise.any 失败 (所有都失败):', error);
        document.getElementById('promiseOutput').textContent = `Promise.any 失败 (所有都失败): ${error.errors.map(e => e.message).join('; ')}`;
    }
});

<h4>Promise 高级模式示例</h4>
<div style="margin-bottom: 15px;">
    <button id="promiseAllBtn" class="promise-btn">Promise.all</button>
    <button id="promiseAllSettledBtn" class="promise-btn">Promise.allSettled</button>
    <button id="promiseRaceBtn" class="promise-btn">Promise.race</button>
    <button id="promiseAnyBtn" class="promise-btn">Promise.any</button>
</div>
<div id="promiseOutput" style="border: 1px dashed #61dafb; padding: 10px; min-height: 50px; background-color: #363636; color: #d0d0d0; word-wrap: break-word;">
    点击按钮查看Promise结果...
</div>
<p style="margin-top: 10px; font-size: 0.9em; color: #bbb;">请打开控制台查看详细的Promise日志。</p>

<style>
    .promise-btn {
        padding: 8px 15px;
        background-color: #a9dfd8;
        color: #333;
        border: none;
        border-radius: 4px;
        cursor: pointer;
        margin-right: 10px;
        margin-bottom: 10px;
    }
    .promise-btn:hover {
        opacity: 0.9;
    }
</style>
4.2.2 async/await 错误处理

async/await是Promise的语法糖,使异步代码看起来像同步代码一样直观。正确处理其错误至关重要。

// 模拟一个可能成功或失败的异步函数
async function getUserData(userId, simulateError = false) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            if (simulateError) {
                reject(new Error(`Failed to fetch user ${userId} data!`));
            } else {
                resolve({ id: userId, name: `User ${userId}`, email: `user${userId}@example.com` });
            }
        }, 800);
    });
}

// 方式一:使用 try...catch
document.getElementById('fetchUserTryCatch').addEventListener('click', async () => {
    console.log('--- 使用 try...catch 处理错误 ---');
    try {
        const user = await getUserData(1, false); // 成功示例
        console.log('User data (try...catch success):', user);
        document.getElementById('asyncAwaitOutput').textContent = `try...catch 成功: ${user.name}`;

        // const errorUser = await getUserData(2, true); // 失败示例
        // console.log('This will not be logged if error occurs:', errorUser);
    } catch (error) {
        console.error('try...catch 捕获到错误:', error.message);
        document.getElementById('asyncAwaitOutput').textContent = `try...catch 失败: ${error.message}`;
    }

    try {
        const errorUser = await getUserData(2, true); // 失败示例
        console.log('This will not be logged if error occurs:', errorUser);
    } catch (error) {
        console.error('try...catch 捕获到错误:', error.message);
        document.getElementById('asyncAwaitOutput').textContent += ` | try...catch 失败: ${error.message}`;
    }
});

// 方式二:使用 .catch() (在顶层await或组合多个await时可能不适用)
document.getElementById('fetchUserCatch').addEventListener('click', () => {
    console.log('--- 使用 .catch() 处理错误 ---');
    getUserData(3, true) // 失败示例
        .then(user => {
            console.log('User data (.catch() success):', user);
            document.getElementById('asyncAwaitOutput').textContent = `.catch() 成功: ${user.name}`;
        })
        .catch(error => {
            console.error('.catch() 捕获到错误:', error.message);
            document.getElementById('asyncAwaitOutput').textContent = `.catch() 失败: ${error.message}`;
        });
});

// 方式三:立即执行函数 (IIFE) + try...catch (处理顶层await错误)
document.getElementById('fetchUserIIFE').addEventListener('click', () => {
    console.log('--- 使用 IIFE + try...catch 处理顶层await错误 ---');
    (async () => {
        try {
            const user = await getUserData(4, true); // 失败示例
            console.log('User data (IIFE success):', user);
            document.getElementById('asyncAwaitOutput').textContent = `IIFE 成功: ${user.name}`;
        } catch (error) {
            console.error('IIFE 捕获到错误:', error.message);
            document.getElementById('asyncAwaitOutput').textContent = `IIFE 失败: ${error.message}`;
        }
    })();
});

// 方式四:Wrapper 函数捕获错误 (推荐用于组件/服务层)
function wrapPromise(promise) {
    return promise.then(data => [null, data]).catch(error => [error, null]);
}

document.getElementById('fetchUserWrapper').addEventListener('click', async () => {
    console.log('--- 使用 Wrapper 函数捕获错误 ---');
    const [error1, user1] = await wrapPromise(getUserData(5, false)); // 成功
    if (error1) {
        console.error('Wrapper 方式错误 (User 5):', error1.message);
        document.getElementById('asyncAwaitOutput').textContent = `Wrapper 方式错误 (User 5): ${error1.message}`;
    } else {
        console.log('Wrapper 方式成功 (User 5):', user1);
        document.getElementById('asyncAwaitOutput').textContent = `Wrapper 方式成功 (User 5): ${user1.name}`;
    }

    const [error2, user2] = await wrapPromise(getUserData(6, true)); // 失败
    if (error2) {
        console.error('Wrapper 方式错误 (User 6):', error2.message);
        document.getElementById('asyncAwaitOutput').textContent += ` | Wrapper 方式错误 (User 6): ${error2.message}`;
    } else {
        console.log('Wrapper 方式成功 (User 6):', user2);
    }
});

<h4>Async/Await 错误处理示例</h4>
<div style="margin-bottom: 15px;">
    <button id="fetchUserTryCatch" class="async-btn">try...catch</button>
    <button id="fetchUserCatch" class="async-btn">.catch()</button>
    <button id="fetchUserIIFE" class="async-btn">IIFE + try...catch</button>
    <button id="fetchUserWrapper" class="async-btn">Wrapper 函数</button>
</div>
<div id="asyncAwaitOutput" style="border: 1px dashed #f8c291; padding: 10px; min-height: 50px; background-color: #363636; color: #d0d0d0; word-wrap: break-word;">
    点击按钮查看异步操作结果...
</div>
<p style="margin-top: 10px; font-size: 0.9em; color: #bbb;">请打开控制台查看详细的错误日志。</p>

<style>
    .async-btn {
        padding: 8px 15px;
        background-color: #a9dfd8;
        color: #333;
        border: none;
        border-radius: 4px;
        cursor: pointer;
        margin-right: 10px;
        margin-bottom: 10px;
    }
    .async-btn:hover {
        opacity: 0.9;
    }
</style>

总结:

4.2.3 Web Workers

Web Workers 允许JavaScript在后台线程中运行,而不阻塞用户界面。这对于执行复杂的计算或大量数据处理非常有用。

graph TD A[主线程 (UI)] -->|创建 Worker| B(Worker 线程) B -->|postMessage(data)| C{执行耗时计算} C -->|postMessage(result)| A A -->|监听 message 事件| D[更新 UI] B -->|错误| E[主线程处理错误]
// main.js (主线程)
const longComputationBtn = document.getElementById('longComputationBtn');
const workerResultDiv = document.getElementById('workerResult');

if (longComputationBtn && workerResultDiv) {
    longComputationBtn.addEventListener('click', () => {
        workerResultDiv.textContent = '正在计算中...';
        longComputationBtn.disabled = true;

        // 检查浏览器是否支持Web Workers
        if (window.Worker) {
            // 创建一个Blob URL来模拟 worker.js 文件
            const workerJsContent = `
                self.onmessage = function(e) {
                    console.log('Worker: 收到主线程消息:', e.data);
                    const number = e.data;
                    let sum = 0;
                    // 模拟耗时计算
                    for (let i = 0; i <= number; i++) {
                        sum += i;
                    }
                    console.log('Worker: 计算完成,发送结果回主线程。');
                    self.postMessage(sum); // 将结果发送回主线程
                };
            `;
            const blob = new Blob([workerJsContent], { type: 'application/javascript' });
            const workerUrl = URL.createObjectURL(blob);
            const myWorker = new Worker(workerUrl);

            // 向 Worker 发送消息
            const numberToCompute = 10000000000; // 模拟一个大数
            myWorker.postMessage(numberToCompute);
            console.log(`主线程: 已发送计算请求给 Worker, 计算到 ${numberToCompute}`);

            // 监听 Worker 发回的消息
            myWorker.onmessage = function(e) {
                console.log('主线程: 从 Worker 收到结果:', e.data);
                workerResultDiv.textContent = `计算完成!结果是: ${e.data.toLocaleString()}`;
                longComputationBtn.disabled = false;
                myWorker.terminate(); // 完成后终止 Worker
                URL.revokeObjectURL(workerUrl); // 释放Blob URL
            };

            // 监听 Worker 错误
            myWorker.onerror = function(error) {
                console.error('主线程: Worker 发生错误:', error);
                workerResultDiv.textContent = `计算出错: ${error.message}`;
                longComputationBtn.disabled = false;
                URL.revokeObjectURL(workerUrl);
            };

        } else {
            workerResultDiv.textContent = '抱歉,您的浏览器不支持 Web Workers。';
            longComputationBtn.disabled = false;
        }
    });
}

<h4>Web Workers 示例</h4>
<p>
    <button id="longComputationBtn" style="padding: 8px 15px; background-color: #61dafb; color: white; border: none; border-radius: 4px; cursor: pointer;">
        执行耗时计算 (在 Worker 中)
    </button>
</p>
<div id="workerResult" style="border: 1px dashed #a9dfd8; padding: 10px; margin-top: 10px; min-height: 50px; background-color: #363636; color: #d0d0d0;">
    点击按钮开始计算...
</div>
<p style="margin-top: 10px; font-size: 0.9em; color: #bbb;">
    <strong>提示:</strong> 在点击按钮后,尝试在计算过程中拖动页面或点击其他元素,会发现界面不会卡顿。
    此示例通过 Blob URL 内联了 Worker 脚本,无需额外文件。
</p>

Web Workers 的限制:


第三部分:实用功能实现

5. 表单处理

表单是前端与后端交互的关键。除了HTML内置验证,JavaScript可以实现更复杂的客户端验证和文件操作。

5.1 复杂表单验证

虽然HTML5提供了基础验证,但JavaScript可以实现自定义规则、实时反馈和更友好的用户体验。

5.1.1 实时验证反馈与自定义验证消息
// 获取表单元素
const registrationForm = document.getElementById('registrationForm');
const usernameInput = document.getElementById('regUsername');
const emailInput = document.getElementById('regEmail');
const passwordInput = document.getElementById('regPassword');
const confirmPasswordInput = document.getElementById('confirmPassword');

// 辅助函数:显示错误消息
function showValidationError(inputElement, message) {
    const errorDisplay = inputElement.nextElementSibling; // 假定错误提示在input后面
    if (errorDisplay && errorDisplay.classList.contains('error-message')) {
        errorDisplay.textContent = message;
        inputElement.classList.add('invalid');
    }
}

// 辅助函数:清除错误消息
function clearValidationError(inputElement) {
    const errorDisplay = inputElement.nextElementSibling;
    if (errorDisplay && errorDisplay.classList.contains('error-message')) {
        errorDisplay.textContent = '';
        inputElement.classList.remove('invalid');
        inputElement.classList.remove('valid');
    }
}

// 辅助函数:检查有效性并更新状态
function validateInput(inputElement, validationFn) {
    const isValid = validationFn(inputElement.value);
    if (!isValid.status) {
        showValidationError(inputElement, isValid.message);
        return false;
    } else {
        clearValidationError(inputElement);
        inputElement.classList.add('valid');
        return true;
    }
}

// 验证规则
const rules = {
    username: (value) => {
        if (!value) return { status: false, message: '用户名不能为空。' };
        if (value.length < 3 || value.length > 15) return { status: false, message: '用户名长度需在3到15个字符之间。' };
        if (!/^[a-zA-Z0-9_]+$/.test(value)) return { status: false, message: '用户名只能包含字母、数字和下划线。' };
        return { status: true };
    },
    email: (value) => {
        if (!value) return { status: false, message: '邮箱不能为空。' };
        if (!/^\S+@\S+\.\S+$/.test(value)) return { status: false, message: '请输入有效的邮箱地址。' };
        return { status: true };
    },
    password: (value) => {
        if (!value) return { status: false, message: '密码不能为空。' };
        if (value.length < 8) return { status: false, message: '密码至少需要8位。' };
        if (!/[A-Z]/.test(value)) return { status: false, message: '密码必须包含一个大写字母。' };
        if (!/[a-z]/.test(value)) return { status: false, message: '密码必须包含一个小写字母。' };
        if (!/[0-9]/.test(value)) return { status: false, message: '密码必须包含一个数字。' };
        return { status: true };
    },
    confirmPassword: (value) => {
        if (!value) return { status: false, message: '请确认密码。' };
        if (value !== passwordInput.value) return { status: false, message: '两次输入的密码不一致。' };
        return { status: true };
    }
};


// 实时验证(失去焦点时和输入时)
if (usernameInput) {
    usernameInput.addEventListener('input', () => validateInput(usernameInput, rules.username));
    usernameInput.addEventListener('blur', () => validateInput(usernameInput, rules.username));
}
if (emailInput) {
    emailInput.addEventListener('input', () => validateInput(emailInput, rules.email));
    emailInput.addEventListener('blur', () => validateInput(emailInput, rules.email));
}
if (passwordInput) {
    passwordInput.addEventListener('input', () => {
        validateInput(passwordInput, rules.password);
        // 如果确认密码有值,也重新验证确认密码
        if (confirmPasswordInput.value) {
            validateInput(confirmPasswordInput, rules.confirmPassword);
        }
    });
    passwordInput.addEventListener('blur', () => validateInput(passwordInput, rules.password));
}
if (confirmPasswordInput) {
    confirmPasswordInput.addEventListener('input', () => validateInput(confirmPasswordInput, rules.confirmPassword));
    confirmPasswordInput.addEventListener('blur', () => validateInput(confirmPasswordInput, rules.confirmPassword));
}


// 表单提交时进行总体验证
if (registrationForm) {
    registrationForm.addEventListener('submit', function(event) {
        event.preventDefault(); // 阻止默认提交行为

        let isValidForm = true;

        isValidForm = validateInput(usernameInput, rules.username) && isValidForm;
        isValidForm = validateInput(emailInput, rules.email) && isValidForm;
        isValidForm = validateInput(passwordInput, rules.password) && isValidForm;
        isValidForm = validateInput(confirmPasswordInput, rules.confirmPassword) && isValidForm;

        if (isValidForm) {
            alert('表单验证成功!可以提交数据。');
            // 这里可以进行Ajax提交
            console.log('表单数据:', {
                username: usernameInput.value,
                email: emailInput.value,
                password: passwordInput.value // 实际不要直接发送密码
            });
            // registrationForm.submit(); // 或者允许原生提交
        } else {
            console.log('表单验证失败,请检查输入。');
        }
    });
}

<h4>复杂表单验证示例</h4>
<form id="registrationForm" style="background-color: #363636; padding: 25px; border-radius: 8px; max-width: 500px; margin: 20px auto;" novalidate>
    <div class="form-group">
        <label for="regUsername">用户名:</label>
        <input type="text" id="regUsername" name="username" placeholder="3-15位字母数字下划线">
        <span class="error-message"></span>
    </div>

    <div class="form-group">
        <label for="regEmail">邮箱:</label>
        <input type="email" id="regEmail" name="email" placeholder="[email protected]">
        <span class="error-message"></span>
    </div>

    <div class="form-group">
        <label for="regPassword">密码:</label>
        <input type="password" id="regPassword" name="password" placeholder="至少8位,含大小写字母和数字">
        <span class="error-message"></span>
    </div>

    <div class="form-group">
        <label for="confirmPassword">确认密码:</label>
        <input type="password" id="confirmPassword" name="confirmPassword" placeholder="请再次输入密码">
        <span class="error-message"></span>
    </div>

    <button type="submit" style="padding: 10px 20px; background-color: #61dafb; color: white; border: none; border-radius: 5px; cursor: pointer; margin-top: 15px;">
        注册
    </button>
</form>

<style>
    .form-group {
        margin-bottom: 15px;
    }
    .form-group label {
        display: block;
        margin-bottom: 5px;
        color: #a9dfd8;
    }
    .form-group input[type="text"],
    .form-group input[type="email"],
    .form-group input[type="password"] {
        width: 100%;
        padding: 10px;
        border: 1px solid #444;
        border-radius: 4px;
        background-color: #3e3e3e;
        color: #e0e0e0;
        font-size: 1em;
        box-sizing: border-box;
    }
    .form-group input:focus {
        outline: none;
        border-color: #61dafb;
        box-shadow: 0 0 0 2px rgba(97, 218, 251, 0.3);
    }
    .error-message {
        color: #e74c3c;
        font-size: 0.9em;
        margin-top: 5px;
        display: block;
        min-height: 1.2em; /* 避免布局抖动 */
    }
    .form-group input.invalid {
        border-color: #e74c3c;
    }
    .form-group input.valid {
        border-color: #27ae60;
    }
</style>

关键点:

5.2 文件处理

JavaScript可以通过File API处理用户选择的文件,实现上传预览、拖放等功能。

5.2.1 文件上传预览

在用户选择图片后,立即在前端展示预览,提升用户体验。

const imageInput = document.getElementById('imageUpload');
const imagePreviewContainer = document.getElementById('imagePreviewContainer');

if (imageInput && imagePreviewContainer) {
    imageInput.addEventListener('change', function(event) {
        imagePreviewContainer.innerHTML = ''; // 清空之前的预览

        const files = event.target.files; // 获取选择的文件列表

        if (files.length === 0) {
            imagePreviewContainer.textContent = '未选择图片。';
            return;
        }

        Array.from(files).forEach(file => {
            // 确保文件是图片类型
            if (!file.type.startsWith('image/')) {
                const p = document.createElement('p');
                p.textContent = `文件 "${file.name}" 不是图片类型,无法预览。`;
                p.style.color = '#e74c3c';
                imagePreviewContainer.appendChild(p);
                return;
            }

            // 使用 FileReader 读取文件内容
            const reader = new FileReader();

            reader.onload = (e) => {
                const img = document.createElement('img');
                img.src = e.target.result; // base64编码的图片数据
                img.alt = file.name;
                img.style.maxWidth = '150px';
                img.style.maxHeight = '150px';
                img.style.margin = '10px';
                img.style.border = '1px solid #61dafb';
                img.style.borderRadius = '4px';

                imagePreviewContainer.appendChild(img);
            };

            reader.onerror = (e) => {
                console.error('文件读取失败:', e.target.error);
                const p = document.createElement('p');
                p.textContent = `文件 "${file.name}" 读取失败。`;
                p.style.color = '#e74c3c';
                imagePreviewContainer.appendChild(p);
            };

            reader.readAsDataURL(file); // 读取文件为Data URL (base64)
        });
    });
}

<h4>文件上传预览示例</h4>
<div style="background-color: #363636; padding: 25px; border-radius: 8px;">
    <label for="imageUpload" style="display: block; margin-bottom: 10px; color: #a9dfd8;">选择图片上传:</label>
    <input type="file" id="imageUpload" accept="image/*" multiple>
    <p style="margin-top: 10px; font-size: 0.9em; color: #bbb;">
        选择图片后,会在下方区域显示预览。
    </p>
    <div id="imagePreviewContainer" style="border: 1px dashed #f8c291; padding: 15px; min-height: 100px; margin-top: 20px; display: flex; flex-wrap: wrap; justify-content: center; align-items: center; background-color: #2b2b2b; color: #d0d0d0;">
        图片预览区域
    </div>
</div>

核心API:

5.2.2 拖放上传

拖放(Drag and Drop)上传提供了更便捷的用户体验。

const dropArea = document.getElementById('dropArea');
const dropFilePreview = document.getElementById('dropFilePreview');

if (dropArea && dropFilePreview) {
    // 阻止浏览器默认行为(打开文件)
    ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
        dropArea.addEventListener(eventName, preventDefaults, false);
    });

    function preventDefaults(e) {
        e.preventDefault();
        e.stopPropagation();
    }

    // 添加/移除高亮样式
    ['dragenter', 'dragover'].forEach(eventName => {
        dropArea.addEventListener(eventName, () => dropArea.classList.add('highlight-drag'), false);
    });

    ['dragleave', 'drop'].forEach(eventName => {
        dropArea.addEventListener(eventName, () => dropArea.classList.remove('highlight-drag'), false);
    });

    // 处理文件拖放
    dropArea.addEventListener('drop', handleDrop, false);

    function handleDrop(e) {
        const dt = e.dataTransfer;
        const files = dt.files; // 获取拖放的文件列表

        handleFiles(files);
    }

    // 处理文件 (与文件预览类似)
    function handleFiles(files) {
        dropFilePreview.innerHTML = ''; // 清空之前的预览
        if (files.length === 0) {
            dropFilePreview.textContent = '未拖放文件。';
            return;
        }

        Array.from(files).forEach(file => {
            // 简单预览文件信息,图片可以像上面那样预览
            const p = document.createElement('p');
            p.textContent = `文件名称: ${file.name}, 大小: ${(file.size / 1024).toFixed(2)} KB, 类型: ${file.type || '未知'}`;
            dropFilePreview.appendChild(p);

            // 如果是图片,也可以像5.2.1那样进行预览
            if (file.type.startsWith('image/')) {
                const reader = new FileReader();
                reader.onload = (e) => {
                    const img = document.createElement('img');
                    img.src = e.target.result;
                    img.alt = file.name;
                    img.style.maxWidth = '100px';
                    img.style.maxHeight = '100px';
                    img.style.marginRight = '10px';
                    img.style.border = '1px solid #a9dfd8';
                    img.style.borderRadius = '4px';
                    dropFilePreview.prepend(img); // 将图片放在前面
                };
                reader.readAsDataURL(file);
            }
        });

        // 实际应用中,这里会将文件上传到服务器,例如使用FormData和Fetch API
        // uploadFiles(files);
    }
}

<h4>拖放上传示例</h4>
<div id="dropArea" style="
    border: 3px dashed #61dafb;
    padding: 50px;
    text-align: center;
    font-size: 1.2em;
    color: #a9dfd8;
    background-color: #363636;
    border-radius: 8px;
    transition: background-color 0.3s ease, border-color 0.3s ease;
    margin-bottom: 20px;
">
    将文件拖放到此处上传
</div>
<div id="dropFilePreview" style="
    border: 1px solid #444;
    padding: 15px;
    min-height: 80px;
    background-color: #2b2b2b;
    color: #d0d0d0;
    border-radius: 4px;
    display: flex;
    flex-wrap: wrap;
    align-items: center;
">
    拖放的文件信息将在此处显示...
</div>

<style>
    #dropArea.highlight-drag {
        background-color: #4a4a4a;
        border-color: #f8c291;
    }
</style>

核心API和事件:

5.2.3 PDF/Excel 生成 (前端实现)

虽然复杂PDF/Excel生成通常在后端完成,但对于一些简单的数据导出,前端库也能实现。

PDF生成: 使用如 jsPDF 结合 html2canvas 将HTML元素转换为图片再嵌入PDF。

document.getElementById('generatePdfBtn').addEventListener('click', async () => {
    try {
        if (typeof window.jspdf === 'undefined' || typeof window.html2canvas === 'undefined') {
            alert('请检查是否已正确引入 jspdf 和 html2canvas 库。');
            console.error('jsPDF or html2canvas not loaded.');
            return;
        }

        const { jsPDF } = window.jspdf; // 获取jsPDF构造函数

        const element = document.getElementById('contentToExport'); // 要导出为PDF的HTML元素
        const doc = new jsPDF();

        // 将HTML元素渲染为canvas图片
        const canvas = await window.html2canvas(element, { scale: 2 }); // 提高scale以获得更高清晰度
        const imgData = canvas.toDataURL('image/png');

        // 计算图片在PDF中的尺寸
        const imgWidth = 210; // A4宽度 (毫米)
        const pageHeight = 297; // A4高度 (毫米)
        const imgHeight = canvas.height * imgWidth / canvas.width;
        let heightLeft = imgHeight;
        let position = 0;

        doc.addImage(imgData, 'PNG', 0, position, imgWidth, imgHeight);
        heightLeft -= pageHeight;

        while (heightLeft >= 0) {
            position = heightLeft - imgHeight;
            doc.addPage();
            doc.addImage(imgData, 'PNG', 0, position, imgWidth, imgHeight);
            heightLeft -= pageHeight;
        }

        doc.save('前端教程内容.pdf');
        alert('PDF生成成功!');
    } catch (error) {
        console.error('生成PDF时发生错误:', error);
        alert('生成PDF失败,请查看控制台。');
    }
});

// Excel生成:使用xlsx库
document.getElementById('generateExcelBtn').addEventListener('click', () => {
    try {
        if (typeof window.XLSX === 'undefined') {
            alert('请检查是否已正确引入 xlsx 库。');
            console.error('xlsx not loaded.');
            return;
        }

        // 示例数据
        const data = [
            ["姓名", "年龄", "城市"],
            ["张三", 30, "北京"],
            ["李四", 24, "上海"],
            ["王五", 35, "广州"]
        ];

        // 创建工作簿
        const ws = window.XLSX.utils.aoa_to_sheet(data); // 从二维数组创建工作表

        // 可以添加单元格样式、宽度等
        ws['!cols'] = [{ width: 15 }, { width: 10 }, { width: 15 }];

        const wb = window.XLSX.utils.book_new();
        window.XLSX.utils.book_append_sheet(wb, ws, "用户数据"); // 添加工作表到工作簿

        // 写入文件并下载
        window.XLSX.writeFile(wb, "用户数据.xlsx");
        alert('Excel文件生成成功!');

    } catch (error) {
        console.error('生成Excel时发生错误:', error);
        alert('生成Excel失败,请查看控制台。');
    }
});

<h4>PDF/Excel 生成示例</h4>
<div style="background-color: #363636; padding: 25px; border-radius: 8px;">
    <p>
        此功能依赖于第三方库 (<a href="https://jspdf.org/" target="_blank">jsPDF</a> / <a href="https://github.com/niklasvh/html2canvas" target="_blank">html2canvas</a> 和 <a href="https://sheetjs.com/" target="_blank">SheetJS (xlsx)</a>)。
        本页面已通过CDN引入这些库,可以直接使用。
    </p>

    <div id="contentToExport" style="background-color: #2b2b2b; padding: 20px; border: 1px solid #a9dfd8; margin-top: 15px; color: #d0d0d0;">
        <h3 style="color: #61dafb;">PDF 导出测试内容</h3>
        <p>这段文字和这个容器将会被导出为 PDF。你可以尝试增加更多内容来测试多页导出。</p>
        <ul>
            <li>列表项 1</li>
            <li>列表项 2</li>
            <li>列表项 3</li>
        </ul>
    </div>

    <button id="generatePdfBtn" style="padding: 10px 20px; background-color: #e74c3c; color: white; border: none; border-radius: 5px; cursor: pointer; margin-top: 20px; margin-right: 15px;">
        生成 PDF
    </button>
    <button id="generateExcelBtn" style="padding: 10px 20px; background-color: #27ae60; color: white; border: none; border-radius: 5px; cursor: pointer; margin-top: 20px;">
        生成 Excel
    </button>
</div>

注意: 前端生成复杂报表或带有交互功能的PDF/Excel通常不是最佳实践。对于大规模、复杂、需要高度定制的场景,推荐使用后端服务生成。

6. 浏览器 API

浏览器提供了丰富的API,使前端能够与用户的设备、存储和网络进行更深度的交互。

6.1 存储方案

前端存储数据的方式有多种,根据数据量、生命周期和访问需求选择合适的方案。

graph TD A[浏览器存储方案] --> B(Cookie) A --> C(localStorage) A --> D(sessionStorage) A --> E(IndexedDB) A --> F(Web SQL - Deprecated) B --> B1[容量小: ~4KB] B --> B2[自动随请求发送到服务器] B --> B3[可设置过期时间] C --> C1[容量大: 5-10MB] C --> C2[持久存储, 无过期时间] C --> C3[同源可访问] D --> D1[容量大: 5-10MB] D --> D2[会话存储, 浏览器关闭即清除] D --> D3[同源可访问] E --> E1[容量巨大: 50MB+] E --> E2[NoSQL数据库, 适合结构化数据] E --> E3[异步API, 性能高]
6.1.1 IndexedDB 操作

IndexedDB 是一种低级API,用于在客户端存储大量结构化数据,包括文件/二进制数据。它是一个事务型数据库系统。

// IndexedDB操作示例
const DB_NAME = 'MyAppData';
const DB_VERSION = 1;
const STORE_NAME = 'users';

let db;

// 打开数据库或创建数据库和对象存储
function openDatabase() {
    return new Promise((resolve, reject) => {
        const request = indexedDB.open(DB_NAME, DB_VERSION);

        request.onerror = (event) => {
            console.error('IndexedDB 错误:', event.target.errorCode);
            document.getElementById('indexedDBStatus').textContent = `IndexedDB 错误: ${event.target.errorCode}`;
            reject('IndexedDB error');
        };

        request.onsuccess = (event) => {
            db = event.target.result;
            console.log('IndexedDB 打开成功');
            document.getElementById('indexedDBStatus').textContent = 'IndexedDB 已连接。';
            resolve();
        };

        // 仅在数据库版本升级或首次创建时触发
        request.onupgradeneeded = (event) => {
            db = event.target.result;
            // 创建对象存储 (表)
            if (!db.objectStoreNames.contains(STORE_NAME)) {
                const objectStore = db.createObjectStore(STORE_NAME, { keyPath: 'id', autoIncrement: true });
                // 创建索引 (可选,用于快速查找)
                objectStore.createIndex('name', 'name', { unique: false });
                objectStore.createIndex('email', 'email', { unique: true });
                console.log('Object store and indexes created.');
            }
        };
    });
}

// 添加数据
async function addUser(name, email) {
    if (!db) { await openDatabase(); }
    return new Promise((resolve, reject) => {
        const transaction = db.transaction([STORE_NAME], 'readwrite');
        const objectStore = transaction.objectStore(STORE_NAME);
        const request = objectStore.add({ name, email });

        request.onsuccess = (event) => {
            console.log('用户添加成功,ID:', event.target.result);
            resolve(event.target.result);
        };
        request.onerror = (event) => {
            console.error('用户添加失败:', event.target.error);
            reject(event.target.error);
        };
    });
}

// 获取所有数据
async function getAllUsers() {
    if (!db) { await openDatabase(); }
    return new Promise((resolve, reject) => {
        const transaction = db.transaction([STORE_NAME], 'readonly');
        const objectStore = transaction.objectStore(STORE_NAME);
        const request = objectStore.getAll();

        request.onsuccess = (event) => {
            console.log('所有用户:', event.target.result);
            resolve(event.target.result);
        };
        request.onerror = (event) => {
            console.error('获取用户失败:', event.target.error);
            reject(event.target.error);
        };
    });
}

// 示例操作
document.getElementById('connectIndexedDB').addEventListener('click', openDatabase);

document.getElementById('addIndexedDBUser').addEventListener('click', async () => {
    const name = prompt('请输入用户名:');
    const email = prompt('请输入邮箱:');
    if (name && email) {
        try {
            const userId = await addUser(name, email);
            alert(`用户 ${name} (ID: ${userId}) 添加成功!`);
        } catch (e) {
            alert(`添加用户失败: ${e.message}`);
        }
    }
});

document.getElementById('listIndexedDBUsers').addEventListener('click', async () => {
    try {
        const users = await getAllUsers();
        const userListDiv = document.getElementById('indexedDBUserList');
        userListDiv.innerHTML = '';
        if (users.length === 0) {
            userListDiv.textContent = '目前没有用户数据。';
        } else {
            users.forEach(user => {
                const p = document.createElement('p');
                p.textContent = `ID: ${user.id}, 姓名: ${user.name}, 邮箱: ${user.email}`;
                userListDiv.appendChild(p);
            });
        }
    } catch (e) {
        alert(`获取用户列表失败: ${e.message}`);
    }
});

// 初始化连接
openDatabase();

<h4>IndexedDB 操作示例</h4>
<div style="background-color: #363636; padding: 25px; border-radius: 8px;">
    <p>IndexedDB 是浏览器提供的一个客户端存储大量结构化数据的方式。</p>
    <p id="indexedDBStatus" style="color: #61dafb; font-weight: bold;">
        正在尝试连接 IndexedDB...
    </p>
    <div style="margin-top: 20px;">
        <button id="connectIndexedDB" class="storage-btn">重新连接 IndexedDB</button>
        <button id="addIndexedDBUser" class="storage-btn">添加用户</button>
        <button id="listIndexedDBUsers" class="storage-btn">列出所有用户</button>
    </div>
    <div id="indexedDBUserList" style="border: 1px dashed #a9dfd8; padding: 15px; min-height: 80px; margin-top: 20px; background-color: #2b2b2b; color: #d0d0d0;">
        用户列表将在此显示...
    </div>
    <p style="margin-top: 10px; font-size: 0.9em; color: #bbb;">
        请注意:IndexedDB 的数据是持久化的,即使关闭浏览器也会保留,直到手动清除。
    </p>
</div>

<style>
    .storage-btn {
        padding: 8px 15px;
        background-color: #f8c291;
        color: #333;
        border: none;
        border-radius: 4px;
        cursor: pointer;
        margin-right: 10px;
        margin-bottom: 10px;
    }
    .storage-btn:hover {
        opacity: 0.9;
    }
</style>
6.1.2 Cookie 安全设置

Cookie 用于在客户端存储少量数据,通常用于会话管理、个性化设置等。理解其安全属性至关重要。

// 设置 Cookie
function setCookie(name, value, days, path = '/', domain = '', secure = false, httpOnly = false, sameSite = 'Lax') {
    let expires = '';
    if (days) {
        const date = new Date();
        date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
        expires = `; expires=${date.toUTCString()}`;
    }
    let cookieString = `${name}=${encodeURIComponent(value)}${expires}; path=${path}`;
    if (domain) {
        cookieString += `; domain=${domain}`;
    }
    if (secure) {
        cookieString += `; secure`; // 仅在HTTPS连接中发送
    }
    if (httpOnly) {
        // httpOnly 不能通过 JS 设置,只能通过服务器端设置
        // 此处仅作注释提醒
        console.warn('HttpOnly cookie flag cannot be set via JavaScript. It must be set by the server.');
    }
    cookieString += `; SameSite=${sameSite}`; // CSRF保护

    document.cookie = cookieString;
    console.log('Cookie 设置:', cookieString);
    alert(`Cookie "${name}" 已设置。`);
}

// 读取 Cookie
function getCookie(name) {
    const nameEQ = name + "=";
    const ca = document.cookie.split(';');
    for(let i = 0; i < ca.length; i++) {
        let c = ca[i];
        while (c.charAt(0) === ' ') c = c.substring(1, c.length);
        if (c.indexOf(nameEQ) === 0) {
            return decodeURIComponent(c.substring(nameEQ.length, c.length));
        }
    }
    return null;
}

// 删除 Cookie
function deleteCookie(name, path = '/', domain = '') {
    setCookie(name, '', -1, path, domain); // 将过期时间设为过去,即可删除
    alert(`Cookie "${name}" 已删除。`);
}

// 示例操作
document.getElementById('setCookieBtn').addEventListener('click', () => {
    setCookie('myAppName', 'user_session_123', 7, '/', '', location.protocol === 'https:', false, 'Lax'); // 7天过期,SameSite=Lax
    document.getElementById('cookieStatus').textContent = `Cookie 'myAppName' 已设置。`;
});

document.getElementById('getCookieBtn').addEventListener('click', () => {
    const value = getCookie('myAppName');
    const msg = value ? `Cookie 'myAppName' 的值是: ${value}` : 'Cookie \'myAppName\' 不存在。';
    alert(msg);
    document.getElementById('cookieStatus').textContent = msg;
});

document.getElementById('deleteCookieBtn').addEventListener('click', () => {
    deleteCookie('myAppName');
    document.getElementById('cookieStatus').textContent = `Cookie 'myAppName' 已删除。`;
});

// localStorage 和 sessionStorage (Web Storage API)
// 它们更简单,容量更大,但无法自动随HTTP请求发送
document.getElementById('setLocalStorageBtn').addEventListener('click', () => {
    localStorage.setItem('mySetting', 'dark_theme');
    alert('localStorage "mySetting" 已设置。');
    document.getElementById('localStorageStatus').textContent = `localStorage 'mySetting' 已设置: ${localStorage.getItem('mySetting')}`;
});

document.getElementById('getLocalStorageBtn').addEventListener('click', () => {
    const value = localStorage.getItem('mySetting');
    const msg = value ? `localStorage 'mySetting' 的值是: ${value}` : 'localStorage "mySetting" 不存在。';
    alert(msg);
    document.getElementById('localStorageStatus').textContent = msg;
});

document.getElementById('removeLocalStorageBtn').addEventListener('click', () => {
    localStorage.removeItem('mySetting');
    alert('localStorage "mySetting" 已删除。');
    document.getElementById('localStorageStatus').textContent = 'localStorage "mySetting" 已删除。';
});

// sessionStorage 与 localStorage 类似,但只在会话期间有效
document.getElementById('setSessionStorageBtn').addEventListener('click', () => {
    sessionStorage.setItem('tempData', '临时数据');
    alert('sessionStorage "tempData" 已设置。');
    document.getElementById('sessionStorageStatus').textContent = `sessionStorage 'tempData' 已设置: ${sessionStorage.getItem('tempData')}`;
});

document.getElementById('getSessionStorageBtn').addEventListener('click', () => {
    const value = sessionStorage.getItem('tempData');
    const msg = value ? `sessionStorage 'tempData' 的值是: ${value}` : 'sessionStorage "tempData" 不存在。';
    alert(msg);
    document.getElementById('sessionStorageStatus').textContent = msg;
});

document.getElementById('removeSessionStorageBtn').addEventListener('click', () => {
    sessionStorage.removeItem('tempData');
    alert('sessionStorage "tempData" 已删除。');
    document.getElementById('sessionStorageStatus').textContent = 'sessionStorage "tempData" 已删除。';
});

<h4>Cookie, localStorage, sessionStorage 示例</h4>
<div style="background-color: #363636; padding: 25px; border-radius: 8px;">
    <h5 style="color: #a9dfd8;">Cookie 操作</h5>
    <div style="margin-bottom: 10px;">
        <button id="setCookieBtn" class="storage-btn">设置 Cookie</button>
        <button id="getCookieBtn" class="storage-btn">读取 Cookie</button>
        <button id="deleteCookieBtn" class="storage-btn">删除 Cookie</button>
    </div>
    <p id="cookieStatus" style="font-size: 0.9em; color: #d0d0d0;">Cookie 状态: 未操作</p>

    <h5 style="color: #a9dfd8; margin-top: 20px;">localStorage 操作</h5>
    <div style="margin-bottom: 10px;">
        <button id="setLocalStorageBtn" class="storage-btn">设置 localStorage</button>
        <button id="getLocalStorageBtn" class="storage-btn">读取 localStorage</button>
        <button id="removeLocalStorageBtn" class="storage-btn">删除 localStorage</button>
    </div>
    <p id="localStorageStatus" style="font-size: 0.9em; color: #d0d0d0;">localStorage 状态: 未操作</p>

    <h5 style="color: #a9dfd8; margin-top: 20px;">sessionStorage 操作</h5>
    <div style="margin-bottom: 10px;">
        <button id="setSessionStorageBtn" class="storage-btn">设置 sessionStorage</button>
        <button id="getSessionStorageBtn" class="storage-btn">读取 sessionStorage</button>
        <button id="removeSessionStorageBtn" class="storage-btn">删除 sessionStorage</button>
    </div>
    <p id="sessionStorageStatus" style="font-size: 0.9em; color: #d0d0d0;">sessionStorage 状态: 未操作</p>

    <p style="margin-top: 20px; font-size: 0.9em; color: #bbb;">
        <strong>安全提示:</strong>
        <ul>
            <li><strong>Secure</strong>: Cookie 只在 HTTPS 连接下发送。</li>
            <li><strong>HttpOnly</strong>: Cookie 不能通过 JavaScript 访问 (XSS 攻击防护)。此属性只能由服务器设置。</li>
            <li><strong>SameSite</strong>: 防止 CSRF 攻击。
                <ul>
                    <li><strong>Lax (默认)</strong>: 跨站请求中,只有顶层导航和GET请求会发送Cookie。</li>
                    <li><strong>Strict</strong>: 只有同站请求才会发送Cookie。</li>
                    <li><strong>None</strong>: 任何跨站请求都会发送Cookie (需同时设置 Secure)。</li>
                </ul>
            </li>
        </ul>
    </p>
</div>

6.2 设备 API

浏览器提供了访问用户设备硬件(如地理位置、摄像头)的能力,这为富客户端应用带来了可能。

6.2.1 地理位置(Geolocation API)

Geolocation API 允许网页获取用户的地理位置信息。这是一个隐私敏感的API,需要用户授权。

const getLocationBtn = document.getElementById('getLocationBtn');
const locationOutput = document.getElementById('locationOutput');

if (getLocationBtn && locationOutput) {
    getLocationBtn.addEventListener('click', () => {
        if (!navigator.geolocation) {
            locationOutput.textContent = '您的浏览器不支持地理位置 API。';
            return;
        }

        locationOutput.textContent = '正在获取位置信息...';
        getLocationBtn.disabled = true;

        // 获取当前位置
        navigator.geolocation.getCurrentPosition(
            (position) => {
                // 成功回调
                const latitude = position.coords.latitude;
                const longitude = position.coords.longitude;
                const accuracy = position.coords.accuracy; // 精度 (米)

                locationOutput.innerHTML = `
                    <p>纬度: <strong>${latitude.toFixed(6)}</strong></p>
                    <p>经度: <strong>${longitude.toFixed(6)}</strong></p>
                    <p>精度: <strong>±${accuracy.toFixed(2)} 米</strong></p>
                `;
                console.log('获取到地理位置:', position.coords);
                getLocationBtn.disabled = false;
            },
            (error) => {
                // 失败回调
                let errorMessage;
                switch(error.code) {
                    case error.PERMISSION_DENIED:
                        errorMessage = '用户拒绝了位置请求。';
                        break;
                    case error.POSITION_UNAVAILABLE:
                        errorMessage = '位置信息不可用。';
                        break;
                    case error.TIMEOUT:
                        errorMessage = '获取位置信息超时。';
                        break;
                    case error.UNKNOWN_ERROR:
                        errorMessage = '发生未知错误。';
                        break;
                    default:
                        errorMessage = '获取位置信息失败。';
                }
                locationOutput.textContent = `错误: ${errorMessage}`;
                console.error('获取地理位置错误:', error);
                getLocationBtn.disabled = false;
            },
            {
                // 可选参数
                enableHighAccuracy: true, // 尝试获取高精度位置
                timeout: 5000,            // 5秒后超时
                maximumAge: 0             // 不使用缓存位置,强制获取最新位置
            }
        );
    });
}

// 监听位置变化 (例如:用于实时导航)
const watchIdBtn = document.getElementById('watchLocationBtn');
const clearWatchBtn = document.getElementById('clearWatchBtn');
let watchId = null;

if (watchIdBtn && clearWatchBtn && locationOutput) {
    watchIdBtn.addEventListener('click', () => {
        if (!navigator.geolocation) {
            locationOutput.textContent = '您的浏览器不支持地理位置 API。';
            return;
        }
        if (watchId) {
            alert('已在监听位置变化。');
            return;
        }
        locationOutput.textContent = '正在持续监听位置变化...';
        watchIdBtn.disabled = true;
        clearWatchBtn.disabled = false;

        watchId = navigator.geolocation.watchPosition(
            (position) => {
                const latitude = position.coords.latitude;
                const longitude = position.coords.longitude;
                const accuracy = position.coords.accuracy;
                locationOutput.innerHTML = `
                    <p>持续监听 - 纬度: <strong>${latitude.toFixed(6)}</strong></p>
                    <p>持续监听 - 经度: <strong>${longitude.toFixed(6)}</strong></p>
                    <p>持续监听 - 精度: <strong>±${accuracy.toFixed(2)} 米</strong></p>
                    <p style="font-size: 0.8em;">(更新时间: ${new Date().toLocaleTimeString()})</p>
                `;
                console.log('位置更新:', position.coords);
            },
            (error) => {
                console.error('监听位置变化错误:', error);
                locationOutput.textContent = `监听错误: ${error.message}`;
                navigator.geolocation.clearWatch(watchId);
                watchId = null;
                watchIdBtn.disabled = false;
                clearWatchBtn.disabled = true;
            },
            { enableHighAccuracy: true, timeout: 10000, maximumAge: 0 }
        );
    });

    clearWatchBtn.addEventListener('click', () => {
        if (watchId !== null) {
            navigator.geolocation.clearWatch(watchId);
            watchId = null;
            locationOutput.textContent = '已停止监听位置变化。';
            console.log('已停止位置监听。');
            watchIdBtn.disabled = false;
            clearWatchBtn.disabled = true;
        }
    });
}

<h4>地理位置 API 示例</h4>
<div style="background-color: #363636; padding: 25px; border-radius: 8px;">
    <p>
        <button id="getLocationBtn" class="device-btn">获取当前位置</button>
    </p>
    <div id="locationOutput" style="border: 1px dashed #f8c291; padding: 15px; min-height: 80px; margin-top: 15px; background-color: #2b2b2b; color: #d0d0d0;">
        您的位置信息将在此显示...
    </div>

    <h5 style="color: #a9dfd8; margin-top: 25px;">持续监听位置变化</h5>
    <p>
        <button id="watchLocationBtn" class="device-btn">开始监听</button>
        <button id="clearWatchBtn" class="device-btn" disabled>停止监听</button>
    </p>
    <p style="margin-top: 10px; font-size: 0.9em; color: #bbb;">
        <strong>隐私提示:</strong> 
        地理位置 API 需要用户明确授权。请在浏览器弹出提示时允许访问。
        在非安全上下文 (HTTP) 中可能无法使用。
    </p>
</div>

<style>
    .device-btn {
        padding: 8px 15px;
        background-color: #61dafb;
        color: white;
        border: none;
        border-radius: 4px;
        cursor: pointer;
        margin-right: 10px;
        margin-bottom: 10px;
    }
    .device-btn:disabled {
        background-color: #555;
        cursor: not-allowed;
    }
</style>
6.2.2 摄像头访问(MediaDevices API)

MediaDevices API(navigator.mediaDevices)允许网页访问用户的摄像头和麦克风。同样,这需要用户授权。

const startCameraBtn = document.getElementById('startCameraBtn');
const stopCameraBtn = document.getElementById('stopCameraBtn');
const videoElement = document.getElementById('cameraFeed');
let mediaStream = null;

if (startCameraBtn && stopCameraBtn && videoElement) {
    startCameraBtn.addEventListener('click', async () => {
        try {
            // 获取用户媒体设备 (视频)
            mediaStream = await navigator.mediaDevices.getUserMedia({
                video: true,  // 请求视频流
                audio: false  // 不请求音频流
            });

            // 将视频流绑定到 video 元素
            videoElement.srcObject = mediaStream;
            videoElement.play(); // 播放视频

            startCameraBtn.disabled = true;
            stopCameraBtn.disabled = false;
            console.log('摄像头已启动。');
            document.getElementById('cameraStatus').textContent = '摄像头已启动。';
        } catch (err) {
            console.error('访问摄像头失败:', err);
            let errorMessage = '访问摄像头失败。';
            if (err.name === 'NotAllowedError' || err.name === 'PermissionDeniedError') {
                errorMessage = '用户拒绝了摄像头访问权限。';
            } else if (err.name === 'NotFoundError') {
                errorMessage = '未找到可用的摄像头设备。';
            } else if (err.name === 'NotReadableError') {
                errorMessage = '摄像头已被占用或无法访问。';
            }
            document.getElementById('cameraStatus').textContent = `错误: ${errorMessage}`;
            alert(`访问摄像头失败: ${errorMessage}`);
            startCameraBtn.disabled = false;
            stopCameraBtn.disabled = true;
        }
    });

    stopCameraBtn.addEventListener('click', () => {
        if (mediaStream) {
            // 停止所有轨道
            mediaStream.getTracks().forEach(track => track.stop());
            videoElement.srcObject = null;
            mediaStream = null;

            startCameraBtn.disabled = false;
            stopCameraBtn.disabled = true;
            console.log('摄像头已停止。');
            document.getElementById('cameraStatus').textContent = '摄像头已停止。';
        }
    });
}

<h4>摄像头访问 API 示例</h4>
<div style="background-color: #363636; padding: 25px; border-radius: 8px;">
    <div style="text-align: center; margin-bottom: 20px;">
        <video id="cameraFeed" autoplay playsinline style="width: 100%; max-width: 400px; height: auto; background-color: black; border: 2px solid #61dafb; border-radius: 8px;">
            您的浏览器不支持视频播放。
        </video>
        <p id="cameraStatus" style="color: #a9dfd8; margin-top: 10px; font-size: 0.9em;">点击“启动摄像头”开始。</p>
    </div>
    <div style="text-align: center;">
        <button id="startCameraBtn" class="device-btn">启动摄像头</button>
        <button id="stopCameraBtn" class="device-btn" disabled>停止摄像头</button>
    </div>
    <p style="margin-top: 20px; font-size: 0.9em; color: #bbb;">
        <strong>隐私提示:</strong>
        摄像头访问需要用户明确授权,并且必须在 <strong>安全上下文 (HTTPS)</strong> 中运行,
        否则 <code>navigator.mediaDevices</code> 会是 <code>undefined</code>。
    </p>
</div>

关键点:

6.2.3 屏幕方向(Screen Orientation API)

Screen Orientation API 允许网页获取屏幕的当前方向(portrait 或 landscape)以及锁定屏幕方向。

const orientationOutput = document.getElementById('orientationOutput');
const lockPortraitBtn = document.getElementById('lockPortraitBtn');
const unlockOrientationBtn = document.getElementById('unlockOrientationBtn');

// 更新屏幕方向信息
function updateOrientation() {
    if (screen.orientation && screen.orientation.type) {
        orientationOutput.innerHTML = `
            当前屏幕方向: <strong>${screen.orientation.type}</strong><br>
            角度: <strong>${screen.orientation.angle}</strong> 度
        `;
        console.log('屏幕方向:', screen.orientation.type, '角度:', screen.orientation.angle);
    } else {
        orientationOutput.textContent = '您的浏览器不支持 Screen Orientation API。';
    }
}

// 监听屏幕方向变化
if (screen.orientation) {
    screen.orientation.addEventListener('change', updateOrientation);
    // 首次加载时更新
    updateOrientation();
} else {
    orientationOutput.textContent = '您的浏览器不支持 Screen Orientation API。';
    if (lockPortraitBtn) lockPortraitBtn.disabled = true;
    if (unlockOrientationBtn) unlockOrientationBtn.disabled = true;
}


// 锁定屏幕方向
if (lockPortraitBtn) {
    lockPortraitBtn.addEventListener('click', async () => {
        if (!screen.orientation || !screen.orientation.lock) {
            alert('您的浏览器不支持锁定屏幕方向。');
            return;
        }
        try {
            await screen.orientation.lock('portrait-primary'); // 锁定为竖屏
            alert('屏幕已锁定为竖屏。');
            lockPortraitBtn.disabled = true;
            unlockOrientationBtn.disabled = false;
            updateOrientation();
        } catch (err) {
            console.error('锁定屏幕方向失败:', err);
            alert(`锁定屏幕方向失败: ${err.message}. 某些浏览器可能只在全屏模式下或PWA应用中允许锁定。`);
        }
    });
}

// 解锁屏幕方向
if (unlockOrientationBtn) {
    unlockOrientationBtn.addEventListener('click', () => {
        if (screen.orientation && screen.orientation.unlock) {
            screen.orientation.unlock(); // 解锁屏幕方向
            alert('屏幕已解锁。');
            lockPortraitBtn.disabled = false;
            unlockOrientationBtn.disabled = true;
            updateOrientation();
        }
    });
}

<h4>屏幕方向 API 示例</h4>
<div style="background-color: #363636; padding: 25px; border-radius: 8px;">
    <div id="orientationOutput" style="border: 1px dashed #a9dfd8; padding: 15px; min-height: 60px; margin-bottom: 20px; background-color: #2b2b2b; color: #d0d0d0;">
        屏幕方向信息将在此显示...
    </div>
    <div style="text-align: center;">
        <button id="lockPortraitBtn" class="device-btn">锁定竖屏</button>
        <button id="unlockOrientationBtn" class="device-btn" disabled>解锁屏幕</button>
    </div>
    <p style="margin-top: 20px; font-size: 0.9em; color: #bbb;">
        <strong>注意:</strong>
        锁定屏幕方向的功能通常在移动设备和全屏模式下才完全可用。
        在桌面浏览器上尝试锁定可能会失败。
    </p>
</div>

关键点:


第四部分:性能与工程化

7. 性能优化

前端性能优化是提升用户体验、降低跳出率的关键。它涉及到加载速度、渲染效率和内存管理。

7.1 加载优化

减少页面首次加载时间是性能优化的首要目标。

7.1.1 代码分割 (Code Splitting)

代码分割是将代码拆分为更小的块,然后按需加载。这在大型单页应用(SPA)中尤为重要,可以显著减少初始加载的JavaScript体积。

在现代前端框架 (如React, Vue, Angular) 和构建工具 (如Webpack, Rollup, Vite) 中,代码分割是开箱即用的特性,通常通过动态import()实现。

// main.js (主入口文件)
console.log('应用主模块已加载。');

const loadComponentBtn = document.getElementById('loadComponentBtn');
const componentArea = document.getElementById('componentArea');

if (loadComponentBtn && componentArea) {
    loadComponentBtn.addEventListener('click', async () => {
        try {
            console.log('正在动态加载组件...');
            
            const componentModuleContent = `
                export function renderComponent(container) {
                    container.innerHTML = \`
                        <div style="padding: 20px; background-color: #a9dfd8; color: #333; border-radius: 5px; margin-top: 15px;">
                            <h3>这是一个动态加载的组件!</h3>
                            <p>只有当你点击按钮时,这段代码才会被下载和执行。</p>
                        </div>
                    \`;
                    console.log('my-component.js 已执行。');
                }
            `;
            const blob = new Blob([componentModuleContent], { type: 'application/javascript' });
            const moduleUrl = URL.createObjectURL(blob);
            
            const { renderComponent } = await import(moduleUrl);
            renderComponent(componentArea);
            URL.revokeObjectURL(moduleUrl);

            loadComponentBtn.disabled = true;
            loadComponentBtn.textContent = '组件已加载';
            console.log('组件加载成功并渲染。');
        } catch (error) {
            console.error('加载组件失败:', error);
            componentArea.textContent = '加载组件失败。';
        }
    });
}

<h4>代码分割示例</h4>
<div style="background-color: #363636; padding: 25px; border-radius: 8px;">
    <p>
        <button id="loadComponentBtn" class="perf-btn">按需加载组件</button>
    </p>
    <div id="componentArea" style="border: 1px dashed #61dafb; padding: 15px; min-height: 100px; margin-top: 15px; background-color: #2b2b2b; color: #d0d0d0;">
        组件内容将在此处显示...
    </div>
    <p style="margin-top: 10px; font-size: 0.9em; color: #bbb;">
        <strong>注意:</strong>
        此示例通过 Blob URL 动态创建了一个模块来演示。
        在真实项目中,你会使用构建工具(如Webpack/Vite)的配置,
        在点击按钮前,打开浏览器的开发者工具的 <span class="highlight">Network</span> 
        面板,可以看到一个额外的JS文件被加载。
    </p>
</div>

<style>
    .perf-btn {
        padding: 8px 15px;
        background-color: #f8c291;
        color: #333;
        border: none;
        border-radius: 4px;
        cursor: pointer;
        margin-right: 10px;
        margin-bottom: 10px;
    }
    .perf-btn:disabled {
        background-color: #555;
        cursor: not-allowed;
    }
</style>

实现方式:

7.1.2 预加载策略(Preload, Prefetch)

预加载(Preload)和预获取(Prefetch)可以提前加载资源,从而加速未来的导航或交互。

<head>
    <!-- Preload: 告诉浏览器立即下载并缓存资源,因为它将在当前页面中很快被用到 -->
    <!-- 通常用于字体、关键CSS、JS,或者在用户交互后立即显示的重要图片 -->
    <link rel="preload" href="/fonts/myfont.woff2" as="font" type="font/woff2" crossorigin>
    <link rel="preload" href="/js/critical-bundle.js" as="script">
    <link rel="preload" href="/css/critical.css" as="style">

    <!-- Prefetch: 告诉浏览器在空闲时下载并缓存资源,它可能在未来的导航中被用到 -->
    <!-- 适用于用户可能访问的下一个页面、不重要的图片、非核心JS/CSS -->
    <link rel="prefetch" href="/next-page.html" as="document">
    <link rel="prefetch" href="/js/optional-feature.js" as="script">
    <link rel="prefetch" href="/images/large-hero-next-page.jpg" as="image">
</head>
<body>
    <h4>预加载策略示例</h4>
    <div style="background-color: #363636; padding: 25px; border-radius: 8px;">
        <p>
            请打开浏览器的开发者工具 <span class="highlight">Network</span> 
            (网络)面板,然后刷新页面,观察资源加载的优先级。<br>
            <code>preload</code> 资源的优先级通常很高,而 <code>prefetch</code> 较低,在浏览器空闲时加载。
        </p>
        <!-- 假设有一个下一页的链接 -->
        <a href="javascript:void(0);" onclick="alert('点击此链接时,如果浏览器空闲,下一页的资源可能已经被预获取!');"
           style="display: inline-block; padding: 10px 20px; background-color: #61dafb; color: white; border-radius: 5px; text-decoration: none;">
            模拟访问下一页
        </a>
    </div>
</body>

区别与应用场景:

特性 <link rel="preload"> <link rel="prefetch">
使用时机 当前页面很快会用到此资源 未来页面可能会用到此资源
加载优先级 高优先级,在关键渲染路径中 低优先级,浏览器空闲时加载
适用资源 关键字体、CSS、JS、背景图 下一个页面、不常用组件、图片
作用域 当前页面 跨页面

7.2 渲染优化

优化页面渲染性能,减少重绘和回流,提升用户界面的响应速度。

7.2.1 虚拟列表 (Virtual List)

虚拟列表(Virtual List)用于处理包含大量数据的长列表。它只渲染当前视口中可见的列表项,而不是全部渲染,从而显著减少DOM元素数量,提高滚动性能。

graph TD A[大量数据列表] --> B{渲染所有元素?} B -->|是| C[性能下降, 内存占用高] B -->|否| D[虚拟列表] D --> D1{计算可视区域内的元素索引} D1 --> D2{只渲染这些元素} D2 --> D3{滚动时动态更新渲染范围} D3 --> E[大幅提升性能, 降低内存]
// 虚拟列表实现原理
// 假设有一个很长的列表数据
const ALL_ITEMS = Array.from({ length: 10000 }, (_, i) => ({ id: i, text: `列表项 ${i + 1}` }));

const virtualListContainer = document.getElementById('virtualListContainer');
const virtualListContent = document.getElementById('virtualListContent');
const itemHeight = 30; // 每个列表项的固定高度 (真实项目中可能需要动态计算)
const bufferSize = 5; // 缓冲区域,多渲染一些元素,防止快速滚动出现空白

let startIndex = 0;
let endIndex = 0;

function renderVirtualList() {
    if (!virtualListContainer || !virtualListContent) return;

    // 计算当前视口可见的起始和结束索引
    const containerHeight = virtualListContainer.clientHeight;
    const scrollTop = virtualListContainer.scrollTop;

    startIndex = Math.max(0, Math.floor(scrollTop / itemHeight) - bufferSize);
    endIndex = Math.min(ALL_ITEMS.length - 1, Math.ceil((scrollTop + containerHeight) / itemHeight) + bufferSize);

    // 计算 translateY 偏移,保持滚动位置正确
    const translateY = startIndex * itemHeight;
    virtualListContent.style.transform = `translateY(${translateY}px)`;

    // 只渲染可见范围内的元素
    let visibleItemsHtml = '';
    for (let i = startIndex; i <= endIndex; i++) {
        const item = ALL_ITEMS[i];
        visibleItemsHtml += `<div class="virtual-list-item" style="height: ${itemHeight}px; line-height: ${itemHeight}px;">${item.text}</div>`;
    }
    virtualListContent.innerHTML = visibleItemsHtml;

    // 设置内容区的总高度,以便滚动条正常显示
    virtualListContainer.style.height = '300px'; // 确保容器有固定高度
    virtualListContent.parentElement.style.height = `${ALL_ITEMS.length * itemHeight}px`;
}

if (virtualListContainer) {
    // 监听滚动事件
    virtualListContainer.addEventListener('scroll', renderVirtualList);

    // 首次渲染
    renderVirtualList();
}

<h4>虚拟列表示例</h4>
<div style="background-color: #363636; padding: 25px; border-radius: 8px;">
    <p>下面的列表包含10000条数据,但只会渲染当前可见的极少数部分,以保证性能。</p>
    <div id="virtualListContainer" style="
        border: 1px solid #a9dfd8;
        height: 300px; /* 固定高度 */
        overflow-y: auto; /* 允许滚动 */
        position: relative; /* 确保子元素transform有效 */
        background-color: #2b2b2b;
    ">
        <div style="position: relative; overflow: hidden;">
            <div id="virtualListContent" style="
                position: absolute; /* 脱离文档流,只负责内容渲染 */
                top: 0;
                left: 0;
                right: 0;
                will-change: transform; /* 提升transform性能 */
            ">
                <!-- 列表项将在此动态生成 -->
            </div>
        </div>
    </div>
    <p style="margin-top: 10px; font-size: 0.9em; color: #bbb;">
        尝试快速滚动列表,观察DOM元素的数量变化和滚动流畅度。
        <br>
        在开发者工具的 <span class="highlight">Elements</span> 面板中观察 <code>#virtualListContent</code> 内部的子元素数量。
    </p>

<style>
    .virtual-list-item {
        padding: 0 10px;
        border-bottom: 1px solid #444;
        color: #d0d0d0;
        font-size: 0.95em;
        box-sizing: border-box;
    }
    .virtual-list-item:nth-child(even) {
        background-color: #333;
    }
</style>

核心思想:

  1. 固定高度: 假定每个列表项高度固定,便于计算。
  2. 计算可见范围: 根据滚动位置和容器高度,计算出当前应渲染的列表项的起始和结束索引。
  3. 动态渲染: 只渲染可见范围内的元素,并设置一个小的缓冲区。
  4. 占位元素: 通过设置一个与所有列表项总高度相同的占位元素(或通过transform: translateY()偏移内容区),使滚动条能够正确显示。

7.2.2 离屏渲染 (Offscreen Rendering)

离屏渲染是指在屏幕不可见的区域或使用OffscreenCanvas在Web Worker中进行渲染。这可以将复杂的图形计算或动画从主线程中剥离,提高主线程的响应性。

OffscreenCanvas 示例 (概念性)

// Main thread (UI)
const canvas = document.getElementById('myCanvas');
const context = canvas ? canvas.getContext('2d') : null;

if (canvas && context) {
    // 简单的主线程绘制
    context.fillStyle = 'red';
    context.fillRect(0, 0, 100, 100);

    document.getElementById('startOffscreenBtn').addEventListener('click', () => {
        if (!canvas.transferControlToOffscreen || !window.Worker) {
            alert('您的浏览器不支持 OffscreenCanvas 或 Web Workers。');
            return;
        }

        try {
            const workerJsContent = `
                let offscreenCanvas;
                let offscreenContext;
                let animationFrameId;
                let x = 0;
                let dx = 2;

                self.onmessage = function(e) {
                    if (e.data.canvas) {
                        offscreenCanvas = e.data.canvas;
                        offscreenContext = offscreenCanvas.getContext('2d');
                        offscreenCanvas.width = e.data.width;
                        offscreenCanvas.height = e.data.height;
                        startAnimation();
                    }
                };

                function startAnimation() {
                    function animateWorker() {
                        if (!offscreenContext) return;
                        offscreenContext.clearRect(0, 0, offscreenCanvas.width, offscreenCanvas.height);
                        offscreenContext.fillStyle = '#61dafb';
                        offscreenContext.fillRect(x, 50, 50, 50);
                        x += dx;
                        if (x + 50 > offscreenCanvas.width || x < 0) {
                            dx = -dx;
                        }
                        animationFrameId = offscreenCanvas.requestAnimationFrame(animateWorker);
                    }
                    animationFrameId = offscreenCanvas.requestAnimationFrame(animateWorker);
                }
            `;
            const blob = new Blob([workerJsContent], { type: 'application/javascript' });
            const workerUrl = URL.createObjectURL(blob);
            const offscreenWorker = new Worker(workerUrl);

            // 将主线程Canvas转移到Worker
            const offscreen = canvas.transferControlToOffscreen();

            // 发送OffscreenCanvas到Worker
            offscreenWorker.postMessage({ canvas: offscreen, width: canvas.width, height: canvas.height }, [offscreen]);

            console.log('OffscreenCanvas 已转移到 Worker 进行渲染。');
            document.getElementById('offscreenStatus').textContent = '复杂动画已转移到 Worker 线程。';
            document.getElementById('startOffscreenBtn').disabled = true;
            
            // 清理 Blob URL (worker启动后即可释放)
            URL.revokeObjectURL(workerUrl);
        } catch (error) {
            console.error('转移 OffscreenCanvas 失败:', error);
            document.getElementById('offscreenStatus').textContent = `错误: ${error.message}`;
        }
    });
}

<h4>离屏渲染 (OffscreenCanvas) 示例</h4>
<div style="background-color: #363636; padding: 25px; border-radius: 8px;">
    <p>OffscreenCanvas 允许将 Canvas 渲染从主线程转移到 Web Worker 线程。</p>
    <canvas id="myCanvas" width="400" height="150" style="border: 2px solid #a9dfd8; display: block; margin: 15px auto; background-color: #2b2b2b;">
        您的浏览器不支持 Canvas。
    </canvas>
    <div style="text-align: center; margin-top: 20px;">
        <button id="startOffscreenBtn" class="perf-btn">启动 OffscreenCanvas 动画</button>
    </div>
    <p id="offscreenStatus" style="margin-top: 10px; font-size: 0.9em; color: #d0d0d0; text-align: center;">
        主线程 Canvas 初始状态。
    </p>
    <p style="margin-top: 10px; font-size: 0.9em; color: #bbb;">
        <strong>注意:</strong>
        此功能需要浏览器支持 <span class="highlight">OffscreenCanvas</span> 和 <span class="highlight">Web Workers</span>。
        在点击按钮后,尝试操作页面,您会发现即使 Canvas 在动画,界面依然流畅。
    </p>
</div>

应用场景:

7.3 内存管理

内存泄漏是前端应用常见的问题,会导致页面性能下降甚至崩溃。了解JavaScript的垃圾回收机制和如何避免内存泄漏至关重要。

7.3.1 垃圾回收 (Garbage Collection)

JavaScript引擎的垃圾回收器会自动回收不再被引用的对象所占用的内存。常见的垃圾回收算法有:

1. 引用计数 (Reference Counting)

当一个对象被引用时,引用计数加1;当引用被移除时,引用计数减1。当引用计数为0时,对象被回收。

缺点: 无法解决循环引用问题。

// 循环引用示例 (在现代浏览器中通常会被标记清除算法解决,但理解概念很重要)
function createCircularReference() {
    let obj1 = {};
    let obj2 = {};

    obj1.prop = obj2;
    obj2.prop = obj1; // 循环引用

    // 此时,即使这两个对象从外部无法访问,如果仅依赖引用计数,它们也无法被回收。
    // 在现代JS引擎中,通常使用更高级的算法如标记清除。
    obj1 = null;
    obj2 = null; // 解除外部引用,理论上可以被回收
}
createCircularReference();
console.log('尝试创建一个循环引用。在现代JS引擎中,这种循环引用通常会被垃圾回收。');

2. 标记清除 (Mark-and-Sweep)

这是现代JavaScript引擎(如V8)中最常用的垃圾回收算法。它分为两个阶段:

标记阶段: 从一组“根”(如全局对象、当前函数调用栈中的变量)开始,遍历所有可达(reachable)的对象,并标记它们。

清除阶段: 遍历堆内存,清除所有未被标记(即不可达)的对象,并回收其占用的内存。

graph TD A[根 Root] --> B(对象B) A --> C(对象C) B --> D(对象D) C --> E(对象E) F(对象F - 不可达) subgraph 标记阶段 Mark Phase M1[从根开始] --> M2[标记所有可达对象] M2 --> B[对象B (已标记)] M2 --> C[对象C (已标记)] B --> D[对象D (已标记)] C --> E[对象E (已标记)] M2 --> F[对象F (未标记)] end subgraph 清除阶段 Sweep Phase S1[遍历整个堆] --> S2[删除所有未标记对象] S2 --> F[对象F (已回收)] S2 --> G[内存碎片] end

优势: 能有效解决循环引用问题。

7.3.2 内存泄漏检测与避免

内存泄漏是由于代码中对不再需要的对象存在持续引用,导致垃圾回收器无法回收其内存。

常见内存泄漏场景及避免:

1. 全局变量:

意外创建的全局变量不会被垃圾回收。

// 泄漏:忘记使用 var/let/const
function leakGlobal() {
    leakingVar = '我是一个泄漏的全局变量'; // 没有声明,会自动成为全局变量
}
leakGlobal();
console.log(window.leakingVar); // 可访问
// 避免:始终使用 var, let, const 声明变量
function noLeak() {
    const noLeakingVar = '我不会泄漏';
}
noLeak();
// console.log(window.noLeakingVar); // undefined
2. 未清除的定时器 (setInterval, setTimeout):

即使DOM元素被移除,如果定时器回调函数中引用了该元素,定时器会阻止该元素被回收。

let intervalId;
function startLeakingInterval() {
    const data = { count: 0, element: document.getElementById('leakyDiv') }; // 引用了DOM元素

    intervalId = setInterval(() => {
        if (data.element) {
            data.count++;
            data.element.textContent = `计数: ${data.count}`;
        } else {
            // 如果元素已经被移除,但定时器还在运行,data对象就无法被回收
            console.log('警告: 元素不存在,但定时器仍在运行!');
        }
    }, 1000);
    console.log('泄漏定时器已启动');
}

function stopLeakingInterval() {
    clearInterval(intervalId);
    console.log('泄漏定时器已停止。');
}

// 避免:在元素不再需要时清除定时器
let goodIntervalId;
function startGoodInterval() {
    const goodElement = document.getElementById('goodDiv');
    if (!goodElement) return;

    let count = 0;
    goodIntervalId = setInterval(() => {
        if (document.body.contains(goodElement)) { // 检查元素是否存在于DOM中
            count++;
            goodElement.textContent = `计数: ${count}`;
        } else {
            console.log('良好定时器: 元素已移除,清除定时器。');
            clearInterval(goodIntervalId);
        }
    }, 1000);
    console.log('良好定时器已启动');
}

function stopGoodInterval() {
    clearInterval(goodIntervalId);
    console.log('良好定时器已停止。');
}

document.getElementById('startLeakyIntervalBtn').addEventListener('click', startLeakingInterval);
document.getElementById('stopLeakyIntervalBtn').addEventListener('click', stopLeakingInterval);
document.getElementById('startGoodIntervalBtn').addEventListener('click', startGoodInterval);
document.getElementById('stopGoodIntervalBtn').addEventListener('click', stopGoodInterval);

document.getElementById('removeLeakyDivBtn').addEventListener('click', () => {
    const leakyDiv = document.getElementById('leakyDiv');
    if (leakyDiv) {
        leakyDiv.remove();
        console.log('leakyDiv 已从DOM中移除。');
    }
});

document.getElementById('removeGoodDivBtn').addEventListener('click', () => {
    const goodDiv = document.getElementById('goodDiv');
    if (goodDiv) {
        goodDiv.remove();
        console.log('goodDiv 已从DOM中移除。');
    }
});

<h4>内存泄漏示例: 未清除的定时器</h4>
<div style="background-color: #363636; padding: 25px; border-radius: 8px; margin-bottom: 20px;">
    <h5 style="color: #f8c291;">潜在泄漏示例 (Leaky Interval)</h5>
    <div id="leakyDiv" style="border: 1px solid #e74c3c; padding: 10px; margin-bottom: 10px; background-color: #2b2b2b; color: #d0d0d0;">
        Leaky Div Content
    </div>
    <button id="startLeakyIntervalBtn" class="perf-btn">启动泄漏定时器</button>
    <button id="stopLeakyIntervalBtn" class="perf-btn">停止泄漏定时器</button>
    <button id="removeLeakyDivBtn" class="perf-btn">移除 Leaky Div</button>
    <p style="font-size: 0.9em; color: #bbb; margin-top: 10px;">
        启动定时器,然后移除 Leaky Div。即使 Div 不在DOM中,定时器仍会运行,如果其回调引用了 Div,
        则 Div 及其相关对象无法被垃圾回收。
    </p>

    <h5 style="color: #a9dfd8; margin-top: 25px;">良好实践示例 (Good Interval)</h5>
    <div id="goodDiv" style="border: 1px solid #27ae60; padding: 10px; margin-bottom: 10px; background-color: #2b2b2b; color: #d0d0d0;">
        Good Div Content
    </div>
    <button id="startGoodIntervalBtn" class="perf-btn">启动良好定时器</button>
    <button id="stopGoodIntervalBtn" class="perf-btn">停止良好定时器</button>
    <button id="removeGoodDivBtn" class="perf-btn">移除 Good Div</button>
    <p style="font-size: 0.9em; color: #bbb; margin-top: 10px;">
        启动定时器,然后移除 Good Div。定时器会检测到元素已移除并自动停止。
    </p>
</div>
3. 未移除的事件监听器:

当DOM元素被移除,但其事件监听器未被移除时,回调函数中引用的对象会造成内存泄漏。

let eventHandlerDiv;
let dataCache = [];

function attachLeakyEventListener() {
    eventHandlerDiv = document.getElementById('eventHandlerDiv');
    if (!eventHandlerDiv) return;

    // 泄漏:匿名函数,无法被 removeEventListener 移除
    eventHandlerDiv.addEventListener('click', function leakyHandler() {
        console.log('泄漏点击事件触发。');
        dataCache.push({ timestamp: new Date() }); // 每次点击都添加数据,如果元素不移除,dataCache可能无限增长
    });
    console.log('泄漏事件监听器已添加。');
}

function removeEventHandlerDiv() {
    if (eventHandlerDiv) {
        eventHandlerDiv.remove();
        console.log('eventHandlerDiv 已从DOM中移除。');
        eventHandlerDiv = null; // 显式置空引用
    }
}

// 避免:使用命名函数或箭头函数引用,以便 removeEventListener
let goodEventHandlerDiv;
const myGoodHandler = function() {
    console.log('良好点击事件触发。');
    // 注意:这里的 dataCache 最好是局部的或通过WeakMap/WeakSet管理
};

function attachGoodEventListener() {
    goodEventHandlerDiv = document.getElementById('goodEventHandlerDiv');
    if (!goodEventHandlerDiv) return;

    goodEventHandlerDiv.addEventListener('click', myGoodHandler);
    console.log('良好事件监听器已添加。');
}

function detachGoodEventListener() {
    if (goodEventHandlerDiv) {
        goodEventHandlerDiv.removeEventListener('click', myGoodHandler);
        console.log('良好事件监听器已移除。');
    }
}

function removeGoodEventHandlerDiv() {
    if (goodEventHandlerDiv) {
        detachGoodEventListener(); // 先移除监听器
        goodEventHandlerDiv.remove();
        console.log('goodEventHandlerDiv 已从DOM中移除。');
        goodEventHandlerDiv = null;
    }
}

document.getElementById('attachLeakyListenerBtn').addEventListener('click', attachLeakyEventListener);
document.getElementById('removeLeakyHandlerDivBtn').addEventListener('click', removeEventHandlerDiv);

document.getElementById('attachGoodListenerBtn').addEventListener('click', attachGoodEventListener);
document.getElementById('detachGoodListenerBtn').addEventListener('click', detachGoodEventListener);
document.getElementById('removeGoodHandlerDivBtn').addEventListener('click', removeGoodEventHandlerDiv);

<h4>内存泄漏示例: 未移除的事件监听器</h4>
<div style="background-color: #363636; padding: 25px; border-radius: 8px;">
    <h5 style="color: #f8c291;">潜在泄漏示例 (Leaky Event Listener)</h5>
    <div id="eventHandlerDiv" style="border: 1px solid #e74c3c; padding: 20px; margin-bottom: 10px; background-color: #2b2b2b; color: #d0d0d0; cursor: pointer;">
        点击我 (泄漏事件)
    </div>
    <button id="attachLeakyListenerBtn" class="perf-btn">添加泄漏监听器</button>
    <button id="removeLeakyHandlerDivBtn" class="perf-btn">移除泄漏 Div</button>
    <p style="font-size: 0.9em; color: #bbb; margin-top: 10px;">
        点击“添加”,然后点击 Div 几次。再点击“移除 Div”。即使 Div 被移除,
        由于监听器是匿名函数,无法被移除,可能导致 Div 无法被回收。
    </p>

    <h5 style="color: #a9dfd8; margin-top: 25px;">良好实践示例 (Good Event Listener)</h5>
    <div id="goodEventHandlerDiv" style="border: 1px solid #27ae60; padding: 20px; margin-bottom: 10px; background-color: #2b2b2b; color: #d0d0d0; cursor: pointer;">
        点击我 (良好事件)
    </div>
    <button id="attachGoodListenerBtn" class="perf-btn">添加良好监听器</button>
    <button id="detachGoodListenerBtn" class="perf-btn">移除良好监听器</button>
    <button id="removeGoodHandlerDivBtn" class="perf-btn">移除良好 Div</button>
    <p style="font-size: 0.9em; color: #bbb; margin-top: 10px;">
        点击“添加”,然后点击 Div 几次。再点击“移除监听器”,最后“移除 Div”。
        所有引用都被正确清除,避免内存泄漏。
    </p>
</div>

避免策略:

8. 现代开发实践

现代前端开发不仅仅是写代码,还包括组件化、测试和构建优化,以提升开发效率和项目质量。

8.1 组件模式

组件化是现代前端开发的核心思想,它将UI拆分成独立、可复用的模块。

8.1.1 Web Components (自定义元素)

Web Components 是一套W3C标准,允许开发者创建可复用的自定义元素,它们具有封装性,可以在任何HTML环境中使用。

Web Components 包含以下四项主要技术:

  1. **Custom Elements(自定义元素):** 允许定义新的HTML标签。
  2. **Shadow DOM(影子DOM):** 提供了一种封装DOM和CSS的方法,使其与主文档的DOM和CSS隔离。
  3. **HTML Templates(HTML模板):** <template><slot>元素,用于声明HTML片段,可以被重复使用。
  4. **ES Modules(ES模块):** 提供模块化的导入和导出机制。
// 1. 定义一个自定义元素
class MyCustomButton extends HTMLElement {
    constructor() {
        super(); // 必须调用父类的构造函数
        // 创建 Shadow DOM
        this.attachShadow({ mode: 'open' }); // open表示可以从外部JS访问,closed则不能

        // 初始化属性
        this._text = this.getAttribute('text') || '默认按钮';
        this._type = this.getAttribute('type') || 'primary';
    }

    // 生命周期回调:当元素被添加到文档时调用
    connectedCallback() {
        this._render();
        // 添加事件监听器
        this.shadowRoot.querySelector('button').addEventListener('click', this._handleClick.bind(this));
        console.log('MyCustomButton connected to DOM.');
    }

    // 生命周期回调:当元素从文档中移除时调用
    disconnectedCallback() {
        // 清理事件监听器,避免内存泄漏
        this.shadowRoot.querySelector('button').removeEventListener('click', this._handleClick.bind(this));
        console.log('MyCustomButton disconnected from DOM.');
    }

    // 生命周期回调:当元素属性发生变化时调用
    static get observedAttributes() {
        return ['text', 'type']; // 声明要观察的属性
    }

    attributeChangedCallback(name, oldValue, newValue) {
        if (oldValue !== newValue) {
            this[`_${name}`] = newValue;
            this._render(); // 重新渲染以更新显示
        }
    }

    _handleClick() {
        alert(`自定义按钮点击: ${this._text} (类型: ${this._type})`);
        // 派发自定义事件
        this.dispatchEvent(new CustomEvent('buttonClick', {
            detail: { text: this._text, type: this._type },
            bubbles: true, // 允许事件冒泡
            composed: true // 允许事件穿透 Shadow DOM
        }));
    }

    _render() {
        this.shadowRoot.innerHTML = `
            <style>
                button {
                    padding: 10px 20px;
                    border: none;
                    border-radius: 5px;
                    cursor: pointer;
                    font-size: 1em;
                    transition: background-color 0.3s ease, transform 0.1s ease;
                    color: white;
                }
                button:hover {
                    transform: translateY(-2px);
                }
                button:active {
                    transform: translateY(0);
                }
                .primary {
                    background-color: #61dafb;
                }
                .primary:hover {
                    background-color: #00bfff;
                }
                .secondary {
                    background-color: #a9dfd8;
                    color: #333;
                }
                .secondary:hover {
                    background-color: #7fbfb7;
                }
                .danger {
                    background-color: #e74c3c;
                }
                .danger:hover {
                    background-color: #c0392b;
                }
            </style>
            <button class="${this._type}">${this._text}</button>
        `;
    }
}

// 2. 注册自定义元素
customElements.define('my-custom-button', MyCustomButton);

// 3. 使用 Slot
class MyCardComponent extends HTMLElement {
    constructor() {
        super();
        this.attachShadow({ mode: 'open' });
        this.shadowRoot.innerHTML = `
            <style>
                .card {
                    border: 1px solid #444;
                    border-radius: 8px;
                    padding: 20px;
                    margin: 10px;
                    background-color: #2b2b2b;
                    box-shadow: 0 2px 10px rgba(0,0,0,0.3);
                    color: #d0d0d0;
                }
                ::slotted(h3) { /* 样式化slot内容中的h3 */
                    color: #61dafb;
                    border-bottom: 1px dashed #444;
                    padding-bottom: 5px;
                    margin-top: 0;
                }
                ::slotted(p) {
                    margin-bottom: 0;
                }
            </style>
            <div class="card">
                <slot name="card-header"><h3>默认标题</h3></slot>
                <slot><p>默认内容。</p></slot>
                <slot name="card-footer"></slot>
            </div>
        `;
    }
}
customElements.define('my-card', MyCardComponent);

// 监听自定义元素的事件
document.getElementById('webComponentSection').addEventListener('buttonClick', (e) => {
    console.log('从 Shadow DOM 冒泡出来的自定义事件:', e.detail);
    document.getElementById('webComponentEventLog').textContent =
        `收到自定义按钮事件: ${e.detail.text} (${e.detail.type})`;
});

// 动态修改属性
document.getElementById('changeButtonTextBtn').addEventListener('click', () => {
    const btn = document.querySelector('my-custom-button');
    if (btn) {
        btn.setAttribute('text', `改变后的文本 ${Math.floor(Math.random() * 100)}`);
        btn.setAttribute('type', 'danger');
    }
});

<h4>Web Components (自定义元素) 示例</h4>
<div id="webComponentSection" style="background-color: #363636; padding: 25px; border-radius: 8px;">
    <p>
        <my-custom-button text="点击我!" type="primary"></my-custom-button>
        <my-custom-button text="次要按钮" type="secondary"></my-custom-button>
    </p>
    <button id="changeButtonTextBtn" class="dev-btn">改变第一个按钮的文本和类型</button>
    <p id="webComponentEventLog" style="margin-top: 15px; font-size: 0.9em; color: #d0d0d0;">
        自定义按钮事件日志: 无
    </p>

    <h5 style="color: #a9dfd8; margin-top: 25px;">使用 Slot 的自定义卡片组件</h5>
    <my-card>
        <h3 slot="card-header">我的自定义卡片标题</h3>
        <p>这是卡片的主体内容。它将通过匿名 slot 渲染。</p>
        <p slot="card-footer">这是卡片底部的页脚内容。</p>
    </my-card>

    <my-card>
        <h3>没有具名Slot的卡片标题</h3>
        <p>如果内容没有 <code>slot</code> 属性,它会进入匿名 slot。</p>
    </my-card>
</div>

<style>
    .dev-btn {
        padding: 8px 15px;
        background-color: #f8c291;
        color: #333;
        border: none;
        border-radius: 4px;
        cursor: pointer;
        margin-right: 10px;
        margin-bottom: 10px;
    }
</style>

优势:

8.2 测试策略

测试是保证代码质量和减少Bug的关键环节。前端测试通常分为单元测试、集成测试和端到端测试。

测试金字塔:

graph TD A[UI层] -->|E2E 测试 Cypress| C B[服务层/组件层] -->|集成测试| C D[单元层 函数/模块] -->|单元测试 Jest| C subgraph 测试金字塔 C1(小) --> DTest[单元测试] C2(中) --> BTest[集成测试] C3(大) --> ATest[E2E 测试] end ATest -->|慢, 贵, 脆弱| Final BTest -->|中等速度, 适中成本| Final DTest -->|快, 便宜, 稳定| Final

金字塔底部是单元测试,数量最多,执行速度最快,成本最低。向上是集成测试和端到端测试,数量逐渐减少,成本和执行时间增加。

8.2.1 Jest 单元测试

Jest 是一个流行的JavaScript测试框架,由Facebook开发,通常用于React项目,但也适用于任何JavaScript项目。

// sum.js (待测试的函数)
export function sum(a, b) {
    return a + b;
}

export function subtract(a, b) {
    return a - b;
}

// sum.test.js (测试文件)
/*
// 假设这是 Jest 测试文件
import { sum, subtract } from './sum'; // 导入待测试的模块

describe('sum function', () => {
    test('adds 1 + 2 to equal 3', () => {
        expect(sum(1, 2)).toBe(3);
    });

    test('adds negative numbers correctly', () => {
        expect(sum(-1, -5)).toBe(-6);
    });

    test('adds zero correctly', () => {
        expect(sum(0, 0)).toBe(0);
    });
});

describe('subtract function', () => {
    test('subtracts 5 - 3 to equal 2', () => {
        expect(subtract(5, 3)).toBe(2);
    });

    test('subtracts negative numbers correctly', () => {
        expect(subtract(-5, -3)).toBe(-2);
    });
});

// Mocking 示例:模拟网络请求
const fetchData = async () => {
    const response = await fetch('https://jsonplaceholder.typicode.com/todos/1');
    const data = await response.json();
    return data;
};

describe('fetchData', () => {
    test('should fetch data successfully', async () => {
        // 模拟 fetch API
        global.fetch = jest.fn(() =>
            Promise.resolve({
                json: () => Promise.resolve({ userId: 1, id: 1, title: 'delectus aut autem', completed: false }),
            })
        );

        const data = await fetchData();
        expect(data.title).toBe('delectus aut autem');
        expect(fetch).toHaveBeenCalledTimes(1);
    });
});
*/

<h4>Jest 单元测试示例 (概念性)</h4>
<div style="background-color: #363636; padding: 25px; border-radius: 8px;">
    <p>
        Jest 是一个流行的 JavaScript 测试框架,非常适合进行单元测试。
        此示例无法在浏览器中直接运行,因为它需要 Node.js 环境和 Jest 包。
    </p>
    <p><strong>安装和运行:</strong></p>
    <pre><code class="language-bash">
npm install --save-dev jest
# 在 package.json 中添加 script
# "test": "jest"
npm test
    </code></pre>

    <p><strong>sum.js:</strong></p>
    <pre><code class="language-javascript">
export function sum(a, b) {
    return a + b;
}

export function subtract(a, b) {
    return a - b;
}
    </code></pre>

    <p><strong>sum.test.js:</strong></p>
    <pre><code class="language-javascript">
import { sum, subtract } from './sum';

describe('sum function', () => {
    test('adds 1 + 2 to equal 3', () => {
        expect(sum(1, 2)).toBe(3);
    });
    test('adds negative numbers correctly', () => {
        expect(sum(-1, -5)).toBe(-6);
    });
});

describe('subtract function', () => {
    test('subtracts 5 - 3 to equal 2', () => {
        expect(subtract(5, 3)).toBe(2);
    });
});
    </code></pre>
</div>

Jest 核心概念:

8.2.2 Cypress E2E 测试

Cypress 是一款现代化的端到端(E2E)测试框架,专注于提供卓越的开发者体验。

特点:

// cypress/e2e/spec.cy.js (Cypress 测试文件)
/*
// 假设这是一个 Cypress E2E 测试文件

describe('My First E2E Test', () => {
    it('Visits the homepage and checks content', () => {
        // 访问指定URL
        cy.visit('http://localhost:3000'); // 替换为你的应用地址

        // 检查页面标题
        cy.title().should('include', '前端开发');

        // 检查是否存在某个H1标题
        cy.get('h1').should('contain', '前端开发常见代码片段');

        // 查找一个按钮并点击它
        cy.get('#loadComponentBtn').click();

        // 检查点击后新出现的元素内容
        cy.get('#componentArea div h3').should('contain', '这是一个动态加载的组件!');
    });

    it('Submits a form with valid data', () => {
        cy.visit('http://localhost:3000'); // 假设表单在首页

        // 填写表单
        cy.get('#regUsername').type('testuser123');
        cy.get('#regEmail').type('[email protected]');
        cy.get('#regPassword').type('Password123');
        cy.get('#confirmPassword').type('Password123');

        // 提交表单
        cy.get('button[type="submit"]').click();

        // 验证提示信息
        cy.on('window:alert', (str) => {
            expect(str).to.equal('表单验证成功!可以提交数据。');
        });
    });

    it('Handles a failed form submission', () => {
        cy.visit('http://localhost:3000');

        // 填写无效数据
        cy.get('#regUsername').type('a'); // 少于3位
        cy.get('#regEmail').type('invalid-email'); // 无效邮箱
        cy.get('button[type="submit"]').click();

        // 检查错误信息是否显示
        cy.get('#regUsername + .error-message').should('not.be.empty');
        cy.get('#regEmail + .error-message').should('not.be.empty');
    });
});
*/

<h4>Cypress E2E 测试示例 (概念性)</h4>
<div style="background-color: #363636; padding: 25px; border-radius: 8px;">
    <p>
        Cypress 是一款用于端到端(E2E)测试的强大工具,它允许您模拟用户在真实浏览器中的操作。
        此示例代码无法在浏览器中直接运行,需要安装 Cypress。
    </p>
    <p><strong>安装和运行:</strong></p>
    <pre><code class="language-bash">
npm install --save-dev cypress
npx cypress open # 启动 Cypress UI
    </code></pre>

    <p><strong>cypress/e2e/spec.cy.js:</strong></p>
    <pre><code class="language-javascript">
describe('My First E2E Test', () => {
    it('Visits the homepage and checks content', () => {
        cy.visit('http://localhost:3000'); // 替换为你的应用地址
        cy.title().should('include', '前端开发');
        cy.get('h1').should('contain', '前端开发常见代码片段');
        cy.get('#loadComponentBtn').click();
        cy.get('#componentArea div h3').should('contain', '这是一个动态加载的组件!');
    });

    it('Submits a form with valid data', () => {
        cy.visit('http://localhost:3000');
        cy.get('#regUsername').type('testuser123');
        cy.get('#regEmail').type('[email protected]');
        cy.get('#regPassword').type('Password123');
        cy.get('#confirmPassword').type('Password123');
        cy.get('button[type="submit"]').click();
        cy.on('window:alert', (str) => {
            expect(str).to.equal('表单验证成功!可以提交数据。');
        });
    });
});
    </code></pre>
</div>

Cypress 最佳实践:

8.3 构建优化

构建优化是指在项目打包部署前,通过各种工具和技术对代码进行处理,以减小体积、提升运行效率。

8.3.1 Tree Shaking

Tree Shaking 是一种通过移除JavaScript死代码(Dead Code)来优化打包体积的技术。它依赖于ES Modules的静态结构分析。

graph LR A[项目代码] --> B{导入模块} B --> C(模块A.js) B --> D(模块B.js) C --> C1[导出函数1] C --> C2[导出函数2] D --> D1[导出函数3] subgraph Tree Shaking 过程 Webpack/Rollup/Vite TS1[分析代码依赖] --> TS2[标记“死代码” 未被使用的导出] TS2 --> TS3[移除死代码] end C1 -->|在项目中被使用| G[最终打包] C2 -->|未被使用| TS2 D1 -->|在项目中被使用| G style TS2 fill:#f8c291,stroke:#333,stroke-width:2px; style TS3 fill:#a9dfd8,stroke:#333,stroke-width:2px;

例如,如果一个模块导出了10个函数,但你的项目只使用了其中1个,Tree Shaking会移除另外9个未使用的函数。


<h4>Tree Shaking 示例 (概念性)</h4>
<div style="background-color: #363636; padding: 25px; border-radius: 8px;">
    <p>
        Tree Shaking 是一种优化技术,用于消除 JavaScript 代码中的“死代码”(未被使用的代码)。
        它主要依赖于 ES Modules 的静态导入/导出特性。
    </p>
    <p>此示例无法直接在浏览器中运行,因为它需要一个构建工具(如 Webpack, Rollup, Vite)来执行 Tree Shaking。</p>

    <p><strong>utils.js:</strong></p>
    <pre><code class="language-javascript">
export function calculateSum(a, b) {
    console.log('calculateSum被调用');
    return a + b;
}

export function calculateProduct(a, b) {
    console.log('calculateProduct被调用');
    return a * b;
}

export function complexUtilityFunction() {
    // 假设这是一个非常大的,但未被使用的函数
    console.log('complexUtilityFunction (未被使用) 被调用');
    // ... 大量代码 ...
    return 'complex result';
}
    </code></pre>

    <p><strong>app.js (只导入 calculateSum):</strong></p>
    <pre><code class="language-javascript">
import { calculateSum } from './utils';

const result = calculateSum(5, 10);
console.log(\`Sum: \${result}\`);

// calculateProduct 和 complexUtilityFunction 在打包时会被移除,因为它们没有被使用。
    </code></pre>
    <p style="margin-top: 10px; font-size: 0.9em; color: #bbb;">
        通过这种方式,您可以保持模块的职责清晰,同时确保最终的打包文件尽可能小。
    </p>
</div>

实现条件:

8.3.2 持久化缓存 (Long-term Caching)

持久化缓存利用浏览器缓存机制,让用户在再次访问时加载更快。通过给静态资源文件名添加内容哈希(Content Hash)实现。

graph TD A[源文件 (e.g., app.js)] --> B{构建工具生成哈希} B --> C[生成文件名 (e.g., app.c0a8b9f.js)] C --> D{部署到CDN/服务器} D -->|首次访问| E[浏览器下载并缓存] D -->|再次访问 (文件未变| F[浏览器直接从缓存加载] D -->|文件内容改变| G[哈希变化 -> 生成新文件名 -> 浏览器下载新文件]

核心思想: 当文件内容不变时,文件名不变,浏览器可以永远从缓存中读取。当文件内容变化时,文件名哈希也会变化,强制浏览器下载新版本。


<h4>持久化缓存示例 (概念性)</h4>
<div style="background-color: #363636; padding: 25px; border-radius: 8px;">
    <p>
        持久化缓存通过给静态资源文件名添加内容哈希(如 <code>app.c0a8b9f.js</code>)来实现。
        这样,当文件内容不变时,浏览器可以长时间缓存文件;当内容变化时,哈希改变,
        浏览器会下载新文件。
    </p>
    <p><strong>构建工具如何实现:</strong></p>
    <p>在 Webpack 配置中,可以使用 <code>[contenthash]</code> 占位符:</p>
    <pre><code class="language-javascript">
// webpack.config.js
module.exports = {
    output: {
        filename: '[name].[contenthash].js', // JS文件
        chunkFilename: '[name].[contenthash].js', // 异步加载的chunk
        assetModuleFilename: 'assets/[name].[contenthash][ext]' // 静态资源
    },
    // ... 其他配置
};
    </code></pre>

    <p><strong>HTML引用示例:</strong></p>
    <pre><code class="language-html">
&lt;link rel="stylesheet" href="/css/main.0a1b2c3d.css">
&lt;script src="/js/app.f4e5d6c7.js">&lt;/script>
&lt;img src="/images/logo.89abcedf.png" alt="Logo">
    </code></pre>

    <p><strong>Nginx 服务器缓存配置示例:</strong></p>
    <pre><code class="language-nginx">
location ~* \.(js|css|png|jpg|jpeg|gif|ico|woff2|ttf|eot|svg)$ {
    expires 365d; # 缓存一年
    add_header Cache-Control "public, immutable"; # 告诉浏览器文件内容永不改变
}

location / {
    # 对于不带哈希的入口文件 (如 index.html),设置较短的缓存或不缓存,以获取最新版本
    expires 1h;
    add_header Cache-Control "public, must-revalidate";
}
    </code></pre>
    <p style="margin-top: 10px; font-size: 0.9em; color: #bbb;">
        这种策略对于提升回访用户体验至关重要,因为他们大部分静态资源可以直接从本地缓存中加载。
    </p>
</div>

实现步骤:

  1. 构建工具配置: 使用Webpack、Rollup、Vite等构建工具,配置输出文件名包含[contenthash]
  2. HTML引用: 确保HTML文件(或由构建工具生成的HTML文件)正确引用了这些带哈希的文件。
  3. 服务器配置: 配置HTTP服务器(如Nginx、Apache)对带哈希的静态资源设置长效缓存策略(如Cache-Control: public, max-age=31536000, immutable)。
  4. 入口文件缓存: 对于不带哈希的入口HTML文件(如index.html),通常设置较短的缓存时间或no-cache,以确保用户总是能获取到最新的资源引用。


第五部分:实战案例库

9. 常见组件实现

这一部分将展示一些常见UI和功能组件的实现,帮助您理解其工作原理。

9.1 UI 组件

9.1.1 轮播图(支持触摸)

一个支持桌面鼠标拖拽和移动端触摸滑动的简单轮播图实现。

const carousel = document.getElementById('simpleCarousel');
const carouselInner = carousel ? carousel.querySelector('.carousel-inner') : null;
const slides = carouselInner ? carouselInner.querySelectorAll('.carousel-item') : [];
const prevBtn = carousel ? carousel.querySelector('.carousel-prev') : null;
const nextBtn = carousel ? carousel.querySelector('.carousel-next') : null;
const dotsContainer = carousel ? carousel.querySelector('.carousel-dots') : null;

let currentIndex = 0;
let startX = 0;
let deltaX = 0;
let isDragging = false;
let autoPlayInterval;

if (carousel && carouselInner && slides.length > 0) {
    // 创建导航圆点
    slides.forEach((_, index) => {
        const dot = document.createElement('span');
        dot.classList.add('dot');
        if (index === 0) dot.classList.add('active');
        dot.addEventListener('click', () => goToSlide(index));
        dotsContainer.appendChild(dot);
    });
    const dots = dotsContainer.querySelectorAll('.dot');

    // 更新轮播图显示
    function updateCarousel() {
        carouselInner.style.transform = `translateX(-${currentIndex * 100}%)`;
        dots.forEach((dot, index) => {
            dot.classList.toggle('active', index === currentIndex);
        });
    }

    // 跳转到指定幻灯片
    function goToSlide(index) {
        currentIndex = (index + slides.length) % slides.length; // 确保索引在有效范围内
        updateCarousel();
    }

    // 下一张
    function nextSlide() {
        goToSlide(currentIndex + 1);
    }

    // 上一张
    function prevSlide() {
        goToSlide(currentIndex - 1);
    }
    
    // 自动播放
    function startAutoPlay() {
        stopAutoPlay();
        autoPlayInterval = setInterval(nextSlide, 3000);
    }
    
    function stopAutoPlay() {
        clearInterval(autoPlayInterval);
    }

    // 鼠标/触摸事件监听
    carouselInner.addEventListener('mousedown', startDrag);
    carouselInner.addEventListener('touchstart', startDrag, { passive: true });

    carouselInner.addEventListener('mousemove', drag);
    carouselInner.addEventListener('touchmove', drag, { passive: true });

    carouselInner.addEventListener('mouseup', endDrag);
    carouselInner.addEventListener('touchend', endDrag);
    carouselInner.addEventListener('mouseleave', endDrag);

    function startDrag(e) {
        stopAutoPlay();
        isDragging = true;
        startX = e.type.includes('mouse') ? e.clientX : e.touches[0].clientX;
        carouselInner.style.transition = 'none';
    }

    function drag(e) {
        if (!isDragging) return;
        const currentX = e.type.includes('mouse') ? e.clientX : e.touches[0].clientX;
        deltaX = currentX - startX;
        carouselInner.style.transform = `translateX(calc(-${currentIndex * 100}% + ${deltaX}px))`;
    }

    function endDrag() {
        if (!isDragging) return;
        isDragging = false;
        carouselInner.style.transition = 'transform 0.5s ease-in-out';

        if (Math.abs(deltaX) > carouselInner.offsetWidth / 4) {
            if (deltaX > 0) {
                prevSlide();
            } else {
                nextSlide();
            }
        } else {
            updateCarousel();
        }
        deltaX = 0;
        startAutoPlay();
    }

    // 导航按钮
    if (prevBtn) prevBtn.addEventListener('click', () => { stopAutoPlay(); prevSlide(); startAutoPlay(); });
    if (nextBtn) nextBtn.addEventListener('click', () => { stopAutoPlay(); nextSlide(); startAutoPlay(); });

    // 鼠标移入暂停自动播放
    carousel.addEventListener('mouseenter', stopAutoPlay);
    carousel.addEventListener('mouseleave', startAutoPlay);

    updateCarousel();
    startAutoPlay();
}

<h4>轮播图(支持触摸)示例</h4>
<div id="simpleCarousel" class="carousel-container">
    <div class="carousel-inner">
        <div class="carousel-item" style="background-color: #61dafb;">Slide 1</div>
        <div class="carousel-item" style="background-color: #a9dfd8;">Slide 2</div>
        <div class="carousel-item" style="background-color: #f8c291;">Slide 3</div>
        <div class="carousel-item" style="background-color: #e74c3c;">Slide 4</div>
    </div>
    <button class="carousel-prev">&#10094;</button>
    <button class="carousel-next">&#10095;</button>
    <div class="carousel-dots"></div>
</div>

<style>
    .carousel-container {
        position: relative;
        width: 100%;
        max-width: 600px;
        margin: 20px auto;
        overflow: hidden;
        border-radius: 8px;
        box-shadow: 0 5px 15px rgba(0,0,0,0.3);
        background-color: #333;
        user-select: none; /* 防止拖拽时选中文字 */
    }
    .carousel-inner {
        display: flex;
        transition: transform 0.5s ease-in-out;
        width: 100%; /* Important for percentage transform */
    }
    .carousel-item {
        min-width: 100%; /* Each slide takes full width */
        height: 250px;
        display: flex;
        justify-content: center;
        align-items: center;
        font-size: 2.5em;
        color: #333;
        flex-shrink: 0; /* Prevent items from shrinking */
    }
    .carousel-prev, .carousel-next {
        position: absolute;
        top: 50%;
        transform: translateY(-50%);
        background-color: rgba(0,0,0,0.5);
        color: white;
        border: none;
        padding: 10px 15px;
        cursor: pointer;
        font-size: 1.5em;
        z-index: 10;
        border-radius: 50%;
        width: 40px;
        height: 40px;
        display: flex;
        justify-content: center;
        align-items: center;
    }
    .carousel-prev { left: 10px; }
    .carousel-next { right: 10px; }
    .carousel-prev:hover, .carousel-next:hover {
        background-color: rgba(0,0,0,0.7);
    }
    .carousel-dots {
        position: absolute;
        bottom: 10px;
        left: 50%;
        transform: translateX(-50%);
        display: flex;
        gap: 8px;
    }
    .carousel-dots .dot {
        width: 12px;
        height: 12px;
        background-color: rgba(255,255,255,0.5);
        border-radius: 50%;
        cursor: pointer;
        transition: background-color 0.3s ease;
    }
    .carousel-dots .dot.active {
        background-color: #61dafb;
    }
</style>

核心实现:

  1. Flexbox布局: .carousel-inner使用display: flex;使所有幻灯片并排,.carousel-item设置min-width: 100%;确保每张幻灯片占据整个视口宽度。
  2. transform: translateX() 通过改变.carousel-innertranslateX值来切换幻灯片。
  3. 触摸事件:
    • touchstart/mousedown:记录起始X坐标,设置isDragging = true,并移除过渡动画。
    • touchmove/mousemove:计算拖动距离deltaX,并实时更新translateX
    • touchend/mouseup:判断deltaX是否超过阈值来决定是否切换幻灯片,然后恢复过渡动画。
  4. 导航点和箭头: 提供视觉指示和手动控制。
  5. 自动播放: 使用setInterval实现自动切换,并通过mouseenter/mouseleave暂停/恢复。

9.1.2 无限滚动列表

无限滚动(或懒加载列表)在用户滚动到底部时自动加载更多内容,常见于社交媒体、新闻流等。

// 模拟后端API,获取更多数据
let currentDataPage = 0;
const PAGE_SIZE = 20; // 每页加载的数量
const TOTAL_ITEMS = 500; // 假设总共有500条数据

function fetchMoreData(page) {
    return new Promise(resolve => {
        setTimeout(() => { // 模拟网络延迟
            const start = page * PAGE_SIZE;
            const end = Math.min(start + PAGE_SIZE, TOTAL_ITEMS);
            const newData = [];
            for (let i = start; i < end; i++) {
                newData.push(`新加载的列表项 ${i + 1}`);
            }
            console.log(`加载了第 ${page + 1} 页数据 (${newData.length} 条)`);
            resolve(newData);
        }, 500);
    });
}

const infiniteList = document.getElementById('infiniteList');
const loadingIndicator = document.getElementById('loadingIndicator');
let isLoading = false;
let hasMore = true;

// 渲染列表项
function appendItems(items) {
    items.forEach(text => {
        const li = document.createElement('li');
        li.textContent = text;
        infiniteList.appendChild(li);
    });
}

// 加载更多数据
async function loadMore() {
    if (isLoading || !hasMore) return;

    isLoading = true;
    if (loadingIndicator) loadingIndicator.style.display = 'block';

    try {
        const newItems = await fetchMoreData(currentDataPage);
        if (newItems.length > 0) {
            appendItems(newItems);
            currentDataPage++;
        } else {
            hasMore = false; // 没有更多数据了
            console.log('没有更多数据了。');
            if (loadingIndicator) loadingIndicator.textContent = '没有更多数据了。';
        }
    } catch (error) {
        console.error('加载数据失败:', error);
        if (loadingIndicator) loadingIndicator.textContent = '加载失败。';
    } finally {
        isLoading = false;
        if (!hasMore && loadingIndicator) {
             loadingIndicator.style.display = 'block'; // 让“没有更多数据”一直显示
        } else if (loadingIndicator) {
            loadingIndicator.style.display = 'none'; // 隐藏加载指示器
        }
    }
}

// 监听滚动事件
if (infiniteList) {
    infiniteList.addEventListener('scroll', () => {
        // 判断是否滚动到底部
        const { scrollTop, scrollHeight, clientHeight } = infiniteList;
        if (scrollTop + clientHeight >= scrollHeight - 5 && !isLoading && hasMore) { // 留5px缓冲
            loadMore();
        }
    });

    // 初始加载第一页数据
    loadMore();
}

<h4>无限滚动列表示例</h4>
<div style="background-color: #363636; padding: 25px; border-radius: 8px;">
    <p>这是一个模拟无限滚动的列表。向下滚动以加载更多内容。</p>
    <ul id="infiniteList" style="
        height: 300px; /* 固定高度,以便滚动 */
        overflow-y: auto; /* 允许滚动 */
        list-style: none;
        padding: 0;
        margin: 0;
        border: 1px solid #a9dfd8;
        background-color: #2b2b2b;
        color: #d0d0d0;
        border-radius: 4px;
    ">
        <!-- 列表项将在此处动态加载 -->
    </ul>
    <div id="loadingIndicator" style="
        text-align: center;
        padding: 10px;
        font-size: 0.9em;
        color: #f8c291;
        display: none; /* 默认隐藏 */
    ">
        加载中...
    </div>
    <p style="margin-top: 10px; font-size: 0.9em; color: #bbb;">
        在开发者工具的 <span class="highlight">Network</span> 面板中,观察何时会发出模拟的“加载更多”请求。
    </p>
</div>

<style>
    #infiniteList li {
        padding: 10px 15px;
        border-bottom: 1px solid #444;
    }
    #infiniteList li:last-child {
        border-bottom: none;
    }
    #infiniteList li:nth-child(even) {
        background-color: #333;
    }
</style>

核心思路:

  1. 滚动监听: 监听容器的scroll事件。
  2. 判断触底:scrollTop + clientHeight >= scrollHeight - threshold时,认为用户滚动到底部(threshold为一个小的缓冲值)。
  3. 异步加载: 调用异步函数(通常是Fetch API请求后端)获取更多数据。
  4. 状态管理: 使用isLoadinghasMore变量防止重复加载和判断是否还有更多数据。
  5. 加载指示器: 显示加载动画或文本,提升用户体验。

9.1.3 可排序表格

实现一个可以在前端进行排序的表格,点击表头即可按升序或降序排序数据。

const sortableTable = document.getElementById('sortableTable');
const tableBody = sortableTable ? sortableTable.querySelector('tbody') : null;
const tableHeaders = sortableTable ? sortableTable.querySelectorAll('th[data-sortable]') : [];

let tableData = [
    { id: 1, name: 'Alice', age: 30, city: 'New York' },
    { id: 2, name: 'Bob', age: 24, city: 'London' },
    { id: 3, name: 'Charlie', age: 35, city: 'Paris' },
    { id: 4, name: 'David', age: 28, city: 'Tokyo' },
    { id: 5, name: 'Eve', age: 22, city: 'Berlin' },
];

let currentSortColumn = null;
let currentSortDirection = 'asc'; // 'asc' or 'desc'

// 渲染表格
function renderTable() {
    if (!tableBody) return;
    tableBody.innerHTML = ''; // 清空现有内容

    tableData.forEach(rowData => {
        const row = document.createElement('tr');
        row.innerHTML = `
            <td>${rowData.id}</td>
            <td>${rowData.name}</td>
            <td>${rowData.age}</td>
            <td>${rowData.city}</td>
        `;
        tableBody.appendChild(row);
    });

    // 更新表头排序指示
    tableHeaders.forEach(header => {
        header.classList.remove('sorted-asc', 'sorted-desc');
        if (header.dataset.sortable === currentSortColumn) {
            header.classList.add(`sorted-${currentSortDirection}`);
        }
    });
}

// 排序函数
function sortTable(columnName) {
    if (currentSortColumn === columnName) {
        currentSortDirection = currentSortDirection === 'asc' ? 'desc' : 'asc';
    } else {
        currentSortColumn = columnName;
        currentSortDirection = 'asc';
    }

    tableData.sort((a, b) => {
        let valA = a[columnName];
        let valB = b[columnName];

        // 处理数字和字符串排序
        if (typeof valA === 'string') {
            valA = valA.toLowerCase();
            valB = valB.toLowerCase();
            return currentSortDirection === 'asc' ? valA.localeCompare(valB) : valB.localeCompare(valA);
        } else {
            return currentSortDirection === 'asc' ? valA - valB : valB - valA;
        }
    });

    renderTable(); // 重新渲染表格
}

// 为每个可排序的表头添加点击事件
tableHeaders.forEach(header => {
    header.addEventListener('click', () => {
        const columnName = header.dataset.sortable;
        if (columnName) {
            sortTable(columnName);
        }
    });
});

// 初始渲染表格
if(sortableTable) renderTable();

<h4>可排序表格示例</h4>
<div style="background-color: #363636; padding: 25px; border-radius: 8px;">
    <table id="sortableTable" style="width: 100%; border-collapse: collapse; margin-bottom: 20px;">
        <thead>
            <tr>
                <th data-sortable="id">ID</th>
                <th data-sortable="name">姓名</th>
                <th data-sortable="age">年龄</th>
                <th data-sortable="city">城市</th>
            </tr>
        </thead>
        <tbody>
            <!-- 表格数据将在此处动态生成 -->
        </tbody>
    </table>
    <p style="margin-top: 10px; font-size: 0.9em; color: #bbb;">
        点击表格的列标题 (ID, 姓名, 年龄, 城市) 以进行排序。
    </p>
</div>

<style>
    #sortableTable th, #sortableTable td {
        border: 1px solid #444;
        padding: 10px;
        text-align: left;
    }
    #sortableTable th {
        background-color: #4a4a4a;
        color: white;
        cursor: pointer;
        position: relative;
        padding-right: 25px; /* 为排序箭头留出空间 */
    }
    #sortableTable th:hover {
        background-color: #555;
    }
    #sortableTable th[data-sortable]::after {
        content: '';
        position: absolute;
        right: 8px;
        top: 50%;
        transform: translateY(-50%);
        width: 0;
        height: 0;
        border-left: 5px solid transparent;
        border-right: 5px solid transparent;
        border-top: 5px solid #bbb; /* 默认向下箭头 */
        opacity: 0.5;
    }
    #sortableTable th.sorted-asc::after {
        border-top: none;
        border-bottom: 5px solid #61dafb; /* 向上箭头 */
        opacity: 1;
    }
    #sortableTable th.sorted-desc::after {
        border-top: 5px solid #61dafb; /* 向下箭头 */
        opacity: 1;
    }
    #sortableTable tbody tr {
        background-color: #2b2b2b;
    }
    #sortableTable tbody tr:nth-child(even) {
        background-color: #333;
    }
    #sortableTable tbody tr:hover {
        background-color: #3e3e3e;
    }
</style>

核心思路:

  1. 数据存储: 将表格数据存储在一个JavaScript数组中。
  2. 动态渲染: 根据数据数组动态生成表格的<tbody>内容。
  3. 排序状态: 维护当前排序的列名和方向(升序/降序)。
  4. 点击事件: 监听表头点击事件,根据data-sortable属性获取列名。
  5. 数组排序: 使用JavaScript的Array.prototype.sort()方法对数据进行排序,根据类型(数字或字符串)进行不同处理。
  6. UI更新: 重新渲染表格,并更新表头的排序指示箭头。

9.2 功能组件

9.2.1 富文本编辑器 (基于 contenteditable)

利用HTML的contenteditable属性,可以快速实现一个简单的富文本编辑器。更复杂的功能通常需要第三方库。

const editor = document.getElementById('richTextEditor');
const boldBtn = document.getElementById('boldBtn');
const italicBtn = document.getElementById('italicBtn');
const underlineBtn = document.getElementById('underlineBtn');
const linkBtn = document.getElementById('linkBtn');
const colorPicker = document.getElementById('colorPicker');
const htmlOutput = document.getElementById('editorHtmlOutput');

// 执行富文本命令
function formatDoc(command, value = null) {
    // document.execCommand 是一个老旧的API,但在简单场景下仍然可用
    // 现代富文本编辑器会使用更复杂的Range和Selection API
    document.execCommand(command, false, value);
    editor.focus(); // 保持焦点在编辑器内
    updateHtmlOutput(); // 实时更新HTML输出
}

// 更新HTML输出
function updateHtmlOutput() {
    if (htmlOutput && editor) {
        htmlOutput.textContent = editor.innerHTML;
        // Re-highlight the code block if Prism is used
        if (window.Prism) {
            Prism.highlightAll();
        }
    }
}

if (boldBtn) boldBtn.addEventListener('click', () => formatDoc('bold'));
if (italicBtn) italicBtn.addEventListener('click', () => formatDoc('italic'));
if (underlineBtn) underlineBtn.addEventListener('click', () => formatDoc('underline'));

if (linkBtn) {
    linkBtn.addEventListener('click', () => {
        const url = prompt('请输入链接地址:');
        if (url) {
            formatDoc('createLink', url);
        }
    });
}

if (colorPicker) {
    colorPicker.addEventListener('input', (e) => {
        formatDoc('foreColor', e.target.value);
    });
}

// 监听编辑器内容变化,实时更新HTML输出
if (editor) {
    editor.addEventListener('input', updateHtmlOutput);
    editor.addEventListener('mouseup', updateHtmlOutput); // 鼠标松开时也更新 (例如选中文本)
    editor.addEventListener('keyup', updateHtmlOutput); // 键盘释放时也更新
    updateHtmlOutput(); // 初始化显示
}

<h4>富文本编辑器示例 (基于 contenteditable)</h4>
<div style="background-color: #363636; padding: 25px; border-radius: 8px;">
    <div class="editor-toolbar" style="margin-bottom: 15px;">
        <button id="boldBtn" class="editor-btn"><strong>B</strong></button>
        <button id="italicBtn" class="editor-btn"><em>I</em></button>
        <button id="underlineBtn" class="editor-btn"><u>U</u></button>
        <button id="linkBtn" class="editor-btn">🔗</button>
        <input type="color" id="colorPicker" value="#61dafb" style="vertical-align: middle; width: 30px; height: 30px; border: none; padding: 0; cursor: pointer; border-radius: 4px;">
    </div>

    <div id="richTextEditor" contenteditable="true" style="
        min-height: 200px;
        border: 1px solid #444;
        padding: 15px;
        background-color: #2b2b2b;
        color: #e0e0e0;
        font-size: 1em;
        line-height: 1.6;
        border-radius: 4px;
        outline: none;
        overflow-y: auto;
    ">
        <p>这是一个 <strong>简单的</strong> <em>富文本</em> <u>编辑器</u>。</p>
        <p>请尝试 <a href="https://example.com" style="color: #61dafb;">点击按钮</a> 或选择文本进行格式化。</p>
    </div>

    <h5 style="color: #a9dfd8; margin-top: 25px;">实时 HTML 输出</h5>
    <pre><code id="editorHtmlOutput" class="language-html" style="
        background-color: #272822;
        padding: 15px;
        border-radius: 5px;
        min-height: 100px;
        overflow-x: auto;
        white-space: pre-wrap; /* 自动换行 */
        color: #f0f0f0;
    "></code></pre>
</div>

<style>
    .editor-btn {
        padding: 8px 12px;
        background-color: #4a4a4a;
        color: white;
        border: none;
        border-radius: 4px;
        cursor: pointer;
        margin-right: 5px;
        font-size: 1em;
    }
    .editor-btn:hover {
        background-color: #555;
    }
</style>

核心概念:

9.2.2 可视化图表 (基于 Chart.js)

前端可视化图表可以直观地展示数据。这里以流行的 Chart.js 为例。

// 示例数据
const chartData = {
    labels: ['一月', '二月', '三月', '四月', '五月', '六月'],
    datasets: [{
        label: '销售额',
        data: [1200, 1900, 3000, 5000, 2000, 3000],
        backgroundColor: [
            'rgba(97, 218, 251, 0.5)', // Blue
            'rgba(169, 223, 216, 0.5)', // Light Blue/Green
            'rgba(248, 194, 145, 0.5)', // Orange
            'rgba(231, 76, 60, 0.5)',  // Red
            'rgba(52, 152, 219, 0.5)', // Sky Blue
            'rgba(39, 174, 96, 0.5)'   // Green
        ],
        borderColor: [
            'rgba(97, 218, 251, 1)',
            'rgba(169, 223, 216, 1)',
            'rgba(248, 194, 145, 1)',
            'rgba(231, 76, 60, 1)',
            'rgba(52, 152, 219, 1)',
            'rgba(39, 174, 96, 1)'
        ],
        borderWidth: 1
    }]
};

// 图表配置
const chartConfig = {
    type: 'bar', // bar, line, pie, doughnut, radar, polarArea, bubble, scatter
    data: chartData,
    options: {
        responsive: true,
        maintainAspectRatio: false, // 允许Canvas拉伸填满父容器
        scales: {
            y: {
                beginAtZero: true,
                grid: {
                    color: '#444' // Y轴网格线颜色
                },
                ticks: {
                    color: '#e0e0e0' // Y轴刻度标签颜色
                }
            },
            x: {
                grid: {
                    color: '#444' // X轴网格线颜色
                },
                ticks: {
                    color: '#e0e0e0' // X轴刻度标签颜色
                }
            }
        },
        plugins: {
            legend: {
                labels: {
                    color: '#e0e0e0' // 图例文字颜色
                }
            },
            title: {
                display: true,
                text: '月度销售数据',
                color: '#61dafb', // 标题颜色
                font: {
                    size: 18
                }
            }
        }
    }
};

let myChartInstance; // 用于存储图表实例,以便更新或销毁
// 创建图表
function createChart() {
    const ctx = document.getElementById('myChart');
    if (!ctx) return;
    
    if (typeof Chart === 'undefined') {
        document.getElementById('chartStatus').textContent = 'Chart.js 库未加载。请检查 CDN 链接。';
        return;
    }

    if (myChartInstance) {
        myChartInstance.destroy(); // 如果图表已存在,先销毁
    }
    myChartInstance = new Chart(ctx.getContext('2d'), chartConfig);
    console.log('图表已创建。');
    document.getElementById('chartStatus').textContent = '图表已渲染。';
}

// 更新数据示例
document.getElementById('updateChartBtn').addEventListener('click', () => {
    if (!myChartInstance) {
        alert('请先创建图表!');
        return;
    }
    // 随机更新数据
    myChartInstance.data.datasets[0].data = myChartInstance.data.datasets[0].data.map(() => Math.floor(Math.random() * 4000) + 1000);
    myChartInstance.update(); // 更新图表
    console.log('图表数据已更新。');
    document.getElementById('chartStatus').textContent = '图表数据已更新。';
});

document.getElementById('changeChartTypeBtn').addEventListener('click', () => {
    if (!myChartInstance) {
        alert('请先创建图表!');
        return;
    }
    // 切换图表类型
    const currentType = myChartInstance.config.type;
    myChartInstance.config.type = currentType === 'bar' ? 'line' : 'bar';
    myChartInstance.update();
    console.log('图表类型已切换。');
    document.getElementById('chartStatus').textContent = `图表类型已切换为 ${myChartInstance.config.type}。`;
});

// 首次加载时创建
createChart();

<h4>可视化图表示例 (基于 Chart.js)</h4>
<div style="background-color: #363636; padding: 25px; border-radius: 8px;">
    <p>
        本页面已通过CDN引入了 <a href="https://www.chartjs.org/" target="_blank">Chart.js</a> 库,可以直接使用。
    </p>

    <div style="width: 100%; max-width: 700px; height: 350px; margin: 20px auto;">
        <canvas id="myChart"></canvas>
    </div>

    <div style="text-align: center; margin-top: 20px;">
        <button id="updateChartBtn" class="dev-btn">更新数据</button>
        <button id="changeChartTypeBtn" class="dev-btn">切换图表类型</button>
    </div>
    <p id="chartStatus" style="margin-top: 15px; font-size: 0.9em; color: #d0d0d0; text-align: center;">
        等待 Chart.js 渲染...
    </p>
    <p style="margin-top: 10px; font-size: 0.9em; color: #bbb;">
        Chart.js 提供了丰富的图表类型和自定义选项,您可以查看其官方文档获取更多信息。
    </p>
</div>

核心概念:

10. 综合项目

这一部分将概述构建一个小型前端应用时需要考虑的关键方面,并提供概念性的代码片段。

10.1 SPA 路由实现

单页应用(SPA)通过前端路由模拟多页应用的用户体验,避免了页面刷新。

const appDiv = document.getElementById('app');

// 模拟页面内容
const routes = {
    '/': {
        title: '首页',
        content: `
            <h2>欢迎来到首页</h2>
            <p>这是一个SPA应用示例。</p>
            <p>了解更多关于我们的 <a href="/about" data-nav>关于页面</a>。</p>
        `
    },
    '/about': {
        title: '关于我们',
        content: `
            <h2>关于我们</h2>
            <p>我们致力于提供高质量的前端开发教程。</p>
            <p><a href="/" data-nav>返回首页</a></p>
        `
    },
    '/contact': {
        title: '联系我们',
        content: `
            <h2>联系我们</h2>
            <p>邮箱: [email protected]</p>
            <p><a href="/" data-nav>返回首页</a></p>
        `
    },
    '/404': {
        title: '页面未找到',
        content: `
            <h2>404 - 页面未找到</h2>
            <p>您访问的页面不存在。</p>
            <p><a href="/" data-nav>返回首页</a></p>
        `
    }
};

// 渲染当前路由对应的页面内容
function renderPage(path) {
    const route = routes[path] || routes['/404'];
    if (appDiv) {
        appDiv.innerHTML = route.content;
        document.title = route.title;
        console.log(`导航到: ${path}`);
    }
}

// 处理点击导航链接
function handleNavLinkClick(event) {
    // 检查点击的是否是带有 data-nav 属性的链接
    if (event.target.matches('a[data-nav]')) {
        event.preventDefault(); // 阻止默认的链接跳转行为
        const path = event.target.getAttribute('href');
        history.pushState({}, '', path); // 改变URL而不刷新页面
        renderPage(path);
    }
}

// 监听浏览器历史状态变化 (前进/后退按钮)
window.addEventListener('popstate', () => {
    renderPage(location.pathname);
});

// 首次加载时渲染当前路径
document.addEventListener('DOMContentLoaded', () => {
    document.body.addEventListener('click', handleNavLinkClick); // 事件委托
    renderPage(location.pathname);
});

<h4>SPA 路由实现示例 (原生 JS)</h4>
<div style="background-color: #363636; padding: 25px; border-radius: 8px;">
    <nav style="margin-bottom: 20px; text-align: center;">
        <a href="/" data-nav class="nav-link">首页</a>
        <a href="/about" data-nav class="nav-link">关于</a>
        <a href="/contact" data-nav class="nav-link">联系</a>
        <a href="/non-existent-page" data-nav class="nav-link">不存在的页面</a>
    </nav>

    <div id="app" style="
        border: 1px solid #61dafb;
        padding: 20px;
        min-height: 200px;
        background-color: #2b2b2b;
        color: #d0d0d0;
        border-radius: 4px;
    ">
        <!-- 页面内容将在此处渲染 -->
    </div>
    <p style="margin-top: 10px; font-size: 0.9em; color: #bbb;">
        尝试点击上方链接,观察浏览器URL和页面内容的变化,但页面不会刷新。
        也可以尝试使用浏览器的前进/后退按钮。
    </p>
</div>

<style>
    .nav-link {
        display: inline-block;
        padding: 8px 15px;
        background-color: #4a4a4a;
        color: white;
        text-decoration: none;
        border-radius: 4px;
        margin: 0 5px;
        transition: background-color 0.3s ease;
    }
    .nav-link:hover {
        background-color: #61dafb;
    }
    #app h2 {
        color: #a9dfd8;
        border-bottom: 1px dashed #444;
        padding-bottom: 5px;
    }
</style>

核心API:

10.2 状态管理方案

在大型前端应用中,管理组件之间共享的状态变得复杂。状态管理模式和库应运而生。

下图展示了一个简化的Flux/Redux-like单向数据流:

graph LR User[用户交互] -->|触发| Action[Action (描述发生了什么)] Action -->|派发| Dispatcher[Dispatcher/Store (处理Action)] Dispatcher -->|更新| State[State (应用状态)] State -->|通知| View[View (UI组件)] View -->|渲染| User style Action fill:#61dafb,stroke:#333,stroke-width:2px,color:#fff; style Dispatcher fill:#a9dfd8,stroke:#333,stroke-width:2px,color:#333; style State fill:#f8c291,stroke:#333,stroke-width:2px,color:#333; style View fill:#e0e0e0,stroke:#333,stroke-width:2px,color:#333;

简单实现一个发布订阅模式 (Event Bus) 进行状态管理:

class SimpleStore extends EventTarget {
    constructor(initialState = {}) {
        super();
        this.state = initialState;
        console.log('Store initialized with state:', this.state);
    }

    // 获取当前状态
    getState() {
        return { ...this.state }; // 返回状态的副本,防止外部直接修改
    }

    // 更新状态并通知订阅者
    setState(newState) {
        // 简单合并状态,实际可进行深合并或Redux-like reducer
        this.state = { ...this.state, ...newState };
        console.log('State updated to:', this.state);
        // 派发自定义事件通知所有订阅者
        this.dispatchEvent(new CustomEvent('stateChange', { detail: this.getState() }));
    }

    // 订阅状态变化
    subscribe(callback) {
        const handler = (e) => callback(e.detail);
        this.addEventListener('stateChange', handler);
        // 返回一个取消订阅的函数
        return () => this.removeEventListener('stateChange', handler);
    }
}

// 实例化Store
const store = new SimpleStore({
    count: 0,
    user: { name: 'Guest', loggedIn: false }
});

// 组件1:计数器显示
const counterDisplay = document.getElementById('counterDisplay');
if (counterDisplay) {
    // 首次渲染
    counterDisplay.textContent = `计数: ${store.getState().count}`;
    // 订阅状态变化
    store.subscribe((newState) => {
        counterDisplay.textContent = `计数: ${newState.count}`;
    });
}

// 组件2:计数器操作
const incrementBtn = document.getElementById('incrementBtn');
const decrementBtn = document.getElementById('decrementBtn');
if (incrementBtn) {
    incrementBtn.addEventListener('click', () => {
        store.setState({ count: store.getState().count + 1 });
    });
}
if (decrementBtn) {
    decrementBtn.addEventListener('click', () => {
        store.setState({ count: store.getState().count - 1 });
    });
}

// 组件3:用户登录/登出
const loginBtn = document.getElementById('loginBtn');
const logoutBtn = document.getElementById('logoutBtn');
const userStatusDisplay = document.getElementById('userStatusDisplay');

if (userStatusDisplay) {
    // 首次渲染
    const currentUser = store.getState().user;
    userStatusDisplay.textContent = currentUser.loggedIn ? `欢迎, ${currentUser.name}!` : '未登录';
    // 订阅状态变化
    store.subscribe((newState) => {
        const user = newState.user;
        userStatusDisplay.textContent = user.loggedIn ? `欢迎, ${user.name}!` : '未登录';
    });
}

if (loginBtn) {
    loginBtn.addEventListener('click', () => {
        store.setState({ user: { name: 'Alice', loggedIn: true } });
    });
}
if (logoutBtn) {
    logoutBtn.addEventListener('click', () => {
        store.setState({ user: { name: 'Guest', loggedIn: false } });
    });
}

// 模拟取消订阅 (例如,组件卸载时)
const unsubscribeBtn = document.getElementById('unsubscribeBtn');
let unsubscribeCounter = null;
if (unsubscribeBtn) {
    unsubscribeBtn.addEventListener('click', () => {
        if (!unsubscribeCounter) {
            unsubscribeCounter = store.subscribe((newState) => {
                console.log('额外订阅者:计数器更新:', newState.count);
                // 这里的逻辑只在控制台输出
            });
            unsubscribeBtn.textContent = '取消额外订阅';
            console.log('额外订阅已添加。');
        } else {
            unsubscribeCounter(); // 调用返回的取消函数
            unsubscribeCounter = null;
            unsubscribeBtn.textContent = '添加额外订阅';
            console.log('额外订阅已取消。');
        }
    });
}

<h4>状态管理示例 (基于 Event Bus)</h4>
<div style="background-color: #363636; padding: 25px; border-radius: 8px;">
    <h5 style="color: #a9dfd8;">计数器组件</h5>
    <p id="counterDisplay" style="font-size: 1.5em; font-weight: bold; color: #61dafb;">计数: 0</p>
    <button id="incrementBtn" class="dev-btn">增加计数</button>
    <button id="decrementBtn" class="dev-btn">减少计数</button>

    <h5 style="color: #a9dfd8; margin-top: 25px;">用户状态组件</h5>
    <p id="userStatusDisplay" style="font-size: 1.2em; color: #f8c291;">未登录</p>
    <button id="loginBtn" class="dev-btn">登录</button>
    <button id="logoutBtn" class="dev-btn">登出</button>

    <h5 style="color: #a9dfd8; margin-top: 25px;">订阅者管理</h5>
    <button id="unsubscribeBtn" class="dev-btn">添加额外订阅</button>
    <p style="margin-top: 10px; font-size: 0.9em; color: #bbb;">
        点击“添加额外订阅”后,在控制台观察额外输出。再点击可取消订阅。
    </p>
</div>

状态管理方案的选择:

10.3 服务端渲染集成 (SSR / Hydration 概念)

服务端渲染 (Server-Side Rendering, SSR) 允许服务器在返回HTML之前预渲染页面内容。这改善了首次内容绘制 (FCP) 和首字节时间 (TTFB),对SEO和用户体验(尤其是低速网络用户)非常有益。

概念流程:

graph TD A[用户请求页面] -->|HTTP 请求| B[Node.js Server] B -->|SSR Logic| C{渲染组件为HTML字符串} C -->|数据填充| D[带数据的HTML] D -->|发送到客户端| E[浏览器接收HTML] E -->|首次绘制| F[用户看到内容 (快)] E -->|下载 JS / Hydration| G[浏览器执行前端JS] G -->|接管并绑定事件| H[可交互应用 (水合)]

核心概念:

SSR 代码示例 (概念性 - 通常需要特定框架支持,如Next.js, Nuxt.js)


<h4>服务端渲染 (SSR / Hydration) 概念示例</h4>
<div style="background-color: #363636; padding: 25px; border-radius: 8px;">
    <p>
        服务端渲染 (SSR) 和水合 (Hydration) 是现代前端框架 (如 Next.js, Nuxt.js) 提供的优化技术,
        旨在提升页面的首次加载性能和 SEO。
    </p>
    <p>此示例仅为概念性说明,无法在浏览器中直接运行,因为它需要一个 Node.js 服务器环境。</p>

    <h5 style="color: #a9dfd8;">SSR 工作流程</h5>
    <ul>
        <li>用户请求页面。</li>
        <li>服务器(通常是 Node.js)运行前端代码,预渲染 HTML。</li>
        <li>将带数据的完整 HTML 发送到浏览器。</li>
        <li>浏览器显示内容(<strong>非常快</strong>,因为是完整的 HTML)。</li>
        <li>同时,浏览器下载客户端 JavaScript。</li>
        <li>JavaScript 执行,并在现有 HTML 上“水合”(Hydrate),使其变为可交互的单页应用。</li>
    </ul>

    <p><strong>SSR 服务器端伪代码 (server.js):</strong></p>
    <pre><code class="language-javascript">
// server.js (Node.js Express 示例)
const express = require('express');
const ReactDOMServer = require('react-dom/server'); // 或 Vue.js renderToString

// 假设 App 是你的 React/Vue 根组件
function App({ data }) {
    return (
        <div>
            <h1>SSR 应用</h1>
            <p>加载的数据: {data.message}</p>
            <button onClick="alert('这是客户端交互')">点击我</button>
        </div>
    );
}

const app = express();

app.get('/', async (req, res) => {
    const initialData = { message: '这是来自服务器的初始数据!' };
    const appHtml = ReactDOMServer.renderToString(React.createElement(App, { data: initialData }));

    const fullHtml = \`<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>SSR 示例</title>
</head>
<body>
    <div id="root">\${appHtml}</div>
    <script>window.__INITIAL_DATA__ = \${JSON.stringify(initialData)};</script>
    <script src="/client.bundle.js"></script>
</body>
</html>\`;
    res.send(fullHtml);
});

app.use(express.static('dist')); // 托管客户端打包后的 JS/CSS
app.listen(3000, () => console.log('SSR Server running on port 3000'));
    </code></pre>

    <p><strong>客户端 Hydration 伪代码 (client.js):</strong></p>
    <pre><code class="language-javascript">
// client.js
import ReactDOM from 'react-dom/client'; // 或 Vue.js createApp
import App from './App';

const initialData = window.__INITIAL_DATA__; // 获取服务器注入的数据

// 使用 hydrateRoot 接管服务器渲染的 HTML
ReactDOM.hydrateRoot(
    document.getElementById('root'),
    <App data={initialData} />
);
console.log('客户端已加载并水合。');
    </code></pre>
    <p style="margin-top: 10px; font-size: 0.9em; color: #bbb;">
        SSR/Hydration 减少了首次内容绘制时间 (FCP),并使页面在 JS 加载前即可被爬虫抓取,对 SEO 友好。
    </p>
</div>

10.4 性能监控与错误上报

在生产环境中,监控应用的性能和错误是至关重要的。这有助于及时发现问题并优化用户体验。

10.4.1 性能指标监控 (Performance API)

浏览器提供了 Performance API,允许开发者测量和分析网页加载及运行时性能指标。

// 监听页面加载性能指标
window.addEventListener('load', () => {
    // 使用 requestAnimationFrame 确保在浏览器绘制下一帧之前获取数据
    requestAnimationFrame(() => {
        const perf = window.performance;
        if (!perf || !perf.timing) {
            console.warn('浏览器不支持 Performance Timing API。');
            document.getElementById('perfMetrics').textContent = '浏览器不支持 Performance API。';
            return;
        }

        const timing = perf.timing;
        const navigationStart = timing.navigationStart;

        // 关键性能指标计算
        const dnsLookupTime = timing.domainLookupEnd - timing.domainLookupStart;
        const tcpConnectTime = timing.connectEnd - timing.connectStart;
        const ttfb = timing.responseStart - timing.requestStart;
        const domReadyTime = timing.domContentLoadedEventEnd - navigationStart;
        const fullLoadTime = timing.loadEventEnd - navigationStart;

        const metrics = `
            <h5>页面加载性能 (Navigation Timing)</h5>
            <p>DNS 解析: <strong>${dnsLookupTime}</strong> ms</p>
            <p>TCP 连接: <strong>${tcpConnectTime}</strong> ms</p>
            <p>首字节时间 (TTFB): <strong>${ttfb}</strong> ms</p>
            <p>DOM 可交互: <strong>${timing.domInteractive - navigationStart}</strong> ms</p>
            <p>DOM 内容加载完成: <strong>${domReadyTime}</strong> ms</p>
            <p>页面完全加载: <strong>${fullLoadTime}</strong> ms</p>
        `;
        document.getElementById('perfMetrics').innerHTML = metrics;
        console.log('页面加载性能指标:', {
            dnsLookupTime, tcpConnectTime, ttfb, domReadyTime, fullLoadTime
        });

        // 使用 PerformanceObserver 监听更现代的指标 (如 LCP, FID, CLS)
        if (typeof PerformanceObserver !== 'undefined') {
            new PerformanceObserver((entryList) => {
                for (const entry of entryList.getEntriesByName('first-contentful-paint')) {
                    console.log('FCP:', entry.startTime);
                }
                const lcpEntry = entryList.getEntries().find(e => e.name === 'largest-contentful-paint');
                if (lcpEntry) {
                    document.getElementById('lcpMetric').textContent = `LCP (最大内容绘制): ${lcpEntry.startTime.toFixed(2)} ms`;
                }
            }).observe({ type: 'largest-contentful-paint', buffered: true });

            new PerformanceObserver((entryList) => {
                 for (const entry of entryList.getEntries()) {
                    document.getElementById('fidMetric').textContent = `FID (首次输入延迟): ${entry.processingStart - entry.startTime} ms`;
                }
            }).observe({ type: 'first-input', buffered: true });

            let cls = 0;
            new PerformanceObserver((entryList) => {
                for (const entry of entryList.getEntries()) {
                    if (!entry.hadRecentInput) {
                        cls += entry.value;
                    }
                }
                document.getElementById('clsMetric').textContent = `CLS (累积布局偏移): ${cls.toFixed(4)}`;
            }).observe({ type: 'layout-shift', buffered: true });
        } else {
            document.getElementById('lcpMetric').textContent = '浏览器不支持 LCP/FID/CLS 观察。';
            document.getElementById('fidMetric').textContent = '';
            document.getElementById('clsMetric').textContent = '';
        }
    });
});

<h4>性能指标监控 (Performance API) 示例</h4>
<div style="background-color: #363636; padding: 25px; border-radius: 8px;">
    <p>以下是使用浏览器 Performance API 捕获的页面加载和核心 Web 指标。</p>
    <div id="perfMetrics" style="border: 1px dashed #61dafb; padding: 15px; margin-bottom: 20px; background-color: #2b2b2b; color: #d0d0d0;">
        <p>等待页面加载性能数据...</p>
    </div>
    <div id="coreWebVitals" style="border: 1px dashed #a9dfd8; padding: 15px; background-color: #2b2b2b; color: #d0d0d0;">
        <h5>核心 Web 指标 (Core Web Vitals)</h5>
        <p id="lcpMetric">LCP (最大内容绘制): 测量中...</p>
        <p id="fidMetric">FID (首次输入延迟): 测量中... (请在页面上进行一次点击或按键)</p>
        <p id="clsMetric">CLS (累积布局偏移): 测量中...</p>
    </div>
    <p style="margin-top: 10px; font-size: 0.9em; color: #bbb;">
        <strong>提示:</strong> 请打开浏览器开发者工具的 <span class="highlight">Console</span> 
        (控制台)和 <span class="highlight">Performance</span> 
        (性能)面板,刷新页面观察详细数据。LCP、FID、CLS 可能会在用户交互或布局变化后出现。
    </p>
</div>

关键指标:

10.4.2 错误上报

捕获并上报前端错误是保障应用稳定性的重要一环。通常会将错误信息发送到集中化的错误监控服务(如 Sentry, Bugsnag, 或自定义服务)。

// 模拟一个错误上报服务
function reportErrorToServer(errorDetails) {
    console.error('---- 模拟上报错误 ----', errorDetails);
    document.getElementById('errorLog').textContent += `\n[${new Date().toLocaleTimeString()}] 上报错误: ${errorDetails.message || '未知错误'}`;
    // 实际场景中会是一个 AJAX 请求
}

// 1. 全局错误捕获:window.onerror
window.onerror = function(message, source, lineno, colno, error) {
    console.group('--- 全局捕获到 JS 错误 (window.onerror) ---');
    console.error('Message:', message);
    console.error('Source:', source);
    console.error('Line/Col:', lineno, colno);
    console.error('Error Object:', error);
    console.groupEnd();

    reportErrorToServer({
        type: 'js_runtime_error',
        message: message,
        stack: error ? error.stack : 'N/A',
        url: source,
        line: lineno,
        column: colno,
        userAgent: navigator.userAgent
    });
    return false;
};

// 2. Promise 错误捕获:unhandledrejection
window.addEventListener('unhandledrejection', function(event) {
    console.group('--- 全局捕获到 Promise 拒绝 (unhandledrejection) ---');
    console.error('Reason:', event.reason);
    console.groupEnd();

    reportErrorToServer({
        type: 'promise_rejection',
        message: event.reason ? event.reason.message : '未知 Promise 拒绝',
        stack: event.reason && event.reason.stack ? event.reason.stack : 'N/A',
        userAgent: navigator.userAgent
    });
});

// 3. 资源加载错误捕获 (img, script, link等)
window.addEventListener('error', function(event) {
    if (event.target !== window && event.target.tagName) {
        console.group('--- 全局捕获到资源加载错误 ---');
        console.error('资源加载失败:', event.target.tagName, event.target.src || event.target.href);
        console.groupEnd();

        reportErrorToServer({
            type: 'resource_load_error',
            tag: event.target.tagName,
            source: event.target.src || event.target.href,
            message: `Failed to load resource: ${event.target.src || event.target.href}`,
            userAgent: navigator.userAgent
        });
    }
}, true);


// 示例触发错误
document.getElementById('triggerJsErrorBtn').addEventListener('click', () => {
    console.log('--- 触发 JS 运行时错误 ---');
    nonExistentFunction();
});

document.getElementById('triggerPromiseErrorBtn').addEventListener('click', () => {
    console.log('--- 触发 Promise 拒绝错误 ---');
    new Promise((resolve, reject) => {
        setTimeout(() => reject(new Error('这是一个未处理的 Promise 错误!')), 500);
    });
});

document.getElementById('triggerResourceErrorBtn').addEventListener('click', () => {
    console.log('--- 触发资源加载错误 ---');
    const img = document.createElement('img');
    img.src = 'https://non-existent-domain.com/non-existent-image.jpg';
    img.alt = '错误图片';
    img.style.display = 'none';
    document.body.appendChild(img);
});

document.getElementById('triggerSyntaxErrorBtn').addEventListener('click', () => {
    console.log('--- 尝试触发语法错误 (eval) ---');
    try {
        eval('const x =;');
    } catch (e) {
        console.error('语法错误在 try...catch 中被捕获:', e.message);
        reportErrorToServer({
            type: 'syntax_error_eval',
            message: e.message,
            stack: e.stack,
            userAgent: navigator.userAgent
        });
    }
});

<h4>前端错误上报示例</h4>
<div style="background-color: #363636; padding: 25px; border-radius: 8px;">
    <p>点击按钮,模拟不同类型的错误,并观察它们如何被全局捕获并“上报”。</p>
    <div style="margin-bottom: 20px;">
        <button id="triggerJsErrorBtn" class="error-btn">触发 JS 运行时错误</button>
        <button id="triggerPromiseErrorBtn" class="error-btn">触发 Promise 错误</button>
        <button id="triggerResourceErrorBtn" class="error-btn">触发资源加载错误</button>
        <button id="triggerSyntaxErrorBtn" class="error-btn">触发语法错误 (eval)</button>
    </div>

    <h5 style="color: #a9dfd8;">错误上报日志 (模拟)</h5>
    <pre id="errorLog" style="
        background-color: #272822;
        padding: 15px;
        border-radius: 5px;
        min-height: 100px;
        overflow-x: auto;
        white-space: pre-wrap;
        color: #f0f0f0;
        border: 1px solid #e74c3c;
    ">错误上报信息将在此显示...</pre>
    <p style="margin-top: 10px; font-size: 0.9em; color: #bbb;">
        请打开浏览器开发者工具的 <span class="highlight">Console</span> 
        (控制台)面板,查看详细的错误信息。
    </p>
</div>

<style>
    .error-btn {
        padding: 8px 15px;
        background-color: #e74c3c;
        color: white;
        border: none;
        border-radius: 4px;
        cursor: pointer;
        margin-right: 10px;
        margin-bottom: 10px;
    }
    .error-btn:hover {
        background-color: #c0392b;
    }
</style>

错误捕获机制:

上报内容:


结语

恭喜您完成了这份《前端开发常见代码片段》的详细教程!我们从HTML的骨架、CSS的造型,深入到JavaScript的交互逻辑、现代特性和异步编程,再探讨了文件处理、浏览器API、性能优化以及组件化和测试等现代工程实践。

前端领域发展迅速,保持持续学习是成功的关键。希望这份教程能为您在前端开发的道路上提供坚实的基础和实用的指引。

实践是最好的老师。请务必动手尝试和修改代码片段,将其运用到您的实际项目中。祝您在前端开发的旅程中取得更大的进步!

互动区域

登录后可以点赞此内容

参与互动

登录后可以点赞和评论此内容,与作者互动交流