Vue 3 实战:从项目搭建到部署上线

2024-09-09 前端

一、项目初始化

1.1 使用 create-vue 创建项目

Vue 官方推荐使用 create-vue 脚手架来初始化项目,它基于 Vite 构建,支持 TypeScript、JSX、Router、Pinia、Vitest 等一系列现代前端工具链的一键集成。

# 全局安装 create-vue(如果尚未安装)
npm create vue@latest

# 或者使用 pnpm(推荐)
pnpm create vue@latest

执行命令后,交互式选择配置项:

✔ Project name: … vue3-admin-dashboard
✔ Add TypeScript? … Yes
✔ Add JSX Support? … Yes
✔ Add Vue Router for Single Page Application development? … Yes
✔ Add Pinia for state management? … Yes
✔ Add Vitest for Unit Testing? … Yes
✔ Add an End-to-End Testing Solution? › Playwright
✔ Add ESLint for code quality? … Yes
✔ Add Prettier for code formatting? … Yes

进入项目并启动开发服务器:

cd vue3-admin-dashboard
pnpm install
pnpm dev

此时浏览器访问 http://localhost:5173 即可看到 Vite + Vue 3 的欢迎页面。

1.2 工程目录结构设计

一个结构清晰、约定明确的目录结构是大型项目可持续维护的基石。以下是我推荐的实战级目录结构:

设计原则:

  • 按功能模块划分 API:每个业务模块一个文件,避免单个 API 文件膨胀到上千行
  • 组件分层:components/common/ 放通用组件,components/ 下放业务组件
  • Composables 集中管理:所有组合式函数统一放在 composables/ 目录
  • 类型定义独立:types/ 目录集中管理全局类型,按模块分文件

1.3 代码规范:ESLint + Prettier + Husky + lint-staged

ESLint + Prettier 配置

create-vue 已经集成了 ESLint 和 Prettier 的基础配置,我们在此基础上做微调:

// .eslintrc.cjs
module.exports = {
  root: true,
  env: { browser: true, es2020: true, node: true },
  extends: [
    'eslint:recommended',
    'plugin:@typescript-eslint/recommended',
    'plugin:vue/vue3-recommended',
    'prettier', // 必须放在最后,覆盖之前的格式规则
  ],
  parser: 'vue-eslint-parser',
  parserOptions: {
    parser: '@typescript-eslint/parser',
    ecmaVersion: 'latest',
    sourceType: 'module',
  },
  plugins: ['@typescript-eslint'],
  rules: {
    'vue/multi-word-component-names': 'off',
    '@typescript-eslint/no-explicit-any': 'warn',
    'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
    'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
  },
};
// .prettierrc.json
{
  "semi": true,
  "singleQuote": true,
  "tabWidth": 2,
  "trailingComma": "all",
  "printWidth": 100,
  "bracketSpacing": true,
  "arrowParens": "always",
  "endOfLine": "lf"
}

Husky + lint-staged 配置

在提交代码前自动执行 lint 检查和格式化,防止不规范代码进入仓库:

# 安装 husky 和 lint-staged
pnpm add -D husky lint-staged

# 初始化 husky
npx husky init

# 添加 pre-commit 钩子
echo 'npx lint-staged' > .husky/pre-commit

# 使钩子可执行
chmod +x .husky/pre-commit
// package.json 中配置 lint-staged
{
  "lint-staged": {
    "*.{js,ts,vue,jsx,tsx}": [
      "eslint --fix",
      "prettier --write"
    ],
    "*.{css,scss,json,md}": [
      "prettier --write"
    ]
  }
}

这样配置后,每次 git commit 时都会自动对暂存区文件执行 lint 检查和格式化,只有通过检查的代码才能提交。

1.4 包管理器选型(pnpm)

为什么选择 pnpm 而非 npm 或 yarn?

  • 磁盘空间节省:pnpm 使用硬链接 + 符号链接,相同版本的依赖只存储一份
  • 安装速度:并行安装,比 npm 快 2-3 倍
  • 严格性:node_modules 目录结构严格,防止非法访问未声明的包
  • Monorepo 支持:内置 workspace 协议,天然支持 monorepo
# 全局安装 pnpm
npm install -g pnpm

# 常用命令
pnpm install        # 安装所有依赖
pnpm add vue        # 安装运行时依赖
pnpm add -D vitest  # 安装开发依赖
pnpm dev            # 启动开发服务器
pnpm build          # 构建生产版本
pnpm upgrade        # 升级依赖

1.5 TypeScript 配置

合理的 TypeScript 配置能让开发体验更流畅:

// tsconfig.json
{
  "compilerOptions": {
    "target": "ES2020",
    "useDefineForEmit": true,
    "module": "ESNext",
    "lib": ["ES2020", "DOM", "DOM.Iterable"],
    "skipLibCheck": true,
    "moduleResolution": "bundler",
    "allowImportingTsExtensions": true,
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,
    "jsx": "preserve",
    "strict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noFallthroughCasesInSwitch": true,
    "paths": {
      "@/*": ["./src/*"]
    },
    "types": ["vite/client"]
  },
  "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue", "env.d.ts"],
  "references": [{ "path": "./tsconfig.node.json" }]
}
// tsconfig.node.json
{
  "compilerOptions": {
    "composite": true,
    "skipLibCheck": true,
    "module": "ESNext",
    "moduleResolution": "bundler",
    "allowSyntheticDefaultImports": true
  },
  "include": ["vite.config.ts"]
}

1.6 路径别名配置

配置 @ 指向 src/ 目录,避免深层嵌套的 ../../../ 地狱。

Vite 侧配置(vite.config.ts):

import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import { resolve } from 'path';

export default defineConfig({
  plugins: [vue()],
  resolve: {
    alias: {
      '@': resolve(__dirname, 'src'),
    },
  },
});

TypeScript 侧配置(已在 tsconfig.json 中配置 paths):

{
  "compilerOptions": {
    "paths": {
      "@/*": ["./src/*"]
    }
  }
}

使用示例:

// 之前:import { formatDate } from '../../../utils/format'
// 现在:
import { formatDate } from '@/utils/format';
import { useAuthStore } from '@/stores/auth';
import ArticleList from '@/views/articles/ArticleList.vue';

二、项目架构设计

2.1 组件目录组织:按功能 vs 按类型

对于中等以上规模的项目(>20 个页面),推荐 先按功能模块分层,再按类型细分 的组织方式:

components/
├── common/          # 跨业务通用的基础组件
│   ├── DataTable.vue
│   ├── FormModal.vue
│   └── Pagination.vue
├── business/        # 业务相关组件
│   ├── article/     # 文章模块组件
│   │   ├── ArticleCard.vue
│   │   └── ArticleFilter.vue
│   └── user/        # 用户模块组件
│       ├── UserAvatar.vue
│       └── UserDropdown.vue
└── layout/          # 布局组件
    ├── Sidebar.vue
    ├── Header.vue
    └── MainLayout.vue

原则:

  • 通用组件(按钮、表格、弹窗)放 common/,全局注册或按需引入
  • 业务组件按模块放在 business/{module}/ 目录下
  • 布局组件独立成 layout/,与业务解耦

2.2 API 层封装

API 层是所有前端请求的入口,一个好的封装能统一处理鉴权、错误、请求取消等横切关注点。

axios 实例与拦截器

API 响应类型

// src/api/types.ts
/** 通用分页参数 */
export interface PaginationParams {
  page: number;
  pageSize: number;
}

/** 通用列表响应 */
export interface ListResponse<T> {
  list: T[];
  total: number;
  page: number;
  pageSize: number;
}

/** 通用 API 响应 */
export interface ApiResponse<T = unknown> {
  code: number;
  data: T;
  message: string;
}

按模块划分的 API 文件

2.3 Store 层设计

