🎁 Get the FREE AI Skills Starter GuideSubscribe →
BytesAgainBytesAgain
🦀 ClawHub

Vue3 Project Standard

by @bovinphang

Vue 3 + TypeScript 项目的完整工程规范,涵盖项目结构、组件设计、Composables、路由、Pinia 状态管理、API 层、错误处理、测试和性能优化。当用户在 Vue 项目中创建、修改组件或模块,涉及架构设计、代码编写时自动激活。

Versionv1.0.0
Downloads519
Stars1
TERMINAL
clawhub install vue3-project-standard

📖 About This Skill


name: vue3-project-standard description: Vue 3 + TypeScript 项目的完整工程规范,涵盖项目结构、组件设计、Composables、路由、Pinia 状态管理、API 层、错误处理、测试和性能优化。当用户在 Vue 项目中创建、修改组件或模块,涉及架构设计、代码编写时自动激活。 version: 2.0.0

Vue 3 项目规范

适用于使用 Vue 3 + TypeScript 的仓库。

项目结构

以下为中大型 Vue 3 项目的业界最佳实践结构,按项目实际情况裁剪:

src/
├── app/                        # 应用入口与全局配置
│   ├── App.vue                 # 根组件
│   ├── main.ts                 # 应用启动入口
│   └── router.ts               # 路由实例与配置
│
├── pages/                      # 页面组件(与路由一一对应)
│   ├── Dashboard/
│   │   ├── DashboardPage.vue
│   │   ├── components/         # 页面私有组件
│   │   └── composables/        # 页面私有 composables
│   ├── UserList/
│   └── Settings/
│
├── layouts/                    # 布局组件
│   ├── MainLayout.vue          # 主布局(侧边栏 + 顶栏 + 内容区)
│   ├── AuthLayout.vue          # 登录/注册页布局
│   └── BlankLayout.vue         # 空白布局(错误页等)
│
├── features/                   # 功能模块(按业务领域划分)
│   ├── auth/
│   │   ├── components/         # 模块组件
│   │   ├── composables/        # 模块 composables
│   │   ├── api.ts              # 模块 API 调用
│   │   ├── types.ts            # 模块类型定义
│   │   ├── constants.ts        # 模块常量
│   │   └── index.ts            # 模块公开导出
│   └── order/
│
├── components/                 # 全局共享 UI 组件
│   ├── AppButton/
│   │   ├── AppButton.vue
│   │   └── __tests__/
│   ├── AppModal/
│   ├── AppForm/
│   └── AppErrorBoundary/
│
├── composables/                # 全局共享 composables
│   ├── useAuth.ts
│   ├── useDebounce.ts
│   └── useMediaQuery.ts
│
├── services/                   # API 基础层
│   ├── request.ts              # Axios/fetch 实例与拦截器
│   └── endpoints/              # API 端点定义(如按领域拆分)
│
├── stores/                     # Pinia 状态管理
│   ├── authStore.ts
│   └── uiStore.ts
│
├── locales/                    # 国际化语言包
│   ├── zh-CN.json              # 中文
│   ├── en-US.json              # 英文
│   └── index.ts                # i18n 实例初始化(vue-i18n)
│
├── assets/                     # 静态资源
│   ├── images/                 # 图片(PNG、JPG、WebP)
│   ├── icons/                  # SVG 图标
│   └── fonts/                  # 自定义字体
│
├── config/                     # 应用配置
│   ├── env.ts                  # 环境变量类型化封装
│   └── features.ts             # Feature Flags 管理
│
├── types/                      # 全局共享类型
│   ├── api.ts                  # API 响应/请求通用类型
│   ├── models.ts               # 业务实体类型
│   └── global.d.ts             # 全局类型扩展(组件类型、模块声明等)
│
├── utils/                      # 纯工具函数
│   ├── format.ts               # 日期、数字、货币格式化
│   ├── validators.ts           # 表单校验规则
│   └── storage.ts              # LocalStorage / SessionStorage 封装
│
├── directives/                 # 自定义指令
│   ├── vPermission.ts          # 权限指令
│   └── vClickOutside.ts        # 点击外部关闭
│
├── plugins/                    # Vue 插件注册
│   ├── i18n.ts                 # vue-i18n 插件配置
│   └── index.ts                # 插件统一注册入口
│
├── styles/                     # 全局样式与主题
│   ├── global.css              # 全局基础样式(reset / normalize)
│   ├── variables.css           # CSS 变量(颜色、间距、字号)
│   ├── breakpoints.ts          # 响应式断点常量
│   └── themes/                 # 主题定义
│       ├── light.css           # 亮色主题变量
│       ├── dark.css            # 暗色主题变量
│       └── index.ts            # 主题切换逻辑
│
└── constants/                  # 全局常量
    ├── routes.ts               # 路由路径常量
    └── config.ts               # 业务常量(分页大小、超时时间等)

