MoreRSS

site iconZhangXinXu | 张鑫旭修改

出版《CSS选择器世界》,喜欢钓鱼、写作。
请复制 RSS 到你的阅读器,或快速订阅到 :

Inoreader Feedly Follow Feedbin Local Reader

ZhangXinXu | 张鑫旭的 RSS 预览

CSS rex、rlh、ric等根家族单位简介

2026-09-17 19:42:29

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

一、从rem单位说开去

说起CSS根单位,大家最熟悉的莫过于 rem 单位。

从IE9浏览器就开始支持。

是目前CSS领域最常用的CSS单位之一,尤其在移动端的弹性布局中。

很多人不知道的是,从Chrome 111(2023年3月)、Safari 16.4(2023年3月)、Firefox 120(2023年11月)开始,还支持了其他一系列的根元素单位,五花八门,将目前基于所有与文字相关的CSS相对单位都囊括了。

具体参见:

rcap

等于根元素字体字号对应的“大写字母的高度”。

CSS的cap是个比较晚支持的CSS单位,中文场景下此单位出场机会不大,因为表示的是英文大写字母的高度。

其兼容性如下(单位caprcap兼容性一致):

rcap单位兼容性

rch
ch表示字符0的宽度,自然,rch就表示当前根元素数字0,Unicode字符 U+0030的宽度。

ch是个IE时代的CSS单位,但是rch却比rlh支持的晚,Safari 17.2(2023年12月)、Firefox 147(2026年1月)才支持.

rch的兼容性

rem

代表根元素(通常是 <html>)的 font-size。在根元素的 font-size 属性内使用时,代表其初始值。浏览器常见默认值为 16px,但用户设置可能改变该值。

rex
等于根元素 font 的 x-height(小写字母x高度)。

rex的兼容性和rch是一样的。

ric
ic这个单位我之前也介绍过,这是与中文环境,东亚语言密切相关的CSS单位。表示字符“水”所占据的宽度。中文字体几乎都是等宽,因此,这个单位可以预估一段纯中文字体占据的宽度。

ric自然就表示根元素字体上“水”这个字符占据的宽度了。

单位ic是2022年各大浏览器开始大规模支持的,目前算是一个可以放心使用的单位了。

ic单位兼容性

至于单位ric的兼容性,则和rex他们一致。

rlh
CSS lhrlh都是新CSS单位,与行高的计算值相关。

然后出乎意料的,lh虽然是新单位,但是rlh的兼容性却比rexrch这些老单位的根元素表示要好。

rlh单位兼容性

二、根字体单位实战案例

苦思冥想,想不到什么好的实战案例。

突然想到了一个办法,问AI。

这是AI提供的案例:

html {
  font-family: system-ui, sans-serif;
  font-size: 16px;
  line-height: 1.6;
}
/* 标题:上下内边距跟随大写字母高度 */
h1 {
  padding-block: 1rcap;
}
/* 正文行内图标,高度匹配小写x高度 */
.inline-icon {
  height: 1rex;
}
/* 6位数字验证码输入框 */
.code-input {
  width: 6rch;
}
/* 中文评论框,最多容纳20个汉字 */
.cn-input {
  width: 20ric;
}
/* 段落间距,基于根行高,全局垂直韵律 */
p {
  margin-block: 1.5rlh;
}

嗯……怎么说呢?感谢都是非root根元素单位的应用场景。

算了算了,大家了解下吧,至少下次看到这些单位知道是什么意思吧。

三、就这么结束啦

关于CSS新单位,几个月前也介绍了一波,有兴趣可以访问“CSS新单位dvh、lvmin、vi、cqb等初解”这里了解。

虽然出了很多CSS新单位,但由于目前的CSS单位够用了,且这些单位大都不是必须不可替代的。

因此,大多都无人问津。

有没有展开介绍的必要。

就这样吧,我们下个视频再见。

再见

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

(本篇完)

新时代下的tooltip提示效果的最佳实现

2026-09-15 10:38:59

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

一、需求背景和手搓实现

富文本输入框中的某个<img>元素hover的时候出现如下图所示的黑色提示效果。

提示效果示意图

如果是传统实现,或者你让AI来实现,多半会在div容器元素上委托mouseentermouseleave事件,对于带三角的黑色tips效果,会使用JS动态位置,小三角会使用CSS伪元素创建。

我说的没错吧。

但是,亲们,时代变了啊,前端技术一直在不停发展的哈。

上面那套实现,虽然也能有最终的效果,但是成本、性能和代码量都不怎么行啊。