使用 Pinia 管理全局状态,按业务模块拆分 Store:

2.4 路由配置与权限控制

路由定义

路由守卫

在 main.ts 中调用守卫安装:

// src/main.ts
import { createApp } from 'vue';
import { createPinia } from 'pinia';
import App from './App.vue';
import router from './router';
import { setupGuards } from './router/guards';
import './assets/styles/global.scss';

const app = createApp(App);
const pinia = createPinia();

app.use(pinia);
app.use(router);

// 安装路由守卫
setupGuards(router);

app.mount('#app');

2.5 工具函数目录

// src/utils/storage.ts
export const ACCESS_TOKEN_KEY = 'access_token';
export const REFRESH_TOKEN_KEY = 'refresh_token';

export function getStorageItem<T = string>(key: string): T | null {
  try {
    const item = localStorage.getItem(key);
    return item ? (JSON.parse(item) as T) : null;
  } catch {
    return localStorage.getItem(key) as T | null;
  }
}

export function setStorageItem<T>(key: string, value: T): void {
  try {
    localStorage.setItem(key, JSON.stringify(value));
  } catch (e) {
    console.error('Storage set error:', e);
  }
}

export function removeStorageItem(key: string): void {
  localStorage.removeItem(key);
}
// src/utils/format.ts
export function formatDate(date: string | Date, format = 'YYYY-MM-DD HH:mm:ss'): string {
  const d = new Date(date);
  const map: Record<string, number> = {
    YYYY: d.getFullYear(),
    MM: d.getMonth() + 1,
    DD: d.getDate(),
    HH: d.getHours(),
    mm: d.getMinutes(),
    ss: d.getSeconds(),
  };
  return format.replace(/YYYY|MM|DD|HH|mm|ss/g, (match) =>
    String(map[match]).padStart(2, '0'),
  );
}

export function truncate(text: string, length = 100): string {
  if (text.length <= length) return text;
  return text.slice(0, length) + '...';
}
// src/utils/validate.ts
export function isValidEmail(email: string): boolean {
  return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
}

export function isValidPassword(password: string): boolean {
  // 最少 8 位,包含大小写字母和数字
  return /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{8,32}$/.test(password);
}

export function isValidUrl(url: string): boolean {
  try {
    new URL(url);
    return true;
  } catch {
    return false;
  }
}

2.6 类型定义管理

// src/types/article.ts
export type ArticleStatus = 'draft' | 'published';

export interface ArticleMeta {
  views: number;
  likes: number;
  comments: number;
}

export interface ArticleCategory {
  id: number;
  name: string;
  description?: string;
}

export interface ArticleTag {
  id: number;
  name: string;
  color?: string;
}
// src/types/user.ts
export type UserRole = 'admin' | 'editor' | 'viewer';

export interface UserProfile {
  id: number;
  username: string;
  email: string;
  avatar: string;
  role: UserRole;
  createdAt: string;
}

2.7 环境变量配置

Vite 使用 import.meta.env 访问环境变量,变量必须以 VITE_ 前缀开头才会暴露给前端代码:

# .env                      # 所有环境通用的变量
VITE_APP_TITLE=Vue 3 Admin
VITE_API_BASE_URL=/api

# .env.development          # 开发环境
VITE_API_BASE_URL=http://localhost:3000/api
VITE_APP_DEBUG=true

# .env.production           # 生产环境
VITE_API_BASE_URL=https://api.example.com
VITE_APP_DEBUG=false

在 Vue 组件或 TS 文件中使用:

const apiUrl = import.meta.env.VITE_API_BASE_URL;
const isDebug = import.meta.env.VITE_APP_DEBUG === 'true';

为环境变量提供 TypeScript 类型提示:

// env.d.ts 或 src/env.d.ts
/// <reference types="vite/client" />

interface ImportMetaEnv {
  readonly VITE_APP_TITLE: string;
  readonly VITE_API_BASE_URL: string;
  readonly VITE_APP_DEBUG: string;
}

