CC 咖啡猫的工作空间 Coding Space

HTML 与 DOM

HTML5 新增

语义化标签

HTML5 引入语义化标签来替代无意义的 <div>,提升可读性、SEO 和无障碍访问。

标签 含义 使用要点
<header> 页眉/区块头部 可包含 logo、导航、搜索栏;一个页面可有多个
<nav> 导航链接区域 用于主导航,不要给所有链接组都加 nav
<main> 页面主体内容 每个页面唯一,不应包含侧栏、导航等重复内容
<article> 独立内容单元 论坛帖子、新闻文章、评论;可嵌套 section
<section> 主题分组 通常含标题,不要作为普通 div 容器使用
<aside> 侧边内容 与主内容间接相关(侧栏、广告、相关链接)
<footer> 页脚/区块尾部 版权、作者信息、相关链接

SEO 与 A11y 收益:屏幕阅读器可直接跳转到 nav/main 等区域,搜索引擎根据语义标签提升内容权重。

表单增强

新 input 类型

<!-- 浏览器会提供对应的键盘/校验/控件 -->
<input type="email">       <!-- 邮箱格式校验 + 移动端@键盘 -->
<input type="url">         <!-- URL 格式校验 -->
<input type="tel">         <!-- 移动端数字键盘 -->
<input type="number">      <!-- 数字步进控件 -->
<input type="range">       <!-- 滑块选择 -->
<input type="color">       <!-- 颜色选择器 -->
<input type="date">        <!-- 日期选择器 -->
<input type="datetime-local">
<input type="month">
<input type="week">
<input type="time">
<input type="search">      <!-- 搜索框(样式差异) -->

新增属性

属性 作用
required 非空校验,替代 JS 校验的基础
pattern="[A-Z]{3}" 正则校验
autocomplete="off" 控制自动填充行为
autofocus 页面加载时自动聚焦
placeholder 占位提示(注意不要代替 label)
min/max/step 数值/日期范围限制
multiple 文件/邮箱多选
novalidate 禁用表单校验(用于草稿保存)

注意:浏览器校验样式不可控,通常使用 :invalid / :valid 伪类自定义样式,或用 novalidate 配合自定义校验。

多媒体标签

<audio controls>
  <source src="music.mp3" type="audio/mpeg">
  <source src="music.ogg" type="audio/ogg">
  您的浏览器不支持 audio 标签
</audio>

<video controls width="720" poster="thumbnail.jpg">
  <source src="movie.mp4" type="video/mp4">
  <source src="movie.webm" type="video/webm">
  <track src="subtitles.vtt" kind="subtitles" srclang="zh" label="中文">
</video>

关键属性:controlsautoplay(移动端受限)、loopmuted(自动播放必须静音)、preload="none|metadata|auto"

Canvas vs SVG

维度 Canvas SVG
本质 位图(像素操作) 矢量图(DOM 节点)
渲染方式 即时模式(逐帧绘制) 保留模式(树形结构)
事件处理 不支持 支持(每个元素是 DOM 节点)
性能(大量元素)
高清屏适配 需手动 devicePixelRatio 自动适配
适用场景 游戏、图像处理、图表 图标、交互式图形、动画

Canvas 示例:

const canvas = document.getElementById('c');
const ctx = canvas.getContext('2d');
// 必须手动处理高清屏
const dpr = window.devicePixelRatio || 1;
canvas.width = rect.width * dpr;
canvas.height = rect.height * dpr;
ctx.scale(dpr, dpr);
ctx.fillStyle = 'red';
ctx.fillRect(10, 10, 100, 100);

Web Storage

// Cookie —— 每次请求自动携带,4KB 限制
document.cookie = "name=value; max-age=3600; path=/; SameSite=Lax";

// localStorage —— 持久存储(除非手动清除),5MB+,同源共享
localStorage.setItem('key', 'value');
localStorage.getItem('key');
localStorage.removeItem('key');

// sessionStorage —— 标签页级隔离,关闭标签页即清除
sessionStorage.setItem('key', 'value');

