dart-common
v0.0.1
Published
DART.IM组件库 - 通用前端工具类
Maintainers
Readme
dart-common
通用前端工具类库。所有函数均为原生 JavaScript 实现,运行时零依赖,产物自包含。
安装
npm i dart-common快速开始
// ESM
import { isEmpty, guid } from 'dart-common'
// CJS
const { isEmpty, guid } = require('dart-common')import { isEmpty, guid } from 'dart-common'
isEmpty({}) // true
isEmpty({ id: 1 }) // false
isEmpty([]) // true
isEmpty(new Map()) // true
guid() // '110ec58a-a0f2-4ac4-8393-c866d813b8d1'API
isEmpty(value)
判断一个值是否为空,采用宽泛语义(与 lodash.isEmpty 一致)。
| 入参 | 返回 |
| ------------------------------------- | ---------------------------- |
| null / undefined | true |
| '' / [] / new Uint8Array(0) | 按 length === 0 判定 |
| new Map() / new Set() | 按 size === 0 判定 |
| {} / Object.create(null) / 类实例 | 按自有可枚举字符串键数量判定 |
| 函数、数字、布尔、Symbol、BigInt | true |
仅统计可枚举字符串键,只含 Symbol 键的对象视为空;
Date、正则按自有可枚举字符串键数量判定。
@param {*} value待判断的值@returns {boolean}
guid()
生成 RFC4122 版本 4(随机)GUID。
- 优先使用
crypto.getRandomValues(浏览器与 Node 18+ 原生支持),不可用时回退Math.random - 置位规则:第 7 字节版本位
0100,第 9 字节变体位10 @returns {string}36 位小写 GUID,形如xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx
isArray(value)
判断是否为数组,内部复用原生 Array.isArray,可正确处理 Proxy、跨 iframe(跨 realm)数组,不受 Symbol.toStringTag 篡改影响。
isArray([]) // true
isArray(new Array(3)) // true
isArray({ length: 0 }) // false
isArray('abc') // falseformatDate(value, pattern?)
把各种时间格式转换为目标字符串。模板占位符区分大小写:
| 占位符 | 含义 | 示例 |
| ------------- | ---------------------------- | ------------- |
| YYYY / YY | 四位 / 两位年份 | 2024 / 24 |
| MM / M | 两位 / 一位月份 | 01 / 1 |
| DD / D | 两位 / 一位日期 | 05 / 5 |
| HH / H | 两位 / 一位小时(24 小时制) | 08 / 8 |
| mm / m | 两位 / 一位分钟 | 03 / 3 |
| ss / s | 两位 / 一位秒 | 09 / 9 |
| SSS | 三位毫秒 | 123 |
不在表中的字符原样保留,所以 YYYY年MM月DD日、YYYY-MM-DDTHH:mm:ss.SSS 都能直接用。默认模板为 YYYY-MM-DD HH:mm:ss,无效时间返回 ''。
formatDate('2024-01-05 08:03:09') // '2024-01-05 08:03:09'
formatDate(1704412989000, 'YYYY/MM/DD') // '2024/01/05'
formatDate('2024年1月5日', 'M月D日') // '1月5日'
formatDate(1704412989123, 'YYYY-MM-DD HH:mm:ss.SSS') // '2024-01-05 08:03:09.123'
formatDate('2024-02-31') // ''(非法日期)toDate(value)
格式化的底层解析函数,也可单独使用。支持 Date 实例、毫秒 / 秒级时间戳(数字或纯数字字符串)、2024-01-01、2024/1/5 8:3:9、2024年1月5日、无时区的时间串(按本地时间)、带 Z 或 +08:00 的 ISO 串。无效输入返回 null;传入 Date 返回副本,不修改原对象。
toDate('2024-01-01') // Date(2024, 0, 1) 本地时间
toDate('1704067200') // 秒级时间戳字符串
toDate('abc') // nullgetFingerprint(options?)
采集浏览器指纹,结构与 FingerprintJS 对齐:返回稳定哈希 visitorId(32 位十六进制)与明文的 components,便于排查指纹漂移。
采集项:userAgent、languages、language、platform、timezone、timezoneOffset、screen、viewport、hardwareConcurrency、deviceMemory、maxTouchPoints、cookieEnabled、doNotTrack、webdriver、storage、canvas、webgl、fonts。
- 哈希算法:内置 MurmurHash3(x86 32 位),四个不同种子拼成 32 位十六进制,不依赖任何第三方库
canvas绘制渐变 + 文本 + 圆形后对像素结果哈希,webgl读取UNMASKED_VENDOR/RENDERER,fonts用文本宽度对比探测已安装字体- 每一项独立
try/catch,单项失败只丢弃该项,不影响整体;非浏览器环境调用也不会抛错 options.exclude数组可跳过指定组件,例如规避某些隐私浏览器下 canvas 噪声
const { visitorId, components } = getFingerprint()
// visitorId: '3f2a9c1d7b4e8a05...'
// components: { canvas: '...', timezone: 'Asia/Shanghai', screen: '1920x1080x24x24', ... }
getFingerprint({ exclude: ['canvas'] }) // 跳过 canvas仅为客户端可用性/防刷场景提供辅助标识,不做服务端强校验;同一浏览器在不同版本、不同 GPU 驱动下组件可能变化。
EventBus
同步事件总线,面向「高频 emit、低频订阅变更」场景。
import { EventBus } from 'dart-common'
const bus = new EventBus()
const off = bus.on('save', (payload) => console.log(payload))
bus.emit('save', { id: 1 }) // 1(返回实际执行成功的监听数)
off()| 方法 | 说明 |
| ------------------------------------- | ------------------------------------------------- |
| on(event, handler) | 订阅,返回退订函数;同一函数重复订阅只保留一份 |
| once(event, handler) | 只触发一次,触发后自动移除;也可用原函数 off 掉 |
| off(event, handler?) | 传 handler 只移除该监听,不传则清空该事件全部监听 |
| emit(event, payload?) | 同步触发,返回执行成功的监听数量 |
| has(event) / listenerCount(event) | 判断是否有监听 / 取监听数量 |
| eventNames() | 当前存在监听的事件名数组 |
| clear(event?) | 清空指定事件或全部事件 |
性能设计:
- 内部
Map<event, Set<handler>>,订阅 / 退订 / 去重都是 O(1),不做数组线性扫描 - 单监听走
set.values().next()快路径,不创建迭代器、不拷贝数组;多监听直接迭代 Set,并用「初始数量守卫」保证 emit 中新增的监听不会在本轮被误触发 - 正常路径不产生额外对象;只有真正发生异常才创建错误收集数组
- 单个回调抛错不影响其余订阅者,错误交给
onError处理;未配置时在本轮全部执行完后重新抛出(多个错误抛AggregateError)
错误处理:
const bus = new EventBus({
onError: (error, event, handler) => console.error(`[${event}]`, error)
})实测:单监听 100 万次 emit 约 48ms(≈48ns/次),5 个监听 10 万次 emit 约 16ms。
debounce(fn, wait?, options?)
通用防抖:连续触发时只执行最后一次,保留 this 与调用参数。
import { debounce } from 'dart-common'
const search = debounce((keyword) => fetch(`/api?q=${keyword}`), 300)
input.addEventListener('input', (e) => search(e.target.value))| 选项 | 默认值 | 说明 |
| ---------- | ------- | ----------------------------------------------------------------------- |
| leading | false | 首次调用立即执行;immediate 为兼容别名,两者同时存在以 leading 为准 |
| trailing | true | 等待结束后执行最后一次调用 |
| maxWait | 不限制 | 持续触发时保证在该时长内至少执行一次,适合滚动/拖拽等长周期场景 |
返回的函数附带三个方法:
search.cancel() // 取消等待中的调用,不执行
search.flush() // 立即执行等待中的调用,返回其结果;无待执行时返回上一次结果
search.pending() // 是否存在等待中的调用// 首次立即执行,之后合并到最后一次
debounce(doSubmit, 500, { leading: true })
// 持续触发时每 200ms 至少执行一次
debounce(onScroll, 100, { maxWait: 200 })哈希:md5 / sha1 / sha256
纯 JS 实现,不依赖 crypto 或第三方库,输入支持字符串(按 UTF-8)、Uint8Array、ArrayBuffer,输出小写十六进制。
import { md5, sha1, sha256 } from 'dart-common'
md5('') // 'd41d8cd98f00b204e9800998ecf8427e'(32 位)
sha1('abc') // 'a9993e364706816aba3e25717850c26c9cd0d89d'(40 位)
sha256('abc') // 'ba7816bf8f01cfea414140de5dae2223...'(64 位)
md5(new Uint8Array([0, 1, 2])) // 直接对二进制求摘要MD5 与 SHA-1 已不具备抗碰撞性,仅用于文件校验、缓存键、兼容旧接口等场景;口令存储与签名请用 SHA-256 及以上,或改用 AES 加密方案。
编码:Base64 / Base64URL / Hex
全部按 UTF-8 处理,中文与 emoji 可正确往返;解码端容忍换行、空格与缺失的 = 填充。
import {
encodeBase64,
decodeBase64,
encodeBase64Url,
decodeBase64Url,
encodeHex,
decodeHex,
decodeBase64Bytes,
decodeHexBytes
} from 'dart-common'
encodeBase64('中文') // '5Lit5paH'
decodeBase64('5Lit5paH') // '中文'
encodeBase64Url('>>>???') // 'Pj4-Pz8_'(无填充,可直接放 URL)
encodeHex('dart') // '64617274'
decodeHex('0x64617274') // 'dart'
// 需要原始字节时
decodeBase64Bytes('AAEC') // Uint8Array [0, 1, 2]
decodeHexBytes('00ff') // Uint8Array [0, 255]aesEncrypt / aesDecrypt
AES-256-GCM 可逆加密,基于浏览器与 Node 18+ 原生的 Web Crypto,不引入额外依赖。
- 密钥由口令经 PBKDF2-SHA256(10 万次迭代)派生
- 每次加密生成新的 16 字节盐与 12 字节 IV,相同明文得到不同密文
- 输出 Base64,结构为
salt(16B) + iv(12B) + 密文(含 16B GCM 认证标签) - GCM 自带完整性校验:口令错误或密文被篡改都会解密失败
import { aesEncrypt, aesDecrypt } from 'dart-common'
const cipher = await aesEncrypt('敏感数据', 'my-password')
const text = await aesDecrypt(cipher, 'my-password') // '敏感数据'
await aesDecrypt(cipher, 'wrong-password') // 抛出:解密失败,口令错误或内容已被篡改派生 10 万次 PBKDF2 单次约 90ms。仅用于前端脱敏、离线数据保护;涉及服务端密钥的安全场景应把密钥与加解密放在服务端。
国密算法:SM2 / SM3 / SM4
全部为纯 JS 实现,零依赖,不依赖 Web Crypto 或任何第三方国密库。
sm3(input)
SM3 杂凑(GB/T 32905-2016),输出 256 位,返回 64 位小写十六进制。
import { sm3 } from 'dart-common'
sm3('abc') // '66c7f0f462eeedd9d1f2d46bdc10e4e24167c4875cf2f7a2297da02b8f4ba8e0'
sm3('') // '1ab21d8355cfa17f8e61194831e81a8f22bec8c728fefb747ed035eb5082aa2b'sm4Encrypt(data, key, options?) / sm4Decrypt(data, key, options?)
SM4 分组密码(GB/T 32907-2016),128 位密钥、128 位分组,支持 ECB / CBC 与 PKCS#7 填充。
| 选项 | 默认值 | 说明 |
| ---------- | ------- | ------------------------------------------------------------------ |
| mode | 'ecb' | 'ecb' 或 'cbc',CBC 需同时提供 iv |
| iv | — | 16 字节 IV,32 位十六进制或字节数组 |
| padding | true | PKCS#7 填充;关闭时明文长度必须是 16 的整数倍 |
| encoding | 'hex' | 密文的输入输出编码:'hex' / 'base64' / 'bytes'(仅加密输出) |
密钥写法:32 位十六进制字符串或 16 字节 Uint8Array。
import { sm4Encrypt, sm4Decrypt } from 'dart-common'
const key = '0123456789abcdeffedcba9876543210'
sm4Encrypt('0123456789abcdeffedcba9876543210', key, { padding: false })
// '681edf34d206965e86b3e94f536e4246'(GB/T 32907 标准向量)
const cipher = sm4Encrypt('机密数据', key, { mode: 'cbc', iv: '00112233445566778899aabbccddeeff' })
sm4Decrypt(cipher, key, { mode: 'cbc', iv: '00112233445566778899aabbccddeeff' }) // '机密数据'ECB 相同明文块会产生相同密文块,仅建议用于兼容既有系统的场景,新系统请用 CBC。
SM2 签名与加密
基于 sm2p256v1 曲线(GB/T 32918),坐标运算用 Jacobian 投影坐标实现,标量乘只在最后做一次模逆。
import {
sm2GenerateKeyPair,
sm2GetPublicKey,
sm2Sign,
sm2Verify,
sm2Encrypt,
sm2Decrypt
} from 'dart-common'
const { privateKey, publicKey } = sm2GenerateKeyPair()
// privateKey: 64 位十六进制,publicKey: 130 位(04 || X || Y)
sm2GetPublicKey(privateKey) === publicKey // true
// 签名 / 验签
const { r, s } = sm2Sign('message digest', privateKey)
sm2Verify('message digest', { r, s }, publicKey) // true
sm2Verify('message digest', r + s, publicKey) // 也接受 128 位十六进制
sm2Verify('tampered', { r, s }, publicKey) // false
// 加密 / 解密
const cipher = sm2Encrypt('机密数据', publicKey)
sm2Decrypt(cipher, privateKey) // '机密数据'| API | 说明 |
| ------------------------------------------------------ | -------------------------------------- |
| sm2GenerateKeyPair() | 生成 { privateKey, publicKey } |
| sm2GetPublicKey(privateKey) | 由私钥推导公钥 |
| sm2GetZ(userId, publicKey) | 计算用户标识杂凑值 Z,用于排查对接问题 |
| sm2Sign(message, privateKey, { userId }) | 返回 { r, s } |
| sm2Verify(message, signature, publicKey, { userId }) | 返回 boolean |
| sm2Encrypt(plaintext, publicKey, { mode }) | 返回十六进制密文,默认 C1C3C2 |
| sm2Decrypt(ciphertext, privateKey, { mode }) | 返回 UTF-8 明文 |
互操作要点:
userId必须与对端一致。默认值1234567812345678,对接网关时若对方使用自定义 ID,验签会失败且不报格式错误。- 密文结构默认
C1C3C2(新国标),兼容旧系统时用{ mode: 'C1C2C3' }。C1 带04前缀输出,解密时也接受 64 字节裸点。 - 签名输出
{ r, s }(各 64 位十六进制),也接受r + s的 128 位拼接形式。 - 私钥/公钥都支持十六进制字符串与
Uint8Array;公钥不接受压缩点格式。 - 依赖
crypto.getRandomValues取随机数,缺少安全随机源时直接抛错而不是退化到Math.random。
验证情况:SM3 与 SM4 已与 OpenSSL(Node 内置 sm3 / sm4-ecb / sm4-cbc)逐字节对拍一致,SM4 通过 GB/T 32907 标准向量;SM2 的曲线参数、基点、b 常量与「私钥推导公钥」均与 GB/T 32918 附录示例一致,签名验签与加解密做了全链路自洽与异常用例测试,但未做跨实现签名对拍 —— 接入具体网关前建议用对端提供的测试向量再验一次。
通用工具函数
按领域分模块,全部为纯函数、零依赖,与上述加密/编码能力同源导出。
类型判断(src/type)
getType、isObject、isPlainObject、isString、isNumber、isFiniteNumber、isFunction、isBoolean、isDate、isRegExp、isMap、isSet、isPromise、isNaNValue、isNil,加上原有的 isArray。
import { getType, isPlainObject, isNil, isPromise } from 'dart-common'
getType([]) // 'array'
getType(new Date()) // 'date'
isPlainObject({}) // true
isNil(undefined) // true
isPromise(Promise.resolve()) // true对象(src/object)
deepClone、deepMerge、get、pick、omit,加上原有的 isEmpty。
deepClone({ a: { b: 1 } }) // 深拷贝,优先 structuredClone
deepMerge({ a: { x: 1 } }, { a: { y: 2 } }) // { a: { x: 1, y: 2 } }
get({ a: { b: { c: 42 } } }, 'a.b.c', 'fb') // 42
pick({ a: 1, b: 2 }, ['a']) // { a: 1 }
omit({ a: 1, b: 2 }, ['a']) // { b: 2 }字符串(src/string)
camelCase、kebabCase、capitalize、truncate、padStart、padEnd、trim(含全角空格)、splitUnique、escapeHtml、unescapeHtml。
camelCase('foo-bar_baz') // 'fooBarBaz'
truncate('hello world', 8) // 'hello w…'
escapeHtml('<div>"a"</div>') // '<div>"a"</div>'数组(src/array)
unique、uniqueBy、chunk、groupBy、sum、max、min、flatten、flattenDeep、shuffle、intersperse、sample。
unique([1, 2, 2, 3]) // [1, 2, 3]
uniqueBy([{ id: 1 }, { id: 1 }], 'id') // [{ id: 1 }]
chunk([1, 2, 3, 4, 5], 2) // [[1, 2], [3, 4], [5]]
groupBy([{ t: 'a' }, { t: 'b' }, { t: 'a' }], 't')数字(src/number)
round、clamp、randomInt、randomFloat、randomIntInclusive、thousandSeparator、formatBytes。
round(1.005, 2) // 1.01(规避浮点误差)
clamp(5, 0, 3) // 3
thousandSeparator(1234567.891) // '1,234,567.891'
formatBytes(1536) // '1.5 KB'函数(src/function)
throttle、once、memoize,加上原有的 debounce。
window.addEventListener('scroll', throttle(onScroll, 100))
const init = once(() => expensiveSetup())
const fib = memoize((n) => (n < 2 ? n : fib(n - 1) + fib(n - 2)))随机(src/random)
randomString、randomCode、randomToken,加上原有的 guid。
randomString(8) // 'aB3xK9pQ'
randomCode(6) // '384920'(数字验证码)
randomToken() // 32 位随机十六进制开发
npm run dev # tsup watch 构建
npm run build # 产出 dist/index.js(ESM) + dist/index.cjs(CJS) + .d.ts
npm run lint # ESLint 检查
npm run format # Prettier 格式化源码位于 src/,按领域分子目录(object/、id/ …),每个模块统一从 src/index.js 导出。类型声明由 tsc 从 JSDoc 反向生成,因此新增函数必须写全 @param / @returns,否则类型会退化为 any。
License
GPL-3.0-or-later
