本章将深入探讨Vue.js应用的性能优化技巧和部署策略,帮助你构建高性能的生产级应用。
9.1 性能分析
性能监控工具
// src/utils/performance.js
class PerformanceMonitor {
constructor() {
this.metrics = new Map()
this.observers = []
this.init()
}
init() {
// 监控页面加载性能
if ('performance' in window) {
window.addEventListener('load', () => {
this.collectLoadMetrics()
})
}
// 监控长任务
if ('PerformanceObserver' in window) {
this.observeLongTasks()
this.observeLayoutShifts()
this.observeLargestContentfulPaint()
}
}
collectLoadMetrics() {
const navigation = performance.getEntriesByType('navigation')[0]
const paint = performance.getEntriesByType('paint')
const metrics = {
// 页面加载时间
loadTime: navigation.loadEventEnd - navigation.loadEventStart,
// DOM内容加载时间
domContentLoaded: navigation.domContentLoadedEventEnd - navigation.domContentLoadedEventStart,
// 首次内容绘制
firstContentfulPaint: paint.find(p => p.name === 'first-contentful-paint')?.startTime || 0,
// 首次绘制
firstPaint: paint.find(p => p.name === 'first-paint')?.startTime || 0,
// DNS查询时间
dnsTime: navigation.domainLookupEnd - navigation.domainLookupStart,
// TCP连接时间
tcpTime: navigation.connectEnd - navigation.connectStart,
// 请求响应时间
responseTime: navigation.responseEnd - navigation.requestStart,
// 页面解析时间
parseTime: navigation.domInteractive - navigation.responseEnd
}
this.metrics.set('load', metrics)
this.reportMetrics('load', metrics)
}
observeLongTasks() {
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (entry.duration > 50) {
this.reportMetrics('longTask', {
duration: entry.duration,
startTime: entry.startTime,
name: entry.name
})
}
}
})
observer.observe({ entryTypes: ['longtask'] })
this.observers.push(observer)
}
observeLayoutShifts() {
let clsValue = 0
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (!entry.hadRecentInput) {
clsValue += entry.value
}
}
this.metrics.set('cls', clsValue)
})
observer.observe({ entryTypes: ['layout-shift'] })
this.observers.push(observer)
}
observeLargestContentfulPaint() {
const observer = new PerformanceObserver((list) => {
const entries = list.getEntries()
const lastEntry = entries[entries.length - 1]
this.metrics.set('lcp', lastEntry.startTime)
this.reportMetrics('lcp', { startTime: lastEntry.startTime })
})
observer.observe({ entryTypes: ['largest-contentful-paint'] })
this.observers.push(observer)
}
// 测量组件渲染时间
measureComponent(name, fn) {
const start = performance.now()
const result = fn()
const end = performance.now()
const duration = end - start
this.reportMetrics('component', {
name,
duration,
timestamp: start
})
return result
}
// 测量异步操作
async measureAsync(name, asyncFn) {
const start = performance.now()
try {
const result = await asyncFn()
const end = performance.now()
this.reportMetrics('async', {
name,
duration: end - start,
success: true
})
return result
} catch (error) {
const end = performance.now()
this.reportMetrics('async', {
name,
duration: end - start,
success: false,
error: error.message
})
throw error
}
}
reportMetrics(type, data) {
// 发送到分析服务
if (process.env.NODE_ENV === 'production') {
this.sendToAnalytics(type, data)
} else {
console.log(`[Performance] ${type}:`, data)
}
}
sendToAnalytics(type, data) {
// 发送到Google Analytics、Sentry等
if (window.gtag) {
window.gtag('event', 'performance_metric', {
metric_type: type,
metric_value: data.duration || data.startTime,
custom_parameter: JSON.stringify(data)
})
}
}
getMetrics() {
return Object.fromEntries(this.metrics)
}
destroy() {
this.observers.forEach(observer => observer.disconnect())
this.observers = []
this.metrics.clear()
}
}
export const performanceMonitor = new PerformanceMonitor()
Vue DevTools性能分析
// src/utils/vuePerformance.js
import { nextTick } from 'vue'
// 组件性能分析装饰器
export function withPerformanceTracking(component, name) {
const originalSetup = component.setup
component.setup = function(props, context) {
const startTime = performance.now()
const result = originalSetup ? originalSetup(props, context) : {}
nextTick(() => {
const endTime = performance.now()
console.log(`[Component] ${name} setup time: ${endTime - startTime}ms`)
})
return result
}
return component
}
// 渲染性能监控
export function trackRenderPerformance(app) {
if (process.env.NODE_ENV === 'development') {
app.config.performance = true
// 监控组件更新
app.mixin({
beforeUpdate() {
this._updateStart = performance.now()
},
updated() {
if (this._updateStart) {
const duration = performance.now() - this._updateStart
if (duration > 16) { // 超过一帧的时间
console.warn(`[Slow Update] ${this.$options.name || 'Anonymous'}: ${duration}ms`)
}
}
}
})
}
}
9.2 代码优化
组件优化
<!-- 优化前的组件 -->
<template>
<div class="user-list">
<div
v-for="user in filteredUsers"
:key="user.id"
class="user-item"
>
<img :src="user.avatar" :alt="user.name">
<div class="user-info">
<h3>{{ user.name }}</h3>
<p>{{ formatDate(user.createdAt) }}</p>
<p>{{ user.posts.length }} 篇文章</p>
<button @click="toggleFollow(user)">
{{ user.isFollowing ? '取消关注' : '关注' }}
</button>
</div>
</div>
</div>
</template>
<script>
import { computed, ref } from 'vue'
import { formatDate } from '@/utils/format'
export default {
name: 'UserList',
props: {
users: {
type: Array,
required: true
},
searchQuery: {
type: String,
default: ''
}
},
setup(props, { emit }) {
// 问题:每次渲染都会重新计算
const filteredUsers = computed(() => {
return props.users.filter(user =>
user.name.toLowerCase().includes(props.searchQuery.toLowerCase())
)
})
// 问题:每次渲染都会创建新函数
function toggleFollow(user) {
emit('toggle-follow', user.id)
}
return {
filteredUsers,
formatDate, // 问题:每次渲染都会传递函数
toggleFollow
}
}
}
</script>
<!-- 优化后的组件 -->
<template>
<div class="user-list">
<UserItem
v-for="user in filteredUsers"
:key="user.id"
:user="user"
@toggle-follow="handleToggleFollow"
/>
</div>
</template>
<script>
import { computed, defineAsyncComponent } from 'vue'
import { useMemoize } from '@/composables/useMemoize'
// 懒加载组件
const UserItem = defineAsyncComponent(() => import('./UserItem.vue'))
export default {
name: 'UserList',
components: {
UserItem
},
props: {
users: {
type: Array,
required: true
},
searchQuery: {
type: String,
default: ''
}
},
emits: ['toggle-follow'],
setup(props, { emit }) {
// 使用记忆化优化过滤
const filteredUsers = useMemoize(
() => {
if (!props.searchQuery) return props.users
const query = props.searchQuery.toLowerCase()
return props.users.filter(user =>
user.name.toLowerCase().includes(query)
)
},
[() => props.users, () => props.searchQuery]
)
// 稳定的事件处理函数
function handleToggleFollow(userId) {
emit('toggle-follow', userId)
}
return {
filteredUsers,
handleToggleFollow
}
}
}
</script>
<!-- UserItem.vue - 拆分的子组件 -->
<template>
<div class="user-item">
<LazyImage
:src="user.avatar"
:alt="user.name"
class="user-avatar"
/>
<div class="user-info">
<h3>{{ user.name }}</h3>
<p>{{ formattedDate }}</p>
<p>{{ postsCount }}</p>
<button
@click="$emit('toggle-follow', user.id)"
:class="followButtonClass"
>
{{ followButtonText }}
</button>
</div>
</div>
</template>
<script>
import { computed } from 'vue'
import LazyImage from './LazyImage.vue'
import { formatDate } from '@/utils/format'
export default {
name: 'UserItem',
components: {
LazyImage
},
props: {
user: {
type: Object,
required: true
}
},
emits: ['toggle-follow'],
setup(props) {
// 计算属性缓存
const formattedDate = computed(() => formatDate(props.user.createdAt))
const postsCount = computed(() => `${props.user.posts.length} 篇文章`)
const followButtonText = computed(() =>
props.user.isFollowing ? '取消关注' : '关注'
)
const followButtonClass = computed(() => ({
'btn': true,
'btn-primary': !props.user.isFollowing,
'btn-secondary': props.user.isFollowing
}))
return {
formattedDate,
postsCount,
followButtonText,
followButtonClass
}
}
}
</script>
记忆化Composable
// src/composables/useMemoize.js
import { ref, computed, watch } from 'vue'
export function useMemoize(fn, deps) {
const cache = ref(new Map())
const memoizedValue = computed(() => {
// 生成缓存键
const key = JSON.stringify(deps.map(dep =>
typeof dep === 'function' ? dep() : dep
))
// 检查缓存
if (cache.value.has(key)) {
return cache.value.get(key)
}
// 计算新值
const result = fn()
// 缓存结果
cache.value.set(key, result)
// 限制缓存大小
if (cache.value.size > 100) {
const firstKey = cache.value.keys().next().value
cache.value.delete(firstKey)
}
return result
})
return memoizedValue
}
// 使用示例
export function useExpensiveCalculation(data, filters) {
return useMemoize(
() => {
// 昂贵的计算
return data.value
.filter(item => filters.value.category ? item.category === filters.value.category : true)
.filter(item => filters.value.search ? item.name.includes(filters.value.search) : true)
.sort((a, b) => {
switch (filters.value.sortBy) {
case 'name': return a.name.localeCompare(b.name)
case 'date': return new Date(b.createdAt) - new Date(a.createdAt)
case 'price': return b.price - a.price
default: return 0
}
})
},
[data, filters]
)
}
虚拟滚动
<!-- src/components/VirtualList.vue -->
<template>
<div
ref="containerRef"
class="virtual-list"
@scroll="handleScroll"
>
<div
class="virtual-list-phantom"
:style="{ height: totalHeight + 'px' }"
></div>
<div
class="virtual-list-content"
:style="{
transform: `translateY(${offsetY}px)`
}"
>
<div
v-for="item in visibleItems"
:key="getItemKey(item)"
class="virtual-list-item"
:style="{ height: itemHeight + 'px' }"
>
<slot :item="item" :index="item.index" />
</div>
</div>
</div>
</template>
<script>
import { ref, computed, onMounted, onUnmounted } from 'vue'
export default {
name: 'VirtualList',
props: {
items: {
type: Array,
required: true
},
itemHeight: {
type: Number,
default: 50
},
containerHeight: {
type: Number,
default: 400
},
buffer: {
type: Number,
default: 5
},
keyField: {
type: String,
default: 'id'
}
},
setup(props) {
const containerRef = ref(null)
const scrollTop = ref(0)
// 计算总高度
const totalHeight = computed(() =>
props.items.length * props.itemHeight
)
// 计算可见区域的起始和结束索引
const startIndex = computed(() => {
const index = Math.floor(scrollTop.value / props.itemHeight)
return Math.max(0, index - props.buffer)
})
const endIndex = computed(() => {
const visibleCount = Math.ceil(props.containerHeight / props.itemHeight)
const index = startIndex.value + visibleCount
return Math.min(props.items.length - 1, index + props.buffer)
})
// 计算可见项目
const visibleItems = computed(() => {
const items = []
for (let i = startIndex.value; i <= endIndex.value; i++) {
if (props.items[i]) {
items.push({
...props.items[i],
index: i
})
}
}
return items
})
// 计算偏移量
const offsetY = computed(() =>
startIndex.value * props.itemHeight
)
// 滚动处理
function handleScroll(event) {
scrollTop.value = event.target.scrollTop
}
// 获取项目键
function getItemKey(item) {
return item[props.keyField] || item.index
}
// 滚动到指定项目
function scrollToItem(index) {
if (containerRef.value) {
const targetScrollTop = index * props.itemHeight
containerRef.value.scrollTop = targetScrollTop
}
}
// 滚动到顶部
function scrollToTop() {
scrollToItem(0)
}
// 滚动到底部
function scrollToBottom() {
scrollToItem(props.items.length - 1)
}
return {
containerRef,
totalHeight,
visibleItems,
offsetY,
handleScroll,
getItemKey,
scrollToItem,
scrollToTop,
scrollToBottom
}
}
}
</script>
<style scoped>
.virtual-list {
position: relative;
overflow-y: auto;
height: v-bind('containerHeight + "px"');
}
.virtual-list-phantom {
position: absolute;
top: 0;
left: 0;
right: 0;
z-index: -1;
}
.virtual-list-content {
position: absolute;
top: 0;
left: 0;
right: 0;
}
.virtual-list-item {
box-sizing: border-box;
}
</style>
图片懒加载组件
<!-- src/components/LazyImage.vue -->
<template>
<div
ref="containerRef"
class="lazy-image-container"
:class="{
'lazy-image-loading': isLoading,
'lazy-image-loaded': isLoaded,
'lazy-image-error': hasError
}"
>
<img
v-if="shouldLoad"
:src="currentSrc"
:alt="alt"
:class="imageClass"
@load="handleLoad"
@error="handleError"
>
<div v-if="isLoading" class="lazy-image-placeholder">
<slot name="loading">
<div class="lazy-image-spinner"></div>
</slot>
</div>
<div v-if="hasError" class="lazy-image-error-placeholder">
<slot name="error">
<div class="lazy-image-error-icon">⚠️</div>
<p>图片加载失败</p>
</slot>
</div>
</div>
</template>
<script>
import { ref, computed, onMounted, onUnmounted } from 'vue'
export default {
name: 'LazyImage',
props: {
src: {
type: String,
required: true
},
alt: {
type: String,
default: ''
},
placeholder: {
type: String,
default: ''
},
threshold: {
type: Number,
default: 0.1
},
rootMargin: {
type: String,
default: '50px'
},
retryCount: {
type: Number,
default: 3
},
retryDelay: {
type: Number,
default: 1000
}
},
setup(props) {
const containerRef = ref(null)
const isIntersecting = ref(false)
const isLoading = ref(false)
const isLoaded = ref(false)
const hasError = ref(false)
const currentRetry = ref(0)
let observer = null
let retryTimer = null
// 是否应该加载图片
const shouldLoad = computed(() =>
isIntersecting.value && !hasError.value
)
// 当前图片源
const currentSrc = computed(() => {
if (isLoaded.value) return props.src
if (isLoading.value && props.placeholder) return props.placeholder
return props.src
})
// 图片类名
const imageClass = computed(() => ({
'lazy-image': true,
'lazy-image-fade-in': isLoaded.value
}))
// 初始化Intersection Observer
function initObserver() {
if (!('IntersectionObserver' in window)) {
// 不支持IntersectionObserver,直接加载
isIntersecting.value = true
return
}
observer = new IntersectionObserver(
(entries) => {
const entry = entries[0]
if (entry.isIntersecting) {
isIntersecting.value = true
isLoading.value = true
observer.unobserve(entry.target)
}
},
{
threshold: props.threshold,
rootMargin: props.rootMargin
}
)
if (containerRef.value) {
observer.observe(containerRef.value)
}
}
// 图片加载成功
function handleLoad() {
isLoading.value = false
isLoaded.value = true
hasError.value = false
currentRetry.value = 0
}
// 图片加载失败
function handleError() {
isLoading.value = false
if (currentRetry.value < props.retryCount) {
// 重试加载
currentRetry.value++
retryTimer = setTimeout(() => {
isLoading.value = true
// 强制重新加载
const img = containerRef.value?.querySelector('img')
if (img) {
img.src = props.src + '?retry=' + currentRetry.value
}
}, props.retryDelay)
} else {
hasError.value = true
}
}
// 手动重试
function retry() {
if (hasError.value) {
hasError.value = false
isLoading.value = true
currentRetry.value = 0
}
}
onMounted(() => {
initObserver()
})
onUnmounted(() => {
if (observer) {
observer.disconnect()
}
if (retryTimer) {
clearTimeout(retryTimer)
}
})
return {
containerRef,
shouldLoad,
currentSrc,
imageClass,
isLoading,
isLoaded,
hasError,
handleLoad,
handleError,
retry
}
}
}
</script>
<style scoped>
.lazy-image-container {
position: relative;
display: inline-block;
overflow: hidden;
}
.lazy-image {
width: 100%;
height: 100%;
object-fit: cover;
transition: opacity 0.3s ease;
}
.lazy-image-fade-in {
opacity: 1;
}
.lazy-image-loading .lazy-image {
opacity: 0;
}
.lazy-image-placeholder,
.lazy-image-error-placeholder {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background: #f5f5f5;
color: #666;
}
.lazy-image-spinner {
width: 2rem;
height: 2rem;
border: 2px solid #e0e0e0;
border-top: 2px solid #007bff;
border-radius: 50%;
animation: spin 1s linear infinite;
}
.lazy-image-error-icon {
font-size: 2rem;
margin-bottom: 0.5rem;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
</style>
9.3 构建优化
Vite配置优化
// vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { resolve } from 'path'
import { visualizer } from 'rollup-plugin-visualizer'
import { compression } from 'vite-plugin-compression'
import { createHtmlPlugin } from 'vite-plugin-html'
export default defineConfig({
plugins: [
vue(),
// HTML模板处理
createHtmlPlugin({
minify: true,
inject: {
data: {
title: 'Vue App',
description: 'A high-performance Vue.js application'
}
}
}),
// Gzip压缩
compression({
algorithm: 'gzip',
ext: '.gz'
}),
// Brotli压缩
compression({
algorithm: 'brotliCompress',
ext: '.br'
}),
// 构建分析
visualizer({
filename: 'dist/stats.html',
open: true,
gzipSize: true,
brotliSize: true
})
],
resolve: {
alias: {
'@': resolve(__dirname, 'src'),
'@components': resolve(__dirname, 'src/components'),
'@utils': resolve(__dirname, 'src/utils'),
'@assets': resolve(__dirname, 'src/assets')
}
},
build: {
// 目标浏览器
target: 'es2015',
// 输出目录
outDir: 'dist',
// 静态资源目录
assetsDir: 'assets',
// 小于此阈值的导入或引用资源将内联为base64编码
assetsInlineLimit: 4096,
// CSS代码拆分
cssCodeSplit: true,
// 生成源码映射
sourcemap: false,
// 构建后是否生成bundle分析报告
reportCompressedSize: false,
// 消除打包大小超过500kb警告
chunkSizeWarningLimit: 2000,
rollupOptions: {
input: {
main: resolve(__dirname, 'index.html')
},
output: {
// 分包策略
manualChunks: {
// 将Vue相关库打包到vendor chunk
vendor: ['vue', 'vue-router', 'pinia'],
// 将UI库单独打包
ui: ['element-plus', '@element-plus/icons-vue'],
// 将工具库单独打包
utils: ['lodash-es', 'dayjs', 'axios']
},
// 文件命名
chunkFileNames: (chunkInfo) => {
const facadeModuleId = chunkInfo.facadeModuleId
if (facadeModuleId) {
const fileName = facadeModuleId.split('/').pop().replace(/\.[^.]*$/, '')
return `js/${fileName}-[hash].js`
}
return 'js/[name]-[hash].js'
},
entryFileNames: 'js/[name]-[hash].js',
assetFileNames: (assetInfo) => {
const info = assetInfo.name.split('.')
const ext = info[info.length - 1]
if (/\.(mp4|webm|ogg|mp3|wav|flac|aac)$/.test(assetInfo.name)) {
return `media/[name]-[hash].${ext}`
}
if (/\.(png|jpe?g|gif|svg|webp|avif)$/.test(assetInfo.name)) {
return `images/[name]-[hash].${ext}`
}
if (/\.(woff2?|eot|ttf|otf)$/.test(assetInfo.name)) {
return `fonts/[name]-[hash].${ext}`
}
return `assets/[name]-[hash].${ext}`
}
},
// 外部依赖
external: (id) => {
// 在生产环境中,可以将某些大型库标记为外部依赖
// 然后通过CDN加载
if (process.env.NODE_ENV === 'production') {
return ['vue', 'vue-router'].includes(id)
}
return false
}
},
// Terser压缩选项
terserOptions: {
compress: {
// 删除console
drop_console: true,
// 删除debugger
drop_debugger: true,
// 删除无用代码
dead_code: true,
// 删除无用变量
unused: true
},
mangle: {
// 混淆变量名
toplevel: true
}
}
},
// 开发服务器配置
server: {
host: '0.0.0.0',
port: 3000,
open: true,
cors: true,
// 代理配置
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '')
}
}
},
// 预览服务器配置
preview: {
host: '0.0.0.0',
port: 4173,
open: true
},
// 依赖优化
optimizeDeps: {
include: [
'vue',
'vue-router',
'pinia',
'axios',
'lodash-es'
],
exclude: [
// 排除某些依赖的预构建
]
}
})
代码分割策略
// src/router/index.js
import { createRouter, createWebHistory } from 'vue-router'
import { performanceMonitor } from '@/utils/performance'
// 路由级别的代码分割
const routes = [
{
path: '/',
name: 'Home',
component: () => import('@/views/Home.vue')
},
{
path: '/about',
name: 'About',
component: () => import('@/views/About.vue')
},
{
path: '/users',
name: 'Users',
component: () => import('@/views/Users.vue'),
children: [
{
path: ':id',
name: 'UserDetail',
component: () => import('@/views/UserDetail.vue')
}
]
},
{
path: '/admin',
name: 'Admin',
component: () => import('@/views/admin/Layout.vue'),
children: [
{
path: 'dashboard',
name: 'AdminDashboard',
component: () => import('@/views/admin/Dashboard.vue')
},
{
path: 'users',
name: 'AdminUsers',
component: () => import('@/views/admin/Users.vue')
}
]
}
]
const router = createRouter({
history: createWebHistory(),
routes,
// 滚动行为
scrollBehavior(to, from, savedPosition) {
if (savedPosition) {
return savedPosition
} else {
return { top: 0 }
}
}
})
// 路由性能监控
router.beforeEach((to, from, next) => {
// 记录路由开始时间
to.meta.startTime = performance.now()
next()
})
router.afterEach((to, from) => {
// 计算路由耗时
if (to.meta.startTime) {
const duration = performance.now() - to.meta.startTime
performanceMonitor.reportMetrics('route', {
from: from.name,
to: to.name,
duration
})
}
})
export default router
组件级别的代码分割
// src/utils/asyncComponent.js
import { defineAsyncComponent, h } from 'vue'
import LoadingComponent from '@/components/LoadingComponent.vue'
import ErrorComponent from '@/components/ErrorComponent.vue'
export function createAsyncComponent(loader, options = {}) {
return defineAsyncComponent({
loader,
// 加载组件
loadingComponent: options.loading || LoadingComponent,
// 错误组件
errorComponent: options.error || ErrorComponent,
// 显示加载组件前的延迟时间
delay: options.delay || 200,
// 超时时间
timeout: options.timeout || 3000,
// 是否可以挂起
suspensible: options.suspensible !== false,
// 错误处理
onError(error, retry, fail, attempts) {
if (attempts <= 3) {
// 重试3次
retry()
} else {
fail()
}
}
})
}
// 使用示例
export const AsyncChart = createAsyncComponent(
() => import('@/components/Chart.vue'),
{
delay: 300,
timeout: 5000
}
)
export const AsyncDataTable = createAsyncComponent(
() => import('@/components/DataTable.vue')
)
9.4 部署策略
Docker部署
# Dockerfile
# 多阶段构建
FROM node:18-alpine as build-stage
# 设置工作目录
WORKDIR /app
# 复制package文件
COPY package*.json ./
# 安装依赖
RUN npm ci --only=production
# 复制源代码
COPY . .
# 构建应用
RUN npm run build
# 生产阶段
FROM nginx:alpine as production-stage
# 复制nginx配置
COPY nginx.conf /etc/nginx/nginx.conf
# 复制构建产物
COPY --from=build-stage /app/dist /usr/share/nginx/html
# 暴露端口
EXPOSE 80
# 启动nginx
CMD ["nginx", "-g", "daemon off;"]
# nginx.conf
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
# 日志格式
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
# 基础配置
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
# Gzip压缩
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_proxied any;
gzip_comp_level 6;
gzip_types
text/plain
text/css
text/xml
text/javascript
application/json
application/javascript
application/xml+rss
application/atom+xml
image/svg+xml;
# Brotli压缩(如果支持)
# brotli on;
# brotli_comp_level 6;
# brotli_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
server {
listen 80;
server_name localhost;
root /usr/share/nginx/html;
index index.html;
# 安全头
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; connect-src 'self' https:; media-src 'self'; object-src 'none'; child-src 'none'; frame-src 'none'; worker-src 'self'; frame-ancestors 'none'; form-action 'self'; base-uri 'self';" always;
# 静态资源缓存
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
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";
add_header Pragma "no-cache";
}
# API代理
location /api/ {
proxy_pass http://backend:8080/;
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;
}
# SPA路由支持
location / {
try_files $uri $uri/ /index.html;
}
# 健康检查
location /health {
access_log off;
return 200 "healthy\n";
add_header Content-Type text/plain;
}
}
}
CI/CD配置
# .github/workflows/deploy.yml
name: Deploy to Production
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run linting
run: npm run lint
- name: Run type checking
run: npm run type-check
- name: Run tests
run: npm run test:run
- name: Run E2E tests
run: npm run test:e2e:headless
build:
needs: test
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Build application
run: npm run build
env:
VITE_API_BASE_URL: ${{ secrets.API_BASE_URL }}
VITE_APP_VERSION: ${{ github.sha }}
- name: Upload build artifacts
uses: actions/upload-artifact@v3
with:
name: dist
path: dist/
deploy:
needs: build
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v3
- name: Download build artifacts
uses: actions/download-artifact@v3
with:
name: dist
path: dist/
- name: Build Docker image
run: |
docker build -t vue-app:${{ github.sha }} .
docker tag vue-app:${{ github.sha }} vue-app:latest
- name: Deploy to staging
run: |
# 部署到测试环境
echo "Deploying to staging..."
- name: Run smoke tests
run: |
# 运行冒烟测试
echo "Running smoke tests..."
- name: Deploy to production
if: success()
run: |
# 部署到生产环境
echo "Deploying to production..."
环境配置
// src/config/index.js
const config = {
development: {
apiBaseUrl: 'http://localhost:8080/api',
enableDevtools: true,
logLevel: 'debug'
},
staging: {
apiBaseUrl: 'https://staging-api.example.com/api',
enableDevtools: false,
logLevel: 'info'
},
production: {
apiBaseUrl: 'https://api.example.com/api',
enableDevtools: false,
logLevel: 'error'
}
}
const env = import.meta.env.MODE || 'development'
export default {
...config[env],
env,
version: import.meta.env.VITE_APP_VERSION || 'dev',
buildTime: import.meta.env.VITE_BUILD_TIME || new Date().toISOString()
}
本章小结
本章我们学习了Vue.js应用的性能优化和部署策略:
- 性能分析:使用工具监控和分析应用性能
- 代码优化:组件优化、记忆化、虚拟滚动等技巧
- 构建优化:Vite配置、代码分割、压缩等策略
- 部署策略:Docker、CI/CD、环境配置等实践
下一章预告
下一章我们将学习Vue.js的生态系统和实战项目,包括: - 常用UI组件库的使用 - 实战项目开发 - 最佳实践总结 - 进阶学习路径
练习题
基础练习
性能监控:
- 实现页面加载时间监控
- 添加组件渲染性能追踪
- 创建性能报告面板
代码优化:
- 优化大列表渲染性能
- 实现图片懒加载
- 添加组件缓存机制
进阶练习
构建优化:
- 配置代码分割策略
- 优化打包体积
- 实现渐进式Web应用(PWA)
部署实践:
- 搭建CI/CD流水线
- 配置多环境部署
- 实现蓝绿部署策略
提示:性能优化是一个持续的过程,需要根据实际情况选择合适的优化策略。