阅读视图

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

深入 HTML-in-Canvas:当 Canvas 学会了渲染 DOM,前端图形生态要变天了

你有没有想过一个问题:为什么 Canvas 里的文字永远那么丑?为什么游戏里的 UI 只能用 Canvas API 手画,而不能直接写个 <div> 上去?为什么每次做图表都要在 ctx.fillText 和 CSS 字体之间反复拉扯?今天,一个正在 Chromium 中孵化的 WICG 提案,要彻底终结这个延续了二十年的困局。

📋 目录

背景:Canvas 二十年的「盲人」困境

2004 年,Apple 在 Safari 中引入了 <canvas> 元素,随后被 WHATWG 和 W3C 标准化。二十多年来,Canvas 成为 Web 上最强大的 2D/3D 图形基元——游戏、图表、数据可视化、创意工具、图像编辑……几乎一切"像素级别的操作"都跑在 Canvas 上。

但它有一个致命的短板:渲染不了真正的 HTML 内容

这听起来像是一个不应该存在的问题——我明明有一个 <div>,为什么不能把它「画」到 Canvas 上?但现实是,开发者们二十年来的解决方案只有这些:

方案 原理 问题
ctx.fillText() 手写文字排版 不支持复杂文本、RTL、国际化排版
html2canvas / dom-to-image 用 JS 重绘整个渲染树 慢(JS 模拟渲染引擎)、不完整、不全准
SVG <foreignObject> 在 SVG 中嵌入 HTML 无法与 Canvas 2D API 交互、不支持 WebGL/WebGPU
截图上传(你没看错) 手动截图当纹理 根本不能算方案

这些问题带来的连锁反应非常具体:

  • 可访问性灾难:Canvas fallback 内容和实际画出来的像素,本质上没有约束关系。开发者写了一个披着 aria-label 外衣的 <canvas>,但里面到底画了什么,屏幕阅读器和实际视觉内容完全是两套。
  • 国际化短板ctx.fillText 不处理 RTL(阿拉伯语/希伯来语)、竖排文字、复杂脚本连字。如果你的图表需要显示阿拉伯语标签,要么自己实现排版引擎,要么放弃。
  • 游戏 UI 的割裂:3D 场景中的 2D 界面(菜单、对话气泡、HUD),要么用 3D 引擎的内置 UI 系统(学习成本高),要么用 Canvas 手绘(质量差),要么用 DOM 覆盖层(无法与 3D 场景融合)。

HTML-in-Canvas 提案的目标就是:让开发者能把真正的 DOM 元素渲染到 Canvas 上,用浏览器的原生排版引擎干活

前传:为什么 Canvas 一直渲染不了 HTML

在深入 API 之前,先搞明白一个核心问题:为什么这件事以前做不到?

浏览器的渲染流水线大致是这样的:

JS → Style → Layout → Paint → Composite
  • Style:计算 CSS 规则
  • Layout:计算盒模型位置
  • Paint:生成绘制指令列表(display list)
  • Composite:合成图层

Canvas 的渲染是脱离这个流水线的。Canvas 的内容通过 JS 调用 Canvas API(fillRectdrawImage 等)写入一个位图缓冲区,然后直接作为一个纹理交给 GPU。浏览器的渲染引擎(Paint/Composite)对 Canvas 内部发生了什么一无所知。

而 HTML 元素的渲染走的是完整的 Style → Layout → Paint → Composite 管道。

所以,要把 HTML 渲染到 Canvas 上,本质上是要让浏览器的渲染管道和 Canvas 的像素缓冲区之间建立一座桥——而且这座桥不能破坏安全模型,不能引入性能问题,还要支持可访问性。

这比听起来难得多。直到 WICG/html-in-canvas 提出了一套优雅的解决方案。

核心原语一:layoutsubtree——一纸「委任状」

提案的第一个原语是一个 HTML 属性——layoutsubtree

<canvas id="myCanvas" layoutsubtree>
  <div id="myContent">
    <h2>Hello Canvas!</h2>
    <p>我可以在 Canvas 里用 HTML 渲染了!</p>
  </div>
</canvas>

加上这个属性的瞬间,发生了三件关键的事情:

  1. Canvas 的子元素获得了 stacking context,成为了其后代元素的 containing block
  2. Canvas 的子元素拥有了 paint containment(绘制包含)
  3. Canvas 的子元素参与了正常布局和 hit testing

翻译成人话:Canvas 的孩子虽然还在 DOM 树里,但它们的视觉渲染被「截胡」了——浏览器的 Paint 阶段不会再把这些孩子渲染到屏幕上,而是把它们的绘制结果存起来,等着开发者用 API 取走。 同时,它们仍然参与布局、可访问性树和事件命中测试。

