猫工具站
帮助中心 猫Blog 关于我们 广告合作 EN

UUID、二维码与 Base64:三个生成页失败时一声不吭

开发更新于 2026-09-30

三个"造东西"的页面:标识符、二维码、编码。它们是三件套里结构最简单的——谁也不把你的输入解析成另一种模型,拿去、生成、给你——也是最难判断成败的,因为容量不够、数字读不出、文件不认的时候,它们不给任何提示。其中一个甚至会给你一张全白的图片,外加一个能正常打开的文件名。

下面的内容全部来自浏览器实测:在中文页上生成 2,000 个 UUID 逐条比对 RFC 4122 的形状,在每个容错等级上二分出还能画出来的最长字符串,再造一批只有几个字节的假文件喂给 Base64 页——包括一个空文件和一个没有扩展名的文件。

UUID 的随机性来自浏览器,且只用到了这么多

UUID 生成器 是逐个字符独立生成的:

var arr = new Uint8Array(1);
crypto.getRandomValues(arr);
uuid += hex[arr[0] & 0x0f];

由此有两条结论。第一,熵源是浏览器提供的加密安全随机数,不是 Math.random(),所以这些串可以放心当密钥用——API Key、nonce、不希望被人猜到的 ID。第二,掩码是 & 0x0f,从 256 个值里等分取 16 个,没有取模偏置,没有任何一个十六进制字符比别的可能性高。代价是每个输出字符都要一次 CSPRNG 调用,一个 UUID 花 32 次——略微浪费,但完全无害。

两个固定位置让它成为一个 v4:

  • 第 14 位硬编码为 4,即版本号;
  • 第 19 位是 (arr[0] & 0x3) | 0x8,即 RFC 4122 规定的变体位,只能是 8、9、a、b 四个值之一。

在中文页上实测 2,000 个:8 495 个、9 489 个、a 516 个、b 500 个,0 个通不过标准式 xxxxxxxx-xxxx-4xxx-[89ab]xxx-xxxxxxxxxxxx 的校验。四个桶各约 500,变体位是均匀的,其余位置也是。

如果你在下游用严格正则校验这些串,[89ab] 这一格正是最常被人写错的地方,而这页写对了。

数量框有两套"默认值"

输入框是 <input type="number" min="1" max="100" value="10">,但 min/max 只是提示——上下箭头会遵守,手打不会。真正决定结果的是脚本:

var count = parseInt(document.getElementById('uuidCount').value, 10) || 10;
if (count < 1) count = 1;
if (count > 100) count = 100;

在中文页上逐个填入并点「重新生成」,得到的行数:

你填的 得到几行 原因
101 100 被夹回上限
-5 1 被夹到下限
2.7 2 parseInt 读到小数点就停
0 10 0 是假值,\|\| 10 生效
(空) 10 NaN 也是假值
abc 10 还是 NaN

中间那行是最让人意外的:0 不代表 0,任何浏览器读不出数字的内容也不代表 0。页面不会告诉你这个框填错了,它只是默默给你默认的十条。想要恰好一条,填 -5 可以,填 0 不行。

「大写」和「连字符」不会即时生效

选项行里有两个勾选框——「大写」和「连字符」,两个在 HTML 里都带 checked,也就是说页面首屏给你的十条本来就是全大写、带连字符的(实测首条形如 575B543D-FE68-475A-8824-640790874198,长度 36)。想要别的形状,必须取消勾选并且再点一次「重新生成」。

两个勾选项只在 generateUUIDs() 内部读取,别处不读。数量框和这两个勾选框都没有 change 或 input 监听——整页脚本里 addEventListener 出现 0 次,实测确认。于是:

  • 生成十条后取消「大写」,你眼前那十条仍然是大写;
  • 取消「连字符」,列表不重绘;
  • 把数量从 10 改成 5,十条还挂在页面上,直到你重新点「重新生成」。

这是个看不出来的坑,所以值得写下来:动完选项就点一次「重新生成」。先改勾选、再把眼前的行复制走,复制到的是旧设置。实测里我把两个勾都取消、读到的是 3d082a04ca3440f6b37367bc5ec56d95(32 位小写无连字符),再改回勾选、行内容一字未变。

