【Vue 3 系列·第八篇·收官】工程化实战:Vite 配置、TypeScript 最佳实践、自动导入与部署优化
本文是Vue 3完全指南系列的收官篇,重点介绍工程化实战。主要内容包括:Vite配置详解(开发服务器、构建优化、CSS处理等);TypeScript最佳实践;自动导入工具unplugin-auto-import和unplugin-vue-components的使用;代码规范配置(ESLint+Prettier);多环境配置、路径别名设置;构建产物分析与优化;以及部署方案(Nginx、Docker、
·
【Vue 3 系列·第八篇·收官】工程化实战:Vite 配置、TypeScript 最佳实践、自动导入与部署优化
更新时间:2026-05-19 | 阅读时长:约 24 分钟
系列:Vue 3 完全指南(共 8 篇)· 收官篇
标签:Vue3ViteTypeScriptESLint自动导入工程化部署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 配置详解
- 二、TypeScript 最佳实践
- 三、自动导入:unplugin-auto-import + unplugin-vue-components
- 四、ESLint + Prettier:代码规范
- 五、环境变量与多环境配置
- 六、路径别名与模块解析
- 七、构建优化与产物分析
- 八、部署:Nginx、Docker 与 CI/CD
- 九、系列收官:完整项目架构最佳实践
一、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 完全指南系列(八篇)完结 🎉
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐

所有评论(0)