Skip to content

前端开发最佳实践

本指南汇总了现代前端开发中的最佳实践,帮助您构建高质量、可维护的前端应用。

项目结构

推荐的目录结构

src/
├── assets/              # 静态资源
│   ├── images/         # 图片资源
│   ├── icons/          # 图标
│   └── styles/         # 全局样式
├── components/          # 组件
│   ├── ui/             # 通用 UI 组件
│   ├── layout/         # 布局组件
│   └── business/       # 业务组件
├── composables/         # 组合式函数 (Vue)
├── hooks/              # 自定义 Hooks (React)
├── stores/             # 状态管理
├── utils/              # 工具函数
├── services/           # API 服务
├── types/              # TypeScript 类型定义
├── views/              # 页面组件
└── router/             # 路由配置

文件命名规范

bash
# 组件文件 - PascalCase
UserProfile.vue
UserProfile.tsx

# 工具函数 - camelCase
formatDate.ts
validateEmail.ts

# 常量文件 - UPPER_SNAKE_CASE
API_ENDPOINTS.ts
DEFAULT_CONFIG.ts

# 页面文件 - kebab-case
user-profile.vue
product-list.tsx

代码规范

HTML 最佳实践

html
<!-- 使用语义化标签 -->
<header>
  <nav>
    <ul>
      <li><a href="/">首页</a></li>
      <li><a href="/about">关于</a></li>
    </ul>
  </nav>
</header>

<main>
  <article>
    <h1>文章标题</h1>
    <section>
      <h2>章节标题</h2>
      <p>段落内容</p>
    </section>
  </article>
</main>

<footer>
  <p>&copy; 2024 公司名称</p>
</footer>

<!-- 无障碍访问 -->
<img src="image.svg" alt="图片描述">
<button aria-label="关闭对话框">×</button>
<input type="text" aria-describedby="help-text">
<div id="help-text">帮助信息</div>

CSS 最佳实践

css
/* 使用 CSS 自定义属性 */
:root {
  --primary-color: #007bff;
  --secondary-color: #6c757d;
  --font-size-base: 16px;
  --line-height-base: 1.5;
  --border-radius: 4px;
}

/* BEM 命名规范 */
.card {
  border: 1px solid var(--border-color);
  border-radius: var(--border-radius);
}

.card__header {
  padding: 1rem;
  border-bottom: 1px solid var(--border-color);
}

.card__title {
  margin: 0;
  font-size: 1.25rem;
}

.card--featured {
  border-color: var(--primary-color);
}

/* 响应式设计 */
.container {
  max-width: 1200px;
  margin: 0 auto;
  padding: 0 1rem;
}

@media (max-width: 768px) {
  .container {
    padding: 0 0.5rem;
  }
}

/* 使用 Flexbox 和 Grid */
.layout {
  display: grid;
  grid-template-columns: 250px 1fr;
  gap: 2rem;
}

.card-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
  gap: 1rem;
}

JavaScript/TypeScript 最佳实践

typescript
// 使用 const 和 let,避免 var
const API_BASE_URL = 'https://api.example.com'
let currentUser: User | null = null

// 使用箭头函数
const users = data.map(item => ({
  id: item.id,
  name: item.name,
  email: item.email
}))

// 使用解构赋值
const { name, email, age } = user
const [first, second, ...rest] = items

// 使用模板字符串
const message = `Hello, ${name}! You have ${count} new messages.`

// 使用可选链和空值合并
const userName = user?.profile?.name ?? 'Anonymous'

