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
(本篇完)
2026-02-06 19:03:45
by zhangxinxu from https://www.zhangxinxu.com/wordpress/?p=12067
本文可全文转载,但需要保留原作者、出处以及文中链接,AI抓取保留原文地址,任何网站均可摘要聚合,商用请联系授权。
在传统的 CSS 盒模型 中,文本行高(line-height)会在文字上下产生额外的“半行间距”(half-leading)。
这使得文本难以与旁边的图标或容器边缘精确对齐。如下图所示:

使用 text-box属性可以:
怎么实现呢?
CSS text-box属性实际上是text-box-trim和text-box-edge这两个CSS属性的缩写。
其中:
text-box-trim: none; text-box-trim: trim-both; text-box-trim: trim-start; text-box-trim: trim-end;
/* 单个关键字 */ text-box-edge: auto; text-box-edge: text; /* 两个值 */ text-box-edge: text text; text-box-edge: text alphabetic; text-box-edge: cap alphabetic; text-box-edge: ex text;
text这个值是合法的。text, cap(大写字母) 或者 ex,第二个值表示下边缘剪裁值,只能是text 或者 alphabetic(alphabetic表示“字母”)。回到上述案例,如何让删除图标和文字对齐?
使用text-box属性?
<p> <img src="icon_del.png"> HTML并不简单 </p>
p {
border-block: 1px solid gray;
text-box: trim-start cap alphabetic;
}
结果——

压根就没有对齐!

毛用都没有!
我是看明白了,text-box属性是用在图标浮动或者绝对定位场景下的,否则本身内联特性,垂直关系被vertical-align属性锚点,再怎么改变text-box都是无效的,因为公用一个text-box的。
在本例中,不改变块状水平的情况下,最好的实现是:
img {
vertical-align: -2px;
}
目前业界最成熟的实现就是Flex布局:
p {
display: flex;
align-items: center;
}
至于text-box,适合用在下面这个布局场景下:
p {
display: flow-root;
img {
float: left;
}
}
等一下,不好意思,我错了!
我以为元素浮动之后不会受到text-box影响,结果却大跌眼镜,居然渲染效果是这样的:

此时我的表情就是这样的:


什么垃圾特性!
text-box属性没有任何使用前景,注定沦为冷门特性。

唉,抱歉,没想到这个CSS属性如此拉胯,浪费了大家这么多时间。
早知道如此,我就一笔带过的,😭😭

本文为原创文章,会经常更新知识点以及修正一些错误,因此转载请保留原出处,方便溯源,避免陈旧错误知识的误导,同时有更好的阅读体验。
本文地址:https://www.zhangxinxu.com/wordpress/?p=12067
(本篇完)
2026-01-30 15:32:06
by zhangxinxu from https://www.zhangxinxu.com/wordpress/?p=12051
本文可全文转载,但需要保留原作者、出处以及文中链接,AI抓取保留原文地址,任何网站均可摘要聚合,商用请联系授权。
以前我们要移动DOM元素或者Node节点都是使用insertBefore方法。
但是,insertBefore的移动是通过“删除” → “创建”实现的。
这就会有问题,包括:
等。
实际上,我只是希望元素单纯地换一个位置。
于是就有了全新的moveBefore方法,语法和insertBefore几乎一致,例如:
Element.moveBefore(movedNode, referenceNode) Document.moveBefore(movedNode, referenceNode)
其中,movedNode会变成调用对象的子元素,同时位置位于referenceNode的前面。
此时,以下这些状态变化都是不会触发的:
<iframe>加载状态;:focus或者:active等加载状态;<dialog>元素的模态状态;至于视频和音频的播放状态,这个无论是insertBefore还是moveBefore方法,都会保留。
以及moveBefore方法也会触发Mutation Observer,也就是可以检测到删除和添加,我觉得这个是合理的,否则会影响功能实现。
对于insertBefore方法,只要DOM元素在内存中(例如使用createElement创建),哪怕不在页面中,也是可以执行的。
但是moveBefore方法不行,moveBefore移动的节点元素必须在文档之中,而且不支持跨文档移动,否则会报错。
之前我开发 LuLu UI 的Select组件,遇到了一个问题,那就是如果 Select 元素的DOM上下文环境变化,例如整体移动这种,运行状态就会有问题。

