前端开发最佳实践
本指南汇总了现代前端开发中的最佳实践,帮助您构建高质量、可维护的前端应用。
项目结构
推荐的目录结构
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>© 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)
}
总结
遵循这些最佳实践可以帮助您:
- 提高代码质量 - 通过规范和测试确保代码可靠性
- 增强可维护性 - 清晰的结构和命名让代码易于理解
- 优化性能 - 通过各种优化技术提升用户体验
- 保障安全 - 防范常见的安全漏洞
- 简化部署 - 自动化构建和部署流程
记住,最佳实践是不断演进的,要根据项目需求和团队情况灵活应用。
相关资源: