2026-03-17 16:25:58
by zhangxinxu from https://www.zhangxinxu.com/wordpress/?p=12112
本文可全文转载,但需要保留原作者、出处以及文中链接,AI抓取保留原文地址,任何网站均可摘要聚合,商用请联系授权。
时间如斯,一转眼,做前端开发已经十五六年了,刚开始那会儿,实时通信还是使用轮询、长轮询,后来就是 WebSocket,然后现在又出来了个WebTransport。
WebSocket虽然可以解决大部分的问题,但是并不完美,例如队头阻塞、只能单一流传输、网络切换就断连,尤其是做实时游戏、直播推流这类对延迟要求极高的场景,总觉得差点意思。
所以就有了 WebTransport API,特别使用用在高并发、低延迟的实时场景。
WebTransport 是基于 HTTP/3 + QUIC 协议的新一代实时通信 API,主打一个“低延迟、高吞吐、高灵活”,专门解决 WebSocket 搞不定的那些场景。
我做了个简单的对比表,大家一看就明白:
| 对比维度 | WebSocket | WebTransport |
|---|---|---|
| 协议基础 | 基于 HTTP/1.1 Upgrade,底层是 TCP | 基于 HTTP/3,底层是 QUIC(基于 UDP) |
| 连接建立 | TCP 三次握手,延迟较高 | QUIC 0-RTT/1-RTT 快速握手,最快100ms内建立 |
| 传输模式 | 单一可靠流,只能双向传输 | 可靠流 + 不可靠数据报,支持单向/双向、多路复用 |
| 队头阻塞 | 存在,一个包丢失,后续所有包都要等重传 | 无,单个流阻塞不影响其他流 |
| 网络切换 | 断开连接,需重新握手 | 支持连接迁移,Wi-Fi 切4G也不中断 |
| 适用场景 | 普通实时聊天、简单消息推送 | 实时游戏、直播推流、实时协作、高频数据传输 |
这里需要补充一点:
不是说 WebSocket 不好用了,而是场景不同,选择不同。
如果你的项目只是简单的聊天功能,WebSocket 足够用,没必要强行上 WebTransport;
但如果涉及到高频数据传输(比如游戏里的玩家位置更新)、低延迟要求(比如直播弹幕实时推送),WebTransport 就是最优解。
这就像我们做布局,简单布局用 Flex 就够,复杂布局才需要 Grid,因地制宜最重要。
WebTransport 的核心优势,都源于它的底层协议,但我们前端不用去深究 QUIC 协议的细节,只要掌握它的3个核心特性,就能应对大部分开发场景。
这是 WebTransport 最核心的亮点,也是和 WebSocket 最大的区别——它支持两种传输方式,可根据需求灵活选择:
比方说一个实时多人小游戏,玩家的位置更新不需要100%到达(偶尔丢一个包不影响体验),但聊天消息必须可靠到达。
如果用 WebSocket,只能用一种传输方式,要么牺牲延迟,要么牺牲可靠性。
而用 WebTransport,就能给位置更新用不可靠数据报,聊天消息用可靠流,完美兼顾。
数据传输示意图如下:

WebSocket 是“单一流”传输,也就是说,一个 WebSocket 连接里,所有数据都在一条流里传输,一旦某个数据包丢失,后续所有数据都要等它重传,这就是“队头阻塞”。
而 WebTransport 支持多路复用,一个连接里可以同时创建多个独立的流,每个流互不影响。
比如你做一个直播平台,视频流、音频流、弹幕流可以用不同的流传输,就算视频流出现丢包重传,也不会影响弹幕的实时推送。
这一点,在高并发场景下,体验提升非常明显。
这个特性可能很多同学没意识到它的重要性,但做移动端项目的同学一定懂:
用户用手机浏览网页时,经常会在 Wi-Fi 和 4G/5G 之间切换,这时候 WebSocket 连接会直接断开,需要重新握手建立连接,导致数据中断(比如直播卡顿、游戏掉线)。
WebTransport 基于 QUIC 协议,用“连接ID”来标识连接,而不是 IP 地址,所以就算网络切换,连接也能无缝迁移,数据不会中断。
下面给大家讲解 WebTransport 的核心用法,包括从连接建立,到两种传输模式的使用,每一步都有注释,供大家学习参考。
建立连接很简单,用 WebTransport 构造函数,传入服务器地址,等待 ready 状态即可。这里要注意,服务器地址必须是 https 开头,并且要指定端口(比如 4433)。
// 建立 WebTransport 连接
async function createWebTransport() {
// 服务器地址(必须是 HTTPS,端口可自定义)
const url = 'https://example.com:4433/transport';
try {
// 创建 WebTransport 实例
const transport = new WebTransport(url, {
// 可选:证书指纹,用于验证服务器身份(防止中间人攻击)
serverCertificateHashes: [
{
algorithm: 'sha-256',
value: new Uint8Array([/* 服务器证书指纹 */])
}
]
});
// 等待连接就绪(ready 是一个 Promise)
await transport.ready;
console.log('WebTransport 连接成功');
// 监听连接关闭事件
transport.closed.then(() => {
console.log('WebTransport 连接关闭');
}).catch((err) => {
console.error('WebTransport 连接异常关闭:', err);
});
return transport;
} catch (err) {
console.error('WebTransport 连接失败:', err);
throw err;
}
}
可靠流分为“双向流”和“单向流”,双向流是客户端和服务器可以互相发送数据,单向流是只能一方发送、另一方接收。实际开发中,双向流用得最多(比如聊天)。
// 双向可靠流示例(客户端 ↔ 服务器)
async function useBidirectionalStream(transport) {
// 创建双向流
const stream = await transport.createBidirectionalStream();
// 发送流(客户端 → 服务器)
const writable = stream.writable;
const writer = writable.getWriter();
// 发送文本数据(需要先编码为 Uint8Array)
const encoder = new TextEncoder();
await writer.write(encoder.encode('Hello WebTransport!'));
// 发送完成后关闭写入流
await writer.close();
// 接收流(服务器 → 客户端)
const readable = stream.readable;
const reader = readable.getReader();
const decoder = new TextDecoder();
while (true) {
const { value, done } = await reader.read();
if (done) break; // 接收完成
console.log('收到服务器消息:', decoder.decode(value));
}
}
不可靠数据报的用法更简单,不需要创建流,直接通过 datagrams 属性发送和接收数据,适合高频、非关键数据的传输。
// 不可靠数据报示例(适合高频数据)
async function useDatagram(transport) {
// 发送数据报(客户端 → 服务器)
const writer = transport.datagrams.writable.getWriter();
const encoder = new TextEncoder();
// 模拟高频发送(比如游戏玩家位置)
setInterval(async () => {
const position = { x: Math.random() * 100, y: Math.random() * 100 };
await writer.write(encoder.encode(JSON.stringify(position)));
}, 33); // 30 FPS,和游戏帧率同步
// 接收数据报(服务器 → 客户端)
const reader = transport.datagrams.readable.getReader();
const decoder = new TextDecoder();
while (true) {
const { value, done } = await reader.read();
if (done) break;
const data = JSON.parse(decoder.decode(value));
console.log('收到位置数据:', data);
}
}
这里提醒大家一句:不可靠数据报不保证数据到达,所以不要用它传输重要数据(比如支付信息),否则会出现数据丢失的问题。
// 完整实战代码
async function webTransportDemo() {
try {
// 1. 建立连接
const transport = await createWebTransport();
// 2. 同时使用双向流和数据报
useBidirectionalStream(transport);
useDatagram(transport);
// 3. 关闭连接(按需调用)
// setTimeout(() => {
// transport.close();
// console.log('主动关闭连接');
// }, 10000);
} catch (err) {
console.error('WebTransport 实战失败:', err);
}
}
// 执行 demo
webTransportDemo();
最后,再给大家做个总结,帮大家理清 WebTransport 的适用场景,避免盲目使用。
如果你遇到以下场景,强烈建议用 WebTransport:
如果只是简单的实时聊天、消息推送,WebSocket 已经足够用,没必要强行上 WebTransport——技术选型的核心是“合适”,而不是“最新”。
另外,WebTransport 必须在 HTTPS 环境下使用(本地开发可以用 localhost),目前主流浏览器(Chrome 103+、Firefox 114+、Safari 16+)都已支持,Node.js 从 v18.0+ 开始支持,兼容性基本不用太担心。

