工程化实战
# 07.工程化实战
# 目录介绍
# 7.1 Vite构建工具
# 7.1.1 Vite核心原理
Vite 之所以快,核心在于两点:
Webpack 的方式:
所有模块 → 打包成bundle → 启动dev server → 浏览器加载bundle
(项目越大,启动越慢)
Vite 的方式:
启动dev server → 浏览器请求模块 → 按需编译返回
(利用浏览器原生ESM,按需编译,启动极快)
1
2
3
4
5
6
7
2
3
4
5
6
7
开发环境:
浏览器发起 ESM 请求
↓
Vite dev server 拦截
↓
按需编译(esbuild预构建 + 单文件编译)
↓
返回编译结果给浏览器
生产环境:
使用 Rollup 进行打包(成熟稳定的打包策略)
1
2
3
4
5
6
7
8
9
10
11
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
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
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
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
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
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 综合案例与思考
思考题:
- Vite 开发环境用 ESM + esbuild,生产环境用 Rollup,为什么不统一?
proxy只在开发环境生效,生产环境的跨域问题怎么解决?- 环境变量为什么必须以
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
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
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
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
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 7.2.4 综合案例与思考
思考题:
- Vue 中 TypeScript 的类型推断在哪些地方表现最好?哪些地方需要手动注解?
defineProps<{...}>()和defineProps({...})两种写法有什么区别?哪种更推荐?- 泛型组件能解决什么问题?没有泛型时,同样的需求怎么实现?
# 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
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
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
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
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
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
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
2
3
4
5
6
# 7.3.6 综合案例与思考
思考题:
- 虚拟列表和分页加载各适用于什么场景?能否结合使用?
KeepAlive缓存过多组件会有什么问题?max属性如何选择?v-memo和computed都有缓存功能,它们的作用层级有什么区别?
# 7.4 项目规范
# 7.4.1 ESLint代码检查
# Vue 项目推荐的 ESLint 配置
npm install -D eslint @vue/eslint-config-typescript eslint-plugin-vue
1
2
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
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
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
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
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
思考题:
- API 层为什么要单独封装?直接在组件里写 fetch 有什么问题?
- 请求拦截器和响应拦截器各适合处理什么逻辑?
- 项目目录结构中
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
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
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
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 /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
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
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
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
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
思考题:
- 为什么前端构建产物中的静态资源文件名要带 hash?这和缓存策略有什么关系?
- Docker 多阶段构建(multi-stage build)的好处是什么?为什么不直接在一个阶段里完成?
- 前端项目的 CI/CD 和后端项目有什么不同?前端部署有哪些特殊要考虑的点?
上次更新: 2026/06/10, 11:13:41