跳到主要内容

内容元素✅

简述 <iframe> 的优缺点及常见应用场景

答案

<iframe> 是 HTML 中用于在当前页面嵌入其他独立文档的标签,具备内容隔离、并行加载等特性,但也带来性能和安全等方面的挑战。

  • 优点
    • 内容分隔与代码隔离:每个 <iframe> 拥有独立的文档上下文,主页面与其内容互不干扰,便于模块化和维护。
    • 并行加载<iframe> 可独立于主页面并行加载资源,提升部分场景下的加载效率。
    • 安全隔离:可用于加载不可信内容,通过同源策略和沙箱属性(sandbox)增强安全性,降低主页面被攻击风险。
  • 缺点
    • SEO 不友好:搜索引擎通常不会索引 <iframe> 内的内容,影响页面整体 SEO 表现。
    • 高度与布局控制难:内容高度动态变化时,主页面难以自适应,需额外脚本处理。
    • 性能开销:每个 <iframe> 都会增加额外的网络请求和渲染负担,过多使用会拖慢页面性能。
    • 安全风险:加载第三方内容时,若未妥善配置,可能引入 XSS 等安全隐患。

代码示例

<!-- 基本用法 -->
<iframe src="https://example.com" width="400" height="300" sandbox></iframe>
  • 嵌入第三方网页(如地图、视频、社交组件)
  • 广告投放与隔离
  • 加载不可信内容实现安全隔离
  • 早期无刷新文件上传
  • 跨域通信(配合 postMessage)

延伸阅读

提示

实际开发中,建议为 <iframe> 添加 sandbox 属性,并限制其权限,减少安全风险。对于动态高度内容,可结合 postMessage 实现自适应。

a 标签如何保存文件?

答案

<a> 标签本身用于跳转链接,但结合 download 属性或 JavaScript,可以实现文件下载功能。常见方式有两类:服务端生成下载、前端生成文件并触发下载。

  • 服务端下载:后端设置响应头(如 Content-Disposition: attachment),前端 <a href="下载地址"> 即可下载文件。适用于大文件或需鉴权的场景。
  • 前端生成:利用 JavaScript 创建 Blob 对象,生成临时 URL,设置 <a>hrefdownload 属性,模拟点击触发下载。适合导出文本、表格等前端可生成的数据。

1. 服务端下载(Node.js/Express)

// Node.js 示例
app.get('/download', (req, res) => {
res.setHeader('Content-Disposition', 'attachment; filename=demo.txt')
res.send('File content here')
})

前端:

<a href="/download">下载文件</a>

2. 前端生成并下载文件

const data = 'Hello, world!'
const blob = new Blob([data], { type: 'text/plain' })
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = 'hello.txt'
a.click()
URL.revokeObjectURL(url)
  • 仅设置 <a href="xxx"> 不会自动下载,需配合 download 属性或服务端响应头。
  • 前端 Blob 下载不适合大文件,且部分移动端浏览器支持有限。
  • 用户数据导出优先用前端 Blob,敏感或大文件用服务端生成。
  • 文件名可通过 download 属性自定义,提升用户体验。

参考资料

提示

如果要导出表格数据为 Excel,可用第三方库如 SheetJS,结合 Blob 和 <a> 标签实现一键下载。

HTML 表格的基本结构有哪些元素?

答案

核心概念:

HTML表格由以下核心元素构成:

  • <table> - 表格容器,定义整个表格
  • <thead> - 表头区域,包含列标题
  • <tbody> - 表格主体,包含数据行
  • <tfoot> - 表尾区域,包含汇总信息
  • <tr> - 表格行,定义水平行
  • <th> - 表头单元格,语义化的列/行标题
  • <td> - 数据单元格,包含实际数据

示例说明:

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>HTML表格基本结构示例</title>
    <style>
        table {
            border-collapse: collapse;
            width: 100%;
            margin: 20px 0;
        }
        
        th, td {
            border: 1px solid #ddd;
            padding: 12px;
            text-align: left;
        }
        
        th {
            background-color: #f2f2f2;
            font-weight: bold;
        }
        
        tbody tr:nth-child(even) {
            background-color: #f9f9f9;
        }
        
        tfoot {
            background-color: #e9e9e9;
            font-weight: bold;
        }
        
        caption {
            caption-side: top;
            padding: 10px;
            font-weight: bold;
            font-size: 1.2em;
        }
    </style>
</head>
<body>
    <table>
        <caption>2023年销售业绩统计表</caption>
        
        <!-- 表头区域 -->
        <thead>
            <tr>
                <th>产品类别</th>
                <th>第一季度</th>
                <th>第二季度</th>
                <th>第三季度</th>
                <th>第四季度</th>
            </tr>
        </thead>
        
        <!-- 表格主体 -->
        <tbody>
            <tr>
                <th>电子产品</th>
                <td>120万</td>
                <td>135万</td>
                <td>150万</td>
                <td>180万</td>
            </tr>
            <tr>
                <th>服装鞋帽</th>
                <td>80万</td>
                <td>95万</td>
                <td>110万</td>
                <td>125万</td>
            </tr>
            <tr>
                <th>家居用品</th>
                <td>60万</td>
                <td>70万</td>
                <td>85万</td>
                <td>90万</td>
            </tr>
        </tbody>
        
        <!-- 表尾区域 -->
        <tfoot>
            <tr>
                <th>总计</th>
                <td>260万</td>
                <td>300万</td>
                <td>345万</td>
                <td>395万</td>
            </tr>
        </tfoot>
    </table>

    <div style="margin-top: 20px; padding: 15px; background-color: #f0f8ff; border-left: 4px solid #007acc;">
        <h3>结构说明:</h3>
        <ul>
            <li><strong>&lt;caption&gt;</strong>: 表格标题,描述表格内容</li>
            <li><strong>&lt;thead&gt;</strong>: 表头区域,包含列标题</li>
            <li><strong>&lt;tbody&gt;</strong>: 表格主体,包含数据行</li>
            <li><strong>&lt;tfoot&gt;</strong>: 表尾区域,包含汇总信息</li>
            <li><strong>&lt;th&gt;</strong>: 表头单元格,语义化标题</li>
            <li><strong>&lt;td&gt;</strong>: 数据单元格,包含实际数据</li>
        </ul>
    </div>
</body>
</html>

面试官视角:

要点清单:

  • 能说出7个核心表格元素及其语义
  • 理解thead、tbody、tfoot的作用和必要性
  • 知道th和td的区别,特别是语义化作用

加分项:

  • 提及scope属性对可访问性的重要性
  • 了解caption元素的作用
  • 知道colgroup和col元素的用途

常见失误:

  • 混淆th和td的使用场景
  • 忽略表格的语义化结构
  • 不了解表格可访问性的基本要求

延伸阅读:

如何实现表格的可访问性最佳实践?

答案

核心概念:

表格可访问性的核心原则:

  • 使用<caption>提供表格描述
  • 通过<th>元素标记表头
  • 使用scope属性明确表头范围
  • 为复杂表格提供headers属性
  • 确保表格有清晰的逻辑结构

示例说明:

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>可访问性表格示例</title>
    <style>
        table {
            border-collapse: collapse;
            width: 100%;
            margin: 20px 0;
        }
        
        th, td {
            border: 1px solid #333;
            padding: 10px;
            text-align: left;
        }
        
        th {
            background-color: #f4f4f4;
            font-weight: bold;
        }
        
        caption {
            caption-side: top;
            padding: 10px;
            font-weight: bold;
            font-size: 1.1em;
            text-align: left;
        }
        
        .highlight {
            background-color: #fff3cd;
        }
        
        .demo-info {
            margin: 20px 0;
            padding: 15px;
            background-color: #d4edda;
            border-left: 4px solid #28a745;
        }
    </style>
</head>
<body>
    <!-- 简单表格示例:scope属性 -->
    <table>
        <caption>学生成绩表 - 使用scope属性提升可访问性</caption>
        <thead>
            <tr>
                <th scope="col">姓名</th>
                <th scope="col">数学</th>
                <th scope="col">英语</th>
                <th scope="col">物理</th>
                <th scope="col">总分</th>
            </tr>
        </thead>
        <tbody>
            <tr>
                <th scope="row">张三</th>
                <td>85</td>
                <td>92</td>
                <td>78</td>
                <td>255</td>
            </tr>
            <tr>
                <th scope="row">李四</th>
                <td>90</td>
                <td>88</td>
                <td>85</td>
                <td>263</td>
            </tr>
            <tr>
                <th scope="row">王五</th>
                <td>78</td>
                <td>95</td>
                <td>82</td>
                <td>255</td>
            </tr>
        </tbody>
    </table>

    <!-- 复杂表格示例:headers属性 -->
    <table>
        <caption>季度销售报告 - 使用headers属性建立复杂关联</caption>
        <thead>
            <tr>
                <th id="product" scope="col">产品</th>
                <th id="q1" scope="colgroup" colspan="2">第一季度</th>
                <th id="q2" scope="colgroup" colspan="2">第二季度</th>
            </tr>
            <tr>
                <th></th>
                <th id="q1-sales" scope="col">销量</th>
                <th id="q1-revenue" scope="col">收入</th>
                <th id="q2-sales" scope="col">销量</th>
                <th id="q2-revenue" scope="col">收入</th>
            </tr>
        </thead>
        <tbody>
            <tr>
                <th id="laptop" scope="row">笔记本电脑</th>
                <td headers="laptop q1 q1-sales">1200台</td>
                <td headers="laptop q1 q1-revenue">360万</td>
                <td headers="laptop q2 q2-sales">1500台</td>
                <td headers="laptop q2 q2-revenue">450万</td>
            </tr>
            <tr>
                <th id="phone" scope="row">智能手机</th>
                <td headers="phone q1 q1-sales">2800台</td>
                <td headers="phone q1 q1-revenue">420万</td>
                <td headers="phone q2 q2-sales">3200台</td>
                <td headers="phone q2 q2-revenue">480万</td>
            </tr>
        </tbody>
    </table>

    <div class="demo-info">
        <h3>可访问性要点:</h3>
        <ul>
            <li><strong>caption</strong>: 为表格提供清晰的标题和描述</li>
            <li><strong>scope="col"</strong>: 标识列表头</li>
            <li><strong>scope="row"</strong>: 标识行表头</li>
            <li><strong>scope="colgroup"</strong>: 标识列组表头</li>
            <li><strong>headers属性</strong>: 在复杂表格中建立单元格与表头的关联</li>
            <li><strong>id属性</strong>: 为表头提供唯一标识符,供headers属性引用</li>
        </ul>
        
        <p><strong>屏幕阅读器体验</strong>: 这些属性帮助屏幕阅读器用户理解表格结构,在浏览时能清楚知道当前单元格对应的行头和列头信息。</p>
    </div>

    <script>
        // 演示:表格线性化测试
        document.addEventListener('DOMContentLoaded', function() {
            console.log('表格可访问性检查:');
            
            // 检查所有表格是否有caption
            const tables = document.querySelectorAll('table');
            tables.forEach((table, index) => {
                const caption = table.querySelector('caption');
                console.log(`表格 ${index + 1}: ${caption ? '有caption ✓' : '缺少caption ✗'}`);
            });
            
            // 检查表头是否使用了scope或headers属性
            const headers = document.querySelectorAll('th');
            let accessibleHeaders = 0;
            headers.forEach(th => {
                if (th.hasAttribute('scope') || th.hasAttribute('headers')) {
                    accessibleHeaders++;
                }
            });
            console.log(`可访问的表头数量: ${accessibleHeaders}/${headers.length}`);
        });
    </script>
