阅读视图

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

借助mediabunny纯JS实现视频水印、剪裁、合成等功能

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

一、推荐使用mediabunny

大约2年前,我更新了大量的音视频处理的文章,不过里面的技术实现大都使用原生代码和WebCodecs API手搓的实现。

因为那个时候,WebCodecs API刚出来,技术还不成熟。

自然而然,就陆续出现了不少基于WebCodecs API封装的音视频处理框架。

经过这些年的发展,有一个媒体工具包异军突起,那就是mediabunny!

项目地址:https://github.com/Vanilagy/mediabunny

mediabubby logo

2个月前,我在微博介绍过的MP4/MOV视频转WebM格式在线工具就使用了此项目。

视频转格式截图

mediabunny的能力不仅仅在于视频格式转换与压缩,添加水印、时长剪裁等都不在话下,本文就通过我跑通的demo给大家看下这类需求该如何实现。

二、给视频添加水印

话不多说,先直接上手体验。

您可以狠狠地点击这里:纯前端实现视频添加水印效果demo

选择视频和需要的水印图片,就可以得到最终的效果了,如下截图所示:

水印合成效果示意

其中,最关键的合成就是下面这部分代码:

let ctx = null;
const conversion = await Conversion.init({
    input,
    output,
    video: {
        process: (sample) => {
            if (!ctx) {
                // 创建canvas
                const canvas = new OffscreenCanvas(
                    sample.displayWidth,
                    sample.displayHeight
                );
                ctx = canvas.getContext('2d');
            }

            ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
            sample.draw(ctx, 0, 0);
            ctx.drawImage(watermark, 80, 80 * watermark.naturalHeight / watermark.naturalWidth);

            return ctx.canvas;
        }
    }
});

使用Conversion.init解码视频的每一帧(sample),然后使用canvas将画面和水印图重新绘制,再返回当前canvas即可。

其实不仅是水印合成,任何画面特效,字幕添加,遮罩,尺寸设置都可以使用此方法实现,原理都是一样的,都是对图像进行处理。

三、Video视频前后剪裁

同样的,先看实现效果。

您可以狠狠地点击这里:纯前端实现视频首尾剪裁demo

选择任意的视频,然后拖选滑竿选择需要的视频片段,点击红色的剪裁按钮,就可以看到被剪裁后的视频了:

剪裁demo示意

实际生产环境,拖拽的应该是缩列图的左右两个小翅膀,这里为了简化使用,使用了LuLu UI的双滑块模拟。

其核心实现代码极为简单:

const input = new Input({
    formats: ALL_FORMATS,
    source: new BlobSource(videoFile),
});
const output = new Output({
    format: new Mp4OutputFormat(), // The format of the file
    target: new BufferTarget(),
});

const eleRange = range.querySelector('input');  
const conversion = await Conversion.init({
    input,
    output,
    trim: {
        start: eleRange.from,
        end: eleRange.to,
    },
});

await conversion.execute();

使用trim参数,指定起止时间就可以了。

完整代码可以访问演示页面。

四、多音频和画面的视频合成

一例胜千言,您可以狠狠地点击这里:纯前端实现画面加音频的视频合成demo

默认提供了字幕、背景图、台词,背景音乐可选,用户可以自己上传,可以合成最终的视频:

视频合成demo截图

我查了下API,mediabunny中似乎缺少AudioBuffer处理方法,当然,也可能有,我自己没找到。

这里的AudioBuffer剪裁和合并用的是我自己之前手搓的方法。

相关源码在页面左侧(移动端在下方)有完整展示,基本上相关的视频合成都可以实现了。

这里提供下核心实现部分:

// 定义一个视频合成输出
const output = new Output({
    format: new Mp4OutputFormat(), 
    target: new BufferTarget(),
});

// 添加视频轨道,画面源自canvas
const videoSource = new CanvasSource(canvas, {
    codec: 'avc',
    bitrate: QUALITY_HIGH,
});
output.addVideoTrack(videoSource);

