阅读视图

发现新文章,点击刷新页面。

Promise.try () 完全指南

在 JavaScript 异步编程中,开发者常面临一个痛点:同步代码的错误无法被 Promise 的 .catch () 捕获,而 setTimeout/setInterval 等宏任务的错误更是 “逃逸” 到全局,难以统一处理。Promise.try () 作为解决这类问题的关键 API,本文将从作用、用法、兼容性、与其他 API 的对比等维度,全面解析其价值和使用场景。

一、核心问题:setTimeout/setInterval 的错误能捕获吗?

1. 直接捕获:几乎不可能

setTimeout/setInterval 的回调函数运行在新的宏任务执行栈中,脱离了原有的 Promise 链 /try-catch 作用域,因此:

js

// ❌ 无法捕获 setTimeout 内的错误
try {
  setTimeout(() => {
    throw new Error('定时器错误');
  }, 100);
} catch (err) {
  console.log('捕获到错误:', err); // 永远不会执行
}

// ❌ Promise.catch 也抓不到
Promise.resolve()
  .then(() => {
    setTimeout(() => {
      throw new Error('定时器错误');
    }, 100);
  })
  .catch(err => console.log('捕获到错误:', err)); // 同样无效

2. 间接处理:手动封装为 Promise

唯一能 “捕获” 定时器错误的方式,是在回调内主动处理,并封装为 Promise:

js

function delayTask(fn, ms) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      try {
        const result = fn(); // 执行任务
        resolve(result);
      } catch (err) {
        reject(err); // 手动捕获错误并 reject
      }
    }, ms);
  });
}

// 使用示例
delayTask(() => {
  throw new Error('定时器内的错误');
}, 100)
.then(res => console.log(res))
.catch(err => console.log('捕获到错误:', err)); // 生效!

3. 关键结论

  • setTimeout/setInterval 的错误无法被外层 try-catch/Promise.catch 直接捕获
  • 必须在回调内部用 try-catch 包裹逻辑,并手动 reject 才能纳入 Promise 链;
  • 而 Promise.try () 的核心价值,正是无需手动 try-catch,自动统一同步 / 异步错误(但对定时器这类宏任务仍需额外封装)。

二、Promise.try () 核心功能:统一同步 / 异步错误处理

1. 为什么需要 Promise.try ()?

日常开发中,一个函数可能混合同步逻辑和异步逻辑,同步错误会直接抛出(而非进入 Promise.catch):

js

// 问题代码:同步错误无法被 catch 捕获
function getUser(id) {
  if (!id) throw new Error('id 不能为空'); // 同步错误
  return fetch(`/api/user/${id}`); // 异步 Promise
}

// 使用时
getUser() // 直接抛出错误,不会进入 catch
  .then(res => res.json())
  .catch(err => console.log('错误:', err));

而 Promise.try () 能把同步代码 “包装” 成 Promise 链,让同步错误也能被 .catch () 捕获:

js

// 修复:用 Promise.try 包裹
function getUser(id) {
  return Promise.try(() => {
    if (!id) throw new Error('id 不能为空'); // 同步错误
    return fetch(`/api/user/${id}`); // 异步 Promise
  });
}

// 使用时
getUser()
  .then(res => res.json())
  .catch(err => console.log('错误:', err)); // 同步/异步错误都能捕获!

2. Promise.try () 的核心作用

表格

核心作用 具体说明
统一错误捕获 同步代码抛出的错误 → 自动转为 Promise.reject,可被 .catch () 捕获
简化代码 无需手动写 try-catch 包裹同步逻辑,代码更简洁
语义化启动 Promise 链 new Promise((resolve) => resolve(fn())) 更直观
兼容返回值类型 无论回调返回同步值、Promise、还是抛出错误,都统一为 Promise 实例

3. 核心特性:“穿透” 异步层级

即使回调内是多层异步逻辑,Promise.try () 也能保持错误捕获的一致性:

js

Promise.try(async () => {
  const userId = await getUserId(); // 异步获取 ID
  if (!userId) throw new Error('无用户 ID'); // 同步判断
  const user = await fetchUser(userId); // 异步请求
  return user;
})
.catch(err => console.log('所有错误都能捕获:', err));

三、Promise.try () 用法全解析

1. 基本语法

js

