普通视图

发现新文章,点击刷新页面。
昨天以前张鑫旭-鑫空间-鑫生活

务必谨慎使用JS WeakRef弱引用

作者 张 鑫旭
2026年4月13日 16:01

by zhangxinxu from https://www.zhangxinxu.com/wordpress/?p=12155
本文可全文转载,但需要保留原作者、出处以及文中链接,AI抓取保留原文地址,任何网站均可摘要聚合,商用请联系授权。

一、weak三剑客

JS语言中的弱类型除了本文要介绍的WeakRef,还有WeakMap和WeakSet,其中WeakMap我5年前有介绍过,参见“JS WeakMap应该什么时候使用”。

WeakMap和WeakSet支持很多很多年了,无需顾忌任何兼容性问题,但是WeakRef则是前几年刚支持的特性。

WeakRef的兼容性

三者的本质区别

用一句话说明WeakMap、WeakSet和WeakRef的区别,那就是:

WeakMap 和 WeakSet 是集合类数据结构,弱引用是它们管理成员的方式;WeakRef 是对单个对象的弱引用包装器,让你能主动检查对象是否还活着。

用更加通俗的话解释就是:

  • WeakMap:我想给对象贴标签/存数据,但不阻止它被回收
  • WeakSet:我想记住哪些对象出现过,但不阻止它们被回收
  • WeakRef:我想保留一个对象的引用,但允许它随时被回收,用时再检查

下面案例时刻:

1. WeakMap —— 以对象为键的键值映射

const wm = new WeakMap();
let obj = { name: 'Alice' };

wm.set(obj, '额外数据');   // 键必须是对象
console.log(wm.get(obj));  // '额外数据'

obj = null; // obj 被回收后,WeakMap 中对应的条目也会自动消失

补充说明:

  • 必须是对象(不能是原始值)
  • 值可以是任意类型
  • 当键对象没有其他引用时,键值对会被 GC 自动回收
  • 常用于:DOM 节点关联数据、类的私有数据存储

2. WeakSet —— 对象的弱引用集合

const ws = new WeakSet();
let obj = { id: 1 };

ws.add(obj);              // 只能添加对象
console.log(ws.has(obj)); // true

obj = null; // obj 被回收后,WeakSet 中的条目也自动消失

补充说明:

  • 只能存储对象,不能存原始值
  • 只有 addhasdelete 三个方法
  • 常用于:标记对象是否”已访问”、”已处理”、防止循环引用

3. WeakRef —— 对单个对象的弱引用

let obj = { data: 'heavy resource' };
const ref = new WeakRef(obj);

// 通过 .deref() 获取目标对象
console.log(ref.deref());        // { data: 'heavy resource' }
console.log(ref.deref()?.data);  // 'heavy resource'

obj = null;
// 某次 GC 之后...
console.log(ref.deref()); // undefined(对象已被回收)

要点说明:

  • 包装单个对象,不是集合
  • 通过 .deref() 取回对象,如果已被 GC 回收则返回 undefined
  • 通常搭配 FinalizationRegistry 使用,在对象被回收时执行清理回调
  • 常用于:缓存(对象在则命中缓存,被回收则重新创建)

二、JS WeakRef 弱引用经典案例

列举几个适合使用JS WeakRef的场景,希望可以让大家对WeakRef的作用有更加深刻的理解。

1. 缓存场景

const cache = new Map();

function getCached(key, createFn) {
  const ref = cache.get(key);
  const cached = ref?.deref();

  if (cached) return cached; // 缓存命中

  // 缓存未命中或已被 GC 回收,重新创建
  const newObj = createFn();
  cache.set(key, new WeakRef(newObj));
  return newObj;
}

这里 WeakRef 允许缓存的大对象在内存紧张时被自动回收,避免内存泄漏,个人观点,缓存处理是WeakRef最具代表性的应用场景。

2. DOM 元素不泄漏的临时引用

先看DOM元素删除,但是内存依然占用的例子:

// 典型泄漏场景:DOM 节点已从页面移除,但 JS 仍持有强引用
const detachedNodes = [];

function addItem() {
  const div = document.createElement('div');
  document.body.appendChild(div);
  detachedNodes.push(div); // 强引用
}

function removeItem(div) {
  document.body.removeChild(div);
  // 虽然从 DOM 树移除了,但 detachedNodes 数组还引用着它
  // → GC 无法回收 → 内存泄漏!
}

WeakRef可以解决这个问题:

const nodeRefs = [];

function addItem() {
  const div = document.createElement('div');
  document.body.appendChild(div);
  nodeRefs.push(new WeakRef(div)); // 弱引用,不阻止 GC
}

function doSomethingWithNodes() {
  for (const ref of nodeRefs) {
    const node = ref.deref();
    if (node) {
      // 节点还活着,正常操作
      node.style.color = 'red';
    } else {
      // 节点已被 GC 回收,跳过
    }
  }
}

当 DOM 节点从文档中移除且没有其他强引用时,WeakRef 不会阻止 GC 回收它。

但是,虽然WeakRef有避免DOM内存泄露的能力,但我个人觉得不推荐这么使用,原因后面会解释。

3. 事件监听 + 不阻止 GC

有内存问题的代码示意:

// ❌ 泄漏:闭包持有 DOM 引用,且监听器未清理
function setup() {
  const el = document.getElementById('target');
  window.addEventListener('resize', () => {
    el.style.width = '100%'; // el 被闭包捕获,永远无法回收
  });
}

如果使用WeakRef:

function setup() {
  const ref = new WeakRef(document.getElementById('target'));
  window.addEventListener('resize', () => {
    const el = ref.deref();
    if (!el) return; // 对象没了,直接跳过
    el.style.width = '100%';
  });
}

还是那句话,这里其实使用AbortController主动移出监听器是更好的做法。

4. 组件实例池 / 对象池(复用但不泄漏)

比方说下面这段代码,对象销毁后自动从池子里 “消失”,不用手动清理。

const pool = new Set();

function addToPool(obj) {
  pool.add(new WeakRef(obj));
}

function foreachActive(cb) {
  for (const ref of pool) {
    const obj = ref.deref();
    if (obj) cb(obj);
    else pool.delete(ref);
  }
}

5. 防止循环引用导致的内存泄漏

这个案例中父 ↔ 子互相引用,但是由于使用了WeakRef,打破了强引用链,GC 能正常工作,内存不会泄露。

class Parent {
  constructor() {
    this.child = null;
  }
}

class Child {
  constructor(parent) {
    this.parentRef = new WeakRef(parent); // 弱引用
  }
}

const p = new Parent();
const c = new Child(p);
p.child = c;

OK,虽然上面展示了很多可以使用WeakRef的场景,但是这些处理手段都不推荐(除了第一个场景),只能作为避免内存泄漏的最后手段。

三、谨慎使用WeakRef

为什么要谨慎使用WeakRef?最重要的原因其实也就四个字——“不可预测”!

垃圾收集何时、如何以及是否发生,取决于任何给定JavaScript引擎的实现。你在一个引擎中观察到的任何行为,在另一个引擎中、在同一引擎的另一个版本中,甚至在相同引擎的相同版本但在稍有不同的情况下,都可能会有所不同。

比方说某个内存,你明明想要使用,但是却被回收了,这就很烦人。

我之前开发就遇到过,大数据量的视频buffer数据,最终视频合成的时候莫名丢失了,使得我不得不一开始的时候强引用,当时我也是没有搞懂浏览器引擎的回收机制,根据查阅资料的说法,就是内存不足的时候,某些未被强引用的巨大变量会被回收,即使这个变量最后需要使用。

又比如说,你一位内存已经被清理了,可实际上内存清理工作可能会比预期晚得多才进行,或者根本不会进行。

因为JavaScript 引擎的内存回收机制极为复杂:

  • 即使两个对象同时变得无法访问(例如,由于分代收集),一个对象可能比另一个对象更早地被垃圾收集。
  • 垃圾收集工作可以通过使用增量和并发技术来分时段进行。
  • 可以使用各种运行时启发式算法来平衡内存使用和响应性。
  • JavaScript 引擎可能会保留对看似无法访问的内容的引用(例如,在闭包或内联缓存中)。
  • 不同的JavaScript引擎处理这些事情的方式可能不同,或者同一个引擎在不同版本中可能会改变其算法。
  • 复杂的因素可能导致对象被意外地保留很长时间,例如与某些应用程序编程接口(API)一起使用时。

所以,在绝大多数场景下,主动清理引用(移除监听器、清空变量、useEffect return cleanup)才是正道。

WeakRef 的 deref() 返回 undefined 的时机依赖 GC,不可预测,所以不应该用它来做确定性的资源管理。

例如上面案例提到的事件管理,下面的AbortController方法是更加推荐的:

function setup() {
  const el = document.getElementById('target');
  const controller = new AbortController();
  
  window.addEventListener('resize', () => {
    el.style.width = '100%';
  }, { signal: controller.signal });
  
  // 需要清理时
  controller.abort(); // 自动移除监听器
}

我个人的观点是这样的:你该怎么开发就怎么开发,不用管什么内存没有回收这些,大多数场景下,你多使用点内存,少一点内存,对用户影响几乎没有,让JS引擎自己去维护吧,除非遇到内存占有很大,不得不去处理的场景,这个时候,大家再去考虑使用WeakRef做内存优化。

四、结语碎碎念

我的个人博客开始出现很多AI味十足的评论,先是一句话总结文章内容,然后对内容和作者无脑吹,什么“硬核”,什么“高大上”。

还不如以前回复的“写的狗屁不通”这样的评论。[叹气]

最近刷漫剧,发现下面也有很多这样的评论,都是AI机器人。

其实龙虾火了之后,这样的东西会越来越普遍。

比方说即有同事弄了自动去社交网站评论的龙虾。

不过我目前还未深入这类AI工具,不急不急,对我而言,掌握更基础的知识与技能,比追逐流行工具更有价值。

好了,就说这么多吧,感谢大家的阅读,我们下周再见!

飞吻再见

😉😊😇
🥰😍😘

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

(本篇完)

CSS六边形头像的实现与蜂巢布局

作者 张 鑫旭
2026年4月7日 16:41

by zhangxinxu from https://www.zhangxinxu.com/wordpress/?p=12118
本文可全文转载,但需要保留原作者、出处以及文中链接,AI抓取保留原文地址,任何网站均可摘要聚合,商用请联系授权。

本文内容分为两啪,一个是六边形头像效果的实现,而是金字塔布局(又称蜂巢布局)的实现。

一、六边形头像

不啰嗦,直接看代码和最终实现的效果,同样的,用的是CSS corner-shape属性。

img {
  aspect-ratio: cos(30deg);
  border-radius: 50% / 25%;
  corner-shape: bevel;
  width: 150px;
  border: 1px solid #0001;
  object-fit: cover;
}

实时渲染效果如下:

如果你是手机访问,或者一些很久没升级的国产浏览器,应当看不到效果,可以看下面的截图:

六边形头像截图效果

六边形头像的CSS代码是固定的,大家使用的时候直接复制粘贴就好了。

