关键词:UUID、crypto.randomUUID、polyfill、兼容性、Node、浏览器、TypeScript
一、背景:为什么不要直接 Math.random()
前端经常需要"全局唯一" id,常见场景:
场景 | 是否可重复 | 后果 |
---|---|---|
上传文件临时名 | ❌ | 秒传失效 / 覆盖 |
表单多条动态行 | ❌ | 删除错位 |
日志 / 埋点 traceId | ❌ | 链路串线 |
Math.random()
随机熵只有 48 bit,理论碰撞概率 1/2²⁴≈1/16M,在 toB 长生命周期系统里并不安全。
Web 标准早已给出加密级 方案:crypto.randomUUID()
,但存在三把"隐形锁":
- 仅 HTTPS / localhost(Secure Context)
- 2022 年才普及,IE、Chrome<92 直接抛错
- Node 需要 ≥14.17,且需
import { randomUUID } from 'node:crypto'
本文目标:一份代码,同时跑在
- 现代浏览器(优先原生)
- 旧浏览器(自动降级)
- Node / SSR(Jest、Next.js、Electron 主进程)
- 小程序(微信 / 支付宝,提供 polyfill 入口)
二、设计思路:能力探测 + 分层降级
arduino
┌─ ① 原生 crypto.randomUUID (最优)
├─ ② crypto.getRandomValues 手动 v4
├─ ③ Node crypto.randomUUID (SSR)
└─ ④ 最末兜底 Math.random(警告)
策略:
- 运行期探测,不污染全局
- Tree-shakable,无三方依赖,包体 <1 KB
- TypeScript 全类型,支持 ESM / CJS 双格式
- 可Mock,测试友好
三、源码:uuid.ts(零依赖)
typescript
/* eslint-disable no-console */
type UUID = `${string}-${string}-${string}-${string}-${string}`;
const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
/** 判断当前是否 Secure Context,可解锁 crypto.randomUUID */
const isSecureContext = () =>
typeof globalThis.crypto !== 'undefined' &&
typeof crypto.randomUUID === 'function';
/** 判断 Node 环境 */
const isNode = () =>
typeof process !== 'undefined' &&
process.versions?.node;
/** 使用 crypto.getRandomValues 手动拼 v4 UUID */
const uuidv4ByRandomValues = (): UUID => {
const bytes = new Uint8Array(16);
crypto.getRandomValues(bytes);
// Per RFC4122, set version (4) and variant (1)
bytes[6] = (bytes[6]! & 0x0f) | 0x40;
bytes[8] = (bytes[8]! & 0x3f) | 0x80;
const hex = [...bytes].map(b => b.toString(16).padStart(2, '0'));
return `${hex.slice(0, 4).join('')}-${hex[4]}${hex[5]}-${hex[6]}${hex[7]}-${hex[8]}${hex[9]}-${hex.slice(10, 16).join('')}` as UUID;
};
/** Node 原生 */
const uuidv4ByNode = (): UUID => {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { randomUUID } = require('node:crypto') as { randomUUID: () => UUID };
return randomUUID();
};
/** 最末兜底:控制台告警,生产请避免 */
const uuidv4ByMath = (): UUID => {
console.warn('[uuid] falling back to Math.random() --- NOT crypto secure!');
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {
const r = (Math.random() * 16) | 0;
const v = c === 'x' ? r : (r & 0x3) | 0x8;
return v.toString(16);
}) as UUID;
};
/**
* 跨环境 UUID v4 生成器
* @returns RFC4122 标准 UUID
*/
export function uuid(): UUID {
if (isSecureContext()) return crypto.randomUUID() as UUID;
if (typeof crypto !== 'undefined' && crypto.getRandomValues) {
return uuidv4ByRandomValues();
}
if (isNode()) return uuidv4ByNode();
return uuidv4ByMath();
}
/** 校验字符串是否为合法 UUID */
export const isUUID = (str: string): str is UUID => UUID_REGEX.test(str);
四、使用示例
1. ESM(Vite / Webpack)
typescript
import { uuid } from '@/utils/uuid';
console.log(uuid()); // a1b2c3d4-e5f6-4g8h-ij90-klmnopqrstuv
2. CommonJS(Node 脚本)
javascript
const { uuid } = require('./dist/uuid.cjs');
console.log(uuid());
3. 小程序入口 polyfill(微信)
微信没有 crypto
,在 app.ts
顶部一次性注入:
javascript
import 'polyfill-crypto-methods'; // npm i polyfill-crypto-methods
// 现在全局有 crypto.randomUUID / getRandomValues
4. Jest 测试固定值
typescript
jest.mock('@/utils/uuid', () => ({ uuid: () => 'test-uuid-123' }));
五、性能 & 体积对比
方案 | 包体 (gzip) | 生成 10w 次 (Chrome 119) | 备注 |
---|---|---|---|
本文函数 | 0.3 KB | 85 ms | 原生最快 |
uuid@9 库 | 4.2 KB | 95 ms | 稳定,需安装 |
nanoid@5 | 1.1 KB | 65 ms | 非标准 UUID 格式 |
测试机:M1 Mac / Node 20
结论:现代浏览器直接用原生,包体≈0;旧端自动降级,无需业务感知。
六、踩坑记录
坑位 | 现象 | 解决 |
---|---|---|
crypto.randomUUID 在 http 线下 502 |
报错 TypeError: crypto.randomUUID is not a function |
先探测 isSecureContext ,再降级 |
Node 14 找不到 randomUUID |
同上报错 | 用 require('node:crypto') 动态导入 |
小程序 undefined | 没有全局 crypto |
引入 polyfill-crypto-methods |
Webpack 4 把 process 打进包 |
导致 isNode() 误判 |
配置 node: { process: false } |
七、FAQ
Q1. 为什么不用 Date.now() + Math.random()
?
→ 时间戳可被预测,且碰撞概率高,不符合 RFC4122。
Q2. 能否生成 v1/v5?
→ 本函数专注无状态 v4 。需要 v1(带 MAC+时间)或 v5(基于命名空间哈希)请用 uuid
库。
Q3. SSR 会重复吗?
→ 不会。Node 的 crypto.randomUUID
同样加密级随机。
Q4. 要支持 IE11?
→ 需额外 crypto
polyfill(如 crypto-js
),但 IE 已正式退役,建议推动业务升级。
八、总结一句话
"先探测、后降级、零依赖、可 Tree-shaking" ------把复杂度封装在工具里,业务代码永远只需
uuid()
。
如果本文对你有帮助,点个赞或在评论区交流更多"唯一序列号"玩法~