【Vue 3 系列·第八篇·收官】工程化实战:Vite 配置、TypeScript 最佳实践、自动导入与部署优化

更新时间:2026-05-19 | 阅读时长:约 24 分钟
系列:Vue 3 完全指南(共 8 篇)· 收官篇
标签Vue3 Vite TypeScript ESLint 自动导入 工程化 部署 CI/CD


在这里插入图片描述

系列完整进度

篇次 主题 状态
第一篇 Vue 3 是什么:Composition API vs Options API ✅ 已发布
第二篇 响应式系统:ref、reactive、computed、watch ✅ 已发布
第三篇 组件通信:props、emit、provide/inject ✅ 已发布
第四篇 生命周期钩子:执行时机与正确用法 ✅ 已发布
第五篇 路由:Vue Router 4 实战 ✅ 已发布
第六篇 状态管理:Pinia 完全指南 ✅ 已发布
第七篇 性能优化:懒加载、KeepAlive、v-memo ✅ 已发布
第八篇(本篇·收官) 工程化:Vite + TypeScript + 最佳实践

目录


一、Vite 配置详解

1.1 完整的 vite.config.ts

// 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'

export default defineConfig(({ command, mode }) => {
  // 加载对应环境的 .env 文件
  const env = loadEnv(mode, process.cwd(), '')

  return {
    plugins: [
      vue(),
      vueJsx(),   // 支持 JSX/TSX 语法(可选)
    ],

    // ── 路径别名 ────────────────────────────────────────
    resolve: {
      alias: {
        '@':          resolve(__dirname, 'src'),
        '@components': resolve(__dirname, 'src/components'),
        '@views':     resolve(__dirname, 'src/views'),
        '@stores':    resolve(__dirname, 'src/stores'),
        '@utils':     resolve(__dirname, 'src/utils'),
        '@assets':    resolve(__dirname, 'src/assets'),
      },
      // 导入时省略的扩展名(不推荐省略 .vue)
      extensions: ['.mjs', '.js', '.ts', '.jsx', '.tsx', '.json'],
    },

    // ── 开发服务器 ──────────────────────────────────────
    server: {
      host:   '0.0.0.0',  // 允许局域网访问
      port:    5173,
      open:    true,       // 启动时自动打开浏览器
      https:   false,

      // API 代理(解决开发时跨域问题)
      proxy: {
        '/api': {
          target:      env.VITE_API_BASE_URL || 'http://localhost:3000',
          changeOrigin: true,
          rewrite:     (path) => path.replace(/^\/api/, ''),
          // 开发环境打印代理日志
          configure: (proxy) => {
            proxy.on('error', (err) => console.log('proxy error', err))
          },
        },
        '/upload': {
          target:      'http://oss.example.com',
          changeOrigin: true,
        },
      },
    },

    // ── 构建配置 ────────────────────────────────────────
    build: {
      // 构建目标(根据需要调整浏览器兼容性)
      target: 'es2015',

      // 输出目录
      outDir:   'dist',
      assetsDir: 'assets',

      // 超过 10KB 的资源内联为 base64(减少请求数)
      assetsInlineLimit: 10 * 1024,

      // 代码压缩:'terser'(更小)| 'esbuild'(更快)
      minify: 'esbuild',

      // source map(生产环境建议关闭,或只用 hidden-source-map)
      sourcemap: mode === 'development',

      // Rollup 配置
      rollupOptions: {
        output: {
          // 代码分割:手动控制 chunk
          manualChunks: {
            'vue-vendor':  ['vue', 'vue-router', 'pinia'],
            'ui-vendor':   ['element-plus'],
            'chart-vendor': ['echarts'],
          },

          // 文件命名格式
          chunkFileNames:  'assets/js/[name]-[hash].js',
          entryFileNames:  'assets/js/[name]-[hash].js',
          assetFileNames:  'assets/[ext]/[name]-[hash].[ext]',
        },
      },

      // chunk 大小警告阈值(KB)
      chunkSizeWarningLimit: 600,
    },

    // ── CSS 配置 ────────────────────────────────────────
    css: {
      preprocessorOptions: {
        scss: {
          // 全局注入 SCSS 变量和混入
          additionalData: `
            @use "@/styles/variables.scss" as *;
            @use "@/styles/mixins.scss" as *;
          `,
        },
      },
      modules: {
        // CSS Modules 类名格式
        localsConvention: 'camelCase',
      },
    },

    // ── 预构建优化 ──────────────────────────────────────
    optimizeDeps: {
      // 强制预构建的依赖(解决某些 ESM 包的兼容问题)
      include: [
        'vue',
        'vue-router',
        'pinia',
        'axios',
        'dayjs',
      ],
      // 排除不需要预构建的包
      exclude: ['@vueuse/core'],
    },
  }
})