实际上,以目前的前端能力,可以零JavaScript代码实现本需求。

具体如下:

  • HTML interestfor属性实现hover悬停目标元素显隐交互,支持设置延时显示时间;
  • CSS锚点定位实现定位;
  • CSS border-shape属性绘制轮廓,使边框线条完美;

演示案例

您可以狠狠地点击这里:纯CSS实现黑色气泡tooltip效果demo

最终的效果如下GIF录屏示意:

GIF录屏示意

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

<button class="button-mention" interestfor="tooltip">黑夜模式封面图</button>
<div id="tooltip" class="mention-tooltip" popover="hint">点击 或 按Tab键插入</div>
.button-mention {
    padding: 2px 8px;
    font-size: 100%;
    border: 0;
    border-radius: 99px;
    background-color: #ffffff26;
    cursor: pointer;
    opacity: .75;
    interest-delay-start: .2s;
    color: #fff;
}
.mention-tooltip {
    --tooltip-shape: shape( from 5% 0%, hline to 95%, arc to 100% 21.05% of 5% 21.05% small cw, vline to 57.89%, arc to 95% 78.95% of 5% 21.05% small cw, hline to 55%, line to 50% 100%, line to 45% 78.95%, hline to 5%, arc to 0% 57.89% of 5% 21.05% small cw, vline to 21.05%, arc to 5% 0% of 5% 21.05% small cw, close );
    position: fixed;
    position-area: top;
    width: max-content;
    margin: 0 0 2px;
    padding: 2px 8px 8px;
    border: 0;
    aspect-ratio: 4.211;
    background-color: #000;
    color: #fff;
    font-size: 12px;
    clip-path: var(--tooltip-shape);
    line-height: 2;
    &:interest-target {
        animation: tinydown 0.2s ease-in-out;
    }
}
@supports (border-shape: none) {
    .mention-tooltip {
        clip-path: none;
        border: 1px solid #fff1;
        border-shape: var(--tooltip-shape);
    }
}

@keyframes tinydown {
    from {
        transform: translateY(-5px);
        opacity: 0;
    }
    to {
        transform: translateY(0);
        opacity: 1;
    }
}

这里的tooltips效果几乎可以说是集这几年前端新特性的大成之作。

二、新特性指引与说明

此案例使用的新特性我在之前的文章都有介绍过。

这综述下:

1. interestfor交互

按钮和链接元素设置HTML interestfor属性,可以让元素在鼠标移入移出的时候,自身和目标元素产生变化。

如果目标元素是popover元素,则目标元素会自动显示与隐藏。

也就是interestfor交互中,目标元素的popover属性不是必须的。

详见此文:HTML interestfor属性与悬停popover交互效果

额外说明:

如果是点击行为交互,则只能是按钮元素与popovertarget属性,链接元素不支持。

2. popover交互

popover交互出现比较早,目前来说已经是相对成熟的技术了。

其中,本案例用到的popover="hint"则是去年才支持的新特性,其作用是,popover浮层显示的时候,不会隐藏之前的浮层,特别适合用在tooltip这种轻提示的场景。

有兴趣可以访问这里了解:HTML popover再进化 – 新增hint类型提示框

3. position-area与锚点定位

popover元素默认是屏幕居中显示的,我们这里的诉求是在按钮上方显示,需要重定位。

这是CSS锚点定位的强项了。

这里的定位比较简单固定,因此,直接一行position-area:top就可以了。

CSS锚点定位是前端必学的技术,LuLu UI组件库的定位已经全部改成CSS锚点定位了,可以节约大量的代码,也不用担心滚动嵌套,以及复杂HTML定位元素嵌套的问题。

文章见:告别JS浮层,全新的CSS Anchor Positioning锚点定位API

4. border-shape与不规则形状边框

仔细看设计图,黑色提示框一圈是有一圈浅色的边框的。

在过去,我们多使用filter: drop-shadow()多投影去模拟,但是效果只是近似,并不完美。

现在又了border-shape属性,自然天然支持。

只不过这里想要得到提示框对应的shape()函数路径参数是实现的难点。

根据我的测试,几乎所有的模型全都全军覆没。

因为shape()函数中的SVG路径语法是一个全新的语法,AI生成的代码看似有模有样,但都跑不起来。

我最终是怎么实现的呢?

让AI去生成传统SVG的提示框路径代码,然后使用我自己弄的“CSS clip-path path() to shape()函数转换工具”转换得到的。

工具转换截图示意

