在 Node.js 中使用 ES Modules 引入 uuid:从安装到全 API 实战指南
在 Node.js 中使用 ES Modules 引入 uuid:从安装到全 API 实战指南
本文基于仓库 examples/node-esmodules 目录下的官方示例,完整讲解如何在 Node.js 原生 ES Modules 环境中安装并引入
uuid包(当前版本 14.0.1,遵循 RFC 9562 / RFC 4122),覆盖命名导入、命名空间导入、v1/v3/v4/v5/v6/v7 生成、v1↔v6 互转、parse/stringify/validate/version工具函数以及NIL/MAX常量等全部公开 API。读完本文,你将能够直接复制运行一个覆盖 uuid 全 API 的 Node.js ESM 示例,并理解其底层实现与导出机制。
示例环境概览
examples/node-esmodules 是一个独立的 Node.js 示例包,其目录结构如下:
examples/node-esmodules/
├── README.md # 示例说明(npm install / npm test)
├── example.mjs # 全 API 演示主文件(ESM 源码)
├── package.json # 示例包配置与脚本
└── package.mjs # 动态导入 package.json 的独立验证脚本
示例的运行方式只有两条命令,这也是本文实战部分的起点:
npm install
npm test
npm test 会通过 npm-run-all 依次执行 test:example(运行 node example.mjs)与 test:package(在 Node.js v20 下额外运行 node package.mjs,验证通过动态 import 读取 uuid/package.json 的能力)。
安装与依赖说明
示例的 package.json 中,uuid 依赖通过本地打包产物引入,而非直接指向 npm registry:
{
"name": "uuid-example-node-esmodules",
"version": "0.0.0",
"private": true,
"scripts": {
"test:package": "( node --version | grep -vq 'v20' ) || ( node package.mjs )",
"test:example": "node example.mjs",
"pretest": "rm -fr node_modules && npm install --no-package-lock",
"test": "npm-run-all test:*"
},
"dependencies": {
"uuid": "file:../../.build/uuid.tgz"
}
}
值得注意的几个细节:
- 依赖指向本地构建产物
file:../../.build/uuid.tgz,即仓库根目录npm run build后生成的 tarball。也就是说,运行此示例前需先在仓库根目录完成构建(根 package.json 的pretest、prepack、prepublishOnly等脚本均会触发npm run build)。 - 每次测试前干净重装:
pretest脚本会删除node_modules并以--no-package-lock重新安装,保证示例始终基于最新的本地构建包运行。 - Node.js 版本条件:
test:package脚本通过node --version | grep -vq 'v20'判断,仅在 Node.js v20 环境才执行package.mjs,用于验证 import 属性(assert/with子句)的兼容行为。
两种导入方式:命名导入与命名空间导入
example.mjs 演示了 ESM 下两种等价引入方式。
命名导入(named import)
将 uuid 的所有公开 API 以别名形式一次性引入:
import {
MAX as MAX_UUID,
NIL as NIL_UUID,
parse as uuidParse,
stringify as uuidStringify,
validate as uuidValidate,
version as uuidVersion,
v1 as uuidv1,
v1ToV6 as uuidv1ToV6,
v3 as uuidv3,
v4 as uuidv4,
v5 as uuidv5,
v6 as uuidv6,
v6ToV1 as uuidv6ToV1,
v7 as uuidv7,
} from 'uuid';
这些导出与 src/index.ts 中的入口定义一一对应:MAX、NIL、parse、stringify、validate、version、v1、v1ToV6、v3、v4、v5、v6、v6ToV1、v7,另加类型导出 UUIDTypes 等(见 src/types.ts)。
命名空间导入(namespace import)
以 import * as uuid from 'uuid' 的方式引入后,通过 uuid.v1()、uuid.parse() 等属性访问,效果完全一致。示例后半段专门对比展示了这两种写法:
console.log('uuid.v1()', uuid.v1());
console.log('uuid.v4()', uuid.v4());
console.log('uuid.v7()', uuid.v7());
console.log('uuid.NIL', uuid.NIL);
console.log('uuid.MAX', uuid.MAX);
为什么 ESM 下推荐显式命名导入
根 package.json 的 exports 字段声明了条件导出:
"exports": {
".": {
"node": {
"types": "./dist/index.d.ts",
"default": "./dist-node/index.js"
},
"default": "./dist/index.js"
},
"./package.json": "./package.json"
}
Node.js 环境会命中 node 条件,加载 dist-node/index.js;其他环境(浏览器、打包器等)回退到默认的 dist/index.js。"type": "module" 配合 "sideEffects": false,使得打包器可以对命名导入做 Tree Shaking——只打包实际用到的函数(如仅 v4),这也是命名导入在现代工程中更受推荐的原因之一。
各版本 UUID 的生成与命名空间用法
示例覆盖了所有生成型 API,可直接复制运行观察输出:
console.log('uuidv1()', uuidv1()); // 时间戳 + 节点标识,版本 1
console.log('uuidv4()', uuidv4()); // 全随机,版本 4
console.log('uuidv7()', uuidv7()); // 时间戳 + 随机序列,版本 7
// v3 基于 MD5 哈希,需命名空间
console.log('uuidv3() DNS', uuidv3('hello.example.com', uuidv3.DNS));
console.log('uuidv3() URL', uuidv3('http://example.com/hello', uuidv3.URL));
// v5 基于 SHA-1 哈希,同样需命名空间
console.log('uuidv5() DNS', uuidv5('hello.example.com', uuidv5.DNS));
console.log('uuidv5() URL', uuidv5('http://example.com/hello', uuidv5.URL));
console.log('uuidv6()', uuidv6()); // 时间排序增强版,版本 6
命名空间(namespace)的正确用法
v3/v5 是命名空间哈希型 UUID,需要同时传入 value 与 namespace。包内预置了两个标准命名空间常量(定义于 src/v35.ts):
DNS=6ba7b810-9dad-11d1-80b4-00c04fd430c8URL=6ba7b811-9dad-11d1-80b4-00c04fd430c8
自定义命名空间必须是属于你应用的 UUID 字符串,且必须为 16 字节(namespace 校验失败会抛出 TypeError('Namespace must be array-like (16 iterable integer values, 0-255)')):
// 可事先用 uuid 命令行工具生成一个专属命名空间
const MY_NAMESPACE = '55238d15-c926-4598-b49d-cf4e913ba13c';
console.log('uuidv3() MY_NAMESPACE', uuidv3('Hello, World!', MY_NAMESPACE));
console.log('uuidv5() MY_NAMESPACE', uuidv5('Hello, World!', MY_NAMESPACE));
底层实现中(src/v35.ts),v3/v5 会将 namespace 字节与 value 字节拼接后做 MD5 或 SHA-1 哈希,随后写入版本位(bytes[6] = (bytes[6] & 0x0f) | version)与变体位(bytes[8] = (bytes[8] & 0x3f) | 0x80),最终格式化为标准字符串。
v7 的时序单调性保障
v7 是 RFC 9562 新增的时间排序 UUID,其内部状态机(src/v7.ts)在无参数调用时:时间前进则重新随机序列号;同一毫秒内则递增 32 位序列号,溢出时将时间戳 +1ms 以维持单调性(RFC 9562 §6.2 允许该做法)。传入 options(msecs/seq/random/rng)时则完全脱离内部状态,生成结果可预期。
v1 与 v6 的互相转换
示例演示了 RFC 9562 中 v1 → v6 的字段重排能力:
const V1_ID = 'f1207660-21d2-11ef-8c4f-419efbd44d48';
const V6_ID = '1ef21d2f-1207-6660-8c4f-419efbd44d48';
console.log('uuidv1ToV6()', uuidv1ToV6(V1_ID)); // 输出 1ef21d2f-1207-6660-8c4f-419efbd44d48
console.log('uuidv6ToV1()', uuidv6ToV1(V6_ID)); // 输出 f1207660-21d2-11ef-8c4f-419efbd44d48
v1ToV6 的实现(src/v1ToV6.ts)通过位运算将 v1 的 60 位时间戳(分布在字节 0-7)重排为 v6 的大端序时间布局,并把版本位改写为 0x60;v6ToV1 则是完全逆向的过程。两者均保持输入输出类型一致:传入字符串返回字符串,传入 Uint8Array 返回新的 Uint8Array。
这一能力对需要从旧 v1 数据平滑迁移到 v6 排序语义、又不想破坏既有标识的场景非常实用。
工具函数与常量
示例覆盖了全部工具型 API 与特殊常量:
console.log('NIL_UUID', NIL_UUID); // 00000000-0000-0000-0000-000000000000
console.log('MAX_UUID', MAX_UUID); // ffffffff-ffff-ffff-ffff-ffffffffffff
console.log('uuidParse()', uuidParse(MY_NAMESPACE)); // 解析为 16 字节 Uint8Array
console.log('uuidStringify()', uuidStringify(uuidParse(MY_NAMESPACE))); // 还原字符串
console.log('uuidValidate()', uuidValidate(MY_NAMESPACE)); // 合法性校验,返回布尔值
console.log('uuidVersion()', uuidVersion(MY_NAMESPACE)); // 返回版本号(如 3/4/5/6/7)
各工具函数对应的源码入口:
NIL:全零 UUID,定义于 src/nil.ts;MAX:全fUUID,定义于 src/max.ts;parse:将 36 字符 UUID 字符串解析为 16 字节数组(src/parse.ts);stringify:将 16 字节数组格式化为标准字符串,内部经validate做一致性校验,非法输入会抛出TypeError('Stringified UUID is invalid')(src/stringify.ts);validate:正则校验 UUID 字符串格式(src/validate.ts);version:解析并返回 UUID 的版本号(src/version.ts)。
parse 与 stringify 构成完整的双向转换闭环,是处理二进制存储、数据库主键与接口字符串之间转换的常用组合。
动态导入 package.json:面向 React Native 等工具链
示例的另一个亮点是动态导入 uuid/package.json:
// Import attribute syntax is still awaiting finalization.
// 为兼容 "assert" 与 "with" 两种语法,这里使用动态 import
const pkg = await import('uuid/package.json', {
assert: { type: 'json' },
with: { type: 'json' },
});
console.log('pkg.name', pkg.default.name); // 'uuid'
注释明确指出:import 属性(JSON 模块的 assert/with 子句)语法尚未完全定稿(见 TC39 import-attributes 提案),因此示例同时传入两种子句以兼容不同 Node.js 版本。with 是新语法,assert 是旧语法,双写可保证在过渡期都能工作。
这依赖根 package.json 中 "./package.json": "./package.json" 的导出映射——包内所有文件均可直接通过子路径导入。package.mjs(Node.js v20 下执行)则展示了顶层 import pkg from 'uuid/package.json' assert { type: 'json' } 的静态写法。动态 import 适合 React Native、Cordova 等需要运行时内省包元信息(如 pkg.name、版本号)的工具链场景。
运行与验证
完成上述文件落地后(或直接在仓库的 examples/node-esmodules 目录中):
npm install # 安装本地构建产物 uuid.tgz
npm test # 依次运行 example.mjs 与(Node 20 下)package.mjs
预期输出会依次打印 v1/v4/v7 随机 UUID、v3/v5 的 DNS/URL/自定义命名空间哈希结果、v6 UUID、v1↔v6 转换结果、NIL/MAX 常量、parse/stringify/validate/version 的工具调用结果,以及命名空间导入等价调用的全部输出,最后打印 pkg.name(uuid)。
小结
examples/node-esmodules 示例以极简的 npm install + npm test 两步走通了 Node.js ESM 环境下 uuid 的全部公开 API 面:两种导入方式、v1/v3/v4/v5/v6/v7 生成、命名空间语义、v1↔v6 双向转换、工具函数与常量、以及面向工具链的 JSON 动态导入。配合根 package.json 的 exports 条件导出与 sideEffects: false,这套用法同样适用于 Vite、Rollup、Webpack 等现代构建链路;需要浏览器端方案时,可进一步参考仓库中的 browser-esmodules 与 node-jest 等示例。