npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

dart-common

v0.0.1

Published

DART.IM组件库 - 通用前端工具类

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') // false

formatDate(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') // null

getFingerprint(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>') // '&lt;div&gt;&quot;a&quot;&lt;/div&gt;'

数组(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