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
    执行无错误。",