https://fjc0k.github.io/vtils/
- 源于日常项目实践,更实用
- 使用 TypeScript 编写,类型友好
- 支持摇树优化(Tree Shaking),只引入使用到的工具
- 浏览器、Node、小程序多端兼容
vtils
自身并不包括一些已有成熟库的工具,如时间处理、网络请求等,在此做下推荐:
- 时间处理:dayjs
- 网络请求:axios、taro-axios
# yarn
yarn add vtils
# or, npm
npm i vtils --save
在线体验:https://stackblitz.com/edit/vtils
import { inBrowser, shuffle } from 'vtils'
if (inBrowser()) {
alert('您在浏览器中...')
}
alert(shuffle([1, 2, 3, 4]))
👇 | 👇 | 👇 | 👇 | 👇 | 👇 | 👇 | 👇 | 👇 |
---|---|---|---|---|---|---|---|---|
AnyFunction | AnyObject | AsyncOrSync | Brand | Defined | If | IsNever | LiteralUnion | Merge |
Omit | OneOrMore | ValueOf |
分配来源对象的可枚举属性到目标对象上。
来源对象的应用规则是从左到右,随后的下一个对象的属性会覆盖上一个对象的属性。
assign(
{},
{ x: 1 },
{ y: 2 },
{ x: 5, z: 9 },
)
// => { x: 5, y: 2, z: 9 }
返回 base64
解码后的字符串。
base64Decode('dnRpbHM=') // => vtils
base64Decode('5Lit5Zu9') // => 中国
base64Decode('8J+RqOKAjfCfkrs=') // => 👨💻
返回 base64
编码后的字符串。
base64Encode('vtils') // => dnRpbHM=
base64Encode('中国') // => 5Lit5Zu9
base64Encode('👨💻') // => 8J+RqOKAjfCfkrs=
返回 base64url
解码后的字符串。
base64UrlDecode('dnRpbHM') // => vtils
base64UrlDecode('5Lit5Zu9') // => 中国
base64UrlDecode('8J-RqOKAjfCfkrs') // => 👨💻
返回 base64url
编码后的字符串。
base64UrlEncode('vtils') // => dnRpbHM
base64UrlEncode('中国') // => 5Lit5Zu9
base64UrlEncode('👨💻') // => 8J-RqOKAjfCfkrs
如果 value
是数组,直接返回;如果 value
不是数组,返回 [value]
。
castArray([123, 456]) // => [123, 456]
castArray(123) // => [123]
castArray('hello') // => ['hello']
castArray(null) // => [null]
将 arr
拆分成多个 size
长度的区块,并将它们组合成一个新数组返回。
如果 arr
无法等分,且设置了 filler
函数,剩余的元素将被 filler
函数的返回值填充。
const arr = [1, 2, 3, 4, 5, 6]
chunk(arr, 2) // => [[1, 2], [3, 4], [5, 6]]
chunk(arr, 3) // => [[1, 2, 3], [4, 5, 6]]
chunk(arr, 4) // => [[1, 2, 3, 4], [5, 6]]
chunk(arr, 4, index => index) // => [[1, 2, 3, 4], [5, 6, 0, 1]]
返回限制在最小值和最大值之间的值。
clamp(50, 0, 100) // => 50
clamp(50, 0, 50) // => 50
clamp(50, 0, 49) // => 49
clamp(50, 51, 100) // => 51
创建 URI 查询字符串。
createURIQuery({ x: 1, y: 'z' }) // => x=1&y=z
创建一个去抖函数,将触发频繁的事件合并成一次执行。
该函数被调用后,计时 wait
毫秒后调用 fn
函数。若在 wait
毫秒内该函数再次被调用,则重新开始计时。
一个应用场景:监听输入框的 input
事件发起网络请求。
document.querySelector('#input').oninput = debounce(
e => {
console.log(e.target.value)
},
500,
)
检查 value
是否是 null
、undefined
、NaN
,是则返回 defaultValue
,否则返回 value
。
defaultTo(1, 2) // => 1
defaultTo(NaN, 2) // => 2
defaultTo(null, 2) // => 2
defaultTo(undefined, 2) // => 2
检查 str
是否以 needle
结尾。
endsWith('hello', 'llo') // => true
endsWith('hello', 'he') // => false
使用 value
来填充(替换) arr
,从 start
位置开始, 到 end
位置结束(但不包括 end
位置)。
fill(Array(5), () => 1) // => [1, 1, 1, 1, 1]
fill(Array(3), (value, index) => index) // => [0, 1, 2]
移动端屏幕适配。
遍历对象的可枚举属性。若遍历函数返回 false
,遍历会提前退出。
注:基于你传入的 obj
,遍历函数中 key
的类型可能为 number
,但在运行时,key
始终为 string
,因此,你应该始终把 key
当作 string
处理。(为什么会这样?https://github.com/microsoft/TypeScript/pull/12253#issuecomment-263132208)
forOwn(
{ x: '1', y: 2 },
(value, key) => {
console.log(key, value)
}
)
获取全局对象。
// 浏览器中
getGlobal() // => window
// Node 中
getGlobal() // => global
检测 value
的类型。
getType(1) // => 'Number'
getType(true) // => 'Boolean'
getType([]) // => 'Array'
getType(/hello/) // => 'RegExp'
根据 iteratee
返回的值对 data
进行分组。
groupBy(
[
{ type: 1, name: '石头' },
{ type: 3, name: '花生' },
{ type: 2, name: '鲸鱼' },
{ type: 1, name: '树木' },
{ type: 2, name: '鲨鱼' },
],
item => item.type,
)
// => {
// => 1: [
// => { type: 1, name: '石头' },
// => { type: 1, name: '树木' },
// => ],
// => 2: [
// => { type: 2, name: '鲸鱼' },
// => { type: 2, name: '鲨鱼' },
// => ],
// => 3: [
// => { type: 3, name: '花生' },
// => ],
// => }
检查 key
是否是对象 obj
自身的属性。
const obj = { x: 1, 2: 'y' }
has(obj, 'x') // => true
has(obj, 2) // => true
has(obj, 'toString') // => false
立即调用函数并返回其返回值。
注:ii = immediately invoke
ii(() => 1) // => 1
检查是否在 Android
设备中。
// Android 设备中
inAndroid() // => true
inAndroid(
() => console.log('你在 Android 设备中'),
)
检查是否在浏览器环境中。
// 浏览器中
inBrowser() // => true
inBrowser(
() => console.log('你在浏览器中'),
)
检查是否在 iOS
设备中。
// iOS 设备中
inIOS() // => true
inIOS(
() => console.log('你在 iOS 设备中'),
)
检查是否在 Node
环境中。
// Node 中
inNode() // => true
inNode(
() => console.log('你在 Node 中'),
)
检查 value
是否在某区间内。
// 2 是否在区间 (0, 2) 内
inRange(2, 0, 2, InRangeIntervalType.open) // => false
// 2 是否在区间 [0, 2] 内
inRange(2, 0, 2, InRangeIntervalType.closed) // => true
// 2 是否在区间 [0, 2) 内
inRange(2, 0, 2, InRangeIntervalType.leftClosedRightOpen) // => false
// 2 是否在区间 (0, 2] 内
inRange(2, 0, 2, InRangeIntervalType.leftOpenRightClosed) // => true
检查是否在微信小程序环境中。
// 微信小程序中
inWechatMiniProgram() // => true
inWechatMiniProgram(
() => console.log('你在微信小程序中'),
)
检查是否在微信浏览器环境中。
// 微信浏览器中
inWechatWebview() // => true
inWechatWebview(
() => console.log('你在微信浏览器中'),
)
检索值 value
是否在数组 arr
中。
includes([1, 2, 3], 1) // => true
includes([NaN, 2, 3], NaN) // => true
includes([1, 2, 3], 4) // => false
检索可枚举属性值 value
是否在对象 obj
中。
includes({ x: 1, y: 2 }, 1) // => true
includes({ x: 1, y: 2 }, 3) // => false
检索值 value
是否在字符串 str
中。
includes('hello', 'h') // => true
includes('hello', 'll') // => true
includes('hello', '123') // => false
检查 value
是否是一个 arguments
对象。
function myFunction() {
console.log(isArguments(arguments)) // true
}
检查 value
是否是一个数组。
isArray(['x']) // => true
isArray('x') // => false
检查 value
是否是一个布尔值。
isBoolean(true) // => true
isBoolean(false) // => true
isBoolean('true') // => false
检查 value
是否是合法的中国大陆居民 18
位身份证号码。
isChineseIDCardNumber('123456') // => false
检查 value
是否是一个日期。
isDate(new Date()) // => true
检查 value
是否是一个邮件地址。
isEmail('[email protected]') // => true
isEmail('hello@foo') // => false
检查 value
是否是空值,包括:undefined
、null
、''
、false
、true
、[]
、{}
。
isEmpty(undefined) // => true
isEmpty(null) // => true
isEmpty('') // => true
isEmpty(false) // => true
isEmpty(true) // => true
isEmpty([]) // => true
isEmpty({}) // => true
检查给定的数组的各项是否相等。
isEqualArray([1], [1]) // => true
isEqualArray([1], [5]) // => false
检查 value
是否是原始有限数值。
isFinite(1) // => true
isFinite(Infinity) // => false
检查 value
是否是一个函数。
isFunction(() => {}) // => true
isFunction(2000) // => false
检查 value
是否全是汉字。
isHan('hello') // => false
isHan('嗨咯') // => true
检查 value
是否是一个整数。
isInteger(1) // => true
isInteger(1.2) // => false
isInteger(-1) // => true
检查 value
是否是 NaN
。
isNaN(NaN) // => true
isNaN(2) // => false
检查 value
是否是一个负整数。
isNegativeInteger(-1) // => true
isNegativeInteger(1) // => false
检查 value
是否是 null
或 undefined
。
isNil(null) // => true
isNil(undefined) // => true
检查 value
是否是 null
。
isNull(null) // => true
检查 value
是否是一个数字。
注:NaN
不被认为是数字。
isNumber(1) // => true
isNumber(0.1) // => true
isNumber(NaN) // => false
检查 value
是否是一个数值。
注:Infinity
、-Infinity
、NaN
不被认为是数值。
isNumeric(1) // => true
isNumeric('1') // => true
检查 value
是否是一个对象。
isObject({}) // => true
isObject(() => {}) // => true
isObject(null) // => false
检查 value
是否是一个普通对象。
isPlainObject({}) // => true
isPlainObject(Object.create(null)) // => true
isPlainObject(() => {}) // => false
检查 value
是否是一个正整数。
isPositiveInteger(1) // => true
isPositiveInteger(-1) // => false
检测 number
是否可能是中国的手机号码。
isPossibleChineseMobilePhoneNumber(18000030000) // => true
isPossibleChineseMobilePhoneNumber(10086) // => false
检测 value
是否可能是中国人的姓名,支持少数名族姓名中间的 ·
号。
isPossibleChineseName('鲁') // => false
isPossibleChineseName('鲁迅') // => true
isPossibleChineseName('买买提·吐尔逊') // => true
检查 value
是否像 Promise
。
isPromiseLike(Promise.resolve()) // => true
检查 value
是否是一个正则对象。
isRegExp(/hello/) // => true
isRegExp(new RegExp('hello')) // => true
检查 value
是否是一个字符串。
isString('') // => true
isString('hello') // => true
检查 value
是否等于 undefined
。
isUndefined(undefined) // => true
isUndefined(void 0) // => true
检查 value
是否是一个有效的网址,仅支持 http
、https
协议,支持 IP
域名。
isUrl('http://foo.bar') // => true
isUrl('https://foo.bar/home') // => true
这是一个 jest 测试辅助函数,等同于 expect(actual).toEqual(expected)
,只不过是加上了类型。
根据 iteratee
返回的键对 data
进行分组,但只保留最后一个结果。
keyBy(
[
{ type: 1, name: '石头' },
{ type: 3, name: '花生' },
{ type: 2, name: '鲸鱼' },
{ type: 1, name: '树木' },
{ type: 2, name: '鲨鱼' },
],
item => item.type,
)
// => {
// => 1: { type: 1, name: '树木' },
// => 2: { type: 2, name: '鲨鱼' },
// => 3: { type: 3, name: '花生' },
// => }
返回 obj
的可枚举属性组成的数组。
注:基于你传入的 obj
,返回的 key
的类型可能为 number
,但在运行时,key
始终为 string
,因此,你应该始终把 key
当作 string
处理。(为什么会这样?https://github.com/microsoft/TypeScript/pull/12253#issuecomment-263132208)
keys({ x: 1, 2: 'y' }) // => ['x', '2'] 或 ['2', 'x']
返回数组 arr
的最后一项。
last([1, 2, 3]) // => 3
加载图片、代码、样式等资源。
loadResource([
'https://foo.bar/all.js',
'https://foo.bar/all.css',
'https://foo.bar/logo.png',
{
type: LoadResourceUrlType.js,
path: 'https://s1.foo.bar/js/full',
alternatePath: 'https://s2.foo.bar/js/full',
},
]).then(() => {
// 资源加载完成后的操作
})
映射对象的可枚举属性值为一个新的值。
mapValues(
{ x: 1, y: 2 },
value => value + 10,
)
// => { x: 11, y: 12 }
函数结果缓存。
let i = 0
const fn = memoize(() => i++)
fn() // => 0
fn() // => 0
无操作函数。
noop() // => undefined
创建一个从 obj
中剔除选中的可枚举属性的对象。
omit({ x: 1, y: 2 }, ['x']) // => { y: 2 }
允许指定一个或多个规则对数据进行排序。
orderBy(
['x', 'xyz', 'xy'],
{
iteratee: item => item.length,
type: OrderByRuleType.desc,
},
)
// => ['xyz', 'xy', 'x']
在 str
右侧填充字符。
padEnd('姓名', 4, '*') // => 姓名**
在 str
左侧填充字符。
padStart('姓名', 4, '*') // => **姓名
并行执行任务,同步任务
、异步任务
皆可。
parallel([
() => 1,
async () => 'hello',
]).then(res => {
// => [1, 'hello']
})
解析 CSS
值的数值和单位。
parseCSSValue('12px') // => { value: 12, unit: 'px' }
parseCSSValue(12) // => { value: 12, unit: 'px' }
parseCSSValue('12%') // => { value: 12, unit: '%' }
解析 URI 查询字符串。
兼容以 ?
开头的查询字符串,因此你可以直接传入 location.search
的值。
parseURIQuery('x=1&y=z') // => { x: '1', y: 'z' }
parseURIQuery('?x=1&y=z') // => { x: '1', y: 'z' }
parseURIQuery(
'x=1&y=z',
parameters => ({
...parameters,
x: Number(parameters.x),
}),
) // => { x: 1, y: 'z' }
创建一个从 obj
中选中的可枚举属性的对象。
pick({ x: 1, y: 2 }, ['x']) // => { x: 1 }
给定大小获取占位猫咪图片,图片来自:https://placekitten.com/
placeKitten(100) // => https://placekitten.com/100/100
给定宽高获取占位猫咪图片,图片来自:https://placekitten.com/
placeKitten(100, 200) // => https://placekitten.com/100/200
将数据中每一项的迭代值组合成一个数组返回。
pluck(
[{ id: 1, name: 'Jay' }, { id: 2, name: 'Lily' }],
item => item.name,
) // => ['Jay', 'Lily']
将数据中每一项的迭代值组合成一个对象返回。
pluck(
[{ id: 1, name: 'Jay' }, { id: 2, name: 'Lily' }],
item => item.name,
item => item.id,
) // => { 1: 'Jay', 2: 'Lily' }
生成一个随机字符串。
randomString() // => m481rnmse1m
创建一个包含从 start
到 end
,但不包含 end
本身范围数字的数组。
range(0, 5) // => [0, 1, 2, 3, 4]
range(0, -5, -1) // => [0, -1, -2, -3, -4]
重复 n
次给定字符串。
repeat('a', 5) // => aaaaa
对传入的数字按给定的精度四舍五入后返回。
round(3.456) // => 3
round(3.456, 1) // => 3.5
round(3.456, 2) // => 3.46
round(345, -2) // => 300
对传入的数字按给定的精度向下取值后返回。
roundDown(3.456) // => 3
roundDown(3.456, 1) // => 3.4
roundDown(3.456, 2) // => 3.45
roundDown(345, -2) // => 300
对传入的数字按给定的精度向上取值后返回。
roundUp(3.456) // => 4
roundUp(3.456, 1) // => 3.5
roundUp(3.456, 2) // => 3.46
roundUp(345, -2) // => 400
从数组中随机获取一个元素。
sample([1, 2, 3]) // => 1 或 2 或 3
从对象中随机获取一个可枚举属性的值。
sample({ x: 1, y: 2, z: 3 }) // => 1 或 2 或 3
顺序执行任务,同步任务
、异步任务
皆可。
sequential([
() => 1,
async () => 'hello',
]).then(res => {
// => [1, 'hello']
})
打乱一个数组。
shuffle([1, 2]) // => [1, 2] 或 [2, 1]
检查 str
是否以 needle
开头。
startsWith('hello', 'he') // => true
startsWith('hello', 'llo') // => false
计算传入值的总和。
sum([1, 2, 3]) // => 6
根据 iteratee
返回的结果计算传入值的总和。
sumBy(
[
{ count: 1 },
{ count: 2 },
{ count: 3 },
],
item => item.count,
)
// => 6
创建一个节流函数,给函数设置固定的执行速率。
- 该函数首次被调用时,会立即调用
fn
函数,并记录首次调用时间。- 该函数第二次被调用时:
- 如果该次调用时间在首次调用时间的
wait
区间内,timer = setTimeout(操作, 时间差)
;- 该函数再次被调用时:
- 如果该次调用时间在首次调用时间的
wait
区间内,什么都不做; - 否则,清除首次调用时间和计时器,回到第一步。
- 如果该次调用时间在首次调用时间的
- 该函数再次被调用时:
- 否则,清除首次调用时间,回到第一步。
- 如果该次调用时间在首次调用时间的
- 该函数第二次被调用时:
一个应用场景:监听窗口的 resize
事件响应相关操作。
window.addEventListener(
'resize',
throttle(
() => console.log('窗口大小改变后的操作'),
1000,
),
)
调用函数 n
次,将每次的调用结果存进数组并返回。
times(4, () => {
// 这里将会执行 4 次
})
尝试执行 accessor
返回值,若其报错,返回默认值 defaultValue
。
const obj = { x: 1 }
tryGet(() => obj.x, 2) // => 1
tryGet(() => obj.x.y, 2) // => 2
尝试执行 accessor
返回值,若其报错,返回 undefined
。
const obj = { x: 1 }
tryGet(() => obj.x) // => 1
tryGet(() => obj.x.y) // => undefined
将给定的数组去重后返回。
unique([1, 2, 1, 3]) // => [1, 2, 3]
返回 obj
自身可枚举属性值组成的数组。
values({ x: 1, 2: 'y' }) // => [1, 'y'] 或 ['y', 1]
等待一段时间。
wait(1000).then(() => {
// 等待 1000 毫秒后执行
})
资源释放器。
const disposer = new Disposer()
const timer = setInterval(
() => console.log('ok'),
1000,
)
disposer.add(() => clearInterval(timer))
document.querySelector('#stop').onclick = () => {
disposer.dispose()
}
微信小程序 Storage
适配器。
由于微信小程序的 wx.getStorageSync
方法对于不存在的项目会返回 空字符串
,导致无法判断项目是否存在,因此,该适配器对存储的内容做了一层封装,以保证相关操作的结果可确定。
数据对象验证器。
interface Data {
name: string,
phoneNumber: string,
pass1: string,
pass2: string,
}
const ev = new EasyValidator<Data>([
{
key: 'name',
type: 'chineseName',
message: '请输入真实姓名',
},
{
key: 'phoneNumber',
type: 'chineseMobilePhoneNumber',
message: '请输入正确的手机号码',
},
{
key: 'phoneNumber',
test: async ({ phoneNumber }, { updateMessage }) => {
const result = await checkPhoneNumberAsync(phoneNumber)
if (!result.valid) {
updateMessage(result.message)
return false
}
},
message: '请输入正确的手机号码'
},
{
key: 'pass1',
test: ({ pass1 }) => pass1.length > 6,
message: '密码应大于6位',
},
{
key: 'pass2',
test: ({ pass1, pass2 }) => pass2 === pass1,
message: '两次密码应一致',
},
])
ev.validate({
name: '方一一',
phoneNumber: '18087030070',
pass1: '1234567',
pass2: '12345678'
}).then(res => {
// => { valid: false, unvalidRules: [{ key: 'pass2', test: ({ pass1, pass2 }) => pass2 === pass1, message: '两次密码应一致' }] }
})
事件巴士,管理事件的发布与订阅。
const bus = new EventBus<{
success: () => void,
error: (message: string) => void,
}>()
const unbindSuccessListener = bus.on('success', () => {
console.log('成功啦')
})
const unbindErrorListener = bus.once('error', message => {
console.error(message)
})
bus.emit('success')
bus.emit('error', '出错啦')
unbindSuccessListener()
bus.off('error')
对微信 JSSDK 的封装。
const wechat = new Wechat()
getWechatConfigAsync().then(config => {
wechat.config(config)
})
wechat.updateShareData({
title: '分享标题',
desc: '分享描述',
link: '分享链接',
imgUrl: '缩略图地址',
})
wechat.invoke('scanQRCode').then(res => {
// => API 调用结果
})
任意函数类型。
任意对象类型。
// before
type X = PromiseLike<string> | string
// after
type X = AsyncOrSync<string>
名义化类型。
type User = { id: Brand<number, User>, name: string }
type Post = { id: Brand<number, Post>, title: string }
type UserIdIsNumber = User['id'] extends number ? true: false // => true
type PostIdIsNumber = Post['id'] extends number ? true: false // => true
type PostIdIsNotUserId = Post['id'] extends User['id'] ? false : true // => true
从 T
中排除 undefined
类型。
interface User {
gender?: 'male' | 'female',
}
// before
type UserGender = Exclude<User['gender'], undefined>
// after
type UserGender = Defined<User['gender']>
条件类型。
type X = 'x'
// before
type IsX = X extends 'x' ? true : false
// after
type IsX = If<X extends 'x', true, false>
检查 T
是否是 never
类型。
type X = never
// before
type XIsNever = [X] extends [never] ? true : false
// after
type XIsNever = IsNever<X>
字面量联合类型。
// before: China, American 将得不到类型提示
type Country = 'China' | 'American' | string
// after: China, American 将得到类型提示
type Country = LiteralUnion<'China' | 'American', string>
合并两个类型,后一个类型的定义将覆盖前一个类型的定义。
type X = Merge<
{ x: number, y: number },
{ x: string, z: string }
>
// => { x: string, y: number, z: string }
从接口 T
中去除指定的属性。
type X = Omit<
{ x: number, y: string, z: boolean },
'x' | 'z'
>
// => { y: string }
// before
type X = number | number[]
// after
type X = OneOrMore<number>
返回接口 T
属性值的类型。
type V = ValueOf<{ x: number, y: string, z: boolean }>
// => number | string | boolean
MIT ©️ Jay Fong