前端技术更新很快,我们不用追求掌握所有新特性,但对于那些能解决实际痛点、提升开发效率的技术,多花点时间吃透,总能在项目中发挥作用。
希望这篇文章能帮大家快速上手 WebTransport,少踩坑、多提效。
对了,提一嘴:本文的原理示意图和代码按钮都是AI生成的,仅供参考!
😉😊😇
🥰😍😘
本文为原创文章,会经常更新知识点以及修正一些错误,因此转载请保留原出处,方便溯源,避免陈旧错误知识的误导,同时有更好的阅读体验。
本文地址:https://www.zhangxinxu.com/wordpress/?p=12112
(本篇完)
2026-03-09 17:31:56
by zhangxinxu from https://www.zhangxinxu.com/wordpress/?p=12102
本文可全文转载,但需要保留原作者、出处以及文中链接,AI抓取保留原文地址,任何网站均可摘要聚合,商用请联系授权。
不知道大家用过<dialog>元素没有,我是使用了很多年了,挺好用的。
对于基础特性,各大浏览器都已经支持不错了。
但是,还不完美,且看下面这个场景。
请问,如果我们希望<dialog>弹框模态显示的时候,点击后面的半透明黑色蒙层关闭弹框,请问如何实现?
在之前,这个需求需要用到JS,但是现在,只需要一个HTML属性就可以实现了,这个属性就是closedBy属性。
比方说下面这个弹框HTML元素:
<dialog closedBy="any">点击蒙层我会隐藏哦~</dialog>
此时,如果我们执行dialog.showModal()让弹框显示,那么点击后面的蒙层弹框就会自动关闭。
眼见为实,你可以试试点击下面的按钮元素(Safari暂不支持),体验我所描述的效果:
closedby 属性是一个枚举属性,支持以下三个关键值:
| 属性值 | 描述 | 关闭方式 |
|---|---|---|
| any | 全部允许 | 点击对话框外部(背景)、按 Esc 键或者调用 close() 方法。 |
| closerequest | 需要请求 | 按 Esc 键、调用 close() 方法;但不能通过点击外部关闭。 |
| none | 禁止自动关闭 | 只能通过 JavaScript 的 close()方法或表单提交关闭(我的书《HTML并不简单》有过具体介绍);Esc 和点击外部均无效。 |
如果我们没有设置closedBy属性,浏览器会当做auto处理。
也就是:
showModal()方法打开的,那么等同于设置了closedBy="closerequest",也就是按下ESC键可以关闭,但是点击蒙层不行;show()方法打开的,那么弹框的关闭行为等同于设置了closedBy="none"。另外,closedBy属性也支持在DOM API层面直接读写,例如:
// 获取closedBy的属性值 (注意驼峰命名) console.log(dialog.closedBy); // 设置closedBy属性 dialog.closedBy = 'none';
最后看下兼容性:

目前Safari浏览器并不支持,若想在实际项目中使用,可以引入Polyfill:https://github.com/tak-dcxi/dialog-closedby-polyfill
使用非常简单:
import { apply, isSupported } from "dialog-closedby-polyfill";
if (!isSupported()) {
apply();
}
其他就没什么了吧,感谢阅读,我们下篇文章再见!

