Web 平台 API / URL Anatomy · 一条网址是怎么拆开的 / 扩展:nanoid——path 里那段「机器读的 id」 待审核 2 / 2
扩展 · nanoid

扩展:nanoid——path 里那段「机器读的 id」

slug 那节给的是「给人看」的标识符;但很多资源没有适合派生 slug 的标题,或者干脆需要一个不透出任何内部信息的随机 id。这正是 nanoid 的位置:一个极小(压缩后约 130 字节)、用密码学随机源、且天生 URL 安全的唯一 id 生成器。它和 slug 互为补充——slug 给人读,nanoid 给机器认,两者都能直接出现在 path 里。

本页所有 id 都是当场用浏览器的 crypto.getRandomValues 跑 nanoid 算法得出的,不是预置字符串;熵与碰撞概率也按所选参数实时计算。

1 · 现场生成

默认 nanoid() 返回 21 个字符,每一个都只由 A-Za-z0-9_- 组成,可以原样塞进 URL 任何一段,无需 percent-encoding。

图 1-1 · 可拖动滑块改长度、点按钮重新生成,观察结果的字符范围始终不变。

核心只有几行:crypto.getRandomValues 一次取 size 个密码学随机字节,每个字节用 & 63 取低 6 位(006363),正好索引到 64 字符字母表里的一个字符。没有 Math.random、没有时间戳、没有计数器——因此不可预测、不可枚举,适合做对外暴露的资源 id。

2 · 字母表与 unreserved 的关系

nanoid 默认字母表共 64 个字符,全部落在 percent-encoding 那节讲的 RFC 3986 unreserved 集合之内,因此任何实现都不会编码它们。

图 2-1 · 64 个字符逐个摊开,符号位(- 与 _)单独标色。顺序被打乱过,字符集合不变。

警示 · 常见的说法是「nanoid 的字母表就等于 unreserved」,这不准确。RFC 3986 §2.3 的 unreserved 是 ALPHA / DIGIT / "-" / "." / "_" / "~",共 66 个字符;nanoid 的字母表是它的真子集,少了 .~。少这两个不是疏忽——舍掉它们才凑成 64=2664 = 2^6,让第 4 节那套掩码取值恰好零废弃。

由于全部字符都属 unreserved,nanoid 出现在 pathqueryfragment 任意位置都原样保留,既不会被 encodeURIComponent 改写,也不会在双重编码或签名对账里出错。这就是它名字里 URL-friendly 的含义。

3 · 熵与碰撞概率

id 够不够唯一,取决于它的熵(bit)=size×log2(字母表大小)\text{熵(bit)} = size \times \log_2(\text{字母表大小})

图 3-1 · 可选一种字母表、调一个长度,看 ID 空间有多大,以及按生日界估算要生成多少个才有 1% 的碰撞概率。

与 UUID v4 对比能看出差距在哪:UUID v4 有 122 bit 随机熵,但字符串长 36 位(32 个十六进制位加 4 个连字符)。nanoid 默认 21 位就有 126 bit——因为字母表更大(64 对 16),同样的安全度下串更短,且不带 UUID 那种固定格式。

4 · 为什么不用「取随机数再取模」

要把一个随机字节(00255255)映射到一个 KK 字符的字母表,最直觉的写法是 byte % K——但这会引入 modulo bias:当 256 不能被 KK 整除时,排在前面的 256modK256 \bmod K 个字符会比其余字符多分到一个字节值,于是出现得更频繁,熵被悄悄削弱。

图 4-1 · 用真实随机字节各打 6 万个样本,对比取模与掩码两种映射的分布。可选不同 K 看效果,K 取 2 的幂时两者重合。

nanoid 的做法是先把 KK 向上取到最接近的「2 的幂减一」做掩码,mask=(2log2(K1))1mask = (2 \ll \log_2(K-1)) - 1,用 byte & mask 取值;落在 [0,K)[0, K) 之外的就丢弃重抽 (rejection sampling)。这样每个字符的概率严格相等,没有 bias。

建议 · 默认字母表取 64 是把三件事一次性对齐的结果。64=2664 = 2^6 是 2 的整数次幂,256 能被它整除,取模根本不产生 bias;掩码 byte & 63 又永不触发 rejection,于是默认路径既均匀又零废弃、零循环,就是第 1 节那段 id += alphabet[byte & 63]。URL 安全、分布均匀、实现最快,三者同时满足。

5 · 四种 id 的分工

URL 的 path 里那个标识符,可能是下面任意一种。它们不是互斥,而是分工。

图 5-1 · 自增主键、UUID、nanoid、slug 四种标识符的特性对照。可逐项展开看各自的取舍。

实务里常见的组合是:对内用自增主键(数据库友好、可排序),对外用 nanoid 或 UUID(不泄露规模、不可枚举),再叠一层 slug 让 URL 可读。例如 /posts/v1StGXR8_Z5jdHi6B-myT/hello-world——前者保唯一与不可猜,后者保可读。

那个可读的后半段 hello-world 怎么从标题派生,以及为什么这些字符进 URL 不用转义,都见 slug 与 percent-encoding 一节。