从 0 到 1 搭建 Vue3+Vite 工程化项目:含路由、状态管理、按需引入

从 0 到 1 搭建 Vue3+Vite 工程化项目:含路由、状态管理、按需引入


目标

  • 快速脚手架:TypeScript + Vite + Vue3 + Pinia + Vue Router
  • 工程化:ESLint/Prettier、Husky/lint-staged、环境变量、按需引入与自动导入
  • 质量与性能:Vitest 单测、构建分包与路由懒加载

初始化与目录结构

bash 复制代码
# 初始化(推荐 pnpm)
pnpm create vite@latest my-app -- --template vue-ts
cd my-app
pnpm i

# 工程化依赖
pnpm i -D eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin eslint-plugin-vue prettier eslint-config-prettier eslint-plugin-prettier
pnpm i -D husky lint-staged
pnpm i pinia vue-router
pnpm i -D unplugin-auto-import unplugin-vue-components @element-plus/icons-vue
pnpm i element-plus
pnpm i -D vitest @vitest/coverage-v8 jsdom

目录建议:

复制代码
my-app/
  src/
    assets/
    components/
    pages/
      Home.vue
      About.vue
    stores/
      user.ts
    router/
      index.ts
    styles/
      index.css
    App.vue
    main.ts
  env.d.ts
  index.html
  tsconfig.json
  vite.config.ts
  .eslintrc.cjs
  .prettierrc.json
  .husky/
  package.json

TypeScript 与 ESLint/Prettier

tsconfig.json

json 复制代码
{
  "compilerOptions": {
    "target": "ES2020",
    "useDefineForClassFields": true,
    "module": "ESNext",
    "moduleResolution": "Bundler",
    "strict": true,
    "jsx": "preserve",
    "baseUrl": ".",
    "paths": { "@/*": ["src/*"] },
    "types": ["vite/client", "jsdom"]
  }
}

.eslintrc.cjs

js 复制代码
module.exports = {
  root: true,
  env: { browser: true, es2021: true, node: true },
  extends: [
    'plugin:vue/vue3-recommended',
    'plugin:@typescript-eslint/recommended',
    'prettier'
  ],
  parserOptions: { ecmaVersion: 'latest', sourceType: 'module' },
  rules: { 'vue/multi-word-component-names': 0 }
}

.prettierrc.json

json 复制代码
{ "semi": false, "singleQuote": true, "printWidth": 100 }

Husky 与 lint-staged

bash 复制代码
npx husky init

package.json

json 复制代码
{
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "preview": "vite preview",
    "test": "vitest",
    "lint": "eslint src --ext .ts,.vue",
    "format": "prettier --write ."
  },
  "lint-staged": {
    "src/**/*.{ts,vue,css}": ["eslint --fix", "prettier --write"]
  }
}

添加钩子:

bash 复制代码
echo 'npx lint-staged' > .husky/pre-commit

Vite 配置与按需引入

vite.config.ts

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({
      imports: ['vue', 'vue-router', 'pinia'],
      resolvers: [ElementPlusResolver()],
      dts: 'src/auto-imports.d.ts'
    }),
    Components({
      resolvers: [ElementPlusResolver()],
      dts: 'src/components.d.ts'
    })
  ],
  resolve: { alias: { '@': '/src' } },
  server: {
    proxy: {
      '/api': {
        target: process.env.VITE_API_BASE || 'http://localhost:3000',
        changeOrigin: true,
        rewrite: p => p.replace(/^\/api/, '')
      }
    }
  },
  build: {
    rollupOptions: {
      output: {
        manualChunks: {
          vue: ['vue', 'vue-router', 'pinia'],
          ui: ['element-plus']
        }
      }
    }
  }
})

说明:

  • AutoImport/Components 自动导入 API 与组件,Element Plus 按需引入免手配
  • manualChunks 分包:框架与 UI 独立,提高缓存与首屏加载

路由(懒加载)

src/router/index.ts

ts 复制代码
import { createRouter, createWebHistory } from 'vue-router'
import { useUserStore } from '@/stores/user'

const routes = [
  { path: '/', name: 'home', component: () => import('@/pages/Home.vue') },
  { path: '/about', name: 'about', component: () => import('@/pages/About.vue') },
  { path: '/dashboard', name: 'dashboard', meta: { requiresAuth: true }, component: () => import('@/pages/Dashboard.vue') }
]

export const router = createRouter({ history: createWebHistory(), routes })

router.beforeEach((to) => {
  const user = useUserStore()
  if (to.meta.requiresAuth && !user.token) return { name: 'home' }
})

src/main.ts

ts 复制代码
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import { router } from '@/router'
import App from './App.vue'

createApp(App).use(createPinia()).use(router).mount('#app')

状态管理(Pinia)

src/stores/user.ts

ts 复制代码
import { defineStore } from 'pinia'

export const useUserStore = defineStore('user', {
  state: () => ({ name: 'Alice', token: '' }),
  getters: { isLogin: (s) => !!s.token },
  actions: {
    login(name: string) { this.name = name; this.token = 'token-' + Date.now() },
    logout() { this.token = '' }
  }
})

持久化与会话恢复:

ts 复制代码
import { watch } from 'vue'
import { useUserStore } from '@/stores/user'
const store = useUserStore()
const saved = localStorage.getItem('user')
if (saved) Object.assign(store, JSON.parse(saved))
watch(() => ({ name: store.name, token: store.token }), (v) => localStorage.setItem('user', JSON.stringify(v)), { deep: true })

页面使用:

vue 复制代码
<script setup lang="ts">
import { useUserStore } from '@/stores/user'
const user = useUserStore()
</script>
<template>
  <div>Hi, {{ user.name }} <el-button @click="user.login('Bob')">Login</el-button></div>
</template>

页面示例

src/pages/Home.vue

vue 复制代码
<script setup lang="ts">
import { ref } from 'vue'
const count = ref(0)
</script>
<template>
  <h1>Home</h1>
  <el-button type="primary" @click="count++">Count: {{ count }}</el-button>
  <router-link to="/about">About</router-link>
</template>

src/pages/About.vue

vue 复制代码
<template>
  <h1>About</h1>
  <el-alert title="Vue3 + Vite 工程化" type="success" />
</template>

环境变量与配置

.env.development

复制代码
VITE_API_BASE=https://api.dev.example.com

src/env.d.ts

ts 复制代码
interface ImportMetaEnv { VITE_API_BASE: string }
interface ImportMeta { readonly env: ImportMetaEnv }

HTTP 封装:

ts 复制代码
export const API = import.meta.env.VITE_API_BASE

src/services/http.ts

ts 复制代码
import axios from 'axios'
import { useUserStore } from '@/stores/user'

export const http = axios.create({ baseURL: import.meta.env.VITE_API_BASE, timeout: 10000 })

http.interceptors.request.use((config) => {
  const user = useUserStore()
  if (user.token) config.headers.Authorization = `Bearer ${user.token}`
  return config
})

http.interceptors.response.use(
  (res) => res.data,
  (err) => Promise.reject(err)
)

单元测试(Vitest)

vitest.config.ts(可选):

ts 复制代码
import { defineConfig } from 'vitest/config'
import vue from '@vitejs/plugin-vue'
export default defineConfig({ plugins: [vue()], test: { environment: 'jsdom', coverage: { provider: 'v8' } } })

示例测试:

ts 复制代码
import { describe, it, expect } from 'vitest'
describe('sum', () => { it('works', () => { expect(1 + 2).toBe(3) }) })

组件与 Store 测试:

ts 复制代码
import { describe, it, expect } from 'vitest'
import { setActivePinia, createPinia } from 'pinia'
import { useUserStore } from '@/stores/user'

describe('user store', () => {
  setActivePinia(createPinia())
  it('login/logout', () => {
    const s = useUserStore()
    s.login('Bob')
    expect(s.isLogin).toBe(true)
    s.logout()
    expect(s.isLogin).toBe(false)
  })
})

性能与工程建议

  • 路由懒加载 + manualChunks 分包
  • AutoImport/Components 实现按需与自动导入,减少手工维护
  • 统一脚本:lint/test/build 在 CI 中执行,保护主干
  • 使用 CDN 加速静态资源,图片与字体采用现代格式

CI(GitHub Actions):

yaml 复制代码
name: CI
on: { push: { branches: [main] }, pull_request: { branches: [main] } }
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: '20', cache: 'pnpm' }
      - run: corepack enable
      - run: pnpm i --frozen-lockfile
      - run: pnpm lint
      - run: pnpm test -- --coverage
      - run: pnpm build

常见问题

  • 组件库样式未加载:Element Plus 需全局样式,Vite 自动导入已内置;若需手动样式可引入 import 'element-plus/theme-chalk/src/index.scss'
  • 类型提示缺失:检查 env.d.ts、AutoImport/Components 的 dts 文件是否生成在 src/
  • 路由 404:确认 createWebHistory 下的部署服务已转发到 index.html

排错清单:

  • AutoImport 未生效:确认 src/auto-imports.d.ts 存在,重启 vite 后生效
  • 代理未转发:校验 server.proxyrewrite 与目标地址,浏览器请求是否命中 /api
  • Pinia 未注入:确认在 main.ts 调用了 use(createPinia())

文件式路由(import.meta.glob)

ts 复制代码
// src/router/index.ts(替代手写 routes)
import { createRouter, createWebHistory } from 'vue-router'
const pages = import.meta.glob('@/pages/**/*.vue')
const routes = Object.keys(pages).map((path) => {
  const name = path.replace('/src/pages/', '').replace('.vue', '')
  return { path: '/' + name.toLowerCase(), name, component: pages[path] }
})
export const router = createRouter({ history: createWebHistory(), routes })

说明:无需手工维护 routes,新增页面即自动加入;可为首页与特殊路由保留手写项并合并。

CSS 预处理与主题定制

ts 复制代码
// vite.config.ts 片段
export default defineConfig({
  css: {
    preprocessorOptions: {
      scss: { additionalData: `@use "@/styles/variables" as *;` }
    }
  }
})
scss 复制代码
// src/styles/variables.scss
$primary: #0ea5e9;

Element Plus 主题变量(可选):

scss 复制代码
// src/styles/ep-theme.scss
@use 'element-plus/theme-chalk/src/common/var.scss' as *;
$colors: (
  'primary': (
    'base': #0ea5e9
  )
);

国际化(vue-i18n)

bash 复制代码
pnpm i vue-i18n
ts 复制代码
// src/i18n.ts
import { createI18n } from 'vue-i18n'
export const i18n = createI18n({ legacy: false, locale: 'zh', messages: { zh: { hello: '你好' }, en: { hello: 'Hello' } } })
ts 复制代码
// src/main.ts
import { i18n } from '@/i18n'
createApp(App).use(createPinia()).use(router).use(i18n).mount('#app')
vue 复制代码
<template>
  <div>{{ $t('hello') }}</div>
  <el-select v-model="$i18n.locale"><el-option value="zh" label="中文"/><el-option value="en" label="English"/></el-select>
  </template>

组件测试(@vue/test-utils)

bash 复制代码
pnpm i -D @vue/test-utils
ts 复制代码
// src/pages/__tests__/Home.test.ts
import { mount } from '@vue/test-utils'
import Home from '../Home.vue'
test('click increments', async () => {
  const w = mount(Home)
  await w.get('button').trigger('click')
  expect(w.html()).toContain('Count: 1')
})

部署:Docker + Nginx(可选)

Dockerfile 复制代码
FROM node:20-alpine AS build
WORKDIR /app
COPY . .
RUN corepack enable && pnpm i --frozen-lockfile && pnpm build

