# `@hyext/utils`

> 小程序业务开发工具库

## Installation

```shell
$ npm i @hyext/utils -S
```

## Usage

```js
import { memory, createLogger, createSDKPolyfill  }  from '@hyext/utils'
```

下述的每个函数都可以参照上方import导入。

## Summary
- [createLogger](#createlogger) - 创建一个logger对象
- [createSDKPolyfill](#createsdkpolyfill) - 基于小程序SDK二次封装的SDK对象
- [createPromisfyFnWithCatch](#createpromisfyfnwithcatch) - Catch Promisy Func
- [once](#once) - once
- [throttle](#throttle) - 防抖
- [createPolling](#createpolling) - 轮询
- [memory](#memory) - 记忆函数
- [promiseTimeout](#promisetimeout) - 超时promise
- [promiseRetry](#promiseretry) - 重试promise
- isUndef (v: any): boolean - 判断值是否为undefined或null
- isDef (v: any): boolean - 判断值不是undefined且不是null
- isTrue (v: any): boolean - v === true
- isFalse (v: any): boolean - v === false
- isObject (v: any): boolean - 判断值是否是一个对象
- isFunction (v: any): boolean - 判断值是否是一个函数
- isArray (v: any): boolean - 判断值是否是一个数组
- isPlainObject(v: any): boolean - 判断值是否是一个pure object
- isRegExp (v: any): boolean - 判断值是否是一个RegExp
- capitalize(v: string): string - 转首字母大写
- camelize(v: string): string - 转驼峰
- hyphenate(v: string): string - 转连字：xxA -> xx-a
- toNumber (val: string): number | string - 尝试将val转成number，失败则返回原字符串
- toString (val: any): string - 将任意值转成一个字符串
- second2Minute (seconds: number): string - 秒转成mm:ss
- delay (duration: number): Promise<void> - 延迟
- getTime(date:Date): string - date实例转成hh:mm:ss
- patchNumber(v: number): string - 数字补0
- second2Minute(seconds: number): string - 秒转·mm:ss·
- limit(v: string|number): string - 收缩范围，1 - +00 -> 1 - 99+
- isWebViewEnv() - 是否在webview环境。
- scalePxOnWebView(px: number, base: number = 750) - 在 webview 环境中使用 scalePx 接口。

## 模块类

### createLogger
- createLogger(options: CreateLoggerOptions) - 创建一个logger对象
- logger.log(msg: string, data: any) - 默认输出绿色字体的log
- logger.info(msg: string, data: any) - 默认输出蓝色字体的log
- logger.warn(msg: string, data: any) - 默认输出黄色字体的log
- logger.error(msg: string, data: any) - 默认输出红色字体的log

#### 入参解析
```js
type CreateLoggerOptions = {
  onBefore?: (logInfo: LogInfo) => void // log之前触发，传入一个LogInfo
  onAfter?: (logInfo: LogInfo) => void // log之后触发，传入一个LogInfo
  prefix?: (() => string) | string // 打印日志前缀
  logColor?: LogColor // 每个接口的字体颜色配置
  enableLog?: boolean | ((logInfo: LogInfo) => boolean) // 是否打印原生log，传入一个LogInfo
}

type LogInfo = {
  prefix: string // CreateLoggerOptions.prefix中获取
  msg: string // logger接口传入的msg
  data: any // logger接口传入的data
  desc: string // desc = prefix + msg
}

type LogColor = {
  info?: string // 颜色，可以是颜色英文或hex： 例如：red, #cccccc
  warn?: string
  error?: string
  log?: string
}
```

#### Demo 

```js
// create
const logger = createLogger({
  onAfter(logInfo) {
    global.hyext.logger.log(logInfo.desc + ' ' + JSON.stringify(logInfo.data))
  },
  prefix () {
    return `miniappName * date:${Date.now()} uid: ${uid} sessionId:${sessionId} -`
  },
  enableLog(logInfo) {
    // 例如我们在APP只通过onAfter钩子打印sdk的log,
    // 禁止打印原生log，提高性能
    if (platform !== 'web') return false 
    return true
  }
})


// call
logger.log('oh my god~')
```

### createSDKPolyfill 
- createSDKPolyfill(options: SDKPolyfillOptions) - 基于小程序SDK二次封装的SDK对象，补丁了错误的catch，可控制其接口的call，resolve, reject

#### 参数解析
```js
type SDKPolyfillOptions = {
  paths: string[] // 接口访问路径队列，例如： hyExt.advance.sendWup -> paths => ['advance.sendWup']
  SDK: SDKModel // 传入global.hyExt
  onError?: (errMsg: string, apiName: string, path: string) => void // SDK调用失败时 触发
  onCall?: (calledArgs: Array<any>, apiName: string, path: string) => void // SDK调用时 触发
  onSuccess?: (res: any, apiName: string, path: string) => void // SDK调用成功时 触发
}

type SDKModel = {
  [key: string]: any
}
```

### Demo
```js
// 与logger配合使用的例子，甩锅神器
const ployfillSDK = createSDKPolyfill({
  SDK: global.hyExt,
  paths: [
    'advance.sendWup'
  ],
  onCall(callArgs, apiName, path) {
    logger.log(`SDK.${path}开始调用`, callArgs)
  },
  onError(errMsg, apiName, path) {
    logger.log(`SDK.${path}调用失败`, errMsg)
  },
  onSuccess(res, apiName, path) {
    logger.log(`SDK.${path}调用成功`, res)
  },
  onPolyfill(polyfillSDK, apiName, apiFN) {
    // 补丁前触发, 可以拦截sdk进行二次处理。
    polyfillSDK[apiName] = jest.fn(apiFN)
  }
})

// ployfillSDK的每个接口只会resolve，因为内部已被catch错误，发生错误res是false
ployfillSDK.sendWup(options).then((res) => {
  if (!res) return; // 发生错误就跳过了

  // do something
}) 
```

## 高阶函数类

### getSetModeWithQueueFn

背景: 小程序SDK原来的setMode方法, resolve后小程序马上会被显示. 但是在新版APP上, setMode resolve 并不表示马上会显示, 而是会在恰当的时候通过 hyExt.popup.onNoticeShow 通知小程序. 这个变更会给部分小程序带来影响, 比如你要10秒后关闭浮窗, 并不能在调用setMode之后开始倒计时, 而是在 onNoticeShow 回调中开始倒计时.

本函数封装了SDK中的以下方法:
- setMode
- onNoticeShow/offNoticeShow
- onNoticeHide/offNoticeHide

getSetModeWithQueueFn  每次调用都会取消监听 noticeShow/noticeHide 事件, 再重新进行监听. 调用后返回 setModeWithQueue 函数

setModeWithQueue 函数是对setMode的封装, 用于支持 notice 排队.

对比setMode, 添加两个额外的参数:
- onShow: 如果当前mode是NOTICE, 那么当收到终端的展示事件时, 会调用这个函数. 如果当前mode不是NOTICE, 则在 setMode resolve 后调用这个函数
- onNoticeHide: 当notice超时被终端隐藏, 或者调用了setMode 切换到非NOTICE模式时 , 会调用这个函数. 

setModeWithQueue 兼容性:
- 可以兼容老版本APP, 在老版本APP上和原始的 setMode 方式行为一致
- 可以兼容不同的mode, 'NOTICE'/'NORMAL'/'RIGHT_BOTTOM_BTN'

#### Demo

```ts
const setModeWithQueue = getSetModeWithQueueFn(msg => console.log(msg))

const App = () => {
  // 收到推送后才显示notice
  const dataPush = useMainSelector(s => s.dataPush)
  const [didShow, setShow] = useState(false)

  useEffect(() => {
    setModeWithQueue({ mode: dataPush ? 'NOTICE' : 'NORMAL' }, onShow: () => {console.log('show')})
  }, [dataPush])

  if (!dataPush) return null

  return (
    <View>
      <Text>我迟早会显示</Text>
    </View>
  )
}
```

#### 参数
```ts
export function getSetModeWithQueueFn(logger: (msg: string) => void): SetModeWithQueueFn;

type SetModeWithQueueFn = (params: {
  mode: string,
  onShow?: Callback,
  onNoticeHide?: Callback,
  showTime?: number, // 期望展示的时间
  liveroomPopupKey?: string,
  waitAfterResetNormal?: number, // 重置为NORMAL后, 等待多久才调用setMode设置其他值, 默认800ms
  skipResetNormal?: boolean, // 是否跳过重置为NORMAL, 默认false
} & { [key: string]: any }) // 其他需要透传给hyExt.popup.setMode的参数
=> Promise<any>
```


### createPromisfyFnWithCatch
- createPromisfyFnWithCatch(options: PromisfyFnWithCatchOptions) - hack一个promisfy函数的Pending、Resolve、Reject过程，返回一个新promisfy函数。

#### 参数解析
```js
type PromisfyFn<T, U> = (...args: Array<T>) => Promise<U>

type PromisfyFnWithCatchOptions = {
  executePromiseFn: PromisfyFn<any, any>,
  onPending?: (callArgs: Array<any>) => void
  onResolve?: (response: any) => void
  onReject?: (error: Error) => void
}
``` 

#### Demo
```js
// __tests__/func.test.ts
const mockFn = createMockFn(() => Promise.resolve({b: 'bar'}))
const mockCall = { a: 'foo' }

const fn = createPromisfyFnWithCatch({
  executePromiseFn: mockFn,
  onPending(args: any) {
    expect(args[0]).toBe(mockCall)
  },
  onResolve(res: any) {
    expect(res).toMatchObject({b: 'bar'})
  }
})

fn(mockCall).then((res) => {
  expect(res).toBeTruthy()
  expect(mockFn.mock.calls.length).toBe(1)
  done()
})
```

### once

- once(fn) - 返回一个cache函数，缓存fn首次调用的结果，fn只会执行一次。

```js
// __tests__/func.test.ts
const mockFn = createMockFn(() => true)
const mockFnOnce = once(mockFn)

const result1 = mockFnOnce()
const result2 = mockFnOnce()

expect(mockFn.mock.calls.length).toBe(1)
expect(result1 === result2).toBe(true)
```

### createPolling

- createPolling(options: PollingOptions) - 返回一个轮询函数

#### 参数解析
```js
type PollingOptions = {
  intervalTime: number // 轮询周期。
  fn: (...args:  Array<any>) => boolean // 执行函数，返回一个是否继续轮询的标记，true代表继续，false代表结束。
  immediately?: boolean // 默认是true, 轮询函数调用就马上执行fn；false，要等到intervalTime时间到达时执行fn。
  onEnd?: NormalFn // 轮询结束时调用。
}
```

#### Demo
```js
// __tests__/func.test.ts
let callCount = 0
const mockFn = createMockFn((res: any) => {
  expect(res).toBe(1)
  callCount += 1
  return callCount > 1 ? false : true
})

const pollingFn = createPolling({
  intervalTime: 500,
  fn: mockFn,
  onEnd() {
    expect(callCount).toBe(2)
    done()
  }
})

pollingFn(1)
```

### throttle
- throttle(delay: number, fn: NormalFn) - 返回一个防抖函数， delay代表延时触发的时间，fn代表执行函数

#### Demo
```js
// __tests__/func.test.ts
const mockFn = createMockFn((res:any) => {
  expect(res).toBe(1)
  expect(mockFn.mock.calls.length).toBe(1)
  done()
})
const throttleFn =  throttle(1000, mockFn)

throttleFn(1)
throttleFn(1)
```

### memory

- memory(fn) - 返回一个缓存每次调用的结果并输出结果的函数，fn代表纯函数，入参成员必须是 number | string, 输出可以是any。

#### Demo
```js
// __tests__/func.test.ts
const mockFn = createMockFn((str: string, num: number) => {
    return str + num
  })
  const cacheFn = memory<[string, number], string>(mockFn)
  const result1 = cacheFn('alex', 1)
  const result2 = cacheFn('alex', 1)
  expect(result1 === result2).toBe(true)
  expect(mockFn.mock.calls.length).toBe(1)
})
```

## Promise风格函数

### promiseTimeout

- promiseTimeout(ms: number, promise: Promise<any>) - 返回一个promise，超时会reject一个超时字符串。

#### Demo

```js
it('promiseTimeout pass', (done) => {
  const passPromise = createDelayPromise(200);
  promiseTimeout(500, passPromise).then((res) => {
    expect(res).toBe(true)
    done()
  }).catch(done)
})

 it('promiseTimeout timeout', (done) => {
    const failPromise = createDelayPromise(200);
    const timeout = 100
    promiseTimeout(timeout, failPromise).catch((err) => {
      expect(err).toMatch(`Timed out in ${timeout}ms.`)
      done()
    })
  })
```

### promiseRetry

- promiseTimeout(maxExeCount: number, interval: number,
  cb: RetryHandleCallback) - 返回一个promise，超过调用数会reject一个错误。

#### 参数解析
```js
type RetryResult = { 
  isDone: boolean
  payload: any
}

type RetryHandleCallback = (currExeCount: number) => Promise<RetryResult>
```

### Demo

```js
 it('retry resolve', (done) => {
    const fn = jest.fn(async (count) => {
      return {
        isDone: count === 3 ? true : false,
        payload: { foo: 'bar' }
      }
    })

    const promise = promiseRetry(3, 100, fn)
    promise.then((payload) => {
      expect(fn).toBeCalledTimes(3)
      expect(payload).toMatchObject({ foo: 'bar' })
      done()
    }).catch(done)
  })
```