二、蜂窝布局实现方法

六边形也正好是蜂巢格子的形状,因此,非常适合用来实现金字塔一样的蜂窝布局。

实际上,这种布局在日常开发中也是比较常见的,例如我最近开发的某个页面就有这样的布局:

金字塔布局示意

一般的开发人员遇到这种状况,可能会手工硬搓每个元素的定位,例如,例如匹配第一项元素,让其绝对定位居中,第二行元素保持Flex布局。

.item:first-child {
  /* 第一行特殊居中处理 */
  position: absolute;
}

其实可以试试Flex倒序排版。

Flex实现蜂窝布局

假设HTML结构如下:

<div class="container">
  <span>1</span>
  <span>2</span>
  <span>3</span>
</div>

则可以试试如下所示的CSS:

.container {
  --size: 40px;
  --gap: 5px;
  --offset: calc((2 * var(--size) + var(--gap)) / (-4 * cos(30deg)));

  width: 240px;
  display: flex;
  flex-wrap: wrap-reverse;
  direction: rtl;
  justify-content: center;
  gap: var(--gap);
  padding-bottom: calc(-1 * var(--offset));
}

.container > span {
  aspect-ratio: cos(30deg);
  border-radius: 50% / 25%;
  corner-shape: bevel;
  width: calc(var(--size) * 2);
  margin-bottom: var(--offset);
  /* 排序倒序 */
  order: calc(-1 * sibling-index());
  /* 提示文字居中 */
  display: grid;
  place-items: center;
  background-color: deepskyblue;
  color: #fff;
}

此时的渲染效果如下截图所示:

Flex实现的蜂巢布局

不过Flex倒序只适合三个数量,如果超过,那么这个布局方法就无效了。

下面问题来了,有没有什么办法,无论列表数量多少,自动金字塔布局呢?

Grid实现蜂巢布局

有,Grid布局是可以实现这样的效果的。

我们先从最简单三个列表项开始实现,假设HTML代码如下:

<div class="container">
  <s></s>
  <s></s>
  <s></s>
</div>

如下CSS代码就可以有蜂窝布局效果了:

.container {
  --size: 40px;
  --gap: 5px;

  width: 240px;
  display: grid;
  grid-template-columns: repeat(auto-fit, var(--size));
  justify-content: center;
  gap: var(--gap);
  padding-bottom: calc((2 * var(--size) + var(--gap)) / (4 * cos(30deg)));
  outline: 1px dotted;
}

.container > s {
  grid-column-end: span 2;
  aspect-ratio: cos(30deg);
  border-radius: 50% / 25%;
  corner-shape: bevel;
  /* 垂直方向间隙和gap保持一致 */
  margin-bottom: calc((2 * var(--size) + var(--gap)) / (-4 * cos(30deg)));
  background-color: deepskyblue;
}

.container > :nth-child(1) {
  grid-column-start: 3;
}

.container > :nth-child(2) {
  grid-column-start: 2;
}

原理很简单,只需要精确指定每一行第一个元素的grid-column-start值就好了,在Grid布局中,每一行后面的元素只会自动跟随排列的。

如果是三个列表元素,那么第一行的首元素序列是1,因此选择器是:nth-child(1),第二行的首元素序列是2,因此选择器是:nth-child(2),最后一个元素自动跟随,无需专门设置。

实时渲染效果如下:

不限数量全自动蜂巢布局

由于Chrome浏览器支持了if函数,因此,纯CSS实现不限数量全自动蜂巢布局成为了可能,具体实现代码如下:

@property --_n {syntax: "<integer>";initial-value: 1;inherits: true}
@property --_i {syntax: "<number>";initial-value: 1;inherits: true}
@property --_j {syntax: "<number>";initial-value: 1;inherits: true}
@property --_c {syntax: "<number>";initial-value: 1;inherits: true}
@property --_d {syntax: "<number>";initial-value: 1;inherits: true}

.container {
  --s: 40px;  /* 尺寸大小  */
  --g: 5px;   /* 间隙大小 */
  
  display: grid;
  grid-template-columns: repeat(auto-fit, var(--s) var(--s));
  justify-content: center;
  gap: var(--g);
  padding-bottom: calc((2 * var(--s) + var(--g)) / (4 * cos(30deg)));
  container-type: inline-size;
}
.container > * {
  grid-column-end: span 2;
  aspect-ratio: cos(30deg);
  border-radius: 50% / 25%;
  corner-shape: bevel;
  margin-bottom: calc((2 * var(--s) + var(--g)) / (-4 * cos(30deg)));
  --_n: round(down, (100cqw + var(--g)) / (2 * (var(--s) + var(--g))));
  --_i: calc((sibling-index() - 2 + (var(--_n) * (3 - var(--_n))) / 2) / (2 * var(--_n) - 1));
  --_c: mod(var(--_i), 1);
  --_j: calc(sqrt(2 * sibling-index() - 1.75) - .5);
  --_d: mod(var(--_j), 1);
  grid-column-start: 
    if(
      style((--_i >= 1) and (--_c: 0)): 2; 
      style(--_d: 0): max(0, var(--_n) - var(--_j));
    );
}

先是根据容器尺寸和元素尺寸计算每行可以显示的数量,然后根据取模的值是不是整数,判断是不是每一行的第一项,通过if()函数设置精准的grid-column-start值。

原理虽然简单,但是实现细节还是很复杂的,比如大家无需深究,直接复制粘贴代码使用就可以了。

只需要将子元素换成图片元素,就可以轻松实现下图所示的蜂巢头像布局效果。

蜂窝头像布局示意

具体不展开,因为受制于兼容性限制,目前只能实验环境使用。

三、结语说明

前端三剑客中,CSS的发展是最快的,你看我写的新特性介绍文章,大多数都是CSS,并不是我刻意挑选,而真TM就是大多数前端新特性都是CSS。

考虑到CSS的学习热潮早就沉寂多年。

我觉得CSS这门语言离断层不远了,只要几年不关注,我跟大家讲,那些前沿的CSS代码,绝对是看不懂的。

各种新函数、属性还有语法糖层出不穷,就好比本文这个金字塔蜂巢布局中的CSS实现细节,我估计9成以上的前端是看不懂什么意思的。

其中出现的这些特性,我之前都有介绍:

  1. corner-shape见此文:大开眼界的CSS corner-shape属性
  2. aspect-ratio见此文:Chrome 88已经支持aspect-ratio属性了,学起来
  3. round()mod()等数学函数:Chrome也支持round等CSS数学函数了
  4. cos()三角函数见:CSS sin()/cos()等数学三角函数简介与应用
  5. sibling-index()索引序号函数介绍出处:CSS索引和数量匹配函数sibling-index sibling-count简介
  6. if()函数介绍:CSS倒反天罡居然支持if()函数了
  7. container-type100cqw属于容器查询里面的知识:2022年最期待的CSS container容器查询

所以还是那句话,学习是不能停止的,时代变化很快,要是安于现状,说不定就会掉队。

参考文章:响应式金字塔网格

😉😊😇
🥰😍😘

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

(本篇完)

CSS corner-shape与背景底纹技术

作者 张 鑫旭
2026年3月30日 14:08

by zhangxinxu from https://www.zhangxinxu.com/wordpress/?p=12115
本文可全文转载,但需要保留原作者、出处以及文中链接,AI抓取保留原文地址,任何网站均可摘要聚合,商用请联系授权。

一、corner-shape之前介绍过

CSS corner-shape属性去年8月份刚刚介绍过,可以实现多种图形效果。

很强,也大开眼界。

有兴趣的可以去这里学习其语法:“大开眼界的CSS corner-shape属性

但是这些图形效果一次只能创建一个,如果可以将这些图形效果批量复制,岂不是可以实现各种复杂的底纹背景效果。

还真可以实现。

二、文字、图形SVG背景技术

这种将HTML内容变成SVG背景图的技术我之前就研究并介绍过,可以参见“如何让文字作为CSS背景图片显示”此文。

对于文字,我们可以使用纯SVG语法。

但是,对于相对有些复杂的图形效果,我们可以借助<foreignObject>元素。

foreignObject我之前也介绍过,可以用来实现DOM截图效果,详见“SVG foreignObject简介与截图等应用”一文。

所以,我们的实现模板就变成了这样:

.template {
  background: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg"><foreignObject width="100%" height="100%"><div xmlns="http://www.w3.org/1999/xhtml">对该DIV设置样式</div></foreignObject></svg>'
}

三、实战-从最简单的开始

在过去,我们要实现网格线,需要使用两个repeating-linear-gradient()渐变函数,函数里面也需要写比较精确的断点。

如果,借助corner-shape属性,我们有了更加渐变的实现方法。

CSS代码示意:

.grid-bg {
  aspect-ratio: 1;
  background: #fff url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg"><foreignObject width="100%" height="100%"><div xmlns="http://www.w3.org/1999/xhtml" style="border: 1px solid deepskyblue;border-radius: calc(50% - 1px);corner-shape: notch;aspect-ratio: 1;width:60px;"></div></foreignObject></svg>');
  background-size: 60px 60px;
}

此时,我们给页面添加一个类名是grid-bg的canvas元素,我们就可以看到如下图所示的底纹效果了:

而CSS的背景图是可以无限叠加的,所以,我们可以再网格线基础上再融合点其他图形,例如,闪烁星星,于是:

..grid-bg2 {
  aspect-ratio: 1;
  --url-star: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg'><foreignObject width='100%' height='100%'><div xmlns='http://www.w3.org/1999/xhtml' style='background:deeppink;width:60px;height:60px;corner-shape:superellipse(-2.5);border-radius:50%;'></div></foreignObject></svg>");
  --url-grid: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg"><foreignObject width="100%" height="100%"><div xmlns="http://www.w3.org/1999/xhtml" style="border: 1px solid deepskyblue;border-radius: calc(50% - .75px);corner-shape: notch;aspect-ratio: 1;width:60px;"></div></foreignObject></svg>');
  background: var(--url-star) 1px 1px, var(--url-grid);
  background-size: 60px 60px;
}

可以有如下图所示的渲染效果:

底纹图形效果示意

更多案例

这里有个codepen地址,里面有多个使用corner-shape属性实现的背景纹理图。

背景纹理图

原理都是一样的,我就不赘述了。

四、结语说明

如果遇到SVG图形直接访问是可以的,但是作为background图片就无法渲染,试试对其进行转义,代码如下:

const encodeSvg = function (str) {
    return "data:image/svg+xml," + str.replace(/"/g,"'").replace(/%/g,"%25").replace(/#/g,"%23").replace(/{/g,"%7B").replace(/}/g,"%7D").replace(/</g,"%3C").replace(/>/g,"%3E");
}

其中 str就是完整的SVG代码。

另外,需要注意的是,使用<foreignObject>元素作为SVG背景图形的时候,里面的HTML祖先元素需要设置xhtml的命名空间,外部的SVG元素也需要SVG的命名空间,否则会有渲染问题。

截止到今天,也就是2026年3月30日,corner-shape属性依然只有Chrome浏览器支持,所以,本技术,大家目前了解下即可,实际生产环境使用,还需要些时日。

corner-shape的兼容性

😉😊😇
🥰😍😘

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

(本篇完)

HTML dialog元素新支持了closedBy属性

作者 张 鑫旭
2026年3月9日 17:31

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属性的语法

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'; 

三、好了,就这么点内容

最后看下兼容性:

dialog closeBy兼容性

目前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年2月25日 11:50

by zhangxinxu from https://www.zhangxinxu.com/wordpress/?p=12082
本文可全文转载,但需要保留原作者、出处以及文中链接,AI抓取保留原文地址,任何网站均可摘要聚合,商用请联系授权。

一、先看跟随放大效果

请看下面的MP4录屏效果(不动点击播放):

除了视频看到的效果,相关实现还支持:

  1. ESC关闭;
  2. 地址栏回退关闭;

眼见为实,您可以狠狠地点击这里:点击缩略图以动画效果呈现大图demo

二、一步步原理说明

1. 跟随放大效果是如何实现的?

这个使用的是startViewTransition实现的,这个是页面级别的transition过渡效果API的语法之一,非常好用。

我们可以无需关注动画细节,只需要符合前后页面的快照,浏览器自动就会补全其中的动画效果,有点类似于keynote中的神奇移动。

无论是删除、移动、还是这里的放大效果,都会有很棒的效果。

这个我在之前详细介绍过,可以访问这里:“页面级可视动画View Transitions API初体验

此特性我已经大量在生产环境使用了。

在本效果中,只需要将viewTransitionName在合适的时机在缩略图和预览图元素上进行设置,就会自动有相关的效果了。

originImg.style.viewTransitionName = "dialogImg";
// 放大执行的时候
document.startViewTransition(() => {
  originImg.style.viewTransitionName = "";
  cloneImg.style.viewTransitionName = "dialogImg";
});

2. 为何使用dialog元素?

使用<dialog>元素主要是两个原因:

  1. 顶层特性;
  2. 无障碍访问天然支持;

顶层特性可以让我们无需关心层级,保证大图效果永远在上面,适用场景更广泛。

<dialog>元素天然聚焦,且支持ESC关闭,可以节约开发成本。

3. 地址栏回退如何实现?

每次弹框显示,我们使用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();
}

4. 是否可以进行封装?

自然可以。

现在的DOM能力已经很强大了,我们无需关心点击事件等行为,也不需要用到Web Components这么重的东西,只需要通过一个简单的属性,就可以让元素拥有点击查看大图的效果了。

我花了点时间,把这个交互效果封装在了一个JS中,大家只需要引用这个JS文件,无需其他任何设置,就可以有对应的效果了。

三、交互封装与gitee开源

小玩具我都是放在gitee上的:https://gitee.com/zhangxinxu/image-preview

使用很方便:

  1. 引入 image-preview.js 文件,注意设置 type="module"
  2. 需要放大的图片元素设置 is-preview 属性即可
  3. 如果希望一次性预览多个图片,设置相同的 is-preview 属性值即可自动成组

如果希望缩略图和大图不是一个地址

如果希望缩略图是小图,点击查看的是大图,可以使用srcset属性,例如:

<img src="large.jpg" srcset="normal.jpg">

本文的demo页面有相关示意,本JS会在鼠标悬停图片的时候,提前预加载大图。

关于srcset更多知识,可以参见此文:“响应式图片srcset全新释义sizes属性w描述符

在我的书籍《HTML并不简单》中则有更加详细的介绍:

HTML并不简单

其他说明

注意,仓库代码使用了CSS嵌套、HTML5 dialog、Page Transition API等新特性,过于陈旧的浏览器运行可能会有问题。

不过这些问题都可以轻松适配,如果你有相关需求,可以fork项目,自行修改,例如CSS嵌套语法改为普通语法,dialog元素补全缺失的CSS。

四、新年快乐,开工大吉

好了,春节回来的第一篇文章。

用了很多学到的新特性,感受到了学习的价值,和新技术带来的开发体验和用户体验的提升。

在新的一年,祝大家万事顺利,节节高升。

春节快乐

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

(本篇完)

JS正则表达式y标识符之粘性匹配

作者 张 鑫旭
2026年2月12日 18:23

by zhangxinxu from https://www.zhangxinxu.com/wordpress/?p=12076
本文可全文转载,但需要保留原作者、出处以及文中链接,AI抓取保留原文地址,任何网站均可摘要聚合,商用请联系授权。

一、前言概述

当年我是捧着JavaScript高级语言设计这本书学习JS正则表达式的,知识基本上都停留在那个时期。

最近偶然发现,正则表达式还支持sticky粘性标识,使用字母y表示。

看了下支持的时间,距今也有五六年的时间了,已经谈不上新特性了。

粘性正则兼容性

趁着春节前比较有空,赶快学习一番。

二、y标识符基础常识

粘性匹配的标识符是y

顺便回顾下其他标识符,全局是g,不缺分大小写是i,多行是m

以上知识都是所有前端开发人员都需要掌握的。

必不可少的lastIndex

粘性匹配在实际使用的时候,一定要指定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);

输出的结果如下图所示:

CSS解析示意

极致的性能优化

在处理长文本时,Sticky 模式具有显著的性能优势。

我们不妨假设一个场景,在这个场景下,我们已知目标内容应该出现在索引 n 处。

此时可以对比下:

  • 全局模式 (/pattern/g): 如果位置 n 不匹配,引擎会继续扫描 n+1, n+2 直到文本结束。
  • Sticky 模式 (/pattern/y): 如果位置 n 不匹配,立即停止并返回 null。这避免了在大规模文本中进行无谓的全量扫描。

模拟锚点匹配

这个场景……也算不上什么优势,只能说是个额外实现技巧。

在非多行模式下,lastIndex0的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

(本篇完)

告别insertBefore,使用moveBefore移动DOM元素

作者 张 鑫旭
2026年1月30日 15:32

by zhangxinxu from https://www.zhangxinxu.com/wordpress/?p=12051
本文可全文转载,但需要保留原作者、出处以及文中链接,AI抓取保留原文地址,任何网站均可摘要聚合,商用请联系授权。

一、新的moveBefore方法

以前我们要移动DOM元素或者Node节点都是使用insertBefore方法。

但是,insertBefore的移动是通过“删除” → “创建”实现的。

这就会有问题,包括:

  • 元素的动画中断;
  • :active状态丢失;
  • 触发Mutation Observer;

等。

实际上,我只是希望元素单纯地换一个位置。

于是就有了全新的moveBefore方法,语法和insertBefore几乎一致,例如:

Element.moveBefore(movedNode, referenceNode)
Document.moveBefore(movedNode, referenceNode)

其中,movedNode会变成调用对象的子元素,同时位置位于referenceNode的前面。

此时,以下这些状态变化都是不会触发的:

  • animation动画transition过渡状态;
  • <iframe>加载状态;
  • :focus或者:active等加载状态;
  • 元素全屏状态;
  • popover浮层的开关状态;
  • <dialog>元素的模态状态;

至于视频和音频的播放状态,这个无论是insertBefore还是moveBefore方法,都会保留。

以及moveBefore方法也会触发Mutation Observer,也就是可以检测到删除和添加,我觉得这个是合理的,否则会影响功能实现。

moveBefore使用限制

对于insertBefore方法,只要DOM元素在内存中(例如使用createElement创建),哪怕不在页面中,也是可以执行的。

但是moveBefore方法不行,moveBefore移动的节点元素必须在文档之中,而且不支持跨文档移动,否则会报错。

Web Components中的作用

之前我开发 LuLu UI 的Select组件,遇到了一个问题,那就是如果 Select 元素的DOM上下文环境变化,例如整体移动这种,运行状态就会有问题。

Select组件代码

就是因为元素移动触发了disconnectedCallback()connectedCallback()生命周期函数执行,导致状态出现问题。

moveBefore似乎就是为了这种情况设计的。

当然,在自定义元素场景下,需要使用其他的生命周期函数配合,叫做connectedMoveCallback()

是这样的:

如果在组件中添加connectedMoveCallback生命周期函数,就像下面这样:

class MyComponent {
  // ...
  connectedMoveCallback() {
    console.log("自定义移动逻辑,如果需要");
  }
  // ...
}

那么组件元素使用moveBefore移动的时候,disconnectedCallback()connectedCallback()生命周期函数是不会执行的。

注意,如果你没有添加connectedMoveCallback函数,无论是moveBefore还是insertBefore,依然遵循传统的生命周期逻辑。

二、moveBefore实践指南

直接说结论,页面内的元素移动,直接使用moveBefore,不需要有任何犹豫。

refNode.parentElement.moveBefore(movedNode, refNode);

不过moveBefore毕竟是新特性,存在兼容性问题,如下图所示:

moveBefore的兼容性

所以在生产环境使用,还需要Polyfill一下,很简单,使用insertBefore接济下,例如:

if (!document.moveBefore) {
  document.moveBefore = document.insertBefore;
}
if (!HTMLElement.prototype.moveBefore) {
  HTMLElement.prototype.moveBefore = HTMLElement.prototype.insertBefore;
}

就可以放心使用了。

案例

我们通过一个简单案例,感受下moveBefore的执行效果,想了下,点击列表置顶效果吧。

你可以点击下面的任意列表色块,看看有没有对应的移动效果。

1
2
3

完整的代码如下所示:

<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编程还是基于历史代码训练而来,趋向于最传统稳健的实现,满足功能,创新能力不足。

也就是说,他能实现东西,但是不一定是最佳实践。

这就是目前开发人员的不可替代之处:

  1. 开发人员有不错的架构设计能力,能够很好地引导AI一步一步实现预期的代码;
  2. 开发人员有创新能力,眼界广泛,知道什么样的代码或者方法才是最好最优解。

所以回到很多开发人员问过的一个问题,都AI时代了,学这些细枝末节的东西有个屁用啊!

如果你的项目仅仅是功能完成就OK,说实话,给自己找个不学习的理由也说得过去。

可如果对业务和产品有更高的要求,无论何时,学习总是不能停的。

无论AI出现与否,我们身在职场,放眼整个行业,毕竟还是人与人的竞争。

即,我比你懂的更多,我能比你更好地使用AI,自然这个行业有我更好的一席之地。

好了,就叨这么多,有什么问题可以评论区交流,我们下个视频再见!

飞吻再见

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

(本篇完)

Promise.try()和Promise.withResolvers()作用速览

作者 张 鑫旭
2026年1月22日 20:52

by zhangxinxu from https://www.zhangxinxu.com/wordpress/?p=12048
本文可全文转载,但需要保留原作者、出处以及文中链接,AI抓取保留原文地址,任何网站均可摘要聚合,商用请联系授权。

本文介绍两个Promise相关的新特性。

一、Promise.try()的作用

之前我们运行一段代码,或者一个函数,想要捕获错误的时候,往往使用的是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()的语法

语法使用示意如下:

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.try()兼容性

三、Promise.withResolvers()的作用

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

(本篇完)

CSS text-decoration-inset属性首发简介

