编程进阶网 编程进阶网
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接

杨充

专注编程 · 终身学习者
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接
  • README
  • Android提升进阶

  • iOS开发和进阶

  • Web开发和进阶

    • README
    • HTML工具手册

    • TypeScript入门

    • Vue高级进阶

      • 基础入门
      • 组件开发
      • 响应式系统
      • 路由管理
      • 状态管理
      • 组合式API
      • 工程化实战
        • 7.1 Vite构建工具
          • 7.1.1 Vite核心原理
          • 7.1.2 配置文件详解
          • 7.1.3 环境变量
          • 7.1.4 常用插件
          • 7.1.5 综合案例与思考
        • 7.2 TypeScript集成
          • 7.2.1 Vue组件中的TS
          • 7.2.2 类型声明文件
          • 7.2.3 泛型组件
          • 7.2.4 综合案例与思考
        • 7.3 性能优化
          • 7.3.1 组件懒加载
          • 7.3.2 虚拟列表
          • 7.3.3 KeepAlive缓存
          • 7.3.4 渲染性能优化
          • 7.3.5 打包体积优化
          • 7.3.6 综合案例与思考
        • 7.4 项目规范
          • 7.4.1 ESLint代码检查
          • 7.4.2 Prettier格式化
          • 7.4.3 目录结构规范
          • 7.4.4 综合案例与思考
        • 7.5 构建部署
          • 7.5.1 构建生产包
          • 7.5.2 Nginx部署
          • 7.5.3 Docker部署
          • 7.5.4 CI/CD流程
          • 7.5.5 综合案例与思考
  • Linux应用开发

  • Apps
  • Web开发和进阶
  • Vue高级进阶
杨充
2026-04-23
目录

工程化实战

# 07.工程化实战

# 目录介绍

  • 7.1 Vite构建工具
    • 7.1.1 Vite核心原理
    • 7.1.2 配置文件详解
    • 7.1.3 环境变量
    • 7.1.4 常用插件
    • 7.1.5 综合案例与思考
  • 7.2 TypeScript集成
    • 7.2.1 Vue组件中的TS
    • 7.2.2 类型声明文件
    • 7.2.3 泛型组件
    • 7.2.4 综合案例与思考
  • 7.3 性能优化
    • 7.3.1 组件懒加载
    • 7.3.2 虚拟列表
    • 7.3.3 KeepAlive缓存
    • 7.3.4 渲染性能优化
    • 7.3.5 打包体积优化
    • 7.3.6 综合案例与思考
  • 7.4 项目规范
    • 7.4.1 ESLint代码检查
    • 7.4.2 Prettier格式化
    • 7.4.3 目录结构规范
    • 7.4.4 综合案例与思考
  • 7.5 构建部署
    • 7.5.1 构建生产包
    • 7.5.2 Nginx部署
    • 7.5.3 Docker部署
    • 7.5.4 CI/CD流程
    • 7.5.5 综合案例与思考

# 7.1 Vite构建工具

# 7.1.1 Vite核心原理

Vite 之所以快,核心在于两点:

Webpack 的方式:
  所有模块 → 打包成bundle → 启动dev server → 浏览器加载bundle
  (项目越大,启动越慢)

Vite 的方式:
  启动dev server → 浏览器请求模块 → 按需编译返回
  (利用浏览器原生ESM,按需编译,启动极快)
1
2
3
4
5
6
7
开发环境:
  浏览器发起 ESM 请求
       ↓
  Vite dev server 拦截
       ↓
  按需编译(esbuild预构建 + 单文件编译)
       ↓
  返回编译结果给浏览器

生产环境:
  使用 Rollup 进行打包(成熟稳定的打包策略)
1
2
3
4
5
6
7
8
9
10
11

关键技术:

  • ESM:利用浏览器原生 ES Module 支持,无需打包
  • esbuild:用 Go 语言编写的超快编译器,用于依赖预构建
  • HMR:热模块替换,修改代码后毫秒级更新

# 7.1.2 配置文件详解