这个设计有一个非常精妙的双重角色:同一个元素既是视觉内容(被绘制到 Canvas 中),也是可访问性内容(作为 Canvas fallback)。不像现在的 <canvas> fallback——画出来的东西和 fallback 内容是两套,永远有不同步的风险。在 HTML-in-Canvas 中,它们就是同一个东西。

layoutsubtree 就像是一纸「委任状」:告诉浏览器「这些孩子交给我来画,但请帮我把它们的布局和绘制结果准备好。」

核心原语二:drawElementImage——画布上的「复印机」

有了 layoutsubtree 把布局和可访问性安排好,下一步就是把子元素「印」到 Canvas 上。这就是 drawElementImage()

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

canvas.onpaint = () => {
  ctx.reset();
  // 把 form_element 画到 Canvas 的 (100, 0) 位置
  const transform = ctx.drawElementImage(form_element, 100, 0);
  // 同步 DOM 位置
  form_element.style.transform = transform.toString();
};

核心行为

  • 只接受 Canvas 的直接子元素(这就是 layoutsubtree 标记的那些孩子)
  • 调用时,返回一个 DOMMatrix(CSS transform 矩阵),你需要把这个矩阵应用到元素的 style.transform 上,让 DOM 位置和画上去的位置保持一致
  • Canvas 的当前变换矩阵(CTM,Current Transformation Matrix)会作用于绘制——也就是说,你可以在画布上 ctx.rotate(45) 然后 drawElementImage,元素就会旋转
  • 子元素的 CSS transform 被忽略(原因见下文——如果不忽略会导致双重变换)
  • 溢出内容被裁切到元素的 border box

destination rect 参数(和 drawImage 一模一样):

// 最简形式:在 (x, y) 处以原始尺寸绘制
ctx.drawElementImage(element, x, y);

// 指定目标尺寸
ctx.drawElementImage(element, x, y, width, height);

// 带 source rect 裁剪
ctx.drawElementImage(element, sx, sy, sw, sh, dx, dy, dw, dh);

WebGL 版本的接口texElementImage2D,把元素渲染到纹理:

// 当你需要把 HTML 内容作为 3D 纹理时
gl.texElementImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, myElement);

WebGPU 版本的接口copyElementImageToTexture

queue.copyElementImageToTexture(myElement, destination);

一个 API,覆盖 2D Canvas、WebGL、WebGPU 三大图形上下文。

形象一点理解:drawElementImage 就像是把浏览器的渲染引擎当作一台复印机,你传一个 DOM 元素进去,它返回一页「复印件」——而且附带一个坐标映射表(DOMMatrix),告诉你怎么把这页「复印件」在画布上的位置同步给 DOM。

核心原语三:paint 事件——智能触发器

drawElementImage 画的是快照。问题来了:当元素的内容发生变化时(比如输入框里有文字输入),开发者怎么知道需要重新绘制?

这就是 paint 事件的用武之地:

canvas.addEventListener('paint', (event) => {
  // event.changedElements 包含渲染发生了变化的子元素
  ctx.reset();
  for (const el of event.changedElements) {
    const t = ctx.drawElementImage(el, 0, 0);
    el.style.transform = t.toString();
  }
});

关键特性

  1. 智能触发:只有当子元素的视觉渲染真正发生变化时才会触发,而不是 60fps 无脑循环。省电、省 CPU。
  2. 时机:在浏览器每一帧渲染管线的 update-the-rendering 阶段中,紧跟在 intersection observer 步骤之后、Paint 步骤之前触发。
... → IntersectionObserver → paint event → Paint → Composite → ...
  1. CSS transform 变化不触发:因为 transform 影响的是位置而非渲染内容,改变 transform 不会重新生成 paint 指令,所以不触发 paint 事件。
  2. paint 内的 DOM 改动推迟到下一帧:你在 paint 回调里改了元素的 class/文本,这一帧不会生效,下一帧才会。
  3. requestPaint():如果你需要每帧都重绘(类似游戏循环),可以调用 canvas.requestPaint() 强制触发 paint 事件,行为和 requestAnimationFrame 类似。

Bonus:captureElementImage——通往 Worker 的传送门

有一个问题:上面的所有 API 都依赖 DOM 元素引用,但 Web Worker 中无法访问 DOM

解决方案是 captureElementImage()

// 主线程:捕获快照并传送到 Worker
canvas.onpaint = () => {
  const elementImage = canvas.captureElementImage(form_element);
  worker.postMessage({ elementImage }, [elementImage]);  // Transferable!
};

// Worker:直接绘制
self.onmessage = (e) => {
  if (e.data.elementImage) {
    ctx.drawElementImage(e.data.elementImage, 100, 0);
  }
};