不过由于border-shape属性特别新,Safari浏览器还没有支持。

因此,最后使用了clip-path属性兜底。

详见文章:

5. 其他一些中老登特性

width:max-content这个CSS声明支持有快10年的历史了,日常开发我们用得不多,并不是因为他不好用,而是大家习惯使用white-space:nowrap实现类似的效果。

我个人推荐使用width:max-content,会显得更高级,且遇到绝对定位拉伸场景的时候,尺寸也不会被随意改变。

aspect-ratio是一个次新的CSS属性,用来控制元素的高宽比例,可以取代具体高宽设置,让CSS代码的适应性更强。

三、如何与vibe 编程结合

如何在 vibe 编程的时候,让 AI 输出比较前沿但是效果和代码都非常好的技术实现呢?

首先第一点,也是最最重要的。

那就是开发者自己必须要知道此需求可以使用何种技术实现,也就是必须要知识广度的积累。

这就是我一直不停学习的原因。

不过,AI总是倾向于采用传统稳健的实现方法,因此,想要让他输出高级的实现方法,一定要好好调教。

首先,按照MDN官方的skills,这个一定要安装,不然AI会产生自以为是的幻觉。

不过,根据我的实操,即使有了MDN skills的学习与约束,AI还是会有很多自以为是。比方说本例中,hover显隐完全CSS就可以cover,AI生成的代码居然自己加了JS代码mouseleave隐藏,多此一举。

导致我需要刻意强调一遍。

以下是我使用国产混元4模型(最近免费)实现tooltips效果的实录。

第一轮:

下面实现button.button-mention的tooltips效果。

此效果完全采用新技术实现,没有任何JS交互的参与。

  1. Hover <button>元素的显示与隐藏,使用interestfor属性实现。

    所有的按钮的interestfor都指向同一个元素:

    <div id="mentionTooltip" class="mention-tooltip" popover="hint">点击 或 按Tab键插入</div>
  2. mentionTooltip元素使用JS动态创建,只需要插入一次,可以在第一次创建button.button-mention元素的时候执行。此元素可以放在<body>元素下。
  3. interestfor + popover只是解决了Hover显隐的问题,并未解决定位的问题。

    所以,还需要使用CSS锚点定位,将#mentionTooltip定位在<button>元素的上方。

    CSS锚点定位已经出来了一两年时间,你这边应该实现没问题,我就不细节指导了。

  4. 补充一点,Hover按钮显示Tooltip,点击的时候按钮不是会消失吗?这个Tooltip无需再额外JS隐藏,因为这是浏览器内置的交互行为,懂否?不是自定义组件那套交互,千万不要自以为是,额外添加冗余代码。
  5. tooltips的图形效果也使用全新的技术实现。具体如下:
    1. 如果用户浏览器支持CSS border-shape属性(可以使用@supports语法判断),那就使用border-shape一次性绘制到位。

      这个已经超出了你的能力范围,所以,核心代码我帮你写好了:

      .mention-tooltip {
        aspect-ratio: 4.211;
        border: 1px solid #fff1;
        border-shape: shape(from 5% 0%,hline to 95%,arc to 100% 21.05% of 5% 21.05% small cw,vline to 57.89%,arc to 95% 78.95% of 5% 21.05% small cw,hline to 55%,line to 50% 100%,line to 45% 78.95%,hline to 5%,arc to 0% 57.89% of 5% 21.05% small cw,vline to 21.05%,arc to 5% 0% of 5% 21.05% small cw,close);
      }
    2. 如果不支持CSS border-shape,那就使用clip-path: 代码示意:
      .mention-tooltip {
        aspect-ratio: 4.211;
        clip-path: shape(from 5% 0%,hline to 95%,arc to 100% 21.05% of 5% 21.05% small cw,vline to 57.89%,arc to 95% 78.95% of 5% 21.05% small cw,hline to 55%,line to 50% 100%,line to 45% 78.95%,hline to 5%,arc to 0% 57.89% of 5% 21.05% small cw,vline to 21.05%,arc to 5% 0% of 5% 21.05% small cw,close);
      }

    border-shapeclip-pathshape()函数代码是一样的。

    因此,为了节约代码,shape()函数可以提取为CSS变量。

    .mention-tooltip剩余的背景色(纯黑)和文字颜色(纯白)以及fixed定位等CSS,你就按照你的理解补充,这个不难。

    对应CSS写在canvas中。

最后再强调一点,此需求其实代码量很少。

有什么疑问可以提出来,如果没有疑问,帮你的实现清单给我看一下,我确认下。

