前端人必备的 JavaScript API 全面指南(含 postMessage、File、Stream、Web 组件等)

1. 跨上下文消息(postMessage

概念

在不同执行上下文(窗口、iframe、Worker、ServiceWorker、MessageChannel 的端口)之间以**结构化克隆(structured clone)可转移(transferable)**方式安全地传递数据的 API。

原理

  • 使用 postMessage(message, targetOrigin[, transfer]) 发送消息。
  • 接收端 message 事件包含:event.data(消息体)、event.origin(发送者 origin)、event.source(发送端 window/worker)、event.ports(MessagePort 列表)。
  • 支持 Structured Clone(可克隆对象:Object、Array、Date、Map、Set、ArrayBuffer、Blob、File、ImageBitmap 等),不能克隆函数、DOM 节点、某些原生句柄。
  • 可传输对象(transferable):例如 ArrayBufferMessagePortImageBitmapOffscreenCanvas 等,传输后原对象在发送端通常会"neuter"(变成 0 长度或不可用)。

对比

  • postMessage vs XMLHttpRequest/fetch:postMessage 是上下文间通信(不经过网络),而 fetch/XHR 是网络请求。
  • postMessage vs MessageChannelMessageChannel 提供一对端口(port1/port2),可实现双向、独立的消息流;postMessage 可携带 ports 以配合 MessageChannel 使用。

实践(示例)

父页面向 iframe 发送消息 & 使用 MessageChannel:

xml 复制代码
<!-- 父页面 -->
<iframe id="f" src="https://example.com/receiver.html"></iframe>
<script>
const iframe = document.getElementById('f');

// 1) 简单 postMessage(确保指定 targetOrigin 提高安全性)
iframe.contentWindow.postMessage({type: 'hello', text: 'hi iframe'}, 'https://example.com');

// 2) 使用 MessageChannel 传递一个专属端口(便于双向流)
const mc = new MessageChannel();
// mc.port1 在父页面使用
mc.port1.onmessage = (e) => { console.log('来自 iframe 的消息(通过 port):', e.data); };
// 把 port2 作为 transferable 发送给 iframe(建立私有双向通道)
iframe.contentWindow.postMessage({type: 'init-port'}, 'https://example.com', [mc.port2]);
</script>

iframe(接收方)示例(receiver.html):

javascript 复制代码
// iframe 内
window.addEventListener('message', (e) => {
  // 验证来源以防 XSS/CSRF
  if (e.origin !== 'https://your-parent-origin.example') return;
  console.log('收到 data:', e.data, 'ports:', e.ports);

  // 如果 parent 发送了端口(MessageChannel.port),使用它
  if (e.ports && e.ports[0]) {
    const port = e.ports[0];
    port.postMessage({ack: true, from: 'iframe'});
    port.onmessage = (m) => console.log('port 收到:', m.data);
  }
});

Transfer 示例(ArrayBuffer)

javascript 复制代码
// 发送端
const ab = new ArrayBuffer(8);
const view = new Uint8Array(ab); view.set([1,2,3,4]);
worker.postMessage({type:'buf', buf: ab}, [ab]); // 把 ab 作为 transferable 传走
// 传输后,ab 在发送端通常变成 neutered(长度 0)

拓展

  • 可以配合 BroadcastChannel 做多窗口广播。
  • 与 ServiceWorker/SharedWorker/MessageChannel 结合实现复杂的消息拓扑(pub/sub、RPC)。

潜在问题

  • 不验证 origin 会导致安全问题(数据泄漏 / 注入)。
  • 误用 transferable 会导致发送端数据"消失"。
  • 结构化克隆失败的类型(函数、DOM 节点)会报错或忽略。

2. Encoding API(字符串 ↔ 定形数组)

主体:TextEncoder / TextEncoderStream / TextDecoder / TextDecoderStream

概念

把 JS 字符串(基于 Unicode code points)编码成字节序列Uint8Array 等),或者把字节序列解码回字符串

原理

  • 编码(例如 UTF-8)会把 Unicode code point 按编码规则映射为一个或多个字节。编码后的结果是字节序列(Uint8Array)。它不是"排序",而是对字符的二进制表示。
  • 解码则是对字节序列按指定编码(如 utf-8, utf-16le)解析回字符串。
  • TextEncoderStream/TextDecoderStream 提供流式(TransformStream)接口,适合处理网络或大数据流。

对比

  • TextEncoder.encode()(一次性) vs TextEncoder.encodeInto()(就地写入、避免分配)
  • TextEncoder(批量) vs TextEncoderStream(流式)
  • TextDecoder.decode(buf, {stream:true})(增量解码) vs TextDecoderStream(更直观、可管道)

2.1 文本编码