三者对比

特性 Cookie localStorage sessionStorage
容量 ~4KB ~5MB+ ~5MB+
作用域 同源所有标签页 同源所有标签页 同源当前标签页(同源同标签页 iframe 共享)
寿命 可设过期时间 持久 标签页关闭即清除
HTTP 请求 自动携带 不携带 不携带
操作 API 原始字符串 key-value key-value
SameSite 安全 支持

WebSocket

const ws = new WebSocket('wss://example.com/chat');

ws.onopen = () => {
  ws.send(JSON.stringify({ type: 'join', room: 'general' }));
};

ws.onmessage = (event) => {
  const data = JSON.parse(event.data);
  console.log('收到消息:', data);
};

ws.onclose = (event) => {
  if (event.code === 1006) {
    // 异常断开,尝试重连
    reconnect();
  }
};

ws.onerror = (err) => {
  console.error('WebSocket 错误:', err);
};

心跳机制:为防止网关/代理断开空闲连接,通常每 30-60s 发送 ping 帧。

对比 HTTP 轮询:建立后全双工通信,头开销只有几个字节 vs HTTP 几百字节。

Service Worker

生命周期

注册 → 安装(install) → 激活(activate) → 运行(fetch/message)
                           ↓ (失败则等待下次)
// 注册(必须在主线程)
if ('serviceWorker' in navigator) {
  navigator.serviceWorker.register('/sw.js')
    .then(reg => console.log('SW 注册成功', reg.scope))
    .catch(err => console.error('SW 注册失败', err));
}

SW 内代码

// sw.js
const CACHE_NAME = 'v1';
const URLS_TO_CACHE = ['/', '/styles.css', '/app.js'];

// 安装:预缓存资源
self.addEventListener('install', (event) => {
  event.waitUntil(
    caches.open(CACHE_NAME).then((cache) => cache.addAll(URLS_TO_CACHE))
  );
  // 强制等待中的 SW 激活
  self.skipWaiting();
});

// 激活:清理旧缓存
self.addEventListener('activate', (event) => {
  event.waitUntil(
    caches.keys().then((keys) =>
      Promise.all(keys.filter(k => k !== CACHE_NAME).map(k => caches.delete(k)))
    )
  );
  // 立即接管所有页面
  self.clients.claim();
});

// 请求拦截:四种缓存策略
self.addEventListener('fetch', (event) => {
  event.respondWith(
    // 策略 1:Cache First —— 静态资源
    caches.match(event.request).then(resp => resp || fetch(event.request))

    // 策略 2:Network First —— API 请求
    // fetch(event.request).catch(() => caches.match(event.request))

    // 策略 3:Stale While Revalidate —— 优先缓存 + 后台更新
    // ...

    // 策略 4:Network Only —— 需要实时数据
  );
});

SW 限制

  • 必须 HTTPS(localhost 除外)
  • 没有 DOM 访问权限
  • 不能使用同步 API(如 XHR synchronous)
  • 作用域:默认在 sw.js 所在目录

History API

// 不刷新页面前进(SPA 路由核心)
history.pushState({ page: 1 }, '标题', '/page1');
history.replaceState({ page: 2 }, '标题', '/page2');

// 监听 popstate(仅后退/前进触发,pushState/replaceState 不触发)
window.addEventListener('popstate', (event) => {
  console.log('状态变化:', event.state);
  renderRoute(location.pathname);
});

// 与 hash 路由比较
// pushState URL 更干净,但服务端需要 fallback 到 index.html
// hash 路由兼容旧浏览器,服务端无需配置

DOM 操作

节点类型

document.nodeType; // 9 — Document
element.nodeType;  // 1 — Element
attr.nodeType;     // 2 — Attribute(已废弃)
text.nodeType;     // 3 — Text
comment.nodeType;  // 8 — Comment
documentFragment.nodeType; // 11 — DocumentFragment

DocumentFragment:批量操作避免回流