第二轮:

  1. constants.ts 新增两个常量这个没必要。

    MENTION_TOOLTIP_ID和MENTION_TOOLTIP_TEXT只会用在一个地方,没有必要提取,这是属于过度提取,反而增加了代码的复杂度。

  2. btn.title 冲突这么处理,如果浏览器支持interestfor属性,那不设置title,否则使用浏览器原生的title属性。
  3. 显示延迟的CSS可以加,200ms足够了。
  4. 文案是固定的。

第三轮:

btn.title实现代码还有些问题,手动修改。

done!

根据我最近的使用,混元4模型还是可以用的,大部分场景的需求是可以满足的。

不足就是思考时间太长,高端与前沿的需求还是差些火候。

不过最近免费不要钱,这些不足也是可以忍一忍的。

四、结语

简单项目AI生成的代码规整,完美,无懈可击。

然而多人合作的复杂项目,你使用A模型,我使用B模型,再加上很多开发无脑accept,时间一长,最终代码惨不忍睹,虽然也能运行,但是性能差到离谱(参见文末图片)。

我现在让AI动手编程之前,一定会让他陈列修改文件清单,评估他的实现。

差不多一大半的时间都在沟通讨论,纠正调教。

今天试用了GLM-5.3,能力比混元强,但是思考时间也长,token消耗也高。

还是Claude Code好用,不过这个贵,在公司开源节流的情况下,还是需要考虑其他模型。

这周再试用下 deepseek 4.1。

手敲代码 vs  AI代码

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

(本篇完)

关键时刻可以救命的Web Locks API

2026-09-10 18:37:44

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

一、大文件断点续传

大文件上传,为了用户体验良好,通常需要接入断点续传。

相关前端实现我十几年前就有介绍过“HTTP协议下文件上传断点续传”。

不过本文要讲的重点不是断点续传本身,而是其中可能遇到的一个用户体验问题。

那就是如果用户同时打开多个同一个URL地址的文件续传标签页,此时该怎么办?

统一标签上传

如果我们放任不管,那么这几个页面都会自主主张给后端发送上传数据。

那还得了,每个页面的上传进度不一样的,后端返回岂不是一会儿30%,一会儿25%,一会儿又是35%。

于是就会看到进度条像得了羊癫疯一样,疯狂跳动,来,社会摇,走起来。

这种疯癫的交互效果就算是精神小妹都无法容忍。

显然需要处理?

该怎么处理呢。

二、跨浏览器标签页开发常见方案

我们不妨看看市面上现有的,跨页面的通信交互方案,能不能优雅地解决此问题。

1. 本地localStorage + 轮询

// 示意代码,勿使用
if (!localStorage.getItem('upload-lock')) { 
  localStorage.setItem('upload-lock', Date.now()); 
  // 开始上传
  // ...
}

问题在于:

  • 竞态条件:localStorage 的读/写操作不是原子操作。所谓原子操作,指的是一次性完整执行完毕的操作。例如:
    // 逻辑:读取计数 +1 再存回去
    let count = Number(localStorage.getItem('counter')); // ①读
    count = count + 1;                                    // ②内存修改
    localStorage.setItem('counter', count);               // ③写
    

    上面代码的读本身,和写本身可以看成是原子操作,但是,整个任务却不是原子的,因为读和写中间有一段业务计算,整套操作被拆分,中间可以被别的标签页抢占。

  • 过期锁定:如果标签页崩溃,锁定状态将永久保留。
  • 轮询开销:您需要持续检查锁定状态
  • 事件限制:存储事件只会在其他标签页中触发,而不会在当前标签页中触发。

2. Broadcast Channel API

广播通道API我去年也介绍过,详见“Broadcast Channel API简介,可实现Web页面广播通信”一文。

BroadcastChannel兼容性

如果是针对当前需求,我们可能会这么处理:

const channel = new BroadcastChannel('upload-coordination');
channel.postMessage({ type: 'REQUEST_UPLOAD_PERMISSION' });

看起来代码还挺简单,不过问题也比较致命。

那就是同时发送的消息可能会造成冲突,需要手动处理心跳检测、崩溃检测和冲突解决。


可以看到,那些常见的跨页面通信手段,都无法真正解决这里的共享文件上传问题。

其实,类似这种多页面操作统一资源的场景,毫无疑问,最好的解决方案就是Web Locks API。

三、了解“锁”的概念

“锁”是计算机开发中的一个重要概念,前端开发人员接触的不多,但是如果是数据库开发,那这个可太熟了。