interface ImportMeta {
  readonly env: ImportMetaEnv;
}

三、项目实战:文章管理后台

3.1 路由设计

项目路由分为三大类:

路径 页面 权限 说明
/login 登录页 公开 用户登录
/dashboard 仪表盘 登录 数据概览
/articles 文章列表 登录 文章 CRUD
/articles/create 创建文章 编辑以上 富文本编辑
/articles/:id/edit 编辑文章 编辑以上 富文本编辑
/settings 系统设置 管理员 站点配置
/:pathMatch(.*)* 404 公开 未匹配路由

3.2 布局组件

布局组件是后台管理系统的骨架,包含 Sidebar(侧边栏)、Header(顶部导航栏)和 MainLayout(布局容器)。

MainLayout

3.3 通用组件

DataTable 通用表格组件

Pagination 分页组件

FormModal 表单弹窗组件

3.4 登录功能:JWT 认证

登录页面

Token 存储与自动刷新

Token 的存储选择和自动刷新机制是后台系统安全性的关键:

  1. 存储位置:使用 localStorage 而非内存变量。刷新页面后 token 不丢失,用户体验更佳
  2. 双 Token 机制:Access Token(短期,15 分钟)和 Refresh Token(长期,7 天)
  3. 自动刷新:401 响应触发刷新逻辑,透明处理,用户无感知

关键代码已在前面 src/api/request.ts 的响应拦截器中实现。核心流程如下:

用户请求 → 返回 401
        ↓
是否正在刷新? → 是 → 排队等待
        ↓ 否
标记 _retry = true
发起 Refresh Token 请求
        ↓
成功 → 更新本地 Token → 执行队列中的请求 → 重试原始请求
失败 → 清空凭证 → 跳转登录页

3.5 文章 CRUD

文章列表页

文章编辑页(创建/编辑复用)

3.6 状态管理总结

我们使用三个 Pinia Store 来管理不同维度的状态:

Store 职责 状态
auth 用户认证 token、refreshToken、user 信息
articles 文章业务 文章列表、当前文章、分页参数、加载状态
app 应用全局UI 侧边栏折叠、暗色模式、主题色

设计原则:

  • 每个 Store 只关注一个业务领域
  • 异步操作(API 调用)放在 Store 的 action 中,组件只负责编排和展示
  • 利用 Pinia 的 $reset() 可在登出时重置所有状态

3.7 404 页面与错误边界

404 页面

错误边界(onErrorCaptured)

在 Vue 3 中,可以使用 onErrorCaptured 生命周期钩子在组件树中捕获错误:

<!-- src/components/common/ErrorBoundary.vue -->
<script setup lang="ts">
import { ref, onErrorCaptured } from 'vue';

const hasError = ref(false);
const errorMessage = ref('');

onErrorCaptured((err: Error) => {
  hasError.value = true;
  errorMessage.value = err.message || '组件发生未知错误';
  console.error('[ErrorBoundary]', err);
  // 阻止错误继续向上冒泡
  return false;
});

function resetError() {
  hasError.value = false;
  errorMessage.value = '';
}
</script>

<template>
  <div v-if="hasError" class="error-boundary">
    <p>{{ errorMessage }}</p>
    <button @click="resetError">重试</button>
  </div>
  <slot v-else />
</template>

在布局中使用:

<ErrorBoundary>
  <router-view />
</ErrorBoundary>

四、样式体系

4.1 CSS 变量主题系统

在 Vue 3 项目中,利用 CSS 自定义属性(CSS Variables)构建主题系统是最轻量高效的方式:

4.2 UnoCSS 配置(推荐替代 Tailwind CSS)

UnoCSS 是原子化 CSS 引擎,性能远超 Tailwind CSS,且兼容 Tailwind 的类名生态:

pnpm add -D unocss @unocss/preset-uno @unocss/transformer-directives
// vite.config.ts
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import UnoCSS from 'unocss/vite';