// 语法 1:基础用法
Promise.try(executor)
  .then(result => { /* 处理成功结果 */ })
  .catch(error => { /* 处理所有错误(同步+异步) */ });

// 语法 2:结合 async/await
Promise.try(async () => {
  // 混合同步/异步逻辑
  const data = await fetchData();
  if (data.length === 0) throw new Error('无数据');
  return data;
})
.catch(err => console.error(err));

2. 常见使用场景

场景 1:封装混合同步 / 异步的函数

js

// 封装工具函数:统一错误处理
function getCache(key) {
  return Promise.try(() => {
    // 同步:先查内存缓存
    const cacheData = localStorage.getItem(key);
    if (cacheData) return JSON.parse(cacheData); // 同步返回
    
    // 异步:缓存不存在则请求接口
    return fetch(`/api/cache/${key}`).then(res => res.json());
  });
}

// 使用:同步/异步错误都能 catch
getCache('user_123')
  .then(data => console.log('数据:', data))
  .catch(err => console.log('错误:', err));

场景 2:替代 try-catch + Promise 手动封装

js

// 传统写法(繁琐)
function doTask() {
  return new Promise((resolve, reject) => {
    try {
      const result = syncOperation(); // 同步操作
      resolve(result);
    } catch (err) {
      reject(err);
    }
  });
}

// Promise.try 写法(简洁)
function doTask() {
  return Promise.try(() => {
    return syncOperation(); // 自动处理同步错误
  });
}

场景 3:处理可能抛出错误的同步函数

js

// 同步函数可能抛错
function parseJSON(str) {
  return JSON.parse(str); // 无效 JSON 会同步抛错
}

// 用 Promise.try 包装,转为 Promise 错误
Promise.try(() => parseJSON('{invalid json}'))
  .catch(err => console.log('JSON 解析错误:', err)); // 生效

3. 与其他类似写法的对比

表格

写法 能否捕获同步错误 代码简洁度 语义化
Promise.try(fn) ✅ 能 ✅ 极简 ✅ 高(明确启动 Promise 链)
new Promise(resolve => resolve(fn())) ❌ 不能(同步错误直接抛出) ❌ 繁琐 ❌ 低
(async () => fn())() ✅ 能 ✅ 简洁 ❌ 语义不明确
Promise.resolve().then(fn) ❌ 不能(同步错误直接抛出) ✅ 简洁 ❌ 低

结论:Promise.try () 是唯一兼顾 “简洁 + 语义化 + 同步错误捕获” 的方案。

四、Promise.try () 兼容性与替代方案

1. 原生兼容性

  • 原生支持:Promise.try() 并非 ES 标准 API,是 Bluebird.js(第三方 Promise 库)率先实现的特性,Node.js/ 浏览器原生 Promise 未内置;

  • 环境支持

    • 直接使用:需引入 Bluebird.js、Q 等第三方 Promise 库;
    • 原生替代:可手动实现 polyfill。

2. 手动实现 Promise.try ()(兼容所有环境)

如果不想引入第三方库,可自己封装一个极简版:

js

// 兼容所有环境的 Promise.try 实现
if (!Promise.try) {
  Promise.try = function (executor) {
    return new Promise((resolve, reject) => {
      try {
        // 执行回调,获取返回值
        const result = executor();
        // 如果返回的是 Promise,直接 resolve;否则包装为 Promise
        resolve(result);
      } catch (err) {
        // 同步错误直接 reject
        reject(err);
      }
    });
  };
}

// 测试:完全兼容原生用法
Promise.try(() => {
  throw new Error('同步错误');
})
.catch(err => console.log('捕获到:', err)); // 生效

3. 用 async/await 替代(ES2017+)

ES2017 后的 async/await 也能实现类似效果,本质是语法糖:

js

// 等价于 Promise.try 的 async/await 写法
async function wrapFn(fn) {
  try {
    return await fn(); // await 会处理同步值/Promise
  } catch (err) {
    return Promise.reject(err);
  }
}

// 使用
wrapFn(() => {
  throw new Error('同步错误');
})
.catch(err => console.log('捕获到:', err));

注意:async 函数本身返回 Promise,因此 await fn() 会自动将同步值转为 resolved Promise,同步错误会被 try-catch 捕获。