锁其实是一种机制,它确保共享资源(变量、对象、函数调用等)一次只能被一个对象访问,并且保证每个调用者都能获得最新信息。

比方说:一家银行在共享内存中存储了多个账户,并且有多台ATM机同时进行取款、存款和查询余额的操作。

如果没有协调(同步),这将导致混乱,如下图所示。

取款混乱示意

此时,我们可以通过锁定账户的读写权限来控制访问。

这样一来,无论执行什么操作(取款、充值或查询余额),第一个获得锁的调用者都将获得该锁。

其他调用者必须等到第一个获得锁的调用者完成操作后,锁才会被释放。

下图展示了同步账户访问后的情况。

有了锁之后

四、navigator.locks与上传锁定

OK,万事具备,就看Web Locks API如何在代码层面解决我们的上传冲突问题了,其实代码很简单。

// 只有成功获取锁才继续处理
navigator.locks.request(LOCK_NAME, { ifavailable: true }
  async (lock) => {
    if (lock) {
      // 执行上传
      upload(); 
      await holdLockUntilComplete(abortController);
    }
    // 获取不到锁则静默跳过 - 其他标签页正在处理
    // ...
  }
);

看到没有,就几行代码。

使用非常简单,就是把原来的实现代码,使用navigator.locks.request()方法包一下就好了。

此时,浏览器会自动加锁和锁判断。

锁的生命周期管理示意

上述代码中的holdLockUntilComplete()可以用来中断上传请求,或者用来管理整个上传的生命周期。

使用示意,供大家参考。

const holdLockUntilComplete = (abortController) => {
  return new Promise((resolve) => {
    const cleanup = () => {
      // 移除所有的监听事件
      uploader.off("complete", cleanup);
      resolve();
    };

    // 上传完成事件
    uploader.on("complete", cleanup);
    // 其他事件略...

    // 组件卸载时也释放
    abortController.signal.addEventListener("abort", cleanup);
  });
};

使用 LockManager 的好处

  • 真正解决冲突问题,零重复上传
  • 更好的用户体验,由于协调逻辑,上传过程不会再出现卡顿。
  • 降低基础设施成本,不再需要重复处理和存储
  • 更简洁的代码,没有复杂的状态机或轮询逻辑

五、一些可选参数说明

navigator.locks.request()方法是支持一些可选参数的,语法如下:

request(name, callback)
request(name, options, callback)

其中,options支持以下一些参数,这些参数大多数场景下都用不到,大家一眼扫一下就可以了。

mode

"exclusive""shared" 之一。默认值是 "exclusive",表示排他,也就是一次只能一个锁。"shared" 表示共享锁。

ifAvailable

如果为 true,则只有在尚未持有锁的情况下才会授予锁请求。如果无法授予,则将使用 null 而不是 Lock 实例来调用回调。默认值为 false

steal

如果为 true,将释放所有同名已持有的锁,并授予该请求。默认值为 false

警告:小心使用!之前在锁内运行的代码会继续运行,并且可能与现在持有锁的代码发生冲突。

signal

一个 AbortSignalAbortControllersignal 属性);如果指定并且 AbortController 被中止,则锁请求将被丢弃(如果尚未授予)。

其他适合使用Locks API的场景

除了本文提到的大文件断点续传,还有以下这些需求场景适合使用 Web Locks API.

  • 当多个标签页同时操作 Indexed Database API 或 LocalStorage 时;
  • 在网页端文档编辑器中,若用户在两个标签页中打开了同一份文档;
  • 在单页面内按顺序执行复杂的异步任务(如批量文件上传、连续串联请求)时;

还有下图所示的情况,也有必要上锁

众女围绕一个资源

六、兼容性以及其他说明

Web Locks API已经出现很多年了,兼容性还是相当OK的,以目前的AI能力,此API使用是没有任何困扰的。

Web Locks API兼容性

除了上面介绍过的request()方法,navigator.locks还有个query()方法,此方法执行后返回一个个Promise,该Promise解析后得到一个对象,其中包含有关已持有锁和挂起锁的信息。

具体有哪些信息,不展开介绍,因为没有意义,正遇到类似需求,直接让AI去运行代码就好了。

好了,本文内容已经比预期的多多了。

其实没必要扯那么多东西的,如今AI这么强,技术细节并没有那么重要。

你是好人

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

(本篇完)

别再使用IndexedDB,大文件读写就用OPFS

2026-09-03 17:39:07

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

一、Web本地化存储