</body>
</html>

面试官视角:

要点清单:

  • 了解caption、scope、headers属性的作用
  • 知道表头单元格应使用th而非td
  • 理解col、colgroup、row等scope值的区别

加分项:

  • 提及ARIA标签在复杂表格中的应用
  • 了解表格线性化的概念
  • 知道如何为表格提供替代文本

常见失误:

  • 所有单元格都使用td元素
  • 忽略为表格提供标题和描述
  • 复杂表格缺少headers属性关联

延伸阅读:

如何处理复杂表格的合并单元格?

答案

核心概念:

复杂表格合并单元格的实现:

  • colspan - 水平合并单元格,跨越多列
  • rowspan - 垂直合并单元格,跨越多行
  • 合并后需要删除被覆盖的单元格
  • 保持表格结构的完整性和可访问性
  • 合理使用scope和headers属性

示例说明:

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>复杂表格合并单元格示例</title>
    <style>
        table {
            border-collapse: collapse;
            width: 100%;
            margin: 20px 0;
        }
        
        th, td {
            border: 1px solid #666;
            padding: 8px;
            text-align: center;
            vertical-align: middle;
        }
        
        th {
            background-color: #f0f0f0;
            font-weight: bold;
        }
        
        .merged-cell {
            background-color: #e3f2fd;
        }
        
        .demo-section {
            margin: 30px 0;
            padding: 20px;
            border: 1px solid #ddd;
            border-radius: 5px;
        }
        
        .code-example {
            background-color: #f8f9fa;
            padding: 15px;
            margin: 10px 0;
            border-left: 4px solid #007acc;
            font-family: monospace;
            white-space: pre-line;
        }
    </style>
</head>
<body>
    <div class="demo-section">
        <h3>示例1: colspan 水平合并</h3>
        <table>
            <caption>产品销售统计 - 水平合并示例</caption>
            <thead>
                <tr>
                    <th rowspan="2">产品类别</th>
                    <th colspan="3" class="merged-cell">2023年销售额(万元)</th>
                    <th rowspan="2">年度总计</th>
                </tr>
                <tr>
                    <th>Q1</th>
                    <th>Q2</th>
                    <th>Q3</th>
                    <!-- 注意:Q4列被上面的"年度总计"占用了 -->
                </tr>
            </thead>
            <tbody>
                <tr>
                    <th scope="row">电子产品</th>
                    <td>120</td>
                    <td>135</td>
                    <td>150</td>
                    <td>405</td>
                </tr>
                <tr>
                    <th scope="row">服装鞋帽</th>
                    <td>80</td>
                    <td>95</td>
                    <td>110</td>
                    <td>285</td>
                </tr>
            </tbody>
        </table>
        
        <div class="code-example">
关键代码:
&lt;th colspan="3" class="merged-cell"&gt;2023年销售额(万元)&lt;/th&gt;
&lt;th rowspan="2"&gt;年度总计&lt;/th&gt;

说明:
- colspan="3" 表示该单元格水平跨越3列
- rowspan="2" 表示该单元格垂直跨越2行
        </div>
    </div>

    <div class="demo-section">
        <h3>示例2: rowspan 垂直合并</h3>
        <table>
            <caption>部门人员组织架构表</caption>
            <thead>
                <tr>
                    <th>部门</th>
                    <th>职位</th>
                    <th>姓名</th>
                    <th>工号</th>
                </tr>
            </thead>
            <tbody>
                <tr>
                    <th rowspan="3" class="merged-cell" scope="rowgroup">技术部</th>
                    <td>部门经理</td>
                    <td>张三</td>
                    <td>T001</td>
                </tr>
                <tr>
                    <!-- 注意:这里不需要部门单元格,因为被上面的rowspan占用了 -->
                    <td>高级工程师</td>
                    <td>李四</td>
                    <td>T002</td>
                </tr>
                <tr>
                    <td>初级工程师</td>
                    <td>王五</td>
                    <td>T003</td>
                </tr>
                <tr>
                    <th rowspan="2" class="merged-cell" scope="rowgroup">市场部</th>
                    <td>部门经理</td>
                    <td>赵六</td>
                    <td>M001</td>
                </tr>
                <tr>
                    <td>市场专员</td>
                    <td>孙七</td>
                    <td>M002</td>
                </tr>
            </tbody>
        </table>
        
        <div class="code-example">
关键代码:
&lt;th rowspan="3" scope="rowgroup"&gt;技术部&lt;/th&gt;

说明:
- rowspan="3" 表示该单元格垂直跨越3行
- scope="rowgroup" 表示该表头适用于一组行
- 被合并的行中不能包含对应位置的单元格
        </div>
    </div>

    <div class="demo-section">
        <h3>示例3: 复杂合并(colspan + rowspan)</h3>
        <table>
            <caption>综合绩效评估表</caption>
            <thead>
                <tr>
                    <th rowspan="2">员工姓名</th>
                    <th colspan="2" class="merged-cell">技术能力</th>
                    <th colspan="2" class="merged-cell">软技能</th>
                    <th rowspan="2">综合评分</th>
                </tr>
                <tr>
                    <th>编程</th>
                    <th>架构</th>
                    <th>沟通</th>
                    <th>团队</th>
                </tr>
            </thead>
            <tbody>
                <tr>
                    <th scope="row">张三</th>
                    <td>85</td>
                    <td>90</td>
                    <td>88</td>
                    <td>92</td>
                    <td class="merged-cell">89</td>
                </tr>
                <tr>
                    <th scope="row">李四</th>
                    <td>92</td>
                    <td>85</td>
                    <td>90</td>
                    <td>87</td>
                    <td class="merged-cell">88.5</td>
                </tr>
                <tr>
                    <th colspan="5" class="merged-cell">部门平均分</th>
                    <td>88.75</td>
                </tr>
            </tbody>
        </table>
        
        <div class="code-example">
复杂合并要点:
1. 计算合并后的表格布局
2. 确保每行的列数一致
3. 被合并的单元格位置不能重复定义
4. 保持表格的可访问性属性

常见错误:
❌ 合并后忘记删除被覆盖的单元格
❌ colspan/rowspan数值计算错误
❌ 忽略合并单元格的scope属性
        </div>
    </div>

    <div class="demo-section">
        <h3>响应式表格处理</h3>
        <style>
            .responsive-table {
                display: block;
                overflow-x: auto;
                white-space: nowrap;
            }
            
            @media (max-width: 600px) {
                .mobile-stack {
                    display: block;
                    width: 100%;
                }
                
                .mobile-stack thead,
                .mobile-stack tbody,
                .mobile-stack th,
                .mobile-stack td,
                .mobile-stack tr {
                    display: block;
                }
                
                .mobile-stack tr {
                    border: 1px solid #ccc;
                    margin-bottom: 10px;
                    padding: 10px;
                }
                
                .mobile-stack td::before {
                    content: attr(data-label) ": ";
                    font-weight: bold;
                    display: inline-block;
                    width: 100px;
                }
            }
        </style>
        
        <div class="responsive-table">
            <table class="mobile-stack">
                <caption>响应式表格示例</caption>
                <thead>
                    <tr>
                        <th>产品</th>
                        <th>价格</th>
                        <th>库存</th>
                        <th>状态</th>
                    </tr>
                </thead>
                <tbody>
                    <tr>
                        <td data-label="产品">笔记本电脑</td>
                        <td data-label="价格">¥5999</td>
                        <td data-label="库存">32台</td>
                        <td data-label="状态">有货</td>
                    </tr>
                    <tr>
                        <td data-label="产品">智能手机</td>
                        <td data-label="价格">¥2999</td>
                        <td data-label="库存">0台</td>
                        <td data-label="状态">缺货</td>
                    </tr>
                </tbody>
            </table>
        </div>
        
        <p><strong>提示:</strong> 调整浏览器窗口大小查看响应式效果。在移动设备上,复杂表格通常需要特殊处理以保证可用性。</p>
    </div>

    <script>
        // 表格调试工具
        function analyzeTable(tableIndex = 0) {
            const table = document.querySelectorAll('table')[tableIndex];
            if (!table) return;
            
            const rows = table.querySelectorAll('tr');
            console.log(`表格 ${tableIndex + 1} 分析:`);
            console.log(`总行数:${rows.length}`);
            
            rows.forEach((row, rowIndex) => {
                const cells = row.querySelectorAll('th, td');
                let cellCount = 0;
                
                cells.forEach(cell => {
                    const colspan = parseInt(cell.getAttribute('colspan')) || 1;
                    cellCount += colspan;
                });
                
                console.log(`第${rowIndex + 1}行:${cells.length}个单元格,占用${cellCount}列`);
            });
        }
        
        // 页面加载后分析所有表格
        document.addEventListener('DOMContentLoaded', function() {
            const tables = document.querySelectorAll('table');
            console.log(`页面包含 ${tables.length} 个表格`);
            
            // 分析每个表格
            for (let i = 0; i < tables.length; i++) {
                analyzeTable(i);
            }
        });
    </script>
