Skip to content

现代 Web 平台新特性全览

HTML 新特性 {#html}

dialog 元素

<dialog id="modal">
  <p>这是一个原生对话框</p>
  <button onclick="this.closest('dialog').close()">关闭</button>
</dialog>

<button onclick="document.getElementById('modal').showModal()">打开</button>
const dialog = document.querySelector("dialog");
dialog.showModal(); // 模态(带 backdrop)
dialog.show(); // 非模态
dialog.close();
dialog.returnValue; // 关闭时的返回值

dialog.addEventListener("close", () => console.log(dialog.returnValue));

popover API(2024)

<!-- 无需 JS,纯 HTML 实现弹出层 -->
<button popovertarget="tip">显示提示</button>
<div id="tip" popover>
  <p>这是一个 Popover</p>
</div>

<!-- 手动控制 -->
<div id="menu" popover="manual">...</div>
const popover = document.querySelector("[popover]");
popover.showPopover();
popover.hidePopover();
popover.togglePopover();

details / summary

<details>
  <summary>点击展开</summary>
  <p>折叠内容...</p>
</details>

<!-- 手风琴效果:name 相同的 details 同时只能开一个 -->
<details name="faq">
  <summary>问题 1</summary>
  ...
</details>
<details name="faq">
  <summary>问题 2</summary>
  ...
</details>

loading="lazy" 懒加载

<img src="photo.jpg" loading="lazy" alt="..." />
<iframe src="embed.html" loading="lazy"></iframe>

fetchpriority

<!-- 提升关键资源优先级 -->
<img src="hero.jpg" fetchpriority="high" />
<link rel="preload" href="font.woff2" as="font" fetchpriority="high" />
<script src="non-critical.js" fetchpriority="low"></script>

inert 属性

<!-- 让整个区域不可交互(不可聚焦、不可点击、对 AT 不可见) -->
<div inert>
  <button>此按钮不可用</button>
  <input type="text" />
</div>

input 新类型和属性

<input type="color" />
<input type="range" min="0" max="100" step="5" />
<input type="date" />
<input type="datetime-local" />
<input type="search" />

<!-- datalist:输入建议 -->
<input list="fruits" placeholder="选择水果" />
<datalist id="fruits">
  <option value="Apple"></option>
  <option value="Banana"></option>
  <option value="Cherry"></option>
</datalist>

form 验证属性

<form>
  <input required minlength="3" maxlength="20" pattern="[a-z]+" />
  <input type="email" />
  <input type="url" />
  <button formnovalidate>跳过验证提交</button>
</form>
input.setCustomValidity("用户名已存在");
input.reportValidity();
form.checkValidity();

View Transitions API(2024)

// 页面切换时添加过渡动画
document.startViewTransition(() => {
  updateDOM(); // 更新 DOM
});

// CSS 控制动画
// ::view-transition-old(root) — 旧页面
// ::view-transition-new(root) — 新页面
::view-transition-old(root) {
  animation: slide-out 300ms ease;
}
::view-transition-new(root) {
  animation: slide-in 300ms ease;
}

CSS 新特性 {#css}

CSS 自定义属性(变量)

:root {
  --color-primary: #007bff;
  --spacing-base: 8px;
  --font-size: 16px;
}

.button {
  background: var(--color-primary);
  padding: calc(var(--spacing-base) * 2);
  /* 回退值 */
  color: var(--text-color, #333);
}
// JS 读写 CSS 变量
getComputedStyle(el).getPropertyValue("--color-primary");
el.style.setProperty("--color-primary", "#ff0000");

CSS Grid

.container {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  grid-template-rows: auto;
  gap: 16px;

  /* 命名区域 */
  grid-template-areas:
    "header header header"
    "sidebar main main"
    "footer footer footer";
}

.header {
  grid-area: header;
}
.sidebar {
  grid-area: sidebar;
}

/* 自动填充 */
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));

/* 子项定位 */
.item {
  grid-column: 1 / 3; /* 跨 2 列 */
  grid-row: span 2; /* 跨 2 行 */
}

CSS Flexbox 新属性

.container {
  display: flex;
  flex-wrap: wrap;
  gap: 16px; /* 现代写法,替代 margin hack */
}

.item {
  flex: 1 1 200px; /* grow shrink basis */
}

Container Queries(2023)

/* 根据容器大小(而非视口)响应式布局 */
.card-wrapper {
  container-type: inline-size;
  container-name: card;
}

@container card (min-width: 400px) {
  .card {
    display: flex;
    flex-direction: row;
  }
}

/* 容器查询单位 */
.card h2 {
  font-size: clamp(1rem, 5cqi, 2rem); /* cqi = 容器行内尺寸的 1% */
}

CSS 嵌套(2024)

/* 原生支持类似 Sass 的嵌套 */
.card {
  padding: 1rem;
  background: white;

  .title {
    font-size: 1.5rem;
    color: navy;
  }

  &:hover {
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
  }

  @media (max-width: 600px) {
    padding: 0.5rem;
  }
}

:is() / :where() / :has()

/* :is() — 选择器列表,权重取最高的 */
:is(h1, h2, h3) > a {
  color: blue;
}

/* :where() — 同 :is(),但权重为 0 */
:where(header, footer) a {
  color: inherit;
}

/* :has() — 父选择器,根据子元素匹配父 */
.card:has(img) {
  padding: 0;
}
label:has(+ input:required)::after {
  content: " *";
  color: red;
}
li:has(> details[open]) {
  background: #f0f8ff;
}

CSS 逻辑属性

/* 替代方向性属性,支持国际化/RTL */
.box {
  margin-inline: auto; /* margin-left + margin-right */
  padding-block: 1rem; /* padding-top + padding-bottom */
  border-inline-start: 2px solid blue; /* RTL 自动翻转 */
  inset-inline-start: 0; /* left in LTR, right in RTL */
}

clamp() / min() / max()

/* 响应式字体大小,无需媒体查询 */
h1 {
  font-size: clamp(1.5rem, 4vw, 3rem); /* 最小 最佳 最大 */
}

.container {
  width: min(100%, 1200px); /* 取较小值 */
  padding: max(1rem, 5%); /* 取较大值 */
}

CSS 颜色函数(2023)

/* oklch — 感知均匀的颜色空间 */
color: oklch(70% 0.15 220);
color: oklch(from var(--base-color) l c h); /* 相对颜色语法 */

/* color-mix */
color: color-mix(in oklch, blue 30%, red);
background: color-mix(in srgb, var(--primary) 80%, transparent);

/* display-p3 — 广色域 */
color: color(display-p3 0.5 0.9 0.3);

滚动相关 CSS

/* 滚动捕捉 */
.scroll-container {
  scroll-snap-type: x mandatory;
  overflow-x: scroll;
}
.scroll-item {
  scroll-snap-align: start;
}

/* 滚动条样式 */
.custom-scroll {
  scrollbar-width: thin;
  scrollbar-color: #888 #f0f0f0;
}

/* 滚动驱动动画(2024) */
@keyframes fade-in {
  from {
    opacity: 0;
    transform: translateY(20px);
  }
  to {
    opacity: 1;
    transform: none;
  }
}
.hero {
  animation: fade-in linear;
  animation-timeline: scroll();
  animation-range: entry 0% entry 50%;
}

@layer(级联层)

/* 控制样式优先级,解决特异性冲突 */
@layer reset, base, components, utilities;

@layer reset {
  * {
    margin: 0;
    box-sizing: border-box;
  }
}

@layer components {
  .button {
    padding: 8px 16px;
  }
}

@layer utilities {
  .mt-4 {
    margin-top: 1rem !important;
  }
}
/* 后声明的 layer 优先级更高 */

@property(自定义属性类型声明)

/* 让 CSS 变量支持动画和类型检查 */
@property --rotation {
  syntax: "<angle>";
  inherits: false;
  initial-value: 0deg;
}

.spinner {
  --rotation: 0deg;
  transform: rotate(var(--rotation));
  transition: --rotation 1s ease;
}
.spinner:hover {
  --rotation: 360deg;
}

text-wrap: balance / pretty

/* 均衡换行,防止最后一行只剩一个词 */
h1,
h2 {
  text-wrap: balance; /* 适合标题 */
}

p {
  text-wrap: pretty; /* 适合正文,优化孤行 */
}

CSS 子网格(subgrid)

.parent {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
}

.child {
  grid-column: span 3;
  display: grid;
  grid-template-columns: subgrid; /* 继承父网格轨道 */
}

Storage & 离线 {#storage}

localStorage / sessionStorage

localStorage.setItem("token", "abc123");
localStorage.getItem("token");
localStorage.removeItem("token");
localStorage.clear();

// sessionStorage 在标签关闭后清除
sessionStorage.setItem("draft", JSON.stringify(data));

IndexedDB

const request = indexedDB.open("MyDB", 1);

request.onupgradeneeded = (e) => {
  const db = e.target.result;
  const store = db.createObjectStore("users", { keyPath: "id" });
  store.createIndex("email", "email", { unique: true });
};

request.onsuccess = (e) => {
  const db = e.target.result;
  const tx = db.transaction("users", "readwrite");
  const store = tx.objectStore("users");
  store.add({ id: 1, name: "Alice", email: "alice@example.com" });
};

// 推荐使用封装库:idb(Jake Archibald)
import { openDB } from "idb";
const db = await openDB("MyDB", 1, {
  upgrade(db) {
    db.createObjectStore("users", { keyPath: "id" });
  },
});
await db.put("users", { id: 1, name: "Alice" });
const user = await db.get("users", 1);

Cache API

// 通常在 Service Worker 中使用
const cache = await caches.open("v1");
await cache.addAll(["/index.html", "/app.js", "/style.css"]);

// 缓存优先策略
const response = (await caches.match(request)) ?? (await fetch(request));

File System Access API

// 读取本地文件
const [fileHandle] = await window.showOpenFilePicker({
  types: [{ accept: { "text/*": [".txt", ".md"] } }],
});
const file = await fileHandle.getFile();
const text = await file.text();

// 写入本地文件
const writable = await fileHandle.createWritable();
await writable.write("Hello World");
await writable.close();

// 选择目录
const dirHandle = await window.showDirectoryPicker();
for await (const [name, handle] of dirHandle) {
  console.log(name, handle.kind);
}

Storage Manager

// 估算存储用量
const { quota, usage } = await navigator.storage.estimate();
console.log(`已用 ${((usage / quota) * 100).toFixed(1)}%`);

// 申请持久化存储(不被浏览器自动清除)
const granted = await navigator.storage.persist();

网络与通信 {#network}

Fetch API

const res = await fetch("/api/users", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ name: "Alice" }),
  signal: AbortController.signal,
});

