vue-application-structure
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseVue Application Structure Skill
Vue应用结构搭建技能
Step 1 — Directory Layout (Feature-Based)
步骤1 — 基于功能的目录结构
Organise by feature, not by file type. Co-locate everything a feature needs:
src/
features/
auth/
AuthLoginForm.vue
useAuth.ts ← composable
auth.store.ts ← Pinia store
auth.types.ts
auth.api.ts ← HTTP calls
components/ ← shared/generic UI components
BaseButton.vue
TheNavbar.vue
composables/ ← shared composables used across features
useBreakpoint.ts
router/
index.ts
guards.ts
stores/ ← global stores (not feature-specific)
ui.store.ts
lib/ ← utilities, formatters, constants
assets/
App.vue
main.ts按功能而非文件类型组织。将一个功能所需的所有内容放在同一位置:
src/
features/
auth/
AuthLoginForm.vue
useAuth.ts ← 组合式函数
auth.store.ts ← Pinia store
auth.types.ts
auth.api.ts ← HTTP请求
components/ ← 共享/通用UI组件
BaseButton.vue
TheNavbar.vue
composables/ ← 跨功能使用的共享组合式函数
useBreakpoint.ts
router/
index.ts
guards.ts
stores/ ← 全局store(非功能专属)
ui.store.ts
lib/ ← 工具函数、格式化器、常量
assets/
App.vue
main.tsStep 2 — Composition API Conventions
步骤2 — Composition API 规范
- Use exclusively — no Options API, no
<script setup lang="ts">.defineComponent - Declare and
definePropswith TypeScript interfaces, not runtime validators.defineEmits - Keep template logic minimal — extract complex expressions into refs or composables.
computed - Use alias imports for cross-directory modules and ESM /
importsyntax only.export
vue
<script setup lang="ts">
interface Props {
userId: string
readonly?: boolean
}
const props = withDefaults(defineProps<Props>(), { readonly: false })
const emit = defineEmits<{ saved: [userId: string] }>()
</script>- 仅使用——不使用Options API,不使用
<script setup lang="ts">。defineComponent - 使用TypeScript接口声明和
defineProps,而非运行时验证器。defineEmits - 保持模板逻辑最简——将复杂表达式提取到引用或组合式函数中。
computed - 跨目录模块使用别名导入,且仅使用ESM /
import语法。export
vue
<script setup lang="ts">
interface Props {
userId: string
readonly?: boolean
}
const props = withDefaults(defineProps<Props>(), { readonly: false })
const emit = defineEmits<{ saved: [userId: string] }>()
</script>Step 3 — Composable Design
步骤3 — 组合式函数设计
- Name composables with the prefix:
use,useAuth,useCart.useDebounce - Each composable has a single responsibility.
- Return reactive state and actions from a composable; do not mutate props.
- Composables that wrap a Pinia store should not duplicate store state — return store refs directly.
ts
// composables/useDebounce.ts
export function useDebounce<T>(value: Ref<T>, delay: number): Readonly<Ref<T>> {
const debounced = ref<T>(value.value) as Ref<T>
watchEffect(() => {
const timer = setTimeout(() => { debounced.value = value.value }, delay)
return () => clearTimeout(timer)
})
return readonly(debounced)
}- 组合式函数以前缀命名:
use、useAuth、useCart。useDebounce - 每个组合式函数遵循单一职责原则。
- 从组合式函数中返回响应式状态和操作;不要修改props。
- 封装Pinia store的组合式函数不应复制store状态——直接返回store引用。
ts
// composables/useDebounce.ts
export function useDebounce<T>(value: Ref<T>, delay: number): Readonly<Ref<T>> {
const debounced = ref<T>(value.value) as Ref<T>
watchEffect(() => {
const timer = setTimeout(() => { debounced.value = value.value }, delay)
return () => clearTimeout(timer)
})
return readonly(debounced)
}Step 4 — Pinia Store Design
步骤4 — Pinia Store 设计
- One store per domain (auth, cart, notifications). Do not create a single monolithic store.
- Use the setup store syntax (returns reactive state + actions, full TypeScript support).
- Keep side effects (API calls) inside actions, not in the template or composables.
ts
// features/auth/auth.store.ts
export const useAuthStore = defineStore('auth', () => {
const user = ref<User | null>(null)
const isAuthenticated = computed(() => user.value !== null)
async function login(credentials: Credentials) {
user.value = await authApi.login(credentials)
}
return { user, isAuthenticated, login }
})- 每个业务域对应一个store(如认证、购物车、通知)。不要创建单一的大型单体store。
- 使用setup store语法(返回响应式状态 + 操作,完全支持TypeScript)。
- 将副作用(API调用)放在actions中,而非模板或组合式函数里。
ts
// features/auth/auth.store.ts
export const useAuthStore = defineStore('auth', () => {
const user = ref<User | null>(null)
const isAuthenticated = computed(() => user.value !== null)
async function login(credentials: Credentials) {
user.value = await authApi.login(credentials)
}
return { user, isAuthenticated, login }
})Step 5 — Vue Router
步骤5 — Vue Router 配置
- Use typed routes (4.x +
vue-routeror manualunplugin-vue-router).RouteNamedMap - Use or name constants for route names — never inline string names in code.
defineRoute - Lazy-load page-level components: .
component: () => import('./views/HomePage.vue') - Put navigation guards in , not inline in route definitions.
router/guards.ts
ts
// router/index.ts
{ path: '/dashboard', component: () => import('@/features/dashboard/DashboardPage.vue'),
meta: { requiresAuth: true } }- 使用类型化路由(4.x +
vue-router或手动定义unplugin-vue-router)。RouteNamedMap - 使用或命名常量定义路由名称——代码中绝不使用内联字符串名称。
defineRoute - 懒加载页面级组件:。
component: () => import('./views/HomePage.vue') - 将导航守卫放在中,而非内联在路由定义里。
router/guards.ts
ts
// router/index.ts
{ path: '/dashboard', component: () => import('@/features/dashboard/DashboardPage.vue'),
meta: { requiresAuth: true } }Step 6 — Component Conventions
步骤6 — 组件命名规范
| Rule | Example |
|---|---|
| PascalCase filenames | |
| |
| |
| Feature prefix for feature components | |
| No abbreviations in names | |
| 规则 | 示例 |
|---|---|
| 文件名使用大驼峰命名 | |
通用UI组件以 | |
单例组件以 | |
| 功能专属组件以功能名称为前缀 | |
| 名称中不使用缩写 | |
Step 7 — Verify
步骤7 — 验证
Run the TypeScript compiler — no type errors allowed:
bash
pnpm tsc --noEmit- All components use .
<script setup lang="ts"> - No Options API usage.
- Pinia stores use setup-store syntax.
- Router uses lazy-loaded page components.
- passes with zero errors.
pnpm tsc --noEmit
运行TypeScript编译器——不允许存在任何类型错误:
bash
pnpm tsc --noEmit- 所有组件均使用。
<script setup lang="ts"> - 未使用Options API。
- Pinia store使用setup-store语法。
- 路由使用懒加载页面组件。
- 执行无错误。",
pnpm tsc --noEmit