最佳实践

最佳实践是前人经验的总结,遵循这些实践可以帮助你写出更高质量、更易维护的代码。

代码组织

模块化原则

// 单一职责原则
// 错误:一个模块做太多事
class User {
  constructor() {}
  validate() {}
  save() {}
  sendEmail() {}
  generateReport() {}
}

// 正确:职责分离
class User {
  constructor(data) {
    this.data = data
  }
}

class UserValidator {
  validate(user) {
    // 验证逻辑
  }
}

class UserRepository {
  save(user) {
    // 存储逻辑
  }
}

class EmailService {
  send(user, content) {
    // 发送邮件
  }
}

目录结构

src/
├── api/              # API 接口
│   ├── index.js
│   └── modules/
├── components/       # 组件
│   ├── common/
│   └── business/
├── hooks/            # 自定义 Hooks
├── utils/            # 工具函数
├── constants/        # 常量
├── types/            # 类型定义
├── styles/           # 样式
├── assets/           # 静态资源
└── pages/            # 页面

模块导出

// utils/index.js - 统一导出
export { formatDate } from './formatDate'
export { debounce } from './debounce'
export { throttle } from './throttle'
export { deepClone } from './deepClone'

// 使用
import { formatDate, debounce } from '@/utils'

// 默认导出 vs 命名导出
// 命名导出:适合工具函数
export function add(a, b) { return a + b }

// 默认导出:适合模块主要功能
export default class UserService {}

错误处理

异步错误处理

// 错误:未捕获的 Promise 错误
async function fetchData() {
  const response = await fetch('/api/data')
  return response.json()
}