// 添加音轨
const audioSource = new AudioBufferSource({
    codec: 'aac',
    bitrate: QUALITY_HIGH,
});
output.addAudioTrack(audioSource);

await output.start();

// 获取音频文件
const duration = audioFile.duration;

// 每秒30帧
for (let frame = 0; frame < 30 * duration; frame++) {
    draw(frame);
    await videoSource.add(frame / 30, 1 / 30);
}

// 获取音频的 audioBuffer……(代码略),然后添加
await audioSource.add(audioBuffer);

await output.finalize();

五、广告时间

推荐下我几年前在掘金上更新的人文类课程《技术写作指南》

技术写作指南

既是关于写作,也是关注个人成长!

OK,就说这么多,如果你觉得本文内容对你的工作与学习有所帮助,欢迎转发,点赞!

😉😊😇
🥰😍😘

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

(本篇完)

🔲 ☆

借助mediabunny纯JS实现视频水印、剪裁、合成等功能

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

一、推荐使用mediabunny

大约2年前,我更新了大量的音视频处理的文章,不过里面的技术实现大都使用原生代码和WebCodecs API手搓的实现。

因为那个时候,WebCodecs API刚出来,技术还不成熟。

自然而然,就陆续出现了不少基于WebCodecs API封装的音视频处理框架。

经过这些年的发展,有一个媒体工具包异军突起,那就是mediabunny!

项目地址:https://github.com/Vanilagy/mediabunny

mediabubby logo

2个月前,我在微博介绍过的MP4/MOV视频转WebM格式在线工具就使用了此项目。

视频转格式截图

mediabunny的能力不仅仅在于视频格式转换与压缩,添加水印、时长剪裁等都不在话下,本文就通过我跑通的demo给大家看下这类需求该如何实现。

二、给视频添加水印

话不多说,先直接上手体验。

您可以狠狠地点击这里:纯前端实现视频添加水印效果demo

选择视频和需要的水印图片,就可以得到最终的效果了,如下截图所示:

水印合成效果示意

其中,最关键的合成就是下面这部分代码:

let ctx = null;
const conversion = await Conversion.init({
    input,
    output,
    video: {
        process: (sample) => {
            if (!ctx) {
                // 创建canvas
                const canvas = new OffscreenCanvas(
                    sample.displayWidth,
                    sample.displayHeight
                );
                ctx = canvas.getContext('2d');
            }

            ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
            sample.draw(ctx, 0, 0);
            ctx.drawImage(watermark, 80, 80 * watermark.naturalHeight / watermark.naturalWidth);

            return ctx.canvas;
        }
    }
});

使用Conversion.init解码视频的每一帧(sample),然后使用canvas将画面和水印图重新绘制,再返回当前canvas即可。

其实不仅是水印合成,任何画面特效,字幕添加,遮罩,尺寸设置都可以使用此方法实现,原理都是一样的,都是对图像进行处理。

三、Video视频前后剪裁

同样的,先看实现效果。

您可以狠狠地点击这里:纯前端实现视频首尾剪裁demo

选择任意的视频,然后拖选滑竿选择需要的视频片段,点击红色的剪裁按钮,就可以看到被剪裁后的视频了:

剪裁demo示意

实际生产环境,拖拽的应该是缩列图的左右两个小翅膀,这里为了简化使用,使用了LuLu UI的双滑块模拟。

其核心实现代码极为简单:

const input = new Input({
    formats: ALL_FORMATS,
    source: new BlobSource(videoFile),
});
const output = new Output({
    format: new Mp4OutputFormat(), // The format of the file
    target: new BufferTarget(),
});

const eleRange = range.querySelector('input');  
const conversion = await Conversion.init({
    input,
    output,
    trim: {
        start: eleRange.from,
        end: eleRange.to,
    },
});

await conversion.execute();

使用trim参数,指定起止时间就可以了。

完整代码可以访问演示页面。

四、多音频和画面的视频合成

一例胜千言,您可以狠狠地点击这里:纯前端实现画面加音频的视频合成demo

