Vue 生态系统与工具链

中等 🟡Vue 生态
11 个标签
预计阅读时间:44 分钟
Vue生态系统工具链CLIViteVue RouterPiniaAxiosVueUseVitest部署

Vue 生态系统与工具链

Vue 生态系统丰富多样,包括官方工具和社区工具,为开发提供了完整的解决方案。一个现代的 Vue 3 项目通常由「构建工具 + 路由 + 状态管理 + HTTP 层 + UI 库 + 工具函数库 + 测试 + 代码规范 + 部署」这几层拼装而成,理解每一层的定位与协作方式,是从「会写组件」到「能独立搭建工程」的关键跨越。

本文按「概念 → 为什么重要 → 原理/用法 → 代码或配置示例 → 真实案例 → 数据对比 → 常见坑 → 最佳实践 → 总结」的顺序,尽可能给出可直接落地的配置与代码。

为什么生态与工具链如此重要

决定开发体验:冷启动、热更新(HMR)速度直接影响每天几百次的保存-刷新循环,慢一秒累积起来就是大量时间浪费。
决定项目上限:路由懒加载、状态管理规范、请求层封装是否合理,决定了项目能否从 3 个页面平滑扩展到 300 个页面。
决定团队协作:ESLint / Prettier / TypeScript / 提交规范让多人代码风格统一,减少无意义的 review 争论。
决定线上质量:测试覆盖、构建产物分析、CDN 与缓存策略、部署自动化,决定了线上出 bug 的概率与修复速度。

下面从「脚手架与构建工具」开始,逐层展开。

脚手架与构建工具

Vue CLI(旧项目仍在用)

Vue CLI 基于 Webpack,是 Vue 2 时代的官方脚手架。官方目前处于维护模式(maintenance mode),新项目不推荐,但大量存量项目仍在使用,需要了解。

bashCode
# 全局安装
npm install -g @vue/cli

# 交互式创建项目
vue create my-project

# 图形化界面创建
vue ui

# 启动开发服务器 / 构建
npm run serve
npm run build

Vue CLI 的配置文件是 `vue.config.js`:

javascriptCode
// vue.config.js
const { defineConfig } = require('@vue/cli-service')

module.exports = defineConfig({
  transpileDependencies: true,
  publicPath: process.env.NODE_ENV === 'production' ? '/app/' : '/',
  productionSourceMap: false,
  devServer: {
    port: 8080,
    proxy: {
      '/api': {
        target: 'http://localhost:3000',
        changeOrigin: true,
        pathRewrite: { '^/api': '' }
      }
    }
  },
  chainWebpack: (config) => {
    // 自定义 webpack 配置,例如设置别名
    config.resolve.alias.set('@', require('path').resolve(__dirname, 'src'))
  }
})

Vite(现代 Vue 项目首选)

Vite 由 Vue 作者尤雨溪开发,开发环境基于原生 ES Module + esbuild,生产环境基于 Rollup,冷启动和 HMR 速度远超 Webpack,已成为 Vue 3 的事实标准。

bashCode
# 创建 Vue 3 + TypeScript 项目
npm create vite@latest my-app -- --template vue-ts

cd my-app
npm install
npm run dev      # 启动开发服务器
npm run build    # 生产构建
npm run preview  # 本地预览构建产物

一个较完整、覆盖别名、代理、按需引入、构建拆包的 `vite.config.ts`:

typescriptCode
// vite.config.ts
import { defineConfig, loadEnv } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueJsx from '@vitejs/plugin-vue-jsx'
import { resolve } from 'path'
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'

// mode 用于区分 development / production / staging
export default defineConfig(({ mode }) => {
  const env = loadEnv(mode, process.cwd(), '')

  return {
    plugins: [
      vue(),
      vueJsx(),
      // 自动导入 ref / computed / watch 等 API,无需手动 import
      AutoImport({
        imports: ['vue', 'vue-router', 'pinia'],
        resolvers: [ElementPlusResolver()],
        dts: 'src/auto-imports.d.ts'
      }),
      // 自动按需注册组件
      Components({
        resolvers: [ElementPlusResolver()],
        dts: 'src/components.d.ts'
      })
    ],
    resolve: {
      alias: {
        '@': resolve(__dirname, 'src'),
        '@components': resolve(__dirname, 'src/components'),
        '@utils': resolve(__dirname, 'src/utils')
      }
    },
    css: {
      preprocessorOptions: {
        scss: {
          // 全局注入变量,无需每个文件单独 import
          additionalData: `@use "@/styles/variables.scss" as *;`
        }
      }
    },
    server: {
      port: 5173,
      open: true,
      proxy: {
        '/api': {
          target: env.VITE_API_BASE || 'http://localhost:3000',
          changeOrigin: true,
          rewrite: (path) => path.replace(/^\/api/, '')
        }
      }
    },
    build: {
      target: 'es2015',
      sourcemap: false,
      chunkSizeWarningLimit: 1500,
      rollupOptions: {
        output: {
          // 手动拆包,把大依赖单独打包,利于长期缓存
          manualChunks: {
            vue: ['vue', 'vue-router', 'pinia'],
            elementPlus: ['element-plus'],
            echarts: ['echarts']
          }
        }
      }
    }
  }
})

环境变量文件(Vite 约定以 `VITE_` 开头的变量才会暴露给客户端):

bashCode
# .env.development
VITE_API_BASE=http://localhost:3000
VITE_APP_TITLE=我的应用(开发)

# .env.production
VITE_API_BASE=https://api.example.com
VITE_APP_TITLE=我的应用

在代码中访问:

typescriptCode
// src/config/index.ts
export const API_BASE = import.meta.env.VITE_API_BASE
export const APP_TITLE = import.meta.env.VITE_APP_TITLE
export const IS_PROD = import.meta.env.PROD

Vite vs Webpack 性能对比

以一个约 1000 个模块的中型项目为例(数据为社区常见量级,实际因机器而异):

| 指标 | Webpack 5 | Vite 5 | 差距 |

| --- | --- | --- | --- |

| 冷启动(首次 dev) | 25 到 40 秒 | 0.3 到 1 秒 | 约 30 到 100 倍 |

| HMR 热更新 | 1 到 3 秒 | 50 到 200 毫秒 | 约 10 到 30 倍 |

| 生产构建 | 40 到 90 秒 | 20 到 50 秒 | 约 1.5 到 2 倍 |

| 配置复杂度 | 高 | 低 | Vite 开箱即用 |

| 生态成熟度 | 极高 | 高(快速追赶) | Webpack 插件更多 |

结论:开发体验 Vite 全面胜出;生产构建 Vite 用 Rollup 也更快;仅在需要极其特殊的老旧构建定制、或依赖某些只有 Webpack loader 的场景,Webpack 才有优势。

Vue Router 4 —— 官方路由

Vue Router 4 是配套 Vue 3 的路由库,支持 Composition API、动态路由、懒加载、导航守卫等。

基础路由配置与懒加载

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

const routes: RouteRecordRaw[] = [
  {
    path: '/',
    name: 'Home',
    // 懒加载:打包时该组件会被单独拆成一个 chunk,按需加载
    component: () => import('@/views/Home.vue'),
    meta: { title: '首页', requiresAuth: false }
  },
  {
    path: '/dashboard',
    name: 'Dashboard',
    component: () => import('@/views/Dashboard.vue'),
    meta: { title: '控制台', requiresAuth: true },
    children: [
      {
        path: 'profile',
        name: 'Profile',
        component: () => import('@/views/Profile.vue'),
        meta: { title: '个人中心' }
      }
    ]
  },
  {
    // 动态路由:/user/123 -> params.id === '123'
    path: '/user/:id',
    name: 'User',
    component: () => import('@/views/User.vue'),
    props: true // 将路由参数作为 props 传入组件
  },
  {
    // 404 兜底
    path: '/:pathMatch(.*)*',
    name: 'NotFound',
    component: () => import('@/views/NotFound.vue')
  }
]

const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes,
  scrollBehavior(to, from, savedPosition) {
    // 前进后退时恢复滚动位置,否则回到顶部
    return savedPosition || { top: 0 }
  }
})

export default router

导航守卫(登录鉴权与标题设置)

typescriptCode
// src/router/guards.ts
import router from './index'
import { useUserStore } from '@/stores/user'

router.beforeEach((to, from, next) => {
  // 动态设置页面标题
  document.title = (to.meta.title as string) || import.meta.env.VITE_APP_TITLE

  const userStore = useUserStore()

  if (to.meta.requiresAuth && !userStore.isLoggedIn) {
    // 未登录,重定向到登录页并携带回跳地址
    next({ name: 'Login', query: { redirect: to.fullPath } })
  } else {
    next()
  }
})

router.afterEach((to) => {
  // 可在此处埋点上报页面浏览
  console.log('[route] navigated to', to.fullPath)
})

组件内使用路由(Composition API)

vueCode
<script setup lang="ts">
import { useRoute, useRouter } from 'vue-router'

const route = useRoute()   // 当前路由信息(响应式)
const router = useRouter()  // 路由实例,用于编程式跳转

// 读取动态参数
const userId = route.params.id

function goToDetail(id: number) {
  router.push({ name: 'User', params: { id } })
}

function goBack() {
  router.back()
}
</script>

<template>
  <div>
    <p>当前用户 ID:{{ userId }}</p>
    <button @click="goToDetail(42)">查看用户 42</button>
    <button @click="goBack">返回</button>
  </div>
</template>

动态添加路由(权限系统常用)

后台管理系统常见做法:登录后根据用户角色,从后端拿到菜单,再动态注册路由。

typescriptCode
// 根据后端返回的菜单动态注册路由
function addDynamicRoutes(menus: MenuItem[]) {
  menus.forEach((menu) => {
    router.addRoute('Dashboard', {
      path: menu.path,
      name: menu.name,
      component: () => import(`@/views/${menu.component}.vue`),
      meta: { title: menu.title, requiresAuth: true }
    })
  })
}

Pinia —— 官方状态管理

Pinia 是 Vue 官方推荐的状态管理库,替代 Vuex,API 更简洁、TypeScript 支持更好、去掉了 mutations 概念。

Option Store 写法

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

export const useCounterStore = defineStore('counter', {
  state: () => ({
    count: 0,
    name: 'Vue'
  }),
  getters: {
    doubleCount: (state) => state.count * 2
  },
  actions: {
    increment() {
      this.count++
    },
    async fetchInitial() {
      const res = await fetch('/api/counter')
      this.count = (await res.json()).count
    }
  }
})

Setup Store 写法(更贴近 Composition API)

typescriptCode
// src/stores/user.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import { login as loginApi, getUserInfo } from '@/api/user'

export const useUserStore = defineStore('user', () => {
  // state
  const token = ref<string>(localStorage.getItem('token') || '')
  const userInfo = ref<UserInfo | null>(null)

  // getters
  const isLoggedIn = computed(() => !!token.value)
  const userName = computed(() => userInfo.value?.name ?? '游客')

  // actions
  async function login(username: string, password: string) {
    const { token: newToken } = await loginApi(username, password)
    token.value = newToken
    localStorage.setItem('token', newToken)
    await loadUserInfo()
  }

  async function loadUserInfo() {
    userInfo.value = await getUserInfo()
  }

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

  return { token, userInfo, isLoggedIn, userName, login, loadUserInfo, logout }
})

在组件中使用(注意用 `storeToRefs` 保持解构后的响应性):

vueCode
<script setup lang="ts">
import { storeToRefs } from 'pinia'
import { useUserStore } from '@/stores/user'

const userStore = useUserStore()
// 直接解构会丢失响应性,必须用 storeToRefs
const { userName, isLoggedIn } = storeToRefs(userStore)
// 方法可以直接解构
const { login, logout } = userStore
</script>

<template>
  <div>
    <span v-if="isLoggedIn">你好,{{ userName }}</span>
    <button v-else @click="login('admin', '123456')">登录</button>
    <button v-if="isLoggedIn" @click="logout">退出</button>
  </div>
</template>

Pinia 持久化

借助 `pinia-plugin-persistedstate` 可以把 store 自动同步到 localStorage / sessionStorage:

bashCode
npm install pinia-plugin-persistedstate
typescriptCode
// src/main.ts
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
import App from './App.vue'

const pinia = createPinia()
pinia.use(piniaPluginPersistedstate)

createApp(App).use(pinia).mount('#app')
typescriptCode
// 在 store 中开启持久化(Option Store 写法)
export const useSettingStore = defineStore('setting', {
  state: () => ({ theme: 'light', lang: 'zh-CN' }),
  persist: {
    key: 'app-setting',
    storage: localStorage,
    // 只持久化部分字段
    pick: ['theme', 'lang']
  }
})

Pinia vs Vuex 对比

| 维度 | Vuex 4 | Pinia |

| --- | --- | --- |

| 核心概念 | state / getters / mutations / actions | state / getters / actions(无 mutations) |

| TypeScript 支持 | 需要繁琐的类型体操 | 原生完善,自动推导 |

| 模块化 | 需要 modules 嵌套与命名空间 | 每个 store 独立,天然扁平 |

| DevTools | 支持 | 支持,且时间旅行更好 |

| 代码量 | 较多样板代码 | 更少 |

| 官方推荐 | 已进入维护模式 | Vue 3 官方推荐 |

| 体积 | 约 10KB | 约 1.5KB |

Axios —— HTTP 请求层封装

直接在组件里到处写 axios 调用会导致重复代码和维护困难,通常会做一层统一封装:统一 baseURL、超时、token 注入、错误处理、响应解包。

typescriptCode
// src/utils/request.ts
import axios, { type AxiosInstance, type AxiosRequestConfig, type AxiosResponse } from 'axios'
import { ElMessage } from 'element-plus'
import { useUserStore } from '@/stores/user'

const service: AxiosInstance = axios.create({
  baseURL: import.meta.env.VITE_API_BASE,
  timeout: 15000,
  headers: { 'Content-Type': 'application/json' }
})

// 请求拦截器:注入 token
service.interceptors.request.use(
  (config) => {
    const userStore = useUserStore()
    if (userStore.token) {
      config.headers.Authorization = `Bearer ${userStore.token}`
    }
    return config
  },
  (error) => Promise.reject(error)
)

// 响应拦截器:统一解包与错误处理
service.interceptors.response.use(
  (response: AxiosResponse) => {
    const res = response.data
    if (res.code !== 0) {
      ElMessage.error(res.message || '请求出错')
      // token 失效,强制登出
      if (res.code === 401) {
        useUserStore().logout()
        window.location.href = '/login'
      }
      return Promise.reject(new Error(res.message || 'Error'))
    }
    return res.data
  },
  (error) => {
    const status = error.response?.status
    const msgMap: Record<number, string> = {
      400: '请求参数错误',
      403: '没有权限',
      404: '资源不存在',
      500: '服务器内部错误',
      502: '网关错误',
      503: '服务不可用'
    }
    ElMessage.error(msgMap[status] || error.message || '网络异常')
    return Promise.reject(error)
  }
)

// 泛型封装,让调用处拿到正确的返回类型
export function request<T = unknown>(config: AxiosRequestConfig): Promise<T> {
  return service(config) as unknown as Promise<T>
}

export default service

按业务模块组织 API:

typescriptCode
// src/api/user.ts
import { request } from '@/utils/request'

export interface LoginResult {
  token: string
}

export interface UserInfo {
  id: number
  name: string
  roles: string[]
}

export function login(username: string, password: string) {
  return request<LoginResult>({
    url: '/auth/login',
    method: 'post',
    data: { username, password }
  })
}

export function getUserInfo() {
  return request<UserInfo>({ url: '/user/info', method: 'get' })
}

UI 库与按需引入

Element Plus 按需引入

全量引入 Element Plus 会让首屏体积暴增,推荐用 `unplugin-vue-components` + `unplugin-auto-import` 自动按需引入(配置见上文 vite.config.ts)。安装:

bashCode
npm install element-plus
npm install -D unplugin-vue-components unplugin-auto-import

配置完成后,组件无需手动 import,直接在模板里使用即可,打包时只会打入用到的组件与样式:

vueCode
<template>
  <el-form :model="form" label-width="80px">
    <el-form-item label="用户名">
      <el-input v-model="form.name" placeholder="请输入" />
    </el-form-item>
    <el-form-item>
      <el-button type="primary" @click="onSubmit">提交</el-button>
    </el-form-item>
  </el-form>
</template>

<script setup lang="ts">
import { reactive } from 'vue'
const form = reactive({ name: '' })
function onSubmit() {
  console.log('submit', form.name)
}
</script>

主流 UI 库对比

| UI 库 | 设计风格 | Vue 版本 | TS 支持 | 适用场景 |

| --- | --- | --- | --- | --- |

| Element Plus | 简洁企业风 | Vue 3 | 良好 | 中后台管理系统(国内主流) |

| Ant Design Vue | 企业级严谨 | Vue 3 | 良好 | 复杂中后台 |

| Naive UI | 现代简约 | Vue 3 | 优秀(TS 编写) | 追求 TS 体验的项目 |

| Vuetify | Material Design | Vue 2/3 | 良好 | Material 风格产品 |

| Quasar | 跨端一体 | Vue 3 | 良好 | 一套代码多端(Web/PWA/移动/桌面) |

VueUse —— 组合式工具函数库

VueUse 提供了 200 多个开箱即用的 Composable,覆盖鼠标、键盘、网络、存储、时间等场景,能大幅减少重复逻辑。

bashCode
npm install @vueuse/core
vueCode
<script setup lang="ts">
import {
  useMouse,
  useLocalStorage,
  useDark,
  useToggle,
  useDebounceFn,
  useWindowSize,
  onClickOutside
} from '@vueuse/core'
import { ref } from 'vue'

// 响应式鼠标坐标
const { x, y } = useMouse()

// 自动同步到 localStorage 的响应式变量
const name = useLocalStorage('user-name', '游客')

// 暗黑模式切换
const isDark = useDark()
const toggleDark = useToggle(isDark)

// 响应式窗口尺寸
const { width, height } = useWindowSize()

// 防抖函数
const onSearch = useDebounceFn((keyword: string) => {
  console.log('搜索:', keyword)
}, 300)

// 点击元素外部关闭弹层
const modal = ref<HTMLElement | null>(null)
onClickOutside(modal, () => console.log('点击了外部'))
</script>

<template>
  <div>
    <p>鼠标位置:{{ x }}, {{ y }}</p>
    <p>窗口尺寸:{{ width }} x {{ height }}</p>
    <input v-model="name" placeholder="名字会被自动记住" />
    <button @click="toggleDark()">切换主题(当前:{{ isDark ? '暗' : '亮' }})</button>
  </div>
</template>

测试 —— Vitest + Vue Test Utils

Vitest 是与 Vite 深度集成的测试框架,API 兼容 Jest,速度快、配置少。配合 Vue Test Utils 可测试组件。

bashCode
npm install -D vitest @vue/test-utils jsdom @vitest/coverage-v8
typescriptCode
// vitest.config.ts
import { defineConfig } from 'vitest/config'
import vue from '@vitejs/plugin-vue'
import { resolve } from 'path'

export default defineConfig({
  plugins: [vue()],
  resolve: {
    alias: { '@': resolve(__dirname, 'src') }
  },
  test: {
    globals: true,
    environment: 'jsdom',
    coverage: {
      provider: 'v8',
      reporter: ['text', 'html'],
      thresholds: { lines: 80, functions: 80, branches: 75 }
    }
  }
})

一个被测组件与它的测试:

vueCode
<!-- src/components/Counter.vue -->
<script setup lang="ts">
import { ref } from 'vue'
const props = defineProps<{ initial?: number }>()
const count = ref(props.initial ?? 0)
const emit = defineEmits<{ change: [value: number] }>()
function increment() {
  count.value++
  emit('change', count.value)
}
</script>

<template>
  <button @click="increment">count is {{ count }}</button>
</template>
typescriptCode
// src/components/__tests__/Counter.spec.ts
import { describe, it, expect } from 'vitest'
import { mount } from '@vue/test-utils'
import Counter from '../Counter.vue'

describe('Counter', () => {
  it('渲染初始值', () => {
    const wrapper = mount(Counter, { props: { initial: 5 } })
    expect(wrapper.text()).toContain('count is 5')
  })

  it('点击后递增并触发 change 事件', async () => {
    const wrapper = mount(Counter, { props: { initial: 0 } })
    await wrapper.find('button').trigger('click')
    expect(wrapper.text()).toContain('count is 1')
    expect(wrapper.emitted('change')?.[0]).toEqual([1])
  })
})

在 `package.json` 中加脚本:

jsonCode
{
  "scripts": {
    "test": "vitest",
    "test:run": "vitest run",
    "test:coverage": "vitest run --coverage"
  }
}

代码规范 —— ESLint + Prettier

统一代码风格是团队协作的基础。Vue 3 + TS 项目常用配置:

bashCode
npm install -D eslint eslint-plugin-vue @vue/eslint-config-typescript \
  @vue/eslint-config-prettier prettier
javascriptCode
// .eslintrc.cjs
module.exports = {
  root: true,
  env: { browser: true, node: true, es2022: true },
  extends: [
    'plugin:vue/vue3-recommended',
    'eslint:recommended',
    '@vue/eslint-config-typescript',
    '@vue/eslint-config-prettier'
  ],
  parserOptions: { ecmaVersion: 'latest', sourceType: 'module' },
  rules: {
    'vue/multi-word-component-names': 'off',
    'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
    '@typescript-eslint/no-explicit-any': 'warn'
  }
}
jsonCode
// .prettierrc.json
{
  "semi": false,
  "singleQuote": true,
  "printWidth": 100,
  "trailingComma": "none",
  "arrowParens": "always"
}

配合 husky + lint-staged 做提交前校验:

jsonCode
// package.json 片段
{
  "lint-staged": {
    "*.{js,ts,vue}": ["eslint --fix", "prettier --write"]
  }
}

构建产物分析

上线前分析产物体积,找出大依赖并优化,是性能治理的重要一步。

bashCode
npm install -D rollup-plugin-visualizer
typescriptCode
// vite.config.ts 中加入
import { visualizer } from 'rollup-plugin-visualizer'

export default defineConfig({
  plugins: [
    vue(),
    // 构建后自动打开体积可视化报告
    visualizer({ open: true, gzipSize: true, brotliSize: true })
  ]
})

常见优化手段:路由懒加载、第三方库 CDN 外链(`build.rollupOptions.external`)、manualChunks 拆包、大组件动态导入、图片压缩与 WebP、开启 gzip/brotli。

部署

Vercel

Vercel 对前端项目零配置友好,通常识别到 Vite 项目会自动构建。可用 `vercel.json` 精细控制路由与缓存:

jsonCode
{
  "buildCommand": "npm run build",
  "outputDirectory": "dist",
  "rewrites": [
    { "source": "/(.*)", "destination": "/index.html" }
  ],
  "headers": [
    {
      "source": "/assets/(.*)",
      "headers": [
        { "key": "Cache-Control", "value": "public, max-age=31536000, immutable" }
      ]
    }
  ]
}

注意 `rewrites` 把所有路径重写到 `index.html`,这是 SPA(history 模式路由)刷新不 404 的关键。

Netlify

Netlify 用 `netlify.toml` 配置:

tomlCode
[build]
  command = "npm run build"
  publish = "dist"

[[redirects]]
  from = "/*"
  to = "/index.html"
  status = 200

[[headers]]
  for = "/assets/*"
  [headers.values]
    Cache-Control = "public, max-age=31536000, immutable"

GitHub Pages(GitHub Actions 自动部署)

yamlCode
# .github/workflows/deploy.yml
name: Deploy to GitHub Pages
on:
  push:
    branches: [main]
jobs:
  build-and-deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: npm
      - run: npm ci
      - run: npm run build
      - uses: peaceiris/actions-gh-pages@v3
        with:
          github_token: ${{ secrets.GITHUB_TOKEN }}
          publish_dir: ./dist

部署到 GitHub Pages 子路径时,别忘了在 `vite.config.ts` 设置 `base: '/仓库名/'`。

真实案例:从零搭建一个中后台项目

某团队搭建内部管理后台,技术选型与落地过程如下:

1.脚手架:`npm create vite@latest admin -- --template vue-ts`,用 Vite 拿到秒级冷启动。
2.路由:Vue Router 4,所有页面懒加载,登录守卫 + 动态路由实现基于角色的菜单权限。
3.状态:Pinia 管理用户信息、权限、全局设置,用户设置通过 persist 插件持久化。
4.请求:Axios 统一封装,拦截器处理 token 注入、401 登出、错误提示。
5.UI:Element Plus 按需引入,首屏 JS 从全量的约 900KB 降到约 300KB(gzip 后)。
6.工具:VueUse 处理暗黑模式、窗口尺寸、防抖搜索等,减少约 40% 自研工具代码。
7.测试:核心表单与工具函数用 Vitest 覆盖,覆盖率维持在 80% 以上。
8.规范:ESLint + Prettier + husky,提交前自动修复,杜绝风格争论。
9.部署:GitHub Actions 构建后推到内部 Nginx,产物开启 brotli 与长期缓存。

上线后,开发平均保存-刷新反馈时间从 Webpack 时代的约 2 秒降到约 0.1 秒,日常迭代效率显著提升。

常见坑

| 坑 | 现象 | 解决方案 |

| --- | --- | --- |

| storeToRefs 忘记用 | 解构 Pinia state 后不再响应 | state 用 storeToRefs 解构,方法可直接解构 |

| SPA 部署刷新 404 | history 路由子页面刷新报 404 | 服务器配置 fallback 到 index.html(rewrites/redirects) |

| GitHub Pages 资源 404 | 部署后 JS/CSS 路径错误 | vite.config.ts 设置正确的 base |

| 环境变量取不到 | import.meta.env 拿到 undefined | 变量必须以 VITE_ 开头才会暴露 |

| Element Plus 全量引入 | 打包体积过大 | 用 unplugin 自动按需引入 |

| 代理不生效 | 请求仍打到前端端口 | 检查 proxy 的 target 与 rewrite 规则 |

| 拦截器里用 Pinia 报错 | 在 pinia 初始化前调用 store | 在函数内部调用 useXxxStore(),而非模块顶层 |

| manualChunks 拆过碎 | HTTP 请求数暴增 | 合理合并公共依赖,避免过度拆分 |

最佳实践

新项目一律用 Vite,享受秒级冷启动与快速 HMR。
全站路由懒加载,配合 manualChunks 优化首屏与缓存。
状态管理用 Pinia,优先 setup store 写法,解构记得 storeToRefs。
HTTP 请求统一封装拦截器,业务按模块组织 API 并带上类型。
UI 库一律按需引入,上线前用 visualizer 分析产物体积。
善用 VueUse,避免重复造轮子。
核心逻辑用 Vitest 覆盖,把覆盖率纳入 CI 门槛。
强制 ESLint + Prettier + 提交钩子,保证团队风格统一。
部署配置好 SPA fallback、正确 base、静态资源长期缓存。
持续关注生态更新(Vite、Vue Router、Pinia 大版本变化)。

总结

| 层次 | 推荐方案 | 一句话定位 |

| --- | --- | --- |

| 构建工具 | Vite | 秒级冷启动,现代 Vue 首选 |

| 路由 | Vue Router 4 | 懒加载 + 守卫 + 动态路由 |

| 状态管理 | Pinia | 轻量、TS 友好、无 mutations |

| HTTP | Axios(封装) | 拦截器统一处理鉴权与错误 |

| UI 库 | Element Plus / Naive UI | 按需引入控制体积 |

| 工具函数 | VueUse | 200+ 开箱即用 Composable |

| 测试 | Vitest + Vue Test Utils | 与 Vite 同源,快 |

| 代码规范 | ESLint + Prettier + husky | 统一风格,提交前校验 |

| 部署 | Vercel / Netlify / GH Pages | 注意 SPA fallback 与缓存 |

掌握这套工具链的协作方式,就能从「会写 Vue 组件」进阶到「能独立搭建并交付一个工程化的 Vue 项目」。生态在快速演进,但「快构建、清晰分层、规范协作、可测可部署」的核心原则始终不变。