本文为原创文章,会经常更新知识点以及修正一些错误,因此转载请保留原出处,方便溯源,避免陈旧错误知识的误导,同时有更好的阅读体验。
本文地址:https://www.zhangxinxu.com/wordpress/?p=12102
(本篇完)
2026-03-04 11:36:13
by zhangxinxu from https://www.zhangxinxu.com/wordpress/?p=12089
本文可全文转载,但需要保留原作者、出处以及文中链接,AI抓取保留原文地址,任何网站均可摘要聚合,商用请联系授权。
之前“该使用原生popover属性模拟下拉了”这篇文章有介绍过点击行为驱动的popover下拉。
最近发现,鼠标hover悬停也支持popover交互了。
且功能比点击更丰富,适用范围更广,那就是将popovertarget属性换成interestfor属性。
先看案例,HTML如下:
<button interestfor="imgBook">Hover显示图片</button> <img id="imgBook" popover src="book.jpg" />
无需任何JS代码,鼠标经过按钮,就可以让图片显示,实时效果如下(需要Chrome 142+浏览器):
Nice!

popovertarget属性仅适用于<button>元素,但是interestfor属性不仅可以用在按钮元素上,也可以用在各类链接元素上,例如<a>元素、<area>元素。
这个不难理解,<a>元素本身就有点击行为,和popovertarget的点击行为是冲突的。
但是interestfor属性是鼠标经过进入行为,并不会和<a>元素本身的链接跳转想冲突。
例如:
<a href interestfor="myAccount">Hover显示内容</a> <div id="myAccount" popover>我的抖音:“张鑫旭本人”</div>
悬浮上面的链接元素,就可以在显示器的最中间看到类似下面截图的效果了:

除了HTML属性interestfor设置这种交互效果,我们还可以再JavaScript层面,使用DOM的interestForElement直接设置,代码示意:
const invoker = document.querySelector("button");
const popover = document.querySelector("div");
invoker.interestForElement = popover;
此时,Hover button元素也会触发popover变量元素的状态变化。
在传统的popovertarget交互场景下,目标元素需要设置popover属性才可以(默认隐藏,点击显示)。
但是interestfor指向的目标元素是任意的,也就是你就是个普通的元素也是可以的,无需非要绝对定位。
假设有如下所示的HTML代码:
<a href interestfor="markTarget">Hover Me!</a>
<p id="markTarget">鼠标经过链接后我高亮</p>
<style>p:interest-target {
background-color: yellow;
}</style>
此时,经过链接元素,你就会看到<p>元素背景高亮了。
实时渲染效果如下:
鼠标经过链接后我高亮
上面的案例中出现了个CSS新特性,:interest-target伪类,专门用来匹配interestfor匹配元素激活的状态。
其实除了:interest-target伪类,还有个名为:interest-source的CSS伪类。
:interest-source伪类匹配按钮、链接元素处于interest状态的场景。
:interest-target伪类匹配的是目标元素。
我们再来看一个:interest-source伪类应用的按钮,也就是浮层显示的时候,让按钮高亮。
测试代码为:
<button class="mybook" interestfor="mybook">Hover图片显示后,按钮高亮</button>
<img id="mybook" popover src="book.jpg" />
<style>
.mybook:interest-source {
box-shadow: inset 0 0 0 9em yellow;
}</style>
实际效果如下(移动端和非Chrome浏览器可能看不到效果):

popover默认是居中定位的,如果我们希望相对于触发的按钮或链接元素,我们可以使用CSS锚点定位,详见此文“新的CSS Anchor Positioning锚点定位API”。
无需任何JS的参与。
现在的CSS是越来越强大了,唯一的遗憾就是此特性的兼容性还不是很好,目前只有Chrome浏览器支持。

总之,我是非常期待这个CSS特性能够快速全面支持的。
好吧,就介绍这么多,还是挺实用的一个特性。