勾选框本身一旦被读取,行为与文案完全一致。「大写」是对成品串调用 uuid.toUpperCase(),两种写法都是 36 字符;取消「连字符」后,循环仍走满 36 次,只是第 8、13、18、23 位改成拼接空串,实测长度 32,纯十六进制、无空格。两个都取消得到 32 位小写——它仍是合法的 v4 紧凑写法:硬编码的 4 落在第 13 个十六进制位上,变体位落在第 17 位,正是 RFC 4122 要求的位置(实测紧凑串第 13 位为 4、第 17 位在 8/9/a/b 内)。

每次点「重新生成」都会先清空列表(list.innerHTML = ''),所以无法点三次凑出 300 条——上一屏已经没了。跨次点击的唯一性不必担心:122 位随机性下撞车不是现实风险,但页面也确实没有做任何查重。

复制是逐行的,走 navigator.clipboard.writeText(),按钮文字变成 ✓ 后 800 毫秒还原;选项行右侧的「复制全部」成功后显示 ✓ 已复制全部 1200 毫秒。一个手机端值得知道的外观问题:每行的「复制」按钮样式是 opacity: 0,只在鼠标悬停时才显形(实测常态计算样式就是 0)。触屏上没有悬停,按钮看不见但仍在原处占着,你只知道有个可点的地方,不知道在哪。

二维码页连一条错误分支都没有

二维码生成器 是每次按键都重画(oninput="generateQR()"),尺寸和容错两个下拉也直接调用它。脚本末尾还会执行一次 generateQR(),但输入框初始是空的,所以那一次只画出占位文案「请输入内容生成二维码」——你敲进内容之后才有码。页面也不会自动填自己的地址,想把本页做成二维码,得自己把网址复制进输入框。

控件:

  • 尺寸:小 (160px)、中 (240px,默认)、大 (320px)、超大 (400px)。
  • 容错:L (7%)、M (15%,默认)、Q (25%)、H (30%)。

尺寸和容量无关,这一点正是大家吃亏的地方。像素只决定每个模块(点)画多大;一张码能装多少内容,由 QR 版本和容错等级决定。页面不会提醒你这回事,因为超出容量的时候它什么都不会提醒。

实测各等级还能画出来的最长 ASCII 串,以及再多一个字符的结果:

等级 能画 再多一个字符
L 2,950 字符 抛错,画布全空
M 2,328 字符 抛错,画布全空
Q 1,660 字符 抛错,画布全空
H 1,270 字符 抛错,画布全空

同一套测试换成中文,在 M 级上是 776 字通过、777 字失败。776 × 3 = 2,328,正好等于 ASCII 的上限——限的是 UTF-8 字节数,不是字符数。带中文的 URL 编码链接实际是它看起来的三倍贵,一个 emoji 四倍。

再一个细节:抛出的错误文本里带着等级的序号——L 报 (reading '0')、M 报 (reading '1')、Q 报 (reading '2')、H 报 (reading '3'),都是 TypeError: Cannot read properties of undefined。这条错误来自 CDN 上的绘图库,页面没有 try/catch,所以异常直接把 generateQR() 打断,后面的代码一行也不会执行。

超容量之后你手上是什么,实测:

  • 容器里只剩 95 个字符的标记:<div id="qrCodeEl"><canvas width="240" height="240"></canvas><img style="display: none;"></div>,那张 <img> 没有 src;
  • 画布上 57,600 个像素里 0 个不透明(正常画成功时是 57,600/57,600);
  • 「下载二维码」按钮照样可点,照样工作:它对这个空画布调用 canvas.toDataURL(),存出 qrcode.png——结构完全合法的 PNG,data URL 长 2,786 个字符,解出来是 2,072 字节的纯透明图。

所以这个失败不只是安静,它还很"圆满":下载完成了,文件能打开,看图软件给你一块白。以后谁发给你一个显示不出东西的二维码 PNG,就是这么来的。