1.2 Vite 插件推荐

// 推荐插件清单
import AutoImport      from 'unplugin-auto-import/vite'
import Components      from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
import Icons           from 'unplugin-icons/vite'
import IconsResolver   from 'unplugin-icons/resolver'
import VueI18nPlugin   from '@intlify/unplugin-vue-i18n/vite'
import Inspect         from 'vite-plugin-inspect'   // 调试插件(可选)

// 在 plugins 数组中使用:
plugins: [
  vue(),

  // 自动导入 Vue/Vue Router/Pinia API
  AutoImport({
    imports: ['vue', 'vue-router', 'pinia'],
    dts: 'src/auto-imports.d.ts',
  }),

  // 自动注册组件
  Components({
    resolvers: [
      ElementPlusResolver(),
      IconsResolver({ prefix: 'Icon' }),
    ],
    dts: 'src/components.d.ts',
  }),

  // 图标按需引入
  Icons({ autoInstall: true }),
]

二、TypeScript 最佳实践

2.1 tsconfig.json 配置

{
  "compilerOptions": {
    "target":  "ES2020",
    "module":  "ESNext",
    "lib":     ["ES2020", "DOM", "DOM.Iterable"],
    "jsx":     "preserve",

    // 模块解析
    "moduleResolution": "bundler",
    "allowImportingTsExtensions": true,
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,

    // 严格模式(强烈推荐全部开启)
    "strict":                    true,
    "noUnusedLocals":            true,   // 禁止未使用的局部变量
    "noUnusedParameters":        true,   // 禁止未使用的参数
    "noFallthroughCasesInSwitch": true,  // switch 必须有 break/return
    "noImplicitReturns":         true,   // 函数所有路径必须有返回值

    // 路径别名(与 vite.config.ts 保持一致)
    "baseUrl": ".",
    "paths": {
      "@/*":           ["src/*"],
      "@components/*": ["src/components/*"],
      "@views/*":      ["src/views/*"],
      "@stores/*":     ["src/stores/*"],
      "@utils/*":      ["src/utils/*"]
    }
  },
  "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
  "exclude": ["node_modules", "dist"]
}

2.2 Vue 组件的 TypeScript 写法

<script setup lang="ts">
import { ref, computed, watch } from 'vue'

// ── Props 类型定义 ─────────────────────────────────────
interface User {
  id:     number
  name:   string
  email:  string
  role:   'admin' | 'editor' | 'viewer'
  avatar?: string
}

interface Props {
  user:       User
  editable?:  boolean
  maxNameLen?: number
}

const props = withDefaults(defineProps<Props>(), {
  editable:   false,
  maxNameLen: 50,
})

// ── Emits 类型定义 ─────────────────────────────────────
const emit = defineEmits<{
  'update':  [user: Partial<User>]
  'delete':  [id: number]
  'select':  [user: User]
}>()

// ── 响应式状态 ─────────────────────────────────────────
const isEditing = ref(false)
const formData  = ref<Partial<User>>({})
const errors    = ref<Partial<Record<keyof User, string>>>({})

// ── 计算属性 ───────────────────────────────────────────
const displayName = computed(() =>
  props.user.name.slice(0, props.maxNameLen)
)

const isAdmin = computed(() => props.user.role === 'admin')

// ── 方法 ───────────────────────────────────────────────
function startEdit() {
  formData.value = { ...props.user }
  isEditing.value = true
}

