Skip to content

最佳实践与 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: 页面加载完成
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原因
搜索输入$debounce1300-500ms等待用户停止输入
窗口 resize$debounce1150-200ms交互结束再重排
滚动事件$throttle2100-200ms首次立即响应,保证 UI 流畅度
按钮防重复$throttle2800-1000ms首次点击立即响应,后续忽略
鼠标移动$throttle250-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分钟(小写=分)
HH24小时制(大写=时)hh12小时制(小写=时)
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 }  ← 对象递归合并
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 传递的数据可能被任意监听器读取,避免传输敏感信息