FROM nginx:alpine
COPY --from=build /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
nginx 复制代码
# nginx.conf(history 路由)
server { listen 80; server_name _; root /usr/share/nginx/html; index index.html;
  location / { try_files $uri $uri/ /index.html; }
  location /api { proxy_pass http://backend:3000; }
}

提交规范与版本治理(可选)

bash 复制代码
pnpm i -D @commitlint/cli @commitlint/config-conventional
echo "module.exports = { extends: ['@commitlint/config-conventional'] }" > commitlint.config.cjs
echo 'npx commitlint --edit "$1"' > .husky/commit-msg

示例提交信息:feat(router): 文件式路由支持 import.meta.glob

依赖预构建与性能微调

ts 复制代码
// vite.config.ts 片段
export default defineConfig({
  optimizeDeps: { include: ['axios', 'pinia', 'vue-i18n'] },
  build: { minify: 'esbuild', sourcemap: false }
})

说明:预构建加快冷启动,生产构建关闭 source map,减小包体;结合路由懒加载优化首屏。

多环境与模式

env 复制代码
VITE_API_BASE=https://api.staging.example.com
env 复制代码
VITE_API_BASE=https://api.example.com
ts 复制代码
const mode = import.meta.env.MODE
const base = import.meta.env.VITE_API_BASE

错误处理与日志上报

ts 复制代码
import { createApp } from 'vue'
import App from './App.vue'
const app = createApp(App)
app.config.errorHandler = (err) => {
  console.error(err)
}
app.mount('#app')

PWA 支持

bash 复制代码
pnpm i -D vite-plugin-pwa
ts 复制代码
import { VitePWA } from 'vite-plugin-pwa'
export default defineConfig({
  plugins: [VitePWA({ registerType: 'autoUpdate', manifest: { name: 'App', short_name: 'App' } })]
})

打包分析与预算

bash 复制代码
pnpm i -D rollup-plugin-visualizer
ts 复制代码
import { visualizer } from 'rollup-plugin-visualizer'
export default defineConfig({
  build: { rollupOptions: { plugins: [visualizer({ filename: 'stats.html' })] } }
})

性能预算建议:控制首屏路由包体、第三方依赖体积与图片资源体积,逐路由分析与优化。

动态菜单与权限

ts 复制代码
import { router } from '@/router'
const menus = router.getRoutes().filter(r => !r.meta?.hidden).map(r => ({ path: r.path, name: r.name }))

组件懒加载

ts 复制代码
import { defineAsyncComponent } from 'vue'
const AsyncChart = defineAsyncComponent(() => import('@/components/Chart.vue'))

E2E 测试(Playwright)

bash 复制代码
pnpm i -D @playwright/test
ts 复制代码
import { test, expect } from '@playwright/test'
test('home counter', async ({ page }) => {
  await page.goto('http://localhost:5173')
  await page.click('text=Count')
  const content = await page.textContent('text=Count:')
  expect(content).toContain('1')
})

总结

  • 从脚手架到工程化关键组件(路由、状态、按需引入)一套打通
  • 通过自动导入与分包优化,降低维护成本、提升首屏与复用效率
  • 配合质量与性能守护(Lint、Test、Coverage),项目可进入持续交付与迭代
相关推荐
一字白首36 分钟前
Node.js 入门,Webpack 核心实战:从概念到打包全流程
前端·webpack·node.js
q***160836 分钟前
【前端】Node.js使用教程
前端·node.js·vim
jjw_zyfx39 分钟前
vue3 vite element根据自定义数据实现离散滑块
javascript·vue.js·ecmascript
北极糊的狐41 分钟前
Vue3 中页面重定向的方式
前端·javascript·vue.js
灵魂学者41 分钟前
Vue3.x 高阶 —— 组合式API
前端·javascript·vue.js
谷歌开发者42 分钟前
Web 开发指向标|在来源面板中使用 Chrome 开发者工具的 AI 辅助功能
前端·人工智能·chrome
小毛驴85043 分钟前
npm 代理配置
前端·npm·node.js
唐古乌梁海44 分钟前
【AJAX】AJAX详解
前端·ajax·okhttp