</body>
</html>

面试官视角:

要点清单:

  • 正确理解colspan和rowspan的计算方式
  • 知道合并后要删除多余的单元格
  • 能处理复杂的表头和数据关联

加分项:

  • 了解如何为合并单元格设置合适的scope
  • 知道使用headers属性建立复杂关联
  • 考虑响应式设计下的表格处理

常见失误:

  • 合并后忘记删除被覆盖的单元格
  • colspan/rowspan计算错误导致表格错位
  • 忽略合并单元格的可访问性处理

延伸阅读:

input 类型有哪些?

答案

HTML <input> 标签的 type 属性支持多种类型,决定了输入控件的表现和用途。常见类型如下:

  • 文本输入
    • text:单行文本输入
    • password:密码输入,内容以掩码显示
    • search:搜索框,样式略有不同
  • 数值输入
    • number:数值输入,可限制范围和步长
    • range:滑块选择数值
  • 日期与时间
    • date:日期选择器
    • time:时间选择器
    • datetime-local:本地日期时间选择
    • monthweek:选择月份或周
  • 选择类型
    • checkbox:复选框,可多选
    • radio:单选按钮,配合 name 属性实现单选
  • 按钮类型
    • submit:提交表单
    • reset:重置表单
    • button:普通按钮
  • 其他
    • email:邮箱输入,带格式校验
    • url:网址输入,带格式校验
    • tel:电话号码输入
    • file:文件上传,可多选
    • color:颜色选择器
    • hidden:隐藏字段,不显示但可提交
<input type="text">
<input type="password">
<input type="number" min="0" max="100">
<input type="date">
<input type="checkbox">
<input type="file" multiple>
提示

实际开发中,合理选择 type 能提升表单体验和数据校验准确性。例如,emailurl 会自动校验格式,number 可用原生控件限制输入范围。

注意

部分类型如 datecolor 在旧版浏览器或部分移动端兼容性有限,需注意回退方案。

延伸阅读

<details><summary> 元素如何使用?

答案

核心概念:

<details><summary> 是HTML5提供的原生折叠/展开组件:

  • <details> - 创建可展开的内容区域容器
  • <summary> - 定义折叠区域的标题/摘要,点击可切换显示状态
  • open 属性 - 控制初始展开状态
  • 原生支持键盘导航和无障碍访问
  • 可通过CSS自定义样式,JavaScript监听toggle事件

示例说明:

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Details 和 Summary 元素示例</title>
    <style>
        body {
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
            max-width: 800px;
            margin: 0 auto;
            padding: 20px;
            line-height: 1.6;
        }

        .demo-section {
            margin: 30px 0;
            padding: 20px;
            border: 1px solid #e1e5e9;
            border-radius: 8px;
            background-color: #f8f9fa;
        }

        /* 基础样式 */
        details {
            border: 1px solid #d0d7de;
            border-radius: 6px;
            padding: 16px;
            margin: 16px 0;
            background-color: white;
        }

        summary {
            font-weight: 600;
            cursor: pointer;
            padding: 8px 0;
            list-style: none;
            outline: none;
        }

        /* 隐藏默认的三角形标记 */
        summary::-webkit-details-marker {
            display: none;
        }

        /* 自定义展开/折叠图标 */
        .custom-details summary {
            position: relative;
            padding-left: 30px;
        }

        .custom-details summary::before {
            content: "▶";
            position: absolute;
            left: 8px;
            top: 50%;
            transform: translateY(-50%);
            transition: transform 0.2s ease;
            color: #656d76;
        }

        .custom-details[open] summary::before {
            transform: translateY(-50%) rotate(90deg);
        }

        /* 动画效果 */
        .animated-details {
            overflow: hidden;
        }

        .animated-details summary {
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            color: white;
            margin: -16px -16px 16px -16px;
            padding: 16px;
            border-radius: 6px 6px 0 0;
        }

        .animated-details[open] summary {
            border-radius: 6px 6px 0 0;
        }

        /* 嵌套样式 */
        .nested-details {
            background-color: #f6f8fa;
            margin-left: 20px;
            border-left: 3px solid #d0d7de;
            border-radius: 0 6px 6px 0;
        }

        /* FAQ样式 */
        .faq-item {
            border-bottom: 1px solid #e1e5e9;
        }

        .faq-item:last-child {
            border-bottom: none;
        }

        .faq-item summary {
            color: #0969da;
            font-size: 1.1em;
        }

        .faq-item summary:hover {
            color: #0550ae;
        }

        /* 状态指示 */
        .status-indicator {
            display: inline-block;
            width: 8px;
            height: 8px;
            border-radius: 50%;
            margin-right: 8px;
        }

        .status-closed {
            background-color: #d1242f;
        }

        .status-open {
            background-color: #1a7f37;
        }

        /* 代码示例样式 */
        .code-example {
            background-color: #f6f8fa;
            border: 1px solid #d0d7de;
            border-radius: 6px;
            padding: 16px;
            margin: 16px 0;
            font-family: 'Consolas', 'Monaco', monospace;
            font-size: 14px;
            overflow-x: auto;
        }
    </style>
</head>
<body>
    <h1>Details 和 Summary 元素完整示例</h1>

    <div class="demo-section">
        <h2>1. 基础用法</h2>
        <details>
            <summary>什么是 HTML5?</summary>
            <p>HTML5 是超文本标记语言的第五个重大版本,引入了许多新的语义化元素、API和功能。它改善了网页的结构、样式和交互能力,使开发者能够创建更加丰富和动态的网页应用。</p>
        </details>

        <details open>
            <summary>默认展开的内容(使用 open 属性)</summary>
            <p>通过在 &lt;details&gt; 标签上添加 <code>open</code> 属性,可以让内容默认处于展开状态。</p>
        </details>
    </div>

    <div class="demo-section">
        <h2>2. 自定义样式</h2>
        <details class="custom-details">
            <summary>自定义展开图标</summary>
            <p>使用 CSS 可以隐藏默认的三角形标记,并添加自定义的图标。这里使用了 CSS 伪元素和过渡动画。</p>
            <div class="code-example">
summary::-webkit-details-marker {
    display: none;
}

summary::before {
    content: "▶";
    transition: transform 0.2s ease;
}

details[open] summary::before {
    transform: rotate(90deg);
}
            </div>
        </details>

        <details class="animated-details">
            <summary>带动画效果的样式</summary>
            <p>通过 CSS 渐变背景和过渡动画,可以创建更加吸引人的视觉效果。</p>
            <p>这种样式特别适合用于产品介绍、功能说明等需要突出显示的内容。</p>
        </details>
    </div>

    <div class="demo-section">
        <h2>3. 嵌套使用</h2>
        <details>
            <summary>前端技术栈</summary>
            <p>现代前端开发涉及多个层面的技术:</p>
            
            <details class="nested-details">
                <summary>HTML/CSS</summary>
                <ul>
                    <li>语义化HTML结构</li>
                    <li>响应式CSS布局</li>
                    <li>CSS预处理器(Sass、Less)</li>
                    <li>CSS框架(Bootstrap、Tailwind)</li>
                </ul>
            </details>

            <details class="nested-details">
                <summary>JavaScript</summary>
                <ul>
                    <li>ES6+ 语法特性</li>
                    <li>DOM 操作和事件处理</li>
                    <li>异步编程(Promise、async/await)</li>
                    <li>模块化开发</li>
                </ul>
            </details>

            <details class="nested-details">
                <summary>框架和工具</summary>
                <ul>
                    <li>React/Vue/Angular</li>
                    <li>Webpack/Vite 构建工具</li>
                    <li>TypeScript 类型系统</li>
                    <li>Git 版本控制</li>
                </ul>
            </details>
        </details>
    </div>

    <div class="demo-section">
        <h2>4. FAQ 常见问题</h2>
        <details class="faq-item">
            <summary>如何检查浏览器对 details 元素的支持?</summary>
            <p>可以使用 JavaScript 检测浏览器支持:</p>
            <div class="code-example">
if ('open' in document.createElement('details')) {
    console.log('浏览器支持 details 元素');
} else {
    console.log('需要使用 polyfill');
}
            </div>
        </details>

        <details class="faq-item">
            <summary>details 元素的无障碍访问特性</summary>
            <p>details 元素具有内置的无障碍访问支持:</p>
            <ul>
                <li>支持键盘导航(空格键或回车键切换)</li>
                <li>屏幕阅读器能正确识别展开/折叠状态</li>
                <li>自动管理 aria-expanded 属性</li>
                <li>提供语义化的内容结构</li>
            </ul>
        </details>

        <details class="faq-item">
            <summary>如何监听 details 的展开/折叠事件?</summary>
            <p>可以监听 toggle 事件来响应状态变化:</p>
            <div class="code-example">