作者 张 鑫旭
2026年1月19日 12:07

by zhangxinxu from https://www.zhangxinxu.com/wordpress/?p=12029
本文可全文转载,但需要保留原作者、出处以及文中链接,AI抓取保留原文地址,任何网站均可摘要聚合,商用请联系授权。

一、text-decoration新特性

CSS text-decoration属性又新增了一个名为text-decoration-inset的子属性,可以改变下划线的左右缩进大小。

MDN上有案例,由于目前兼容性还不好,仅Firefox浏览器才支持,所以,我录个GIF屏给大家看下。

下划线缩进效果

语法

/* auto 关键字 */
text-decoration-inset: auto;

/* 一个长度值 */
text-decoration-inset: 20px;
text-decoration-inset: -2rem;

/* 两个长度值 */
text-decoration-inset: 20px 1em;
text-decoration-inset: -0.5rem -1.5rem;
text-decoration-inset: -2ex 1vw;

如果属性值是两个长度值,则分别表示左右方位的下划线缩进大小。

美足配图

二、关于关键字值auto

根据MDN文档的说法,如果连续两个元素的下划线设置text-decoration-inset:auto,那么下划线之间会有间隙。

我们不妨测试下:

例如:

<u>HTML</u><u>并不</u><u>简单</u>
u {
  text-decoration-color: red;
  text-decoration-thickness: 3px;
  text-decoration-inset: auto;
}

实际的渲染效果如下所示(目前Firefox 146+ 效果可见):

HTML并不简单

然后我在Firefox浏览器下跑了下,果然可以看到下划线之间有明显的间隙,如下截图所示:

下划线间隙

这倒是有点意思。

如果不设置text-decoration-inset:auto,效果就是严丝合缝,一条下划线贯穿左右。

严丝合缝

三、直接结语吧

应该是国内第一个介绍text-decoration-inset属性的,因为太新了,兼容性还是一片红,除了最新的Firefox浏览器。

text-decoration-inset兼容性

这个特性可以用来绘制很长的一条波浪线,基于字符的,还是有些妙用的。

好了,就说这么多吧。

AI时代,大家知道有这么个东西就好了,无需进一步深入。

我们下周再见。

😉😊😇
🥰😍😘

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

(本篇完)

纯CSS实现折线连接两个任意元素效果

作者 张 鑫旭
2026年1月16日 20:58

by zhangxinxu from https://www.zhangxinxu.com/wordpress/?p=12026
本文可全文转载,但需要保留原作者、出处以及文中链接,AI抓取保留原文地址,任何网站均可摘要聚合,商用请联系授权。

一、事情的起因

之前介绍CSS锚点定位的时候有提到:

我们可以利用此特性,轻松实现任意两个点相连的折线效果,在过去,类似这样的效果一定要借助JS才可以。

折线效果问答

然后我就抽空自己试验了自己的想法,发现此事并没有自己想的那么简单。

二、先看下效果

先看GIF录屏效果,纯CSS实现的:

拖拽

演示页面

您可以狠狠地点击这里:纯CSS实现两个元素之间折线自动相连demo

实现原理

先从最简单的说起,两个圆和一条线。

<div class="circle circle-a"></div>
<div class="circle circle-b"></div>
<i class="line"></i> 

圆的样式很简单,50%圆角绝对定位就可以了,对于本文需要实现的效果,重要的是定义锚点的名称:

.circle-a {
  anchor-name: --a;
}
.circle-b {
  anchor-name: --b;
}

此时,我们的线就可以左右两个球定位了:

.line {
  position: absolute;
  --posA: calc(anchor(--a inside) + anchor-size(--a) / 2);
  --posB: calc(anchor(--b inside) + anchor-size(--b) / 2);
  top: var(--posA);
  bottom: var(--posB);
  left: var(--posA);
  right: var(--posB);
  outline: dashed;
}

此时的效果就会是这样的:

原理示意步骤1

此时,我们就可以使用对角线渐变连接线条了(clip-path剪裁也可以):

.line {
  background: linear-gradient(to left bottom, transparent calc(50% - 2px), currentColor calc(50% - 2px) calc(50% + 2px), transparent calc(50% + 2px)) no-repeat;
}

效果如下图所示:

对角线连接2

这线都跑到圆球上了,怎么办?

可以遮罩处理下,正好端点弄两个径向渐变遮罩下。