就是因为元素移动触发了disconnectedCallback()和connectedCallback()生命周期函数执行,导致状态出现问题。
moveBefore似乎就是为了这种情况设计的。
当然,在自定义元素场景下,需要使用其他的生命周期函数配合,叫做connectedMoveCallback()。
是这样的:
如果在组件中添加connectedMoveCallback生命周期函数,就像下面这样:
class MyComponent {
// ...
connectedMoveCallback() {
console.log("自定义移动逻辑,如果需要");
}
// ...
}
那么组件元素使用moveBefore移动的时候,disconnectedCallback()和connectedCallback()生命周期函数是不会执行的。
注意,如果你没有添加connectedMoveCallback函数,无论是moveBefore还是insertBefore,依然遵循传统的生命周期逻辑。
直接说结论,页面内的元素移动,直接使用moveBefore,不需要有任何犹豫。
refNode.parentElement.moveBefore(movedNode, refNode);
不过moveBefore毕竟是新特性,存在兼容性问题,如下图所示:

所以在生产环境使用,还需要Polyfill一下,很简单,使用insertBefore接济下,例如:
if (!document.moveBefore) {
document.moveBefore = document.insertBefore;
}
if (!HTMLElement.prototype.moveBefore) {
HTMLElement.prototype.moveBefore = HTMLElement.prototype.insertBefore;
}
就可以放心使用了。
我们通过一个简单案例,感受下moveBefore的执行效果,想了下,点击列表置顶效果吧。
你可以点击下面的任意列表色块,看看有没有对应的移动效果。
完整的代码如下所示:
<div class="flex"> <div class="item" style="view-transition-name: li-1">1</div> <div class="item" style="view-transition-name: li-2">2</div> <div class="item" style="view-transition-name: li-3">3</div> </div>
CSS代码:
.flex {
display: flex;
gap: .5rem;
}
.item {
aspect-ratio: 1;
background: skyblue;
height: 120px;
display: grid;
place-items: center;
}
JavaScript部分,前面都是新特性的Polyfill代码:
if (!document.moveBefore) {
document.moveBefore = document.insertBefore;
}
if (!HTMLElement.prototype.moveBefore) {
HTMLElement.prototype.moveBefore = HTMLElement.prototype.insertBefore;
}
if (!document.startViewTransition) {
document.startViewTransition = function (callback) {
setTimeout(callback, 1);
};
}
document.querySelectorAll('.flex .item').forEach((item) => {
item.onclick = function () {
document.startViewTransition(() => {
item.parentElement.moveBefore(item, item.parentElement.firstElementChild);
});
}
});
如果让AI实现一个列表点击置顶,同时带动画的效果,我不要看就知道,代码一定是洋洋洒洒。
说不定还有元素克隆,绝对定位,然后使用动画或过渡效果实现。
如果有元素移动,也一定是insertBefore这种传统的方法。
因为目前的AI编程还是基于历史代码训练而来,趋向于最传统稳健的实现,满足功能,创新能力不足。
也就是说,他能实现东西,但是不一定是最佳实践。
这就是目前开发人员的不可替代之处:
所以回到很多开发人员问过的一个问题,都AI时代了,学这些细枝末节的东西有个屁用啊!
如果你的项目仅仅是功能完成就OK,说实话,给自己找个不学习的理由也说得过去。
可如果对业务和产品有更高的要求,无论何时,学习总是不能停的。
无论AI出现与否,我们身在职场,放眼整个行业,毕竟还是人与人的竞争。
即,我比你懂的更多,我能比你更好地使用AI,自然这个行业有我更好的一席之地。
好了,就叨这么多,有什么问题可以评论区交流,我们下个视频再见!