function validateForm(): boolean {
  errors.value = {}
  if (!formData.value.name?.trim()) {
    errors.value.name = '姓名不能为空'
    return false
  }
  if (formData.value.name.length > props.maxNameLen) {
    errors.value.name = `姓名不能超过 ${props.maxNameLen} 个字符`
    return false
  }
  return true
}

function saveEdit() {
  if (!validateForm()) return
  emit('update', formData.value)
  isEditing.value = false
}

function handleDelete() {
  if (confirm(`确认删除 ${props.user.name}?`)) {
    emit('delete', props.user.id)
  }
}
</script>

<template>
  <div class="user-card">
    <div v-if="!isEditing">
      <h3>{{ displayName }}</h3>
      <p>{{ user.email }}</p>
      <span :class="`badge badge-${user.role}`">{{ user.role }}</span>
      <div v-if="editable" class="actions">
        <button @click="startEdit">编辑</button>
        <button @click="handleDelete">删除</button>
      </div>
    </div>
    <form v-else @submit.prevent="saveEdit">
      <input v-model="formData.name" />
      <span v-if="errors.name" class="error">{{ errors.name }}</span>
      <button type="submit">保存</button>
      <button type="button" @click="isEditing = false">取消</button>
    </form>
  </div>
</template>

2.3 全局类型声明

// src/types/global.d.ts

// 全局通用类型(无需导入直接使用)
declare global {
  // API 响应格式
  interface ApiResponse<T = any> {
    code:    number
    message: string
    data:    T
  }

  // 分页数据格式
  interface PageResult<T = any> {
    list:       T[]
    total:      number
    page:       number
    pageSize:   number
    totalPages: number
  }

  // 表单验证规则
  type Validator<T = any> = (value: T) => true | string

  // 可空类型简写
  type Nullable<T> = T | null
  type Maybe<T>    = T | null | undefined
}

export {}
// src/types/api.ts:接口返回数据类型
export interface UserInfo {
  id:         number
  name:       string
  email:      string
  avatar:     string
  role:       UserRole
  createdAt:  string
}

export type UserRole = 'admin' | 'editor' | 'viewer'

export interface LoginRequest {
  email:    string
  password: string
}

export interface LoginResponse {
  token:      string
  user:       UserInfo
  expiresAt:  number
}
// src/api/user.ts:封装 API 请求(带类型)
import axios from '@/utils/request'
import type { UserInfo, LoginRequest, LoginResponse } from '@/types/api'

export const userAPI = {
  login: (data: LoginRequest) =>
    axios.post<ApiResponse<LoginResponse>>('/auth/login', data),

  getUserInfo: (id: number) =>
    axios.get<ApiResponse<UserInfo>>(`/users/${id}`),

  updateUser: (id: number, data: Partial<UserInfo>) =>
    axios.patch<ApiResponse<UserInfo>>(`/users/${id}`, data),

  deleteUser: (id: number) =>
    axios.delete<ApiResponse<void>>(`/users/${id}`),

  getUserList: (params: { page: number; pageSize: number; keyword?: string }) =>
    axios.get<ApiResponse<PageResult<UserInfo>>>('/users', { params }),
}

三、自动导入:减少样板代码

3.1 unplugin-auto-import 配置

// vite.config.ts 中配置自动导入
import AutoImport from 'unplugin-auto-import/vite'

AutoImport({
  // 自动导入的 API 来源
  imports: [
    'vue',               // ref, reactive, computed 等
    'vue-router',        // useRoute, useRouter 等
    'pinia',             // defineStore, storeToRefs 等
    '@vueuse/core',      // useWindowSize, useDark 等
    {
      // 自定义导入
      'axios': [['default', 'axios']],
      '@/utils/request': [['default', 'request']],
    },
  ],

  // 生成类型声明文件(TypeScript 支持)
  dts: 'src/auto-imports.d.ts',

  // ESLint 集成(防止 "no-undef" 报错)
  eslintrc: {
    enabled:  true,
    filepath: './.eslintrc-auto-import.json',
  },

  // 目录下的 Composable 自动导入
  dirs: [
    'src/composables',
    'src/stores',
    'src/utils',
  ],
})
<!-- 配置后无需手动 import,直接使用 -->
<script setup lang="ts">
// ❌ 之前需要这些 import
// import { ref, computed, watch, onMounted } from 'vue'
// import { useRoute, useRouter } from 'vue-router'
// import { useWindowSize } from '@vueuse/core'