.line {
  mask: radial-gradient(circle at 0 0, #000 85px, transparent 85px no-repeat, 
	  radial-gradient(circle at right bottom, #000 65px, transparent 65px no-repeat, 
	  linear-gradient(#000, #000);
}

最终的效果

原理还是很简单的,但是实际上,两个球的位置是不固定的,上下左右都有可能,所以,如果只考虑一个方位,那么两个球的位置变化的时候,直线可能就不显示,因此,最终是使用了4条线。

<div class="circle circle-a"></div>
<div class="circle circle-b"></div>
<i class="line line1"></i> 
<i class="line line2"></i> 
<i class="line line3"></i> 
<i class="line line4"></i>

完整代码可以参考demo页面。

三、其他一些说明

不过上面的实现并不完美,当两个圆的圆心在同一水平线,或者在同一垂直线上的时候,连接线是不显示的。

这个其实也有方法解决,不过麻烦了些。

  1. 需要在设置line为container容器元素;
  2. 图形效果使用子元素绘制,同时设置最小尺寸;
  3. 基于 100cqw100cwh计算的角度判断子元素是否显示,利用opacity属性的边界特性。

有个codepen案例就是这么实现的,有兴趣可以研究下。

时间有限,我就不深入了,因为这个实现成本已经超过JS了。

好吧,就说这么多,如果觉得内容不错,欢迎转发,分享。

我们下一篇文章再见👋🏻

含韵挥手

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

(本篇完)

学会使用CSSStyleSheet构造CSS样式

作者 张 鑫旭
2026年1月5日 16:32

by zhangxinxu from https://www.zhangxinxu.com/wordpress/?p=12014
本文可全文转载,但需要保留原作者、出处以及文中链接,AI抓取保留原文地址,任何网站均可摘要聚合,商用请联系授权。

一、创建style元素的问题

如果想要在页面中插入一段全新的CSS样式,大多数的前端开发人员都是通过创建 <style> 元素,然后插入字符串CSS代码实现的,示意:

const styleEl = document.createElement('style');
styleEl.innerHTML = `.my-class { color: red; font-size: 16px; }`;
document.head.appendChild(styleEl);

这种方法的不足非常明显:

  1. 难以进行细粒度操作:要修改、删除或查询某一条具体的CSS规则,必须手动解析整个庞大的CSS字符串,这既繁琐又容易出错。

    例如,我们想要在添加一段CSS样式,代码需要类似这样处理:

    const styleEl = document.querySelector('style');
    const oldText = styleEl.innerHTML;
    styleEl.innerHTML = oldText + '\n.new-rule { background: blue; }';
    
  2. 性能低下:每次修改都意味着要替换整个 <style> 标签的内容,浏览器需要重新解析整个样式字符串,对于频繁的样式更新,这会带来不必要的性能消耗。
  3. 容易引发错误:在字符串拼接或替换过程中,一个微小的语法错误(如缺少分号、括号不匹配)就可能导致整个样式表失效,且错误难以定位。
  4. 无法利用已解析的结构:浏览器在加载页面时,已经将CSS解析成了结构化的规则对象(CSSOM)。而字符串操作完全绕过了这个高效的结构,迫使开发者自己处理原始文本。

有不足就有需求,于是CSSStyleSheet()构造函数应运而生,专门用来创建CSS样式。

二、CSSStyleSheet使用指南

案例是最快速的学习方式,例如,给当前文章的所有 <h3> 标题添加阴影,则可以:

// 构造空白的样式表
const sheet = new CSSStyleSheet();
// 给样式表添加对应的CSS规则
sheet.replaceSync("article h3 { text-shadow: 2px 2px 4px #0006; }");
// 添加到页面中
document.adoptedStyleSheets.push(sheet);

这段代码是事实运行的,大家可以仔细观察文章的标题文字,看看是不是有投影。

语法

语法如下:

const sheet = new CSSStyleSheet(options)

其中,options是可选参数,包括下面这些参数值:

baseURL
样式表中的URL地址的根地址
media
媒体查询规则。可以是单个字符串规则,也可以是MediaList。
使用示意:
const stylesheet = new CSSStyleSheet({ media: "print" });
console.log(stylesheet.media);
disabled
是否禁用当前的样式表。默认值是 false.

然后,返回值sheet是个CSSStyleSheet对象,包含以下一些属性和CSS规则处理方法:

三、CSSStyleSheet对象的属性和方法

两个属性,用来返回当前样式表的样式:

cssRules
只读属性,返回一个实时CSSRuleList。
ownerRule
如果使用@import规则将此样式表导入文档,ownerRule属性将返回相应的CSSImportRule;否则,此属性的值为null

ownerRule属性很少使用,我们看下cssRules属性的细节。

1. 嵌套语法的输出

比方说我们看下CSS嵌套语法返回的内容:

const sheet = new CSSStyleSheet();
sheet.replaceSync(`.project-x {
  display: flex;
  &.many {
    font-size: calc(1em - 2px);
    .project-item {
      padding: .5em .75em;
    }
    max-height: calc(100vh - var(--rem) * 20);
    overflow: hidden;
  }
}`);
console.log(sheet.cssRules);

最终的输出结果出乎意料,只有一个CSS规则,如下截图所示:

CSS嵌套语句的输出

2. 如果语句铺平

如果嵌套语句打平,就像这样:

const sheet = new CSSStyleSheet();
sheet.replaceSync(`.project-x {
  display: flex;
}
.project-x.many { font-size: calc(1em - 2px); }
.project-x .project-item {
  padding: .5em .75em;
}`);
console.log(sheet.cssRules);

那么输出的规则就返回预期了:

多个规则示意

嗯……🤔……

没有预想的强大啊,嵌套语句也应该帮忙解析成一个一个规则才是。

3. 如果使用AT规则

测试代码为:

const sheet = new CSSStyleSheet();
sheet.replaceSync(`@media (width < 480px) {
   body { font-size: 16px; }
}
@scope(.container) {
  .list {
    color: red;
  }
}`);
console.log(sheet.cssRules);

结果是两条规则,还有不同的type,嗯……🤔……看来,这里面水还挺深的。

AT规则遍历结果


再来看下比属性更加常用的方法。

deleteRule(index)
删除规则,参数是规则索引值。
insertRule(rule, index)
插入CSS规则,rule是CSS规则字符串,index参数可选,表示新CSS规则插入的位置,默认是0
replace()
样式完整替换,返回的是Promise。
replaceSync()
同步样式替换,平时我们使用这个更多一些,属于后来支持的新特性。

相比传统的innerHTML DOM元素替换,CSSStyleSheet要更加规范。

四、优先级、兼容性等特性

CSSStyleSheet构造样式的优先级和常规的CSS样式优先级规则一致。

并且在页面中是没有对应的DOM元素的,该样式会有对应的constructed stylesheet标识,如下截图所示:

构造样式标识

Shadow DOM

CSSStyleSheet构造的样式也可以再shadow中创建,例如:

const node = document.createElement("div");
const shadow = node.attachShadow({ mode: "open" });
// 添加样式表到 shadow DOM 中
shadow.adoptedStyleSheets = [sheet];

兼容性

CSSStyleSheet的大部分特性浏览器很早就支持了,但是其中一个方法,也就是 replaceSync() 方法属于新特性,最近一两年才支持的,兼容性如下图所示:

replaceSync()兼容性

基本上大家是可以放心使用的。

五、结语

传统的字符串操作样式的方式,本质上是在“模拟”浏览器的工作,笨重且不灵活。

而 CSSStyleSheet API 提供了一套浏览器原生的、面向对象的接口,让开发者能够以浏览器“理解”的方式去直接操作样式规则本身。
它带来的改变是根本性的:

  • 从“全文替换”到“精准手术”。
  • 从“容易出错”到“稳定可控”。
  • 从“性能消耗大”到“高效更新”。

因此,当您需要动态、精细、高性能地管理页面样式时,尤其是在开发复杂的交互应用、浏览器扩展、主题系统或组件库时,CSSStyleSheet API 是远比操作字符串更现代、更专业的解决方案。

😉😊😇
🥰😍😘

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

(本篇完)

今日学习CSS style()样式查询及其range范围语法

作者 张 鑫旭
2025年12月29日 09:10

by zhangxinxu from https://www.zhangxinxu.com/wordpress/?p=11975
本文可全文转载,但需要保留原作者、出处以及文中链接,AI抓取保留原文地址,任何网站均可摘要聚合,商用请联系授权。

一、先了解下样式查询

@container规则除了尺寸匹配(这个支持有一段时间了),还支持样式匹配。

目前除了Chrome浏览器,Safari浏览器也已经支持了,实在是好消息啊!

容器样式匹配查询兼容性

我们先看一个基础的案例,基于--status变量显示不同的样式。

HTML代码如下:

<section class="card" style="--status: success;">
  <div class="status">成功</div>
</section>
<section class="card" style="--status: error;">
  <div class="status">失败</div>
</section>
<section class="card" style="--status: warning;">
  <div class="status">警告</div>
</section>

此时,我们就可以根据容器元素上的--status变量值进行针对性的样式设置,例如:

@container style(--status: success) {
  .status {
    background-color: green;
  }
}
@container style(--status: error) {
  .status {
    background-color: darkred;
  }
}
@container style(--status: warning) {
  .status {
    background-color: orange;
  }
}

此时,就可以得到类似下图的渲染效果:

容器变量背景色

想要查看完整CSS代码,您可以狠狠地点击这里:CSS容器样式查询基本使用demo

指定容器名称

如果有多个容器,用了同样的变量名称,那么上面的案例可能就有冲突的问题。

此时,我们可以通过给容器命名的方式进行处理。

请看案例。

<section class="card" style="--status: success;">
  <div class="status">卡片成功</div>
</section>
<section class="toast" style="--status: success;">
  <div class="status">提示成功</div>
</section>

以上两个<section>元素设置的变量是一样的,子元素也是一样的,此时想要有所区别,就需要使用container-name属性,CSS代码参考:

.card {
  container-name: card;   
}
.toast {
  container-name: toast;    
}
@container card style(--status: success) {
  .status {
    background-color: green;
  }
}
@container toast style(--status: success) {
  .status {
    background-color: #000;
    &::after {
      content: '√';
      margin-left: .5ch;
    }
  }
}

此时,就有如下截图所示的渲染效果:

不同名称的容器查询

同样的,也演示页面,您可以狠狠地点击这里:CSS容器名称精确匹配样式查询demo

二、再看下范围解析语法

单纯的CSS变量匹配并不能让开发人员兴奋,因为,通过属性选择器,某种意义上也是可以对CSS变量进行匹配的,例如:

.toast[style*="--status: success"] {
  /* 子元素巴拉巴拉…… */
}

但是,下面这个范围解析语法,一定会让你双眼放光。

举个简单的例子,不同的分数显示不同的颜色。

90分以上是金色,金色传说。
80分以上是绿色,60~80橙色,60分以下是红色。

则无需JS判断,CSS就能处理。

HTML如下所示:

<span class="score" style="--score: 95;">
  <data>95</data>
</span>
<span class="score" style="--score: 85;">
  <data>85</data>
</span>
<span class="score" style="--score: 65;">
  <data>65</data>
</span>
<span class="score" style="--score: 35;">
  <data>35</data>
</span>

此时,就可以在style()函数中,使用大于号,小于号进行匹配,代码来咯:

@container style(--score >= 90) {
  data {
    color: gold;
  }
}
@container style(--score >= 80) and style(--score < 90) {
  data {
    color: green;
  }
}
@container style(--score >= 60) and style(--score < 80) {
  data {
    color: orange;
  }
}
@container style(--score < 60) {
  data {
    color: red;
  }
}

通俗易懂,三岁小孩也能知道是什么意思。

效果图参考:

数字,金色传说

同样的,这个例子也有演示页面,您可以狠狠地点击这里:CSS样式查询range范围匹配分数颜色demo

四、其实使用普通属性也是可以的

Chrome浏览器已经支持全属性attr()函数语法了,所以,样式匹配的时候,普通属性也是可以支持的。

例如:

<span class="score" data-score="95">
  <data>95</data>
</section>
<span class="score" data-score="85">
  <data>85</data>
</section>
<span class="score" data-score="65">
  <data>65</data>
</section>
<span class="score" data-score="35">
  <data>35</data>
</section>
.score {
  --score: attr(data-score type(<number>));
}
@container style(--score >= 90) {
  data {
    color: gold;
  }
}
/* 这部分代码都是一样的,略 */

最终的效果效果是一样的:

数字,金色传说

五、我想匹配容器自身

只要是@container容器匹配,无论是尺寸查询、样式查询、滚动查询还是锚点查询,都是只能对后代元素进行设置,容器本身是无法直接匹配。

但是,对于样式查询而言,是有曲线救国的方案的,那就是使用 if() 函数。

所以,上面的分数高亮案例,HTML代码可以进一步简化:

<data class="score" value="95"></data>
<data class="score" value="85"></data>
<data class="score" value="65"></data>
<data class="score" value="35"></data>

更语义,更原生,更简洁。

CSS代码同样简单了很多:

.score {
  --score: attr(value type(<number>));
  &::before {
    content: attr(value);
  }
  color: if(
    style(--score >= 90): gold;
    style(--score >= 80): green;
    style(--score >= 60): orange;
    else: red;
  );
}

还是相当nice的!

if函数实现的数字颜色

如果您正在使用的是Chrome浏览器,那么您可以狠狠地点击这里:CSS样式查询if()函数匹配容器自身

六、样式查询也支持尺寸设置

style()函数里面也可以设置min-width,或者height等尺寸设置。

例如:

@container style(min-width: 200px) { … }

由于@container本身就支持尺寸查询,故而这个不可替代性一般,其典型应用还是CSS变量匹配。

哦呵呵~

七、2025年就这样结束啦

2025年就这样结束啦,这一年前端依然在不断进步,如果大家注意看我的更新的新特性,7~8成都是CSS新特性,倒不是我刻意筛选,真特么就是CSS特性比DOM和JS特性多。

2024年年底的时候,绝对想不到CSS会有上面这样的写法。

要是这一年不跟着学习。

CSS就会变成你不认识的样子。

相比之下,JavaScript还是当年的样子,满屏的Promise,async和await和箭头函数,其他大同小异。

CSS则是从书写到特性都大变样了。

展望2026,新的一年,还是要持续学习的啦,加油!

加油!

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

(本篇完)

补全不足,CSS锚点定位支持锚定容器回退检测了

作者 张 鑫旭
2025年12月22日 09:00

by zhangxinxu from https://www.zhangxinxu.com/wordpress/?p=11972
本文可全文转载,但需要保留原作者、出处以及文中链接,AI抓取保留原文地址,任何网站均可摘要聚合,商用请联系授权。

一、CSS锚点定位的不足

CSS锚点定位是非常强大且实用的CSS新特性,这个特性我去年就介绍过,参见“全新的CSS Anchor Positioning锚点定位API

虽然强大,但不完美。

这个问题我在升级LuLu UI的Select组件的的时候也遇到过,看下图一目了然:

下拉圆角问题

看箭头指示的那里,圆角是在上面的,实际上,当列表朝上的时候(底部空间不足),圆角应该在左下方和右下方。

为什么会出现这种情况,那就是因为CSS锚点定位使用候补定位的时候,开发人员是不知道的,就没有在使用候补定位的时候进行专门的处理。

在上面的例子中,LuLu UI下拉框朝上的样式处理最后还是放弃了锚点定位的候补位置方法。

不过,现在,Chrome 143+新支持了一个特性,可以实现锚定容器回退检测了。

二、回退检测语法的使用

CSS锚点定位的回退检测使用很简单。

第一步,设置容器类型为anchored,同时指定自动回退的方位类型,这个对应的CSS属性是position-try-fallbacks属性,例如设置边界自动垂直翻转:

.float-element {
  position-try-fallbacks: flip-block;
  container-type: anchored;
}

第二步,使用@container anchored()函数进行匹配,示意:

@container anchored(fallback: flip-block) {
  .float-element {
    /* 如果垂直定位方向改变,如何如何…… */
  }
}

就可以了!

三、fallback案例

最好的学习方法还是案例,您可以狠狠地点击这里:CSS锚点定位回退检测与tooltip效果实现demo

在默认状态下,黑色提示框是在上面的,箭头朝下。

随着滚动进行,当提示框触碰到浏览器上边缘的时候,提示框的位置就会使用候补位置,垂直翻转,此时,大家可以看到箭头位置朝上了,这就是用的新的语法实现的。

以上效果需要Chrome 143+浏览器支持,如果您的浏览器不满足条件,也可以查看下面的GIF录屏效果,体会我说的意思。

边缘自动翻转GIF

实现代码

HTML代码如下:

<span class="tooltip">我是提示内容</span>
<button class="anchor">我是按钮</button>

CSS完整代码:

.anchor {
  anchor-name: --my-anchor;
}

/* 提示浮层元素 */
.tooltip {
  position: fixed;
  margin-top: 1rem;
  position-anchor: --my-anchor;
  position-area: bottom;
  /* 垂直方向翻转 */
  position-try-fallbacks: flip-block; 
  /* 设置为锚点查询容器类型 */
  container-type: anchored;

  /* 向上小三角效果 */
  &::before {
    content: '';
    position: absolute;
    bottom: 100%; 
    inset-inline: 0;
    width: 1em;
    margin-inline: auto;
    aspect-ratio: 3/2;
    clip-path: polygon(50% 0, 100% 100%, 0% 100%);
    background: inherit;
  }
  background: #121212;
  color: #fff;
  padding: 4px 8px;
}

/* 如果触发了垂直翻转 */
@container anchored(fallback: flip-block) {
  .tooltip::before {
    /* 小三角翻转,定位调整 */
    scale: 1 -1;
    bottom: auto;
    top: 100%;
  }
}

四、点到为止、结语

关于CSS锚点定位容器查询,本文就先说这么多,不做进一步展开,为什么呢?

很简单,兼容性还很一般,目前Chrome 143才支持,而Chrome 143就是最近的正式版本。

锚点位置查询兼容性

考虑到我现在的年纪,不知道有生之年,有没有可能在正式项目中使用这个特性。

嘿嘿!

嘿嘿一笑

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

(本篇完)

CSS progress()函数简介

作者 张 鑫旭
2025年12月12日 19:15

by zhangxinxu from https://www.zhangxinxu.com/wordpress/?p=11969
本文可全文转载,但需要保留原作者、出处以及文中链接,AI抓取保留原文地址,任何网站均可摘要聚合,商用请联系授权。

一、progress()函数语法

最近CSS又出了个新的函数,名叫progress(),返回0-1的进度值,我研究了下,这个函数还是有一定的实用性的。

我们先看下他的语法:

progress(<value>, <start>, <end>)

这个函数支持3个参数,分别是:

value
当前进度值
start
范围起始值
end
范围结束值

因此:

  • progress(300, 0, 1000)的返回值就是0.3
  • progress(50px, 0px, 100px)的返回值就是0.5
  • progress(50%, 30%, 80%)的返回值就是……这个要计算下:(50% - 30%) / 80%,结果是0.25

需要注意的是,progress()的各个参数的计算值类型需要一致,如果一个是时间值,一个是长度值,或者一个是数值,一个是单位值,是不合法的,下面这两个progress()语句就是非法的:

progress(3s, 0px, 100px)
progress(3em, 0, 100)

二、progress()场景应用

在实际开发中,progress()函数都是与CSS变量,或者是CSS相对单位结合使用的,因为只有此时,其返回值才是动态的,才有使用的意义,否则,类似progress(300, 0, 1000)这样的固定值,还不如直接写个0.3来得快活呢!

例如:

// CSS变量
progress(var(--container-width), 320, 1200)
// 相对单位
progress(100vw, 360px, 1024px)

案例

官方案例都是使用vw单位,但是这个不太方便看文章的小伙伴体验。

所以,我打算使用cqw单位。

HTML如下,一个容器,里面有一个图片:

<div class="container">
  <img src="mybook.jpg" alt="CSS新世界">
</div>

此时,我们就可以使用progress()函数,让图片的宽度基于容器尺寸动态变化,CSS代码如下:

.container {
  padding: 10px;
  border: dashed gray;
  container-type: inline-size;
  overflow: hidden;
  resize: both;
  
  img {
    width: calc(100px + 200px * progress(80cqw, 150px, 800px));
  }
}

80cqw表示.container容器元素80%的宽度,然后这个宽度计算值和150px-800px范围计算进度,最终,图片的尺寸会在100px~300px变化。

实时渲染效果如下所示(需要Chrome 138+)(拖拽右下角的resize拖拽条):

CSS新世界

三、兼容性以及其他

progress()使用还挺灵活的,函数参数支持其他常见的CSS运算函数,例如calc()计算:

progress(calc(20 + 30), 0, 100)

也可以是calc()函数的参数,例如:

calc((progress(var(--container-width), 20%, 80%) / 2) + 0.5)

支持和其他CSS函数,例如random()clamp()混合使用。

总之,哪里需要他,就使用他。

边界特性

7年前,我写过一篇文章“CSS文字和背景color自动配色技术简介”,利用的是opacity的边界特性实现的,比较取巧。

现在有了progress()函数,那就比较正统了,也更容易理解了。

例如:

// 亮度大于0.5,颜色黑色
// 亮度小于 0.5,颜色白色
color: hsl(0, 0%, calc(100% * progress(
  calc((0.5 - var(--lightness)) * infinity), 0, 1))
);

要是对infinity关键字感兴趣,可以访问这篇文章:“了解infinity、pi等CSS calc()计算关键字

兼容性

兼容性一般,目前Chrome Only!

progress()兼容性

目前还只是个玩具。

结语

progress() 是 CSS 中用于表示进度值的特殊函数(属于 CSS 数值函数范畴),核心作用是将「0~1 范围内的进度值」映射为可视化的数值 / 颜色 / 尺寸等,常用于进度条、动态过渡、动画关键帧等场景,是 CSS 原生实现进度关联样式的核心工具。

不过目前受制于兼容性,暂时无法大规模使用,大家了解即可!

最后,我家白衣慕沛灵希望大家多多点赞转发。

白衣慕沛灵

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

(本篇完)

单IMG标签的图片内阴影效果实现

作者 张 鑫旭
2025年12月4日 17:11

by zhangxinxu from https://www.zhangxinxu.com/wordpress/?p=11969
本文可全文转载,但需要保留原作者、出处以及文中链接,AI抓取保留原文地址,任何网站均可摘要聚合,商用请联系授权。

一、内阴影不难实现

如果只是实现个内阴影效果,并不难,嵌套大法就可以了。

<div class="shadow-inset">
  <img src="follow-me.jpg" alt="最会钓鱼的程序员">
</div>
.shadow-inset {
  box-shadow: inset 2px 2px 6px #0009;
  display: flex;
  width: fit-content;
  border-radius: 1rem;
  img {
    width: 200px;
    position: relative;
    z-index: -1;
  }
  overflow: hidden;
}

渲染效果如下图所示:

图片内阴影效果实现示意

下面问题来了,若是只有一个IMG元素,如何给图片实现内阴影效果呢?

二、单IMG图片的内阴影效果

<img>元素是替换元素,内阴影属于装饰性效果,在Web中,内容的层叠顺序是高于内阴影的,因此,<img>元素设置内阴影是看不到效果的,因为被图像挡住了,除非?

1. 如果图像背景正好是纯色

如果我们的图片的背景是纯色,我们可以使用padding撑开间距,让阴影显示出来。

还是上面的那个示意图,正好背景色是纯色的,于是,我们就可以:

<img src="follow-me.jpg" class="shadow">
.shadow {
  width: 200px;
  padding: 8px;
  background: rgb(66,127,178);
  box-shadow: inset 2px 2px 6px #0009;
  border-radius: 1rem;
}

实时渲染效果如下:

可实际场景下,大部分的图片,尤其是需要使用内阴影效果的图片,都不会是纯色背景,因此,上面的方法是行不通的,此时该怎么办呢?

2. attr()新语法

这个方法的原理是,隐藏原本的图片内容,然后图片作为背景图显示,这个就需要用到全新的attr()全属性语法,关于此特性,强烈建议了解下,详见:“震惊,有生之年居然看到CSS attr()全属性支持”。

使用示意:

<img src="follow-me.jpg" class="shadow2">
.shadow2 {
  width: 0;
  box-shadow: inset 2px 2px 6px #0009;
  border-radius: 1rem;
  padding: 112px 100px;
  background: image-set(attr(src)) no-repeat center / contain;
}

如果你是Chrome浏览器,应该就可以看到效果了,实时渲染如下:

此方法,虽然巧妙,但是有兼容性的问题,目前还无法再生产环境使用。

那有没有什么兼容性好,同时适用场景广泛的方法呢,有,SVG滤镜!

3. SVG滤镜实现内阴影

这个实现非常简单,只需要在页面任意位置插入这么一段SVG代码:

<svg width="0" height="0">
  <filter id="shadowInset">
    <feOffset in="SourceAlpha" dx="2" dy="2"></feOffset>
    <feGaussianBlur stdDeviation="6"></feGaussianBlur>
    <feComposite in="SourceAlpha" operator="out"></feComposite>
    <feBlend in2="SourceGraphic"></feBlend>
  </filter>
</svg>

然后给对应的图片元素应用#shadowInset滤镜就可以了。

例如:

<img src="follow-me.jpg" class="shadow3">
.shadow3 {
  width: 200px;
  border-radius: 1rem;
  filter: url(#shadowInset);
}

就可以看到如下图所示的效果了:

三、收工打道回府

OK,以上就是本文的全部内容。

估计用不了多久就能变成AI的养料了,愁人,写了没人看,被AI弄过去,也不知道这些东西是我写的。

周周更新图个啥。

蒜鸟蒜鸟,不唠叨了,若是大家觉得内容不错,欢迎转发哈。

蒜鸟蒜鸟

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

(本篇完)

醒醒,该使用CookieStore新建和管理cookie了

作者 张 鑫旭
2025年11月28日 16:16

by zhangxinxu from https://www.zhangxinxu.com/wordpress/?p=11954
本文可全文转载,但需要保留原作者、出处以及文中链接,AI抓取保留原文地址,任何网站均可摘要聚合,商用请联系授权。

一、为什么要抛弃传统Cookie操作?

在前端开发的长河中,Cookie始终扮演着重要角色。

从用户身份识别到状态维持,它是浏览器与服务器之间轻量通信的核心载体。

但长期以来,我们操作Cookie的方式始终停留在通过document.cookie拼接字符串的“原始阶段”,不仅代码冗余易出错,还无法应对异步场景下的复杂需求。

比方说,我们要设置一个Cookie,需要先获取,然后再手动进行键值对+属性的字符串拼接,例如:

document.cookie = "name=zhangxinxu; max-age=3600; path=/"

这就很麻烦,属性顺序、符号格式稍有偏差就会导致失效。

宋玉摊手

然后,我们要获取Cookie的时候,也是先获取,再字符串split分隔,再去匹配,代码冗长,还不支持批量操作。

比方说,我想一次性删除多个符合条件的Cookie,需循环遍历逐个处理。

正是因为上面这些不足,CookieStore API才应运而生。

它将Cookie操作封装为标准化的异步方法,让Cookie管理变得简洁、可控。

二、CookieStore对象特性概览

CookieStore是浏览器提供的现代API,隶属于Window对象,用于以异步、面向对象的方式操作Cookie。

包含的方法名如下所示:

// 删除cookie
cookieStore.delete()

// 获取cookie
cookieStore.get()

// 获取所有cookie
cookieStore.getAll()

// 设置cookie
cookieStore.set()

以上这些方法都是基于Promise设计,支持对所有Cookie属性进行精准控制,同时提供了监听Cookie变化的能力,使用示意:

// cookieStore也是全局的
cookieStore.addEventListener("change", (event) => {
  console.log("cookie改变");
});

兼容性

CookieStore已被主流浏览器全面支持,如下截图所示:

CookieStore的兼容性

对于需要兼容低版本浏览器的场景,可通过@ungap/cookie-store等polyfill库补全功能,无需担心技术落地问题。

if (!'CookieStore' in window) {
  // 使用降级方案
}

三、CookieStore常见使用示意

CookieStore的API设计极为简洁,核心围绕“增、删、改、查、监听”五大场景,所有方法均返回Promise,支持async/await语法。

1. 新建/修改Cookie:set()方法

set()方法是CookieStore的核心,既可以新建Cookie,也可以修改已有Cookie(通过键名匹配)。

它支持两种参数格式:键值对+配置对象,或包含完整信息的单个对象。

// 方式1:键名、键值与配置分离
async function setUserCookie() {
  await cookieStore.set(
    'username', // 键名
    'zhangxinxu', // 键值
    {
      maxAge: 3600, // 存活时间(秒),替代传统expires
      path: '/', // 作用路径
      domain: 'zhangxinxu.com', // 作用域
      secure: true, // 仅HTTPS下生效
      sameSite: 'strict' // 防止CSRF攻击,可选strict/lax/none
    }
  );
  console.log('Cookie设置成功');
}
// 方式2:单个对象参数(更推荐,结构清晰)
async function setTokenCookie() {
  await cookieStore.set({
    name: 'token',
    value: '欢迎购买我的新书:html并不简单...',
    expires: new Date(Date.now() + 24 * 60 * 60 * 1000), // 过期时间(Date对象)
    secure: true,
    sameSite: 'lax'
  });
}

注意:当设置的Cookie键名已存在时,set()方法会自动覆盖原有Cookie,无需手动删除;maxAgeexpires二选一即可,maxAge以秒为单位,更符合前端开发习惯。

2. 读取Cookie:get()与getAll()方法

读取Cookie时,get()用于获取指定键名的单个Cookie,getAll()用于获取所有符合条件的Cookie(支持按path、domain过滤),彻底告别字符串拆分的麻烦。

// 读取单个Cookie
async function getUserCookie() {
  const cookie = await cookieStore.get('username');
  if (cookie) {
    console.log('用户名:', cookie.value);
    console.log('过期时间:', cookie.expires);
  } else {
    console.log('Cookie不存在');
  }
}

// 读取所有Cookie
async function getAllCookies() {
  // 获取所有Cookie
  const allCookies = await cookieStore.getAll();
  console.log('所有Cookie:', allCookies);

  // 按路径过滤Cookie
  const rootCookies = await cookieStore.getAll({ path: '/' });
  console.log('根路径Cookie:', rootCookies);
}

返回的Cookie对象包含namevalueexpirespathdomain等完整属性,可直接用于业务逻辑处理,无需二次解析。

3. 删除Cookie:delete()方法

删除Cookie时,只需指定Cookie的键名及对应的pathdomain(需与设置时一致,否则无法匹配),操作简洁且不易出错。

async function deleteUserCookie() {
  await cookieStore.delete('username', {
    path: '/',
    domain: 'example.com' // 若设置时指定了domain,删除时必须一致
  });
  console.log('Cookie删除成功');
}

注意:若删除时未指定pathdomain,默认匹配当前页面的pathdomain,若与设置时的属性不匹配,会导致删除失败,这一点与传统Cookie操作规则一致。

4. 监听Cookie变化:addEventListener()

CookieStore最强大的特性之一是支持监听Cookie的变化(新增、修改、删除),这在需要实时响应Cookie状态的场景(如登录状态同步)中极为实用。

// 监听Cookie变化
function listenCookieChange() {
  cookieStore.addEventListener('change', (event) => {
    console.log('Cookie变化类型:', event.changed.length ? '新增/修改' : '删除');
    // 变化的Cookie列表
    event.changed.forEach(cookie => {
      console.log('修改的Cookie:', cookie.name, '->', cookie.value);
    });
    // 删除的Cookie列表
    event.deleted.forEach(cookie => {
      console.log('删除的Cookie:', cookie.name);
    });
  });
}

// 初始化监听
listenCookieChange();

当页面中任何Cookie发生变化时,change事件都会被触发,通过event.changedevent.deleted可清晰区分变化类型,实现登录状态同步、权限更新等场景的无缝衔接。

宋玉抛媚眼

四、CookieStore的核心优势总结

对比传统的document.cookie,CookieStore的优势堪称降维打击:

  1. 异步非阻塞:基于Promise的异步操作,避免同步操作阻塞页面渲染,提升前端性能。
  2. 语法简洁直观:告别字符串拼接与拆分,通过方法调用实现精准操作,代码可维护性大幅提升。
  3. 完整的错误处理:支持try/catch捕获操作异常,如设置Cookie时的格式错误、跨域Cookie访问限制等,排错更高效。
  4. 强大的监听能力:实时响应Cookie变化,无需轮询,简化登录、权限等场景的业务逻辑。
  5. 批量操作支持:通过getAll()实现批量读取,配合过滤条件可精准获取目标Cookie。

宋玉噤声手势

五、使用注意事项与最佳实践

尽管CookieStore优势显著,但在使用过程中仍需注意这些细节:

  • 同源策略限制:与传统Cookie一致,CookieStore无法访问跨域Cookie,仅能操作当前域名下的Cookie。
  • Secure属性要求:当Cookie设置为secure: true时,仅能在HTTPS协议下操作,本地开发时可使用localhost绕过该限制。
  • SameSite属性配置:为防止CSRF攻击,建议设置sameSite:'strict''sameSite:'lax',若需要跨域访问Cookie,可设置为sameSite:'none'(需配合secure:true)。
  • 兼容性处理:针对不支持CookieStore的浏览器,可使用polyfill库兜底,代码示例如下:
// 引入polyfill(通过CDN或npm安装)
<script src="https://cdn.jsdelivr.net/npm/@whatwg-node/cookie-store@0.2.3/cjs/index.min.js"></script>

或者:

// 封装兼容方法
async function getCookie(name) {
  if ('CookieStore' in window) {
    return (await cookieStore.get(name))?.value;
  } else {
    // 传统方式兜底
    const match = document.cookie.match(new RegExp(`(^| )${name}=([^;]+)`));
    return match ? match[2] : null;
  }
}

六、结语

document.cookie的字符串拼接,到CookieStore的异步化、标准化操作,前端Cookie管理的效率实现了质的飞跃。

CookieStore不仅解决了传统方式的诸多痛点,还通过监听能力拓展了Cookie的应用场景,是现代前端开发中值得优先采用的技术方案。

无论是简单的登录状态存储,还是复杂的权限管理场景,CookieStore都能以简洁、高效的方式满足需求。

现在就拿起这项技术,告别繁琐的Cookie操作,让代码更优雅、更易维护吧!

宋玉飞吻

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

(本篇完)

巧用CSS ::details-content伪元素实现任意展开动画

作者 张 鑫旭
2025年11月24日 15:51

by zhangxinxu from https://www.zhangxinxu.com/wordpress/?p=11900
本文可全文转载,但需要保留原作者、出处以及文中链接,AI抓取保留原文地址,任何网站均可摘要聚合,商用请联系授权。

一、温故而知新

<details><summary>元素我很早就介绍过了,看了一下,居然是2018年,七年前,奶奶的,时间过得也太快了,长叹一口气。

宋玉 叹气

有兴趣访问这里:“借助HTML5 details,summary无JS实现各种交互效果

本以为这个HTML元素已经很成熟了,结果在接下来的岁月中,其又发生了一些迭代与变化。

首先,哪个三角箭头改为::marker伪元素,和<ul><ol>这些元素的项目符号保持一致。

然后,支持name属性,也就是如果多个<details>元素使用同一个name属性值,那么这些<details>元素就会产生关联,每次最多只会展开一个<details>元素,可以实现手风琴这样的展开与收起交互效果。

然后,支持内容元素hash匹配自动展开(本文后面会有案例)。

再到本文要重点介绍的::details-content伪元素,可以匹配内容区域的Shadow DOM元素(见下图示意),目前最具代表性的应用就是实现展开与收起的动画效果。

details-content匹配示意

二、::details-content让任意尺寸展开动画

直接看案例,下面的效果为实时渲染(点击下面的小三角标题):

//zxx: 如果浏览器版本不足,会看不到动画效果

欢迎关注我的抖音

钓鱼账号:最会钓鱼的程序员

技术账号:张鑫旭本人

相关代码如下所示:

<details>
  <summary>欢迎关注我的抖音</summary>
  <p><strong>钓鱼账号:</strong>最会钓鱼的程序员</p>
  <p><strong>技术账号:</strong>张鑫旭本人</p>
</details>

CSS样式部分:

::details-content {
  transition: height 0.5s ease, content-visibility 0.5s ease allow-discrete;
  height: 0;
  overflow: clip;
}

details {
  interpolate-size: allow-keywords;
}

[open]::details-content {
  height: auto;
}

一些说明

  • <details>元素的显隐是通过content-visibility属性(内容隐藏,锚点匹配显示)控制的,所以transition的关键字值之一就是content-visibility属性。
  • allow-discrete也是新特性,可以让离散的CSS属性也支持transition过渡效果,例如display属性。详见我之前撰写的这篇文章:“CSS transition-behavior让display none也有动画效果
  • interpolate-size:allow-keywords可以让auto尺寸也能transition过渡效果,这个之前也介绍过,参见“这啥?CSS calc-size和interpolate-size,真学不动了”一文。

上述效果属于渐进增强特性,浏览器不支持也没关系,不影响展开与收起,所以大家放心使用。

宋玉端坐

三、锚点匹配与自动展开

这个其实也是新的特性,之前没有的,不过这个新特性比较隐蔽。

那就是,如今呢,我家主人已经结成元婴……错了错了,串场了,如今呢,只要<details>元素的内容被URL的hash锚点匹配,那么,<details>元素的会自动展开。

例如:

<details>
    <summary>欢迎关注我的抖音</summary>
    <p id="account1"><strong>钓鱼账号:</strong>最会钓鱼的程序员</p>
    <p id="account2"><strong>技术账号:</strong>张鑫旭本人</p>
</details>

<p>
    <a href="#account1">关注钓鱼账号</a>
    <a href="#account2">关注技术账号</a>
</p>

点击链接,则会看到展开效果,如下GIF录屏示意:

锚点匹配打开details元素示意

这个效果其实有些类似之前介绍过的hidden="until-found",有兴趣的可以点击这里进行访问,他也是使用的content-visibility隐藏,同样是锚点匹配,或者被搜索匹配,就会显示,我怀疑这是content-visibility隐藏内容公用特性。

宋玉微笑

补充小技巧

锚点定位会触发页面的滚动,并将匹配的元素定位在浏览器的上边缘。

这就会有个问题,会将<summary>元素的内容定位在屏幕之外,导致看不到,影响体验。

此时,可以使用CSS scroll-margin-block-start属性进行调整,例如:

details :target {
  scroll-margin-block-start: 6em;
}

此时,定位的滚动位置会距离上边缘6em大小。

四、其他补充信息碎碎念

最后来看一下::details-content伪元素的兼容性,今年所有现代浏览器都已经支持:

::details-content伪元素的兼容性

看起来像是约好了的,几乎都是同一时间支持的。

语法参考:

selector::details-content

使用示意:

details[open]::details-content {
  /* 样式,CSS属性基本上都支持 */
}

其他碎碎念

人果然是赚不到认知以外的钱的。

记得3年前,疫情刚结束那会儿,还在和我老婆说,我们什么时候也能遇到08年金融危机那种资产大抄底的时候就好了,我老婆也表示赞同。

结果,这一年,我们就把闲钱用来在临港买了套房投资。

尼玛,现在跌到贷款还完都没有多余钱的地步了。

也就是那种资产大跌的时刻其实就在眼前,但是,眼界和认知不足,我们两人完全没有意识到这种情况,要是那时候持有现金,现在换个大房子都没什么压力。

但是,我们又比普通人好一些,风险意识强,量力而为,杠杆小,加上前两年股市低迷的时候,重仓了基金,目前清了70%多,还算游刃有余。

这也符合目前我在整个社会阶层的位置,看明白了这一点,其实心态还是很平和的,人最重要的还是认清自己。

好了,就扯这多吧,如果觉得内容还不错,欢迎分享,点赞,转发!

宋玉飞吻

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

(本篇完)

介绍下与CSS自定义组件相关的:state()函数

作者 张 鑫旭
2025年11月17日 15:48

by zhangxinxu from https://www.zhangxinxu.com/wordpress/?p=11897
本文可全文转载,但需要保留原作者、出处以及文中链接,AI抓取保留原文地址,任何网站均可摘要聚合,商用请联系授权。

一、你需要提前知道的

Web Components组件开发又新增新特性,为:state()伪类函数。

4年多前,有介绍过::part()伪元素函数,可以穿透组件内部,对里面的样式进行设置。

详见这篇文章:“使用::part伪元素改变Shadow DOM的CSS样式

:state()伪类函数,则是穿透组件内部,匹配状态进行样式设置。

你可以理解为,::part()伪元素函数更像是 [disabled] 这种属性选择器,需要内部HTML有对应的属性设置(通过设置part属性)才能匹配,但是:state()伪类函数更像是 :disabled 这类伪类选择器,看中的是元素的真实状态,而非是否有这个属性。

attachInternals方法

那如何给组件设置状态,让:state()伪类函数匹配呢?这个就需要用到今年年初介绍的attachInternals方法。

可以访问这里了解:“研究下attachInternals方法,可让普通元素有表单特性

例如:

const someInternals = someCustomElement.attachInternals() 

而这个someInternals就是个ElementInternals对象,其包含一个名为states的属性(见下图)。

status语法

states属性的返回值是一个CustomStateSet对象,包含以下一些属性和方法:

// 属性
CustomStateSet.size
// 方法
CustomStateSet.add()
CustomStateSet.clear()
CustomStateSet.delete()
CustomStateSet.entries()
CustomStateSet.forEach()
CustomStateSet.has()
CustomStateSet.keys()
CustomStateSet.values()

我们日常开发,用的比较多的讲就是add()方法添加状态,delete()方法删除状态。

二、:state()函数案例

一例胜千言,让我想想,弄个什么案例好呢……要经典,又要具有代表性,嗯……算了,随便示意下效果吧。

JS和CSS代码如下所示:

class UiLoading extends HTMLElement {
  constructor() {
    super();

    const internals = this.attachInternals();
    
    this.addEventListener('click', () => {
      internals.states.add('loading');
    
      setTimeout(() => {
        internals.states.delete('loading');
      }, 3000);
    });
    
  }

  connectedCallback() {
    this.click();
  }
}
// 定义自定义组件
if (!customElements.get('ui-loading')) {
  customElements.define('ui-loading', UiLoading);
}
ui-loading {
  display: inline-grid;
  place-items: center;
  width: 150px; height: 150px;
  border: 2px dashed;
}
ui-loading:state(loading) {
  border-color: red;
  background: lightgreen;
}

此时,只要页面上有 <ui-loading> 元素,就可以看到样式变化了。

默认是绿底红框,几秒钟时候,就是黑框了。

实时效果如下所示,如果看不到效果,请点击下面的框框。

可以看到,:state()函数的设计初衷就是为了方便暴露组件内部的状态。

例如,选中与否,失败与否,加载与否等。

三、兼容性与结语

CSS :state()伪类函数在2024年5月份的时候,已经被所有现代浏览器都支持,大规模应用的时机还不成熟,不过不得不说,这个东西可以提高组件开发的档次感(虽然part属性和::part()函数也能实现类似效果,但没有这个看起来高大上)。

:state()函数兼容性

总结

:state() 伪类是未来 Web 组件样式化的一个强大且语义化的工具。它将样式控制的逻辑从“如何渲染”(暴露内部部件)转变为“在什么状态下渲染”,使得组件开发者能更好地封装内部结构,同时为组件使用者提供清晰、强大的样式定制能力。

吐槽

昨天遇到个难受的事情,我鱼竿包里翻来覆去,我的一根鱼竿不见了,达亿瓦一击枫,是我买的价格比较高的杆子之一,用了很多年了,也比较有感情了。

思来想去,是上周在情人谷垂钓园钓鲫鱼的时候,没有及时收起来,搁在了河边,走的时候,只收了另外一只杆子,同时损失的还有一只浮漂。

好难受,钓鱼这么多年,还是头一次丢杆子。

打电话给老板,老板也没见到,估计被人捡走了。

郁闷!

鱼没有钓到几条,杆子没了,我都不敢告诉家里人,不然又要吐槽丢三落四,只能悄悄又买了个新杆子。

舍不得买贵的,就买了个200不到的,唉……

哭泣

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

(本篇完)

CSS锚点定位实战-鼠标跟随交互效果

作者 张 鑫旭
2025年11月10日 17:54

by zhangxinxu from https://www.zhangxinxu.com/wordpress/?p=11886
本文可全文转载,但需要保留原作者、出处以及文中链接,AI抓取保留原文地址,任何网站均可摘要聚合,商用请联系授权。

一、前言之锚点定位简介

锚点定位去年出来的时候就详细介绍过,参见“全新的CSS Anchor Positioning锚点定位API”,非常强大,非常实用。

能够解决overflow剪裁定位元素的问题,包含自动调整方向,尺寸自动适应等特性。

基本上任何浮层元素之间的定位都不再需要JavaScript代码的参与。

我看了下兼容性,所有现代浏览器目前均已支持。

anchor-name兼容性

不过,上述文章更多的理论知识介绍,下面展示下其一个比较基础,但又比较实用的小应用,列表类选项悬停或者选中的跟随动画效果。

二、案例1:选中态标识动效

直接先看效果,点击下面的单选框(点击文字也可以选中),就可以看到后面有个绿色的勾勾出现,切换的时候,就会动画效果移动过去。

博主的钓鱼抖音账号名称是:

相关的HTML和CSS代码如下所示:

<fieldset class="fieldset">
    <legend>博主的钓鱼抖音账号名称是:</legend>
    <p>
        <input type="radio" id="follow" name="follow" value="1">
        <label for="follow">最会钓鱼的程序员</label>
    </p>
    <p>
        <input type="radio" id="follow2" name="follow" value="2">
        <label for="follow2">张鑫旭本人</label>
    </p>
    <p>
        <input type="radio" id="follow3" name="follow" value="3">
        <label for="follow3">其他</label>
    </p>
</fieldset>
.fieldset::before {
    content: '✓';
    position: absolute;
    display: none;
    color: green;
    transition: 0.5s ease;
    position-anchor: --checked;
    top: anchor(center);
    left: calc(anchor(right) + 5px);
    translate: 0 -50%;
}
.fieldset:has(:checked)::before {
    display: block;
    animation: fade-in 0.2s ease-in-out;
}
.fieldset :checked + label {
    anchor-name: --checked;
}
@keyframes fade-in {
    0% { opacity: 0; }
    100% { opacity: 1; }
}

实现原理说明

首先,使用伪元素创建绿色的勾勾,默认隐藏。

当有单选项选中的时候,显示,这里的显示还使用了淡出动画效果。

绿色的勾勾设置锚点定位,定位对象的名称就是--checked

然后,只需要当单选项选中的时候,设置后面的<label>元素的锚点名称是--checkedanchor-name: --checked),这个勾勾元素,就会自动定位过去。

都坐下,基操勿六。

慕沛灵坐下

三、案例2:菜单hover悬停跟随效果

实时效果如下(如果没有效果,请升级浏览器或者系统):

  • 联系博主
  • 欢迎关注
  • 点赞
  • 分享
  • 想了下,还是录了个GIF示意了下:

    鼠标悬停背景跟随

    宽度自动适应,无需JS计算,CSS伪元素创建背景内阴影,无需额外的HTML占用。

    完整的代码如下所示:

    <menu id="menu">
        <li><a href="##">联系博主</a></li>
        <li><a href="##">欢迎关注</a></li>
        <li><a href="##">点赞</a></li>
        <li><a href="##">分享</a></li>
    </menu>

    CSS部分:

    menu {
        display: flex;
        align-items: center;
        list-style: none;
        padding: 0;
        margin: 0;
        background-color: #333;
        width: 320px;
        /* 动效元素创建 */
        &::before {
            content: '';
            position: absolute;
            position-anchor: --anchor-select;
            left: anchor(left);
            top: anchor(top);
            width: anchor-size(width);
            height: calc(1.5em + 12px);
            transition: .3s ease-in-out;
            box-shadow: inset 0 0 8px color-mix(in srgb, lightblue 80%, transparent);
            pointer-events: none;
        }
        li {
            flex: auto;
            line-height: 1.5;
        }
        a {
            display: block;
            padding: 6px 10px;
            text-decoration: none;
            color: #fff;
            text-align: center;
        }
        :where(&:not(:hover)) .selected,
        a:hover {
            anchor-name: --anchor-select;
        }
    }

    然后几行JS模拟点击选中的状态:

    document.getElementById('menu').onclick = function (e) {
        e.preventDefault();
        this.querySelector('.selected')?.classList.remove('selected');
        e.target.classList.add('selected');
    }

    就结束了,实际上还是比较简单的。

    如果浏览器不支持?

    如果浏览器不支持,那我们就直接当前菜单元素创建个内阴影就好了,不影响功能。

    也就是,锚点定位其实可以作为增强特性使用,不会影响现有的实现,赶快在现在的项目用起来吧。

    慕沛灵斟茶

    四、结束语,再聊AI

    今天遇到个奇怪的事情,我在本地创建demo的时候,AI居然自动把我的抖音钓鱼账号给吐出来了,我靠,怎么回事?

    我的抖音账号

    最近使用AI的频率更多,更熟练了,会员也买多了,什么WPS会员,剪映SVIP,都是因为一些AI功能。

    还有就是最近的校招……嗯,这个不能说,过。

    项目开发也用得比较多,特别是组件提取,搭好框架之后的填充,都大大提高了效率。

    还有就是项目发布时候的报错,以前那些Node报错看得我头大,显然粘贴给AI,分分钟找到问题所在,比方说最近的安装Prettier导致发布出错,AI在某个地方加了个 | exit 0解决了,要是我自己,没有个半天,是搞不定的。

    不过也有蛋疼的事情,比方说有一张图,我想要去掉水印,就让AI处理,结果AI打上了自己的水印,还真是……有趣。

    不过也有不足,我认为豆包的图像生成能力反而下降了,可能一致性这块提高了,但是最终效果总是想要的不同,比方说最近文章封面的生成,还有小说封面的生成,都远不如之前。

    行了,就说这么多吧。

    韩立与慕沛灵

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

    (本篇完)

    ❌
    ❌