- API 路由统一: 移除 /api/h5/,统一 /api/pms/ 入口 - 二进制改名: api-server -> pms-api(与服务名一致) - nginx: 合并 /api/ + /pms/api/ 为 /api/pms/ - 新增 docs/API.md 接口文档 - 后端: CabinetDetail 增加 rssi/pow_fail/address/soc 等字段 - Redis 管道批量读取仓体实时数据(SOC/电压/电流/功率) - DB: 所有 TIMESTAMP 列改为 DATETIME(修复类型不匹配) - 用户管理: 新增创建时间/修改时间列,状态改用 Switch - 角色管理/用户管理: 操作按钮改为图标 - PC 设备详情: 基本信息补全 RSSI/ICCID/电源/地址 - PC 卡片: 通道统计改用 Lucide 图标,颜色统一 - PC 二维码: canvas 渲染,含编号文字,支持右键下载 - H5: 按 uni-app 规范修正导航(navigateTo 替代 reLaunch) - H5: 详情页加返回导航条 - H5: 扫码支持摄像头(jsQR)+ ?id= 参数 - H5: 底部 tabBar 在所有页面常显 - H5: 首页统计改为 6 项(设备/通道/空闲/充电/充满/故障) - H5: 下拉刷新 / 上拉加载 - 产品首页: 添加业务联系 - 文档: 清理过时文件,补充缺失规范章节
5.7 KiB
5.7 KiB
title, impact, impactDescription, type, tags
| title | impact | impactDescription | type | tags | ||||||
|---|---|---|---|---|---|---|---|---|---|---|
| Avoid Hidden Side Effects in Composables | HIGH | Side effects hidden in composables make debugging difficult and create implicit coupling between components | best-practice |
|
Avoid Hidden Side Effects in Composables
Impact: HIGH - Composables should encapsulate stateful logic, not hide side effects that affect things outside their scope. Hidden side effects like modifying global state, using provide/inject internally, or manipulating the DOM directly make composables unpredictable and hard to debug.
When a composable has unexpected side effects, consumers can't reason about what calling it will do. This leads to bugs that are difficult to trace and composables that can't be safely reused.
Task Checklist
- Avoid using provide/inject inside composables (make dependencies explicit)
- Don't modify Pinia/Vuex store state internally (accept store as parameter instead)
- Don't manipulate DOM directly (use template refs passed as arguments)
- Document any unavoidable side effects clearly
- Keep composables focused on returning reactive state and methods
Incorrect:
// WRONG: Hidden provide/inject dependency
export function useTheme() {
// Consumer has no idea this depends on a provided theme
const theme = inject('theme') // What if nothing provides this?
const isDark = computed(() => theme?.mode === 'dark')
return { isDark }
}
// WRONG: Modifying global store internally
import { useUserStore } from '@/stores/user'
export function useLogin() {
const userStore = useUserStore()
async function login(credentials) {
const user = await api.login(credentials)
// Hidden side effect: modifying global state
userStore.setUser(user)
userStore.setToken(user.token)
// Consumer doesn't know the store was modified!
}
return { login }
}
// WRONG: Hidden DOM manipulation
export function useFocusTrap() {
onMounted(() => {
// Which element? Consumer has no control
document.querySelector('.modal')?.focus()
})
}
// WRONG: Hidden provide that affects descendants
export function useFormContext() {
const form = reactive({ values: {}, errors: {} })
// Components calling this have no idea it provides something
provide('form-context', form)
return form
}
Correct:
// CORRECT: Explicit dependency injection
export function useTheme(injectedTheme) {
// If no theme passed, consumer must handle it
const theme = injectedTheme ?? { mode: 'light' }
const isDark = computed(() => theme.mode === 'dark')
return { isDark }
}
// Usage - dependency is explicit
const theme = inject('theme', { mode: 'light' })
const { isDark } = useTheme(theme)
// CORRECT: Return actions, let consumer decide when to call them
export function useLogin() {
const user = ref(null)
const token = ref(null)
const isLoading = ref(false)
const error = ref(null)
async function login(credentials) {
isLoading.value = true
error.value = null
try {
const response = await api.login(credentials)
user.value = response.user
token.value = response.token
return response
} catch (e) {
error.value = e
throw e
} finally {
isLoading.value = false
}
}
return { user, token, isLoading, error, login }
}
// Consumer decides what to do with the result
const { user, token, login } = useLogin()
const userStore = useUserStore()
async function handleLogin(credentials) {
await login(credentials)
// Consumer explicitly updates the store
userStore.setUser(user.value)
userStore.setToken(token.value)
}
// CORRECT: Accept element as parameter
export function useFocusTrap(targetRef) {
onMounted(() => {
targetRef.value?.focus()
})
onUnmounted(() => {
// Cleanup focus trap
})
}
// Usage - consumer controls which element
const modalRef = ref(null)
useFocusTrap(modalRef)
// CORRECT: Separate composable from provider
export function useFormContext() {
const form = reactive({ values: {}, errors: {} })
return form
}
// In parent component - explicit provide
const form = useFormContext()
provide('form-context', form)
Acceptable Side Effects (With Documentation)
Some side effects are acceptable when they're the core purpose of the composable:
/**
* Tracks mouse position globally.
*
* SIDE EFFECTS:
* - Adds 'mousemove' event listener to window (cleaned up on unmount)
*
* @returns {Object} Mouse coordinates { x, y }
*/
export function useMouse() {
const x = ref(0)
const y = ref(0)
// This side effect is the whole point of the composable
// and is properly cleaned up
onMounted(() => window.addEventListener('mousemove', update))
onUnmounted(() => window.removeEventListener('mousemove', update))
function update(event) {
x.value = event.pageX
y.value = event.pageY
}
return { x, y }
}
Pattern: Dependency Injection for Flexibility
// Composable accepts its dependencies
export function useDataFetcher(apiClient, cache = null) {
const data = ref(null)
async function fetch(url) {
if (cache) {
const cached = cache.get(url)
if (cached) {
data.value = cached
return
}
}
data.value = await apiClient.get(url)
cache?.set(url, data.value)
}
return { data, fetch }
}
// Usage - dependencies are explicit and testable
const apiClient = inject('apiClient')
const cache = inject('cache', null)
const { data, fetch } = useDataFetcher(apiClient, cache)