# kayran / 七零八碎工具函数合集

**Installation**

![NPM](https://nodei.co/npm/kayran.png)

```bash
pnpm add kayran lodash-es
```

<br>

## waitFor / 以优雅方式书写 await

### Param

```ts
/**
 * @param {Promise} 需要包装的Promise
 * @returns {any[]} [0]为Promise.prototype.then返回的结果，[1]为Promise.prototype.catch返回的结果
 */
```

```ts
// 示例

import { waitFor } from 'kayran'

const [res] = await waitFor(new Promise((resolve, reject) => {
  resolve('res')
}))
console.log(res) // 'res'

const [, err] = await waitFor(new Promise((resolve, reject) => {
  reject('err')
}))
console.log(err) // 'err'
```

<br>

## getMediaDuration / 获取音视频文件的时长

### Param

```ts
/**
 * @param {File|string} 音视频url或二进制文件
 * @returns {Promise<number>} 时长 单位秒 四舍五入
 */
```

```ts
// 示例

import { getMediaDuration } from 'kayran'

getMediaDuration('https://xxx.mp4')
```

<br>

## typeOf / 获取变量的精确类型

动机：原生js的typeof等类型检测手段都存在各种缺陷

### Param

```ts
/**
 * @param {any} 需要判断的变量
 * @returns {string} 变量类型（全小写） 如string null undefined file...
 */
```

```ts
// 示例

import { typeOf } from 'kayran'

typeOf(1) // 'number'
```

<br>

## isEllipsis / 判断dom是否触发溢出省略（text-overflow: ellipsis）

### Param

```ts
/**
 * @param {Element} 需要判断的元素
 * @returns {boolean} 是否触发溢出省略
 */
```

```ts
// 示例

import { isEllipsis } from 'kayran'

isEllipsis(document.querySelector('.text'))
```

<br>

## isEmpty & notEmpty / 判空 & 判非空

notEmpty() 等同于 !isEmpty()

### Param

```ts
/**
 * @param {any} 需要判断的变量
 * @returns {boolean} 结果
 */
```

```ts
// 示例

import { isEmpty, notEmpty } from 'kayran'

isEmpty(0)  // false
isEmpty({}) // true
isEmpty([]) // true
```

<br>

## jsonToFormData / json转FormData

### Param

```ts
/**
 * @param {object} 需要转换的对象
 * @param {(value?: any, key?: any) => any} mapFn 每个属性会执行该回调函数，返回值为新的属性值
 * @returns {FormData} 转换后的FormData实例
 */
```

```ts
// 示例

import { jsonToFormData } from 'kayran'

const formData = jsonToFormData({
  a: 1
})

formData.get('a') // '1'
```

```ts
// 绑定到FormData上，像Array.from一样使用

import { jsonToFormData } from 'kayran'

if (FormData.from === undefined) {
  FormData.from = jsonToFormData
}

const formData = FormData.from({
  a: '1',
  b: '2'
})
```

```ts
// mapFn示例

import { jsonToFormData } from 'kayran'

const formData = jsonToFormData({
  a: 1
}, v => v + 1)

formData.get('a') // '2'
```

::: tip  
回调函数的返回值为undefined时，该元素不会被添加到FormData实例中（类似filter效果）。
:::

<br>

## pickDeepBy / lodash-pickBy的递归版本

### Param

```ts
/**
 * @param {object} obj 原始对象
 * @param {(value?: any, key?: any) => boolean} predicate 每个属性都会调用的方法
 * @returns {object} 新的对象
 */
```

```ts
// 示例

import { pickDeepBy } from 'kayran'

pickDeepBy({
  a: 1,
  b: NaN
}, (v, k) => ![NaN, null, undefined].includes(v))

// { a: 1 }
```

> pickDeepBy不会改变原始对象。

<br>

## isSameOrigin / 判断是否同源

### Param

```ts
/**
 * @param {string} url 需要判断的url
 * @returns {boolean}
 */
```

```ts
// 示例

import { isSameOrigin } from 'kayran'

isSameOrigin('www.google.com')
```

<br>

## parseQueryString / 获取 url 中某个查询参数的值

### Param

```ts
/**
 * 获取当前url某个查询参数的值
 * 支持微信公众号授权特例：授权后会重定向回来并在链接中加入一个code参数 但微信没有考虑hash路由的情况 所以获取这个code需要特殊处理
 * @param {object}
 *        {string} key 查询参数的key 如果为空 则返回查询参数映射的对象 可以解构使用
 *        {string} [mode='hash'] router模式 可选值'history'/'hash'
 *        {boolean} del 是否在url中删除该值（删除会引发页面刷新）
 * @returns {string|object} 查询参数中key对应的value / 如果key为空 则返回查询参数整个对象
 */
```

```ts
// 示例

import { parseQueryString } from 'kayran'

const code = parseQueryString('code')

//or

const { code } = parseQueryString()
```

注意：如果search和hash中同时包含code 如```http://localhost:8080/?code=1#/?code=2``` 取的是search中的code 即返回1

<br>

## loadLink / 动态加载link

### Param

```ts
/**
 * @param {string|object} src link url
 * @returns {Promise}
 */
```

```ts
// 示例

import { loadLink } from 'kayran'

await loadLink('https://cdn.bootcdn.net/ajax/libs/normalize/8.0.1/normalize.min.css')
```

<br>

## loadScript / 动态加载js

### Param

```ts
/**
 * @param {string|object} src 脚本url
 * @returns {Promise}
 */
```

```ts
// 示例

import { loadScript } from 'kayran'

loadScript('https://cdn.jsdelivr.net/npm/vue/dist/vue.js').then(e => {
  console.log(Vue)
})
```

<br>

## loadStyle / 动态加载style

### Param

```ts
/**
 * @param {string|object|HTMLElement} arg style元素的innerText 或传对象指定style的各项属性 或style元素本身
 * @returns {Promise<HTMLElement>}
 */
```

```ts
// 示例

import { loadStyle } from 'kayran'

await loadStyle(`
ul {
  list-style: none;
}
`)
```

<br>

## getFinalProp / 获取最终prop（适用于组件开发者）

Vue提供了prop的局部配置和默认值配置，但在封装组件时，还会非常需要一个“全局配置”，否则可能导致每个组件实例进行重复的配置。

举个例子，`Element` 的size与zIndex就是支持全局配置的。

当配置多了以后，由于存在不同的优先级，最终组件采用的是哪一项配置，需要进行一定的判断，

在涉及到对象和函数时，判断可能会变得相当复杂。

getFinalProp的作用就是帮助你计算出最终的配置。

### Features

- 和Vue的props一样，提供是否必传、数据类型和自定义的校验
- 对于plain object类型的prop，支持深合并、浅合并和直接覆盖
- 对于function类型的prop，支持融合、直接覆盖
- 支持将对象的键统一为驼峰命名
- 支持动态生成默认值

### Param

```ts 
/**
 * @param {any[]} propSequence - prop序列（优先级从高到低，最后是默认值）
 * @param {object} [config] - 配置
 * @param {string} [config.name] - prop名称，用于报错提示
 * @param {string|string[]} [config.type] - 数据类型校验
 * @param {any} [config.default] - 默认值（显式）
 * @param {boolean} [config.defaultIsDynamic = false] - 动态生成默认值
 * @param {boolean} [config.required = false] - 是否必传校验
 * @param {function} [config.validator] - 自定义校验
 * @param {string} [config.camelCase = true] - 是否将对象的键统一为驼峰命名
 * @param {false|string} [config.mergeObject = 'deep'] - 合并对象的方式
 * @param {boolean} [config.mergeObjectApplyOnlyToDefault = false] - mergeObject仅作用于default
 * @param {false|((accumulator, currentValue, index?, array?) => Function)} [config.mergeFunction = false] - 融合函数的方式
 * @param {boolean} [config.mergeFunctionApplyOnlyToDefault = true] - mergeFunction仅作用于default
 * @returns {any} 最终的prop
 */
```

```ts
// 示例

import { getFinalProp } from 'kayran'

getFinalProp([1, 2, undefined]) // 1
```

### 怎么判断某个prop有没有传？

以该prop是否全等于undefined作为标识

### config.mergeObject

- `'deep'`: 深合并，高权重prop的对象键会覆盖低权重prop的同名键，包含嵌套的对象（默认值）
- `'shallow'`: 浅合并，高权重prop的对象键会覆盖低权重prop的同名键，不含嵌套的对象
- `false`: 不合并，直接覆盖，高权重prop的对象会直接覆盖低权重prop的对象，与值类型的表现一致

### config.mergeObjectApplyOnlyToDefault

默认关闭，仅在mergeObject开启时有效。

开启时，mergeObject的规则仅会应用于最后与default进行比对的环节中，之前的对象依然会直接覆盖。

关闭时，mergeObject的规则会应用至所有对象类型prop的权重比对中。

使用场景：组件作者想要将组件内部的配置与组件使用者的配置进行合并，但组件使用者自身的各级配置依然保持直接覆盖的规则。

### config.mergeFunction

使用场景：在封装组件时，你可能需要通过配置选项的方式监听底层依赖的某些事件，

在将该依赖的配置选项暴露出去时，组件使用者的配置就会与你的配置发生冲突。

mergeFunction提供定制化的方式来融合函数类型的prop。

举个例子，知名的富文本库tinymce的配置选项中有一个叫 `init_instance_callback` 的回调，

在封装这个库时，可以藉此来做一些初始化的工作，为了不破坏组件的灵活性，也会将tinymce的配置选项暴露出去，

问题来了，组件使用者一旦配置了这个回调，就会与你的配置发生冲突。

与其他数据类型的配置不同的是，函数类型的prop，往往不期望被用户的配置直接覆盖掉，会有需要进行“融合”的需求。

融合：既执行组件使用者配置的函数，也执行组件内部配置的函数。

函数类型的prop包括两种情况：

- prop本身是函数
- prop是含有函数属性的对象

getFinalProp内部使用 [Array.prototype.reduce](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Array/Reduce)
来执行函数融合，mergeFunction将被用作参数1。

```ts
getFinalProp([
  () => {
    console.log('我是参数1')
  },
  () => {
    console.log('我是参数2')
  }
], {
  default: () => {
    console.log('我是显式默认值')
  },
  mergeFunction: (accumulator, item) => (...args) => {
    accumulator(...args)
    item?.(...args)
  },
  mergeFunctionApplyOnlyToDefault: false,
})()

// 结果会打印 '我是显式默认值' '我是参数2' '我是参数1' 
```

### config.mergeFunctionApplyOnlyToDefault

默认开启，仅在mergeFunction开启时有效。

函数融合毕竟是一个特殊行为，往往只有组件作者会用到这个功能，

对于组件使用者来说，函数类型的配置可能更希望的是和其他原始类型一样，直接覆盖掉就好了。

开启时，mergeFunction的规则仅会应用于最后与default进行比对的环节中，之前的函数依然会直接覆盖。

关闭时，mergeFunction的规则会应用至所有函数类型prop的权重比对中。

### config.default

显式指定默认值，如果没有开启 `mergeObjectApplyOnlyToDefault` 或 `mergeFunctionApplyOnlyToDefault` 的话，则没有必要使用该参数，将默认值放在 `propSequence` 的末尾即可。

#### config.camelCase

Vue的prop同时支持驼峰和短横线格式，如果组件使用者同时传了同一个prop的两种格式，值还是不相同的，问题来了，此时应该取哪一个值？

在多个配置进行合并时，结果会更加难以预测，所以getFinalProp在**合并对象后**默认将对象的键统一为驼峰命名。

::: tip 为什么不默认使用短横线命名？  
参见[Vue官方风格指南](https://v3.cn.vuejs.org/style-guide/#prop-%E5%90%8D%E7%A7%B0%E5%BC%BA%E7%83%88%E6%8E%A8%E8%8D%90)
:::

### 动态生成默认值

使用场景：需要根据组件使用者传的参数来决定默认值

```ts
// 示例

getFinalProp([{
  a: {
    a: 1
  }
}, {
  a: {
    a: 2,
    b: 1
  }
}], {
  // userProp是参数1的计算结果
  default: userProp => ({
    a: {
      c: userProp.a.a === 1 ? 1 : null
    }
  }),
  defaultIsDynamic: true,
})

/**
 * 将得到：
 * {
 *   a: {
 *     a: 1,
 *     b: 1,
 *     c: 1
 *   }
 * }
 */
```

<br>

## getGlobalAttrs / 获取全局attrs（适用于组件开发者）

在Vue组件中，声明过的prop可以通过 `this.$props` 获取，没有声明的可以通过 `this.$attrs` 获取，

但是全局配置就无法区分哪部分是props哪部分是attrs了。

getGlobalAttrs就是帮助你获取全局配置中的attrs的。

使用场景：二次封装组件时，往往需要使用 `v-bind="$attrs"` 来实现不破坏底层组件的原生能力，

getGlobalAttrs就是在此基础上提供全局配置的能力。

### Param

```ts
/**
 * @param {object} globalConfig - 组件使用者的全局配置
 * @param {object} props - 组件内部的props对象
 * @returns {object} globalAttrs - 全局配置中的attrs部分
 */
```

### 用于Vue3（script setup）

```vue
<!-- 示例 -->

<template>
  <某底层组件 v-bind="Attrs"/>
</template>

<script setup>
import { computed, useAttrs, toRaw } from 'vue'
import { getFinalProp, getGlobalAttrs } from 'kayran'
import globalConfig from './config'

const props = defineProps(['modelValue'])

// Attrs即为局部与全局attrs经过权重计算得到的结果
const Attrs = computed(() => getFinalProp([
  { ...useAttrs() },
  getGlobalAttrs(globalConfig, toRaw(props)),
]))
</script>
```

### 用于Vue3

```vue
<!-- 示例 -->

<template>
  <某底层组件 v-bind="Attrs"/>
</template>

<script>
import { defineComponent, computed, toRaw } from 'vue'
import { getFinalProp, getGlobalAttrs } from 'kayran'
import globalConfig from './config'

export default defineComponent({
  setup (props, { attrs }) {
    // Attrs即为局部与全局attrs经过权重计算得到的结果
    const Attrs = computed(() => getFinalProp([
      { ...attrs },
      getGlobalAttrs(globalConfig, toRaw(props)),
    ]))
    
    return {
      Attrs,
    }
  }
})
</script>
```

### 用于Vue2

```vue
<!-- 示例 -->

<template>
  <某底层组件 v-bind="Attrs"/>
</template>

<script>
import { getFinalProp, getGlobalAttrs } from 'kayran'
import globalConfig from './config'

export default {
  computed: {
    // Attrs即为局部与全局attrs经过权重计算得到的结果
    Attrs () {
      return getFinalProp([
        this.$attrs,
        getGlobalAttrs(globalConfig, this.$props)
      ])
    }
  }
}
</script>
```