if (!res.ok) throw new Error(`HTTP ${res.status}`);

const data = await res.json();
// 其他格式:res.text(), res.blob(), res.arrayBuffer(), res.formData()

// 流式响应
const reader = res.body.getReader();
while (true) {
  const { done, value } = await reader.read();
  if (done) break;
  process(value);
}

AbortController

const controller = new AbortController();
const { signal } = controller;

setTimeout(() => controller.abort(), 5000); // 5 秒超时

try {
  const res = await fetch("/api/data", { signal });
} catch (err) {
  if (err.name === "AbortError") console.log("请求已取消");
}

// AbortSignal 工具方法
const signal = AbortSignal.timeout(5000); // 超时信号
const combined = AbortSignal.any([s1, s2]); // 任一中止即触发

WebSocket

const ws = new WebSocket("wss://example.com/socket");

ws.onopen = () => ws.send(JSON.stringify({ type: "hello" }));
ws.onmessage = (e) => console.log(JSON.parse(e.data));
ws.onerror = (e) => console.error(e);
ws.onclose = (e) => console.log("closed", e.code);

// 关闭
ws.close(1000, "Normal closure");

Server-Sent Events (SSE)

// 服务器单向推送,自动重连
const es = new EventSource("/api/stream");

es.onmessage = (e) => console.log(e.data);
es.addEventListener("update", (e) => console.log(e.data));
es.onerror = () => console.error("连接断开,将自动重连");
es.close();