默认提供了字幕、背景图、台词,背景音乐可选,用户可以自己上传,可以合成最终的视频:

视频合成demo截图

我查了下API,mediabunny中似乎缺少AudioBuffer处理方法,当然,也可能有,我自己没找到。

这里的AudioBuffer剪裁和合并用的是我自己之前手搓的方法。

相关源码在页面左侧(移动端在下方)有完整展示,基本上相关的视频合成都可以实现了。

这里提供下核心实现部分:

// 定义一个视频合成输出
const output = new Output({
    format: new Mp4OutputFormat(), 
    target: new BufferTarget(),
});

// 添加视频轨道,画面源自canvas
const videoSource = new CanvasSource(canvas, {
    codec: 'avc',
    bitrate: QUALITY_HIGH,
});
output.addVideoTrack(videoSource);

// 添加音轨
const audioSource = new AudioBufferSource({
    codec: 'aac',
    bitrate: QUALITY_HIGH,
});
output.addAudioTrack(audioSource);

await output.start();

// 获取音频文件
const duration = audioFile.duration;

// 每秒30帧
for (let frame = 0; frame < 30 * duration; frame++) {
    draw(frame);
    await videoSource.add(frame / 30, 1 / 30);
}

// 获取音频的 audioBuffer……(代码略),然后添加
await audioSource.add(audioBuffer);

await output.finalize();

五、广告时间

推荐下我几年前在掘金上更新的人文类课程《技术写作指南》

技术写作指南

既是关于写作,也是关注个人成长!

OK,就说这么多,如果你觉得本文内容对你的工作与学习有所帮助,欢迎转发,点赞!

😉😊😇
🥰😍😘

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

(本篇完)

🔲 ☆

务必谨慎使用JS WeakRef弱引用

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

(本篇完)

🔲 ⭐

务必谨慎使用JS WeakRef弱引用

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六边形头像的实现与蜂巢布局

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六边形头像的实现与蜂巢布局

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与背景底纹技术

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

(本篇完)

🔲 ☆

CSS corner-shape与背景底纹技术

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

(本篇完)

🔲 ☆

浅学WebTransport API:下一代Web双向通信技术

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

一、比 WebSocket 更懂低延迟的开发新利器

时间如斯,一转眼,做前端开发已经十五六年了,刚开始那会儿,实时通信还是使用轮询、长轮询,后来就是 WebSocket,然后现在又出来了个WebTransport。

WebSocket虽然可以解决大部分的问题,但是并不完美,例如队头阻塞、只能单一流传输、网络切换就断连,尤其是做实时游戏、直播推流这类对延迟要求极高的场景,总觉得差点意思。

所以就有了 WebTransport API,特别使用用在高并发、低延迟的实时场景。

二、WebTransport和WebSocket的区别

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 的3个关键特性

WebTransport 的核心优势,都源于它的底层协议,但我们前端不用去深究 QUIC 协议的细节,只要掌握它的3个核心特性,就能应对大部分开发场景。

1. 双重传输模式:可靠流 + 不可靠数据报

这是 WebTransport 最核心的亮点,也是和 WebSocket 最大的区别——它支持两种传输方式,可根据需求灵活选择:

  • 可靠流(Stream):和 WebSocket 类似,保证数据有序、不丢失、不重复,适合传输重要数据(比如聊天消息、协作工具的编辑操作);
  • 不可靠数据报(Datagram):不保证数据的有序性和到达率,但延迟极低,适合传输非关键数据(比如游戏玩家的实时位置、直播的视频帧)。

比方说一个实时多人小游戏,玩家的位置更新不需要100%到达(偶尔丢一个包不影响体验),但聊天消息必须可靠到达。

如果用 WebSocket,只能用一种传输方式,要么牺牲延迟,要么牺牲可靠性。

而用 WebTransport,就能给位置更新用不可靠数据报,聊天消息用可靠流,完美兼顾。

数据传输示意图如下:

WebTransport原理示意图

2. 多路复用:一个连接,多个流并行

WebSocket 是“单一流”传输,也就是说,一个 WebSocket 连接里,所有数据都在一条流里传输,一旦某个数据包丢失,后续所有数据都要等它重传,这就是“队头阻塞”。

而 WebTransport 支持多路复用,一个连接里可以同时创建多个独立的流,每个流互不影响。

比如你做一个直播平台,视频流、音频流、弹幕流可以用不同的流传输,就算视频流出现丢包重传,也不会影响弹幕的实时推送。

这一点,在高并发场景下,体验提升非常明显。

3. 连接迁移:网络切换不中断

这个特性可能很多同学没意识到它的重要性,但做移动端项目的同学一定懂:

用户用手机浏览网页时,经常会在 Wi-Fi 和 4G/5G 之间切换,这时候 WebSocket 连接会直接断开,需要重新握手建立连接,导致数据中断(比如直播卡顿、游戏掉线)。

WebTransport 基于 QUIC 协议,用“连接ID”来标识连接,而不是 IP 地址,所以就算网络切换,连接也能无缝迁移,数据不会中断。

四、WebTransport 核心 API 用法

下面给大家讲解 WebTransport 的核心用法,包括从连接建立,到两种传输模式的使用,每一步都有注释,供大家学习参考。

1. 第一步:建立 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;
  }
}

2. 第二步:使用可靠流(Stream)传输数据

可靠流分为“双向流”和“单向流”,双向流是客户端和服务器可以互相发送数据,单向流是只能一方发送、另一方接收。实际开发中,双向流用得最多(比如聊天)。

// 双向可靠流示例(客户端 ↔ 服务器)
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));
  }
}

3. 第三步:使用不可靠数据报(Datagram)传输数据

不可靠数据报的用法更简单,不需要创建流,直接通过 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);
  }
}

这里提醒大家一句:不可靠数据报不保证数据到达,所以不要用它传输重要数据(比如支付信息),否则会出现数据丢失的问题。

4. 完整实战代码(整合连接、流、数据报)

// 完整实战代码
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 的适用场景,避免盲目使用。

如果你遇到以下场景,强烈建议用 WebTransport:

  • 实时游戏:需要低延迟、高频数据传输,允许少量数据丢失;
  • 直播推流/拉流:视频帧、音频帧用不可靠数据报,控制信令用可靠流;
  • 实时协作工具:多人同时编辑,需要低延迟、可靠的数据传输;
  • 移动端实时应用:需要支持网络切换不中断,提升用户体验。

如果只是简单的实时聊天、消息推送,WebSocket 已经足够用,没必要强行上 WebTransport——技术选型的核心是“合适”,而不是“最新”。

另外,WebTransport 必须在 HTTPS 环境下使用(本地开发可以用 localhost),目前主流浏览器(Chrome 97+、Firefox 114+、Safari 26.4+)都已支持,不过离在正式环境使用还需要一两年的缓冲时间,除非你的项目不需要管Safari浏览器。

WebTransport兼容性

前端技术更新很快,我们不用追求掌握所有新特性,但对于那些能解决实际痛点、提升开发效率的技术,多花点时间吃透,总能在项目中发挥作用。

希望这篇文章能帮大家快速上手 WebTransport,少踩坑、多提效。

对了,提一嘴:本文的原理示意图和代码按钮都是AI生成的,仅供参考!

😉😊😇
🥰😍😘

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

(本篇完)

🔲 ☆

HTML dialog元素新支持了closedBy属性

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

(本篇完)

🔲 ☆

HTML dialog元素新支持了closedBy属性

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

(本篇完)

🔲 ☆

HTML interestfor属性与悬停popover交互效果

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

一、悬停popover也原生支持了

之前“该使用原生popover属性模拟下拉了”这篇文章有介绍过点击行为驱动的popover下拉。

最近发现,鼠标hover悬停也支持popover交互了。