2.1.1 批量编码(TextEncoder

  • TextEncoder.encode(str) 返回 Uint8Array(UTF-8 编码)。
  • encodeInto(source, destinationUint8Array) 尝试把编码写入已分配的目标数组,返回 {read, written};避免多次分配。

示例:

javascript 复制代码
// 批量编码示例
const encoder = new TextEncoder();                  // 默认 UTF-8
const bytes = encoder.encode('Hello €');            // -> Uint8Array
console.log(bytes); // [72,101,108,108,111,32,226,130,172]

// 使用 encodeInto 减少分配(适合高频或流式写入)
const target = new Uint8Array(32);                  // 事先分配好 buffer
const res = encoder.encodeInto('Hello €', target);  // { read: x, written: y }
console.log(res, target.subarray(0, res.written));  // 查看写入的部分

(注:encode 总是返回新分配的 Uint8ArrayencodeInto 更节省内存)

2.1.2 流编码(TextEncoderStream

  • TextEncoderStream 返回 { writable, readable }:向 writable 写字符串,readable 输出 Uint8Array 的流。
  • 无法直接强制每次输出固定字节数 :流的分片由实现/写入策略决定。若需固定大小,需在写入端分割字符串或在 TransformStream 中手动分片。

示例(写字符串到一个可读字节流并读取):

javascript 复制代码
// TextEncoderStream 示例
const encoderStream = new TextEncoderStream();          // 创建流式编码器
const writer = encoderStream.writable.getWriter();      // 获取写入端(写字符串)
const reader = encoderStream.readable.getReader();      // 获取读取端(读 Uint8Array)

await writer.write('chunk1');    // 写入字符串
await writer.write('chunk2');    // 再写入

writer.close();                  // 完成写入,触发 readable 结束

while (true) {
  const {value, done} = await reader.read();
  if (done) break;
  console.log('收到字节块:', value); // value 是 Uint8Array
}

2.2 文本解码

2.2.1 批量解码(TextDecoder

  • const decoder = new TextDecoder('utf-8')decoder.decode(u8)
  • decoder.decode(u8, { stream: true }) 可用于增量解码,最后一次调用不需 stream:true

示例:

ini 复制代码
const decoder = new TextDecoder('utf-8');
const s = decoder.decode(new Uint8Array([0xE2,0x82,0xAC])); // '€'

2.2.2 流解码(TextDecoderStream

  • TextDecoderStream 把字节 ReadableStream 转换为字符串 ReadableStream,方便与 fetch().body 配合。
  • 示例 1:用 Uint16Array.of(102,111,111)(这些值是 UTF-16 code units:102='f'111='o') ------ 用 utf-16le 解码。
javascript 复制代码
// 示例:用 Uint16Array 解码(utf-16 little endian)
const u16 = Uint16Array.of(102, 111, 111);           // [102, 111, 111] => 'f','o','o'(UTF-16 code units)
const u8 = new Uint8Array(u16.buffer);               // 视内存为字节流 (little-endian)
const readable = new ReadableStream({
  start(controller) {
    controller.enqueue(u8);      // 推入字节
    controller.close();
  }
});

const decoded = await readable
  .pipeThrough(new TextDecoderStream('utf-16le'))    // 指定 utf-16le
  .getReader()
  .read();

console.log(decoded.value); // "foo"
  • 示例 2:fetch 动态拉取并流式解码(UTF-8):
javascript 复制代码
// fetch + TextDecoderStream 示例
async function fetchAndDecode(url) {
  const res = await fetch(url);                       // 请求
  // res.body 是 ReadableStream<Uint8Array>
  const textStream = res.body.pipeThrough(new TextDecoderStream('utf-8'));
  const reader = textStream.getReader();
  let all = '';
  while (true) {
    const {value, done} = await reader.read();
    if (done) break;
    all += value;           // value 是字符串片段
  }
  return all;               // 整个响应解码为字符串
}

拓展

  • TextEncoderStream / TextDecoderStream 在处理大文件、实时数据(WebSocket 内容、fetch 流)时非常有用。
  • 如果需要更细的分片控制,可插入自定义 TransformStream 做分片或缓冲管理。

潜在问题

  • 编码/解码时选择错误的编码(如用 utf-8 解 utf-16 数据)会导致乱码。
  • encodeInto 如果目标空间不足,会只写部分内容;需检查 written 并循环写入剩余。
  • 浏览器兼容性(老浏览器可能不支持 *Stream 类型),需降级或 polyfill。

3. File API 与 Blob API(由来与基本用法)

概念

  • Blob:表示不可变的类文件对象(原始数据的二进制块),可从字符串、ArrayBuffer、TypedArray、Blob 片段构造,用于上传、建立对象 URL、切片等。
  • File:继承自 Blob,表示来自用户计算机的文件,包含元数据(name, lastModified)。
    这套 API 最初为浏览器端文件上传、处理本地文件提供能力(不用提交到服务器也能读取/预览)。

原理

  • File 就是带有文件名和最后修改时间的 Blob。两者提供 .size, .type 和读取方法(arrayBuffer(), text(), stream())。
  • File 最常见来源:<input type="file">、Drag & Drop、File System Access API(更现代的浏览器 API)。

对比

  • File vs BlobFilenamelastModifiedBlob 更轻量。
  • 旧的读取方式(FileReader) vs 新的 Promise 风格(blob.text(), blob.arrayBuffer())。推荐使用新的 Promise API(更清晰、支持 async/await)。

3.1 File 类型(属性与方法)

常见属性:

  • file.name:文件名(string)。
  • file.size:字节数(number)。
  • file.type:MIME 类型(string)。
  • file.lastModified:最后修改时间戳(ms, number)。(历史上有 lastModifiedDate,已不推荐)
    方法(继承自 Blob):
  • file.slice(start, end, contentType):返回片段 Blob。
  • file.arrayBuffer()file.text()file.stream():现代读取方式。

使用场景:上传、预览图片、读取日志文件内容、将本地文件转为 ArrayBuffer 处理等。最新获取本地文件方式还包括 File System Access API (如 showOpenFilePicker()),但需 HTTPS 与权限。

示例(读取 <input>):

typescript 复制代码
<input id="f" type="file" />
<script>
const ip = document.getElementById('f');
ip.addEventListener('change', () => {
  const file = ip.files[0];                 // File 对象
  console.log(file.name, file.size, file.type, file.lastModified);
  // 现代读取
  file.text().then(text => console.log(text));
});
</script>

3.2 FileReader 类型(事件式 API)

方法:

  • readAsText(file, encoding?):读成字符串(可指定编码)。
  • readAsDataURL(file):读成 base64 data URL(用于直接 img.src 显示)。
  • readAsArrayBuffer(file):读成 ArrayBuffer(二进制处理)。
  • readAsBinaryString(file):已被弃用/不建议使用。

事件/回调:

  • onprogressonload(读取成功)、onerroronabort
  • abort():取消正在进行的读操作。

示例(展示多种方法):

ini 复制代码
<input id="file" type="file" />
<script>
const fI = document.getElementById('file');
fI.addEventListener('change', () => {
  const file = fI.files[0];
  const fr = new FileReader();

  fr.onprogress = (e) => console.log('已读字节:', e.loaded, '/', e.total);
  fr.onload = () => console.log('读取完成, 结果:', fr.result);
  fr.onerror = (err) => console.error('读取出错', err);
  fr.onabort = () => console.log('读取被中止');

  // 作为文本读取
  fr.readAsText(file, 'utf-8');

  // 若想读取为 ArrayBuffer 可用 readAsArrayBuffer(file)
  // 若想作为 data URL(图片预览)用 readAsDataURL(file)
});
</script>

3.3 FileReaderSync(只能在 Worker 中)

  • 同步版本,只能在 Worker 的全局作用域(不是主线程)中使用。
    示例(worker.js):
ini 复制代码
// worker.js
self.onmessage = (e) => {
  const file = e.data.file; // 从主线程传来的 File(可能作为 transferable)
  const frs = new FileReaderSync();           // 仅 Worker 可用
  const text = frs.readAsText(file);          // 同步返回字符串
  postMessage({ text });
};

3.4 Blob 与部分读取(slice)

  • blob.slice(start, end, contentType) 返回新 Blob(子片),便于分片上传。
  • new Blob([part1, part2], {type:'...'}) 创建 Blob。
  • Blob 的便利方法:blob.text()blob.arrayBuffer()blob.stream()

示例(Blob):

ini 复制代码
const b = new Blob(['Hello',' world'], {type: 'text/plain'});
const part = b.slice(0,5, 'text/plain'); // 'Hello'
const text = await part.text();          // 'Hello'

3.5 对象 URL 与 Blob(URL.createObjectURL

  • const url = URL.createObjectURL(blob):创建一个本地的 blob: URL,可赋值给 <img><a> 等。
  • 使用完后应调用 URL.revokeObjectURL(url) 释放资源(避免内存泄漏)。

示例(图片预览并释放):

ini 复制代码
const img = document.createElement('img');
const blob = new Blob([/* image bytes */], { type: 'image/png' });
const url = URL.createObjectURL(blob);
img.src = url;
img.onload = () => {
  URL.revokeObjectURL(url); // 加载完成后释放
};
document.body.appendChild(img);

3.6 读取拖动文件(Drag & Drop)

  • drop 事件中的 e.dataTransfer.files 提供 FileList
  • 常见场景:网页支持拖入文件后预览或上传。

示例(拖放文件并读取文件名):

xml 复制代码
<div id="dropZone">拖放文件到这里</div>
<script>
const dz = document.getElementById('dropZone');
dz.addEventListener('dragover', (e) => { e.preventDefault(); /* 表示接受 drop */ });
dz.addEventListener('drop', async (e) => {
  e.preventDefault();
  const files = e.dataTransfer.files;
  for (const f of files) {
    console.log('文件名:', f.name, '大小:', f.size);
    // 例如读取文本
    const text = await f.text();
    console.log(text.slice(0, 200)); // 预览前 200 字符
  }
});
</script>

潜在问题(File / Blob)

  • 大文件时直接 .text() 可能消耗大量内存,推荐流式处理(stream())。
  • URL.createObjectURL 若忘记 revoke 会占内存。
  • File System Access API 浏览器实现差异与权限限制。

4. 媒体元素(<video><audio>

概念

浏览器原生播放音视频的 HTML 元素与 API,支持控制、事件、媒体流等。

原理

浏览器加载媒体资源(通过 <source>src 或 Media Streams),解码器判断能否播放,通过媒体元素 API 暴露播放控制、元数据、事件与缓冲信息。

对比

  • <video> 支持视频画面,而 <audio> 仅音频(无画面)。
  • 原生控件 vs 自定义控件:原生控件简单、兼容好;自定义控件灵活可交互,可与 Accessibility 注意点结合。

4.1 常用属性(示例)

  • srccurrentTimedurationpausedplaybackRatevolumemutedcontrolsbufferedreadyStateseeking 等。

4.2 常用事件

  • playpauseendedtimeupdateloadedmetadatacanplaycanplaythrougherrorseekingseeked 等。

4.3 自定义媒体播放(示例)

一个最小自定义控制示例(按钮 + 进度):

ini 复制代码
<video id="v" width="480" src="video.mp4"></video>
<button id="play">播放/暂停</button>
<input id="seek" type="range" min="0" max="100" value="0" />
<script>
const v = document.getElementById('v');
const playBtn = document.getElementById('play');
const seek = document.getElementById('seek');

playBtn.addEventListener('click', async () => {
  if (v.paused) {
    await v.play(); // 返回 Promise,可能因浏览器策略被拒绝
    playBtn.textContent = '暂停';
  } else {
    v.pause();
    playBtn.textContent = '播放';
  }
});

v.addEventListener('loadedmetadata', () => {
  seek.max = Math.floor(v.duration);
});
v.addEventListener('timeupdate', () => {
  seek.value = Math.floor(v.currentTime);
});

seek.addEventListener('input', () => {
  v.currentTime = Number(seek.value);
});
</script>

(注:play() 返回 Promise,若浏览器阻止自动播放,会 reject)

4.4 检测编解码器(canPlayType

示例:

javascript 复制代码
const v = document.createElement('video');
console.log(v.canPlayType('video/mp4; codecs="avc1.42E01E, mp4a.40.2"')); // 'probably' | 'maybe' | ''

4.5 音频格式

常见支持格式(因浏览器而异):

  • audio/mpeg(.mp3)
  • audio/wav(.wav)
  • audio/ogg(.ogg,Vorbis)
  • audio/webm(WebM/Opus)

潜在问题

  • 编解码器支持受浏览器/平台限制(Safari 对某些格式支持差异)。
  • 自动播放策略(需要用户交互或静音)。
  • 大文件或流式播放需处理缓冲、带宽变化。

5. 原生拖放(Drag & Drop)

概念

HTML5 提供拖放事件与 DataTransfer 对象用于在页面内部或跨页面拖放数据/文件。

原理

拖动元素时触发 dragstart,目标元素触发 dragenter/dragover,放置时触发 drop。浏览器通过 dataTransfer 在拖放源与目标之间传递数据/文件。

5.1 拖放事件

事件:dragstartdragdragenddragenterdragoverdragleavedrop

关键点:在 dragover 中需要 event.preventDefault() 来表示目标接受 drop(否则 drop 不会触发)。

示例(基本拖放):

xml 复制代码
<div id="drag" draggable="true">拖我</div>
<div id="target">放到这里</div>
<script>
const d = document.getElementById('drag');
const t = document.getElementById('target');

d.addEventListener('dragstart', (e) => {
  e.dataTransfer.setData('text/plain', '这是拖动的数据'); // 设置可取回的数据
  e.dataTransfer.effectAllowed = 'copyMove';               // 允许的操作
});

t.addEventListener('dragover', (e) => { e.preventDefault(); // 允许 drop
  e.dataTransfer.dropEffect = 'copy';  // 鼠标指针效果
});
t.addEventListener('drop', (e) => {
  e.preventDefault();
  const txt = e.dataTransfer.getData('text/plain'); // 读取数据
  console.log('drop got:', txt);
});
</script>

5.2 自定义放置位置(视觉反馈)

通过在 dragenter/dragleave 中添加/移除 CSS 类来突出可放置区域,并在 dragover 中设置 dropEffect 让用户知道会发生什么(copy/move/link)。

5.3 dataTransfer 对象(属性与方法)

  • 属性:dataTransfer.types(可获取数据类型)、dataTransfer.files(FileList)、dataTransfer.items(DataTransferItemList)、dropEffecteffectAllowed
  • 方法:setData(format, data), getData(format), clearData([format]), setDragImage(img, x, y)(自定义拖拽时的影像)。

示例(自定义拖影):

ini 复制代码
e.dataTransfer.setDragImage(myCanvasOrImgElement, 10, 10);

注意:DataTransferItemList.add() 在某些浏览器有实现差异(要注意兼容性)。

5.4 dropEffecteffectAllowed

  • effectAllowed(在拖动源设置): 'none'|'copy'|'copyLink'|'copyMove'|'link'|'linkMove'|'move'|'all'|'uninitialized'
  • dropEffect(在目标设置): 'none'|'copy'|'link'|'move'
    目标会用 dropEffect 表示实际会执行哪种操作(例如按住键修改为 copy 或 move)。

5.5 可拖动能力

  • 默认可拖动:链接 <a>、图片 <img>、选中文本等;其他元素可通过 draggable="true" 设置为可拖动。
  • 某些元素(form 表单内部)行为特殊,需测试。

5.6 其他成员

  • setDragImage:自定义拖拽光标/影像。
  • types:列出可获取的数据格式(如 ["text/plain", "text/html"])。
  • clearData:清除特定格式或全部数据。

潜在问题

  • 跨 iframe 或跨域拖放需要谨慎安全策略。
  • 浏览器实现差异(尤其是 DataTransferItemList 操作)。
  • 移动端对拖放支持有限(常用触摸事件替代)。

6. Notifications API(通知)

概念

在浏览器中展示系统/浏览器层级通知(非内嵌 UI),用于提醒用户事件或新的信息(通常需用户许可)。

6.1 通知权限

  • Notification.permission:返回 'default' | 'granted' | 'denied'
  • Notification.requestPermission():请求用户授权(通常需用户交互触发,且只能在安全上下文 https 下)。

示例:

ini 复制代码
if (Notification.permission === 'default') {
  const perm = await Notification.requestPermission(); // 返回 'granted' 或 'denied'
  console.log('权限:', perm);
}

6.2 显示与隐藏通知

  • new Notification(title, options) 创建并显示通知(如果权限为 granted)。
  • notification.close() 隐藏通知。

示例:

ini 复制代码
const n = new Notification('新消息', { body: '你有一条新消息', icon: '/icon.png' });
n.onclick = () => window.focus();
setTimeout(() => n.close(), 5000); // 5 秒后自动关闭

在 Push 场景通常在 Service Worker 中用 self.registration.showNotification(...)

6.3 通知的生命周期事件

  • onshowonclickoncloseonerror --- 可绑定回调处理交互。

潜在问题

  • 只能在 HTTPS 下使用(或 localhost)。
  • 用户可能拒绝权限;滥用会导致用户关闭/封禁。
  • 一些平台(移动)对通知的样式/行为受限。

7. Page Visibility API(页面可见性)

概念

页面可见性 API 允许检测页面是否为"可见状态(visible)"或"隐藏(hidden)",以便做节流或暂停不必要的任务(如动画、定时轮询)。

原理

  • document.visibilityState:字符串 visible / hidden / prerender(可能有其他状态)。
  • document.hidden:布尔值,true 表示页面被隐藏。
  • 事件 visibilitychange:当可见性改变时触发。

实践(节流示例)

scss 复制代码
let timerId = null;
function startPolling() {
  timerId = setInterval(() => {
    console.log('轮询后台数据...');
  }, 5000);
}
function stopPolling() {
  if (timerId) { clearInterval(timerId); timerId = null; }
}

document.addEventListener('visibilitychange', () => {
  if (document.hidden) {
    stopPolling(); // 页面不可见时停止轮询,节省资源
  } else {
    startPolling(); // 恢复轮询
  }
});

// 启动初始轮询(若页面可见)
if (!document.hidden) startPolling();

潜在问题

  • 做"短任务"的节流没必要每次都停止;需根据任务成本来决定是否停止。
  • 移动设备的省电模式和浏览器策略可能进一步限制后台工作。

8. Streams API(可读、可写、转换流)

本节是核心:理解流、控制反压(backpressure)、管道(pipe)等。

8.1 理解流(概念)

  • 可读流(ReadableStream) :产生数据(chunks),消费者通过 getReader()for await 获取。
  • 可写流(WritableStream) :接收数据,写入端通过 getWriter() 写入。
  • 转换流(TransformStream) :包含 writablereadable,用于对数据进行中间处理(map/filter/batch)。
  • 块(chunks) :流中处理的数据单元(可以是字符串、Uint8Array 等)。
  • 内部队列 & 反压 :流有内部队列与 queuingStrategyhighWaterMark),当消费者处理不及时时,反压会让生产者放慢写入速率。反压非常重要,防止内存爆炸。

8.2 可读流(controller & reader)

  • ReadableStreamstart(controller) / pull(controller) / cancel(reason) 用来推数据。
  • reader = stream.getReader()await reader.read() 得到 {value, done}
    示例(自定义可读流):
javascript 复制代码
const rs = new ReadableStream({
  start(controller) {
    controller.enqueue('chunk1');   // 推入第一个片段
    controller.enqueue('chunk2');   // 推入第二个片段
    controller.close();             // 结束流
  }
});

const r = rs.getReader();
(async () => {
  while (true) {
    const {value, done} = await r.read();
    if (done) break;
    console.log('读到:', value);
  }
})();

8.3 可写流(WritableStream 与 writer)

  • const writer = writable.getWriter(); 然后 await writer.ready(当内部队列空时 resolve),调用 writer.write(chunk),最后 writer.close()
    示例:
javascript 复制代码
const ws = new WritableStream({
  write(chunk) { console.log('写入:', chunk); },
  close() { console.log('写入流已关闭'); }
});
const writer = ws.getWriter();
await writer.ready;           // 等待写入器准备好(注意:ready 可以反映内部队列状态)
await writer.write('hello');  // 写入
await writer.write('world');
await writer.close();         // 结束

(注:writer.ready 是一个 Promise,反映何时可以安全写入或内部队列下游已消化)

8.4 转换流(TransformStream)

  • new TransformStream({ transform(chunk, controller) { ... controller.enqueue(newChunk); } })
  • transform() 中可以进行异步处理(可返回 Promise),并 enqueue 结果。

示例: 读取源(Readable),先把读取全部完成(示范"先读取完再写入"的理念),然后再写入到目标 Writable:

javascript 复制代码
// 先把可读流全部读完(聚合),然后写入到可写流(示例用于理解)
async function readAllThenWrite(readable, writable) {
  const reader = readable.getReader();
  const chunks = [];
  while (true) {
    const {value, done} = await reader.read();
    if (done) break;
    chunks.push(value); // 收集所有块
  }
  // 等待读取逻辑完成(现在我们有全部 chunks)
  const writer = writable.getWriter();
  await writer.ready;
  for (const c of chunks) {
    await writer.write(c); // 顺序写入
  }
  await writer.close();
}

8.5 通过管道连接流(pipeThrough / pipeTo

  • readable.pipeThrough(transformStream):产生新的可读流(把 transform 的 readable 作为输出),常用于串联处理。
  • readable.pipeTo(writable):把 readable 直接写到 writable,返回 Promise 在管道完成时 resolve。
    示例(文本流转大写):
javascript 复制代码
// 源:ReadableStream 输出字符串
const source = new ReadableStream({
  start(controller) {
    controller.enqueue('hello ');
    controller.enqueue('world');
    controller.close();
  }
});

// Transform:把字符串转成大写
const t = new TransformStream({
  transform(chunk, controller) {
    controller.enqueue(chunk.toUpperCase());
  }
});

// 目标:输出到控制台(Writable)
const target = new WritableStream({
  write(chunk) { console.log('写到目标:', chunk); }
});

// 管道
await source.pipeThrough(t).pipeTo(target); // 连接并等待完成

拓展

  • fetch()Response.bodyReadableStream,可以直接与 TextDecoderStreamTransformStream 链接做实时处理。
  • 剖析反压参数:queuingStrategyhighWaterMarksize 回调决定何时触发 ready / backpressure。

潜在问题

  • 不正确处理 backpressure 可能导致内存飙升。
  • 需要谨慎处理错误(pipeTo 默认在错误时会关闭两侧流,可用 { preventClose, preventAbort, preventCancel } 选项调整)。
  • 浏览器间行为细节(ready Promise、策略)需测试。

9. 计时 API(Performance)

概念

高精度时间测量 API 用于性能度量:performance.now()performance.timeOrigin,以及 Timeline API(performance.markperformance.measuregetEntriesByType 等)。

9.1 High Resolution Time API

  • performance.now():返回以毫秒为单位的高精度时间(相对于 timeOrigin)。
  • performance.timeOrigin:表示纪元(通常是 Date.now() 的基准)用于把 now() 转为 UNIX 时间戳。

示例:

ini 复制代码
const t0 = performance.now();
// ... 执行耗时操作
const t1 = performance.now();
console.log('耗时 ms:', t1 - t0);
console.log('time origin(UNIX ms):', performance.timeOrigin);

9.2 Performance Timeline API

  • performance.mark('name'):打点。
  • performance.measure('mName', 'startMark', 'endMark'):测量两点时间差。
  • performance.getEntriesByType('measure') / getEntriesByName(...) 获取测量记录。
  • 还有 PerformanceResourceTiming(资源加载)、PerformanceNavigationTiming(导航),PerformancePaintTiming(首次绘制相关)等。

示例(User Timing):

arduino 复制代码
performance.mark('start');
// 做事
performance.mark('end');
performance.measure('my-op', 'start', 'end');
console.log(performance.getEntriesByType('measure'));

潜在问题

  • 记得在适当时机清理 mark/measure(performance.clearMarks() / clearMeasures())以免占用内存。
  • 不同浏览器可能在性能条目细节上略有差异。

10. Web 组件(增加 DOM 的工作 & Shadow DOM)

概念

Web Components 是一组技术:Custom Elements(自定义元素)、Shadow DOM(封装样式和 DOM)、HTML Templates(模板)等,用于构建可复用、封装的组件。

原理

  • Shadow DOM 提供 DOM/样式封装,避免样式冲突。
  • Custom Elements 提供组件生命周期和注册机制。
  • Template 提供可克隆内容。

实践与拓展将在下面 Shadow DOM 与 Custom Elements 章节详细展开(见 12 / 13)。


11. HTML 模板(<template>DocumentFragment

概念

<template> 标签在 DOM 中包含不可渲染的 HTML 片段,可作为复用模板克隆并插入文档。DocumentFragment 是轻量的"文档片段",适合批量 DOM 更新,避免多次回流。

原理

  • template.content 返回 DocumentFragment,其中的 <script> 不会在克隆时自动执行(因此可用于存放模板脚本或需要动态执行的脚本)。
  • document.createDocumentFragment() 用作临时容器,插入一次到 DOM 时只触发一次渲染,性能更好。

示例(DocumentFragment + template)

ini 复制代码
<template id="tpl">
  <div class="card">
    <h3 class="title"></h3>
    <p class="body"></p>
  </div>
</template>

<script>
const tpl = document.getElementById('tpl');
const frag = document.createDocumentFragment();

for (let i=0;i<3;i++) {
  // 克隆模板内容(深拷贝)
  const clone = tpl.content.cloneNode(true);
  clone.querySelector('.title').textContent = '标题 #' + (i+1);
  clone.querySelector('.body').textContent = '内容示例';
  frag.appendChild(clone); // 先放到片段中
}
document.body.appendChild(frag); // 一次性插入(只触发一次 DOM 更新)
</script>

模板脚本

模板内的 <script> 标签不会在克隆时执行;若需要执行脚本,可在插入后动态创建脚本节点并插入(或使用 type="application/javascript" 并手动 eval/insert)。

潜在问题

  • 小组件大量使用模板与克隆可带来复杂性(需合理管理事件绑定、内存与清理)。

12. 影子 DOM(Shadow DOM)

12.1 理解影子 DOM(概念)

Shadow DOM 将组件的 DOM 与样式封装在宿主元素内部,外部样式不会穿透进来,内部样式(除 ::slotted)也不会泄露到外界。事件会"重定向(retargeting)",即事件目标会在阴影边界上被重新表示,以保护封装。

12.2 创建影子 DOM(attachShadow

  • el.attachShadow({ mode: 'open' }):返回 shadowRoot,外部可通过 el.shadowRoot 访问。
  • mode: 'closed':外部 el.shadowRoot 返回 null(无法访问 shadow root)。

示例:

scala 复制代码
class MyBox extends HTMLElement {
  constructor() {
    super();
    // open 模式:外部可以通过 el.shadowRoot 访问
    const sr = this.attachShadow({mode: 'open'});
    sr.innerHTML = `<style>p{color: blue}</style><p>我是影子DOM</p>`;
  }
}
customElements.define('my-box', MyBox);

// 使用示例
document.body.appendChild(document.createElement('my-box'));

12.3 使用影子 DOM(多个示例组件)

一个演示多个具有不同 shadow DOM 风格的组件(注意:每个元素只能有一个 shadowRoot,所以使用三个不同元素展示三个不同颜色):

scala 复制代码
<my-box-red></my-box-red>
<my-box-green></my-box-green>
<my-box-blue></my-box-blue>

<script>
class BaseBox extends HTMLElement {
  constructor(color) {
    super();
    const sr = this.attachShadow({mode: 'open'});
    sr.innerHTML = `<style>p{color:${color};}</style><p>颜色:${color}</p>`;
  }
}

customElements.define('my-box-red', class extends BaseBox { constructor(){ super('red'); }});
customElements.define('my-box-green', class extends BaseBox { constructor(){ super('green'); }});
customElements.define('my-box-blue', class extends BaseBox { constructor(){ super('blue'); }});
</script>

12.4 合成与影子 DOM 粒位(composed / retargeting)

  • 当事件穿过 shadow boundary 时,event.target 在外部会显示为宿主元素(retargeting),但 event.composedPath() 可以查看完整路径。
  • composed: true 的自定义事件可穿过 shadow boundary 到达外部(new CustomEvent('x',{composed:true}))。

示例(事件重定向):

javascript 复制代码
const host = document.querySelector('my-box-red');
host.shadowRoot.querySelector('p').addEventListener('click', (e) => {
  console.log('shadow 内 target:', e.target);
  const ev = new CustomEvent('inner-click', { bubbles: true, composed: true });
  host.dispatchEvent(ev);
});
host.addEventListener('inner-click', () => console.log('外部捕获到 inner-click'));

12.5 事件重定向示例

上面已示范:composed: true 使自定义事件穿越影子边界;普通 DOM 事件如 click 默认也能冒泡出 shadow,但 event.target 在宿主外部会被 retarget。

潜在问题

  • Shadow DOM 会改变事件的 target 表现,调试时需使用 event.composedPath()
  • closed mode 会影响测试/调试与外部框架交互。
  • 选择器(如 :host, ::slotted)需熟悉以正确做样式封装。

13. 自定义元素(Custom Elements)

13.1 创建自定义元素

  • 通过 class MyEl extends HTMLElement { ... } 定义类,再用 customElements.define('my-el', MyEl) 注册。
  • 也可以创建"自定义内建元素" extends HTMLButtonElement 等,但需 is="..." 用法并且兼容性有限。

示例(基本):

scala 复制代码
class HelloEl extends HTMLElement {
  constructor() {
    super();
    // 通常只在 constructor 中做最少 DOM 初始化(不访问属性或子节点)
    this.attachShadow({mode:'open'});
    this.shadowRoot.innerHTML = `<p>Hello</p>`;
  }
}
customElements.define('hello-el', HelloEl);

13.2 添加 Web 组件内容(在 constructor 中 setup)

constructor 中创建 shadow DOM 和初始 DOM。复杂 DOM 与事件绑定可以在 connectedCallback 中进行(保证已插入文档)。

13.3 生命周期钩子

  • constructor():元素构造(不保证已连接文档)。
  • connectedCallback():元素被插入到 DOM。
  • disconnectedCallback():被移除时调用。
  • attributeChangedCallback(name, old, new):属性变化时调用(需要在 static get observedAttributes() 中声明要观察的属性)。
  • adoptedCallback():元素被移动到新的文档(如 iframe)。

示例(属性观察):

javascript 复制代码
class MyEl extends HTMLElement {
  static get observedAttributes() { return ['title']; }
  constructor() {
    super();
    this.attachShadow({mode:'open'});
  }
  connectedCallback() {
    this._render();
  }
  attributeChangedCallback(name, oldV, newV) {
    if (name === 'title') this._render();
  }
  _render() {
    this.shadowRoot.innerHTML = `<h3>${this.getAttribute('title') || '默认'}</h3>`;
  }
}
customElements.define('my-el', MyEl);

13.4 反向自定义属性(通过 getter/setter 代理)

  • 可以在类中定义 get/set 以反映属性到属性存储/属性到 DOM attribute 的同步。

示例(属性反射):

javascript 复制代码
class ReflectEl extends HTMLElement {
  get count() { return Number(this.getAttribute('count') || 0); }
  set count(v) { this.setAttribute('count', String(v)); }
  attributeChangedCallback(name) {
    if (name === 'count') this.render();
  }
  static get observedAttributes() { return ['count']; }
  render(){ this.textContent = `count=${this.count}`; }
}
customElements.define('reflect-el', ReflectEl);

13.5 升级自定义元素(customElementRegistry)

  • customElements.whenDefined(name) 返回一个 Promise,在定义完成时 resolve。
  • customElements.get(name) 返回已注册的构造函数(或 undefined)。
  • customElements.upgrade(rootOrElement) 可以把已存在的未升级元素升级为自定义元素(当您在文档中先插入了标签,后定义时有用)。

示例(whenDefined & upgrade):

scala 复制代码
// 假设 DOM 中有 <delayed-el></delayed-el>,但还未定义
customElements.whenDefined('delayed-el').then(() => {
  console.log('delayed-el 已定义');
});

// 手动定义并 upgrade
class DelayedEl extends HTMLElement { connectedCallback(){ this.textContent='ok'; } }
customElements.define('delayed-el', DelayedEl);
customElements.upgrade(document); // 把 document 下的未升级元素升级(谨慎使用)

潜在问题

  • 自定义元素一旦定义不可重复定义同名(customElements.define 会报错)。
  • extends(自定义内建元素)在部分浏览器不完全支持或行为差异。
  • 使用 closed shadowRoot 会影响外部访问与测试。
相关推荐
ruokkk1 分钟前
AI 编程真香!我用 Next.js + AI 助手,给孩子们做了个专属绘本网站
前端·后端·ai编程
兮漫天4 分钟前
bun + vite7 的结合,孕育的 Robot Admin 【靓仔出道】(二十)终章
前端·javascript·vue.js
失忆爆表症10 分钟前
基于 MediaPipe + Three.js 的实时姿态可视化前端
前端·webgl
乘风破浪酱5243611 分钟前
Bearer Token介绍
前端·后端
li理11 分钟前
鸿蒙应用开发深度解析:从基础列表到瀑布流,全面掌握界面布局艺术
前端·前端框架·harmonyos
coding随想38 分钟前
掌控右键宇宙!HTML5 contextmenu事件的终极使用指南,支持自定义右键菜单
前端
Ratten1 小时前
10.TypeScript tsconfig.json 配置文件详解
前端
Ratten1 小时前
09.TypeScript Class类
前端
skeletron20111 小时前
【antd】表单动态增减项
前端
用户游民1 小时前
Flutter Android端启动加载流程梳理
前端