前端如何优雅地生成唯一标识?——一份跨环境 UUID 工具函数的封装与实战

关键词:UUID、crypto.randomUUID、polyfill、兼容性、Node、浏览器、TypeScript

一、背景:为什么不要直接 Math.random()

前端经常需要"全局唯一" id,常见场景:

场景 是否可重复 后果
上传文件临时名 秒传失效 / 覆盖
表单多条动态行 删除错位
日志 / 埋点 traceId 链路串线

Math.random() 随机熵只有 48 bit,理论碰撞概率 1/2²⁴≈1/16M,在 toB 长生命周期系统里并不安全。

Web 标准早已给出加密级 方案:crypto.randomUUID(),但存在三把"隐形锁":

  1. 仅 HTTPS / localhost(Secure Context)
  2. 2022 年才普及,IE、Chrome<92 直接抛错
  3. 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()

如果本文对你有帮助,点个赞或在评论区交流更多"唯一序列号"玩法~

相关推荐
真的想不出名儿2 小时前
登录前验证码校验实现
java·前端·python
云舟吖2 小时前
Playwright的元素定位器
前端
我是日安2 小时前
从零到一打造 Vue3 响应式系统 Day 24 - Watch:Options
前端·javascript·vue.js
吹晚风吧2 小时前
什么是跨域?跨域怎么解决?跨域解决的是什么问题?
java·vue.js·js·cors
浅浅的学一下3 小时前
实现在富文本中直接Ctrl+C复制图片并自动上传,并支持HTML格式的图片的复制
前端
wifi歪f3 小时前
🎨 探究Function Calling 和 MCP 的奥秘
前端·ai编程·mcp
BrendanDash3 小时前
React 19.2 已发布,现已上线 npm!
前端·react.js
sheji34163 小时前
【开题答辩全过程】以 Web数据挖掘在电子商务中的应用研究为例,包含答辩的问题和答案
前端·人工智能·数据挖掘
whltaoin3 小时前
Vue 与 React 深度对比:技术差异、选型建议与未来趋势
前端·前端框架·vue·react·技术选型