news 2026/10/21 16:01:26

JS UUID生成全指南:四种方案对比与避坑实操

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
JS UUID生成全指南:四种方案对比与避坑实操

做前端时间久了,你会发现很多不起眼的小工具函数,平时用的时候没觉得怎样,可真到出问题的时候才知道损失了多少时间。“Js 生成uuid”这件事就属于这一类。某次上线前排查一个后台系统的数据重复问题,定位到最后,竟然是一段用Date.now()加上随机数拼出来的“假 uuid”在同一毫秒内撞了车。从那以后我再也不敢小看 UUID 的生成逻辑。

UUID(Universally Unique Identifier)是一个 128 位的标识符,最常用的形式是b9f1a2c3-4d5e-4f6a-8b7c-1d2e3f4a5b6c这种8-4-4-4-12的字符串。它本身不能保证绝对的唯一,但唯一性概率高到实际使用中可以忽略碰撞。这篇内容想和你聊清楚 Js 里生成 uuid 的四种主流方法:原生crypto.randomUUID()、基于crypto.getRandomValues()的手工拼装、第三方工具库,以及很多人还在用的过期方案。适合刚接触唯一标识的小白,也适合想在项目里做技术选型的老手。

1. 先弄清UUID是什么,以及判断方案优劣的三把尺子

在动手写代码之前,最好先把“UUID 到底是什么”这件事理清楚。很多人只知道它是一串很长的随机字符串,却不知道这串字符串里其实藏着固定规则。理解了这些规则,你才看得懂为什么不同生成方案的代码长得不一样,也才知道哪一种更适合自己的业务场景。

1.1 UUID的版本与格式

UUID 的标准格式是 32 个十六进制字符,以xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx形式呈现,中间用连字符拆成 5 段。这里的每一段都有讲究:前 8 位是时间低位,中间 4 位是时间中位,第 3 段是高时间位和版本号,第 4 段是变体标识和时钟序列,最后一段是节点标识。不过对于大多数前端工程师来说,真正需要用到的只有 v4 版本,因为它完全基于随机数生成,不依赖时间和 MAC 地址。

v4 UUID 有两位是固定的:第 13 个字符固定为4,表示这个 UUID 的版本;第 17 个字符固定是8、9、a或b中的一个,这是变体标识。拿4d5e4f6a举例,第 13 位是4,说明这是 v4 类型的 UUID。这个规则看起来枯燥,却是后面手工拼装方案里那几行位运算的根本原因。

1.2 衡量UUID生成方案的三个核心指标

判断一个方案值不值得用,我建议你只看三件事。第一是随机数质量,UUID 唯一性的根基全部建立在“随机数到底够不够随机”上。如果随机数来自非加密级算法,哪怕代码写得再漂亮,碰撞概率也会明显上升。第二是环境兼容性,同一个代码要在浏览器、Node.js、老版本系统里跑出同样的结果,需要做降级适配。第三是维护成本,指的是这段代码写完之后,团队里其他人能不能看懂、有没有额外依赖、后续好不好替换。

这三个标准会贯穿后面的每一个方案。你会发现在不同场景下,最优解并不一样:纯前端的小工具函数最看重便捷,大型项目最看重可控性和可维护性,而涉及订单号、验证码这类敏感场景时,随机数到底安不安全就变成了生死攸关的问题。

2. 方案一:crypto.randomUUID()——原生API,一行代码就能拿到合规UUID

先说结论,这是目前我最推荐的方案。它直接调用底层操作系统提供的加密级随机数源,输出的随机数质量最高,而且代码量几乎为零。

2.1 浏览器和Node.js中的基本用法

浏览器端的调用方式非常简洁,一个方法返回一个标准的 v4 字符串:

const myUUID = crypto.randomUUID(); console.log(myUUID); // 形如 b9f1a2c3-4d5e-4f6a-8b7c-1d2e3f4a5b6c

需要注意,浏览器里的crypto在严格安全上下文(HTTPS 或 localhost)下才完全可用。如果你的站点还是 HTTP 协议,调用时可能会被浏览器策略限制。正常开发环境因为使用了 localhost 不会受影响,但部署到内网 IP 或测试环境时要特别留意。

Node.js 侧的用法入口略有不同,从node:crypto模块导出:

const { randomUUID } = require('node:crypto'); const id = randomUUID();

如果你用的是较新的 Node.js 版本,也可以用 ESM 方式导入:

import { randomUUID } from 'node:crypto';