// 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'),
      '@components': resolve(__dirname, 'src/components'),
      '@views': resolve(__dirname, 'src/views')
    }
  },

  // 开发服务器配置
  server: {
    port: 3000,
    host: true,     // 允许局域网访问
    open: true,     // 自动打开浏览器
    // API 代理(解决跨域)
    proxy: {
      '/api': {
        target: 'http://localhost:8080',
        changeOrigin: true,
        rewrite: (path) => path.replace(/^\/api/, '')
      }
    }
  },

  // 构建配置
  build: {
    outDir: 'dist',
    sourcemap: false,
    // 分包策略
    rollupOptions: {
      output: {
        manualChunks: {
          'vue-vendor': ['vue', 'vue-router', 'pinia'],
          'ui-vendor': ['element-plus']
        }
      }
    },
    // gzip 压缩阈值
    chunkSizeWarningLimit: 1000
  },

  // CSS 配置
  css: {
    preprocessorOptions: {
      scss: {
        additionalData: `@import "@/styles/variables.scss";`
      }
    }
  }
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59

# 7.1.3 环境变量

# .env                  → 所有环境加载
VITE_APP_TITLE=我的应用

# .env.development      → 开发环境
VITE_API_BASE_URL=http://localhost:8080/api

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

# .env.staging          → 预发布环境
VITE_API_BASE_URL=https://staging-api.example.com
1
2
3
4
5
6
7
8
9
10
11
// 在代码中使用(只有 VITE_ 前缀的变量才会暴露给客户端)
console.log(import.meta.env.VITE_API_BASE_URL)
console.log(import.meta.env.VITE_APP_TITLE)
console.log(import.meta.env.MODE)     // 'development' | 'production'
console.log(import.meta.env.DEV)      // true/false
console.log(import.meta.env.PROD)     // true/false
1
2
3
4
5
6
// TypeScript 类型声明
// src/env.d.ts
/// <reference types="vite/client" />

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

interface ImportMeta {
  readonly env: ImportMetaEnv
}
1
2
3
4
5
6
7
8
9
10
11
12

# 7.1.4 常用插件

// vite.config.ts
import vue from '@vitejs/plugin-vue'
import Components from 'unplugin-vue-components/vite'
import AutoImport from 'unplugin-auto-import/vite'
import { visualizer } from 'rollup-plugin-visualizer'
import viteCompression from 'vite-plugin-compression'

