最佳实践与 FAQ
本文汇总 @jiess/utils 三大模块在实际项目中的推荐用法、性能优化建议、常见踩坑及迁移指南。
一、跨模块组合模式
模式 1:环境感知 + 按需加载
typescript
import { getDeviceType, getOSInfo } from '@jiess/utils/helper'
import { $canUseDom } from '@jiess/utils'
if ($canUseDom) {
const ua = navigator.userAgent
const device = getDeviceType(ua)
const os = getOSInfo(ua)
if (device === 'Mobile') {
import('./mobile-bundle')
} else {
import('./desktop-bundle')
}
if (os === 'iOS') {
document.documentElement.classList.add('ios-env')
}
}模式 2:类型守卫 + 路径安全读取
typescript
import { $check, $getByPath } from '@jiess/utils'
function processApiResponse(raw: unknown) {
if (!$check(raw, 'object', false)) {
console.warn('响应格式异常')
return null
}
// TypeScript 自动将 raw 收窄为 object
const userId = $getByPath(raw, 'data.user.id')
return userId ?? 'anonymous'
}模式 3:Midder + 日期格式化
typescript
import Midder, { $dateFormat } from '@jiess/utils'
const mockJiess = {} as any
const logBus = new Midder(mockJiess, 'appLog', 'multiple')
logBus.addListener('action', (payload) => {
console.log(`[${$dateFormat(new Date(), 'HH:mm:ss')}] ${payload.type}: ${payload.msg}`)
})
logBus.dispatch('action', { type: 'INFO', msg: '页面加载完成' })
// [14:30:05] INFO: 页面加载完成模式 4:Cookie + 网络工具
typescript
import { $getCookie, $setCookie } from '@jiess/utils'
import { getHostname } from '@jiess/utils/helper'
function initAuthToken() {
const existing = $getCookie('jwt_token')
if (existing) return existing
const host = getHostname(window.location.href)
const token = fetchTokenFromServer(host)
$setCookie('jwt_token', token, 7)
return token
}模式 5:防抖 + 节流组合
typescript
import { $debounce1, $throttle1 } from '@jiess/utils'
// 搜索输入:防抖(等用户停止输入再查询)
const onSearchInput = (val: string) => {
$debounce1(() => fetch(`/api/search?q=${val}`), 300)
}
// 列表滚动:节流(固定频率更新位置指示器)
window.addEventListener('scroll', () => {
$throttle1(() => updateScrollIndicator(window.scrollY), 100)
})二、性能优化
2.1 环境信息只检测一次
环境信息在整个应用生命周期内不变,应缓存而非重复调用。
typescript
// ❌ 不推荐:每次渲染都检测
function Component() {
const device = getDeviceType(navigator.userAgent)
return <Layout mode={device === 'Mobile' ? 'compact' : 'wide'} />
}
// ✅ 推荐:模块顶层缓存
const deviceType = getDeviceType(navigator.userAgent)
function Component() {
return <Layout mode={deviceType === 'Mobile' ? 'compact' : 'wide'} />
}2.2 深拷贝前评估必要性
$clone 和 $copy 是递归深拷贝,对大型对象有性能开销。仅在确实需要独立副本时使用。
typescript
// ❌ 不推荐:传递引用即可的场景做了深拷贝
function display(data: Data) {
const copied = $clone(data) // 不需要
render(copied)
}
// ✅ 推荐:仅在需要修改且不影响原对象时深拷贝
function transformAndSave(data: Data) {
const working = $clone(data)
working.timestamp = Date.now()
saveToDB(working) // data 不受影响
}2.3 防抖/节流的 wait 时间选择
| 场景 | 推荐函数 | 推荐 wait | 原因 |
|---|---|---|---|
| 搜索输入 | $debounce1 | 300-500ms | 等待用户停止输入 |
| 窗口 resize | $debounce1 | 150-200ms | 交互结束再重排 |
| 滚动事件 | $throttle2 | 100-200ms | 首次立即响应,保证 UI 流畅度 |
| 按钮防重复 | $throttle2 | 800-1000ms | 首次点击立即响应,后续忽略 |
| 鼠标移动 | $throttle2 | 50-100ms | 平滑跟手 |
2.4 Midder 防抖开关
Midder 默认 isDebounce: true,同一事件循环内的重复 dispatch 会自动合并。对于高频场景(如窗口 resize、滚动事件),建议保持默认。
typescript
// ✅ 默认防抖,高频场景自动合并
const bus = new Midder(mockJiess, 'scrollBus', 'multiple', true)
// ⚠️ 仅在需要每次 dispatch 都执行时关闭
const bus = new Midder(mockJiess, 'preciseBus', 'multiple', false)三、注意事项与边界处理
3.1 $dateFormat 格式码大小写敏感性
| 格式码 | 含义 | 格式码 | 含义 |
|---|---|---|---|
MM | 月份(大写=月) | mm | 分钟(小写=分) |
HH | 24小时制(大写=时) | hh | 12小时制(小写=时) |
typescript
const d = new Date('2023-06-15 14:05:03')
$dateFormat(d, 'yyyy-MM-dd HH:mm:ss') // '2023-06-15 14:05:03' ✅
$dateFormat(d, 'yyyy-mm-dd hh:MM:ss') // '2023-05-15 02:06:03' ❌ 混淆了大小写3.2 $querystring.parse 同名 key 自动聚合为数组
typescript
$querystring.parse('a=1&a=2&a=3')
// { a: ['1', '2', '3'] } —— 同名 key 自动聚合成数组
$querystring.parse('a=1&b=2')
// { a: '1', b: '2' } —— 单值保持字符串3.3 $check 类型守卫的使用时机
typescript
// ✅ 直接在条件语句中使用,TypeScript 能正确收窄类型
if ($check(val, 'string')) {
val.toUpperCase() // TypeScript 推断为 string
}
// ❌ 提前赋值后类型收窄失效
const ok = $check(val, 'string')
if (ok) {
val.toUpperCase() // ❌ TypeScript 仍认为 val 是 unknown!
}
// ✅ state=false 不影响类型守卫语义,TypeScript 仍能收窄
if ($check(val, 'string', false)) {
val.toUpperCase() // TypeScript 推断为 string
}3.4 $diff 的深度语义
typescript
$diff(NaN, NaN) // true(特殊处理)
// Promise 对象始终返回 false(不同的 Promise 实例)
$diff(Promise.resolve(1), Promise.resolve(1)) // false
// 带 isNotClone 标记的属性被跳过
// 这是 Jiess 内部机制,业务代码无需关注3.5 $deepMerge 不合并数组
typescript
const target = { list: [1, 2, 3], meta: { page: 1 } }
$deepMerge(target, { list: [4, 5], meta: { total: 100 } })
// target.list === [4, 5] ← 数组直接替换,不是 [1,2,3,4,5]
// target.meta === { page: 1, total: 100 } ← 对象递归合并3.6 Cookie 操作的环境要求
typescript
import { $getCookie, $setCookie, $canUseDom } from '@jiess/utils'
// $getCookie/$setCookie 内部已做 DOM 检测
// 非浏览器环境:$getCookie 返回 null,$setCookie 静默忽略
// 但显式检查可以避免不必要的调用
function safeGetToken() {
if (!$canUseDom) return null
return $getCookie('auth_token')
}3.7 非加密安全的随机数
typescript
// ❌ 不推荐:用于安全 token 生成
const apiToken = $getUUID(32)
// ✅ $getUUID 仅适用于非安全场景
const tempId = $getUUID(8) // 临时 DOM id
const requestId = $getUUID(16) // 日志追踪 id
// 需要加密安全时,请使用 crypto.randomUUID()
const secureToken = crypto.randomUUID()3.8 customFuction / customAsyncFuction 安全边界
typescript
import { customFuction } from '@jiess/utils/helper'
// ❌ 绝对禁止:执行用户输入
const userInput = req.body.code // 来自 HTTP 请求
const fn = customFuction(userInput) // 严重安全风险!
// ✅ 仅在受控的脚本引擎中使用
const TRUSTED_SCRIPTS = new Set(['rule1', 'rule2'])
if (TRUSTED_SCRIPTS.has(scriptName)) {
const fn = customFuction(loadTrustedScript(scriptName))
}3.9 Midder 监听器异常不会中断执行
typescript
const bus = new Midder(mockJiess, 'chain', 'multiple', false)
bus.addListener('step', (data) => {
if (!data.valid) throw new Error('验证失败')
return { ...data, step1: true }
})
bus.addListener('step', (data) => {
// ⚠️ 前一个监听器抛异常时,仍会执行(异常被 catch 并 console.error)
return { ...data, step2: true }
})
bus.dispatch('step', { defaultValue: { valid: false } }).then(result => {
// 结果为 undefined(异常中断了链式传递)
})四、迁移指南
从旧文档迁移
若你的项目是基于旧版 @jiess/utils 文档开发的,以下是最关键的变更:
| 变更项 | 旧用法 | 新用法 | 影响 |
|---|---|---|---|
| Midder 注册监听 | bus.addListener(...) | 不变 | — |
| Midder 触发广播 | bus.dispatch(...) | 不变 | — |
| Midder 移除监听 | bus.clean(...) | 不变 | — |
| Midder 构造函数 | — | new Midder(jiess, ref, model, isDebounce?) | 参数结构说明 |
$check 参数名 | $check(val, type, b) | $check(val, type, state?) | 参数命名规范化 |
$setCookie 参数名 | $setCookie(n, v, days) | $setCookie(n, v, Days?) | 首字母大小写 |
| 文档文件名 | 介绍&上手.md 等 | 快速开始.md 等 | 文件名更新 |
从 lodash/underscore 迁移
| lodash / 原生 | @jiess/utils 等价 | 差异说明 |
|---|---|---|
_.isEqual(a, b) | $diff(a, b) | $diff 额外支持循环引用 |
_.cloneDeep(obj) | $clone(obj) | $clone 支持自定义处理回调 |
_.get(obj, path) | $getByPath(obj, path) | 语法相近,支持数组路径 |
_.set(obj, path, val) | $setByPath(obj, path, val) | 自动创建中间路径 |
_.debounce(fn, wait) | $debounce1(fn, wait) | 行为一致 |
_.throttle(fn, wait) | $throttle1(fn, wait) | 行为一致 |
new EventEmitter() | new Midder(...) | Midder 额外支持链式传递、防抖、响应式 |
五、常见问题
Q1: $check 和 $identify 有什么区别?
没有区别。 $identify 就是 $check 的别名(export const $identify = $check),两者可以互换使用。
Q2: $debounce1 和 $debounce2 的选择?
- 两者均为延迟执行型防抖,行为基本一致
$debounce1对应的工厂$baseDebounce1支持immediate参数,可改为首次立即执行模式$debounce2纯延迟执行,适合搜索输入、表单验证等场景- 按钮防重复推荐
$throttle2(首次立即执行,后续忽略)
Q2+: $throttle1 和 $throttle2 的选择?
- 滚动、鼠标移动 →
$throttle2(时间戳模式,首次立即执行) - 需要首次延迟的场景 →
$throttle1(计时器模式,首次等待wait)
Q3: $clone 和 $copy 的区别?
$clone(target)→ 返回一个新对象$copy(newObj, oldObj)→ 将旧对象的属性拷贝到已有新对象中(就地修改)
typescript
const a = $clone(source) // a 是全新对象
const b = {}
$copy(b, source) // b 被就地填充Q4: Midder 如何在 Vue/React 中正确清理?
关键原则:组件卸载时必须调用 clean 移除监听器,否则造成内存泄漏。
typescript
// React
useEffect(() => {
const handler = (data) => { /* ... */ }
bus.addListener('event', handler)
return () => bus.clean('event')
}, [])
// Vue 3
import { onMounted, onUnmounted } from 'vue'
const handler = (data) => { /* ... */ }
onMounted(() => bus.addListener('event', handler))
onUnmounted(() => bus.clean('event'))Q5: $getUUID 能用于生成密码或 Token 吗?
不能。 $getUUID 基于 Math.random(),不是加密安全的随机数生成器。密码或 JWT Token 请使用 crypto.randomUUID() 或服务端生成的加密随机值。
Q6: 非浏览器环境(SSR/Node.js)能用哪些函数?
除 $setCookie、$getCookie 等 DOM 相关函数外,大部分函数在 Node.js 环境可直接使用。环境检测函数(getBrowserInfo 等)只要传入正确的 userAgent 字符串即可。
Q7: $deepMerge 会合并数组吗?
不会。 数组被视为不可合并的原子类型,直接被源对象的数组替换。只有"纯粹对象"({})才会递归合并。
Q8: 如何为生产环境做 Tree-shaking?
typescript
// ✅ 按需导入,打包工具会自动 tree-shake
import { $clone, $diff } from '@jiess/utils'
// ❌ 避免全量导入(除非确实需要全部函数)
import * as JiessUtils from '@jiess/utils'六、安全清单
| 检查项 | 说明 |
|---|---|
customFuction 输入源 | 确保代码字符串来自受信源,禁止从用户输入直接构造 |
$getUUID 用途 | 仅用于非安全标识(临时 ID、日志追踪),不用于密码/Token |
| Cookie 敏感信息 | 避免在 Cookie 中明文存储密码或隐私数据 |
$getByPath 路径来源 | 路径字符串来自用户输入时,注意避免路径遍历攻击 |
| Midder 数据传输 | 通过 dispatch 传递的数据可能被任意监听器读取,避免传输敏感信息 |