本来想巴拉巴拉说一堆东西的。

可一想,现在都是快餐时代 + AI时代了,耐心缺失 + 学习热情缺失。

连孙晨宇的小作文都只是看个大概,还指望那些洋洋洒洒的技术内容有人认真拜读,异想天开了。

所以,我决定了,直接一个表格一把梭。

存储方案 存储容量 持久化特性 存储类型 运行线程 主要适合场景 缺点 & 局限
Cookie 约 4KB 可设置 expires 持久;默认会话级关闭标签失效 仅字符串;每次 http 请求自动带到后端 主线程 服务端读取标识、会话 token、简单埋点标记 容量极小;请求头携带增加网络开销;只能存字符串;易被 XSS 窃取
localStorage 5‑10MB 持久,手动删除 / 清站点数据才消失 仅字符串 只能主线程 简单小 key‑value 配置、简单用户偏好设置 只能字符串;同步读写,大数据会阻塞主线程;容量有限
sessionStorage 5‑10MB 标签页会话级别,关闭当前标签立刻清空 仅字符串 只能主线程 单标签临时状态缓存,表单临时草稿 多 tab 之间隔离;关标签数据全部丢失;同步 API 阻塞;不能存二进制
IndexedDB 一般几十 MB~ 浏览器配额上限 持久,清站点数据删除 String、Blob、ArrayBuffer、Object 等,支持二进制 主线程 + WebWorker 结构化数据、Blob 资源缓存(字体 / 图片),需要索引查询的业务 API 繁琐;大 Blob 存取存在结构化克隆内存开销;不能像文件一样 append 追加
OPFS
(getDirectory)
受 origin 总配额限制 (可几十‑几百 MB) 持久,清站点数据删除 ArrayBuffer、Uint8Array,真实文件句柄 主线程 (异步 API);Worker 可高性能同步句柄 大二进制文件缓存(字体、音视频片段、本地草稿文件)、字节级随机读写、追加写入 兼容性差;无查询索引能力,就是文件系统;用户看不到物理文件;不能跨 origin 共享文件

一直到IndexedDB这里的存储方式,想必大多数前端都有所了解,而最后出现的OPFS(Origin private file system)知道的人就不多了。

这是一类类似于 SQLite 数据库的文件系统,通过 navigator.storage.getDirectory() 方法返回一个 FileSystemDirectoryHandle,继而对文件进行读写追加等管理。

一例胜千言,话不多说,直接上案例。

二、OPFS与完整中文字体案例

您可以狠狠地点击这里:OPFS 与中文字体持久存储demo

首次进入走请求:

字体请求

之后每次进来,就是秒开了,开销只存在第一次开发场景。

腾讯字体demo渲染示意

此demo是混元4模型生成的,AI还真好使,以前弄个demo都要手搓,时间要花很多倍,现在只需要提出诉求,然后自己稍微改改就好了。


在过去的Web开发中,引入完整的中文字体是不现实的,因为字体都很大,会严重拖慢页面的加载速度。

可现在,类似于font-spider这样的字体动态生成工具的必要性已经大不如从前了。

很简单,一个完整的中文字体平均大小6M左右,如果转为woff2字体合适,可以降低到3M左右。

3M的文件大小是什么概念呢?

也就是一个开屏广告的大小,甚至不如一个GIF表情包的大小。

而字体文件是没有任何所谓的迭代概念的。

字体一旦设计出来,几乎就没有改动。

所以,只要有一个可靠的存储方案,中文字体完全就可以全局引入。

具体为:

  • 第一次加载走请求,按照现在的网速,可能几秒钟就结束了,然后存储在本地。
  • 下一次加载的时候,直接从本地读取,用户几乎无感知。

问题来了,该使用什么本地存储技术呢?

在之前,IndexedDB是不二选择,现在有了OPFS,IndexedDB就没用任何使用的必要了,原因很简单:

OPFS的性能太高了,代码也简单太多了。

代码

性能大家可能感觉不出来,但是代码一眼可见,下面展示的就是读写和字体应用的核心代码。

// ① 打开浏览器私有文件系统(OPFS)根目录
const root = await navigator.storage.getDirectory();
const KEY  = 'key-font-tencent';

// ② 读取:不传 create,文件不存在时会抛 NotFoundError
let file;
try {
    const handle = await root.getFileHandle(KEY);
    file = await handle.getFile();   // 二次进入:零网络请求
} catch {
    // ③ 写入:响应流管道直连落盘,大字体文件不会整体驻留内存
    const res = await fetch('./tencent.woff2');
    const handle = await root.getFileHandle(KEY, { create: true });
    await res.body.pipeTo(await handle.createWritable());
    file = await handle.getFile();
}