本文为原创文章,会经常更新知识点以及修正一些错误,因此转载请保留原出处,方便溯源,避免陈旧错误知识的误导,同时有更好的阅读体验。
本文地址:https://www.zhangxinxu.com/wordpress/?p=12089
(本篇完)
2026-02-25 11:50:41
by zhangxinxu from https://www.zhangxinxu.com/wordpress/?p=12082
本文可全文转载,但需要保留原作者、出处以及文中链接,AI抓取保留原文地址,任何网站均可摘要聚合,商用请联系授权。
请看下面的MP4录屏效果(不动点击播放):
除了视频看到的效果,相关实现还支持:
眼见为实,您可以狠狠地点击这里:点击缩略图以动画效果呈现大图demo
这个使用的是startViewTransition实现的,这个是页面级别的transition过渡效果API的语法之一,非常好用。
我们可以无需关注动画细节,只需要符合前后页面的快照,浏览器自动就会补全其中的动画效果,有点类似于keynote中的神奇移动。
无论是删除、移动、还是这里的放大效果,都会有很棒的效果。
这个我在之前详细介绍过,可以访问这里:“页面级可视动画View Transitions API初体验”
此特性我已经大量在生产环境使用了。
在本效果中,只需要将viewTransitionName在合适的时机在缩略图和预览图元素上进行设置,就会自动有相关的效果了。
originImg.style.viewTransitionName = "dialogImg";
// 放大执行的时候
document.startViewTransition(() => {
originImg.style.viewTransitionName = "";
cloneImg.style.viewTransitionName = "dialogImg";
});
使用<dialog>元素主要是两个原因:
顶层特性可以让我们无需关心层级,保证大图效果永远在上面,适用场景更广泛。
<dialog>元素天然聚焦,且支持ESC关闭,可以节约开发成本。
每次弹框显示,我们使用history.pushState添加一条历史记录,当发生popstate变化的时候,判断当前的弹框状态,如果弹框正常展示,则执行关闭操作。
为了保证历史准确回退,可以在history.pushState执行的时候传递状态对象,在弹框关闭之后,对该状态对象进行判定,如果匹配,则执行history.back()。
完整的交互逻辑参见:
// modal就是弹框元素
const handlePopState = () => {
if (modal.isConnected) {
modal.dispatchEvent(new Event("click"));
}
};
// 弹框显示的时候
// 增加历史记录
history.pushState({ modal: true }, '', location.href);
// 监听地址栏变化
window.addEventListener("popstate", handlePopState);
// 弹框元素移除的时候
// 移除地址栏变化监听
window.removeEventListener("popstate", handlePopState);
// 历史回退
if (history.state && history.state.modal) {
history.back();
}
自然可以。
现在的DOM能力已经很强大了,我们无需关心点击事件等行为,也不需要用到Web Components这么重的东西,只需要通过一个简单的属性,就可以让元素拥有点击查看大图的效果了。
我花了点时间,把这个交互效果封装在了一个JS中,大家只需要引用这个JS文件,无需其他任何设置,就可以有对应的效果了。
小玩具我都是放在gitee上的:https://gitee.com/zhangxinxu/image-preview
使用很方便:
image-preview.js 文件,注意设置 type="module"
is-preview 属性即可is-preview 属性值即可自动成组如果希望缩略图是小图,点击查看的是大图,可以使用srcset属性,例如:
<img src="large.jpg" srcset="normal.jpg">
本文的demo页面有相关示意,本JS会在鼠标悬停图片的时候,提前预加载大图。
关于srcset更多知识,可以参见此文:“响应式图片srcset全新释义sizes属性w描述符”
在我的书籍《HTML并不简单》中则有更加详细的介绍:

注意,仓库代码使用了CSS嵌套、HTML5 dialog、Page Transition API等新特性,过于陈旧的浏览器运行可能会有问题。
不过这些问题都可以轻松适配,如果你有相关需求,可以fork项目,自行修改,例如CSS嵌套语法改为普通语法,dialog元素补全缺失的CSS。
好了,春节回来的第一篇文章。
用了很多学到的新特性,感受到了学习的价值,和新技术带来的开发体验和用户体验的提升。
在新的一年,祝大家万事顺利,节节高升。