本文为原创文章,会经常更新知识点以及修正一些错误,因此转载请保留原出处,方便溯源,避免陈旧错误知识的误导,同时有更好的阅读体验。
本文地址:https://www.zhangxinxu.com/wordpress/?p=12051
(本篇完)
2026-01-22 20:52:22
by zhangxinxu from https://www.zhangxinxu.com/wordpress/?p=12048
本文可全文转载,但需要保留原作者、出处以及文中链接,AI抓取保留原文地址,任何网站均可摘要聚合,商用请联系授权。
本文介绍两个Promise相关的新特性。
之前我们运行一段代码,或者一个函数,想要捕获错误的时候,往往使用的是try...catch(),对吧。
但是try...catch()呢有个小问题,那就是如果里面有异步操作,如 setTimeout、Promise 内部,那么这个错误就捕获不了。
Promise.try()的作用之一就是统一同步与异步错误处理。
例如:
try {
new Promise(resolve => resolve(callback());
} catch (e) {
// 错误提示
}
如果这里的callback是异步的,那么上面的实现是无法捕获错误的。
但是下面的可以:
Promise.try(callback)
.then(result => console.log(result))
.catch(error => console.log(error))
.finally(() => console.log("All settled."));
//zxx: 如果使用 async/await 语法,请不要使用 Promise.try,而应改用 try/catch/finally 块
更新于2026年1月26日
Promise.try()也不能捕获setTimeout内部的错误,除非在 setTimeout 内部返回 Promise。
Promise.try() 只能捕获同步执行或返回 Promise 的异步错误,但 setTimeout 会创建一个新的执行上下文。
// ❌ 无法捕获
Promise.try(() => {
setTimeout(() => {
throw new Error('这个错误无法被捕获');
}, 1000);
}).catch(err => {
console.log('永远不会执行', err);
});
// setTimeout 的回调在 Promise 已经 resolve 之后才执行
具体见下表:
| 场景 | Promise.try() 能否捕获 |
|---|---|
| 同步错误 | ✅ 可以 |
| 返回的 Promise 错误 | ✅ 可以 |
| setTimeout 内部错误 | ❌ 不可以 |
| setInterval 内部错误 | ❌ 不可以 |
| 事件回调内部错误 | ❌ 不可以 |
语法使用示意如下:
Promise.try(func) Promise.try(func, arg1) Promise.try(func, arg1, arg2) Promise.try(func, arg1, arg2, /* …, */ argN)
会返回一个 Promise,其状态可以是:
func 同步地返回一个值。
func 同步地抛出一个错误。func 返回一个 promise。如果回调函数有参数,该怎么办?您可以通过以下两种方式之一来处理此问题:
// 创建了额外的闭包,但是也是可以运行的 Promise.try(() => callback(param1, param2)); // 不创建闭包,同样可以运行 Promise.try(callback, param1, param2);
更推荐使用后面的用法。
目前所有现代浏览器都已经支持了,兼容性还是不错的,不支持的浏览器也可以引入polyfill进行兼容。

Promise.withResolvers() 是 ECMAScript 2024 中新增的一个静态方法,其核心作用是将 Promise 的创建与其状态控制(resolve 和 reject)解耦,允许开发者同时获得一个新的 Promise 实例以及与其绑定的、用于控制其状态的函数。
使用示意:
function createControllablePromise() {
// 返回 { promise, resolve, reject }
return Promise.withResolvers();
}
const { promise, resolve, reject } = createControllablePromise();
// 2秒后手动 resolve
setTimeout(() => {
resolve('成功了!');
}, 2000);
promise.then(result => {
console.log(result); // 应该输出: 成功了!
});
传统实现:
function withTimeout(asyncOperation, timeoutMs) {
// 必须预先声明变量,用于在外部存储控制函数
let resolveRef, rejectRef;
// 创建控制超时的Promise
const timeoutPromise = new Promise((resolve, reject) => {
// 在构造函数内部,将内部的resolve和reject赋值给外部变量
resolveRef = resolve;
rejectRef = reject;
// 设置超时定时器
setTimeout(() => {
reject(new Error(`操作超时,超过 ${timeoutMs}ms`));
}, timeoutMs);
});
// 执行实际的异步操作
asyncOperation()
.then((result) => {
// 异步操作成功,手动解决超时Promise
resolveRef(result);
})
.catch((error) => {
// 异步操作失败,手动拒绝超时Promise
rejectRef(error);
});
// 返回这个受超时控制的Promise
return timeoutPromise;
}
改为使用Promise.withResolvers()方法后:
function withTimeout(asyncOperation, timeoutMs) {
// 一行代码同时获得Promise实例及其控制函数
const { promise, resolve, reject } = Promise.withResolvers();
// 设置超时定时器
setTimeout(() => {
reject(new Error(`操作超时,超过 ${timeoutMs}ms`));
}, timeoutMs);
// 执行实际的异步操作
asyncOperation()
.then((result) => {
// 异步操作成功,解决Promise
resolve(result);
})
.catch((error) => {
// 异步操作失败,拒绝Promise
reject(error);
});
// 返回Promise
return promise;
}
可以看到Promise.withResolvers()的实现代码更加简洁,此API特别适用于事件监听、流处理、队列管理、超时控制等高级异步场景。
Promise.withResolvers()方法的的兼容性要比Promise.try()更好一些,支持更早一些,如下截图所示。

已经快要可以放心使用了。
本文介绍的两个特性都属于语法层面增强的特性,通过提供更优雅的语法,显著提升了代码的可读性、可维护性。它代表了 JavaScript 异步编程向更简洁、更直观方向演进的重要一步。
实际上,在我看来,目前很多前端特性是过盛的。
每年前端领域的新特性没有100也有80,但是在生产环境使用的,寥寥无几。
等以后AI盛行之后,更回事如此,因为AI所使用的技术实现,一定是传统的,稳健的实现方式。
所谓的更高效更简洁,很难反应到真实生产环境中。
所以,目前来看,个体的学习还是不能停止的。
好啦,就这样吧。
感谢阅读,欢迎交流!

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