实用的规则:如果什么都没出现,那是内容超出这个等级的容量——换到 L,而不是换更大的像素尺寸。下载之前先看一眼预览,因为这个按钮不会有任何"画布是空的"提示(实测超容状态下点下载,一条提示都没有)。要印出来就选 320 或 400px,但请记住它改变的只是每个模块画多大。

反过来,输入真正为空时页面反而会说话:那时容器里没有画布,点「下载二维码」会弹出「请先生成二维码」。整个三件套只有两处明确提示——这一处,以及 Base64 页的「请先上传图片」。内容超出容量,两处都不会出现。

Base64 相信浏览器对文件的猜测

图片转 Base64 一次只收一个文件——输入框是 accept="image/*" 且没有 multiple——输出可以是完整 Data URI 或纯 Base64(第一个选项、也就是默认,是完整 Data URI)。和 UUID 页那两个不工作的勾选框相反,这个下拉是活的:脚本给它挂了 addEventListener('change', updateOutput),所以传一次文件就能在两种形态之间来回切换,不用重选。

输出里的 MIME 来自 file.type,也就是你的操作系统和浏览器认为这个文件是什么,没有任何一处去读文件头。用只有几个字节的假文件实测:

文件 file.type 页面输出
a.png,8 字节任意内容 image/png data:image/png;base64,iVBORw0KGgo=
note.txt,5 字节 text/plain data:text/plain;base64,aGVsbG8=
doc.pdf,8 字节 application/pdf data:application/pdf;base64,JVBERi0xLjQ=
mystery(无扩展名) (空) data:application/octet-stream;base64,eHl6
mystery.png(构造时不给类型,file.type 为空) (空) data:application/octet-stream;base64,iVBORw0KGgo=
a.svg,118 字节真实 SVG image/svg+xml data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0…
empty.bin,0 字节 application/octet-stream data:application/octet-stream;base64,

两个后果。第一,那个类型是个标签,不是校验:一个装着 PDF 却叫 .png 的文件会得到 data:image/png: 前缀,浏览器随后拒绝显示它,而页面已经高高兴兴把结果给你了。第二,扩展名不起作用——上表第五行是一个叫 mystery.png 的文件,因为操作系统没给出类型,输出照样退到 application/octet-stream,前缀里也再没有"image"。

预览区在输出框下面,只对真正的图片有用。对其他文件它做了什么,实测:它给 <img> 加上 show 类并把 src 设成这个 data URI,于是框子看起来是就绪的(计算样式从 none 变成 inline-block)。而对 PNG 残片、文本文件、PDF、无类型文件、空文件,图片的 naturalWidth × naturalHeight 全是 0×0,什么都不渲染。只有那份真 SVG 画出了东西(300×150)。空文件还有它自己的特例:输出是 data:application/octet-stream;base64,,逗号后面什么都没有,信息行写着 empty.bin | 0 B。

大小标签有个坑,建议在编码大文件前先知道。格式化函数在 1 KB 和 1 MB 处换单位,此后不再换:

字节数 显示为
0 0 B
1,023 1023 B
1,024 1.0 KB
1,536 1.5 KB
1,048,575 1024.0 KB
1,048,576 1.00 MB
1,073,741,824(1 GiB) 1024.00 MB
5,368,709,120(5 GiB) 5120.00 MB

MB 以上没有单位,1 GB 会自报 1024 MB;小数位数也随单位变——KB 一位、MB 两位。这是外观问题,但它也是你拿到的唯一大小反馈,而这里的大文件意味着一个大的字符串:Base64 的开销约是原文件的四分之三倍,且整个结果以文本形式待在标签页里。实测一个 3,000 字节的文件,纯 Base64 输出 4,000 个字符,完整 Data URI 是 4,022 个字符(前缀占 22),信息行写 big.png | 2.9 KB。没有任何东西被上传——页面脚本里一次请求都没有——但一张 20 MB 的图就是 <textarea> 里一个 27 MB 的字符串,代价由标签页付。

「清除」会把两个框都清空、撤掉预览的 show 类、清掉信息行、也清掉已选文件,唯独不动你的格式选择(实测清除后下拉仍写着 raw);此时再切换格式也不会让旧内容回来,因为缓存的 data URI 已经置空。复制一个空框会弹出「请先上传图片」,成功时则是「已复制到剪贴板」的弹窗——注意这是 alert,不是行内提示,它会真的挡住页面直到你点确定。