const fragment = document.createDocumentFragment();
for (let i = 0; i < 1000; i++) {
  const li = document.createElement('li');
  li.textContent = `Item ${i}`;
  fragment.appendChild(li);
}
ul.appendChild(fragment); // 一次回流

增删改查 API

// 查询
document.getElementById('id');
document.querySelector('.class');      // 第一个匹配
document.querySelectorAll('.class');    // NodeList(静态快照)
document.elementFromPoint(x, y);       // 点击测试
elem.closest('.parent');               // 向上查找
elem.matches('.active');               // 是否匹配

// 创建
document.createElement('div');
document.createTextNode('text');
document.createDocumentFragment();
elem.cloneNode(true); // 深拷贝(含子节点)

// 插入
parent.appendChild(child);
parent.insertBefore(newChild, refChild);
parent.prepend(child);    // 开头插入
parent.append(child);     // 末尾插入(可传多个)
child.after(newSibling);  // 兄弟节点后插入
child.before(newSibling);
elem.insertAdjacentHTML('beforeend', '<span>HTML</span>');
// 可选:'beforebegin' | 'afterbegin' | 'beforeend' | 'afterend'

// 替换/删除
parent.replaceChild(newChild, oldChild);
elem.remove();           // 直接删除自身
parent.removeChild(child);

性能注意

  • querySelectorAll 返回静态快照,getElementsByTagName 返回动态集合(性能更好但行为有陷阱)
  • 频繁 DOM 操作使用 DocumentFragment 或 innerHTML 批量构建

事件流

捕获阶段 (Capture) → 目标阶段 (Target) → 冒泡阶段 (Bubble)
     ↓                     ↓                    ↓
  window → document → parent → target → parent → document → window

addEventListener 第三个参数

// capture: true —— 在捕获阶段触发
elem.addEventListener('click', handler, { capture: true });
// 等价于
elem.addEventListener('click', handler, true);

// once: true —— 自动移除(无需手动 removeEventListener)
elem.addEventListener('click', handler, { once: true });

// passive: true —— 声明不会调用 preventDefault
// 对 touchstart/touchmove 提升滚动性能(Chrome 66+ 默认 passive: true)
elem.addEventListener('touchstart', handler, { passive: true });

事件对象方法

event.preventDefault();  // 阻止默认行为(如链接跳转、表单提交)
event.stopPropagation(); // 阻止事件传播(捕获+冒泡)
event.stopImmediatePropagation(); // 同上 + 阻止当前元素其余事件处理

// event.target vs event.currentTarget
// target: 触发事件的元素
// currentTarget: 绑定事件处理器的元素 (this)

事件委托(事件代理)

将事件监听器绑定到父元素,利用事件冒泡处理子元素事件。

// ❌ 低效:每个 li 绑定一个监听器
document.querySelectorAll('li').forEach(li => {
  li.addEventListener('click', () => handleClick(li));
});

// ✅ 高效:一个监听器处理所有
document.querySelector('ul').addEventListener('click', (e) => {
  const li = e.target.closest('li');
  if (!li) return;         // 不是 li 点击,忽略
  handleClick(li.dataset.id);
});

优势:减少内存占用、动态添加的元素自动生效。

限制:不适用于不冒泡的事件(focusblurscrollmouseentermouseleave)。

替代方案focus/blurfocusin/focusout(可冒泡)。

自定义事件

// 创建
const event = new CustomEvent('userLogin', {
  detail: { userId: 123, name: 'Alice' },
  bubbles: true,
  cancelable: true,
});

// 派发
elem.dispatchEvent(event);

// 监听
elem.addEventListener('userLogin', (e) => {
  console.log(e.detail); // { userId: 123, name: 'Alice' }
});

DOM 遍历

// NodeIterator —— 简单遍历
const iterator = document.createNodeIterator(
  root,
  NodeFilter.SHOW_ELEMENT, // 只遍历元素节点
  {
    acceptNode(node) {
      return node.classList.contains('skip')
        ? NodeFilter.FILTER_REJECT // 跳过该节点及其子节点
        : NodeFilter.FILTER_ACCEPT;
    }
  }
);
let node;
while ((node = iterator.nextNode())) {
  console.log(node.tagName);
}