// ✅ 现在全部自动导入!
const count = ref(0)              // vue
const route = useRoute()          // vue-router
const { width } = useWindowSize() // @vueuse/core

const doubled = computed(() => count.value * 2)
</script>

3.2 unplugin-vue-components 配置

// vite.config.ts 中配置组件自动注册
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'

Components({
  // 组件目录(自动扫描)
  dirs: ['src/components', 'src/layouts'],

  // 组件解析器(第三方库按需引入)
  resolvers: [
    // Element Plus 按需引入
    ElementPlusResolver({
      importStyle: 'sass',  // 使用 SASS 样式
    }),
  ],

  // 生成类型声明文件
  dts: 'src/components.d.ts',

  // 包含子目录
  deep: true,
})
<!-- 配置后无需手动注册组件 -->
<script setup>
// ❌ 之前需要
// import MyButton from '@/components/MyButton.vue'
// import ElButton from 'element-plus'

// ✅ 现在直接使用,自动导入!
</script>

<template>
  <!-- 本地组件自动注册 -->
  <MyButton>点击</MyButton>

  <!-- Element Plus 按需引入 -->
  <ElButton type="primary">提交</ElButton>
  <ElInput v-model="value" />
  <ElTable :data="tableData" />
</template>

四、ESLint + Prettier:代码规范

4.1 安装与配置

npm install -D eslint @eslint/js typescript-eslint eslint-plugin-vue
npm install -D prettier eslint-config-prettier eslint-plugin-prettier
// eslint.config.js(Flat Config,ESLint 9+)
import globals         from 'globals'
import pluginJs         from '@eslint/js'
import tseslint         from 'typescript-eslint'
import pluginVue        from 'eslint-plugin-vue'
import autoImportConfig from './.eslintrc-auto-import.json' assert { type: 'json' }

export default [
  // 基础 JS 规则
  pluginJs.configs.recommended,

  // TypeScript 规则
  ...tseslint.configs.recommended,

  // Vue 规则
  ...pluginVue.configs['flat/recommended'],

  // 自动导入的全局变量(防止 no-undef)
  { languageOptions: { globals: autoImportConfig.globals } },

  {
    files: ['**/*.{js,ts,vue}'],
    languageOptions: {
      globals: { ...globals.browser, ...globals.node },
    },
    rules: {
      // TypeScript 规则
      '@typescript-eslint/no-explicit-any':        'warn',
      '@typescript-eslint/explicit-function-return-type': 'off',
      '@typescript-eslint/no-unused-vars':         ['error', { argsIgnorePattern: '^_' }],

      // Vue 规则
      'vue/multi-word-component-names':    'off',    // 允许单词组件名
      'vue/require-default-prop':          'off',    // 不强制要求默认 prop
      'vue/html-self-closing':             ['error', {
        html: { void: 'always', normal: 'never', component: 'always' },
      }],
      'vue/component-name-in-template-casing': ['error', 'PascalCase'],

      // 通用规则
      'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
      'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
      'prefer-const': 'error',
      'no-var':       'error',
    },
  },
]
// .prettierrc
{
  "semi":           false,
  "singleQuote":    true,
  "printWidth":     100,
  "tabWidth":       2,
  "trailingComma":  "all",
  "bracketSpacing": true,
  "arrowParens":    "always",
  "endOfLine":      "lf",
  "vueIndentScriptAndStyle": false
}
// package.json scripts
{
  "scripts": {
    "dev":       "vite",
    "build":     "vue-tsc && vite build",
    "preview":   "vite preview",
    "lint":      "eslint src --ext .vue,.ts,.tsx --fix",
    "format":    "prettier --write src",
    "type-check": "vue-tsc --noEmit"
  }
}

4.2 Git Hooks:提交前自动检查

