@shivip/common-util 工具参考
@shivip/common-util 是公司内部与 UI、Vue 和运行平台无关的 TypeScript 基础工具包。它由 @shivip/mp-core 转发,因此所有公开成员均可直接从 @hfyidu/api/core 使用。
import { Option, joinPath, validMobile } from '@hfyidu/api/core'URL 与路径工具
ApiPathBuilder
以服务路径为基础拼接一个接口片段:
import { ApiPathBuilder } from '@hfyidu/api/core'
const users = ApiPathBuilder.create('/user')
users.resolve() // '/user'
users.resolve('login') // '/user/login'它只拼接单个片段,不会规范化重复斜杠;需要规范化多个路径时使用 joinPath。
joinPath(...paths)
合并路径、压缩重复 /、去掉结尾 /,并确保结果以 / 开头。
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)
把对象编码为查询字符串:
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 追加查询参数:
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) | 是否符合中国大陆手机号格式,可带 +86 或 0086 |
validObject(value) | 是否为普通对象 |
nonNullable(value) | 是否既不是 null 也不是 undefined |
这些函数大多是 TypeScript 类型守卫:
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') // truevalidAsyncFunction
它只识别声明为 async 的函数。普通函数即使返回 Promise,也不会被判定为异步函数。
可选值与集合工具
Option
提供空值包装和常见集合操作。
Option.of(value)
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)
返回不包含指定字段的新对象:
const safeUser = Option.omit(
{ id: 1, nickname: 'Andy', token: 'secret' },
['token'],
)
// { id: 1, nickname: 'Andy' }Option.chunk(array, size)
按指定数量切分数组:
Option.chunk([1, 2, 3, 4, 5], 2)
// [[1, 2], [3, 4], [5]]Option.groupBy(array, key)
按对象属性分组:
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)
递归复制由普通对象、数组和基础类型组成的数据:
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'适用范围
这是“简单”深拷贝,不保留类实例原型,也不专门处理 Date、Map、Set、循环引用、函数或不可枚举属性。复杂数据请使用针对性的序列化或克隆方案。
属性描述符工具
这些函数用于配合 Object.defineProperty 或 Object.defineProperties:
| 函数 | 生成的描述符 |
|---|---|
readonlyDescriptor(value) | 只提供 getter 的只读属性 |
notWriteDescriptor(value) | writable: false |
writableDescriptor(value) | writable: true |
getSetDescriptor(get, set) | 自定义 getter 和 setter |
notEnumerateDescriptor(value) | enumerable: false |
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 不会出现调用方如需精确控制 configurable、enumerable 等选项,应自行构造完整的 PropertyDescriptor。
错误处理
ErrorManager.format(error)
统一提取错误文本,优先级为:
- 错误本身是字符串
error.messageerror.errMsg- 默认文本
发生未知错误~
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
检查对象必填字段是否为 null 或 undefined:
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注意:0、false 和空字符串不会被视为缺失。
DDD 与请求基类
BaseUseCase<Params, Response>
定义统一的用例执行协议。子类实现 onRun,调用方使用 run,返回结果包含数据和完成时间:
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) // 完成时的毫秒时间戳相关类型:
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) | 声明按映射条件进行转换 |
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 转换;完整的 fromJson、toList 转换函数由公司内部 DTO 代码生成流程产生。手写 DTO 时不要假设仅添加装饰器就会自动转换字段。
BaseEvent<EventArgs>
简单的订阅、广播和释放机制:
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() // 清空全部订阅者对应类型为 EventArgs 和 EventHandler<E>。
BaseHttpApi
抽象 HTTP 客户端,要求实现 baseUrl 和 request。受保护的 pathBuilder 会处理绝对地址、相对地址和查询参数。
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 },
})公开请求类型:
HttpRequestMethod:GET、POST、PUT、DELETEHttpRequestOption<T>:url、method、header、query、body、formatterHttpResponse<T>:当前为响应类型别名
BaseRepository
保存 HTTP 客户端和服务名前缀,并向子类提供 pathBuilder:
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 领域入口的空抽象基类,用于统一领域类型和语义:
import { BaseDomain } from '@hfyidu/api/core'
class UserDomain extends BaseDomain {
static readonly cases = {
// login: new LoginUseCase(),
}
}BaseRouteModule
统一页面路由声明,getFullPages() 返回冻结后的只读路由表:
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,以及可选的 code、name。
通用 TypeScript 类型
Nullable<T>
import type { Nullable } from '@hfyidu/api/core'
let userId: Nullable<number> = null
userId = 1等价于 T | null,不包含 undefined。
Optional<T, K>
把指定字段变为可选:
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 限制:
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 或属性描述符施加的只读限制。