// TreeWalker —— 更灵活(支持 parent/sibling 导航)
const walker = document.createTreeWalker(
  root,
  NodeFilter.SHOW_TEXT,
  null,
  false // entityReferenceExpansion
);
while ((node = walker.nextNode())) {
  console.log(node.textContent.trim());
}

事件模型

addEventListener 第三个参数详解

// 对象形式(推荐)
elem.addEventListener('click', handler, {
  capture: false,   // 捕获阶段触发
  once: false,      // 只执行一次后自动移除
  passive: false,   // 不调用 preventDefault(滚动性能优化)
  signal: abortController.signal, // 与 AbortController 配合移除
});

// 使用 AbortController 批量移除
const controller = new AbortController();
elem.addEventListener('click', handler1, { signal: controller.signal });
elem.addEventListener('scroll', handler2, { signal: controller.signal });
controller.abort(); // 同时移除两个

preventDefault vs stopPropagation vs stopImmediatePropagation

方法 阻止默认行为 阻止传播 阻止同级其他监听 可取消判断
preventDefault() event.cancelable
stopPropagation()
stopImmediatePropagation()
return false(jQuery)

最佳实践:不要滥用 stopPropagation,可能破坏页面级事件委托。如确实需要阻止,考虑判断 event.target 自行过滤。

事件代理的深入理解

// 一个真实的菜单事件代理
menu.addEventListener('click', (e) => {
  const item = e.target.closest('[data-action]');
  if (!item) return;

  const action = item.dataset.action;
  e.preventDefault(); // 阻止链接跳转

  switch (action) {
    case 'edit': openEditor(item.dataset.id); break;
    case 'delete': confirmDelete(item.dataset.id); break;
    case 'share': openShareDialog(item.dataset.id); break;
  }
});

Web Components

Custom Elements

// 自定义元素(必须包含连字符)
class MyButton extends HTMLElement {
  // 指定需要观察的属性变化
  static get observedAttributes() {
    return ['disabled', 'variant'];
  }

  constructor() {
    super();
    // 初始化状态,但不要访问属性或 DOM
    this._variant = 'primary';
  }

  // 元素被插入 DOM
  connectedCallback() {
    this.render();
    console.log('组件已挂载');
  }

  // 元素从 DOM 移除
  disconnectedCallback() {
    console.log('组件已卸载,清理定时器/事件');
  }

  // 属性变化时触发
  attributeChangedCallback(name, oldValue, newValue) {
    if (name === 'variant') this._variant = newValue;
    this.render();
  }

  render() {
    this.innerHTML = `
      <button class="btn btn-${this._variant}" ${this.getAttribute('disabled') !== null ? 'disabled' : ''}>
        <slot></slot>
      </button>
    `;
  }
}

customElements.define('my-button', MyButton);

生命周期

  • constructor() —— 元素创建(不能访问 DOM)
  • connectedCallback() —— 插入 DOM
  • disconnectedCallback() —— 从 DOM 移除
  • attributeChangedCallback(name, old, new) —— observedAttributes 中属性变化
  • adoptedCallback() —— 被移动至新文档

Shadow DOM

class MyCard extends HTMLElement {
  constructor() {
    super();
    // 创建 Shadow DOM
    this.attachShadow({ mode: 'open' }); // open: 外部可通过 element.shadowRoot 访问
    // mode: 'closed' —— 外部无法访问 shadowRoot(但 inspect element 仍可查看)
  }