npm install -D husky lint-staged commitlint @commitlint/config-conventional
npx husky init
// package.json 中配置 lint-staged
{
  "lint-staged": {
    "*.{vue,ts,tsx}": [
      "eslint --fix",
      "prettier --write"
    ],
    "*.{json,md,css,scss}": [
      "prettier --write"
    ]
  }
}
# .husky/pre-commit
npx lint-staged

# .husky/commit-msg(提交信息规范检查)
npx --no -- commitlint --edit $1
// commitlint.config.js:提交信息格式规范
// 格式:type(scope): description
// 示例:feat(auth): add OAuth login support
export default {
  extends: ['@commitlint/config-conventional'],
  rules: {
    'type-enum': [2, 'always', [
      'feat',    // 新功能
      'fix',     // Bug 修复
      'docs',    // 文档更新
      'style',   // 代码格式(不影响逻辑)
      'refactor',// 重构
      'perf',    // 性能优化
      'test',    // 测试
      'chore',   // 构建/工具变动
      'revert',  // 回退
      'ci',      // CI/CD 配置
    ]],
  },
}

五、环境变量与多环境配置

5.1 .env 文件

# .env(所有环境通用)
VITE_APP_NAME=MyApp
VITE_APP_VERSION=1.0.0

# .env.development(开发环境)
VITE_API_BASE_URL=http://localhost:3000
VITE_ENABLE_MOCK=true
VITE_LOG_LEVEL=debug

# .env.staging(预发布环境)
VITE_API_BASE_URL=https://api-staging.example.com
VITE_ENABLE_MOCK=false
VITE_LOG_LEVEL=warn

# .env.production(生产环境)
VITE_API_BASE_URL=https://api.example.com
VITE_ENABLE_MOCK=false
VITE_LOG_LEVEL=error

# .env.local(本地覆盖,不提交 Git)
VITE_API_BASE_URL=http://192.168.1.100:3000
// src/env.d.ts:环境变量类型声明
/// <reference types="vite/client" />

interface ImportMetaEnv {
  readonly VITE_APP_NAME:     string
  readonly VITE_APP_VERSION:  string
  readonly VITE_API_BASE_URL: string
  readonly VITE_ENABLE_MOCK:  string
  readonly VITE_LOG_LEVEL:    'debug' | 'info' | 'warn' | 'error'
}

interface ImportMeta {
  readonly env: ImportMetaEnv
}
// src/utils/env.ts:统一读取环境变量
export const ENV = {
  APP_NAME:    import.meta.env.VITE_APP_NAME,
  API_BASE:    import.meta.env.VITE_API_BASE_URL,
  ENABLE_MOCK: import.meta.env.VITE_ENABLE_MOCK === 'true',
  IS_DEV:      import.meta.env.DEV,
  IS_PROD:     import.meta.env.PROD,
  MODE:        import.meta.env.MODE,
} as const

// 使用
// import { ENV } from '@/utils/env'
// const baseURL = ENV.API_BASE
// package.json:多环境构建命令
{
  "scripts": {
    "build":         "vite build --mode production",
    "build:staging": "vite build --mode staging",
    "build:dev":     "vite build --mode development"
  }
}

六、路径别名与模块解析

