每个组件应该只关注一件事情,保持组件简洁和可复用。
<!-- UserCard.vue -- 职责单一 -->
<template>
<div class="user-card">
<img :src="avatar" alt="用户头像">
<h3>{{ name }}</h3>
<p>{{ email }}</p>
</div>
</template>
<!-- 职责过多 -->
<template>
<div class="user-card">
<!-- 用户信息 -->
<img :src="avatar" alt="用户头像">
<h3>{{ name }}</h3>
<!-- 用户列表 -->
<ul>
<li v-for="user in users">{{ user.name }}</li>
</ul>
<!-- 搜索功能 -->
<input v-model="search" placeholder="搜索用户">
</div>
</template>
<script>
export default {
name: 'MyComponent',
props: {
// 必填属性
title: {
type: String,
required: true,
validator: (value) => value.length > 0
},
// 可选属性,带默认值
count: {
type: Number,
default: 0,
validator: (value) => value >= 0
},
// 复杂对象
user: {
type: Object,
default: () => ({ name: '匿名用户' })
},
// 数组属性
items: {
type: Array,
default: () => []
},
// 自定义验证
status: {
type: String,
validator: (value) => {
return ['pending', 'active', 'inactive'].includes(value)
}
}
}
}
</script>
| 文件类型 | 命名规范 | 示例 |
|---|---|---|
| 单文件组件 | PascalCase(大驼峰) | UserProfile.vue |
| 组合式函数 | camelCase(小驼峰)+ use前缀 | useUserData.js |
| 工具函数 | camelCase | formatDate.js |
| 常量文件 | UPPER_SNAKE_CASE | CONSTANTS.js |
| 路由文件 | kebab-case | user-routes.js |
export default {
// 1. 组件标识
name: 'MyComponent',
// 2. 继承/组合
mixins: [],
extends: {},
// 3. 组件属性
props: {},
emits: {},
// 4. 组合式 API (Composition API)
setup() {},
// 5. 选项式 API (Options API)
data() {
return {}
},
computed: {},
watch: {},
// 6. 生命周期钩子(按调用顺序)
beforeCreate() {},
created() {},
beforeMount() {},
mounted() {},
beforeUpdate() {},
updated() {},
beforeUnmount() {},
unmounted() {},
activated() {},
deactivated() {},
errorCaptured() {},
// 7. 方法
methods: {},
// 8. 渲染函数
render() {}
}
// 路由懒加载
const routes = [
{
path: '/dashboard',
component: () => import('./views/Dashboard.vue')
}
]
// 组件懒加载
const LazyComponent = defineAsyncComponent(() =>
import('./components/LazyComponent.vue')
)
<!-- 使用 vue-virtual-scroller -->
<RecycleScroller
:items="largeList"
:item-size="50"
key-field="id"
>
<template v-slot="{ item }">
<div>{{ item.name }}</div>
</template>
</RecycleScroller>
// 推荐:使用计算属性缓存结果
computed: {
filteredUsers() {
// 结果会被缓存,只有当依赖变化时才重新计算
return this.users.filter(user =>
user.active && user.age > 18
)
}
}
// 避免:在模板中进行复杂计算
// <div>{{ users.filter(u => u.active).length }}</div>
// 推荐:使用深度侦听时指定选项
watch: {
user: {
handler(newVal, oldVal) {
// 处理变化
},
deep: true, // 深度侦听
immediate: true // 立即执行
}
}
// 推荐:使用flush选项优化性能
watch: {
value: {
handler() {
// DOM更新后执行
},
flush: 'post'
}
}
// 使用v-html时确保内容安全
<div v-html="sanitizedHtml"></div>
// 使用DOMPurify等库清理HTML
import DOMPurify from 'dompurify'
export default {
computed: {
sanitizedHtml() {
return DOMPurify.sanitize(this.unsafeHtml)
}
}
}
// 危险:直接渲染不可信的HTML
<div v-html="userProvidedContent"></div>
// 危险:拼接JavaScript代码
const script = `alert('${userInput}')`
eval(script)
// 使用环境变量存储敏感信息
const apiKey = import.meta.env.VITE_API_KEY
// 请求拦截器中添加认证
import axios from 'axios'
const api = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL
})
api.interceptors.request.use(config => {
const token = localStorage.getItem('token')
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
return config
})
// 响应拦截器中处理错误
api.interceptors.response.use(
response => response,
error => {
if (error.response?.status === 401) {
// 处理未授权
router.push('/login')
}
return Promise.reject(error)
}
)
测试单个组件或函数
npm test -- unit
测试组件交互和渲染
npm test -- component
测试完整用户流程
npm test -- e2e
// Counter.spec.js
import { mount } from '@vue/test-utils'
import Counter from './Counter.vue'
describe('Counter.vue', () => {
it('renders the initial count', () => {
const wrapper = mount(Counter)
expect(wrapper.text()).toContain('Count: 0')
})
it('increments count when button is clicked', async () => {
const wrapper = mount(Counter)
const button = wrapper.find('button')
await button.trigger('click')
expect(wrapper.text()).toContain('Count: 1')
})
it('emits increment event', async () => {
const wrapper = mount(Counter)
await wrapper.vm.increment()
expect(wrapper.emitted().increment).toBeTruthy()
expect(wrapper.emitted().increment[0]).toEqual([1])
})
})
// 组合式函数测试
import { useCounter } from './useCounter'
import { ref } from 'vue'
describe('useCounter', () => {
it('should increment count', () => {
const { count, increment } = useCounter(ref(0))
increment()
expect(count.value).toBe(1)
})
})
| 方案 | 适用场景 | 复杂度 |
|---|---|---|
ref/reactive |
简单组件、局部状态 | 低 |
provide/inject |
深层嵌套组件通信 | 中 |
Pinia |
中大型应用、需要状态共享 | 中 |
Vuex |
遗留项目、需要时间旅行调试 | 高 |
// stores/userStore.js
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import api from '@/api/user'
export const useUserStore = defineStore('user', () => {
// 状态
const user = ref(null)
const loading = ref(false)
const error = ref(null)
// getter
const isLoggedIn = computed(() => !!user.value)
const userName = computed(() => user.value?.name || '访客')
// action
async function login(credentials) {
loading.value = true
error.value = null
try {
const response = await api.login(credentials)
user.value = response.data
localStorage.setItem('token', response.token)
} catch (err) {
error.value = err.message
throw err
} finally {
loading.value = false
}
}
function logout() {
user.value = null
localStorage.removeItem('token')
}
// 返回状态和方法
return {
user,
loading,
error,
isLoggedIn,
userName,
login,
logout
}
})
src/
├── api/ # API请求模块
│ ├── user.js
│ ├── product.js
│ └── index.js
├── assets/ # 静态资源
│ ├── images/
│ ├── fonts/
│ └── styles/
│ ├── variables.scss
│ ├── mixins.scss
│ └── global.scss
├── components/ # 公共组件
│ ├── common/ # 通用组件
│ ├── layout/ # 布局组件
│ └── ui/ # UI基础组件
├── composables/ # 组合式函数
│ ├── useFetch.js
│ ├── useLocalStorage.js
│ └── useForm.js
├── directives/ # 自定义指令
│ └── clickOutside.js
├── router/ # 路由配置
│ ├── index.js
│ ├── routes.js
│ └── guards.js
├── stores/ # 状态管理
│ ├── userStore.js
│ ├── cartStore.js
│ └── index.js
├── utils/ # 工具函数
│ ├── helpers.js
│ ├── validators.js
│ └── constants.js
├── views/ # 页面组件
│ ├── Home.vue
│ ├── Login.vue
│ └── Dashboard/
│ ├── index.vue
│ └── components/ # 页面私有组件
├── App.vue # 根组件
└── main.js # 应用入口
// vite.config.js
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
export default defineConfig({
plugins: [
Components({
resolvers: [ElementPlusResolver()],
dts: true, // 生成类型声明文件
dirs: ['src/components'] // 自动导入的目录
})
]
})
// 使用组件时无需手动导入
// 直接在模板中使用
<template>
<MyComponent />
</template>
{
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"lint": "eslint . --ext .vue,.js,.jsx,.ts,.tsx",
"lint:fix": "eslint . --ext .vue,.js,.jsx,.ts,.tsx --fix",
"type-check": "vue-tsc --noEmit",
"test:unit": "vitest",
"test:e2e": "cypress run",
"prepare": "husky install"
}
}