vue-application-structure

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Vue 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.ts

Step 2 — Composition API Conventions

步骤2 — Composition API 规范

  • Use
    <script setup lang="ts">
    exclusively — no Options API, no
    defineComponent
    .
  • Declare
    defineProps
    and
    defineEmits
    with TypeScript interfaces, not runtime validators.
  • Keep template logic minimal — extract complex expressions into
    computed
    refs or composables.
  • Use alias imports for cross-directory modules and ESM
    import
    /
    export
    syntax only.
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>
  • 仅使用
    <script setup lang="ts">
    ——不使用Options API,不使用
    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
    use
    prefix:
    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 (
    vue-router
    4.x +
    unplugin-vue-router
    or manual
    RouteNamedMap
    ).
  • Use
    defineRoute
    or name constants for route names — never inline string names in code.
  • Lazy-load page-level components:
    component: () => import('./views/HomePage.vue')
    .
  • Put navigation guards in
    router/guards.ts
    , not inline in route definitions.
ts
// router/index.ts
{ path: '/dashboard', component: () => import('@/features/dashboard/DashboardPage.vue'),
  meta: { requiresAuth: true } }
  • 使用类型化路由
    vue-router
    4.x +
    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 — 组件命名规范

RuleExample
PascalCase filenames
UserProfileCard.vue
Base
prefix for generic UI
BaseButton.vue
,
BaseModal.vue
The
prefix for singletons
TheNavbar.vue
,
TheFooter.vue
Feature prefix for feature components
AuthLoginForm.vue
No abbreviations in names
UserProfileCard
not
UsrProfCard
规则示例
文件名使用大驼峰命名
UserProfileCard.vue
通用UI组件以
Base
为前缀
BaseButton.vue
BaseModal.vue
单例组件以
The
为前缀
TheNavbar.vue
TheFooter.vue
功能专属组件以功能名称为前缀
AuthLoginForm.vue
名称中不使用缩写
UserProfileCard
而非
UsrProfCard

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.
  • pnpm tsc --noEmit
    passes with zero errors.
运行TypeScript编译器——不允许存在任何类型错误:
bash
pnpm tsc --noEmit
  • 所有组件均使用
    <script setup lang="ts">
  • 未使用Options API。
  • Pinia store使用setup-store语法。
  • 路由使用懒加载页面组件。
  • pnpm tsc --noEmit
    执行无错误。",