// 服务端(Node.js)
res.setHeader("Content-Type", "text/event-stream");
res.write("data: Hello\n\n");
res.write('event: update\ndata: {"count":1}\n\n');

WebRTC

// 点对点音视频通信
const pc = new RTCPeerConnection({
  iceServers: [{ urls: "stun:stun.l.google.com:19302" }],
});

// 添加媒体流
const stream = await navigator.mediaDevices.getUserMedia({
  video: true,
  audio: true,
});
stream.getTracks().forEach((track) => pc.addTrack(track, stream));

// 数据通道
const channel = pc.createDataChannel("chat");
channel.send("Hello peer!");

Beacon API

// 页面卸载时可靠发送数据(不阻塞页面关闭)
window.addEventListener("visibilitychange", () => {
  if (document.visibilityState === "hidden") {
    navigator.sendBeacon("/analytics", JSON.stringify({ event: "page_exit" }));
  }
});

多线程与并发 {#workers}

Web Worker

// main.js
const worker = new Worker("worker.js");
worker.postMessage({ data: [1, 2, 3] });
worker.onmessage = (e) => console.log(e.data);
worker.terminate();

// Transferable — 零拷贝转移(转移后原线程不可用)
const buffer = new ArrayBuffer(1024 * 1024);
worker.postMessage(buffer, [buffer]);

// worker.js
self.onmessage = ({ data }) => {
  const result = heavyCompute(data.data);
  self.postMessage(result);
};

Shared Worker

// 多个标签页/窗口共享同一个 worker
const sw = new SharedWorker("shared.js");
sw.port.start();
sw.port.postMessage("hello");
sw.port.onmessage = (e) => console.log(e.data);

// shared.js
self.onconnect = (e) => {
  const port = e.ports[0];
  port.onmessage = ({ data }) => port.postMessage(data);
};

Service Worker

// 注册
navigator.serviceWorker.register("/sw.js", { scope: "/" });

// sw.js
self.addEventListener("install", (e) => {
  e.waitUntil(
    caches.open("v1").then((c) => c.addAll(["/index.html", "/app.js"])),
  );
});

self.addEventListener("fetch", (e) => {
  e.respondWith(
    caches.match(e.request).then((cached) => cached ?? fetch(e.request)),
  );
});

self.addEventListener("push", (e) => {
  e.waitUntil(
    self.registration.showNotification("新消息", { body: e.data.text() }),
  );
});

图形与媒体 {#graphics}

Canvas 2D

const canvas = document.querySelector("canvas");
const ctx = canvas.getContext("2d");

ctx.fillStyle = "#007bff";
ctx.fillRect(10, 10, 100, 50);

ctx.beginPath();
ctx.arc(100, 100, 40, 0, Math.PI * 2);
ctx.stroke();

// 图片
const img = new Image();
img.onload = () => ctx.drawImage(img, 0, 0);
img.src = "/photo.jpg";

// 导出
canvas.toBlob((blob) => saveFile(blob), "image/webp", 0.9);

WebGL / WebGL2

const gl = canvas.getContext("webgl2");
// 着色器 + 缓冲区 + 绘制调用
// 实际项目推荐使用 Three.js / Babylon.js

WebGPU(2024)

// 下一代 GPU API,支持计算着色器
const adapter = await navigator.gpu.requestAdapter();
const device = await adapter.requestDevice();

// 比 WebGL 更低层级、性能更好
// 适合 ML 推理、复杂渲染、通用计算

OffscreenCanvas

// 在 Worker 中进行 Canvas 渲染,不阻塞主线程
const offscreen = canvas.transferControlToOffscreen();
worker.postMessage({ canvas: offscreen }, [offscreen]);

// worker.js
self.onmessage = ({ data }) => {
  const ctx = data.canvas.getContext("2d");
  ctx.fillRect(0, 0, 100, 100);
};

Media Session API

// 控制系统媒体通知/锁屏控件
navigator.mediaSession.metadata = new MediaMetadata({
  title: "歌曲名称",
  artist: "艺术家",
  artwork: [{ src: "/cover.jpg", sizes: "512x512" }],
});

navigator.mediaSession.setActionHandler("play", () => audio.play());
navigator.mediaSession.setActionHandler("pause", () => audio.pause());
navigator.mediaSession.setActionHandler("nexttrack", () => playNext());

Screen Capture API

// 录制屏幕
const stream = await navigator.mediaDevices.getDisplayMedia({
  video: { displaySurface: "window" },
  audio: true,
});
const recorder = new MediaRecorder(stream);
recorder.ondataavailable = (e) => chunks.push(e.data);
recorder.start(1000);

性能 API {#performance}

Performance Observer

const observer = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    console.log(entry.name, entry.duration);
  }
});

