Skip to content

@shivip/common-util 工具参考

@shivip/common-util 是公司内部与 UI、Vue 和运行平台无关的 TypeScript 基础工具包。它由 @shivip/mp-core 转发,因此所有公开成员均可直接从 @hfyidu/api/core 使用。

ts
import { Option, joinPath, validMobile } from '@hfyidu/api/core'

URL 与路径工具

ApiPathBuilder

以服务路径为基础拼接一个接口片段:

ts
import { ApiPathBuilder } from '@hfyidu/api/core'

const users = ApiPathBuilder.create('/user')

users.resolve()        // '/user'
users.resolve('login') // '/user/login'

它只拼接单个片段,不会规范化重复斜杠;需要规范化多个路径时使用 joinPath

joinPath(...paths)

合并路径、压缩重复 /、去掉结尾 /,并确保结果以 / 开头。

ts
import { joinPath } from '@hfyidu/api/core'

joinPath('api', 'user', 'profile')       // '/api/user/profile'
joinPath('/api/', '/user/', '/profile/') // '/api/user/profile'
joinPath()                               // ''

该函数面向 URL path,不应用于 Windows 文件系统路径。

toQueryString(params)

把对象编码为查询字符串:

ts
import { toQueryString } from '@hfyidu/api/core'

toQueryString({
  keyword: '短剧 推荐',
  page: 1,
  tags: ['热门', '免费'],
  empty: null,
  ignored: undefined,
})
// keyword=%E7%9F%AD%E5%89%A7%20%E6%8E%A8%E8%8D%90&page=1
// &tags=%5B%22%E7%83%AD%E9%97%A8%22%2C%22%E5%85%8D%E8%B4%B9%22%5D&empty=

规则:

  • key 和 value 都会执行 encodeURIComponent
  • 对象和数组先执行 JSON.stringify
  • null 输出为空值,例如 empty=
  • undefined 直接忽略
  • Date 不做 JSON 序列化,使用其字符串表达

urlWithQuery(url, params?)

安全地给已有 URL 追加查询参数:

ts
import { urlWithQuery } from '@hfyidu/api/core'

urlWithQuery('/api/list', { page: 2 })
// '/api/list?page=2'

urlWithQuery('/api/list?type=novel', { page: 2 })
// '/api/list?type=novel&page=2'

校验与类型收窄

函数判断规则
validString(value)是否为字符串
validNumber(value)是否为数字且不是 NaN
validBoolean(value)是否为布尔值
validArray(value, minLength = 0)是否为数组且长度达到下限
validFunction(value)是否为函数
validAsyncFunction(value)是否为原生 async function
validMobile(value)是否符合中国大陆手机号格式,可带 +860086
validObject(value)是否为普通对象
nonNullable(value)是否既不是 null 也不是 undefined

这些函数大多是 TypeScript 类型守卫:

ts
import {
  nonNullable,
  validArray,
  validMobile,
  validNumber,
  validObject,
} from '@hfyidu/api/core'

const input: unknown = 10
if (validNumber(input)) {
  console.log(input.toFixed(2))
}

const users = [{ id: 1 }, null, { id: 2 }].filter(nonNullable)

validArray(users, 1)            // true
validObject({ id: 1 })          // true
validObject(new Date())         // false
validMobile('13800138000')      // true
validMobile('+8613800138000')   // true

validAsyncFunction

它只识别声明为 async 的函数。普通函数即使返回 Promise,也不会被判定为异步函数。

可选值与集合工具

Option

提供空值包装和常见集合操作。

Option.of(value)

ts
import { Option } from '@hfyidu/api/core'

const nickname = Option.of<string>(null)

nickname.isSome()                         // false
nickname.isNone()                         // true
nickname.toNullable()                     // null
nickname.getOrElse(() => '匿名用户')       // '匿名用户'

Option.none.isNone()                      // true
Option.eqv(1, 1)                          // true,使用严格相等

Option.omit(object, keys)

返回不包含指定字段的新对象:

ts
const safeUser = Option.omit(
  { id: 1, nickname: 'Andy', token: 'secret' },
  ['token'],
)
// { id: 1, nickname: 'Andy' }

Option.chunk(array, size)

按指定数量切分数组:

ts
Option.chunk([1, 2, 3, 4, 5], 2)
// [[1, 2], [3, 4], [5]]

Option.groupBy(array, key)

按对象属性分组:

ts
const grouped = Option.groupBy(
  [
    { type: 'novel', id: 1 },
    { type: 'movie', id: 2 },
    { type: 'novel', id: 3 },
  ],
  'type',
)
// {
//   novel: [{ type: 'novel', id: 1 }, { type: 'novel', id: 3 }],
//   movie: [{ type: 'movie', id: 2 }]
// }

simpleDeepClone(value)

递归复制由普通对象、数组和基础类型组成的数据:

ts
import { simpleDeepClone } from '@hfyidu/api/core'

const source = {
  profile: { nickname: 'Andy' },
  tags: ['admin'],
}
const cloned = simpleDeepClone(source)

cloned.profile.nickname = 'New name'
console.log(source.profile.nickname) // 'Andy'

适用范围

这是“简单”深拷贝,不保留类实例原型,也不专门处理 DateMapSet、循环引用、函数或不可枚举属性。复杂数据请使用针对性的序列化或克隆方案。

属性描述符工具

这些函数用于配合 Object.definePropertyObject.defineProperties

函数生成的描述符
readonlyDescriptor(value)只提供 getter 的只读属性
notWriteDescriptor(value)writable: false
writableDescriptor(value)writable: true
getSetDescriptor(get, set)自定义 getter 和 setter
notEnumerateDescriptor(value)enumerable: false
ts
import {
  getSetDescriptor,
  notEnumerateDescriptor,
  readonlyDescriptor,
} from '@hfyidu/api/core'

const state: Record<string, any> = {}
let count = 0

Object.defineProperties(state, {
  appName: readonlyDescriptor('Novel App'),
  count: getSetDescriptor(
    () => count,
    value => {
      count = Math.max(0, value)
    },
  ),
  internalId: notEnumerateDescriptor('internal-001'),
})

state.count = -1
console.log(state.count)       // 0
console.log(Object.keys(state)) // internalId 不会出现

调用方如需精确控制 configurableenumerable 等选项,应自行构造完整的 PropertyDescriptor

错误处理

ErrorManager.format(error)

统一提取错误文本,优先级为:

  1. 错误本身是字符串
  2. error.message
  3. error.errMsg
  4. 默认文本 发生未知错误~
ts
import { ErrorManager } from '@hfyidu/api/core'

ErrorManager.format('网络异常')                    // '网络异常'
ErrorManager.format(new Error('请求失败'))         // '请求失败'
ErrorManager.format({ errMsg: 'uni.request:fail' }) // 'uni.request:fail'

ErrorManager.assert(name, json, ...keys)assert

检查对象必填字段是否为 nullundefined

ts
import { ErrorManager, assert } from '@hfyidu/api/core'

ErrorManager.assert('UserDto', { id: 1, nickname: 'Andy' }, 'id', 'nickname')

class UserDto {
  static assert = assert
}

UserDto.assert({ id: 1 }, 'id')
// UserDto.assert({}, 'id') 会抛出:
// Error: The UserDto key id cannot be null

注意:0false 和空字符串不会被视为缺失。

DDD 与请求基类

BaseUseCase<Params, Response>

定义统一的用例执行协议。子类实现 onRun,调用方使用 run,返回结果包含数据和完成时间:

ts
import { BaseUseCase } from '@hfyidu/api/core'

class GetGreetingUseCase extends BaseUseCase<
  { name: string },
  string
> {
  protected async onRun(params: { name: string }) {
    return `你好,${params.name}`
  }
}

const response = await new GetGreetingUseCase().run({ name: 'Andy' })
console.log(response.data)      // '你好,Andy'
console.log(response.updatedAt) // 完成时的毫秒时间戳

相关类型:

ts
type UseCaseParams = Record<string, any> | null

interface UseCaseResponse<T> {
  data: T | null
  updatedAt: number
}

BaseDto 与 DTO 装饰器

BaseDto 是生成式 DTO 的公共基类。装饰器用于给公司内部代码生成工具提供字段元数据:

装饰器功能
@Required()字段必填
@Default(value)字段缺失时使用默认值
@OriginalKey(key)映射后台原始字段名
@Freeze标记 DTO 实例不可变
@List标记 DTO 支持列表转换
@WhenList(key)声明按列表条件进行转换
@WhenMap(key)声明按映射条件进行转换
ts
import {
  BaseDto,
  Default,
  List,
  OriginalKey,
  Required,
} from '@hfyidu/api/core'

@List
class UserDto extends BaseDto {
  @Required()
  id!: number