这个方法不需要额外装任何依赖,返回的结果已经是标准的 v4 格式,不需要自己处理大小写和连字符。在每次调用都会生成一个全新随机 UUID 的前提下,高并发场景下也不需要担心重复。

2.2 兼容边界与降级链路

原生 API 虽好,但兼容性边界一定要清楚。它在大约 2021 年开始陆续进入主流浏览器:Chrome 92 以上、Firefox 95 以上、Safari 15.4 以上、Edge 92 以上才完整支持。Node.js 则要求 14.17.0 以上版本。如果项目需要支持更老的环境,直接裸用crypto.randomUUID()会在某些用户的浏览器里抛TypeError。

一个比较稳妥的做法是做一个降级判断,在不支持的浏览器里自动切换到手写方案:

function generateUUID() { if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') { return crypto.randomUUID(); } // 降级到基于 getRandomValues 的手写逻辑 return uuidv4ByGetRandomValues(); }

这样可以保证不管用户用什么浏览器,得到的都是一个合规且安全性足够的 UUID。这个降级链路在后面的方案里会给你一个完整的补充实现。

3. 方案二:crypto.getRandomValues()手动拼装——不挑环境,安全性依旧拉满

如果你的项目环境太老,或者你希望有一个不依赖新 API 的通用解法,那么手工拼装是一个值得掌握的基础能力。它的原理不算复杂:先拿到 16 个加密级随机字节,再把其中两位改成 v4 要求的固定值,最后拼成标准格式的字符串。

3.1 手工拼装UUID v4的完整实现

这段代码是最常见的 v4 生成实现,你可以直接放进自己的工具函数库里:

function uuidv4ByGetRandomValues() { const bytes = new Uint8Array(16); crypto.getRandomValues(bytes); // 设置第 7 个字节的高四位为 0100,表示版本号 4 bytes[6] = (bytes[6] & 0x0f) | 0x40; // 设置第 9 个字节的高两位为 10,表示变体 bytes[8] = (bytes[8] & 0x3f) | 0x80; const hex = Array.from(bytes, (byte) => byte.toString(16).padStart(2, '0')).join(''); return ( hex.substring(0, 8) + '-' + hex.substring(8, 12) + '-' + hex.substring(12, 16) + '-' + hex.substring(16, 20) + '-' + hex.substring(20) ); }

逐行拆开说一下重点。crypto.getRandomValues()是 Web Crypto API 的一部分,它接受一个TypedArray作为参数,这里用Uint8Array(16)正好对应 UUID 的 128 位长度。bytes[6]和bytes[8]的位运算是在刻意“污染”随机结果:先把原有的高四位或高两位清零,再用|运算写入固定值。0x40的二进制是01000000,写入后第 13 位必为4;0x80的二进制是10000000,配合0x3f清理后,让第 9 字节最高两位固定在10上,对应8、9、a、b。

最后把 16 个字节转成十六进制字符串,再用substring按照8-4-4-4-12的长度切开,拼接成标准格式。注意每个字节转十六进制时都要padStart(2, '0'),否则小于 16 的字节会输出单字符,导致最终字符串长度变成 31 位而非 32 位,这是手写方案里最常见的低级错误。

3.2 为什么getRandomValues比Math.random安全

很多人可能会问:既然 Math.random 也能生成随机数,为什么不能直接拿它来拼 UUID?根本区别在于安全等级。getRandomValues底层使用操作系统提供的加密级安全随机数源,在 Unix 系系统上通常来自/dev/urandom,在 Windows 上来自系统级加密 API。这些随机数的特征是:不可预测、不可重现,即使攻击者知道前几个值,也无法推算后续的值。

而Math.random本质上是伪随机数生成器,多数现代引擎使用带种子状态的算法(比如 xorshift128+ 或类似变体)。只要知道了内部状态,后续输出是可以被推算出来的。用在 UUID 上,碰撞概率不一定会立刻爆发,但在订单号、令牌这类对外暴露的场景里,随机数可预测本身就是重大安全隐患。另外Math.random的重复模式在高频调用下会被放大,批量循环里连续生成的 UUID 很容易出现肉眼可见的规律,这对数据主键来说是不可接受的。

3.3 手工方案的可扩展性

手工方案最大的价值在于可控。如果你需要在自己的 ID 里保留业务前缀,或者希望前几位包含固定标识去追踪数据来源,原生randomUUID()反而不太容易定制。你可以基于上面的逻辑稍作调整,比如把前两个字节改成固定值,生成prod-b9f1a2c3-4d5e-...这样的业务追踪 ID。当然,这种自定义会牺牲一部分随机空间,要确保剩下的随机位数仍然足够撑起唯一性。

4. 方案三:第三方工具库——大型项目的省心之选

如果你的项目里已经有成熟的依赖管理习惯,或者团队里有多处地方都要生成 UUID,我会建议直接用一个社区里广泛使用的工具库。它省去了自己维护手写逻辑的麻烦,同时把兼容性、格式校验、模块化导出这些都替你处理好了。

4.1 为什么要为一个小函数引入依赖

有人觉得为了一行代码装一个依赖不划算,但实际在大型项目里,手写方案最大的问题不是代码本身,而是“标准不统一”。我见过团队里有人用Math.random拼 36 位字符串,有人用Date.now()加随机数截断,还有人自己实现了getRandomValues手写版但格式不对。最后做数据清洗时,三种格式互相不兼容,极其痛苦。

第三方库的价值在于:它把格式、随机源、版本选择都收敛在一个统一接口后面,所有人调出来的结果长一个样。而且库本身会处理不同运行环境的差异,不需要你手动维护降级逻辑。如果你需要 v1(基于时间戳和 MAC)、v3/v5(基于命名空间和哈希)等非 v4 版本,手写代码的工作量会直线上升,这时候依赖库的收益就更明显了。

4.2 核心用法与版本选择

大多数情况只需要 v4 这一个函数,安装和调用方式都很直接:

npm install uuid

ESM 模块里这样用:

import { v4 as uuidv4 } from 'uuid'; const id = uuidv4();

CommonJS 环境下则是:

const { v4: uuidv4 } = require('uuid'); const id = uuidv4();

库也提供 v1、v3、v5 等函数,但绝大多数场景用不到。v1 包含时间戳和节点 MAC 地址,虽然具备一定的时间有序性,但在隐私敏感的系统中会暴露机器标识,不建议默认使用。v3 和 v5 主要用于需要“同一名字生成同一 UUID”的场景,比如把用户邮箱映射成一个固定标识,属于少数派需求。

4.3 封装一次,别到处裸调

第三方库用得越多,越要警惕“依赖散落”的问题。我不建议每个页面都直接import { v4 } from 'uuid',而是先封装到一个公共工具文件里:

import { v4 as uuidv4 } from 'uuid'; export function generateUuid(prefix = '') { const raw = uuidv4(); return prefix ? `${prefix}_${raw}` : raw; }

这样一个项目中所有 UUID 的入口都集中在同一个文件中。将来要替换成原生crypto.randomUUID()或者调整前缀规则,只需要改一个文件,而不是全局搜索替换。配合 ESLint 的no-restricted-imports规则,甚至可以禁止其他人绕过封装直接引库,保证出口统一。另外注意,打包时尽量用按需引入的方式,现代的打包工具能通过 tree-shaking 把最终的代码体积控制在很小的范围内,不会成为项目负担。

5. 四种方案的横向对比与真实场景选型

到这你可能已经看出来了,四种方案没有绝对的优劣,关键是找到适合你当前项目的那个。为了让你能更快决策,我直接把它们放同一张表里对比。

5.1 一张表看清四种方案的差异

方案随机源安全级别代码复杂度依赖情况适用场景
crypto.randomUUID()加密级,系统熵源极低无现代浏览器、Node.js 14.17+
getRandomValues手写加密级,系统熵源中无需要兼容旧浏览器、需要定制格式
第三方工具库加密级低有(一个小依赖)大型项目、需要多版本控制、统一管理
Math.random拼接伪随机,可预测极低无不推荐,仅限完全非敏感场景

从表里可以看到,前三个方案都建立在加密级随机数之上,真正拉开差距的是兼容性和维护成本。第四个方案基本属于“能跑但别用”的行列,安全性没有任何保证。我见过有人在活动页生成一次性按钮 ID 时直接用Math.random,最后因为并发点击里出现重复 ID 导致弹窗错乱,尽管影响不大,但排查起来同样费劲。

5.2 不同项目场景下的决策建议

我给你几个可以直接抄的选型建议。如果你在做新的纯前端项目,并且浏览器环境较新,无脑选crypto.randomUUID();如果你在做 Node.js 后端服务,同样优先选randomUUID,它是 Node 内置模块导出的,性能很好。如果你面对的是历史遗留项目,用户群体里可能有大量旧版本浏览器,那么手写getRandomValues方案是最不依赖环境的通解。如果你所在团队的工程化水平较高,已经有统一的工具库维护习惯,直接引入第三方库更省心,尤其是在你需要 v1/v3/v5 的情况下。

不要因为某一次听说Math.random也能生成 UUID 就觉得它够用,这点我在下一节重点展开。

6. 避坑实录:Math.random和时间戳拼接为什么会被我拉进黑名单

这一节我想再重提一下开头的那次事故。当时某后台系统需要在一次批量操作中为一千多个任务生成临时标识,同事图省事用了一段看起来没什么问题的代码:

const id = `${Date.now()}_${Math.random().toString(16).slice(2)}`;

结果上线后日志里出现了若干个一模一样的 ID,直接导致数据覆盖。当时我们查了很久才发现是 ID 重复的问题,而不是逻辑并发问题。

6.1 在真实项目里遇到的重复ID问题

这段代码的问题其实藏在两层。第一层是Date.now()只精确到毫秒,在同一毫秒内批量调用时,时间部分完全相同。第二层是Math.random().toString(16).slice(2)看似随机,实际能提供的熵远小于想象:它把浮点数转成十六进制字符串后从第 2 位开始截取,得到的一段字符通常只有 13 到 14 位十六进制,如果运气不好数值很小,截出来的长度还会更短。

更致命的是,某些引擎在短时间内高速调用Math.random()时,输出的序列会表现出一定的周期性。即使从概率上算,Date.now()加随机字符串的方案也不是完全不能碰撞,但在批量并发场景下,它把碰撞概率从一个“可以忽略”的级别拉到了“偶尔会发生”的级别,这就足以成为事故的导火索。

6.2 可预测性问题与格式合规问题

就算没碰上碰撞,这段代码生成的也不是标准 UUID,而是一个自定义格式的字符串。这里有个容易被忽略的现实问题:当数据量变大、日志要接入外部分析平台时,非标准格式的标识无法被各种 UUID 解析工具直接识别。有些数据分析系统会把字符串按类型分类,你的“假 UUID”会被当成普通文本,无法和真实 UUID 进行关联查询。与其等数据阶段再后悔,不如在生成源头就遵守标准。

从安全角度看,可预测性更是硬伤。Math.random的算法在某些引擎里状态空间有限,在了解若干输出样本的情况下,是有可能反推出内部状态的。UUID 本身虽然不用于加密,但很多业务设计里会拿它直接作为资源路径的一部分,比如https://xxx.com/resource/b9f1a2c3-4d5e-4f6a-8b7c-1d2e3f4a5b6c。如果这个标识可预测,等于给攻击者开了一扇遍历资源的门。你可能觉得 UUID 不是 token,但实际上这类设计非常常见。

6.3 生成UUID后的两个小细节

写到最后,顺便分享两个我平时很容易在意的小细节。第一,UUID 统一存小写。标准十六进制字符虽然大小写不敏感,但有些数据库排序规则和字符串比较会区分大小写,从源头统一小写能省掉很多无谓的兼容问题。第二,别把 UUID 当自增主键用。UUID 不具备时间有序性,在数据库索引上会产生随机写入,高并发场景下对 InnoDB 类的聚簇索引并不友好。如果业务需要主键的自增特性,可以额外保留自增数字主键,把 UUID 作为业务标识。

我个人的习惯是:新项目一律先上crypto.randomUUID(),一行代码够用;要兼容旧环境就把getRandomValues的手写版放在公共工具库里;如果团队里已经有不少历史代码靠第三方库生成,那也不必强推原生 API,继续用库反而更稳。最后再提醒一句,UUID 只是唯一标识,不是加密凭证,别把它当 token 用。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/19 5:24:12

视觉SLAM中Sophus库SO(3)与SE(3)位姿表示及优化实战指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/22 5:48:17

C/C++课程设计:从能跑通到可交付的工程能力实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/18 9:28:37

Unity3D FPS完整版工程下载避坑指南:从选型到跑通

简介:这是一份基于Unity3D的FPS游戏完整源代码包,面向希望掌握第一人称射击游戏开发的中高级开发者,可用于毕业设计、课程项目或独立游戏起步参考。资源围绕Unity3D在FPS领域的关键技术展开,从场景构建、角色控制器、射击机制、网…

作者头像 李华
网站建设 2026/10/21 17:21:43

基于MP2642GR-Z与STM32的2S电池主动均衡方案

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/19 0:35:06

LCS转LIS:排列序列下最长公共子序列的O(n log n)优化

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华