export default defineConfig({
  plugins: [
    vue(),
    UnoCSS(),   // ⚡ 零运行时原子化 CSS
  ],
});
// uno.config.ts
import { defineConfig, presetUno, presetAttributify } from 'unocss';

export default defineConfig({
  presets: [
    presetUno(),          // 默认预设(包含 Tailwind 兼容类)
    presetAttributify(),  // 属性化模式 <div text="center" />
  ],
  shortcuts: {
    'flex-center': 'flex items-center justify-center',
    'text-ellipsis': 'truncate',
    'card': 'bg-white rounded-lg shadow-sm p-6',
  },
  rules: [
    [/^text-(.*)$/, ([, c]) => ({ color: c })],
  ],
  theme: {
    colors: {
      primary: '#409eff',
    },
  },
});
// main.ts
import 'virtual:uno.css';

使用示例

<template>
  <!-- 使用 UnoCSS 原子类 -->
  <div class="flex items-center justify-between p-4 bg-white rounded-lg shadow-sm">
    <h1 class="text-lg font-bold text-gray-800">{{ title }}</h1>
    <button class="px-4 py-2 text-white bg-primary rounded hover:opacity-85">
      新建
    </button>
  </div>

  <!-- 属性化模式 -->
  <div flex="~ items-center" p="4" bg="white" rounded="lg" shadow="sm">
    <span text="lg" font="bold">Hello</span>
  </div>
</template>

4.3 暗色模式切换

暗色模式的核心是切换 html 标签上的 dark class。前面在 app Store 中已经实现了这一逻辑。CSS 变量会根据 .dark 选择器自动切换颜色值。

具体实现:

  • useAppStore().toggleDarkMode() 切换 darkMode 状态
  • watch 监听 darkMode,同步到 document.documentElement.classList
  • 所有组件样式使用 CSS 变量引用颜色,无需手动调整

4.4 响应式布局

使用 CSS 媒体查询 + CSS 变量实现响应式:

如果使用 UnoCSS,断点类名更加简洁:

<div class="grid grid-cols-4 lg:grid-cols-3 md:grid-cols-2 sm:grid-cols-1">

五、测试

5.1 Vitest 单元测试

Vitest 与 Vite 共享配置,零配置即可运行,速度比 Jest 快 10-20 倍。

pnpm add -D vitest @vue/test-utils jsdom
// vitest.config.ts
import { defineConfig } from 'vitest/config';
import vue from '@vitejs/plugin-vue';
import { resolve } from 'path';

export default defineConfig({
  plugins: [vue()],
  test: {
    environment: 'jsdom',
    globals: true,
    setupFiles: ['./src/test/setup.ts'],
    include: ['src/**/*.{test,spec}.{ts,tsx}'],
  },
  resolve: {
    alias: {
      '@': resolve(__dirname, 'src'),
    },
  },
});
// src/test/setup.ts
import '@testing-library/jest-dom';

测试工具函数

测试 Store

5.2 Vue Test Utils 组件测试

// src/components/common/__tests__/FormModal.test.ts
import { describe, it, expect } from 'vitest';
import { mount } from '@vue/test-utils';
import FormModal from '../FormModal.vue';

describe('FormModal', () => {
  it('renders when visible is true', () => {
    const wrapper = mount(FormModal, {
      props: { visible: true, title: '测试弹窗' },
      slots: { default: '表单内容' },
    });
    expect(wrapper.text()).toContain('测试弹窗');
    expect(wrapper.text()).toContain('表单内容');
  });

  it('does not render when visible is false', () => {
    const wrapper = mount(FormModal, {
      props: { visible: false, title: '测试弹窗' },
    });
    expect(wrapper.find('.modal-overlay').exists()).toBe(false);
  });

  it('emits close on overlay click', async () => {
    const wrapper = mount(FormModal, {
      props: { visible: true, title: '测试' },
    });
    await wrapper.find('.modal-overlay').trigger('click');
    expect(wrapper.emitted('close')).toBeTruthy();
  });
});