// ④ 注册为自定义字体,之后即可用 font-family: Tencent
const face = new FontFace('Tencent', await file.arrayBuffer());
document.fonts.add(await face.load());

可以看到,无论是文件的读还是写,就是两行代码的事情。

而IndexedDB的语法就复杂多了,理解成本也高,往往需要借助第三方组件简化使用。

所以,眼下,对于大文件的本次持久化存储,别再使用IndexedDB,使用getDirectory()吧。

除非,你需要存储的东西是结构化的数据,或者要兼容很多陈旧的设备,否则,IndexedDB没有任何优势。

三、和File System Access API的区别

origin private file system (OPFS)属于File System API的一部分,那它和之前介绍过的File System Access API又是什么关系呢?

一句话:

OPFS是File System API的核心能力,而File System Access API是构建在 File System API 之上的上层扩展。

OPFS可以看成是文件系统API的亲儿子,而FSAA可以看成是文件系统API的外戚。

前者被所有现代浏览器支持,后者目前仅Chrome支持,也就是之前介绍过的showOpenFilePicker()手动触发本地文件选择的方法。

详见此文:“不使用file类型input也能触发文件上传

看了下,是5年前的文章,妈呀,5年过去了,Safari和Firefox浏览器都还没支持File System Access API,估计以后也难了。

还是通过表格对比下吧,比叽里咕噜说一堆话有用多了。

对比维度 OPFS 源私有文件系统 File System Access API(FSA)
文件位置 浏览器内部沙盒,用户资源管理器不可见;磁盘真实存在,但路径不对外暴露 操作系统真实磁盘,用户看得见(桌面、文档等)
触发方式 直接调用 navigator.storage.getDirectory()无需弹窗、无需用户点击手势,后台静默读写 必须用户主动点击手势触发文件选择弹窗,用户手动挑选文件 / 文件夹,不能后台静默打开本地文件
权限模型 Origin 隔离;无弹窗授权;受浏览器存储配额管理;清除站点数据全部销毁 操作系统文件权限;句柄可保存,但刷新页面后读写权限会失效,需要重新申请权限;用户可在浏览器设置撤销权限
配额限制 和 IndexedDB / CacheStorage 共享 origin 总配额,可用navigator.storage.estimate()查询;磁盘满抛配额溢出错误 不受浏览器站点配额限制,受操作系统磁盘大小限制;直接读写用户磁盘
线程能力 主线程异步 API;WebWorker 支持高性能同步句柄 createSyncAccessHandle(),支持字节随机读写 全部接口为异步 Promise;Worker 不能调用 Picker 弹窗 API
数据销毁 需要OPFS API删除,或存储不足浏览器自动删除(一般不会) 文件永久保存在用户磁盘;网页销毁不会删除磁盘文件,需要网页主动调用 remove
典型使用场景 Web 应用内部缓存:缓存字体、离线资源、本地草稿、WebAssembly‑SQLite 数据库,应用内部私有数据 Web 编辑器打开 / 保存用户本地文档;webIDE 读写用户本地项目文件夹,用户主动管理的文件
浏览器兼容性 Chrome86+ only

兼容性

OPFS的兼容性比FSA好多了,如下图所示,基本上可以畅快使用了,只要不是那种to C的项目。

getDirectory兼容性

四、再说点什么

接下来的内容大家悄悄传播,千万不要让厂子的法务知道,不然又要让我删除了。

本月开始,Token额度大降,从之前每人6K,降到500元,其中还有120元的占位费,也就是灵活支配的额度只有380元。

超过的,要走报销。

我这人最怕麻烦了,一想到报销就算了,所以,眼下只能省着点用了。

不过之前用得也不多,平均10%多一点。

窍门就在于让AI担任填充角色,然后多花时间阅读代码,给AI指明路径与策略,就会节约大量AI的思考,就省钱了。

然后现在国产模型的能力也相当不错,日常需求非常够用了。

所以,一个月500元足够了。

本月到现在,我一分钱还没花呢?

Token本月消耗

窍门在哪里?

很简单,混元模型最近免费,哈哈哈,羊毛不薅白不薅。

还有,厂子的前端技术中心的组织架构好像没了。

大家都分散到各个业务线了。

我的组织架构也变了,从以前的XXX换到XXX了。

果然,时代变了。

好了,不能再多透露了,我说完了。

