什么是 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
进入项目目录,安装依赖:
cd my-app
npm install # 或 pnpm install / yarn
2. 安装 Element Plus
npm install element-plus
完整引入(简单但体积大)
在 main.ts 中:
import { createApp } from 'vue'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import App from './App.vue'
const app = createApp(App)
app.use(ElementPlus)
app.mount('#app')
按需引入(推荐:自动导入,零配置)
安装 unplugin-vue-components 和 unplugin-auto-import:
npm install -D unplugin-vue-components unplugin-auto-import
修改 vite.config.ts:
import { defineConfig } from 'vite'
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(),
AutoImport({
resolvers: [ElementPlusResolver()],
// 自动导入 Vue 相关函数:ref, reactive, computed 等
imports: ['vue', 'vue-router', 'pinia'],
dts: 'src/auto-imports.d.ts',
}),
Components({
resolvers: [ElementPlusResolver()],
dts: 'src/components.d.ts',
}),
],
})
配置后,在模板中直接使用 Element Plus 组件,无需 import:
<template>
<el-button type="primary" @click="handleClick">点击</el-button>
<el-input v-model="inputVal" placeholder="请输入" />
</template>
<script setup lang="ts">
// ref、reactive 等也自动导入,无需手写 import
const inputVal = ref('')
const handleClick = () => {
ElMessage.success('操作成功')
}
</script>
3. 完整的 vite.config.ts
import { defineConfig } from 'vite'
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'
import { fileURLToPath, URL } from 'node:url'
export default defineConfig({
plugins: [
vue(),
AutoImport({
resolvers: [ElementPlusResolver()],
imports: ['vue', 'vue-router', 'pinia'],
dts: 'src/auto-imports.d.ts',
eslintrc: {
enabled: true, // 生成 ESLint 配置,避免 no-undef 警告
filepath: '.eslintrc-auto-import.json',
},
}),
Components({
resolvers: [ElementPlusResolver()],
dts: 'src/components.d.ts',
}),
],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)),
},
},
server: {
port: 3000,
open: true,
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, ''),
},
},
},
build: {
outDir: 'dist',
sourcemap: false,
rollupOptions: {
output: {
// 代码分割:第三方库单独打包
manualChunks: {
'vue-vendor': ['vue', 'vue-router', 'pinia'],
'element-plus': ['element-plus'],
},
},
},
},
})
4. Pinia Store 完整示例
定义 Store
// src/stores/user.ts
import { defineStore } from 'pinia'
interface UserInfo {
id: number
name: string
email: string
avatar: string
roles: string[]
}
interface UserState {
userInfo: UserInfo | null
token: string
isLoading: boolean
}
export const useUserStore = defineStore('user', {
state: (): UserState => ({
userInfo: null,
token: localStorage.getItem('token') || '',
isLoading: false,
}),
getters: {
isLoggedIn: (state) => !!state.token,
userName: (state) => state.userInfo?.name ?? '未登录',
hasRole: (state) => (role: string) => state.userInfo?.roles.includes(role) ?? false,
},
actions: {
async login(username: string, password: string) {
this.isLoading = true
try {
// 调用登录 API
const response = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password }),
})
const data = await response.json()
this.token = data.token
this.userInfo = data.userInfo
localStorage.setItem('token', data.token)
} finally {
this.isLoading = false
}
},
logout() {
this.token = ''
this.userInfo = null
localStorage.removeItem('token')
},
async fetchUserInfo() {
if (!this.token) return
const response = await fetch('/api/user/info', {
headers: { Authorization: `Bearer ${this.token}` },
})
this.userInfo = await response.json()
},
},
})
在组件中使用(storeToRefs)
<template>
<div>
<p v-if="isLoggedIn">欢迎,{{ userName }}</p>
<el-button v-else @click="handleLogin" :loading="isLoading">
登录
</el-button>
<el-button v-if="isLoggedIn" @click="handleLogout">退出</el-button>
</div>
</template>
<script setup lang="ts">
import { useUserStore } from '@/stores/user'
import { storeToRefs } from 'pinia'
const userStore = useUserStore()
// storeToRefs:将 state 和 getter 解构为响应式 ref
// 注意:actions 不能用 storeToRefs,直接从 store 解构
const { isLoggedIn, userName, isLoading } = storeToRefs(userStore)
const { login, logout } = userStore
const handleLogin = async () => {
await login('admin', 'password123')
ElMessage.success('登录成功')
}
const handleLogout = () => {
logout()
ElMessage.info('已退出登录')
}
</script>
Composition API 风格的 Store(推荐)
// src/stores/counter.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
export const useCounterStore = defineStore('counter', () => {
// state
const count = ref(0)
const name = ref('Counter')
// getters(computed)
const doubleCount = computed(() => count.value * 2)
// actions
function increment() {
count.value++
}
function decrement() {
count.value--
}
async function incrementAsync() {
await new Promise((resolve) => setTimeout(resolve, 500))
count.value++
}
return { count, name, doubleCount, increment, decrement, incrementAsync }
})
5. Vue Router 4 配置
路由文件
// src/router/index.ts
import { createRouter, createWebHistory, type RouteRecordRaw } from 'vue-router'
import { useUserStore } from '@/stores/user'
// 懒加载路由(推荐:只在访问时才加载对应 chunk)
const routes: RouteRecordRaw[] = [
{
path: '/',
name: 'Layout',
component: () => import('@/layouts/DefaultLayout.vue'),
children: [
{
path: '',
name: 'Home',
component: () => import('@/views/HomeView.vue'),
meta: { title: '首页', requiresAuth: false },
},
{
path: 'dashboard',
name: 'Dashboard',
component: () => import('@/views/DashboardView.vue'),
meta: { title: '仪表盘', requiresAuth: true },
},
{
path: 'users',
name: 'Users',
component: () => import('@/views/UsersView.vue'),
meta: { title: '用户管理', requiresAuth: true, roles: ['admin'] },
},
],
},
{
path: '/login',
name: 'Login',
component: () => import('@/views/LoginView.vue'),
meta: { title: '登录', requiresAuth: false },
},
{
path: '/:pathMatch(.*)*',
name: 'NotFound',
component: () => import('@/views/NotFound.vue'),
},
]
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes,
scrollBehavior(to, from, savedPosition) {
if (savedPosition) {
return savedPosition
}
return { top: 0 }
},
})
// 全局路由守卫
router.beforeEach(async (to, from, next) => {
// 设置页面标题
document.title = to.meta.title ? `${to.meta.title} - My App` : 'My App'
const userStore = useUserStore()
// 需要登录的页面
if (to.meta.requiresAuth && !userStore.isLoggedIn) {
next({ name: 'Login', query: { redirect: to.fullPath } })
return
}
// 已登录不能访问登录页
if (to.name === 'Login' && userStore.isLoggedIn) {
next({ name: 'Dashboard' })
return
}
next()
})
export default router
6. TypeScript 类型扩充
扩充路由 meta 类型
// src/types/router.d.ts
import 'vue-router'
declare module 'vue-router' {
interface RouteMeta {
title?: string
requiresAuth?: boolean
roles?: string[]
keepAlive?: boolean
icon?: string
}
}
扩充全局属性
// src/types/global.d.ts
// 扩充 window 对象
declare global {
interface Window {
__APP_CONFIG__: {
apiBaseUrl: string
version: string
}
}
}
// 给 Vue 组件实例添加全局属性
declare module 'vue' {
interface ComponentCustomProperties {
$filters: {
formatDate: (date: Date) => string
formatMoney: (amount: number) => string
}
}
}
export {}
环境变量类型
// src/types/env.d.ts
/// <reference types="vite/client" />
interface ImportMetaEnv {
readonly VITE_APP_TITLE: string
readonly VITE_API_BASE_URL: string
readonly VITE_APP_ENV: 'development' | 'staging' | 'production'
}
interface ImportMeta {
readonly env: ImportMetaEnv
}
7. 项目目录结构最佳实践
my-app/
├── public/ # 静态资源(不经过 Vite 处理)
│ └── favicon.ico
├── src/
│ ├── api/ # API 请求层
│ │ ├── http.ts # axios 实例配置
│ │ ├── user.ts # 用户相关 API
│ │ └── index.ts # 统一导出
│ ├── assets/ # 静态资源(经 Vite 处理)
│ │ ├── images/
│ │ └── styles/
│ │ ├── variables.scss
│ │ └── global.scss
│ ├── components/ # 通用组件
│ │ ├── common/ # 公共基础组件
│ │ └── business/ # 业务组件
│ ├── composables/ # 组合式函数(hooks)
│ │ ├── useRequest.ts
│ │ └── useTheme.ts
│ ├── layouts/ # 布局组件
│ │ ├── DefaultLayout.vue
│ │ └── AuthLayout.vue
│ ├── router/
│ │ └── index.ts
│ ├── stores/ # Pinia stores
│ │ ├── user.ts
│ │ ├── app.ts
│ │ └── index.ts
│ ├── types/ # TypeScript 类型定义
│ │ ├── api.d.ts
│ │ ├── router.d.ts
│ │ ├── env.d.ts
│ │ └── global.d.ts
│ ├── utils/ # 工具函数
│ │ ├── format.ts
│ │ └── storage.ts
│ ├── views/ # 页面组件
│ │ ├── HomeView.vue
│ │ ├── DashboardView.vue
│ │ └── LoginView.vue
│ ├── App.vue
│ ├── main.ts
│ ├── auto-imports.d.ts # 自动生成(unplugin-auto-import)
│ └── components.d.ts # 自动生成(unplugin-vue-components)
├── .env # 环境变量
├── .env.development
├── .env.production
├── vite.config.ts
├── tsconfig.json
└── package.json
8. 常见报错和解决方案
报错 1:找不到模块 ‘element-plus’
Cannot find module 'element-plus' or its corresponding type declarations.
解决:确保安装了 element-plus 和类型声明:
npm install element-plus
Element Plus 自带 TypeScript 类型,不需要单独安装 @types。
报错 2:ElMessage 未定义
使用 unplugin-auto-import 后,ElMessage、ElMessageBox 等应该自动导入,但有时需要重启 TS 服务:
# 在 VSCode 中:Ctrl+Shift+P → TypeScript: Restart TS Server
或者手动在需要的文件中引入:
import { ElMessage } from 'element-plus'
报错 3:Pinia 报错 “getActivePinia was called with no active Pinia”
在 router.beforeEach 中使用 store 时,需要确保 Pinia 已经在 createApp 时挂载:
// main.ts - 顺序很重要
const app = createApp(App)
app.use(createPinia()) // 必须在 router 之前
app.use(router)
app.mount('#app')
报错 4:路由懒加载在开发模式下很慢
这是正常现象,Vite 在开发模式下按需编译。生产构建后每个路由会生成独立 chunk,加载速度正常。
报错 5:storeToRefs 导致响应式丢失
直接解构 store(不使用 storeToRefs)会导致响应式丢失:
// ❌ 错误:这样 count 不是响应式的
const { count } = useCounterStore()
// ✅ 正确:使用 storeToRefs
const { count } = storeToRefs(useCounterStore())
注意:actions 不能用 storeToRefs,直接解构即可:
const store = useCounterStore()
const { count } = storeToRefs(store) // state/getter 用 storeToRefs
const { increment } = store // action 直接解构
报错 6:TypeScript 报错 “模块 ‘@/xxx’ 无法找到”
检查 tsconfig.json 是否配置了路径别名:
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
}
}
vite.config.ts 中的 resolve.alias 和 tsconfig.json 中的 paths 需要同步配置。
总结
这套组合(Vite + Vue3 + TypeScript + Element Plus + Pinia + Vue Router)是 2024 年 Vue 生态最主流的选型:
- 开发体验:Vite 的 HMR 极快,TypeScript 提供类型安全
- UI:Element Plus 组件丰富,按需引入零配置
- 状态管理:Pinia 比 Vuex 更简洁,完整的 TypeScript 支持
- 路由:Vue Router 4 的懒加载和路由守卫满足企业级需求
按照本文的配置搭建,可以得到一个生产可用的前端项目骨架。