document.addEventListener('toggle', function(event) {
    if (event.target.tagName === 'DETAILS') {
        console.log('Details 状态改变:', event.target.open);
    }
});
            </div>
        </details>
    </div>

    <div class="demo-section">
        <h2>5. 实时状态指示</h2>
        <div id="status-demo">
            <details class="status-details">
                <summary>
                    <span class="status-indicator status-closed"></span>
                    服务器状态监控
                </summary>
                <p>当前所有服务正常运行,响应时间良好。</p>
                <ul>
                    <li>API 服务器: ✅ 正常</li>
                    <li>数据库: ✅ 正常</li>
                    <li>CDN: ✅ 正常</li>
                    <li>缓存服务: ✅ 正常</li>
                </ul>
            </details>
        </div>
    </div>

    <div class="demo-section">
        <h2>6. JavaScript 控制</h2>
        <button onclick="toggleAllDetails()">切换所有折叠区域</button>
        <button onclick="openAllDetails()">展开所有</button>
        <button onclick="closeAllDetails()">折叠所有</button>
        
        <div style="margin-top: 16px;">
            <details id="js-details-1">
                <summary>JavaScript 控制示例 1</summary>
                <p>这个区域可以通过上面的按钮进行控制。</p>
            </details>

            <details id="js-details-2">
                <summary>JavaScript 控制示例 2</summary>
                <p>演示如何批量操作多个 details 元素。</p>
            </details>
        </div>
    </div>

    <script>
        // 监听所有 details 元素的 toggle 事件
        document.addEventListener('toggle', function(event) {
            if (event.target.tagName === 'DETAILS') {
                const statusIndicator = event.target.querySelector('.status-indicator');
                if (statusIndicator) {
                    if (event.target.open) {
                        statusIndicator.className = 'status-indicator status-open';
                    } else {
                        statusIndicator.className = 'status-indicator status-closed';
                    }
                }
                
                console.log(`Details "${event.target.querySelector('summary').textContent.trim()}" 
                    ${event.target.open ? '已展开' : '已折叠'}`);
            }
        });

        // 控制函数
        function toggleAllDetails() {
            const details = document.querySelectorAll('#js-details-1, #js-details-2');
            details.forEach(detail => {
                detail.open = !detail.open;
            });
        }

        function openAllDetails() {
            const details = document.querySelectorAll('#js-details-1, #js-details-2');
            details.forEach(detail => {
                detail.open = true;
            });
        }

        function closeAllDetails() {
            const details = document.querySelectorAll('#js-details-1, #js-details-2');
            details.forEach(detail => {
                detail.open = false;
            });
        }

        // 页面加载完成后的统计
        document.addEventListener('DOMContentLoaded', function() {
            const allDetails = document.querySelectorAll('details');
            const openDetails = document.querySelectorAll('details[open]');
            
            console.log(`页面包含 ${allDetails.length} 个 details 元素,
                其中 ${openDetails.length} 个默认展开`);
        });

        // 键盘快捷键支持
        document.addEventListener('keydown', function(event) {
            // Ctrl + E 展开所有
            if (event.ctrlKey && event.key === 'e') {
                event.preventDefault();
                openAllDetails();
            }
            // Ctrl + R 折叠所有  
            if (event.ctrlKey && event.key === 'r') {
                event.preventDefault();
                closeAllDetails();
            }
        });
    </script>

    <div style="margin-top: 40px; padding: 20px; background-color: #f0f8ff; border-left: 4px solid #0969da;">
        <h3>💡 使用技巧</h3>
        <ul>
            <li><strong>键盘支持</strong>: 使用空格键或回车键切换展开/折叠状态</li>
            <li><strong>快捷键</strong>: Ctrl+E 展开所有,Ctrl+R 折叠所有</li>
            <li><strong>状态监听</strong>: 查看控制台了解 toggle 事件的触发</li>
            <li><strong>样式定制</strong>: 通过 CSS 可以完全自定义外观和动画</li>
            <li><strong>无障碍</strong>: 原生支持屏幕阅读器和键盘导航</li>
        </ul>
    </div>
</body>
</html>

面试官视角:

要点清单:

  • 了解details/summary的基本语法和语义
  • 知道open属性的作用和控制方式
  • 理解原生元素相比自定义实现的优势

加分项:

  • 提及无障碍访问的内置支持
  • 了解toggle事件的使用
  • 知道如何通过CSS自定义展开/折叠图标

常见失误:

  • 在summary外部放置可点击元素
  • 忽略键盘导航的支持
  • 不了解浏览器兼容性问题

延伸阅读:

<dialog> 元素的功能和使用场景?

答案

核心概念:

<dialog> 是HTML5.2引入的原生对话框元素:

  • 提供模态和非模态对话框功能
  • showModal() 方法创建模态对话框,阻止与背景页面交互
  • show() 方法创建非模态对话框
  • close() 方法关闭对话框,可传递返回值
  • 内置焦点管理和键盘导航支持
  • 原生支持ESC键关闭和背景点击关闭