运行测试:

pnpm vitest run          # 单次运行
pnpm vitest              # 监听模式

5.3 Playwright E2E 测试

Playwright 是多浏览器端到端测试框架,支持 Chromium、Firefox、WebKit。

pnpm add -D @playwright/test
npx playwright install
// e2e/login.spec.ts
import { test, expect } from '@playwright/test';

test('user can log in and see dashboard', async ({ page }) => {
  await page.goto('/login');
  await page.fill('#username', 'admin');
  await page.fill('#password', 'Admin123!');
  await page.click('button[type="submit"]');
  await expect(page).toHaveURL(/\/dashboard/);
  await expect(page.locator('.dashboard')).toBeVisible();
});

test('redirects to login when not authenticated', async ({ page }) => {
  await page.goto('/articles');
  await expect(page).toHaveURL(/\/login/);
});

六、构建与部署

6.1 Vite 构建配置

6.2 代码分割(动态 import)

Vue Router 天然支持动态导入,按路由拆分包:

// 懒加载路由组件
const DashboardView = () => import('@/views/dashboard/DashboardView.vue');
const ArticleList = () => import('@/views/articles/ArticleList.vue');
const ArticleEdit = () => import('@/views/articles/ArticleEdit.vue');
const NotFound = () => import('@/views/error/NotFound.vue');

Vite 的 manualChunks 配置进一步将第三方库拆分为独立的 vendor 包,利用浏览器缓存机制,减少用户重复下载。

6.3 构建分析

rollup-plugin-visualizer 可以生成可视化的打包分析报告:

pnpm build  # 构建完成后会自动打开分析报告

报告以交互式饼图展示每个模块的体积,帮助你快速定位需要优化的部分。

运行构建:

pnpm build
# 输出示例:
# ✓ built in 3.2s
# dist/
# ├── index.html
# ├── static/
# │   ├── js/
# │   │   ├── index-[hash].js      (主入口)
# │   │   ├── vue-vendor-[hash].js  (Vue 生态)
# │   │   └── ArticleList-[hash].js (路由懒加载)
# │   └── css/
# │       └── index-[hash].css

6.4 Nginx 部署配置

6.5 Docker 部署

多阶段构建

# Dockerfile

# 第一阶段:构建
FROM node:20-alpine AS builder
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN corepack enable && pnpm install --frozen-lockfile
COPY . .
RUN pnpm build

# 第二阶段:运行
FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
# 构建镜像
docker build -t vue3-admin:latest .

# 运行容器
docker run -d \
  --name vue3-admin \
  -p 80:80 \
  -e VITE_API_BASE_URL=https://api.example.com \
  vue3-admin:latest

docker-compose.yml

6.6 CI/CD 流程

使用 GitHub Actions 实现自动化构建与部署:


七、性能优化

7.1 打包体积优化

策略 配置 效果
路由懒加载 () => import(...) 按需加载页面代码
第三方库拆分 manualChunks.vue-vendor 利用浏览器缓存
Tree Shaking 使用 ESM 导出 移除未使用的代码
移除调试代码 drop_console: true 减少生产包体积
图片压缩 使用 WebP 格式 + vite-imagetools 减少图片体积

分析工具:在 CI 中集成 rollup-plugin-visualizer,每次构建后自动生成体积报告,跟踪体积变化趋势。

7.2 首屏加载优化

1. 预加载关键资源

<!-- index.html 中添加预加载提示 -->
<link rel="preload" href="/static/js/vue-vendor-[hash].js" as="script" />
<link rel="preload" href="/static/css/index-[hash].css" as="style" />

2. 路由级代码分割

Vue Router 的懒加载确保只加载当前页面需要的代码,首屏不加载后台文章编辑页等重组件。

3. 异步组件

对于非首屏可见的组件,使用 Vue 3 的 defineAsyncComponent 延迟加载:

import { defineAsyncComponent } from 'vue';