五、避坑指南:Promise.try () 的常见误区

误区 1:认为能捕获 setTimeout 等宏任务错误

js

// ❌ 错误认知:Promise.try 无法直接捕获定时器错误
Promise.try(() => {
  setTimeout(() => {
    throw new Error('定时器错误');
  }, 100);
})
.catch(err => console.log('捕获到:', err)); // 无效

原因:setTimeout 回调是新的宏任务,脱离了当前 Promise 链的执行栈,必须在回调内手动 try-catch + reject。

误区 2:忽略回调返回非 Promise 的情况

js

// ✅ 正确:Promise.try 会自动包装同步返回值为 Promise
const res = Promise.try(() => 123);
console.log(res instanceof Promise); // true
res.then(num => console.log(num)); // 123

误区 3:与 Promise.resolve () 混淆

js

// ❌ Promise.resolve 无法捕获同步错误
Promise.resolve(() => {
  throw new Error('同步错误');
})
.catch(err => console.log('捕获到:', err)); // 无效

// ✅ Promise.try 能捕获
Promise.try(() => {
  throw new Error('同步错误');
})
.catch(err => console.log('捕获到:', err)); // 生效

核心区别:Promise.resolve () 只是包装 “值” 为 Promise,不会执行回调;而 Promise.try () 会立即执行回调,并捕获执行过程中的错误。

六、总结

核心要点

  1. setTimeout/setInterval 错误:无法被外层 try-catch/Promise.catch 直接捕获,需在回调内手动 try-catch + 封装为 Promise;
  2. Promise.try () 核心价值:统一同步 / 异步错误捕获,让同步代码的错误也能进入 Promise.catch,无需手动写 try-catch;
  3. 兼容性:非原生 ES 标准,需引入 Bluebird 或手动实现 polyfill,也可通过 async/await 实现等价效果;
  4. 关键误区:Promise.try () 无法捕获宏任务(如定时器)的错误,仅能处理当前执行栈内的同步 / 微任务错误。

最佳实践

  • 封装混合同步 / 异步逻辑的函数时,优先使用 Promise.try () 统一错误处理;
  • 处理定时器 / 事件回调等宏任务时,需在回调内手动 try-catch,并封装为 Promise;
  • 无第三方库时,用 async/await + try-catch 作为 Promise.try () 的替代方案。

Promise.try () 虽非原生标准,但它解决了异步编程中 “同步错误逃逸” 的核心痛点,是编写健壮、统一的异步代码的重要工具。

WebTransport 核心用法及身份验证和应用

WebTransport 核心用法与注意事项

WebTransport 是浏览器提供的新一代网络传输 API,基于 HTTP/3 协议,支持双向、低延迟的多路复用通信,可替代传统的 WebSocket 或 XHR,特别适用于实时音视频、游戏、低延迟数据交互等场景。


一、核心用法

1. 基础连接建立

WebTransport 连接基于 HTTP/3 协议,需服务端支持 HTTP/3(如 Nginx、Caddy 或自定义服务器),客户端通过 URL 建立连接:

javascript

运行

// 1. 建立 WebTransport 连接(URL 需使用 https 或 w3t 协议)
async function connect() {
  try {
    // 服务端地址(需配置 HTTP/3 证书)
    const transport = new WebTransport('https://your-server.com:4433/webtransport');
    
    // 等待连接就绪
    await transport.ready;
    console.log('WebTransport 连接成功');

    // 监听连接关闭事件
    transport.closed.then(() => {
      console.log('连接已关闭');
    });

    return transport;
  } catch (error) {
    console.error('连接失败:', error);
  }
}

2. 双向通信方式

WebTransport 支持两种核心通信模式:

(1)双向流(Bidirectional Streams)

类似 TCP 流,支持客户端 / 服务端双向读写,适合连续数据传输(如实时音频):

javascript

运行

async function createBidirectionalStream(transport) {
  // 创建双向流
  const stream = await transport.createBidirectionalStream();
  
  // 写入数据到服务端
  const writer = stream.writable.getWriter();
  const encoder = new TextEncoder();
  await writer.write(encoder.encode('Hello Server!'));
  writer.releaseLock(); // 释放写入锁

  // 读取服务端返回的数据
  const reader = stream.readable.getReader();
  const decoder = new TextDecoder();
  const { value, done } = await reader.read();
  if (!done) {
    console.log('服务端返回:', decoder.decode(value));
  }
}