本文为原创文章,会经常更新知识点以及修正一些错误,因此转载请保留原出处,方便溯源,避免陈旧错误知识的误导,同时有更好的阅读体验。
本文地址:https://www.zhangxinxu.com/wordpress/?p=12082
(本篇完)
2026-02-12 18:23:48
by zhangxinxu from https://www.zhangxinxu.com/wordpress/?p=12076
本文可全文转载,但需要保留原作者、出处以及文中链接,AI抓取保留原文地址,任何网站均可摘要聚合,商用请联系授权。
当年我是捧着JavaScript高级语言设计这本书学习JS正则表达式的,知识基本上都停留在那个时期。
最近偶然发现,正则表达式还支持sticky粘性标识,使用字母y表示。
看了下支持的时间,距今也有五六年的时间了,已经谈不上新特性了。

趁着春节前比较有空,赶快学习一番。
粘性匹配的标识符是y。
顺便回顾下其他标识符,全局是g,不缺分大小写是i,多行是m。
以上知识都是所有前端开发人员都需要掌握的。
粘性匹配在实际使用的时候,一定要指定lastIndex,因为他的含义就是指定索引位置的匹配。
例如:
const str = "table football"; const regex = /foo/y; regex.lastIndex = 6; console.log(regex.test(str)); // 输出结果是: true console.log(regex.test(str)); // 输出结果是: false
上面的示意代码,第一个regex.test(str)之所以为true,是因为字符串"table football"的索引6位置是空格,正好后面的字符就是 foo。
而第二个regex.test(str)返回值是false是因为粘性匹配完成后,如果匹配,则lastIndex自动定位到匹配字符的结尾,也就是tball,自然就返回false。
如果粘性定位匹配失败,那么lastIndex会变成0.
下图就是运行结果示意:

粘性匹配y标识符适合具有规律结构的复杂字符串匹配。
例如解析 Token(标记化)、构建词法分析器、解析特定格式数据流。
下面以解析一段简单的 CSS 声明块示意:
const cssInput = "color: #fff; display: block; margin: 20px;";
// 定义 Sticky 正则
// 匹配 "属性名: 值;" 这种结构,并允许属性名前后有可选空格
const propRegex = /\s*([a-z-]+)\s*:\s*([^;]+)\s*;/y;
function parseCSS(input) {
const declarations = [];
// 只要匹配成功,propRegex.lastIndex 就会自动更新到下一次匹配的起点
while (true) {
const match = propRegex.exec(input);
if (match) {
const [fullMatch, property, value] = match;
declarations.push({ property, value: value.trim() });
} else {
// 检查是否是因为解析到了末尾而停止,还是因为遇到了非法格式
if (propRegex.lastIndex < input.length) {
console.warn(`解析中断,剩余内容不符合 CSS 格式。`);
}
break;
}
}
return declarations;
}
const result = parseCSS(cssInput);
console.table(result);
输出的结果如下图所示:

在处理长文本时,Sticky 模式具有显著的性能优势。
我们不妨假设一个场景,在这个场景下,我们已知目标内容应该出现在索引 n 处。
此时可以对比下:
这个场景……也算不上什么优势,只能说是个额外实现技巧。
在非多行模式下,lastIndex为0的Sticky正则其行为类似于带了行首锚点 ^ 的正则。
所以如果我们希望强制正则从头开始匹配,且不希望在正则字符串里硬编码 ^,可以使用 y 标志。
例如:
/^\d+/
可以写成:
/\d+/y
我们可以借助RegExp.prototype.sticky判断一个正则是不是粘性匹配的。
例如:
const regex = /foo/y; console.log(regex.sticky); // 返回结果: true
想想看,还有没有其他遗漏的。
哦,有个细节,就是exec()和test()方法的一个差异,按照MDN文档的说法:
对于exec()方法,同时具有粘性(sticky)和全局(global)特性的正则表达式与同时具有粘性和非全局特性的正则表达式行为相同。由于test()是exec()的简单封装,因此它会忽略全局标志,同样执行粘性匹配。
就我个人而言,exec()方法很少使用,所以,上面的细节差异,我也懒得深究了。
好了,就说这么多吧,我们春节后再见!

本文为原创文章,会经常更新知识点以及修正一些错误,因此转载请保留原出处,方便溯源,避免陈旧错误知识的误导,同时有更好的阅读体验。
本文地址:https://www.zhangxinxu.com/wordpress/?p=12076
(本篇完)