  connectedCallback() {
    this.shadowRoot.innerHTML = `
      <style>
        :host {            /* :host 指代宿主元素本身 */
          display: block;
          border: 1px solid #ddd;
          border-radius: 8px;
          padding: 16px;
        }
        :host([highlight]) { /* 宿主有 highlight 属性时 */
          border-color: gold;
        }
        :host-context(.dark) { /* 宿主在 .dark 容器内 */
          background: #333;
          color: white;
        }
        ::slotted(h2) {    /* 插槽内容样式 */
          margin-top: 0;
        }
        .card-content {
          margin-top: 8px;
        }
      </style>
      <slot name="title">默认标题</slot>
      <div class="card-content">
        <slot></slot> <!-- 默认插槽 -->
      </div>
    `;

    // 样式隔离:外部样式不会穿透 shadow DOM
    // 外部可使用 CSS 自定义属性进行主题定制
  }
}
customElements.define('my-card', MyCard);
<my-card highlight>
  <h2 slot="title">Card 标题</h2>
  <p>这是卡片内容,外部样式不影响内部。</p>
</my-card>

HTML Templates

<template id="user-card-tpl">
  <style>
    .user-card { display: flex; align-items: center; gap: 12px; }
    .avatar { width: 48px; height: 48px; border-radius: 50%; }
    .name { font-weight: bold; }
  </style>
  <div class="user-card">
    <img class="avatar" src="" alt="avatar">
    <div class="name"></div>
  </div>
</template>
class UserCard extends HTMLElement {
  connectedCallback() {
    const template = document.getElementById('user-card-tpl');
    // template.content 是 DocumentFragment
    const clone = template.content.cloneNode(true); // 每次克隆新实例
    clone.querySelector('.avatar').src = this.getAttribute('avatar');
    clone.querySelector('.name').textContent = this.getAttribute('name');

    this.attachShadow({ mode: 'open' }).appendChild(clone);
  }
}
customElements.define('user-card', UserCard);

可访问性(A11y)

ARIA 属性

<!-- 角色(role) -->
<div role="button" tabindex="0" @click="handleClick" @keydown.enter="handleClick">
  自定义按钮
</div>

<!-- 状态与属性 -->
<div role="alert" aria-live="assertive">操作成功</div>
<input aria-label="搜索关键词" aria-required="true" aria-invalid="false">
<div aria-expanded="false" aria-controls="menu-panel">菜单</div>
<div role="progressbar" aria-valuenow="30" aria-valuemin="0" aria-valuemax="100">