observer.observe({ type: "longtask", buffered: true });
observer.observe({ type: "layout-shift", buffered: true });
observer.observe({ type: "largest-contentful-paint", buffered: true });
observer.observe({ type: "navigation", buffered: true });
observer.observe({ type: "resource", buffered: true });

User Timing API

performance.mark("start-task");
// ... 执行任务
performance.mark("end-task");
performance.measure("task-duration", "start-task", "end-task");

const [measure] = performance.getEntriesByName("task-duration");
console.log(`耗时:${measure.duration.toFixed(2)}ms`);

const [nav] = performance.getEntriesByType("navigation");
console.log("DNS:", nav.domainLookupEnd - nav.domainLookupStart);
console.log("TCP:", nav.connectEnd - nav.connectStart);
console.log("TTFB:", nav.responseStart - nav.requestStart);
console.log("DOM解析:", nav.domContentLoadedEventEnd - nav.responseEnd);

Scheduler API(2024)

// 调度任务,让出主线程
await scheduler.yield(); // 让出控制权,处理用户输入

scheduler.postTask(
  () => {
    // 低优先级后台任务
  },
  { priority: "background" },
); // user-blocking | user-visible | background

requestIdleCallback

// 浏览器空闲时执行非关键任务
requestIdleCallback((deadline) => {
  while (deadline.timeRemaining() > 0 && tasks.length > 0) {
    tasks.shift()(); // 执行一个任务
  }
  if (tasks.length > 0) requestIdleCallback(processTasks);
});