示例说明:

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Dialog 元素完整示例</title>
    <style>
        body {
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
            max-width: 1000px;
            margin: 0 auto;
            padding: 20px;
            line-height: 1.6;
        }

        .demo-section {
            margin: 30px 0;
            padding: 20px;
            border: 1px solid #e1e5e9;
            border-radius: 8px;
            background-color: #f8f9fa;
        }

        /* 按钮样式 */
        button {
            background: #0969da;
            color: white;
            border: none;
            padding: 10px 16px;
            border-radius: 6px;
            cursor: pointer;
            margin: 8px 8px 8px 0;
            font-size: 14px;
            transition: background-color 0.2s;
        }

        button:hover {
            background: #0550ae;
        }

        button:disabled {
            background: #8c959f;
            cursor: not-allowed;
        }

        .btn-secondary {
            background: #656d76;
        }

        .btn-secondary:hover {
            background: #4c535a;
        }

        .btn-danger {
            background: #d1242f;
        }

        .btn-danger:hover {
            background: #a40e26;
        }

        /* Dialog 基础样式 */
        dialog {
            border: none;
            border-radius: 8px;
            box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12);
            padding: 0;
            max-width: 500px;
            width: 90vw;
        }

        /* 模态对话框背景 */
        dialog::backdrop {
            background: rgba(0, 0, 0, 0.5);
            backdrop-filter: blur(4px);
        }

        /* Dialog 头部 */
        .dialog-header {
            padding: 20px 24px 16px;
            border-bottom: 1px solid #e1e5e9;
            background: #f6f8fa;
            border-radius: 8px 8px 0 0;
        }

        .dialog-title {
            margin: 0;
            font-size: 18px;
            font-weight: 600;
            color: #24292f;
        }

        /* Dialog 内容 */
        .dialog-content {
            padding: 20px 24px;
        }

        /* Dialog 底部 */
        .dialog-footer {
            padding: 16px 24px 20px;
            border-top: 1px solid #e1e5e9;
            background: #f6f8fa;
            text-align: right;
            border-radius: 0 0 8px 8px;
        }

        /* 表单样式 */
        .form-group {
            margin-bottom: 16px;
        }

        label {
            display: block;
            margin-bottom: 6px;
            font-weight: 500;
        }

        input[type="text"],
        input[type="email"],
        textarea {
            width: 100%;
            padding: 8px 12px;
            border: 1px solid #d0d7de;
            border-radius: 6px;
            font-size: 14px;
            box-sizing: border-box;
        }

        input:focus,
        textarea:focus {
            outline: none;
            border-color: #0969da;
            box-shadow: 0 0 0 3px rgba(9, 105, 218, 0.1);
        }

        /* 确认对话框样式 */
        .confirm-dialog {
            max-width: 400px;
        }

        .confirm-content {
            text-align: center;
            padding: 32px 24px;
        }

        .confirm-icon {
            font-size: 48px;
            margin-bottom: 16px;
        }

        .warning-icon {
            color: #d1242f;
        }

        .info-icon {
            color: #0969da;
        }

        .success-icon {
            color: #1a7f37;
        }

        /* 非模态对话框样式 */
        .non-modal-dialog {
            position: fixed;
            top: 20px;
            right: 20px;
            max-width: 320px;
            border: 1px solid #d0d7de;
            background: white;
        }

        /* 进度对话框 */
        .progress-bar {
            width: 100%;
            height: 8px;
            background: #e1e5e9;
            border-radius: 4px;
            margin: 16px 0;
            overflow: hidden;
        }

        .progress-fill {
            height: 100%;
            background: linear-gradient(90deg, #0969da, #2ea043);
            border-radius: 4px;
            transition: width 0.3s ease;
            width: 0%;
        }

        /* 状态消息 */
        .status-message {
            margin: 16px 0;
            padding: 12px;
            border-radius: 6px;
            font-size: 14px;
        }

        .status-success {
            background: #dafbe1;
            color: #1a7f37;
            border: 1px solid #a2eeaf;
        }

        .status-error {
            background: #ffebe9;
            color: #d1242f;
            border: 1px solid #ffb3ab;
        }

        .status-info {
            background: #ddf4ff;
            color: #0969da;
            border: 1px solid #80ccff;
        }

        /* 代码示例 */
        .code-block {
            background: #f6f8fa;
            border: 1px solid #d0d7de;
            border-radius: 6px;
            padding: 16px;
            margin: 16px 0;
            font-family: 'Consolas', 'Monaco', monospace;
            font-size: 13px;
            overflow-x: auto;
        }

        /* 响应式 */
        @media (max-width: 640px) {
            dialog {
                width: 95vw;
                margin: 10px;
            }
            
            .dialog-header,
            .dialog-content,
            .dialog-footer {
                padding-left: 16px;
                padding-right: 16px;
            }
        }
    </style>
</head>
<body>
    <h1>Dialog 元素完整示例</h1>

    <div class="demo-section">
        <h2>1. 基础模态对话框</h2>
        <p>使用 <code>showModal()</code> 方法创建模态对话框,会阻止与背景页面的交互。</p>
        
        <button onclick="openBasicDialog()">打开基础对话框</button>
        <button onclick="openFormDialog()">打开表单对话框</button>

        <!-- 基础对话框 -->
        <dialog id="basicDialog">
            <div class="dialog-header">
                <h3 class="dialog-title">提示信息</h3>
            </div>
            <div class="dialog-content">
                <p>这是一个基础的模态对话框示例。点击确定或取消按钮来关闭对话框。</p>
                <p>你也可以按 ESC 键关闭对话框。</p>
            </div>
            <div class="dialog-footer">
                <button type="button" class="btn-secondary" onclick="closeDialog('basicDialog')">取消</button>
                <button type="button" onclick="closeDialog('basicDialog', 'confirm')">确定</button>
            </div>
        </dialog>

        <!-- 表单对话框 -->
        <dialog id="formDialog">
            <form method="dialog" onsubmit="handleFormSubmit(event)">
                <div class="dialog-header">
                    <h3 class="dialog-title">用户信息</h3>
                </div>
                <div class="dialog-content">
                    <div class="form-group">
                        <label for="userName">姓名:</label>
                        <input type="text" id="userName" name="userName" required>
                    </div>
                    <div class="form-group">
                        <label for="userEmail">邮箱:</label>
                        <input type="email" id="userEmail" name="userEmail" required>
                    </div>
                    <div class="form-group">
                        <label for="userNote">备注:</label>
                        <textarea id="userNote" name="userNote" rows="3"></textarea>
                    </div>
                </div>
                <div class="dialog-footer">
                    <button type="button" class="btn-secondary" onclick="closeDialog('formDialog')">取消</button>
                    <button type="submit">提交</button>
                </div>
            </form>
        </dialog>
    </div>

    <div class="demo-section">
        <h2>2. 确认对话框</h2>
        <p>用于重要操作的确认提示,通常包含警告图标和明确的操作描述。</p>
        
        <button onclick="openConfirmDialog()">删除操作确认</button>
        <button onclick="openInfoDialog()">信息提示</button>

        <!-- 确认对话框 -->
        <dialog id="confirmDialog" class="confirm-dialog">
            <div class="confirm-content">
                <div class="confirm-icon warning-icon">⚠️</div>
                <h3>确认删除</h3>
                <p>您确定要删除这个项目吗?此操作无法撤销。</p>
                <div style="margin-top: 24px;">
                    <button type="button" class="btn-secondary" onclick="closeDialog('confirmDialog')">取消</button>
                    <button type="button" class="btn-danger" onclick="confirmDelete()">删除</button>
                </div>
            </div>
        </dialog>

        <!-- 信息对话框 -->
        <dialog id="infoDialog" class="confirm-dialog">
            <div class="confirm-content">
                <div class="confirm-icon info-icon">ℹ️</div>
                <h3>操作完成</h3>
                <p>您的操作已成功完成!</p>
                <div style="margin-top: 24px;">
                    <button type="button" onclick="closeDialog('infoDialog')">确定</button>
                </div>
            </div>
        </dialog>
    </div>

    <div class="demo-section">
        <h2>3. 非模态对话框</h2>
        <p>使用 <code>show()</code> 方法创建非模态对话框,用户仍可与背景页面交互。</p>
        
        <button onclick="openNonModalDialog()">打开通知</button>
        <button onclick="openProgressDialog()">显示进度</button>

        <!-- 非模态对话框 -->
        <dialog id="nonModalDialog" class="non-modal-dialog">
            <div class="dialog-header">
                <h4 class="dialog-title">系统通知</h4>
            </div>
            <div class="dialog-content">
                <p>这是一个非模态对话框,您可以继续与页面其他部分交互。</p>
                <button onclick="closeDialog('nonModalDialog')" style="width: 100%;">关闭通知</button>
            </div>
        </dialog>

        <!-- 进度对话框 -->
        <dialog id="progressDialog">
            <div class="dialog-header">
                <h3 class="dialog-title">处理中...</h3>
            </div>
            <div class="dialog-content">
                <p>正在处理您的请求,请稍候。</p>
                <div class="progress-bar">
                    <div class="progress-fill" id="progressFill"></div>
                </div>
                <div id="progressText">0%</div>
            </div>
            <div class="dialog-footer">
                <button type="button" class="btn-secondary" onclick="cancelProgress()" id="cancelBtn">取消</button>
            </div>
        </dialog>
    </div>

    <div class="demo-section">
        <h2>4. JavaScript API 演示</h2>
        <div class="code-block">
// 打开模态对话框
dialog.showModal();

// 打开非模态对话框  
dialog.show();

// 关闭对话框(可选返回值)
dialog.close('returnValue');

// 监听对话框关闭事件
dialog.addEventListener('close', function() {
    console.log('返回值:', dialog.returnValue);
});

// 检查对话框状态
console.log('是否打开:', dialog.open);
        </div>

        <button onclick="showAPI()">测试 API</button>
        <button onclick="clearLog()">清空日志</button>
        <div id="apiLog" class="status-message status-info" style="display: none;"></div>
    </div>

    <div class="demo-section">
        <h2>5. 浏览器兼容性检查</h2>
        <div id="supportInfo"></div>
    </div>

    <script>
        // 对话框控制函数
        function openBasicDialog() {
            document.getElementById('basicDialog').showModal();
        }

        function openFormDialog() {
            document.getElementById('formDialog').showModal();
        }

        function openConfirmDialog() {
            document.getElementById('confirmDialog').showModal();
        }

        function openInfoDialog() {
            document.getElementById('infoDialog').showModal();
        }

        function openNonModalDialog() {
            document.getElementById('nonModalDialog').show();
        }

        function openProgressDialog() {
            const dialog = document.getElementById('progressDialog');
            dialog.showModal();
            startProgress();
        }

        function closeDialog(dialogId, returnValue = '') {
            const dialog = document.getElementById(dialogId);
            dialog.close(returnValue);
        }

        // 表单提交处理
        function handleFormSubmit(event) {
            event.preventDefault();
            const formData = new FormData(event.target);
            const data = Object.fromEntries(formData);
            
            // 模拟处理
            console.log('表单数据:', data);
            
            // 显示成功消息
            showStatusMessage('表单提交成功!', 'success');
            
            // 关闭对话框
            closeDialog('formDialog', 'submitted');
            
            // 重置表单
            event.target.reset();
        }

        // 确认删除
        function confirmDelete() {
            showStatusMessage('项目已删除', 'success');
            closeDialog('confirmDialog', 'deleted');
        }

        // 进度条控制
        let progressInterval;
        function startProgress() {
            let progress = 0;
            const progressFill = document.getElementById('progressFill');
            const progressText = document.getElementById('progressText');
            const cancelBtn = document.getElementById('cancelBtn');
            
            progressInterval = setInterval(() => {
                progress += Math.random() * 15;
                if (progress >= 100) {
                    progress = 100;
                    clearInterval(progressInterval);
                    progressText.textContent = '完成!';
                    cancelBtn.textContent = '关闭';
                    cancelBtn.onclick = () => closeDialog('progressDialog', 'completed');
                } else {
                    progressText.textContent = Math.round(progress) + '%';
                }
                progressFill.style.width = progress + '%';
            }, 500);
        }

        function cancelProgress() {
            if (progressInterval) {
                clearInterval(progressInterval);
                document.getElementById('progressFill').style.width = '0%';
                document.getElementById('progressText').textContent = '0%';
                document.getElementById('cancelBtn').textContent = '取消';
                document.getElementById('cancelBtn').onclick = cancelProgress;
            }
            closeDialog('progressDialog', 'cancelled');
        }

        // 状态消息显示
        function showStatusMessage(message, type = 'info') {
            const existing = document.querySelector('.temp-status');
            if (existing) existing.remove();
            
            const div = document.createElement('div');
            div.className = `status-message status-${type} temp-status`;
            div.textContent = message;
            div.style.position = 'fixed';
            div.style.top = '20px';
            div.style.left = '50%';
            div.style.transform = 'translateX(-50%)';
            div.style.zIndex = '1000';
            div.style.maxWidth = '400px';
            
            document.body.appendChild(div);
            
            setTimeout(() => {
                div.remove();
            }, 3000);
        }

        // API 演示
        function showAPI() {
            const log = document.getElementById('apiLog');
            log.style.display = 'block';
            
            // 创建测试对话框
            const testDialog = document.createElement('dialog');
            testDialog.innerHTML = `
                <div class="dialog-content">
                    <h4>API 测试对话框</h4>
                    <p>这是通过 JavaScript 创建的对话框</p>
                    <button onclick="this.closest('dialog').close('api-test')">关闭</button>
                </div>
            `;
            
            document.body.appendChild(testDialog);
            
            // 添加事件监听
            testDialog.addEventListener('close', function() {
                log.innerHTML += `<br>对话框已关闭,返回值: "${this.returnValue}"`;
                document.body.removeChild(this);
            });
            
            log.innerHTML = '对话框已创建并打开...';
            testDialog.showModal();
        }

        function clearLog() {
            document.getElementById('apiLog').style.display = 'none';
        }

        // 监听所有对话框的关闭事件
        document.addEventListener('DOMContentLoaded', function() {
            const dialogs = document.querySelectorAll('dialog');
            dialogs.forEach(dialog => {
                dialog.addEventListener('close', function() {
                    console.log(`对话框 ${this.id} 关闭,返回值:`, this.returnValue);
                });
                
                // ESC 键关闭事件
                dialog.addEventListener('cancel', function() {
                    console.log(`对话框 ${this.id} 被 ESC 键关闭`);
                });
            });
            
            // 检查浏览器支持
            checkDialogSupport();
        });

        // 背景点击关闭模态对话框
        document.addEventListener('click', function(event) {
            if (event.target.tagName === 'DIALOG') {
                const rect = event.target.getBoundingClientRect();
                const isInDialog = (
                    rect.top <= event.clientY && event.clientY <= rect.top + rect.height &&
                    rect.left <= event.clientX && event.clientX <= rect.left + rect.width
                );
                if (!isInDialog) {
                    event.target.close();
                }
            }
        });

        // 浏览器支持检查
        function checkDialogSupport() {
            const supportInfo = document.getElementById('supportInfo');
            
            if (typeof HTMLDialogElement === 'function') {
                supportInfo.innerHTML = `
                    <div class="status-success">
                        ✅ 您的浏览器支持原生 Dialog 元素
                    </div>
                `;
            } else {
                supportInfo.innerHTML = `
                    <div class="status-error">
                        ❌ 您的浏览器不支持原生 Dialog 元素<br>
                        建议使用 Chrome 37+、Firefox 98+、Safari 15.4+ 或相应的 Polyfill
                    </div>
                `;
            }
        }
    </script>

    <div style="margin-top: 40px; padding: 20px; background-color: #f0f8ff; border-left: 4px solid #0969da;">
        <h3>💡 Dialog 元素特性总结</h3>
        <ul>
            <li><strong>模态vs非模态</strong>: showModal() 创建模态对话框,show() 创建非模态对话框</li>
            <li><strong>返回值</strong>: close(value) 可以设置返回值,通过 returnValue 属性获取</li>
            <li><strong>事件监听</strong>: 支持 close 和 cancel 事件监听</li>
            <li><strong>键盘支持</strong>: ESC 键自动关闭模态对话框</li>
            <li><strong>焦点管理</strong>: 自动管理焦点转移和恢复</li>
            <li><strong>背景样式</strong>: ::backdrop 伪元素可自定义模态背景</li>
            <li><strong>表单集成</strong>: method="dialog" 可将表单与对话框关联</li>
        </ul>
    </div>
</body>
</html>

面试官视角:

要点清单:

  • 区分模态和非模态对话框的使用场景
  • 了解show()、showModal()、close()方法
  • 知道如何处理对话框的返回值

加分项:

  • 理解对话框的焦点管理机制
  • 了解::backdrop伪元素的样式定制
  • 知道浏览器的Polyfill解决方案

常见失误:

  • 混淆show()和showModal()的使用
  • 忽略对话框的无障碍访问
  • 不处理对话框外部点击的逻辑

延伸阅读:

如何创建现代化的交互式组件?

答案

核心概念:

现代HTML交互式组件设计原则:

  • 语义化优先 - 使用原生HTML元素作为基础
  • 渐进增强 - 从基础功能逐步添加交互特性
  • 无障碍友好 - 支持键盘导航、屏幕阅读器
  • 响应式设计 - 适配不同设备和交互方式
  • 可定制样式 - 通过CSS自定义外观
  • 事件驱动 - 合理使用JavaScript增强交互

示例说明:

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>现代化交互式组件示例</title>
    <style>
        * {
            box-sizing: border-box;
        }

        body {
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
            max-width: 1200px;
            margin: 0 auto;
            padding: 20px;
            line-height: 1.6;
            color: #24292f;
        }

        .demo-grid {
            display: grid;
            grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
            gap: 24px;
            margin: 24px 0;
        }

        .demo-card {
            background: white;
            border: 1px solid #d0d7de;
            border-radius: 8px;
            padding: 24px;
            box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
        }

        .demo-card h3 {
            margin-top: 0;
            color: #0969da;
            border-bottom: 1px solid #e1e5e9;
            padding-bottom: 8px;
        }

        /* 自定义选择器组件 */
        .custom-select {
            position: relative;
            display: inline-block;
            width: 100%;
            margin: 12px 0;
        }

        .select-trigger {
            background: white;
            border: 1px solid #d0d7de;
            border-radius: 6px;
            padding: 8px 32px 8px 12px;
            cursor: pointer;
            user-select: none;
            position: relative;
            width: 100%;
            display: flex;
            align-items: center;
            justify-content: space-between;
            transition: border-color 0.2s;
        }

        .select-trigger:hover {
            border-color: #0969da;
        }

        .select-trigger[aria-expanded="true"] {
            border-color: #0969da;
            box-shadow: 0 0 0 3px rgba(9, 105, 218, 0.1);
        }

        .select-arrow {
            transition: transform 0.2s;
        }

        .select-trigger[aria-expanded="true"] .select-arrow {
            transform: rotate(180deg);
        }

        .select-dropdown {
            position: absolute;
            top: 100%;
            left: 0;
            right: 0;
            background: white;
            border: 1px solid #d0d7de;
            border-radius: 6px;
            box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
            z-index: 1000;
            opacity: 0;
            transform: translateY(-8px);
            pointer-events: none;
            transition: all 0.2s ease;
            max-height: 200px;
            overflow-y: auto;
        }

        .select-dropdown.show {
            opacity: 1;
            transform: translateY(0);
            pointer-events: auto;
        }

        .select-option {
            padding: 10px 12px;
            cursor: pointer;
            transition: background-color 0.1s;
        }

        .select-option:hover,
        .select-option:focus {
            background-color: #f6f8fa;
        }

        .select-option[aria-selected="true"] {
            background-color: #0969da;
            color: white;
        }

        /* 交互式标签页 */
        .tab-container {
            margin: 20px 0;
        }

        .tab-list {
            display: flex;
            border-bottom: 1px solid #d0d7de;
            margin: 0;
            padding: 0;
            list-style: none;
        }

        .tab-button {
            background: none;
            border: none;
            padding: 12px 16px;
            cursor: pointer;
            border-bottom: 2px solid transparent;
            transition: all 0.2s;
            font-size: 14px;
        }

        .tab-button:hover {
            background-color: #f6f8fa;
        }

        .tab-button[aria-selected="true"] {
            border-bottom-color: #0969da;
            color: #0969da;
            font-weight: 600;
        }

        .tab-panel {
            padding: 20px 0;
            display: none;
        }

        .tab-panel[aria-hidden="false"] {
            display: block;
        }

        /* 可折叠面板 */
        .accordion {
            border: 1px solid #d0d7de;
            border-radius: 8px;
            overflow: hidden;
            margin: 16px 0;
        }

        .accordion-item {
            border-bottom: 1px solid #d0d7de;
        }

        .accordion-item:last-child {
            border-bottom: none;
        }

        .accordion-header {
            background: #f6f8fa;
            border: none;
            width: 100%;
            padding: 16px;
            text-align: left;
            cursor: pointer;
            font-size: 16px;
            font-weight: 500;
            display: flex;
            justify-content: space-between;
            align-items: center;
            transition: background-color 0.2s;
        }

        .accordion-header:hover {
            background-color: #eaeef2;
        }

        .accordion-content {
            padding: 0 16px;
            max-height: 0;
            overflow: hidden;
            transition: max-height 0.3s ease, padding 0.3s ease;
        }

        .accordion-item.active .accordion-content {
            max-height: 200px;
            padding: 16px;
        }

        .accordion-icon {
            transition: transform 0.3s ease;
        }

        .accordion-item.active .accordion-icon {
            transform: rotate(180deg);
        }

        /* 工具提示 */
        .tooltip-container {
            position: relative;
            display: inline-block;
            margin: 8px;
        }

        .tooltip-trigger {
            background: #0969da;
            color: white;
            border: none;
            padding: 8px 12px;
            border-radius: 4px;
            cursor: pointer;
        }

        .tooltip {
            position: absolute;
            bottom: 120%;
            left: 50%;
            transform: translateX(-50%);
            background: #24292f;
            color: white;
            padding: 8px 12px;
            border-radius: 4px;
            font-size: 14px;
            white-space: nowrap;
            opacity: 0;
            pointer-events: none;
            transition: opacity 0.2s;
            z-index: 1000;
        }

        .tooltip::after {
            content: '';
            position: absolute;
            top: 100%;
            left: 50%;
            transform: translateX(-50%);
            border: 5px solid transparent;
            border-top-color: #24292f;
        }

        .tooltip-container:hover .tooltip,
        .tooltip-container:focus-within .tooltip {
            opacity: 1;
        }

        /* 切换开关 */
        .switch {
            position: relative;
            display: inline-block;
            width: 60px;
            height: 34px;
            margin: 8px;
        }

        .switch input {
            opacity: 0;
            width: 0;
            height: 0;
        }

        .slider {
            position: absolute;
            cursor: pointer;
            top: 0;
            left: 0;
            right: 0;
            bottom: 0;
            background-color: #ccc;
            transition: 0.4s;
            border-radius: 34px;
        }

        .slider:before {
            position: absolute;
            content: "";
            height: 26px;
            width: 26px;
            left: 4px;
            bottom: 4px;
            background-color: white;
            transition: 0.4s;
            border-radius: 50%;
        }

        input:checked + .slider {
            background-color: #0969da;
        }

        input:checked + .slider:before {
            transform: translateX(26px);
        }

        /* 进度指示器 */
        .progress-container {
            margin: 20px 0;
        }

        .progress-ring {
            transform: rotate(-90deg);
            margin: 16px;
        }

        .progress-ring-circle {
            stroke: #d0d7de;
            stroke-width: 4;
            fill: transparent;
            transition: stroke-dasharray 0.3s ease;
        }

        .progress-ring-progress {
            stroke: #0969da;
            stroke-width: 4;
            fill: transparent;
            stroke-linecap: round;
        }

        /* 可拖拽列表 */
        .draggable-list {
            list-style: none;
            padding: 0;
            margin: 16px 0;
            background: #f6f8fa;
            border-radius: 8px;
            padding: 8px;
        }

        .draggable-item {
            background: white;
            border: 1px solid #d0d7de;
            border-radius: 6px;
            padding: 12px;
            margin: 4px 0;
            cursor: move;
            transition: all 0.2s;
            display: flex;
            align-items: center;
            gap: 12px;
        }

        .draggable-item:hover {
            box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
        }

        .draggable-item.dragging {
            opacity: 0.5;
            transform: scale(1.02);
        }

        .drag-handle {
            cursor: grab;
            color: #656d76;
        }

        .drag-handle:active {
            cursor: grabbing;
        }

        /* 响应式 */
        @media (max-width: 768px) {
            .demo-grid {
                grid-template-columns: 1fr;
            }
            
            .tab-list {
                overflow-x: auto;
            }
            
            .tab-button {
                white-space: nowrap;
                min-width: 120px;
            }
        }

        /* 实用样式 */
        .code-preview {
            background: #f6f8fa;
            border: 1px solid #d0d7de;
            border-radius: 6px;
            padding: 12px;
            font-family: 'Consolas', 'Monaco', monospace;
            font-size: 13px;
            margin: 12px 0;
            overflow-x: auto;
        }

        .status-badge {
            display: inline-block;
            padding: 4px 8px;
            border-radius: 12px;
            font-size: 12px;
            font-weight: 500;
            margin: 2px;
        }

        .badge-primary { background: #ddf4ff; color: #0969da; }
        .badge-success { background: #dafbe1; color: #1a7f37; }
        .badge-warning { background: #fff8c5; color: #9a6700; }
        .badge-danger { background: #ffebe9; color: #d1242f; }
    </style>
</head>
<body>
    <h1>现代化交互式组件示例</h1>
    <p>展示如何结合HTML、CSS和JavaScript创建现代化的交互式组件,遵循无障碍访问和渐进增强原则。</p>

    <div class="demo-grid">
        <!-- 自定义选择器 -->
        <div class="demo-card">
            <h3>自定义选择器</h3>
            <p>基于语义化HTML增强的下拉选择器,支持键盘导航。</p>
            
            <div class="custom-select" id="customSelect">
                <button class="select-trigger" aria-haspopup="listbox" aria-expanded="false" aria-labelledby="select-label">
                    <span id="select-value">请选择...</span>
                    <span class="select-arrow"></span>
                </button>
                <ul class="select-dropdown" role="listbox" aria-labelledby="select-label">
                    <li class="select-option" role="option" data-value="html">HTML</li>
                    <li class="select-option" role="option" data-value="css">CSS</li>
                    <li class="select-option" role="option" data-value="javascript">JavaScript</li>
                    <li class="select-option" role="option" data-value="react">React</li>
                    <li class="select-option" role="option" data-value="vue">Vue.js</li>
                </ul>
            </div>
            
            <div class="code-preview">
选择值: <span id="selectedValue">未选择</span>
            </div>
        </div>

        <!-- 交互式标签页 -->
        <div class="demo-card">
            <h3>无障碍标签页</h3>
            <p>遵循ARIA规范的标签页组件,支持键盘导航。</p>
            
            <div class="tab-container">
                <ul class="tab-list" role="tablist">
                    <li role="presentation">
                        <button class="tab-button" role="tab" aria-selected="true" aria-controls="tab1-panel" id="tab1">基础</button>
                    </li>
                    <li role="presentation">
                        <button class="tab-button" role="tab" aria-selected="false" aria-controls="tab2-panel" id="tab2">进阶</button>
                    </li>
                    <li role="presentation">
                        <button class="tab-button" role="tab" aria-selected="false" aria-controls="tab3-panel" id="tab3">高级</button>
                    </li>
                </ul>
                
                <div class="tab-panel" role="tabpanel" aria-labelledby="tab1" id="tab1-panel" aria-hidden="false">
                    <h4>基础知识</h4>
                    <p>HTML、CSS、JavaScript基础语法和概念。</p>
                    <span class="status-badge badge-primary">入门级</span>
                </div>
                
                <div class="tab-panel" role="tabpanel" aria-labelledby="tab2" id="tab2-panel" aria-hidden="true">
                    <h4>进阶技能</h4>
                    <p>框架应用、工程化工具、性能优化等。</p>
                    <span class="status-badge badge-warning">中级</span>
                </div>
                
                <div class="tab-panel" role="tabpanel" aria-labelledby="tab3" id="tab3-panel" aria-hidden="true">
                    <h4>高级架构</h4>
                    <p>系统设计、微前端、跨端开发等。</p>
                    <span class="status-badge badge-danger">高级</span>
                </div>
            </div>
        </div>

        <!-- 手风琴面板 -->
        <div class="demo-card">
            <h3>可折叠面板</h3>
            <p>平滑动画的手风琴组件,支持多项展开。</p>
            
            <div class="accordion">
                <div class="accordion-item">
                    <button class="accordion-header" aria-expanded="false">
                        <span>前端开发工具</span>
                        <span class="accordion-icon"></span>
                    </button>
                    <div class="accordion-content">
                        <p>VSCode、Chrome DevTools、Git、Node.js等开发必备工具。</p>
                    </div>
                </div>
                
                <div class="accordion-item">
                    <button class="accordion-header" aria-expanded="false">
                        <span>构建工具链</span>
                        <span class="accordion-icon"></span>
                    </button>
                    <div class="accordion-content">
                        <p>Webpack、Vite、Rollup等现代化构建工具的配置和使用。</p>
                    </div>
                </div>
                
                <div class="accordion-item">
                    <button class="accordion-header" aria-expanded="false">
                        <span>部署策略</span>
                        <span class="accordion-icon"></span>
                    </button>
                    <div class="accordion-content">
                        <p>CDN、Docker、CI/CD等现代化部署和运维方案。</p>
                    </div>
                </div>
            </div>
        </div>

        <!-- 工具提示和开关 -->
        <div class="demo-card">
            <h3>交互元素集合</h3>
            <p>工具提示、切换开关等常用交互元素。</p>
            
            <div style="margin: 16px 0;">
                <h4>工具提示</h4>
                <div class="tooltip-container">
                    <button class="tooltip-trigger">悬停显示提示</button>
                    <div class="tooltip">这是一个工具提示</div>
                </div>
                
                <div class="tooltip-container">
                    <button class="tooltip-trigger">键盘焦点提示</button>
                    <div class="tooltip">支持键盘导航</div>
                </div>
            </div>
            
            <div style="margin: 16px 0;">
                <h4>切换开关</h4>
                <label class="switch">
                    <input type="checkbox" id="darkMode">
                    <span class="slider"></span>
                </label>
                <label for="darkMode">深色模式</label>
                
                <label class="switch">
                    <input type="checkbox" id="notifications" checked>
                    <span class="slider"></span>
                </label>
                <label for="notifications">推送通知</label>
            </div>
        </div>

        <!-- 进度指示器 -->
        <div class="demo-card">
            <h3>进度指示器</h3>
            <p>环形和线性进度条组件。</p>
            
            <div class="progress-container">
                <h4>环形进度</h4>
                <svg class="progress-ring" width="80" height="80">
                    <circle class="progress-ring-circle" cx="40" cy="40" r="30"/>
                    <circle class="progress-ring-progress" cx="40" cy="40" r="30" 
                            stroke-dasharray="0 188.5" id="progressCircle"/>
                </svg>
                
                <div style="margin: 16px 0;">
                    <button onclick="updateProgress(25)">25%</button>
                    <button onclick="updateProgress(50)">50%</button>
                    <button onclick="updateProgress(75)">75%</button>
                    <button onclick="updateProgress(100)">100%</button>
                </div>
                
                <h4>线性进度</h4>
                <div style="background: #e1e5e9; height: 8px; border-radius: 4px; overflow: hidden;">
                    <div style="background: linear-gradient(90deg, #0969da, #2ea043); height: 100%; width: 60%; transition: width 0.3s ease;" id="linearProgress"></div>
                </div>
            </div>
        </div>

        <!-- 可拖拽列表 -->
        <div class="demo-card">
            <h3>可拖拽列表</h3>
            <p>支持鼠标和触摸的拖拽排序组件。</p>
            
            <ul class="draggable-list" id="dragList">
                <li class="draggable-item" draggable="true">
                    <span class="drag-handle">⋮⋮</span>
                    <span>HTML 语义化</span>
                </li>
                <li class="draggable-item" draggable="true">
                    <span class="drag-handle">⋮⋮</span>
                    <span>CSS 布局</span>
                </li>
                <li class="draggable-item" draggable="true">
                    <span class="drag-handle">⋮⋮</span>
                    <span>JavaScript ES6+</span>
                </li>
                <li class="draggable-item" draggable="true">
                    <span class="drag-handle">⋮⋮</span>
                    <span>React/Vue 框架</span>
                </li>
            </ul>
            
            <div class="code-preview">
                拖拽项目重新排序,查看控制台日志
            </div>
        </div>
    </div>

    <div style="margin-top: 40px; padding: 20px; background-color: #f0f8ff; border-left: 4px solid #0969da;">
        <h3>🚀 现代化组件设计原则</h3>
        <ul>
            <li><strong>语义化优先</strong>: 使用正确的HTML元素和ARIA属性</li>
            <li><strong>键盘导航</strong>: 所有交互都支持键盘操作</li>
            <li><strong>视觉反馈</strong>: 清晰的状态指示和平滑动画</li>
            <li><strong>响应式设计</strong>: 适配不同设备和屏幕尺寸</li>
            <li><strong>渐进增强</strong>: 基础功能无需JavaScript即可使用</li>
            <li><strong>性能优化</strong>: 合理使用CSS动画和事件委托</li>
        </ul>
    </div>

    <script>
        // 自定义选择器
        class CustomSelect {
            constructor(element) {
                this.element = element;
                this.trigger = element.querySelector('.select-trigger');
                this.dropdown = element.querySelector('.select-dropdown');
                this.options = element.querySelectorAll('.select-option');
                this.valueElement = element.querySelector('#select-value');
                this.selectedValueElement = document.getElementById('selectedValue');
                
                this.isOpen = false;
                this.selectedIndex = -1;
                
                this.bindEvents();
            }
            
            bindEvents() {
                this.trigger.addEventListener('click', () => this.toggle());
                this.trigger.addEventListener('keydown', (e) => this.handleKeyDown(e));
                
                this.options.forEach((option, index) => {
                    option.addEventListener('click', () => this.selectOption(index));
                });
                
                document.addEventListener('click', (e) => {
                    if (!this.element.contains(e.target)) {
                        this.close();
                    }
                });
            }
            
            toggle() {
                this.isOpen ? this.close() : this.open();
            }
            
            open() {
                this.isOpen = true;
                this.trigger.setAttribute('aria-expanded', 'true');
                this.dropdown.classList.add('show');
                this.focusOption(0);
            }
            
            close() {
                this.isOpen = false;
                this.trigger.setAttribute('aria-expanded', 'false');
                this.dropdown.classList.remove('show');
                this.trigger.focus();
            }
            
            selectOption(index) {
                this.selectedIndex = index;
                const option = this.options[index];
                const value = option.dataset.value;
                const text = option.textContent;
                
                this.valueElement.textContent = text;
                this.selectedValueElement.textContent = value;
                
                this.options.forEach(opt => opt.setAttribute('aria-selected', 'false'));
                option.setAttribute('aria-selected', 'true');
                
                this.close();
            }
            
            focusOption(index) {
                if (index >= 0 && index < this.options.length) {
                    this.options[index].focus();
                }
            }
            
            handleKeyDown(e) {
                switch(e.key) {
                    case 'Enter':
                    case ' ':
                        e.preventDefault();
                        this.toggle();
                        break;
                    case 'ArrowDown':
                        e.preventDefault();
                        if (this.isOpen) {
                            this.focusOption(0);
                        } else {
                            this.open();
                        }
                        break;
                    case 'Escape':
                        this.close();
                        break;
                }
            }
        }

        // 标签页组件
        class TabComponent {
            constructor(container) {
                this.container = container;
                this.tabList = container.querySelector('[role="tablist"]');
                this.tabs = container.querySelectorAll('[role="tab"]');
                this.panels = container.querySelectorAll('[role="tabpanel"]');
                
                this.bindEvents();
            }
            
            bindEvents() {
                this.tabs.forEach((tab, index) => {
                    tab.addEventListener('click', () => this.selectTab(index));
                    tab.addEventListener('keydown', (e) => this.handleKeyDown(e, index));
                });
            }
            
            selectTab(index) {
                this.tabs.forEach((tab, i) => {
                    const isSelected = i === index;
                    tab.setAttribute('aria-selected', isSelected);
                    this.panels[i].setAttribute('aria-hidden', !isSelected);
                });
            }
            
            handleKeyDown(e, currentIndex) {
                let newIndex;
                switch(e.key) {
                    case 'ArrowRight':
                        newIndex = (currentIndex + 1) % this.tabs.length;
                        break;
                    case 'ArrowLeft':
                        newIndex = (currentIndex - 1 + this.tabs.length) % this.tabs.length;
                        break;
                    case 'Home':
                        newIndex = 0;
                        break;
                    case 'End':
                        newIndex = this.tabs.length - 1;
                        break;
                    default:
                        return;
                }
                
                e.preventDefault();
                this.selectTab(newIndex);
                this.tabs[newIndex].focus();
            }
        }

        // 手风琴组件
        class AccordionComponent {
            constructor(element) {
                this.element = element;
                this.items = element.querySelectorAll('.accordion-item');
                this.bindEvents();
            }
            
            bindEvents() {
                this.items.forEach(item => {
                    const header = item.querySelector('.accordion-header');
                    header.addEventListener('click', () => this.toggleItem(item));
                });
            }
            
            toggleItem(item) {
                const isActive = item.classList.contains('active');
                const header = item.querySelector('.accordion-header');
                
                if (isActive) {
                    item.classList.remove('active');
                    header.setAttribute('aria-expanded', 'false');
                } else {
                    item.classList.add('active');
                    header.setAttribute('aria-expanded', 'true');
                }
                
                console.log(`手风琴项目 ${isActive ? '关闭' : '展开'}`);
            }
        }

        // 拖拽组件
        class DragSortList {
            constructor(element) {
                this.element = element;
                this.draggedItem = null;
                this.bindEvents();
            }
            
            bindEvents() {
                this.element.addEventListener('dragstart', (e) => this.handleDragStart(e));
                this.element.addEventListener('dragover', (e) => this.handleDragOver(e));
                this.element.addEventListener('drop', (e) => this.handleDrop(e));
                this.element.addEventListener('dragend', (e) => this.handleDragEnd(e));
            }
            
            handleDragStart(e) {
                this.draggedItem = e.target;
                e.target.classList.add('dragging');
                e.dataTransfer.effectAllowed = 'move';
            }
            
            handleDragOver(e) {
                e.preventDefault();
                e.dataTransfer.dropEffect = 'move';
                
                const afterElement = this.getDragAfterElement(e.clientY);
                if (afterElement == null) {
                    this.element.appendChild(this.draggedItem);
                } else {
                    this.element.insertBefore(this.draggedItem, afterElement);
                }
            }
            
            handleDrop(e) {
                e.preventDefault();
                console.log('列表重新排序');
            }
            
            handleDragEnd(e) {
                e.target.classList.remove('dragging');
                this.draggedItem = null;
            }
            
            getDragAfterElement(y) {
                const draggableElements = [...this.element.querySelectorAll('.draggable-item:not(.dragging)')];
                
                return draggableElements.reduce((closest, child) => {
                    const box = child.getBoundingClientRect();
                    const offset = y - box.top - box.height / 2;
                    
                    if (offset < 0 && offset > closest.offset) {
                        return { offset: offset, element: child };
                    } else {
                        return closest;
                    }
                }, { offset: Number.NEGATIVE_INFINITY }).element;
            }
        }

        // 进度更新函数
        function updateProgress(percent) {
            const circle = document.getElementById('progressCircle');
            const linear = document.getElementById('linearProgress');
            const circumference = 2 * Math.PI * 30; // r=30
            const offset = circumference - (percent / 100) * circumference;
            
            circle.style.strokeDasharray = `${circumference - offset} ${circumference}`;
            linear.style.width = percent + '%';
            
            console.log(`进度更新至 ${percent}%`);
        }

        // 初始化所有组件
        document.addEventListener('DOMContentLoaded', function() {
            // 初始化自定义选择器
            const customSelect = document.getElementById('customSelect');
            new CustomSelect(customSelect);
            
            // 初始化标签页
            const tabContainer = document.querySelector('.tab-container');
            new TabComponent(tabContainer);
            
            // 初始化手风琴
            const accordion = document.querySelector('.accordion');
            new AccordionComponent(accordion);
            
            // 初始化拖拽列表
            const dragList = document.getElementById('dragList');
            new DragSortList(dragList);
            
            // 开关状态监听
            document.getElementById('darkMode').addEventListener('change', function() {
                console.log('深色模式:', this.checked ? '开启' : '关闭');
            });
            
            document.getElementById('notifications').addEventListener('change', function() {
                console.log('推送通知:', this.checked ? '开启' : '关闭');
            });
            
            console.log('所有现代化组件初始化完成');
        });
    </script>
</body>
</html>

面试官视角:

要点清单:

  • 了解原生HTML交互元素的优势
  • 知道如何结合CSS和JavaScript增强功能
  • 理解渐进增强的设计理念

加分项:

  • 提及Web Components的应用
  • 了解ARIA属性在自定义组件中的作用
  • 知道如何处理触摸设备的交互

常见失误:

  • 完全依赖JavaScript实现基础功能
  • 忽略键盘用户的操作需求
  • 不考虑屏幕阅读器的兼容性

延伸阅读:

web components 了解多少?

答案

Web Components 是一套原生前端技术标准,允许开发者创建可复用、封装良好的自定义元素。其核心包括三大要素:

  • Custom Elements(自定义元素)
    通过 JavaScript API 定义新标签及其行为。例如:

    class MyElement extends HTMLElement {
    connectedCallback () { /* ... */ }
    }
    window.customElements.define('my-element', MyElement)

    支持生命周期钩子(如 connectedCallbackdisconnectedCallbackadoptedCallbackattributeChangedCallback),便于管理元素挂载、卸载、属性变化等。

  • Shadow DOM(影子 DOM)
    为元素创建独立的 DOM 子树,实现样式和结构的隔离,避免外部样式污染。通过 this.attachShadow({mode: 'open'}) 启用。

    提示

    使用 Shadow DOM 可实现样式隔离,适合开发 UI 组件库。

  • HTML Templates & Slot
    <template> 标签可声明可复用的 DOM 结构,<slot> 用于分发内容。简化组件开发,提升灵活性。

  • 属性传递:通过设置属性或 property 传递数据,复杂数据可用 JSON 字符串。

  • 事件通信:自定义事件(如 this.dispatchEvent(new CustomEvent('change', {...})))实现父子通信。

  • 双向绑定:可监听 input 事件并同步属性,实现数据回流。

  • 直接在自定义标签或 Shadow DOM 内部添加 <style>

  • 也可通过 JS 动态插入 <style><link>,支持外部样式文件。

const style = document.createElement('style')
style.textContent = '.header{color:red}'
shadow.appendChild(style)

常见误区

  • 并非所有场景都需用 Shadow DOM,简单组件可只用 Custom Elements。
  • 兼容性:IE 不支持,部分高级特性需 polyfill。
  • 复杂数据建议用 property 传递,属性变化监听有限。

延伸阅读

备注

Web Components 适合开发可复用、跨框架的 UI 组件,已被主流框架和浏览器广泛支持。