ElementImage 是一个 Transferable 对象,和 ImageBitmapArrayBuffer 一样,支持零拷贝传输。这为 OffscreenCanvas 在 Worker 中高性能渲染 HTML 内容铺平了道路。

整个对象只有三个方法/属性:

  • width / height:快照的尺寸
  • close():释放资源

轻量、简洁、高效。

深水区:事件循环中的时序博弈

如果说前面的 API 是"皮毛",那下面这部分才是 HTML-in-Canvas 最深的设计决策——paint 事件到底应该在哪一刻触发?

规范文档中记录了三种方案,我们逐一分析:

Option A:在 ResizeObserver 时机触发(带循环)

位置:在 update-the-rendering 流程的第 16.2.6 步(Deliver resize observations),如果 paint 事件中又修改了样式,就循环回到第 16.2.1 步(Recalculate styles and update layout)。

问题

  • 需要在这个时间点同步执行 Paint 步骤来生成子元素的绘制快照。Paint 本身很消耗性能,还要可能跑多次。
  • Gecko(Firefox 的渲染引擎)的架构导致了这里实现困难——某些引擎在这个时间点根本拿不到完整的绘制结果。
  • 最致命的问题:WebGL。WebGL 的 gl.getError()gl.getParameter() 等 API 需要触发 GPU 命令缓冲区刷新(flush),如果在 Paint 完成之前调用,会导致死锁或不一致的渲染状态。

Option B:紧接在 Paint 步骤之后触发(带循环)

位置:在浏览器的 Paint 步骤完成后立即触发。

优势:不需要上面那种"同步 Paint Canvas 子元素"的操作——因为 Paint 已经跑完了,每个元素的绘制结果是可用的。

问题:仍然需要循环。如果 paint event 中改动了 DOM,又得回退到 style recalc → layout → paint 的完整循环,可能在一次帧中跑多次,十分昂贵。

Option C:紧接在 Paint 步骤之后触发(循环)—— 被选中的方案

核心思想:paint event 在一帧中只跑一次

如果开发者在 paint event 中修改了 DOM?改了就改了,但这一帧已经锁死了——DOM 修改的效果留到下一帧的渲染管线去处理。

这带来一个非常有趣的对称性:浏览器的 Paint 步骤也是不可循环的——你无法在一个帧内让浏览器画两次。paint event 的行为和浏览器原生的 Paint 步骤完全对齐。

方案  |  循环  |  是否需要同步 Paint  |  兼容 WebGL  |  复杂度
A     |  是    |  是                  |  否(死锁)  |  高
B     |  是    |  否                  |  是          |  高
C     |  否    |  否                  |  是          |  低 ✅

这个决策过程是 HTML-in-Canvas 提案中最精妙的设计之一。它不强求"开发者改了我就立刻刷新",而是承认一帧内做到绝对实时是不现实的,通过"延迟到下一帧"来换取架构的简洁性和跨浏览器兼容性。

就像 React 的虚拟 DOM 不追求"每次修改立刻更新真实 DOM"一样,HTML-in-Canvas 也不追求"每个 CSS 变化都立刻刷新 Canvas 绘制"。延迟带来一致性。

同步公式:CSS Transform 背后的线性代数

前面提到,drawElementImage() 返回一个 DOMMatrix,需要设置到元素的 style.transform 上。为什么要这么做?

因为浏览器的 hit testing(点击命中测试)、intersection observer、可访问性功能都依赖元素的 DOM 位置。如果你把一个 <div> 画到了 Canvas 的 (100, 200) 位置,但它在 DOM 树中还在原始位置,点击 (100, 200) 就命中不了这个元素。

解决方案是:把 DOM 元素通过 CSS transform 移动到与绘制位置匹配。

drawElementImage() 返回的 DOMMatrix 就是按如下公式计算的:

T_sync = T_origin⁻¹ · S_css→grid⁻¹ · T_draw · S_css→grid · T_origin

其中:

  • T_draw:绘制到 Canvas 上的变换矩阵,等于 CTM · T(x, y) · S(destScale)(CTM + 位置偏移 + 缩放)
  • T_origin:元素的 transform-origin 矩阵
  • S_css→grid:CSS 像素到 Canvas 网格像素的缩放矩阵

直观理解:这个公式做的事情就是把"在 Canvas 网格坐标系中的绘制位置"反向映射回"DOM 中的 CSS 像素位置"

对于 WebGL/WebGPU 中的 3D 场景,还有一个辅助方法 canvas.getElementTransform(element, drawTransform),让你传入任意变换矩阵并计算出对应的 CSS transform。

// 2D Canvas 直接返回
const transform = ctx.drawElementImage(element, x, y);

// WebGL/WebGPU 需要手动计算
const drawTransform = new DOMMatrix([...]); // 你自定义的 3D 变换
const cssTransform = canvas.getElementTransform(element, drawTransform);
element.style.transform = cssTransform.toString();