const HeavyChart = defineAsyncComponent(() => import('@/components/HeavyChart.vue'));

// 可选的加载占位
const HeavyChart = defineAsyncComponent({
  loader: () => import('@/components/HeavyChart.vue'),
  loadingComponent: () => h('div', '图表加载中...'),
  delay: 200,
  timeout: 10000,
});

7.3 图片与静态资源优化

1. 使用 WebP 格式

<picture>
  <source srcset="@/assets/images/hero.webp" type="image/webp" />
  <img src="@/assets/images/hero.png" alt="Hero" />
</picture>

2. 使用 vite-imagetools 自动优化

pnpm add -D vite-imagetools
// vite.config.ts
import { defineConfig } from 'vite';
import { imagetools } from 'vite-imagetools';

export default defineConfig({
  plugins: [
    imagetools({
      defaultDirectives: () => new URLSearchParams('webp&quality=80'),
    }),
  ],
});

3. SVG 图标优化

使用 vite-svg-loader 将 SVG 文件直接作为 Vue 组件使用,避免额外的网络请求:

pnpm add -D vite-svg-loader
// vite.config.ts
import svgLoader from 'vite-svg-loader';

export default defineConfig({
  plugins: [svgLoader()],
});
<script setup lang="ts">
import IconUser from '@/assets/icons/user.svg?component';
import IconSetting from '@/assets/icons/setting.svg?component';
</script>

<template>
  <IconUser class="icon" />
  <IconSetting class="icon" />
</template>

7.4 缓存策略

1. 浏览器缓存

Nginx 配置中已经针对带 hash 的静态资源设置了 Cache-Control: public, immutable:

location /static/ {
  expires    365d;
  add_header Cache-Control "public, immutable";
}

immutable 指令告诉浏览器,这个资源永远不会变化,可以放心缓存,无需向服务器发起条件请求。

2. Service Worker 缓存

使用 vite-plugin-pwa 为 SPA 添加 Service Worker 支持,实现离线可用和更快再次访问:

pnpm add -D vite-plugin-pwa
// vite.config.ts
import { VitePWA } from 'vite-plugin-pwa';

export default defineConfig({
  plugins: [
    VitePWA({
      registerType: 'autoUpdate',
      workbox: {
        globPatterns: ['**/*.{js,css,html,ico,png,svg,webp}'],
        runtimeCaching: [
          {
            urlPattern: /^https?:\/\/api\.example\.com\/.*/i,
            handler: 'NetworkFirst',
            options: {
              cacheName: 'api-cache',
              expiration: {
                maxEntries: 100,
                maxAgeSeconds: 60 * 60 * 24, // 1 天
              },
            },
          },
        ],
      },
    }),
  ],
});

3. API 请求缓存

在 axios 请求层添加缓存逻辑,对 GET 请求进行内存缓存:


总结

本文从零开始,完整地构建了一个 Vue 3 + TypeScript + Vite 的文章管理后台系统,涵盖以下核心知识点:

模块 技术要点
项目初始化 create-vue 脚手架、目录结构设计、ESLint + Prettier + Husky + lint-staged 代码规范
架构设计 API 层封装、Pinia Store 设计、路由权限控制、TypeScript 类型管理
实战功能 JWT 认证与 Token 自动刷新、文章 CRUD、通用组件(DataTable、Pagination、FormModal)
样式体系 CSS 变量主题、UnoCSS 原子化、暗色模式、响应式布局
测试 Vitest 单元测试、Vue Test Utils 组件测试、Playwright E2E 测试
构建部署 Vite 配置优化、代码分割、Nginx 部署、Docker 容器化、CI/CD 自动化
性能优化 打包体积分析、首屏加载优化、图片优化、多级缓存策略

这套架构和最佳实践可以直接作为实际项目的基线模板,也可根据业务需求灵活裁剪。Vue 3 的组合式 API 加上 TypeScript 的类型系统,让中大型前端项目的可维护性和开发效率都提升到了新的高度。