最佳实践是前人经验的总结,遵循这些实践可以帮助你写出更高质量、更易维护的代码。
// 单一职责原则
// 错误:一个模块做太多事
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)
}
// 数据获取 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) {
// 实现
}
掌握最佳实践后,你可以继续学习:
东巴文(db-w.cn)—— 让代码更专业