// 正确:try/catch 处理
async function fetchData() {
  try {
    const response = await fetch('/api/data')
    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`)
    }
    return response.json()
  } catch (error) {
    console.error('获取数据失败:', error)
    throw error
  }
}

// 统一错误处理函数
async function withErrorHandling(fn, fallback = null) {
  try {
    return await fn()
  } catch (error) {
    console.error('操作失败:', error)
    return fallback
  }
}

const data = await withErrorHandling(() => fetchData(), [])

错误边界

// React 错误边界
class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props)
    this.state = { hasError: false, error: null }
  }

  static getDerivedStateFromError(error) {
    return { hasError: true, error }
  }

  componentDidCatch(error, errorInfo) {
    console.error('错误边界捕获:', error, errorInfo)
  }

  render() {
    if (this.state.hasError) {
      return <ErrorFallback error={this.state.error} />
    }
    return this.props.children
  }
}

// 使用
<ErrorBoundary>
  <App />
</ErrorBoundary>

自定义错误类

class AppError extends Error {
  constructor(message, code, statusCode = 500) {
    super(message)
    this.name = 'AppError'
    this.code = code
    this.statusCode = statusCode
    this.timestamp = new Date().toISOString()
  }
}

class ValidationError extends AppError {
  constructor(message, errors = []) {
    super(message, 'VALIDATION_ERROR', 400)
    this.name = 'ValidationError'
    this.errors = errors
  }
}

class NotFoundError extends AppError {
  constructor(resource) {
    super(`${resource} 未找到`, 'NOT_FOUND', 404)
    this.name = 'NotFoundError'
    this.resource = resource
  }
}

// 使用
throw new ValidationError('输入验证失败', [
  { field: 'email', message: '邮箱格式不正确' }
])

性能优化

防抖与节流

// 防抖:延迟执行,重复调用重置计时
function debounce(fn, delay) {
  let timer = null
  return function (...args) {
    clearTimeout(timer)
    timer = setTimeout(() => fn.apply(this, args), delay)
  }
}

// 使用
const handleSearch = debounce((query) => {
  fetchSearchResults(query)
}, 300)

input.addEventListener('input', (e) => handleSearch(e.target.value))

// 节流:固定间隔执行
function throttle(fn, interval) {
  let lastTime = 0
  return function (...args) {
    const now = Date.now()
    if (now - lastTime >= interval) {
      lastTime = now
      fn.apply(this, args)
    }
  }
}

// 使用
const handleScroll = throttle(() => {
  updateScrollPosition()
}, 100)

window.addEventListener('scroll', handleScroll)

懒加载

// 图片懒加载
const lazyImages = document.querySelectorAll('img[data-src]')

const imageObserver = new IntersectionObserver((entries) => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      const img = entry.target
      img.src = img.dataset.src
      img.removeAttribute('data-src')
      imageObserver.unobserve(img)
    }
  })
})

lazyImages.forEach(img => imageObserver.observe(img))

// 组件懒加载(React)
const LazyComponent = React.lazy(() => import('./HeavyComponent'))

function App() {
  return (
    <React.Suspense fallback={<Loading />}>
      <LazyComponent />
    </React.Suspense>
  )
}

// 路由懒加载
const routes = [
  {
    path: '/dashboard',
    component: () => import('./views/Dashboard.vue')
  }
]

虚拟列表

function VirtualList({ items, itemHeight, containerHeight }) {
  const [scrollTop, setScrollTop] = useState(0)
  
  const startIndex = Math.floor(scrollTop / itemHeight)
  const endIndex = Math.min(
    startIndex + Math.ceil(containerHeight / itemHeight) + 1,
    items.length
  )
  
  const visibleItems = items.slice(startIndex, endIndex)
  const offsetY = startIndex * itemHeight
  
  return (
    <div 
      style={{ height: containerHeight, overflow: 'auto' }}
      onScroll={e => setScrollTop(e.target.scrollTop)}
    >
      <div style={{ height: items.length * itemHeight, position: 'relative' }}>
        <div style={{ transform: `translateY(${offsetY}px)` }}>
          {visibleItems.map((item, index) => (
            <div key={startIndex + index} style={{ height: itemHeight }}>
              {item.content}
            </div>
          ))}
        </div>
      </div>
    </div>
  )
}

缓存策略

// 简单内存缓存
const cache = new Map()

async function fetchWithCache(key, fetcher, ttl = 60000) {
  const cached = cache.get(key)
  
  if (cached && Date.now() - cached.timestamp < ttl) {
    return cached.data
  }
  
  const data = await fetcher()
  cache.set(key, { data, timestamp: Date.now() })
  return data
}

// 使用
const users = await fetchWithCache('users', () => fetchUsers())

// 带过期时间的缓存类
class Cache {
  constructor() {
    this.store = new Map()
  }
  
  set(key, value, ttl) {
    this.store.set(key, {
      value,
      expiry: Date.now() + ttl
    })
  }
  
  get(key) {
    const item = this.store.get(key)
    if (!item) return null
    
    if (Date.now() > item.expiry) {
      this.store.delete(key)
      return null
    }
    
    return item.value
  }
  
  has(key) {
    return this.get(key) !== null
  }
  
  delete(key) {
    this.store.delete(key)
  }
  
  clear() {
    this.store.clear()
  }
}

内存管理

避免内存泄漏

// 1. 清除定时器
useEffect(() => {
  const timer = setInterval(() => {
    // do something
  }, 1000)
  
  return () => clearInterval(timer)
}, [])

// 2. 清除事件监听
useEffect(() => {
  const handleResize = () => {
    // do something
  }
  
  window.addEventListener('resize', handleResize)
  
  return () => window.removeEventListener('resize', handleResize)
}, [])

// 3. 清除观察器
useEffect(() => {
  const observer = new IntersectionObserver(() => {})
  
  return () => observer.disconnect()
}, [])

// 4. 清除 WebSocket
useEffect(() => {
  const ws = new WebSocket('wss://example.com')
  
  return () => ws.close()
}, [])

// 5. 避免闭包陷阱
function createHandlers() {
  const handlers = []
  
  for (let i = 0; i < 10; i++) {
    handlers.push(() => console.log(i))  // 正确:使用 let
  }
  
  return handlers
}

对象池

class ObjectPool {
  constructor(createFn, resetFn, initialSize = 10) {
    this.createFn = createFn
    this.resetFn = resetFn
    this.pool = []
    
    for (let i = 0; i < initialSize; i++) {
      this.pool.push(this.createFn())
    }
  }
  
  acquire() {
    return this.pool.length > 0 
      ? this.pool.pop() 
      : this.createFn()
  }
  
  release(obj) {
    this.resetFn(obj)
    this.pool.push(obj)
  }
}

// 使用
const vectorPool = new ObjectPool(
  () => ({ x: 0, y: 0 }),
  (v) => { v.x = 0; v.y = 0 }
)

const v = vectorPool.acquire()
v.x = 10
v.y = 20
// 使用完后释放
vectorPool.release(v)

代码复用

组合优于继承

// 继承方式(不推荐)
class Animal {
  move() {}
}

class Dog extends Animal {
  bark() {}
}

class Fish extends Animal {
  swim() {}
}

// 组合方式(推荐)
const canMove = {
  move() {
    console.log('移动')
  }
}

const canBark = {
  bark() {
    console.log('汪汪')
  }
}

const canSwim = {
  swim() {
    console.log('游泳')
  }
}

function createDog() {
  return Object.assign({}, canMove, canBark)
}

function createFish() {
  return Object.assign({}, canMove, canSwim)
}

自定义 Hooks

// 数据获取 Hook
function useFetch(url) {
  const [data, setData] = useState(null)
  const [loading, setLoading] = useState(true)
  const [error, setError] = useState(null)
  
  useEffect(() => {
    let cancelled = false
    
    async function fetchData() {
      try {
        const response = await fetch(url)
        const json = await response.json()
        
        if (!cancelled) {
          setData(json)
        }
      } catch (err) {
        if (!cancelled) {
          setError(err)
        }
      } finally {
        if (!cancelled) {
          setLoading(false)
        }
      }
    }
    
    fetchData()
    
    return () => { cancelled = true }
  }, [url])
  
  return { data, loading, error }
}

// 使用
function UserProfile({ userId }) {
  const { data: user, loading, error } = useFetch(`/api/users/${userId}`)
  
  if (loading) return <Loading />
  if (error) return <Error error={error} />
  
  return <div>{user.name}</div>
}

// 本地存储 Hook
function useLocalStorage(key, initialValue) {
  const [value, setValue] = useState(() => {
    const stored = localStorage.getItem(key)
    return stored ? JSON.parse(stored) : initialValue
  })
  
  useEffect(() => {
    localStorage.setItem(key, JSON.stringify(value))
  }, [key, value])
  
  return [value, setValue]
}

// 使用
const [theme, setTheme] = useLocalStorage('theme', 'light')

可访问性

// 语义化 HTML
// 错误
<div onClick={handleClick}>点击</div>

// 正确
<button onClick={handleClick}>点击</button>

// 图片替代文本
<img src="chart.png" alt="2024年销售数据图表" />

// 表单标签
<label htmlFor="email">邮箱</label>
<input id="email" type="email" />

// ARIA 属性
<button 
  aria-label="关闭对话框"
  aria-expanded={isOpen}
  aria-controls="dialog-content"
>
  <CloseIcon />
</button>

// 键盘导航
<div 
  role="button"
  tabIndex={0}
  onKeyDown={(e) => {
    if (e.key === 'Enter' || e.key === ' ') {
      handleClick()
    }
  }}
>
  可聚焦元素
</div>

// 焦点管理
function Modal({ isOpen, onClose }) {
  const modalRef = useRef()
  
  useEffect(() => {
    if (isOpen) {
      modalRef.current?.focus()
    }
  }, [isOpen])
  
  return (
    <div 
      ref={modalRef}
      role="dialog"
      aria-modal="true"
      tabIndex={-1}
    >
      {/* 内容 */}
    </div>
  )
}

文档与注释

/**
 * 格式化日期
 * @param {Date|string|number} date - 日期对象、字符串或时间戳
 * @param {string} format - 格式化模板,默认 'YYYY-MM-DD'
 * @returns {string} 格式化后的日期字符串
 * @example
 * formatDate(new Date(), 'YYYY年MM月DD日')
 * // 返回 '2024年01月15日'
 */
function formatDate(date, format = 'YYYY-MM-DD') {
  // 实现
}

/**
 * @typedef {Object} User
 * @property {number} id - 用户ID
 * @property {string} name - 用户名
 * @property {string} email - 邮箱
 */

/**
 * 获取用户信息
 * @param {number} userId - 用户ID
 * @returns {Promise<User>} 用户信息
 */
async function getUser(userId) {
  // 实现
}

最佳实践清单

代码质量

  • 遵循单一职责原则
  • 函数名清晰表达意图
  • 避免过深的嵌套
  • 避免魔法数字,使用常量
  • 及时删除无用代码

性能

  • 合理使用防抖节流
  • 大列表使用虚拟滚动
  • 图片懒加载
  • 合理使用缓存
  • 避免不必要的重渲染

安全

  • 验证所有用户输入
  • 转义输出内容
  • 使用 HTTPS
  • 不在前端存储敏感数据
  • 及时清理资源

可维护性

  • 编写清晰的注释
  • 保持代码风格一致
  • 编写单元测试
  • 合理的目录结构
  • 完善的错误处理

下一步

掌握最佳实践后,你可以继续学习:


东巴文(db-w.cn)—— 让代码更专业