// 错误处理
async function fetchUserData(id: string): Promise<User | null> {
  try {
    const response = await fetch(`/api/users/${id}`)
    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`)
    }
    return await response.json()
  } catch (error) {
    console.error('Failed to fetch user data:', error)
    return null
  }
}

// 使用类型守卫
function isUser(obj: unknown): obj is User {
  return typeof obj === 'object' && 
         obj !== null && 
         'id' in obj && 
         'name' in obj
}

// 函数式编程
const processUsers = (users: User[]) => 
  users
    .filter(user => user.isActive)
    .map(user => ({ ...user, displayName: user.name.toUpperCase() }))
    .sort((a, b) => a.name.localeCompare(b.name))

组件设计原则

单一职责原则

vue
<!-- ❌ 不好的例子 - 组件职责过多 -->
<template>
  <div>
    <header>导航栏</header>
    <main>
      <form>表单</form>
      <table>数据表格</table>
    </main>
    <footer>页脚</footer>
  </div>
</template>

<!-- ✅ 好的例子 - 职责分离 -->
<template>
  <div>
    <AppHeader />
    <main>
      <UserForm @submit="handleSubmit" />
      <UserTable :users="users" @edit="handleEdit" />
    </main>
    <AppFooter />
  </div>
</template>

组件通信

vue
<!-- 父组件 -->
<template>
  <UserList 
    :users="users"
    :loading="loading"
    @user-select="handleUserSelect"
    @user-delete="handleUserDelete"
  />
</template>

<script setup lang="ts">
interface User {
  id: string
  name: string
  email: string
}

const users = ref<User[]>([])
const loading = ref(false)

const handleUserSelect = (user: User) => {
  console.log('Selected user:', user)
}

const handleUserDelete = (userId: string) => {
  users.value = users.value.filter(user => user.id !== userId)
}
</script>

<!-- 子组件 -->
<template>
  <div class="user-list">
    <div v-if="loading" class="loading">加载中...</div>
    <div v-else>
      <div 
        v-for="user in users" 
        :key="user.id"
        class="user-item"
        @click="$emit('user-select', user)"
      >
        <span>{{ user.name }}</span>
        <button @click.stop="$emit('user-delete', user.id)">
          删除
        </button>
      </div>
    </div>
  </div>
</template>

<script setup lang="ts">
interface Props {
  users: User[]
  loading: boolean
}

const props = defineProps<Props>()
const emit = defineEmits<{
  'user-select': [user: User]
  'user-delete': [userId: string]
}>()
</script>

性能优化

代码分割

typescript
// 路由级别的代码分割
const routes = [
  {
    path: '/',
    component: () => import('@/views/Home.vue')
  },
  {
    path: '/about',
    component: () => import('@/views/About.vue')
  }
]

// 组件级别的代码分割
const HeavyComponent = defineAsyncComponent(() => 
  import('@/components/HeavyComponent.vue')
)

图片优化

html
<!-- 响应式图片 -->
<picture>
  <source media="(max-width: 768px)" srcset="image-mobile.svg">
  <source media="(max-width: 1200px)" srcset="image-tablet.svg">
  <img src="image-desktop.svg" alt="描述" loading="lazy">
</picture>

<!-- 使用 SVG 格式 -->
<img src="image.svg" alt="描述" loading="lazy">

虚拟滚动

vue
<template>
  <div class="virtual-list" ref="containerRef">
    <div 
      class="virtual-list-item"
      v-for="item in visibleItems"
      :key="item.id"
      :style="{ transform: `translateY(${item.top}px)` }"
    >
      {{ item.content }}
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'

const containerRef = ref<HTMLElement>()
const scrollTop = ref(0)
const itemHeight = 50
const containerHeight = 400

const visibleItems = computed(() => {
  const startIndex = Math.floor(scrollTop.value / itemHeight)
  const endIndex = Math.min(
    startIndex + Math.ceil(containerHeight / itemHeight),
    items.value.length
  )
  
  return items.value.slice(startIndex, endIndex).map((item, index) => ({
    ...item,
    top: (startIndex + index) * itemHeight
  }))
})
</script>

状态管理

Pinia 最佳实践

typescript
// stores/user.ts
import { defineStore } from 'pinia'

interface User {
  id: string
  name: string
  email: string
}

interface UserState {
  users: User[]
  currentUser: User | null
  loading: boolean
  error: string | null
}

export const useUserStore = defineStore('user', () => {
  // State
  const state = reactive<UserState>({
    users: [],
    currentUser: null,
    loading: false,
    error: null
  })

  // Getters
  const activeUsers = computed(() => 
    state.users.filter(user => user.isActive)
  )

  const userCount = computed(() => state.users.length)

  // Actions
  const fetchUsers = async () => {
    state.loading = true
    state.error = null
    
    try {
      const response = await api.getUsers()
      state.users = response.data
    } catch (error) {
      state.error = error.message
    } finally {
      state.loading = false
    }
  }

  const addUser = (user: User) => {
    state.users.push(user)
  }

  const updateUser = (id: string, updates: Partial<User>) => {
    const index = state.users.findIndex(user => user.id === id)
    if (index !== -1) {
      Object.assign(state.users[index], updates)
    }
  }

  const deleteUser = (id: string) => {
    const index = state.users.findIndex(user => user.id === id)
    if (index !== -1) {
      state.users.splice(index, 1)
    }
  }

  return {
    // State
    ...toRefs(state),
    
    // Getters
    activeUsers,
    userCount,
    
    // Actions
    fetchUsers,
    addUser,
    updateUser,
    deleteUser
  }
})

测试策略

单元测试

typescript
// utils/formatDate.test.ts
import { describe, it, expect } from 'vitest'
import { formatDate } from './formatDate'

describe('formatDate', () => {
  it('should format date correctly', () => {
    const date = new Date('2024-01-15')
    expect(formatDate(date, 'YYYY-MM-DD')).toBe('2024-01-15')
  })

  it('should handle invalid date', () => {
    expect(formatDate(null)).toBe('')
  })
})

// components/UserCard.test.ts
import { mount } from '@vue/test-utils'
import { describe, it, expect } from 'vitest'
import UserCard from './UserCard.vue'

describe('UserCard', () => {
  const mockUser = {
    id: '1',
    name: 'John Doe',
    email: 'john@example.com'
  }

  it('should render user information', () => {
    const wrapper = mount(UserCard, {
      props: { user: mockUser }
    })

    expect(wrapper.text()).toContain('John Doe')
    expect(wrapper.text()).toContain('john@example.com')
  })

  it('should emit follow event when button clicked', async () => {
    const wrapper = mount(UserCard, {
      props: { user: mockUser }
    })

    await wrapper.find('button').trigger('click')
    expect(wrapper.emitted('follow')).toBeTruthy()
    expect(wrapper.emitted('follow')[0]).toEqual([mockUser.id])
  })
})

E2E 测试

typescript
// tests/e2e/user-management.spec.ts
import { test, expect } from '@playwright/test'

test.describe('User Management', () => {
  test('should create new user', async ({ page }) => {
    await page.goto('/users')
    
    // 点击新建用户按钮
    await page.click('[data-testid="add-user-btn"]')
    
    // 填写表单
    await page.fill('[data-testid="user-name"]', 'John Doe')
    await page.fill('[data-testid="user-email"]', 'john@example.com')
    
    // 提交表单
    await page.click('[data-testid="submit-btn"]')
    
    // 验证用户已创建
    await expect(page.locator('[data-testid="user-list"]')).toContainText('John Doe')
  })
})

安全最佳实践

XSS 防护

typescript
// 输入验证和清理
import DOMPurify from 'dompurify'

const sanitizeHTML = (html: string): string => {
  return DOMPurify.sanitize(html)
}

// 使用 v-html 时要小心
const safeHTML = computed(() => sanitizeHTML(userInput.value))

CSRF 防护

typescript
// 添加 CSRF Token
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content')

axios.defaults.headers.common['X-CSRF-TOKEN'] = csrfToken

内容安全策略 (CSP)

html
<meta http-equiv="Content-Security-Policy" 
      content="default-src 'self'; 
               script-src 'self' 'unsafe-inline'; 
               style-src 'self' 'unsafe-inline'; 
               img-src 'self' data: https:;">

构建和部署

Vite 配置优化

typescript
// vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { resolve } from 'path'

export default defineConfig({
  plugins: [vue()],
  
  resolve: {
    alias: {
      '@': resolve(__dirname, 'src')
    }
  },
  
  build: {
    // 代码分割
    rollupOptions: {
      output: {
        manualChunks: {
          vendor: ['vue', 'vue-router'],
          ui: ['element-plus']
        }
      }
    },
    
    // 压缩配置
    minify: 'terser',
    terserOptions: {
      compress: {
        drop_console: true,
        drop_debugger: true
      }
    }
  },
  
  // 开发服务器配置
  server: {
    port: 3000,
    proxy: {
      '/api': {
        target: 'http://localhost:8080',
        changeOrigin: true
      }
    }
  }
})

环境变量管理

bash
# .env.development
VITE_API_BASE_URL=http://localhost:8080/api
VITE_APP_TITLE=开发环境

# .env.production
VITE_API_BASE_URL=https://api.example.com
VITE_APP_TITLE=生产环境
typescript
// config/index.ts
export const config = {
  apiBaseUrl: import.meta.env.VITE_API_BASE_URL,
  appTitle: import.meta.env.VITE_APP_TITLE,
  isDevelopment: import.meta.env.DEV,
  isProduction: import.meta.env.PROD
}

监控和错误处理

错误边界

vue
<!-- ErrorBoundary.vue -->
<template>
  <div v-if="hasError" class="error-boundary">
    <h2>出现了错误</h2>
    <p>{{ error?.message }}</p>
    <button @click="retry">重试</button>
  </div>
  <slot v-else />
</template>

<script setup lang="ts">
import { ref, onErrorCaptured } from 'vue'

const hasError = ref(false)
const error = ref<Error | null>(null)

onErrorCaptured((err) => {
  hasError.value = true
  error.value = err
  
  // 上报错误
  console.error('Component error:', err)
  
  return false
})

const retry = () => {
  hasError.value = false
  error.value = null
}
</script>

性能监控

typescript
// 性能监控
const observer = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    if (entry.entryType === 'navigation') {
      console.log('页面加载时间:', entry.loadEventEnd - entry.loadEventStart)
    }
  }
})

observer.observe({ entryTypes: ['navigation', 'measure'] })

// 用户行为追踪
const trackEvent = (eventName: string, properties?: Record<string, any>) => {
  // 发送到分析服务
  analytics.track(eventName, properties)
}

总结

遵循这些最佳实践可以帮助您:

  1. 提高代码质量 - 通过规范和测试确保代码可靠性
  2. 增强可维护性 - 清晰的结构和命名让代码易于理解
  3. 优化性能 - 通过各种优化技术提升用户体验
  4. 保障安全 - 防范常见的安全漏洞
  5. 简化部署 - 自动化构建和部署流程

记住,最佳实践是不断演进的,要根据项目需求和团队情况灵活应用。


相关资源:

vitepress开发指南