(2)单向流(Unidirectional Streams)

仅客户端→服务端或服务端→客户端的单向传输,适合批量数据推送:

javascript

运行

async function createUnidirectionalStream(transport) {
  // 客户端向服务端发送单向流
  const stream = await transport.createUnidirectionalStream();
  const writer = stream.getWriter();
  await writer.write(new Uint8Array([1, 2, 3, 4]));
  await writer.close(); // 关闭流
}

// 监听服务端主动推送的单向流
function listenServerUnidirectionalStream(transport) {
  (async () => {
    const reader = transport.incomingUnidirectionalStreams.getReader();
    while (true) {
      const { value: stream, done } = await reader.read();
      if (done) break;
      // 读取服务端推送的数据
      const dataReader = stream.getReader();
      const { value } = await dataReader.read();
      console.log('服务端推送:', value);
    }
  })();
}

(3)数据报(Datagrams)

基于 UDP 的无连接、不可靠传输,适合低延迟、允许少量丢包的场景(如游戏同步):

javascript

运行

async function useDatagrams(transport) {
  // 检查数据报是否可用
  if (!transport.datagrams) {
    console.error('数据报功能不可用');
    return;
  }

  // 发送数据报
  const writer = transport.datagrams.writable.getWriter();
  const encoder = new TextEncoder();
  await writer.write(encoder.encode('UDP 数据'));

  // 接收数据报
  const reader = transport.datagrams.readable.getReader();
  const decoder = new TextDecoder();
  while (true) {
    const { value, done } = await reader.read();
    if (done) break;
    console.log('收到数据报:', decoder.decode(value));
  }
}

3. 连接关闭与错误处理

javascript

运行

async function handleTransportEvents(transport) {
  // 监听连接错误
  transport.addEventListener('error', (error) => {
    console.error('连接错误:', error);
  });

  // 主动关闭连接
  async function closeTransport() {
    await transport.close({
      code: 0, // 关闭码(自定义)
      reason: '客户端主动关闭' // 关闭原因
    });
  }
}

二、关键注意事项

1. 环境与兼容性

  • 浏览器支持:仅现代浏览器支持(Chrome 97+、Edge 97+、Firefox 114+),Safari 暂未完全支持;
  • 协议要求:必须基于 HTTP/3 协议,服务端需配置 HTTP/3 证书(HTTPS 强制),本地测试需使用 localhost 或合法证书;
  • 端口配置:服务端需开放 HTTP/3 端口(通常为 443,或自定义端口如 4433),且防火墙需允许 UDP 流量(HTTP/3 基于 UDP)。

2. 安全性限制

  • 同源策略:默认遵循同源策略,跨域需服务端配置 CORS 头(如 Access-Control-Allow-Origin: *);
  • 证书要求:必须使用合法的 TLS 证书(自签名证书仅本地测试可用,生产环境需信任证书);
  • 权限限制:仅在安全上下文(HTTPS/localhost)中可用,HTTP 页面无法使用。

3. 传输特性与可靠性

  • 流的可靠性:双向 / 单向流基于 HTTP/3 的流机制,是可靠、有序的(类似 TCP);
  • 数据报的不可靠性:Datagrams 基于 UDP,无可靠性、无顺序保证,需业务层自行处理丢包、重传;
  • 多路复用:单个 WebTransport 连接可创建多个流 / 数据报,无需建立多个连接,但需注意流的并发数限制(服务端通常有默认阈值)。

4. 服务端适配

  • 服务端需实现 HTTP/3 + WebTransport 协议(如使用 Node.js 的 quiche、Go 的 quic-go、Nginx 1.25+ 等);
  • 避免过度创建流:单个连接的流数量过多可能导致性能下降,建议合理复用流;
  • 处理连接超时:服务端需配置连接超时机制,清理闲置连接。

5. 错误处理与降级

  • 需兼容低版本浏览器:可检测 WebTransport 是否存在,降级使用 WebSocket 或 Fetch API;
  • 监听连接状态:通过 transport.readytransport.closederror 事件处理断连重连逻辑;
  • 数据编码:传输二进制数据时建议使用 Uint8Array,文本数据使用 TextEncoder/TextDecoder 避免编码问题。