观察者 API {#observers}

IntersectionObserver

// 元素进入/离开视口
const observer = new IntersectionObserver(
  (entries) => {
    entries.forEach((entry) => {
      if (entry.isIntersecting) {
        entry.target.classList.add("visible");
        observer.unobserve(entry.target);
      }
    });
  },
  {
    threshold: 0.1, // 10% 可见时触发
    rootMargin: "0px 0px -100px 0px",
  },
);

document.querySelectorAll(".lazy").forEach((el) => observer.observe(el));

MutationObserver

// 监听 DOM 变化
const observer = new MutationObserver((mutations) => {
  for (const mutation of mutations) {
    if (mutation.type === "childList") {
      mutation.addedNodes.forEach((node) => init(node));
    }
    if (mutation.type === "attributes") {
      console.log(`属性 ${mutation.attributeName} 变化`);
    }
  }
});

observer.observe(document.body, {
  childList: true,
  subtree: true,
  attributes: true,
  attributeFilter: ["class", "data-state"],
});

ResizeObserver

// 监听元素尺寸变化(非 window resize)
const observer = new ResizeObserver((entries) => {
  for (const entry of entries) {
    const { width, height } = entry.contentRect;
    updateLayout(width, height);
  }
});

observer.observe(document.querySelector(".chart"));

设备与硬件 {#device}

Geolocation

navigator.geolocation.getCurrentPosition(
  ({ coords }) => console.log(coords.latitude, coords.longitude),
  (err) => console.error(err),
  { enableHighAccuracy: true, timeout: 5000 },
);

const watchId = navigator.geolocation.watchPosition(handler);
navigator.geolocation.clearWatch(watchId);

Clipboard API

// 写入剪贴板
await navigator.clipboard.writeText("复制内容");
await navigator.clipboard.write([new ClipboardItem({ "image/png": blob })]);

// 读取剪贴板
const text = await navigator.clipboard.readText();
const items = await navigator.clipboard.read();

Web Bluetooth

const device = await navigator.bluetooth.requestDevice({
  filters: [{ services: ["heart_rate"] }],
});
const server = await device.gatt.connect();
const service = await server.getPrimaryService("heart_rate");
const char = await service.getCharacteristic("heart_rate_measurement");
char.startNotifications();
char.addEventListener("characteristicvaluechanged", handler);

Vibration API

navigator.vibrate(200); // 振动 200ms
navigator.vibrate([100, 50, 100]); // 振动-停-振动
navigator.vibrate(0); // 停止

Screen Orientation

screen.orientation.type; // 'portrait-primary'
await screen.orientation.lock("landscape");
screen.orientation.unlock();
screen.orientation.addEventListener("change", handler);

Battery Status API

const battery = await navigator.getBattery();
console.log(battery.level, battery.charging);
battery.addEventListener("levelchange", () => {
  if (battery.level < 0.2) showLowBatteryWarning();
});

Device Orientation / Motion

window.addEventListener("deviceorientation", ({ alpha, beta, gamma }) => {
  // alpha: 绕 Z 轴旋转 (0-360)
  // beta:  绕 X 轴旋转 (-180~180,前后倾斜)
  // gamma: 绕 Y 轴旋转 (-90~90,左右倾斜)
});

window.addEventListener("devicemotion", ({ acceleration, rotationRate }) => {
  console.log(acceleration.x, acceleration.y, acceleration.z);
});

Web Components {#web-components}

Custom Elements

class MyCard extends HTMLElement {
  static observedAttributes = ["title", "theme"];

  constructor() {
    super();
    this.attachShadow({ mode: "open" });
  }

  connectedCallback() {
    this.render();
  }

  attributeChangedCallback(name, oldVal, newVal) {
    this.render();
  }

  render() {
    this.shadowRoot.innerHTML = `
      <style>
        :host { display: block; padding: 1rem; }
        :host([theme="dark"]) { background: #333; color: white; }
      </style>
      <h2>${this.getAttribute("title")}</h2>
      <slot></slot>
    `;
  }
}

customElements.define("my-card", MyCard);
<my-card title="Hello" theme="dark">
  <p>内容在这里</p>
</my-card>

Shadow DOM

const shadow = element.attachShadow({ mode: "open" });

// 样式隔离
shadow.innerHTML = `
  <style>p { color: red; }</style>  <!-- 不影响外部 -->
  <p>Shadow DOM 内容</p>
`;

// CSS 穿透
// ::slotted(p) — 选择插槽内的元素
// :host — 选择宿主元素
// :host-context(.dark) — 宿主在 .dark 祖先下

HTML Templates

<template id="card-tpl">
  <div class="card">
    <h2 class="title"></h2>
    <slot name="content"></slot>
  </div>
</template>
const tpl = document.getElementById("card-tpl");
const clone = tpl.content.cloneNode(true);
clone.querySelector(".title").textContent = "Hello";
element.appendChild(clone);

安全与权限 {#security}

Permissions API

const result = await navigator.permissions.query({ name: "camera" });
// result.state: 'granted' | 'denied' | 'prompt'

result.addEventListener("change", () => {
  console.log("权限状态变化:", result.state);
});

Content Security Policy (CSP)

<meta
  http-equiv="Content-Security-Policy"
  content="default-src 'self'; script-src 'self' 'nonce-abc123'; img-src *"
/>
// 随机 nonce(服务端生成)
<script nonce="abc123">/* 允许执行 */</script>

SubtleCrypto API

// 生成密钥
const key = await crypto.subtle.generateKey(
  { name: "AES-GCM", length: 256 },
  true,
  ["encrypt", "decrypt"],
);

// 加密
const iv = crypto.getRandomValues(new Uint8Array(12));
const encrypted = await crypto.subtle.encrypt(
  { name: "AES-GCM", iv },
  key,
  new TextEncoder().encode("secret message"),
);

// 哈希
const hash = await crypto.subtle.digest("SHA-256", data);

// 随机数
crypto.randomUUID(); // UUID v4
crypto.getRandomValues(new Uint8Array(16)); // 随机字节

Sanitizer API(实验性)

// 安全地插入 HTML,防止 XSS
const sanitizer = new Sanitizer();
element.setHTML("<b>粗体</b><script>alert(1)</script>", { sanitizer });
// script 标签被移除

PWA(Progressive Web App){#pwa}

Web App Manifest

{
  "name": "My App",
  "short_name": "App",
  "start_url": "/",
  "display": "standalone",
  "background_color": "#ffffff",
  "theme_color": "#007bff",
  "icons": [
    { "src": "/icon-192.png", "sizes": "192x192", "type": "image/png" },
    { "src": "/icon-512.png", "sizes": "512x512", "type": "image/png" }
  ],
  "screenshots": [...],
  "shortcuts": [
    { "name": "新建", "url": "/new", "icons": [...] }
  ]
}

安装提示

let deferredPrompt;
window.addEventListener("beforeinstallprompt", (e) => {
  e.preventDefault();
  deferredPrompt = e;
  showInstallButton();
});

installButton.addEventListener("click", async () => {
  deferredPrompt.prompt();
  const { outcome } = await deferredPrompt.userChoice;
  console.log(outcome); // 'accepted' | 'dismissed'
});

Push Notifications

// 订阅推送
const sub = await registration.pushManager.subscribe({
  userVisibleOnly: true,
  applicationServerKey: urlBase64ToUint8Array(VAPID_PUBLIC_KEY),
});

// 将 sub 发送到服务器
await fetch("/api/push/subscribe", {
  method: "POST",
  body: JSON.stringify(sub),
});

// Service Worker 接收推送
self.addEventListener("push", (e) => {
  const { title, body } = e.data.json();
  e.waitUntil(
    self.registration.showNotification(title, {
      body,
      icon: "/icon.png",
      badge: "/badge.png",
      actions: [{ action: "open", title: "查看" }],
    }),
  );
});

Background Sync

// 网络恢复后自动重试
navigator.serviceWorker.ready.then((reg) => {
  reg.sync.register("send-message");
});

// sw.js
self.addEventListener("sync", (e) => {
  if (e.tag === "send-message") {
    e.waitUntil(sendPendingMessages());
  }
});

速查表

特性 类别 要点
dialog HTML 原生模态/非模态对话框
popover HTML 纯 HTML 弹出层
loading="lazy" HTML 图片/iframe 懒加载
inert HTML 禁用整个区域交互
View Transitions HTML API 页面切换动画
CSS 变量 CSS --var 全局主题
CSS Grid CSS 二维布局
Container Queries CSS 基于容器响应式
CSS 嵌套 CSS 原生 Sass 式嵌套
:has() CSS 父选择器
clamp() CSS 流体尺寸
@layer CSS 级联层控制
oklch CSS 现代颜色空间
text-wrap: balance CSS 均衡换行
滚动驱动动画 CSS 无 JS 滚动联动
Fetch API 网络 现代请求
AbortController 网络 取消请求
WebSocket 网络 双向实时通信
SSE 网络 服务器推送
Beacon 网络 页面卸载发数据
Web Worker 并发 后台线程
Service Worker 并发 离线/缓存/推送
IndexedDB 存储 客户端数据库
Cache API 存储 请求级缓存
File System Access 存储 读写本地文件
IntersectionObserver 观察者 视口进入检测
MutationObserver 观察者 DOM 变化监听
ResizeObserver 观察者 元素尺寸变化
Clipboard API 设备 读写剪贴板
SubtleCrypto 安全 加密/哈希
Custom Elements 组件 自定义 HTML 元素
Shadow DOM 组件 样式/结构隔离
Push Notifications PWA 推送通知
Background Sync PWA 离线数据同步