ARIA 使用原则(WAI-ARIA 第一原则):

  • 不要使用 ARIA 如果可以用原生 HTML 语义元素
  • 不要改变原生语义(如给 <h2>role="button"
  • 所有交互式 ARIA 角色必须支持键盘操作

语义化 HTML 与屏幕阅读器

非语义方式 语义化方式 效果
<div class="nav"> <nav> 自动识别导航区域
<div class="footer"> <footer> 自动识别页脚
<div onclick="..."> <button> 可聚焦、可键盘操作、有角色
<div class="heading"> 加 CSS <h1>~<h6> 标题层级导航
<img alt=""> 始终提供 alt 图片描述
<span class="required">* <input required> + aria-required 浏览器自动提示

Landmark 角色bannernavigationmaincomplementarycontentinfo。显式 role 在旧浏览器中仍有价值,新浏览器可直接使用语义标签。

键盘导航

<!-- tabindex 控制 -->
<button>可访问</button>                    <!-- 默认 0,自然顺序 -->
<div tabindex="0">编程焦点</div>            <!-- 加入 Tab 键顺序 -->
<div tabindex="-1">可 JS 聚焦</div>         <!-- Tab 键跳过,但可 focus() -->

<!-- 不要使用正数 tabindex → 破坏自然顺序 -->

焦点管理原则

  • 模态框打开时,焦点锁定在模态框内(focus trap
  • 模态框关闭时,焦点返回触发元素
  • 单页路由切换时,焦点移至新内容顶部(skip to content
  • 使用 :focus-visible 替代 :focus(在有键盘操作时显示焦点环)

焦点管理示例

// 模态框焦点陷阱
class Modal {
  constructor(triggerEl) {
    this.triggerEl = triggerEl;
    this.modal = document.getElementById('modal');
    this.focusableEls = this.modal.querySelectorAll(
      'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
    );
    this.firstFocusable = this.focusableEls[0];
    this.lastFocusable = this.focusableEls[this.focusableEls.length - 1];
  }

  open() {
    this.modal.style.display = 'block';
    this.firstFocusable.focus();
    this.modal.addEventListener('keydown', this._trapFocus);
  }

  close() {
    this.modal.style.display = 'none';
    this.triggerEl.focus(); // 焦点回归
    this.modal.removeEventListener('keydown', this._trapFocus);
  }

  _trapFocus = (e) => {
    if (e.key !== 'Tab') return;
    if (e.shiftKey && document.activeElement === this.firstFocusable) {
      e.preventDefault();
      this.lastFocusable.focus();
    } else if (!e.shiftKey && document.activeElement === this.lastFocusable) {
      e.preventDefault();
      this.firstFocusable.focus();
    }
  }
}

性能相关

defer vs async

<!-- 默认:解析遇到则阻塞下载+执行 -->
<script src="normal.js"></script>

<!-- async:下载不阻塞,下载完成后立即执行(执行可能阻塞) -->
<!-- 执行顺序与 DOM 位置无关,多个 async 执行顺序不确定 -->
<script async src="analytics.js"></script>

<!-- defer:下载不阻塞,DOMContentLoaded 之前按顺序执行 -->
<script defer src="app.js"></script>
<script defer src="dep.js"></script> <!-- 在 app.js 之前执行 -->
特性 普通 async defer
下载阻塞解析
执行阻塞解析
执行时机 立即 下载完立即 DOMContentLoaded 前
执行顺序 按文档顺序 不确定 按文档顺序
DOMContentLoaded 等待
适用场景 独立第三方脚本 依赖 DOM 的脚本

preload / prefetch / dns-prefetch

<!-- preload:当前页面必需的资源,高优先级,尽早加载 -->
<link rel="preload" href="font.woff2" as="font" crossorigin>
<link rel="preload" href="hero.jpg" as="image">
<link rel="preload" href="style.css" as="style">

<!-- prefetch:下一个页面可能需要的资源,低优先级,空闲时加载 -->
<link rel="prefetch" href="/next-page.js" as="script">

<!-- dns-prefetch:提前解析 DNS,减少跨域请求延迟 -->
<link rel="dns-prefetch" href="//api.example.com">

<!-- preconnect:DNS + TCP + TLS 握手 -->
<link rel="preconnect" href="//cdn.example.com">

<!-- 优先级:preload > preconnect > dns-prefetch > prefetch -->

注意preload 需要配合 as 属性指定资源类型,否则浏览器无法确定优先级。

关键渲染路径中的 HTML 优化

关键渲染路径(Critical Rendering Path):浏览器从 HTML 到屏幕像素的步骤。

HTML → DOM 树  ┐
                 → 渲染树 → 布局 → 绘制 → 合成
CSS → CSSOM 树 ┘

优化策略

  1. 减少阻塞:内联关键 CSS(Critical CSS),首屏 CSS <link><head>,非关键 CSS 异步加载
  2. 压缩资源:HTML/CSS/JS 压缩,移除注释空格
  3. 渐进式加载:图片懒加载 <img loading="lazy">
  4. 减少 DOM 深度:清除冗余嵌套减少渲染树节点
  5. 避免布局抖动:批量 DOM 操作、使用 requestAnimationFrame 读布局:
// ❌ 强制同步布局(每次读 offsetHeight 都会触发重新计算)
element.style.width = '100px';
console.log(element.offsetHeight); // 强制回流
element.style.height = '200px';

// ✅ 先批量读,再批量写
const height = element.offsetHeight; // 读
requestAnimationFrame(() => {
  element.style.width = '100px';     // 写
  element.style.height = '200px';
});
  1. 长任务分解:超过 50ms 的任务会导致交互延迟,使用 requestIdleCallback 分片:
function processLargeArray(arr) {
  const chunkSize = 50;
  let index = 0;

  function doChunk() {
    const end = Math.min(index + chunkSize, arr.length);
    for (; index < end; index++) {
      processItem(arr[index]);
    }
    if (index < arr.length) {
      requestIdleCallback(doChunk, { timeout: 100 });
    }
  }
  doChunk();
}