且功能比点击更丰富,适用范围更广,那就是将popovertarget属性换成interestfor属性。

先看案例,HTML如下:

<button interestfor="imgBook">Hover显示图片</button>
<img id="imgBook" popover src="book.jpg" />

无需任何JS代码,鼠标经过按钮,就可以让图片显示,实时效果如下(需要Chrome 142+浏览器):


Nice!

Nice!

二、链接元素也支持

popovertarget属性仅适用于<button>元素,但是interestfor属性不仅可以用在按钮元素上,也可以用在各类链接元素上,例如<a>元素、<area>元素。

这个不难理解,<a>元素本身就有点击行为,和popovertarget的点击行为是冲突的。

但是interestfor属性是鼠标经过进入行为,并不会和<a>元素本身的链接跳转想冲突。

例如:

<a href interestfor="myAccount">Hover显示内容</a>
<div id="myAccount" popover>我的抖音:“张鑫旭本人”</div>

Hover显示内容

我的抖音:“张鑫旭本人”

悬浮上面的链接元素,就可以在显示器的最中间看到类似下面截图的效果了:

效果示意

interestForElement属性

除了HTML属性interestfor设置这种交互效果,我们还可以再JavaScript层面,使用DOM的interestForElement直接设置,代码示意:

const invoker = document.querySelector("button");
const popover = document.querySelector("div");

invoker.interestForElement = popover;

此时,Hover button元素也会触发popover变量元素的状态变化。

三、非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>元素背景高亮了。

实时渲染效果如下:

Hover Me!

鼠标经过链接后我高亮

上面的案例中出现了个CSS新特性,:interest-target伪类,专门用来匹配interestfor匹配元素激活的状态。

其实除了:interest-target伪类,还有个名为:interest-source的CSS伪类。

四、配套CSS伪类:interest-source/target

: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浏览器支持。

interset invokers 兼容性

总之,我是非常期待这个CSS特性能够快速全面支持的。

好吧,就介绍这么多,还是挺实用的一个特性。

飞吻

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

(本篇完)

🔲 ☆

点击图片放大查看交互效果的最佳实现

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

(本篇完)

🔲 ☆

点击图片放大查看交互效果的最佳实现

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标识符之粘性匹配

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

(本篇完)

🔲 ☆

CSS text-box属性又是干嘛用的?

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

一、text-box的设计初衷

在传统的 CSS 盒模型 中,文本行高(line-height)会在文字上下产生额外的“半行间距”(half-leading)。

这使得文本难以与旁边的图标或容器边缘精确对齐。如下图所示:

文字和图标不对齐

使用 text-box属性可以:

  • 去除文本顶部和底部的多余空白。
  • 实现严格的垂直居中和视觉对齐。

怎么实现呢?

二、text-box属性的语法

CSS text-box属性实际上是text-box-trimtext-box-edge这两个CSS属性的缩写。

其中:

text-box-trim
指定要裁剪哪个边缘(顶部、底部或两者)
语法示意:
text-box-trim: none;
text-box-trim: trim-both;
text-box-trim: trim-start;
text-box-trim: trim-end;
text-box-edge
指定裁剪到字体的哪个度量线(如大写字母顶部、基线等),语法示意:
/* 单个关键字 */
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-box-edge语法说明

  • 如果只有一个值,那表示上下边缘使用同一个值,目前仅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

什么垃圾特性!

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

  1. 脱离实际,根本没有实用价值。
  2. 语法复杂,什么基线、Cap线,ex线学习成本高,且都是针对英文语言的,学习成本高,现在的小年轻谁愿意学!
  3. 兼容性差,生产环境不敢实用啊
  4. 有更好的替代实现方式,完全没有实用的理由!

text-box兼容性

唉,抱歉,没想到这个CSS属性如此拉胯,浪费了大家这么多时间。

早知道如此,我就一笔带过的,😭😭

哭逃

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

(本篇完)

🔲 ☆

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

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

(本篇完)

🔲 ☆

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

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()作用速览

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

(本篇完)

❌