关键原则

  • pages/ 做路由映射和布局组合,不放业务逻辑
  • layouts/ 定义页面骨架(侧边栏、顶栏、面包屑),由路由配置的 component 引用
  • features/ 按业务领域划分,模块内自包含(components + composables + api + types)
  • components/ 仅放无业务耦合的通用组件,可跨项目复用
  • composables/ 仅放通用逻辑(防抖、媒体查询等),业务 composables 放到对应 feature 中
  • locales/ 存放语言包 JSON 文件,模板中使用 $t('key') 而非硬编码文案
  • assets/ 存放静态资源,图标优先使用 SVG,图片优先使用 WebP/AVIF
  • config/ 封装环境变量和 Feature Flags,禁止组件中直接读取 import.meta.env
  • styles/themes/ 通过 CSS 变量实现主题切换,组件中引用变量而非硬编码颜色
  • 每个模块通过 index.ts 管控公开 API,避免深层路径导入
  • 组件设计规范

  • 使用

  • Props 和 Emits 使用 TypeScript interface 定义
  • 使用 withDefaults 设置默认值
  • 禁止使用 any,优先使用精确类型
  • defineExpose 暴露的方法需有类型约束
  • Composables 规范

    设计原则

  • use 前缀命名
  • 返回值使用对象,明确标注类型
  • 内部处理 loading / error / data 三态
  • 支持参数响应式(接受 Ref 或 getter)
  • export function useUserList(params: MaybeRef) {
        const data = ref([]);
        const loading = ref(false);
        const error = ref(null);

    async function fetch() { loading.value = true; error.value = null; try { const res = await getUserList(toValue(params)); data.value = res.list; } catch (e) { error.value = e as Error; } finally { loading.value = false; } }

    watchEffect(() => { fetch(); });

    return { data: readonly(data), loading: readonly(loading), error: readonly(error), refetch: fetch }; }

    Composable 使用原则

  • 返回 readonly 引用防止外部意外修改
  • 数据请求场景优先使用 VueQuery / VueUse 等库(如项目已引入)
  • onUnmounted 中清理定时器、事件监听等副作用
  • 避免在 composable 中直接操作 DOM
  • Slots 与 Provide/Inject

    Slots

  • 实现组件组合,而非过多 props
  • 具名 slot 用于明确的布局区域
  • 作用域 slot 传递数据给父组件自定义渲染
  • 
    

    Provide/Inject

  • 用于跨多层级的上下文共享(主题、配置、权限)
  • 提供 InjectionKey 保证类型安全
  • 不要用 provide/inject 替代 props 传递直接父子数据
  • // keys.ts
    export const ThemeKey: InjectionKey> = Symbol('theme');

    // Provider.vue provide(ThemeKey, theme);

    // Consumer.vue const theme = inject(ThemeKey);

    路由规范

    路由组织

    // app/router.ts
    const routes: RouteRecordRaw[] = [
        {
            path: '/',
            component: MainLayout,
            children: [
                { path: '', name: 'Dashboard', component: () => import('@/pages/Dashboard/DashboardPage.vue') },
                { path: 'users', name: 'UserList', component: () => import('@/pages/UserList/UserListPage.vue') },
                { path: 'users/:id', name: 'UserDetail', component: () => import('@/pages/UserDetail/UserDetailPage.vue') },
                { path: 'settings', name: 'Settings', component: () => import('@/pages/Settings/SettingsPage.vue') },
            ],
        },
        { path: '/login', name: 'Login', component: () => import('@/pages/Login/LoginPage.vue') },
        { path: '/:pathMatch(.*)*', name: 'NotFound', component: () => import('@/pages/NotFound.vue') },
    ];
    

    路由原则

  • 路由配置集中管理,每个路由必须有 name
  • 页面组件使用动态 import() 按需加载
  • 权限控制使用路由守卫(beforeEach),而非在每个页面内判断
  • URL 参数(分页、筛选、排序)与路由状态同步
  • // 导航守卫
    router.beforeEach((to) => {
        const authStore = useAuthStore();
        if (to.meta.requiresAuth && !authStore.isLoggedIn) {
            return { name: 'Login', query: { redirect: to.fullPath } };
        }
    });
    

    状态管理(Pinia)

    | 状态类型 | 推荐方案 | |----------|----------| | 组件内临时 UI 状态 | ref / reactive | | 跨组件共享业务状态 | Pinia store | | 服务端数据缓存 | VueQuery / 自定义 composable | | URL 驱动状态 | 路由参数 / useRoute().query | | 表单状态 | VeeValidate / FormKit |

    Pinia Store 规范

    使用 Composition API 风格(setup store):

    // stores/authStore.ts
    export const useAuthStore = defineStore('auth', () => {
        const user = ref(null);
        const token = ref(localStorage.getItem('token'));

    const isLoggedIn = computed(() => !!token.value);

    async function login(credentials: LoginParams) { const res = await authApi.login(credentials); user.value = res.user; token.value = res.token; localStorage.setItem('token', res.token); }

    function logout() { user.value = null; token.value = null; localStorage.removeItem('token'); }

    return { user: readonly(user), isLoggedIn, login, logout }; });

    Store 原则

  • 每个 store 职责单一,按领域拆分
  • 对外暴露 readonly 的状态,通过 action 修改
  • 不要在 store 中存放 UI 临时状态(modal 开关、表单输入等)
  • 服务端数据优先用请求库管理,而非手动存入 store
  • API 层规范

    请求实例

    // services/request.ts
    const request = axios.create({
        baseURL: import.meta.env.VITE_API_BASE_URL,
        timeout: 10000,
    });

    request.interceptors.request.use((config) => { const authStore = useAuthStore(); if (authStore.token) { config.headers.Authorization = Bearer ${authStore.token}; } return config; });

    request.interceptors.response.use( (res) => res.data, (error) => { if (error.response?.status === 401) { const authStore = useAuthStore(); authStore.logout(); router.push({ name: 'Login' }); } return Promise.reject(normalizeError(error)); }, );

    API 函数

    // features/user/api.ts
    export function getUserList(params: UserQueryParams): Promise> {
        return request.get('/users', { params });
    }

    export function updateUser(id: string, data: UpdateUserDTO): Promise { return request.put(/users/${id}, data); }

  • API 函数按 feature 组织,而非全部堆在一个文件
  • 请求参数和响应都有类型约束
  • 拦截器统一处理认证、错误格式化
  • 错误处理

    全局错误捕获

    // main.ts
    app.config.errorHandler = (err, instance, info) => {
        reportError(err, { component: instance?.$options.name, info });
    };
    

    组件级错误

  • 使用 onErrorCaptured 在父组件中捕获子组件错误
  • 数据请求失败需有用户可见的提示和重试机制
  • 不要吞掉错误(空 catch 块)
  • 自定义指令

    // directives/vPermission.ts
    export const vPermission: Directive = {
        mounted(el, binding) {
            const authStore = useAuthStore();
            if (!authStore.hasPermission(binding.value)) {
                el.parentNode?.removeChild(el);
            }
        },
    };
    

  • 指令只处理 DOM 层面的操作
  • 业务逻辑不要放在指令中
  • 需要响应式更新时实现 updated 钩子
  • 样式规范

  • 使用