说完了,表情包

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

(本篇完)

超级Web特性HTML-in-Canvas初体验

2026-08-28 18:14:29

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

一、等不及了

Chrome 150+之后,可以通过开启#canvas-draw-element特性体验HTML-in-Canvas的神奇效果。

Chrome地址栏输入下面代码可开启:

chrome://flags/#canvas-draw-element

HTML-in-Canvas开启示意

通常Web新特性都都要等浏览器正式支持才介绍。

但是这次的这个特性,实在是等不及了。

在过去,Canvas里面实现元素动效是麻烦的,啰嗦的,往往需要借助第三方的组件。

而CSS实现动画动效非常方便。

现在,有了HTML-in-Canvas,所有的Web动效,包括SVG动效,都可以无缝接入到Canvas中,Canvas的应用能力直接飙升。

比方说HTML原生的<dialog>弹框使用非常方便,于是,过去,WebGL/Canvas 游戏中的弹窗和提示面板就可以直接使用HTML <dialog>元素,而无需像以前一样重写一套游戏 UI 交互:

  1. 游戏外悬浮 DOM:弹窗跟随游戏视口缩放很麻烦;游戏画布拖动缩放时 DOM 同步成本高。
  2. 手写绘图 API 绘制 UI:成本极高,不能复用现有组件库。

又比如HTML转图片这个功能。

之前我们大多使用 html2canvas 这个项目,简单点的会借助SVG <forginObject>元素实现(详见“SVG foreignObject简介与截图等应用”此文)。

但是html2canvas还有不支持CSS锥形渐变,一些CSS混合模式效果不支持的问题。

现在又了HTML-in-Canvas,等于原生有了HTML转图片的能力,既全面,又高性能,html2canvas直接可以说拜拜了。

二、使用非常简单

将HTML绘制在Canvas上的代码非常简单,几行代码的事情。

最核心的一行代码就是:

canvas.getContext('2d').drawElementImage(htmlElement, 0, 0);

就可以了。

Chrome开发者社区提供了很多HTML-in-Canvas的案例:https://chrome.dev/html-in-canvas/

HTML-in-Canvas案例

可以看到,无论是Video视频、Form表单还是iframe内嵌框架,都可以绘制在Canvas画布上。

而且目前主流的前端3D开发框架,如There.js、Pixi.js都已经加入了对HTML-in-Canvas的支持。

所谓实践出真知,看别人写的Demo,不如自己写个Demo。

脑筋一转,不妨试试看能不能把LuLu UI的表单验证直接搬到Canvas画布上。

案例

您可以狠狠地点击这里:HTML in Canvas 绘制LuLu UI表单组件演示页面

可以看到,表单不仅渲染了,特么的自定义的JS验证UI居然也一起带到Canvas里面去了。

真是牛了个大发了!

表单效果与验证提示

相关的代码如下所示:

<canvas id="canvas" style="width: 100%;" layoutsubtree>
<form id="validateForm" is-validate>
    ...略...
</form>
</canvas>
<script>
    const pixelRatio = window.devicePixelRatio || 1;
    const canvas = document.getElementById('canvas');
    const validateForm = document.getElementById('validateForm');
    canvas.style.width = validateForm.offsetWidth + 'px';
    canvas.style.height = validateForm.offsetHeight + 'px';
    canvas.width = canvas.clientWidth * pixelRatio;
    canvas.height = canvas.clientHeight * pixelRatio;

    const ctx = canvas.getContext('2d');

    canvas.onpaint = () => {
        ctx.reset();
        ctx.drawElementImage(validateForm, 0, 0);
    };
</script>

JS主要做两件事情,其一,设置合适的尺寸;其二,渲染。

然后就没有然后了,就这么Easy!

Nice表情包

补充说明

<canvas>元素上设置的layoutsubtree属性是必须的,这个千万不能删除,否则是没有实时渲染效果的。

– 我看到不少案例,包括官方案例,都在表单元素上设置了drawable属性,但是根据自己的实践,此属性不设置似乎不影响效果。但是暂时又没有改属性相关的资料,所以,其最终作用,暂时我也不清楚。

三、好了,暂时就先说这么多

AI编程时代,技术细节没必要深究,加上本身目前兼容性还不太好,我觉得了解这么多就好了。

静待浏览器大规模支持吧!

最后,想问下大家,目前还有哪些前端周刊还在更新的吗❓

发现AI编码来了之后,很多都停更了,难道前端这个行业这是自此不再上进了吗? ​​​

🤔

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

(本篇完)