WebTransport 身份验证方案与典型应用场景

WebTransport 本身未内置身份验证机制,但可结合 HTTP/3 协议特性、请求头、令牌(Token)等方式实现身份验证,核心思路是在建立连接或传输数据前完成身份校验,确保通信安全。以下是具体实现方案、代码示例及典型应用场景。


三、WebTransport 身份验证的核心实现方式

身份验证需结合「连接建立阶段」和「数据传输阶段」,优先在连接初始化时完成校验,避免无效连接占用资源。

1. 方式 1:URL 携带令牌(Token)(最简单)

在建立 WebTransport 连接时,通过 URL 参数携带身份令牌(如 JWT),服务端解析 URL 并校验令牌合法性。

客户端实现:

javascript

运行

async function connectWithToken() {
  // 从本地存储获取预生成的 JWT 令牌(如登录后返回的 Token)
  const authToken = localStorage.getItem('user_token');
  if (!authToken) {
    throw new Error('未获取到身份令牌');
  }

  // 拼接 Token 到 URL 参数中
  const transportUrl = `https://your-server.com:4433/webtransport?token=${encodeURIComponent(authToken)}`;
  
  try {
    const transport = new WebTransport(transportUrl);
    await transport.ready;
    console.log('身份验证通过,连接成功');
    return transport;
  } catch (error) {
    // 服务端校验失败会返回连接错误(如 401)
    console.error('身份验证失败:', error);
    // 可触发重新登录逻辑
  }
}

服务端校验逻辑(以 Node.js + quic-go 为例):

服务端解析 URL 中的 token 参数,验证签名 / 有效期,若无效则拒绝建立 HTTP/3 连接(返回 401 状态码)。

2. 方式 2:HTTP/3 请求头携带凭证(推荐)

WebTransport 连接建立时会先发送 HTTP/3 握手请求,可通过自定义请求头携带身份凭证(如 Authorization 头),更安全且符合 HTTP 规范。

客户端实现:

javascript

运行

async function connectWithHeader() {
  const authToken = localStorage.getItem('user_token');
  // 构造请求头(需服务端允许跨域携带该头)
  const transport = new WebTransport('https://your-server.com:4433/webtransport', {
    headers: {
      'Authorization': `Bearer ${authToken}`, // JWT 标准格式
      'X-User-ID': '123456' // 自定义业务头
    }
  });

  try {
    await transport.ready;
    console.log('连接并身份验证成功');
    return transport;
  } catch (error) {
    console.error('身份验证失败:', error);
  }
}

关键注意:

服务端需配置 CORS 允许自定义头,如返回 Access-Control-Allow-Headers: Authorization, X-User-ID,否则浏览器会拦截请求。

3. 方式 3:连接后握手验证(补充校验)

若需更复杂的验证(如双向认证),可在连接建立后,通过双向流发送身份凭证,服务端校验后返回结果,校验失败则主动关闭连接。

客户端实现:

javascript

运行

async function handshakeAfterConnect(transport) {
  // 连接建立后,创建双向流发送身份信息
  const stream = await transport.createBidirectionalStream();
  const writer = stream.writable.getWriter();
  const encoder = new TextEncoder();

  // 发送身份凭证(如加密后的用户名+密码/Token)
  const authData = encoder.encode(JSON.stringify({
    token: localStorage.getItem('user_token'),
    timestamp: Date.now() // 防重放攻击
  }));
  await writer.write(authData);
  await writer.close();

  // 读取服务端校验结果
  const reader = stream.readable.getReader();
  const decoder = new TextDecoder();
  const { value, done } = await reader.read();
  if (done) throw new Error('验证流被关闭');

  const result = JSON.parse(decoder.decode(value));
  if (!result.success) {
    // 校验失败,关闭连接
    await transport.close({ reason: '身份验证失败' });
    throw new Error(result.message);
  }
  console.log('握手验证成功,可正常通信');
}

4. 安全增强:Token 过期与重连

javascript

运行