export default defineConfig({
  plugins: [
    vue(),

    // 自动导入 Vue API(ref, computed 等无需手动 import)
    AutoImport({
      imports: ['vue', 'vue-router', 'pinia'],
      dts: 'src/auto-imports.d.ts'
    }),

    // 自动注册组件(无需手动 import 组件)
    Components({
      dts: 'src/components.d.ts'
    }),

    // 打包分析(构建后生成可视化报告)
    visualizer({ open: true }),

    // Gzip 压缩
    viteCompression({ algorithm: 'gzip' })
  ]
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

# 7.1.5 综合案例与思考

思考题:

  1. Vite 开发环境用 ESM + esbuild,生产环境用 Rollup,为什么不统一?
  2. proxy 只在开发环境生效,生产环境的跨域问题怎么解决?
  3. 环境变量为什么必须以 VITE_ 前缀开头?不加前缀的变量去哪了?

# 7.2 TypeScript集成

# 7.2.1 Vue组件中的TS

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

// 1. 基本类型注解
const count = ref<number>(0)
const name = ref<string>('Vue')
const items = ref<string[]>([])

// 2. 接口定义
interface User {
  id: number
  name: string
  email: string
  role: 'admin' | 'user'
}

const user = ref<User | null>(null)

// 3. Props 类型
const props = defineProps<{
  title: string
  count?: number
  user: User
}>()

// 4. Emit 类型
const emit = defineEmits<{
  (e: 'update', value: string): void
  (e: 'delete', id: number): void
}>()

// 5. 计算属性自动推断类型
const displayName = computed(() => user.value?.name ?? '未知')  // string

// 6. 事件处理函数类型
const handleInput = (event: Event) => {
  const target = event.target as HTMLInputElement
  console.log(target.value)
}

// 7. 异步函数类型
const fetchUser = async (id: number): Promise<User> => {
  const response = await fetch(`/api/users/${id}`)
  return response.json()
}

onMounted(async () => {
  user.value = await fetchUser(1)
})
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50

# 7.2.2 类型声明文件

// src/types/index.ts —— 全局类型定义
export interface ApiResponse<T> {
  code: number
  message: string
  data: T
}

export interface PageResult<T> {
  list: T[]
  total: number
  page: number
  pageSize: number
}

export interface User {
  id: number
  name: string
  email: string
  avatar: string
  role: 'admin' | 'editor' | 'viewer'
  createdAt: string
}

export interface MenuItem {
  key: string
  label: string
  icon?: string
  children?: MenuItem[]
  permission?: string
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// src/types/env.d.ts —— 环境类型
/// <reference types="vite/client" />

// .vue 文件声明
declare module '*.vue' {
  import type { DefineComponent } from 'vue'
  const component: DefineComponent<{}, {}, any>
  export default component
}
1
2
3
4
5
6
7
8
9

# 7.2.3 泛型组件

Vue 3.3+ 支持泛型组件:

<!-- GenericList.vue -->
<script setup lang="ts" generic="T extends { id: number }">
defineProps<{
  items: T[]
  selected?: T
}>()

defineEmits<{
  (e: 'select', item: T): void
}>()

// T 会根据使用时传入的数据自动推断
</script>

<template>
  <ul>
    <li
      v-for="item in items"
      :key="item.id"
      :class="{ active: selected?.id === item.id }"
      @click="$emit('select', item)"
    >
      <slot :item="item">
        {{ item }}
      </slot>
    </li>
  </ul>
</template>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<!-- 使用泛型组件 -->
<template>
  <!-- T 被推断为 User 类型 -->
  <GenericList :items="users" :selected="currentUser" @select="onSelect">
    <template #default="{ item }">
      {{ item.name }} - {{ item.email }}
    </template>
  </GenericList>
</template>

<script setup lang="ts">
interface User { id: number; name: string; email: string }
const users = ref<User[]>([])
const currentUser = ref<User>()
const onSelect = (user: User) => { currentUser.value = user }
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# 7.2.4 综合案例与思考

思考题:

  1. Vue 中 TypeScript 的类型推断在哪些地方表现最好?哪些地方需要手动注解?
  2. defineProps<{...}>() 和 defineProps({...}) 两种写法有什么区别?哪种更推荐?
  3. 泛型组件能解决什么问题?没有泛型时,同样的需求怎么实现?

# 7.3 性能优化

# 7.3.1 组件懒加载

import { defineAsyncComponent } from 'vue'

// 异步组件:按需加载
const HeavyChart = defineAsyncComponent(() =>
  import('./components/HeavyChart.vue')
)

// 带加载和错误状态
const AsyncModal = defineAsyncComponent({
  loader: () => import('./components/Modal.vue'),
  loadingComponent: LoadingSpinner,
  errorComponent: ErrorFallback,
  delay: 200,      // 200ms后显示loading
  timeout: 10000   // 10s超时
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 7.3.2 虚拟列表

渲染大量数据时,只渲染可视区域的元素:

<template>
  <div class="virtual-list" ref="containerRef" @scroll="onScroll">
    <div :style="{ height: totalHeight + 'px' }">
      <div
        :style="{ transform: `translateY(${offsetY}px)` }"
      >
        <div
          v-for="item in visibleItems"
          :key="item.id"
          class="list-item"
          :style="{ height: itemHeight + 'px' }"
        >
          {{ item.name }}
        </div>
      </div>
    </div>
  </div>
</template>

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

const props = defineProps<{
  items: Array<{ id: number; name: string }>
  itemHeight: number
}>()

const containerRef = ref<HTMLElement | null>(null)
const scrollTop = ref(0)
const containerHeight = 400

const totalHeight = computed(() => props.items.length * props.itemHeight)

const startIndex = computed(() =>
  Math.floor(scrollTop.value / props.itemHeight)
)

const endIndex = computed(() =>
  Math.min(
    startIndex.value + Math.ceil(containerHeight / props.itemHeight) + 1,
    props.items.length
  )
)

const visibleItems = computed(() =>
  props.items.slice(startIndex.value, endIndex.value)
)

const offsetY = computed(() => startIndex.value * props.itemHeight)

const onScroll = (e: Event) => {
  scrollTop.value = (e.target as HTMLElement).scrollTop
}
</script>

<style scoped>
.virtual-list { height: 400px; overflow-y: auto; border: 1px solid #eee; }
.list-item { padding: 8px 16px; border-bottom: 1px solid #f0f0f0; }
</style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59

# 7.3.3 KeepAlive缓存

缓存不活跃的组件实例,避免重复创建:

<template>
  <div>
    <button v-for="tab in tabs" :key="tab" @click="currentTab = tab">
      {{ tab }}
    </button>

    <KeepAlive :include="['ListView', 'FormView']" :max="5">
      <component :is="currentComponent" />
    </KeepAlive>
  </div>
</template>

<script setup lang="ts">
import { ref, computed, shallowRef } from 'vue'
import ListView from './ListView.vue'
import FormView from './FormView.vue'
import ChartView from './ChartView.vue'

const tabs = ['列表', '表单', '图表']
const currentTab = ref('列表')

const componentMap: Record<string, any> = {
  '列表': ListView,
  '表单': FormView,
  '图表': ChartView
}

const currentComponent = computed(() => componentMap[currentTab.value])
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<!-- 被缓存组件可以使用 activated/deactivated 钩子 -->
<script setup lang="ts">
import { onActivated, onDeactivated } from 'vue'

onActivated(() => {
  console.log('组件被激活(从缓存恢复)')
  // 可以在这里刷新数据
})

onDeactivated(() => {
  console.log('组件被停用(进入缓存)')
})
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13

# 7.3.4 渲染性能优化

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

// 1. shallowRef:大数据只追踪顶层变化
const bigList = shallowRef<any[]>([])

// 2. markRaw:标记不需要响应式的对象
import * as echarts from 'echarts'
const chartInstance = markRaw(echarts.init(document.getElementById('chart')))

// 3. v-once:只渲染一次,后续不更新
// <p v-once>{{ staticContent }}</p>

// 4. v-memo:有条件地跳过更新(Vue 3.2+)
// <div v-for="item in list" :key="item.id" v-memo="[item.selected]">
//   只有 item.selected 变化时才重新渲染
// </div>

// 5. 计算属性缓存 vs 方法调用
const filtered = computed(() => {
  // 依赖不变时直接返回缓存值
  return bigList.value.filter(item => item.active)
})
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

# 7.3.5 打包体积优化

// vite.config.ts
export default defineConfig({
  build: {
    rollupOptions: {
      output: {
        // 分包:将第三方库拆分为独立chunk
        manualChunks(id) {
          if (id.includes('node_modules')) {
            if (id.includes('vue')) return 'vue-vendor'
            if (id.includes('lodash')) return 'lodash-vendor'
            return 'vendor'
          }
        }
      }
    },
    // 开启 CSS 代码分割
    cssCodeSplit: true,
    // 最小化
    minify: 'terser',
    terserOptions: {
      compress: {
        drop_console: true,   // 删除 console
        drop_debugger: true   // 删除 debugger
      }
    }
  }
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 按需引入组件库(以 Element Plus 为例)
// ❌ 全量引入(体积大)
// import ElementPlus from 'element-plus'

// ✅ 按需引入(配合 unplugin-vue-components)
import { ElButton, ElInput } from 'element-plus'
1
2
3
4
5
6

# 7.3.6 综合案例与思考

思考题:

  1. 虚拟列表和分页加载各适用于什么场景?能否结合使用?
  2. KeepAlive 缓存过多组件会有什么问题?max 属性如何选择?
  3. v-memo 和 computed 都有缓存功能,它们的作用层级有什么区别?

# 7.4 项目规范

# 7.4.1 ESLint代码检查

# Vue 项目推荐的 ESLint 配置
npm install -D eslint @vue/eslint-config-typescript eslint-plugin-vue
1
2
// .eslintrc.cjs
module.exports = {
  root: true,
  extends: [
    'plugin:vue/vue3-recommended',
    'eslint:recommended',
    '@vue/eslint-config-typescript'
  ],
  rules: {
    'vue/multi-word-component-names': 'warn',
    'vue/no-unused-vars': 'error',
    '@typescript-eslint/no-unused-vars': 'warn',
    'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off'
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 7.4.2 Prettier格式化

npm install -D prettier @vue/eslint-config-prettier
1
// .prettierrc
{
  "semi": false,
  "singleQuote": true,
  "trailingComma": "none",
  "printWidth": 100,
  "tabWidth": 2,
  "vueIndentScriptAndStyle": true
}
1
2
3
4
5
6
7
8
9

# 7.4.3 目录结构规范

src/
├── api/                   # API 请求层
│   ├── user.ts
│   └── product.ts
├── assets/                # 静态资源
│   ├── images/
│   └── styles/
├── components/            # 公共组件
│   ├── base/              # 基础UI组件
│   │   ├── BaseButton.vue
│   │   └── BaseInput.vue
│   └── business/          # 业务组件
│       └── UserCard.vue
├── composables/           # 可组合函数
│   ├── useFetch.ts
│   └── useAuth.ts
├── directives/            # 自定义指令
│   └── lazyLoad.ts
├── layouts/               # 布局组件
│   ├── DefaultLayout.vue
│   └── AdminLayout.vue
├── router/                # 路由配置
│   └── index.ts
├── stores/                # Pinia 状态管理
│   ├── auth.ts
│   └── settings.ts
├── types/                 # TypeScript 类型
│   └── index.ts
├── utils/                 # 工具函数
│   ├── request.ts         # axios 封装
│   └── format.ts
├── views/                 # 页面组件
│   ├── home/
│   │   └── index.vue
│   └── user/
│       ├── index.vue
│       └── detail.vue
├── App.vue                # 根组件
└── main.ts                # 入口文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39

# 7.4.4 综合案例与思考

综合案例:Axios请求封装

// utils/request.ts
import axios, { type AxiosRequestConfig, type AxiosResponse } from 'axios'
import { useAuthStore } from '@/stores/auth'
import router from '@/router'

const request = axios.create({
  baseURL: import.meta.env.VITE_API_BASE_URL,
  timeout: 10000
})

// 请求拦截器
request.interceptors.request.use(
  (config) => {
    const authStore = useAuthStore()
    if (authStore.token) {
      config.headers.Authorization = `Bearer ${authStore.token}`
    }
    return config
  },
  (error) => Promise.reject(error)
)

// 响应拦截器
request.interceptors.response.use(
  (response: AxiosResponse) => {
    const { code, message, data } = response.data
    if (code === 200) return data
    if (code === 401) {
      const authStore = useAuthStore()
      authStore.logout()
      router.push('/login')
    }
    return Promise.reject(new Error(message))
  },
  (error) => {
    console.error('请求失败:', error.message)
    return Promise.reject(error)
  }
)

export default request
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// api/user.ts
import request from '@/utils/request'
import type { User, PageResult } from '@/types'

export function getUserList(params: { page: number; size: number }) {
  return request.get<any, PageResult<User>>('/users', { params })
}

export function getUserById(id: number) {
  return request.get<any, User>(`/users/${id}`)
}

export function createUser(data: Omit<User, 'id' | 'createdAt'>) {
  return request.post<any, User>('/users', data)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

思考题:

  1. API 层为什么要单独封装?直接在组件里写 fetch 有什么问题?
  2. 请求拦截器和响应拦截器各适合处理什么逻辑?
  3. 项目目录结构中 components 和 views 的区别是什么?如何判断一个组件应该放哪里?

# 7.5 构建部署

# 7.5.1 构建生产包

# 构建
npm run build

# 构建输出分析
npx vite build --mode production

# 预览构建结果
npm run preview
1
2
3
4
5
6
7
8

构建产物结构:

dist/
├── assets/
│   ├── index-[hash].js       # 主包
│   ├── index-[hash].css      # 样式
│   ├── vue-vendor-[hash].js  # Vue相关库
│   └── images/               # 图片资源
├── favicon.ico
└── index.html
1
2
3
4
5
6
7
8

# 7.5.2 Nginx部署

# /etc/nginx/conf.d/vue-app.conf
server {
    listen 80;
    server_name example.com;
    root /var/www/vue-app/dist;
    index index.html;

    # 静态资源缓存(带hash,强缓存1年)
    location /assets/ {
        expires 1y;
        add_header Cache-Control "public, immutable";
    }

    # Gzip 压缩
    gzip on;
    gzip_types text/plain text/css application/json application/javascript text/xml;
    gzip_min_length 1024;

    # History 模式兜底
    location / {
        try_files $uri $uri/ /index.html;
    }

    # API 代理
    location /api/ {
        proxy_pass http://backend:8080/;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

# 7.5.3 Docker部署

# Dockerfile
# 构建阶段
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run 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;"]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 构建并运行
docker build -t vue-app .
docker run -d -p 80:80 vue-app
1
2
3

# 7.5.4 CI/CD流程

# .github/workflows/deploy.yml
name: Build and Deploy

on:
  push:
    branches: [main]

jobs:
  build-and-deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'

      - name: Install dependencies
        run: npm ci

      - name: Build
        run: npm run build

      - name: Deploy to server
        uses: appleboy/scp-action@master
        with:
          host: ${{ secrets.SERVER_HOST }}
          username: ${{ secrets.SERVER_USER }}
          key: ${{ secrets.SSH_KEY }}
          source: 'dist/*'
          target: '/var/www/vue-app/'
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

# 7.5.5 综合案例与思考

综合案例:完整的部署检查清单

Vue 项目上线检查清单:

□ 构建检查
  ├── npm run build 无报错
  ├── 检查打包体积(主包 < 200KB)
  ├── 检查是否有未使用的依赖
  └── 检查 console.log 是否已移除

□ 环境配置
  ├── .env.production 配置正确
  ├── API 地址指向生产环境
  └── 第三方服务 Key 已切换为生产版本

□ 性能检查
  ├── 图片已压缩(WebP 格式优先)
  ├── 路由懒加载已配置
  ├── 第三方库按需引入
  └── Gzip 压缩已开启

□ 安全检查
  ├── XSS 防护(v-html 不渲染用户输入)
  ├── CSRF Token 已配置
  ├── 敏感信息不在前端代码中
  └── HTTPS 已启用

□ 兼容性
  ├── 目标浏览器已配置(browserslist)
  ├── Polyfill 按需引入
  └── 移动端适配已完成

□ 监控
  ├── 错误监控已接入
  ├── 性能监控已接入
  └── 日志收集已配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34

思考题:

  1. 为什么前端构建产物中的静态资源文件名要带 hash?这和缓存策略有什么关系?
  2. Docker 多阶段构建(multi-stage build)的好处是什么?为什么不直接在一个阶段里完成?
  3. 前端项目的 CI/CD 和后端项目有什么不同?前端部署有哪些特殊要考虑的点?
上次更新: 2026/06/10, 11:13:41
组合式API
README

← 组合式API README→

最近更新
01
信号崩溃快速排查
06-15
02
CoreDump破案
06-15
03
perf火焰图实战
06-15
更多文章>
Theme by Vdoing | Copyright © 2019-2026 杨充 | MIT License | 桂ICP备2024034950号 | 桂公网安备45142202000030
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式