用 Vite+(vp)从零搭建 Vue3 + TypeScript + Element Plus + Pinia + Vue Router

使用 Vite+ 统一工具链(vp)一条命令搭建 Vue3 全家桶,涵盖按需导入、Pinia store、路由配置,以及常见坑的解决方案。

(更新于 2026年4月30日)

什么是 Vite+(vp)

Vite+ 是 VoidZero 推出的统一 Web 工具链,把 Vite、Vitest、Oxlint、Rolldown、tsdown 集成到一个 vp 命令里。Node.js 版本管理、包管理器检测、dev/build/test 全部通过 vp 统一入口,不需要分别装多个工具。

# 安装(Linux / macOS)
curl -fsSL https://vite.plus | bash

# 验证
vp --version

1. 创建项目

vp create vue 底层调用 create-vue,支持交互式选项,一次性把 TypeScript、Pinia、Vue Router 全勾上:

# 交互式(推荐,会问你要不要 TS/Router/Pinia 等)
vp create vue

# 非交互式(全部选 yes,直接得到完整骨架)
vp create vue -- --ts --router --pinia --eslint --default

create-vue 生成的目录结构:

my-app/
├── src/
│   ├── assets/
│   ├── components/
│   ├── router/
│   │   └── index.ts          ← 已生成
│   ├── stores/
│   │   └── counter.ts        ← 已生成
│   ├── views/
│   ├── App.vue
│   └── main.ts
├── vite.config.ts
└── tsconfig.json

2. 安装依赖

vp install 会自动检测项目里用的是 pnpm/bun/npm/yarn,无需手动指定:

cd my-app
vp install

# 追加 Element Plus
vp install element-plus @element-plus/icons-vue
vp install -D unplugin-auto-import unplugin-vue-components

3. Element Plus 按需导入

修改 vite.config.ts

// vite.config.ts
import { defineConfig } from 'vite-plus'   // ← 用 vite-plus 的 defineConfig
import vue from '@vitejs/plugin-vue'
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'

export default defineConfig({
  plugins: [
    vue(),
    // 自动导入 Vue/Vue Router/Pinia Composition API,无需手写 import
    AutoImport({
      imports: ['vue', 'vue-router', 'pinia'],
      resolvers: [ElementPlusResolver()],
      dts: 'src/auto-imports.d.ts',
    }),
    // 自动按需导入 Element Plus 组件
    Components({
      resolvers: [ElementPlusResolver()],
      dts: 'src/components.d.ts',
    }),
  ],
  resolve: {
    alias: { '@': '/src' },
  },
})

为什么用 vite-plusdefineConfig 而不是 vite vite-plus re-export 了 Vite 所有 API,同时扩展了 test/lint/fmt 等字段。两者兼容,项目迁移零成本。

⚠️ Element Plus 样式丢失

按需导入不会自动引入全局 CSS,在 main.ts 加一行兜底:

import 'element-plus/dist/index.css'

4. Pinia Store

Composition API 风格(推荐,类型推断最好):

// src/stores/user.ts
import { defineStore } from 'pinia'

export const useUserStore = defineStore('user', () => {
  const token = ref(localStorage.getItem('token') ?? '')
  const name  = ref('')
  const isLoggedIn = computed(() => !!token.value)

  function login(t: string, n: string) {
    token.value = t
    name.value  = n
    localStorage.setItem('token', t)
  }

  function logout() {
    token.value = ''
    name.value  = ''
    localStorage.removeItem('token')
  }

  return { token, name, isLoggedIn, login, logout }
})

5. Vue Router

// src/router/index.ts
import { createRouter, createWebHistory } from 'vue-router'

const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes: [
    { path: '/', redirect: '/home' },
    {
      path: '/home',
      component: () => import('@/views/HomeView.vue'),
      meta: { title: '首页', requiresAuth: false },
    },
    {
      path: '/dashboard',
      component: () => import('@/views/DashboardView.vue'),
      meta: { title: '控制台', requiresAuth: true },
    },
    {
      path: '/:pathMatch(.*)*',
      component: () => import('@/views/NotFoundView.vue'),
    },
  ],
  scrollBehavior: (_, __, saved) => saved ?? { top: 0 },
})

// 路由守卫
router.beforeEach((to, _, next) => {
  // ✅ 在守卫内部调用 store,pinia 此时已经通过 app.use(pinia) 激活
  const userStore = useUserStore()
  document.title = (to.meta.title as string) ?? '应用'
  if (to.meta.requiresAuth && !userStore.isLoggedIn) {
    next({ path: '/login', query: { redirect: to.fullPath } })
  } else {
    next()
  }
})

export default router

⚠️ 守卫里用 Pinia 的正确姿势

必须在守卫函数内部调用 useXxxStore(),不能在模块顶层调用——那时 app.use(pinia) 还没执行,会报 getActivePinia was called with no active Pinia


6. main.ts 完整写法

// src/main.ts
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
import router from './router'
import 'element-plus/dist/index.css'
import * as Icons from '@element-plus/icons-vue'

const app = createApp(App)

// pinia 必须在 router 之前注册,路由守卫才能正常使用 store
app.use(createPinia())
app.use(router)

// 全局注册 Element Plus 图标
Object.entries(Icons).forEach(([k, v]) => app.component(k, v))

app.mount('#app')

7. 日常开发命令(全用 vp)

vp dev          # 启动开发服务器(Vite HMR)
vp build        # 生产构建(Rolldown)
vp check        # lint + type-check + format 三合一
vp test         # 运行 Vitest 测试
vp install      # 安装依赖(自动检测包管理器)

常见坑速查

问题原因解法
Element Plus 无样式按需导入未引入全局 CSSimport 'element-plus/dist/index.css'
Pinia 守卫报错模块顶层调用 store,pinia 未激活守卫函数内部调用 useXxxStore()
TS 找不到组件类型components.d.ts 未加入 tsconfig加进 include
vp 命令找不到PATH 未配置source ~/.bashrc 或重开终端