// src/utils/request.ts:封装 Axios,带拦截器
import axios from 'axios'
import type { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'
import { ENV } from '@/utils/env'

// 创建实例
const request: AxiosInstance = axios.create({
  baseURL: ENV.API_BASE,
  timeout: 15_000,
  headers: { 'Content-Type': 'application/json' },
})

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

// 响应拦截器:统一处理错误
request.interceptors.response.use(
  (response: AxiosResponse<ApiResponse>) => {
    const { code, message, data } = response.data

    if (code === 200) return data

    // 业务错误
    if (code === 401) {
      // token 过期,跳转登录
      localStorage.removeItem('token')
      window.location.href = '/login'
    }

    return Promise.reject(new Error(message || '请求失败'))
  },
  (error) => {
    // HTTP 错误
    const status = error.response?.status
    const msg = {
      400: '请求参数错误',
      401: '未登录或登录已过期',
      403: '没有操作权限',
      404: '请求的资源不存在',
      500: '服务器内部错误',
      502: '网关错误',
      503: '服务暂时不可用',
    }[status] ?? '网络错误,请检查网络连接'

    console.error(`[HTTP ${status}] ${msg}`)
    return Promise.reject(new Error(msg))
  }
)

export default request

七、构建优化与产物分析

7.1 产物分析

# 安装分析插件
npm install -D rollup-plugin-visualizer
// vite.config.ts
import { visualizer } from 'rollup-plugin-visualizer'

plugins: [
  vue(),
  // 构建时生成 stats.html(可视化 bundle 大小)
  visualizer({
    open:      true,         // 构建完成自动打开
    filename:  'stats.html',
    gzipSize:  true,
    brotliSize: true,
  }),
]
npm run build
# 自动打开 stats.html,查看各模块大小
# 找到体积大的模块,针对性优化

7.2 常见体积优化方案

// 1. 按需引入 Element Plus
// ✅ 配合 unplugin-vue-components,完全自动按需引入
// ❌ 不要 import 整个组件库

// 2. lodash → lodash-es(支持 tree shaking)
// import _ from 'lodash'        // ❌ 全量引入
// import { debounce } from 'lodash-es'  // ✅ 按需引入

// 3. dayjs 替代 moment(体积是 moment 的 1/40)
// import moment from 'moment'   // ❌ 230KB
// import dayjs from 'dayjs'     // ✅ 7KB

// 4. 图标按需引入(unplugin-icons)
// import { House } from '@element-plus/icons-vue'  // ❌ 可能全量
// <IconHouse />  // ✅ unplugin-icons 自动按需

// vite.config.ts 中的优化配置
build: {
  rollupOptions: {
    output: {
      manualChunks(id) {
        // node_modules 中的包单独打包
        if (id.includes('node_modules')) {
          // 大包单独成 chunk
          if (id.includes('echarts'))       return 'echarts'
          if (id.includes('element-plus'))  return 'element-plus'
          if (id.includes('@vue'))          return 'vue-ecosystem'
          // 其他第三方包合并成 vendor chunk
          return 'vendor'
        }
      },
    },
  },
},

八、部署:Nginx、Docker 与 CI/CD

8.1 Nginx 配置

# /etc/nginx/sites-available/myapp.conf

server {
    listen 80;
    server_name example.com www.example.com;

    # 重定向到 HTTPS
    return 301 https://$server_name$request_uri;
}

server {
    listen 443 ssl http2;
    server_name example.com www.example.com;

    # SSL 证书
    ssl_certificate     /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
    ssl_protocols       TLSv1.2 TLSv1.3;

    # 静态文件根目录(Vite 构建产物)
    root /var/www/myapp/dist;
    index index.html;

    # Gzip 压缩
    gzip on;
    gzip_types text/plain text/css application/javascript application/json image/svg+xml;
    gzip_min_length 1024;
    gzip_comp_level 6;

    # 静态资源缓存(hash 文件名,长期缓存)
    location /assets/ {
        expires 1y;
        add_header Cache-Control "public, immutable";
        add_header Vary Accept-Encoding;
    }

    # HTML 文件:不缓存(确保用户获取最新版本)
    location ~* \.html$ {
        expires -1;
        add_header Cache-Control "no-cache, no-store, must-revalidate";
    }

    # ✅ 关键:Vue Router history 模式,所有路由返回 index.html
    location / {
        try_files $uri $uri/ /index.html;
    }

    # API 反向代理
    location /api/ {
        proxy_pass         http://localhost:3000/;
        proxy_http_version 1.1;
        proxy_set_header   Host              $host;
        proxy_set_header   X-Real-IP         $remote_addr;
        proxy_set_header   X-Forwarded-For   $proxy_add_x_forwarded_for;
        proxy_set_header   X-Forwarded-Proto $scheme;
    }
}

8.2 Docker 部署

# Dockerfile(多阶段构建)

# ── 阶段1:构建 ─────────────────────────────────────────
FROM node:20-alpine AS builder

WORKDIR /app

# 先复制 package.json,利用 Docker 层缓存
COPY package*.json ./
RUN npm ci --frozen-lockfile

# 复制源码并构建
COPY . .
RUN npm run build

# ── 阶段2:运行(只包含最终产物,镜像更小)────────────────
FROM nginx:alpine AS runner

# 复制 Nginx 配置
COPY nginx.conf /etc/nginx/conf.d/default.conf

# 复制构建产物
COPY --from=builder /app/dist /usr/share/nginx/html

# 暴露端口
EXPOSE 80

CMD ["nginx", "-g", "daemon off;"]
# docker-compose.yml
version: '3.8'

services:
  frontend:
    build: .
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
      - /etc/letsencrypt:/etc/letsencrypt:ro
    restart: unless-stopped

  # 如果需要,同时部署后端
  backend:
    image: myapp-backend:latest
    ports:
      - "3000:3000"
    environment:
      NODE_ENV: production
    restart: unless-stopped

8.3 GitHub Actions CI/CD

# .github/workflows/deploy.yml

name: Build and Deploy

on:
  push:
    branches: [main]     # main 分支推送时触发
  pull_request:
    branches: [main]     # PR 时也跑一遍(但不部署)

jobs:
  # ── 代码质量检查 ──────────────────────────────────────
  quality:
    name: Code Quality
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'

      - name: Install dependencies
        run: npm ci

      - name: Type check
        run: npm run type-check

      - name: Lint
        run: npm run lint

  # ── 构建 ──────────────────────────────────────────────
  build:
    name: Build
    needs: quality
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'

      - name: Install dependencies
        run: npm ci

      - name: Build
        run: npm run build
        env:
          VITE_API_BASE_URL: ${{ secrets.API_BASE_URL }}

      - name: Upload build artifacts
        uses: actions/upload-artifact@v4
        with:
          name: dist
          path: dist/
          retention-days: 7

  # ── 部署(仅 main 分支)────────────────────────────────
  deploy:
    name: Deploy
    needs: build
    if: github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    environment: production   # 需要手动审批
    steps:
      - name: Download artifacts
        uses: actions/download-artifact@v4
        with:
          name: dist
          path: dist/

      - name: Deploy to server
        uses: appleboy/scp-action@master
        with:
          host:     ${{ secrets.SERVER_HOST }}
          username: ${{ secrets.SERVER_USER }}
          key:      ${{ secrets.SERVER_SSH_KEY }}
          source:   "dist/*"
          target:   "/var/www/myapp"
          rm:       true   # 部署前删除旧文件

      - name: Reload Nginx
        uses: appleboy/ssh-action@master
        with:
          host:     ${{ secrets.SERVER_HOST }}
          username: ${{ secrets.SERVER_USER }}
          key:      ${{ secrets.SERVER_SSH_KEY }}
          script:   nginx -s reload

九、系列收官:完整项目架构最佳实践

9.1 推荐目录结构

src/
├── api/                    # API 请求层
│   ├── user.ts             # 用户相关接口
│   ├── product.ts          # 商品相关接口
│   └── index.ts            # 统一导出
│
├── assets/                 # 静态资源
│   ├── images/
│   ├── icons/
│   └── styles/
│       ├── variables.scss  # 全局变量
│       ├── mixins.scss     # 全局混入
│       └── global.scss     # 全局样式
│
├── components/             # 公共组件
│   ├── base/               # 基础组件(Button、Input 等封装)
│   ├── business/           # 业务组件(UserCard、ProductItem 等)
│   └── layout/             # 布局组件(Header、Sidebar、Footer)
│
├── composables/            # 可复用的组合函数
│   ├── useAuth.ts
│   ├── useWindowSize.ts
│   ├── useDebouncedRef.ts
│   └── useIntersectionObserver.ts
│
├── directives/             # 自定义指令
│   ├── vLazyLoad.ts
│   └── vPermission.ts
│
├── layouts/                # 页面布局
│   ├── AdminLayout.vue
│   ├── AuthLayout.vue
│   └── BlankLayout.vue
│
├── router/                 # 路由配置
│   ├── index.ts
│   ├── guards.ts           # 路由守卫
│   └── routes/
│       ├── auth.ts
│       ├── admin.ts
│       └── public.ts
│
├── stores/                 # Pinia Store
│   ├── auth.ts
│   ├── cart.ts
│   ├── ui.ts
│   └── product.ts
│
├── types/                  # TypeScript 类型定义
│   ├── global.d.ts         # 全局类型
│   ├── api.ts              # API 接口类型
│   ├── router.d.ts         # 路由元信息类型
│   └── env.d.ts            # 环境变量类型
│
├── utils/                  # 工具函数
│   ├── request.ts          # Axios 封装
│   ├── env.ts              # 环境变量读取
│   ├── storage.ts          # localStorage 封装
│   └── format.ts           # 格式化工具
│
├── views/                  # 页面组件
│   ├── auth/
│   │   ├── LoginView.vue
│   │   └── RegisterView.vue
│   ├── dashboard/
│   │   └── DashboardView.vue
│   └── error/
│       ├── 403View.vue
│       └── 404View.vue
│
├── App.vue                 # 根组件
├── main.ts                 # 入口文件
└── auto-imports.d.ts       # 自动导入类型(自动生成)

9.2 系列核心知识总结

Vue 3 八篇系列的完整知识体系:

┌─ 第一篇:Vue 3 是什么 ────────────────────────────────┐
│  Composition API vs Options API                       │
│  <script setup> 语法糖                                │
│  Fragment / Teleport / Suspense                       │
└───────────────────────────────────────────────────────┘

┌─ 第二篇:响应式系统 ──────────────────────────────────┐
│  Proxy 替代 defineProperty(根本改进)                │
│  ref(任意类型)+ reactive(对象)                    │
│  computed(缓存)+ watch + watchEffect                 │
└───────────────────────────────────────────────────────┘

┌─ 第三篇:组件通信 ────────────────────────────────────┐
│  props(父→子)+ emit(子→父)                        │
│  v-model(双向绑定语法糖)                             │
│  provide/inject(跨层)                               │
│  expose + useTemplateRef(父访问子)                  │
└───────────────────────────────────────────────────────┘

┌─ 第四篇:生命周期 ────────────────────────────────────┐
│  setup() = beforeCreate + created                     │
│  onMounted(初始化 DOM)                               │
│  onUnmounted(清理资源)                               │
│  Composable 中可以使用生命周期钩子                     │
└───────────────────────────────────────────────────────┘

┌─ 第五篇:Vue Router 4 ────────────────────────────────┐
│  useRoute / useRouter                                  │
│  beforeEach 全局守卫(登录鉴权)                       │
│  路由懒加载(代码分割)                                 │
│  路由 meta(标题/权限/缓存)                           │
└───────────────────────────────────────────────────────┘

┌─ 第六篇:Pinia ───────────────────────────────────────┐
│  Setup Store(推荐)                                   │
│  无 mutation,直接修改 state                           │
│  storeToRefs(解构保响应式)                           │
│  pinia-plugin-persistedstate(持久化)                 │
└───────────────────────────────────────────────────────┘

┌─ 第七篇:性能优化 ────────────────────────────────────┐
│  v-memo(跳过不必要渲染)                              │
│  KeepAlive(组件缓存)                                 │
│  虚拟列表(海量数据)                                  │
│  markRaw / shallowRef(减少追踪开销)                  │
└───────────────────────────────────────────────────────┘

┌─ 第八篇:工程化(本篇)──────────────────────────────┐
│  Vite 配置(代理/别名/构建)                           │
│  TypeScript 严格模式                                   │
│  自动导入(unplugin)                                  │
│  ESLint + Prettier + Husky                            │
│  Docker + Nginx + GitHub Actions                      │
└───────────────────────────────────────────────────────┘

💬 八篇系列全部看完了!你觉得 Vue 3 最让你惊艳的是哪个特性? 欢迎评论区分享!

🙏 「Vue 3 完全指南」系列(八篇)完结撒花!如果整个系列帮到你,最后一次三连(点赞👍 + 收藏⭐ + 关注)!感谢一路相伴!


本文为原创技术分享。最后更新:2026-05-19
Vue 3 完全指南系列(八篇)完结 🎉

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