重要提醒:CSS transform 的变化不会触发 paint 事件——因为 transform 只影响位置,不影响绘制内容,所以 paint event 不会因为你在同步 transform 而反复触发。这避免了死循环。

隐私保护:看不见的边界

drawElementImage() 能让 Canvas 读取 DOM 元素的像素,这就带来了一个安全问题:如果 Canvas 能读取任何元素的内容,那跨域保护怎么办?

提案的隐私模型遵循一个核心理念:drawElementImage 不会暴露任何 JavaScript 当前不可访问的信息。 换句话说,它不会打开新的攻击面。

被排除在绘制之外的敏感内容:

排除项 原因
跨域 iframe、跨域图片 同 Canvas drawImage 的跨域保护一致
CSS url() 引用的跨域资源(如 background-image 同上
系统颜色/主题/偏好 否则可通过像素读取猜出系统主题
拼写/语法检查标记 可能暴露用户的拼写习惯
已访问链接的颜色 经典的隐私泄露向量
自动填充(autofill)预览内容 包含敏感个人信息
次像素抗锯齿 可用作浏览器指纹

不被视为敏感(允许绘制)的内容:

保留项 理由
页面查找(Find in Page)高亮 低安全性影响
滚动条和表单控件外观 已可通过 SVG foreignObject 检测
光标闪烁频率 低熵信息
forced-colors 模式 已可通过 CSS media query 获取

注意:这是预防性设计——在提案还处于 WICG 孵化阶段就考虑了完整的安全模型。这与 W3C TAG 审查(issue #1204)和 WHATWG 标准化讨论中的安全关注点保持一致。

生态地图:谁已经上车了

虽然提案还在孵化中,浏览器端只有 Chrome Canary 和 Brave Stable (Chromium 147+) 通过 flag 支持,但开源社区已经在积极适配:

three.js — 原生 WebGL 纹理集成

mrdoob/three.js 已经在 WebGL 和 WebGPU 两个渲染后端中集成了 HTML-in-Canvas:

// three.js 内部实现(简化)
if ('texElementImage2D' in gl) {
  const canvas = gl.canvas;
  if (!canvas.hasAttribute('layoutsubtree')) {
    canvas.setAttribute('layoutsubtree', 'true');
  }
  // HTML 元素直接作为纹理源
  gl.texElementImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, htmlElement);
}

这意味你可以把任意的 HTML 内容作为 three.js 的纹理,直接贴到 3D 模型上。

相关 PR: mrdoob/three.js#31233

PlayCanvas — 3D 产品配置器 + HtmlSync

PlayCanvas 引擎甚至在官方示例中完整实现了基于 HTML-in-Canvas 的交互式 3D 产品配置器。一个 HTML 面板被渲染为 WebGL 纹理,用户点击 3D 场景中的 HTML 按钮时的 hit testing 完全由浏览器的原生 DOM 事件处理——通过 getElementTransform 同步位置。

关键助手类 HtmlSync 被设计为可复用的工具类,处理 canvas ↔ 3D 平面的坐标映射。

VFX-JS — 视觉特效框架

fand/vfx-js 提供了一个优雅的 addHTML() 方法:

const vfx = new VFX();
await vfx.addHTML(element, { shader: 'liquidGlass' });

它内部先检查 supportsHtmlInCanvas(),如果可用就使用原生 API,否则优雅降级到传统的 dom-to-canvas 方案——渐进增强的最佳实践。

three-html-render — 纯 JS Polyfill

最令人兴奋的生态项目之一是 repalash/three-html-render——一个在浏览器不支持原生 API 时的 Polyfill。它通过 CSS matrix3d() 变换和 iframe / embed 技术模拟了 drawElementImage 的核心行为。

即使你的用户没有启用 chrome://flags/#canvas-draw-element,这个 Polyfill 也能工作。这是一个很聪明的策略——用 Polyfill 降低采用门槛,让框架生产环境可用。

未解之谜与未来方向

提案仍处于活跃讨论中(仓库中有 16 个 open issues),以下几个话题值得关注:

Open Issues 选读

Issue 核心问题
#94 — Hit testing and layer ordering draw 多个元素时,z-index 如何与 hit testing 协调?
#85removedElements 当子元素被删除,paint 事件是否需要提供单独的 removedElements 列表?
#82 — 新的指纹向量 onpaint 事件即使不读取像素,也能通过监听事件频率来获取指纹信息(如光标闪烁频率)
#31 — 动图/视频支持 GIF、WebP 动画、视频元素如何支持?
#47mix-blend-modebackdrop-filter 效果在 Canvas 中未正确反映

未来:自动更新 Canvas

规范文档中提到了一个令人兴奋的未来方向——auto-updating canvas

目前的模型是:你在 paint 事件中调用 drawElementImage,浏览器绘制快照。但如果支持了「自动更新模式」,drawElementImage 会在 Canvas 的命令缓冲区中记录一个"占位符",浏览器可以在滚动或动画更新时自动重新执行绘制,无需阻塞 JS 主线程

这意味着 Canvas 中的 HTML 内容可以和原生滚动完美同步,不再受 JS 事件循环的延迟影响。这个模式对 2D Canvas 已可行,对 WebGPU 也只需少量 API 扩展。

标准化进程

提案正处于标准化流程的以下位置:

WICG 孵化(当前)→ WHATWG Stage 2 → WHATWG Standard → 浏览器默认启用
  • WHATWG Spec PR: #11588
  • W3C TAG 早期审查: #1204(2026年3月启动)
  • 跨浏览器共识:Chromium / Gecko / WebKit 已在设计上达成一致(paint 事件 Option C 时序)

总结

HTML-in-Canvas 不只是 Canvas 的一个新功能——它是 Web 图形平台二十年来最重要的一次基础能力补全。

它的核心贡献不是加了几个 API,而是在浏览器的渲染流水线和 Canvas 的像素缓冲区之间,架起了一座精心设计的桥梁

  • layoutsubtree 用属性声明边界
  • drawElementImage 用返回值解决同步
  • paint 用精妙的时序设计避免死循环和性能灾难
  • captureElementImage 用 Transferable 搞定 Worker 并行

Three primitives + one helper,四个接口把"把 DOM 渲染到 Canvas"从不可能变成了可能——而且是在不破坏现有安全模型、不影响性能、保持可访问性的前提下。

三个核心判断:

  1. 技术设计质量很高:从事件时序的选择(Option C)到隐私模型的预防性设计,到 drawElementImage 的返回值用作 style.transform,每个决策都有清晰的权衡分析。这不是一个"先上线再说"的功能。

  2. 生态已经开始拥抱:three.js、PlayCanvas、VFX-JS 等知名图形项目的积极适配远超预期。尤其在 3D 游戏和可视化领域,需求非常强烈。

  3. 还有一段路要走:目前只在 Chromium flag 后可用,Firefox 和 Safari 还没有明确的实现计划。标准化进程仍在 WICG 阶段。

如果你是图形/可视化方向的开发者,建议立刻打开 Chrome Canary,启用 chrome://flags/#canvas-draw-element,跑一下官方 Demo。虽然它还不是正式标准,但方向已经明确——而且这个方向,可能改变前端图形生态的底层逻辑。


关注 【iDao技术魔方】,获取更多全栈到AI可落地的实战干货。

别让爬虫白嫖你的导航站了:纯免费,手把手实现加密字体防爬

一种让爬虫抓狂、用户无感的轻量级反爬方案

你是否也经历过这样的痛?

熬夜整理的导航站,上线没几天,内容就被竞品爬虫一锅端。链接、描述、分类,一字不差地出现在别人网站上。而你除了骂两句,似乎什么都做不了。

传统的反爬手段——验证码、频率限制、IP封禁——要么影响用户体验,要么成本高昂,要么道高一尺魔高一丈。

今天,我想和你分享一个「四两拨千斤」的方案:自定义加密字体

它的核心逻辑简单到令人意外:让浏览器正常显示文字,但爬虫抓到的源码全是乱码。整个过程完全免费,无需写代码,10分钟就能搞定第一版

先看一眼最终效果:

  • 你看到的网页:AI写作工具、AI图像生成
  • 爬虫抓到的源码&#xe073; &#xe219;  —— 一串毫无意义的私有编码

这就是加密字体的魔力。下面,我从原理到实操,完整拆解给你。


一、先理解原理:为什么几个字符能挡住爬虫?

要理解加密字体,我们得先回到文字显示的最底层。

正常情况下的文字显示:

每个字符在计算机世界都有一个唯一的「身份证号」,也就是 Unicode 编码。比如:

  • 字母 A → U+0041
  • 数字 1 → U+0031
  • 汉字"测" → U+6D4B

普通字体文件的作用,就是在这些标准编码位置上,放上对应的字形图案。浏览器读取到 U+0041,就去字体里找到那个位置,把"A"画出来。爬虫抓取源码时,自然也拿到的是明文。

加密字体做了什么?

这里引入一个关键概念:Unicode 私有空白区域(U+E000 ~ U+F8FF)。

这是 Unicode 标准里专门预留的一段编码空间,没有任何系统默认字体覆盖。普通环境下,这些编码对应的位置是空的,浏览器只能显示方框或问号。

加密字体的核心策略,就是「鸠占鹊巢」:

  1. 前端代码写的是正常明文:ChatGPT、AI导航、工具
  2. CSS 指定使用自定义加密字体
  3. 字体内部做了强制映射:把"A"的字形从标准位置 U+0041,搬到了私有区域 U+E073;把"工"的字形从 U+5DE5,搬到了 U+E331
  4. 同时删掉原位置的字形:标准编码 U+0041 的位置变成空白

于是产生了奇妙的双面效果:

  • 普通爬虫/默认字体下 → 访问私有编码区域 → 乱码/方框
  • 你的网页 + 专属加密字体 → 正确读取私有区域字形 → 完美显示

就像你给所有文字换了锁,只有你的网站拿着对应的钥匙。


二、第一步:确定你要保护哪些字符

加密字体不是越大越好,恰恰相反——越小越好

每多一个字符,字体文件就大一点,加载就慢一点。所以我们要精打细算,只加密「真正需要保护」的内容。

对于大多数导航站,三类字符就够了:

1. 大小写英文字母:a-z A-Z
2. 数字:0-9  
3. 导航常用中文(精简到极致):
   AI导航、电商、素材、开发、办公、智能体、
   聊天、模型、设计、工具、资源、免费、付费、
   官网、链接、搜索、推荐、热门、最新
4. 常用符号:/ : . - _ @

按照这个范围精简,最终字体文件只有 20~80KB。作为对比,一个普通中文字体动辄 5MB 以上。这个体积对网页加载几乎没影响,放到 CDN 上更是秒开。


三、方案一:在线可视化制作(零基础首选)

如果你不想安装任何软件,这套方案 10 分钟就能跑通。

用到的工具(全部免费):

  • Glyphr Studio 在线字体编辑器:glyphr.studio/
  • 任意 Unicode 编码查询网站(百度搜一下就有)

完整制作步骤:

步骤1:导入基础字体骨架

打开 Glyphr Studio,新建一个空白字体项目。给它起个名字,比如 mysite-encrypt-font

然后导入一个轻量级的基础字体作为骨架。推荐用 Inter(英文)或 思源黑体精简版(中英文)。这个基础字体只是提供初始字形轮廓,后面会被我们大幅改造。

步骤2:清空私有编码区

这是最关键的一步。

在左侧字形列表里,找到编码范围 E000EFFF。这些就是我们要利用的私有区域。把它们全部清空,确保每个位置都是空白画布。

步骤3:建立字符映射表

现在,我们需要决定每个文字搬到哪个私有编码位置。

这条是安全核心——映射必须随机打乱,绝不能顺序排列。

❌ 错误示范(顺序递增,容易被逆向推演):

A → E001
B → E002  
C → E003

✅ 正确做法(完全随机乱序):

A → E073
B → E219
C → E156
工具 → E331 / E092
AI导航 → E400 / E512 / E087

你完全可以自己设计一套只有你知道的映射规则。就算扔个骰子随机分配,也比顺序排列安全百倍。

这是我推荐的一套映射结构,你可以直接参考:

原文字 标准编码 私有加密编码
A U+0041 U+E073
I U+0049 U+E219
U+5BFC U+E087
U+822A U+E512
U+5DE5 U+E331
U+5177 U+E092
1 U+0031 U+E201
. U+002E U+E3A1

具体操作:

  1. 在编辑器里选中正常文字"A"的字形
  2. 复制这个字形
  3. 跳到私有编码 U+E073 的画布,粘贴进去
  4. 重复这个过程,把你需要的所有文字都「搬家」到各自的私有编码位置

步骤4:删掉原位置的字形

所有文字都搬完家后,回到它们的标准编码位置(U+0041、U+5DE5 等),把这些位置的原始字形全部删除

这一步相当于「毁掉原路」——任何使用标准编码来访问你字体的人,只能看到一片空白。

步骤5:导出字体文件

顶部菜单选择导出,格式务必选 WOFF2。这是专门为网页优化的字体格式,体积最小,兼容性也好。

下载下来,你得到了一个 encrypt.woff2 文件。这就是你的专属加密武器。


四、方案二:FontForge 专业工具(适合长期维护)

如果你需要加密的字符特别多,或者计划定期更换加密方案,推荐用 FontForge。

下载地址:fontforge.org/ 免费开源,Windows 和 Mac 都能用。

它的核心操作逻辑和在线版完全一致,但支持批量操作

  1. 打开字体文件
  2. 批量选中需要加密的字符
  3. 「编辑 → 复制字形」
  4. 跳转到 Private Use Area(U+E000 ~ F8FF)
  5. 批量粘贴并重新绑定编码
  6. 删除原生编码位置的原始字形
  7. 压缩并导出 woff2

FontForge 的学习曲线稍陡,但一旦上手,制作一套新映射字体只需几分钟。适合需要定期轮换加密方案的场景。


五、进阶安全:让破解者无从下手

加密字体的原理决定了它有天然的安全性,但想做得更稳妥,还有几件事值得注意。

1. 映射关系的随机性

再次强调:不要有任何可循的规律。不要按字母顺序,不要按拼音顺序,不要用简单算法。最安全的方式就是真随机——把要保护的字符列表扔进随机数生成器,随机分配私有编码位置。

2. 定期轮换加密方案

任何防御都不是一劳永逸的。如果有心人花大量时间手工对比你的网页显示和源码输出,理论上可以逐渐还原映射表。

但你可以让这个成本变得高到不划算:

  • 每 30 天重新制作一套新的映射字体
  • R2 上替换文件,前端改一行 CSS 链接
  • 映射规则全部重排,旧的爬虫规则立刻失效

维护成本几乎为零,但破解者的积累全部归零。

3. 字体文件的极致精简

再强调一次:只打包你真正用到的字符

全量中文字体 5MB+,精简到导航常用字 30KB 以内。这不仅提升了加载速度,也减少了字体文件被反编译分析的风险面。

4. SEO 的平衡处理

加密字体主要针对的是恶意爬虫,而不是搜索引擎。如果你的站点依赖 SEO 流量,可以加一层中间件判断:

  • 识别百度/谷歌爬虫的 User-Agent → 返回纯文本版本,不加载加密字体
  • 普通无头浏览器/未知爬虫 → 走加密字体渲染

这样既保护了内容不被同行抓取,又不影响正常收录。


六、前端接入:三步上线

制作好的字体怎么部署到网站上?非常简单。

步骤1:上传字体到 Cloudflare R2

在 Cloudflare 后台新建一个 R2 存储桶,把 encrypt.woff2 上传进去,开启公开访问权限。

你会得到一个直链地址,类似:

https://xxx.r2.cloudflarestorage.com/fonts/encrypt.woff2

R2 有每月 10GB 的免费流量额度,对字体文件这种小体积资源完全够用。如果你习惯用其他对象存储(阿里云 OSS、腾讯云 COS 等),流程一样。

步骤2:在 CSS 中声明加密字体

@font-face {
  font-family: 'site-encrypt';
  src: url('https://你的R2地址/fonts/encrypt.woff2') format('woff2');
  font-weight: normal;
  font-style: normal;
  /* 指定字体生效的编码范围 */
  unicode-range: U+0030-007A, U+4E00-9FA5, U+E000-F8FF;
  font-display: swap;
}

.text-encrypt {
  font-family: 'site-encrypt' !important;
  /* 额外保护:禁止选中文本 */
  user-select: none;
}

步骤3:给需要保护的文字加上类名

<!-- 源码里是明文,但渲染出来的效果取决于字体 -->
<div class="text-encrypt">
  AI写作工具、AI图像生成、https://idao.fun
</div>

就这么简单。你的 HTML 代码里写的仍然是正常文字,方便维护。但在浏览器渲染层面,走的已经是加密字体的私有编码映射。


七、验证效果:眼见为实

部署完成后,你可以从三个角度验证:

普通用户视角: 打开你的网页,文字显示完全正常。字体、大小、颜色、主题适配——一切如常,用户感知不到任何差异。

爬虫视角(右键查看网页源码): 你看到的不是 AI写作工具,而是一堆类似 &#xe073; &#xe219;  的乱码字符串。爬虫无法从中提取可读的站点名称、链接描述等结构化信息。

开发者工具检测: 打开 F12,检查被保护的文字元素。你会看到浏览器实际渲染的是私有编码区域的内容,但 CSS 的 font-family 指向了你的加密字体,所以显示正常。


八、关于这个方案的一些补充说明

这种加密字体方案的本质,是增加了爬虫批量提取内容的成本,而不是绝对阻止。

如果对方真的铁了心要爬你的站,可以通过截图 OCR、手工比对等方式逐步还原。但对绝大多数批量抓取、自动采集的爬虫而言,这个额外成本足以让它们放弃你的站点,转向更轻松的猎物。

对于个人开发者和小团队来说,这是一个性价比极高的方案:零成本、低维护、不影响用户体验、不需要写复杂反爬逻辑。

你只需要花 10 分钟制作一个字体文件,就可以一劳永逸地保护你的导航站资源。

也许多年以后回头看,今天花在设置防爬上的这半小时,会是你整个项目里 ROI 最高的半小时。

独立开发者主流技术栈(2026最新)

一般情况独立开发者的技术栈核心追求:全栈统一、开发高效、部署简单、成本极低、生态完善,以下按Web、移动端、桌面端、数据库、运维/工具、AI辅助六大维度,整理当前最主流、最实用的技术选型(含热门组合与单类选项)。

一、Web全栈(最主流,SaaS/工具/网站首选)

1. 前端(React系,2026绝对主流)

  • 核心框架Next.js 15(全栈React,App Router+Server Components,一人搞定前后端)
  • 备选框架:Nuxt 3(Vue全栈,上手快)、Remix、SvelteKit
  • 样式方案Tailwind CSS + shadcn/ui(无样式组件+自由定制,开发最快)
  • 备选UI:Ant Design、Material Design、DaisyUI、Chakra UI
  • 状态管理:Zustand、Jotai、Redux Toolkit、Pinia(Vue)
  • 数据请求:TanStack Query(React Query)、SWR、Axios
  • 表单/验证:React Hook Form + Zod、Formik
  • 语言TypeScript(必选,类型安全,减少bug)

2. 后端(全栈JS/Python为主,轻量优先)

  • Node.js生态(最主流)
    • 框架:Express、NestJS(企业级)、Hono(轻量Edge)
    • 全栈:Next.js API Routes/Edge Functions(无需单独后端)
  • Python生态(AI/数据/快速原型)
    • 框架:FastAPI(高性能API)、Flask(极简)、Django(全功能)
  • 备选:Go(Gin,高性能)、Rust(Axum,安全高效)
  • ORMPrisma(全数据库支持,生态最好)、Drizzle(轻量Serverless)

二、移动端(跨平台优先,减少学习成本)

  • 跨平台首选Flutter(Dart,性能接近原生,一套代码双端)
  • Web开发者首选React Native(React语法,复用Web技能)
  • 轻量/小程序转App:UniApp(Vue语法,支持多端+小程序)、Taro
  • 原生(性能极致):Android(Kotlin+Jetpack Compose)、iOS(Swift+SwiftUI)

三、桌面端(跨平台,Web技术复用)

  • 主流Electron(React/Vue+Node,成熟稳定,如VS Code)
  • 新锐轻量Tauri(Rust后端,体积小、性能优)
  • 备选:Qt(C++,跨平台原生)、WPF(Windows原生)

四、数据库(免费+托管优先,减少运维)

1. 关系型(主流)

  • 托管首选Supabase(PostgreSQL,免费500MB,自带认证/存储/实时)
  • 备选托管:Neon、PlanetScale(Serverless MySQL)、Turso(SQLite)
  • 自建:PostgreSQL、MySQL(经典稳定)

2. 非关系型

  • 文档型:MongoDB(托管MongoDB Atlas)
  • 缓存/实时:Redis(托管Upstash)
  • 向量数据库(AI):Milvus、Pinecone、Chroma

五、运维/部署/工具(零成本+自动化)

  • 部署(免费额度足)
    • Web:Vercel(Next.js最佳搭档,一键部署)、Cloudflare Pages
    • Serverless:Cloudflare Workers(免费10万次/天)、Vercel Edge Functions
  • 认证:Supabase Auth、NextAuth.js、Better Auth、Clerk
  • 支付(SaaS必备):Stripe(全球)、PayPal、微信/支付宝(国内)
  • 邮件:Resend(免费3000封/月)、Nodemailer
  • 存储:Cloudflare R2、AWS S3、Supabase Storage
  • 监控/分析:Sentry(错误)、Posthog、Umami、Plausible(用户分析)
  • CI/CD:GitHub Actions(免费)
  • 开发工具:VS Code、Git、Figma(设计)、Postman(API测试)

六、AI辅助(2026必备,效率翻倍)

  • 代码生成:GitHub Copilot、Cursor、Claude Code、Vercel v0(前端UI)
  • AI工具链:LangChain、LlamaIndex(大模型应用)、OpenAI/Anthropic API
  • 设计/素材:Midjourney、DALL·E 3(图片)、Runway(视频)

七、2026独立开发者「黄金技术栈组合」(直接抄作业)

  1. SaaS/Web应用(最强) Next.js 15 + TypeScript + Tailwind + shadcn/ui + Zustand + Supabase + Vercel + Stripe
  2. Vue生态(易上手) Nuxt 3 + Tailwind + Supabase + Prisma + Pinia + Vercel
  3. AI应用 Next.js + FastAPI(Python) + Supabase + Pinecone(向量) + OpenAI API
  4. 移动App Flutter + Supabase + Riverpod(状态)

八、选型核心原则(独立开发必看)

  1. 全栈统一:优先JS/TS(前后端同语言),减少切换成本
  2. 托管优先:不用自建服务器,用Supabase/Vercel等BaaS,零运维
  3. 免费起步:所有工具选有 generous 免费额度的,验证PMF再付费
  4. 生态成熟:选文档全、社区大、坑少的技术,独立开发没时间踩坑
  5. AI赋能:全程用AI工具,代码/设计/文案全流程提效
❌