async function connectWithReAuth() {
  let transport;
  while (true) {
    try {
      transport = await connectWithHeader(); // 尝试连接
      // 监听连接错误(如 Token 过期)
      transport.addEventListener('error', async (err) => {
        if (err.message.includes('401')) {
          // Token 过期,重新获取 Token
          const newToken = await refreshToken(); // 调用登录接口刷新 Token
          localStorage.setItem('user_token', newToken);
          // 关闭旧连接,重新连接
          await transport.close();
          transport = await connectWithHeader();
        }
      });
      break; // 连接成功,退出循环
    } catch (err) {
      console.error('重连失败,5秒后重试');
      await new Promise(resolve => setTimeout(resolve, 5000));
    }
  }
  return transport;
}

// 刷新 Token 的辅助函数
async function refreshToken() {
  const res = await fetch('https://your-server.com/refresh-token', {
    method: 'POST',
    credentials: 'include' // 携带 cookie(若用 cookie 存储 refreshToken)
  });
  const data = await res.json();
  return data.token;
}

四、WebTransport 典型应用场景

WebTransport 结合 HTTP/3 的「低延迟、多路复用、双向通信」特性,适用于传统 WebSocket 或 XHR 无法满足的场景:

1. 实时音视频互动(核心场景)

  • 场景:视频会议、直播连麦、在线 K 歌、实时监控;

  • 优势

    • 基于 HTTP/3 的多路复用,可同时传输音频流、视频流、控制指令(如静音 / 画质调整),无需建立多个连接;
    • 数据报(Datagrams)可传输低延迟音频帧(允许少量丢包),流传输保证视频帧的有序性;
    • 相比 WebSocket,HTTP/3 拥塞控制更优,弱网下延迟更低。

2. 实时游戏(电竞 / 云游戏)

  • 场景:多人在线竞技游戏、云游戏画面传输、游戏状态同步;

  • 优势

    • 数据报(UDP 基础)支持 10-20ms 级低延迟,适合游戏角色位置、操作指令同步(允许少量丢包);
    • 双向流可传输可靠的游戏配置、玩家信息,多路复用避免连接数限制;
    • HTTP/3 穿透 NAT 能力更强,相比传统 UDP 游戏通信更稳定。

3. 低延迟物联网(IoT)数据交互

  • 场景:智能家居实时控制、工业设备数据采集、无人机远程操控;

  • 优势

    • 支持双向通信,设备可主动推送实时数据(如传感器数值),客户端可下发控制指令;
    • 数据报适合低功耗设备的轻量数据传输,流传输保证固件升级等可靠数据的完整性;
    • 基于 HTTPS 安全上下文,避免物联网设备被非法接入。

4. 大文件分片传输(断点续传)

  • 场景:大文件上传(如视频、工程文件)、断点续传、云盘同步;

  • 优势

    • 多路单向流可将文件分片并行传输,利用 HTTP/3 多路复用提升传输速度;
    • 相比 Fetch API,支持服务端实时反馈分片传输状态,客户端可动态调整分片大小;
    • 连接中断后可快速重连,基于已传输的分片续传,无需重新开始。

5. 金融实时行情推送

  • 场景:股票 / 期货行情实时更新、交易指令下发;

  • 优势

    • 流传输保证行情数据的有序性和可靠性,避免价格数据错乱;
    • 低延迟特性可将行情推送延迟降至毫秒级,满足金融交易的时效性要求;
    • 支持批量行情数据复用单个连接传输,降低服务端连接压力。

总结

  1. 身份验证核心:WebTransport 需通过「URL 参数、HTTP/3 请求头、连接后握手」实现身份验证,优先选择请求头方式(更安全),并做好 Token 过期重连、错误处理;
  2. 关键注意:验证需在安全上下文(HTTPS)中进行,服务端需配置 CORS 允许自定义头,且需校验 Token 合法性(防伪造 / 重放);
  3. 核心场景:实时音视频、游戏、IoT 控制、大文件传输、金融行情推送,核心优势是低延迟、多路复用、兼顾可靠 / 不可靠传输。
  4. 核心能力:WebTransport 基于 HTTP/3 提供双向流(可靠)、单向流、数据报(低延迟)三种通信方式,适配不同实时性需求;
  5. 环境要求:需 HTTP/3 服务端、HTTPS 安全上下文、现代浏览器,且开放 UDP 端口;
  6. 关键注意:流传输可靠但延迟稍高,数据报低延迟但不可靠,需根据业务场景选择,同时做好兼容性和错误处理。
❌