第一次打开一个Vue3项目时,很多人会被各种目录和文件搞得晕头转向。经过三年多的Vue3实战开发,我发现理解其代码结构的关键在于把握"模块化设计"和"组合式API"这两大核心理念。与Vue2的选项式API不同,Vue3的目录结构更强调功能逻辑的聚合,而非技术类型的分类。
典型的Vue3项目包含以下核心目录(以Vite构建工具为例):
code复制├── public/ # 静态资源
├── src/
│ ├── assets/ # 编译处理的静态资源
│ ├── components/ # 公共组件
│ ├── composables/ # 组合式函数
│ ├── router/ # 路由配置
│ ├── stores/ # Pinia状态管理
│ ├── styles/ # 全局样式
│ ├── utils/ # 工具函数
│ ├── views/ # 页面级组件
│ ├── App.vue # 根组件
│ └── main.js # 应用入口
现代Vue3的入口文件已经简化为:
javascript复制import { createApp } from 'vue'
import App from './App.vue'
const app = createApp(App)
app.mount('#app')
关键变化在于:
典型的App.vue结构示例:
vue复制<script setup>
import { ref } from 'vue'
const count = ref(0)
</script>
<template>
<div>{{ count }}</div>
</template>
<style scoped>
div { color: red }
</style>
Vue3的SFC有三个重要改进:
<script setup>语法糖简化组合式API写法推荐按功能而非类型划分组件:
code复制components/
├── User/
│ ├── UserCard.vue
│ ├── UserForm.vue
│ └── UserList.vue
└── Product/
├── ProductCard.vue
└── ProductFilter.vue
在composables目录下创建可复用的逻辑:
javascript复制// useMousePosition.js
import { onMounted, onUnmounted, ref } from 'vue'
export function useMousePosition() {
const x = ref(0)
const y = ref(0)
const update = e => {
x.value = e.pageX
y.value = e.pageY
}
onMounted(() => window.addEventListener('mousemove', update))
onUnmounted(() => window.removeEventListener('mousemove', update))
return { x, y }
}
现代Vue项目推荐使用Pinia替代Vuex:
javascript复制// stores/counter.js
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', {
state: () => ({ count: 0 }),
actions: {
increment() {
this.count++
}
}
})
利用动态import实现路由级代码分割:
javascript复制const routes = [
{
path: '/dashboard',
component: () => import('@/views/Dashboard.vue')
}
]
在vite.config.js中常用的Vue插件配置:
javascript复制import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [
vue({
reactivityTransform: true // 启用响应性语法糖
})
]
})
推荐的Vue3 ESLint规则:
javascript复制module.exports = {
extends: [
'eslint:recommended',
'plugin:vue/vue3-recommended'
],
rules: {
'vue/multi-word-component-names': 'off'
}
}
使用defineAsyncComponent实现组件级懒加载:
vue复制<script setup>
import { defineAsyncComponent } from 'vue'
const AsyncComponent = defineAsyncComponent(() =>
import('./components/HeavyComponent.vue')
)
</script>
推荐将小于4KB的图片转为Base64内联:
javascript复制// vite.config.js
export default defineConfig({
build: {
assetsInlineLimit: 4096
}
})
经过多个Vue3企业级项目的实践,我总结出以下经验:
关键提示:避免在组件目录中混入工具函数或组合式函数,这会导致代码难以定位。保持每个目录的单一职责是维护大型项目的关键。
当组件相互引用时,推荐使用动态导入:
javascript复制// Parent.vue
import { defineAsyncComponent } from 'vue'
export default {
components: {
Child: defineAsyncComponent(() => import('./Child.vue'))
}
}
利用Vite的glob导入实现自动注册:
javascript复制// main.js
const modules = import.meta.glob('./components/**/*.vue')
Object.entries(modules).forEach(([path, module]) => {
const name = path.split('/').pop().replace('.vue', '')
app.component(name, defineAsyncComponent(module))
})
手动配置chunk分割规则:
javascript复制// vite.config.js
export default defineConfig({
build: {
rollupOptions: {
output: {
manualChunks: {
lodash: ['lodash'],
vue: ['vue', 'vue-router', 'pinia']
}
}
}
}
})
使用rollup-plugin-visualizer分析包大小:
javascript复制import { visualizer } from 'rollup-plugin-visualizer'
export default defineConfig({
plugins: [
visualizer()
]
})
推荐使用commitlint配置:
javascript复制module.exports = {
extends: ['@commitlint/config-conventional'],
rules: {
'type-enum': [
2,
'always',
['feat', 'fix', 'docs', 'style', 'refactor', 'test', 'chore']
]
}
}
在大型Vue3项目中,我通常会配置Husky+lint-staged实现提交前自动校验:
json复制{
"lint-staged": {
"*.{js,vue}": ["eslint --fix", "prettier --write"]
}
}
为组合式函数添加类型定义:
typescript复制// composables/useCounter.ts
import { ref } from 'vue'
interface CounterOptions {
initialValue?: number
}
export function useCounter(options: CounterOptions = {}) {
const count = ref(options.initialValue || 0)
const increment = () => count.value++
return { count, increment }
}
使用defineProps的泛型参数:
vue复制<script setup lang="ts">
interface Props {
title: string
size?: 'small' | 'medium' | 'large'
}
const props = defineProps<Props>()
</script>
使用Vitest的推荐配置:
javascript复制// vitest.config.js
import { defineConfig } from 'vitest/config'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
test: {
environment: 'happy-dom'
}
})
测试组合式函数:
javascript复制import { useCounter } from './useCounter'
import { renderHook } from '@testing-library/vue'
test('should increment counter', () => {
const { result } = renderHook(() => useCounter())
expect(result.current.count.value).toBe(0)
result.current.increment()
expect(result.current.count.value).toBe(1)
})
现代Vue3的i18n配置:
javascript复制import { createI18n } from 'vue-i18n'
const i18n = createI18n({
legacy: false,
locale: 'zh',
messages: {
zh: { hello: '你好' },
en: { hello: 'Hello' }
}
})
app.use(i18n)
实现语言包的动态加载:
javascript复制const loadLocaleMessages = async (locale) => {
const messages = await import(`./locales/${locale}.json`)
i18n.global.setLocaleMessage(locale, messages.default)
}
使用DOMPurify清理HTML内容:
vue复制<script setup>
import DOMPurify from 'dompurify'
const dirty = '<img src=x onerror=alert(1)>'
const clean = DOMPurify.sanitize(dirty)
</script>
在Vite中配置内容安全策略:
javascript复制// vite.config.js
export default defineConfig({
server: {
headers: {
'Content-Security-Policy': "default-src 'self'"
}
}
})
启用Brotli压缩:
javascript复制// vite.config.js
import viteCompression from 'vite-plugin-compression'
export default defineConfig({
plugins: [
viteCompression({
algorithm: 'brotliCompress'
})
]
})
对静态页面使用预渲染:
javascript复制import { createSSGApp } from 'vue'
import prerender from 'vite-plugin-prerender'
export default defineConfig({
plugins: [
prerender({
routes: ['/', '/about']
})
]
})
在长期维护Vue3项目的过程中,我发现保持代码结构清晰的关键在于:早期建立严格的目录规范、中期坚持模块化拆分、后期定期进行架构重构。每个季度花时间整理项目结构,其收益会随着项目规模增长呈指数级上升。