  @OriginalKey('nick_name')
  @Default('')
  nickname!: string
}

生成式 DTO

这些装饰器在运行时本身不完成 JSON 转换;完整的 fromJsontoList 转换函数由公司内部 DTO 代码生成流程产生。手写 DTO 时不要假设仅添加装饰器就会自动转换字段。

BaseEvent<EventArgs>

简单的订阅、广播和释放机制:

ts
import { BaseEvent } from '@hfyidu/api/core'

interface LoginEvent {
  userId: number
}

class UserLoggedInEvent extends BaseEvent<LoginEvent> {}

const event = new UserLoggedInEvent()
const handler = ({ userId }: LoginEvent) => console.log('login:', userId)

event.subscribe(handler)
event.broadcast({ userId: 1 })
event.unsubscribe(handler)
event.dispose() // 清空全部订阅者

对应类型为 EventArgsEventHandler<E>

BaseHttpApi

抽象 HTTP 客户端,要求实现 baseUrlrequest。受保护的 pathBuilder 会处理绝对地址、相对地址和查询参数。

ts
import {
  BaseHttpApi,
  HttpRequestMethod,
  type HttpRequestOption,
} from '@hfyidu/api/core'

class UniHttpApi extends BaseHttpApi {
  get baseUrl() {
    return 'https://api.example.com'
  }

  async request<T>(option: HttpRequestOption<T>): Promise<T> {
    const url = this.pathBuilder(option.url, option.query)

    return new Promise((resolve, reject) => {
      uni.request({
        url,
        method: option.method,
        header: option.header,
        data: option.body,
        success: ({ data }) => {
          resolve(option.formatter ? option.formatter(data) : (data as T))
        },
        fail: reject,
      })
    })
  }
}

const client = new UniHttpApi()
await client.request({
  url: '/user/profile',
  method: HttpRequestMethod.GET,
  query: { id: 1 },
})

公开请求类型:

  • HttpRequestMethodGETPOSTPUTDELETE
  • HttpRequestOption<T>urlmethodheaderquerybodyformatter
  • HttpResponse<T>:当前为响应类型别名

BaseRepository

保存 HTTP 客户端和服务名前缀,并向子类提供 pathBuilder

ts
import {
  BaseHttpApi,
  BaseRepository,
  HttpRequestMethod,
} from '@hfyidu/api/core'

class UserRepository extends BaseRepository {
  constructor(httpApi: BaseHttpApi) {
    super(httpApi, 'user')
  }

  profile(id: number) {
    return this.httpApi.request({
      url: this.pathBuilder.resolve('profile'),
      method: HttpRequestMethod.GET,
      query: { id },
    })
  }
}

BaseDomain

DDD 领域入口的空抽象基类,用于统一领域类型和语义:

ts
import { BaseDomain } from '@hfyidu/api/core'

class UserDomain extends BaseDomain {
  static readonly cases = {
    // login: new LoginUseCase(),
  }
}

BaseRouteModule

统一页面路由声明,getFullPages() 返回冻结后的只读路由表:

ts
import { BaseRouteModule, type RouteConfig } from '@hfyidu/api/core'

class UserRoutes extends BaseRouteModule {
  get pages(): Record<string, RouteConfig> {
    return {
      profile: {
        path: '/pages/profile/index',
        name: '个人中心',
      },
    }
  }
}

const routes = new UserRoutes().getFullPages()

RouteConfig 包含 path,以及可选的 codename

通用 TypeScript 类型

Nullable<T>

ts
import type { Nullable } from '@hfyidu/api/core'

let userId: Nullable<number> = null
userId = 1

等价于 T | null,不包含 undefined

Optional<T, K>

把指定字段变为可选:

ts
import type { Optional } from '@hfyidu/api/core'

interface User {
  id: number
  nickname: string
}

type NewUser = Optional<User, 'id'>
const user: NewUser = { nickname: 'Andy' }

CanWrite<T>

递归移除对象属性的 readonly 限制:

ts
import type { CanWrite } from '@hfyidu/api/core'

interface ReadonlyConfig {
  readonly theme: {
    readonly color: string
  }
}

const config = {
  theme: { color: 'blue' },
} as CanWrite<ReadonlyConfig>

config.theme.color = 'green'

它只改变 TypeScript 类型,不会解除运行时通过 Object.freeze 或属性描述符施加的只读限制。

内部工具包 · 未开源授权,仅限公司内部授权团队使用