哪个活儿用哪个页

你要做的事 用哪个页 注意什么
不能被人猜到的标识符 uuid-generator 改完勾选要再点「重新生成」
恰好一个 UUID uuid-generator,数量填 -5 填 0 和 abc 都给你十条
短链接的码 qrcode-generator,M 级 没事
长 URL 的码 qrcode-generator,换 L 级 调大像素没有用
要打印的码 qrcode-generator,320 或 400px 下载前先看有没有画出来
内联进 CSS/HTML 的图片 image/base64,完整 Data URI MIME 是猜的,不是校验
给 API 请求体的 Base64 image/base64,纯 Base64 清除保留这个选择,可能已经是它
把图片链接发给人 qrcode-generator,先把链接粘进输入框 输入框是空的,本页地址不会自动填进去

FAQ

这些 UUID 能当令牌用吗? 能。随机数来自 crypto.getRandomValues,即浏览器的加密安全来源,30 个自由字符每个都是从完整字节里等分取出的十六进制位,没有偏置。形状是 v4:中文页实测 2,000 条全部通过 RFC 式校验,变体位落在 8/9/a/b 四个合法值上且分布均匀。

为什么页面不理我的勾选? 因为勾选项只在「重新生成」执行时读取。勾选框和数量框上没有任何监听器,已有列表不会重绘。两个框默认都是勾上的,所以首屏那十条是大写带连字符;取消一个之后必须再点一次「重新生成」。

我填 0 条,却给了 10 条,哪个对? 10 条,这是脚本写死的:parseInt(value, 10) || 10,0 是假值所以取默认。想要最少一条,填 -5。

我下载到的二维码是一张全白方块,文件坏了吗? 文件没坏,是码从来没被画出来。你的文本超出了该容错等级的字节容量,绘图库抛错,页面没有捕获任何东西,「下载二维码」于是把空画布导了出来。换 L 级或缩短内容,把像素尺寸调大不会有任何改变。

到底能装多少内容? 实测 L 级 2,950 个 ASCII 字符、M 级 2,328、Q 级 1,660、H 级 1,270,多一个字符就画不出。请按 UTF-8 字节算:同一套测试里中文正好是 ASCII 的三倍贵(M 级 776 个汉字)。

为什么页面打开时没有码,只有一张空占位? 因为输入框初始是空的,generateQR() 遇到空内容只画「请输入内容生成二维码」这句占位文案。它也不会替你填本页地址——想给当前页面生成二维码,自己把网址粘进输入框。

我的图片为什么不预览? 看你传的是什么。页面对任何文件都会把预览标记成"已显示",但只有真图片才有非零尺寸——PNG 残片、文本文件、PDF 全部报 0×0,却照样产出了文本。编码本身是成功的。

能用它转 PDF 或字体吗? 能,把文件拖到区域上即可——accept="image/*" 只过滤文件选择器,不作用于拖拽处理器,脚本也不看扩展名。结果只会按浏览器推断出的 MIME 打标签,推不出来就是 application/octet-stream。

这些操作会离开我的电脑吗? 你的内容不会。UUID 页用浏览器熵池,二维码页本地绘制,Base64 页用 FileReader 在同一个标签页里读文件,三个页面自身都一次请求都不发。有一个诚实的例外:二维码页的绘图库是从 cdn.jsdelivr.net 加载的(qrcodejs@1.0.0,没有 integrity 校验),打开这个页面本身就是一次对第三方的请求——里面不含你的文本,但含你的 IP。Base64 页的代价是内存而非隐私:整串编码一直作为字符串留在页面里。

行上的「复制」按钮看不见? 它是刻意的样式:.copy-btn { opacity: 0 },只在 .uuid-row:hover 时变成 1(实测常态计算样式为 0)。触屏没有悬停,所以按钮在但不显示。另外 navigator.clipboard.writeText() 的失败分支是一个空的 .catch(function() {})——写入被浏览器拒绝时,页面不会有任何提示,也不会有 ✓。

本指南用到的工具

广告