代码组织与架构设计

项目结构最佳实践

vue-ssr-project/
├── src/
│   ├── components/          # 通用组件
│   │   ├── base/           # 基础组件
│   │   ├── business/       # 业务组件
│   │   └── layout/         # 布局组件
│   ├── views/              # 页面组件
│   │   ├── home/
│   │   ├── about/
│   │   └── posts/
│   ├── composables/        # 组合式函数
│   │   ├── useAuth.js
│   │   ├── useApi.js
│   │   └── useCache.js
│   ├── stores/             # 状态管理
│   │   ├── modules/
│   │   └── index.js
│   ├── router/             # 路由配置
│   │   ├── routes/
│   │   ├── guards.js
│   │   └── index.js
│   ├── utils/              # 工具函数
│   │   ├── api.js
│   │   ├── helpers.js
│   │   └── constants.js
│   ├── plugins/            # 插件
│   ├── directives/         # 自定义指令
│   ├── assets/             # 静态资源
│   ├── styles/             # 样式文件
│   ├── App.vue
│   ├── app.js
│   ├── entry-client.js
│   └── entry-server.js
├── server/                 # 服务端代码
│   ├── api/               # API路由
│   ├── middleware/        # 中间件
│   ├── config/            # 配置
│   ├── utils/             # 服务端工具
│   ├── dev-server.js      # 开发服务器
│   └── prod-server.js     # 生产服务器
├── public/                # 公共静态文件
├── tests/                 # 测试文件
├── docs/                  # 文档
├── scripts/               # 构建脚本
└── config/                # 配置文件

组件设计原则

<!-- 好的组件设计示例 -->
<template>
  <article class="post-card" :class="cardClasses">
    <header class="post-card__header">
      <h2 class="post-card__title">
        <router-link :to="postUrl" class="post-card__link">
          {{ post.title }}
        </router-link>
      </h2>
      <PostMeta :post="post" :show-author="showAuthor" />
    </header>
    
    <div class="post-card__content">
      <PostExcerpt 
        :content="post.excerpt" 
        :max-length="excerptLength"
      />
    </div>
    
    <footer class="post-card__footer">
      <PostTags :tags="post.tags" :max-tags="maxTags" />
      <PostActions 
        :post="post" 
        @like="handleLike"
        @share="handleShare"
      />
    </footer>
  </article>
</template>

<script setup>
import { computed } from 'vue'
import PostMeta from './PostMeta.vue'
import PostExcerpt from './PostExcerpt.vue'
import PostTags from './PostTags.vue'
import PostActions from './PostActions.vue'

// Props定义
const props = defineProps({
  post: {
    type: Object,
    required: true,
    validator: (post) => {
      return post && typeof post.id !== 'undefined' && post.title
    }
  },
  variant: {
    type: String,
    default: 'default',
    validator: (value) => ['default', 'featured', 'compact'].includes(value)
  },
  showAuthor: {
    type: Boolean,
    default: true
  },
  excerptLength: {
    type: Number,
    default: 150
  },
  maxTags: {
    type: Number,
    default: 3
  }
})

// Emits定义
const emit = defineEmits([
  'like',
  'share',
  'click'
])

// 计算属性
const cardClasses = computed(() => ({
  [`post-card--${props.variant}`]: props.variant !== 'default',
  'post-card--featured': props.post.featured
}))

const postUrl = computed(() => `/posts/${props.post.slug || props.post.id}`)

// 事件处理
const handleLike = () => {
  emit('like', props.post)
}

const handleShare = (platform) => {
  emit('share', { post: props.post, platform })
}
</script>

<style scoped>
.post-card {
  @apply bg-white rounded-lg shadow-md overflow-hidden transition-shadow duration-200;
}

.post-card:hover {
  @apply shadow-lg;
}

.post-card--featured {
  @apply border-l-4 border-blue-500;
}

.post-card--compact {
  @apply flex flex-row;
}

.post-card__header {
  @apply p-6 pb-4;
}

.post-card__title {
  @apply text-xl font-semibold text-gray-900 mb-2;
}

.post-card__link {
  @apply text-inherit no-underline hover:text-blue-600 transition-colors;
}

.post-card__content {
  @apply px-6 pb-4;
}

.post-card__footer {
  @apply px-6 pb-6 flex justify-between items-center;
}
</style>

状态管理最佳实践

// stores/modules/posts.js
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import { postsApi } from '@/utils/api'
import { useCache } from '@/composables/useCache'

export const usePostsStore = defineStore('posts', () => {
  // State
  const posts = ref([])
  const currentPost = ref(null)
  const loading = ref(false)
  const error = ref(null)
  const pagination = ref({
    page: 1,
    limit: 10,
    total: 0,
    totalPages: 0
  })
  
  // Cache
  const { get: getCached, set: setCached, clear: clearCache } = useCache('posts')
  
  // Getters
  const featuredPosts = computed(() => 
    posts.value.filter(post => post.featured)
  )
  
  const publishedPosts = computed(() => 
    posts.value.filter(post => post.status === 'published')
  )
  
  const getPostById = computed(() => (id) => 
    posts.value.find(post => post.id === id)
  )
  
  const hasNextPage = computed(() => 
    pagination.value.page < pagination.value.totalPages
  )
  
  // Actions
  async function fetchPosts(params = {}) {
    const cacheKey = `posts-${JSON.stringify(params)}`
    const cached = getCached(cacheKey)
    
    if (cached && !params.force) {
      posts.value = cached.data
      pagination.value = cached.pagination
      return cached
    }
    
    loading.value = true
    error.value = null
    
    try {
      const response = await postsApi.getAll(params)
      
      posts.value = response.data
      pagination.value = {
        page: response.page,
        limit: response.limit,
        total: response.total,
        totalPages: response.totalPages
      }
      
      // 缓存结果
      setCached(cacheKey, {
        data: response.data,
        pagination: pagination.value
      }, 5 * 60 * 1000) // 5分钟缓存
      
      return response
    } catch (err) {
      error.value = err.message
      throw err
    } finally {
      loading.value = false
    }
  }
  
  async function fetchPostById(id, options = {}) {
    const cacheKey = `post-${id}`
    const cached = getCached(cacheKey)
    
    if (cached && !options.force) {
      currentPost.value = cached
      return cached
    }
    
    loading.value = true
    error.value = null
    
    try {
      const post = await postsApi.getById(id)
      currentPost.value = post
      
      // 更新posts数组中的对应项
      const index = posts.value.findIndex(p => p.id === id)
      if (index !== -1) {
        posts.value[index] = post
      }
      
      // 缓存结果
      setCached(cacheKey, post, 10 * 60 * 1000) // 10分钟缓存
      
      return post
    } catch (err) {
      error.value = err.message
      throw err
    } finally {
      loading.value = false
    }
  }
  
  async function createPost(postData) {
    loading.value = true
    error.value = null
    
    try {
      const newPost = await postsApi.create(postData)
      posts.value.unshift(newPost)
      
      // 清除相关缓存
      clearCache()
      
      return newPost
    } catch (err) {
      error.value = err.message
      throw err
    } finally {
      loading.value = false
    }
  }
  
  async function updatePost(id, updates) {
    loading.value = true
    error.value = null
    
    try {
      const updatedPost = await postsApi.update(id, updates)
      
      // 更新posts数组
      const index = posts.value.findIndex(p => p.id === id)
      if (index !== -1) {
        posts.value[index] = updatedPost
      }
      
      // 更新当前文章
      if (currentPost.value?.id === id) {
        currentPost.value = updatedPost
      }
      
      // 清除相关缓存
      clearCache()
      
      return updatedPost
    } catch (err) {
      error.value = err.message
      throw err
    } finally {
      loading.value = false
    }
  }
  
  async function deletePost(id) {
    loading.value = true
    error.value = null
    
    try {
      await postsApi.delete(id)
      
      // 从数组中移除
      posts.value = posts.value.filter(p => p.id !== id)
      
      // 清除当前文章
      if (currentPost.value?.id === id) {
        currentPost.value = null
      }
      
      // 清除相关缓存
      clearCache()
      
    } catch (err) {
      error.value = err.message
      throw err
    } finally {
      loading.value = false
    }
  }
  
  function resetState() {
    posts.value = []
    currentPost.value = null
    loading.value = false
    error.value = null
    pagination.value = {
      page: 1,
      limit: 10,
      total: 0,
      totalPages: 0
    }
  }
  
  return {
    // State
    posts,
    currentPost,
    loading,
    error,
    pagination,
    
    // Getters
    featuredPosts,
    publishedPosts,
    getPostById,
    hasNextPage,
    
    // Actions
    fetchPosts,
    fetchPostById,
    createPost,
    updatePost,
    deletePost,
    resetState
  }
})

性能优化进阶

智能预加载策略

// composables/useIntelligentPreload.js
import { ref, onMounted, onUnmounted } from 'vue'
import { useRouter } from 'vue-router'

export function useIntelligentPreload() {
  const router = useRouter()
  const preloadedRoutes = ref(new Set())
  const observer = ref(null)
  
  // 预加载策略配置
  const config = {
    // 连接类型阈值
    connectionThresholds: {
      '4g': 0.8,
      '3g': 0.5,
      '2g': 0.2,
      'slow-2g': 0.1
    },
    // 设备内存阈值
    memoryThreshold: 4, // GB
    // 电池电量阈值
    batteryThreshold: 0.2,
    // 数据节省模式
    respectDataSaver: true
  }
  
  // 检查设备能力
  function getDeviceCapabilities() {
    const connection = navigator.connection || navigator.mozConnection || navigator.webkitConnection
    const memory = navigator.deviceMemory || 4
    
    return {
      connection: connection?.effectiveType || '4g',
      memory,
      saveData: connection?.saveData || false,
      battery: null // 需要异步获取
    }
  }
  
  // 获取电池信息
  async function getBatteryInfo() {
    if ('getBattery' in navigator) {
      try {
        const battery = await navigator.getBattery()
        return {
          level: battery.level,
          charging: battery.charging
        }
      } catch (error) {
        console.warn('无法获取电池信息:', error)
      }
    }
    return { level: 1, charging: true }
  }
  
  // 计算预加载概率
  async function calculatePreloadProbability() {
    const capabilities = getDeviceCapabilities()
    const battery = await getBatteryInfo()
    
    let probability = 1.0
    
    // 根据连接类型调整
    const connectionMultiplier = config.connectionThresholds[capabilities.connection] || 0.8
    probability *= connectionMultiplier
    
    // 根据内存调整
    if (capabilities.memory < config.memoryThreshold) {
      probability *= 0.6
    }
    
    // 根据电池状态调整
    if (!battery.charging && battery.level < config.batteryThreshold) {
      probability *= 0.3
    }
    
    // 数据节省模式
    if (config.respectDataSaver && capabilities.saveData) {
      probability *= 0.1
    }
    
    return Math.max(0, Math.min(1, probability))
  }
  
  // 预加载路由
  async function preloadRoute(to) {
    const routeKey = typeof to === 'string' ? to : to.path
    
    if (preloadedRoutes.value.has(routeKey)) {
      return
    }
    
    const probability = await calculatePreloadProbability()
    
    // 根据概率决定是否预加载
    if (Math.random() > probability) {
      return
    }
    
    try {
      // 预加载路由组件
      await router.resolve(to)
      preloadedRoutes.value.add(routeKey)
      
      console.log(`预加载路由: ${routeKey}, 概率: ${(probability * 100).toFixed(1)}%`)
    } catch (error) {
      console.warn('预加载路由失败:', routeKey, error)
    }
  }
  
  // 设置链接悬停预加载
  function setupHoverPreload() {
    const links = document.querySelectorAll('a[href^="/"]')
    
    links.forEach(link => {
      let hoverTimer = null
      
      link.addEventListener('mouseenter', () => {
        hoverTimer = setTimeout(() => {
          const href = link.getAttribute('href')
          if (href) {
            preloadRoute(href)
          }
        }, 100) // 100ms延迟,避免误触
      })
      
      link.addEventListener('mouseleave', () => {
        if (hoverTimer) {
          clearTimeout(hoverTimer)
          hoverTimer = null
        }
      })
    })
  }
  
  // 设置可见性预加载
  function setupVisibilityPreload() {
    observer.value = new IntersectionObserver(
      async (entries) => {
        for (const entry of entries) {
          if (entry.isIntersecting) {
            const link = entry.target
            const href = link.getAttribute('href')
            
            if (href && href.startsWith('/')) {
              await preloadRoute(href)
            }
          }
        }
      },
      {
        rootMargin: '50px' // 提前50px开始预加载
      }
    )
    
    // 观察所有内部链接
    const links = document.querySelectorAll('a[href^="/"]')
    links.forEach(link => observer.value.observe(link))
  }
  
  // 预加载关键路由
  async function preloadCriticalRoutes() {
    const criticalRoutes = [
      '/',
      '/about',
      '/posts'
    ]
    
    for (const route of criticalRoutes) {
      await preloadRoute(route)
    }
  }
  
  onMounted(async () => {
    // 延迟初始化,避免阻塞首屏渲染
    setTimeout(async () => {
      await preloadCriticalRoutes()
      setupHoverPreload()
      setupVisibilityPreload()
    }, 2000)
  })
  
  onUnmounted(() => {
    if (observer.value) {
      observer.value.disconnect()
    }
  })
  
  return {
    preloadRoute,
    preloadedRoutes: preloadedRoutes.value
  }
}

渐进式水合

// utils/progressive-hydration.js
import { nextTick } from 'vue'

class ProgressiveHydration {
  constructor() {
    this.hydrationQueue = []
    this.isHydrating = false
    this.observer = null
    this.idleCallback = null
  }
  
  // 注册需要渐进式水合的组件
  register(component, element, priority = 0) {
    this.hydrationQueue.push({
      component,
      element,
      priority,
      hydrated: false
    })
    
    // 按优先级排序
    this.hydrationQueue.sort((a, b) => b.priority - a.priority)
  }
  
  // 开始渐进式水合
  async start() {
    if (this.isHydrating) return
    
    this.isHydrating = true
    
    // 首先水合高优先级组件
    await this.hydrateHighPriority()
    
    // 然后根据可见性水合其他组件
    this.setupVisibilityHydration()
    
    // 最后在空闲时间水合剩余组件
    this.scheduleIdleHydration()
  }
  
  // 水合高优先级组件
  async hydrateHighPriority() {
    const highPriorityItems = this.hydrationQueue.filter(
      item => item.priority > 5 && !item.hydrated
    )
    
    for (const item of highPriorityItems) {
      await this.hydrateComponent(item)
    }
  }
  
  // 设置可见性水合
  setupVisibilityHydration() {
    this.observer = new IntersectionObserver(
      async (entries) => {
        for (const entry of entries) {
          if (entry.isIntersecting) {
            const item = this.hydrationQueue.find(
              item => item.element === entry.target && !item.hydrated
            )
            
            if (item) {
              await this.hydrateComponent(item)
              this.observer.unobserve(entry.target)
            }
          }
        }
      },
      {
        rootMargin: '50px'
      }
    )
    
    // 观察未水合的组件
    this.hydrationQueue
      .filter(item => !item.hydrated && item.priority <= 5)
      .forEach(item => {
        if (item.element) {
          this.observer.observe(item.element)
        }
      })
  }
  
  // 调度空闲时间水合
  scheduleIdleHydration() {
    const hydrateInIdle = () => {
      const unhydratedItems = this.hydrationQueue.filter(
        item => !item.hydrated
      )
      
      if (unhydratedItems.length === 0) {
        return
      }
      
      this.idleCallback = requestIdleCallback(
        async (deadline) => {
          while (deadline.timeRemaining() > 0 && unhydratedItems.length > 0) {
            const item = unhydratedItems.shift()
            await this.hydrateComponent(item)
          }
          
          // 如果还有未水合的组件,继续调度
          if (unhydratedItems.length > 0) {
            hydrateInIdle()
          }
        },
        { timeout: 5000 }
      )
    }
    
    hydrateInIdle()
  }
  
  // 水合单个组件
  async hydrateComponent(item) {
    if (item.hydrated) return
    
    try {
      const start = performance.now()
      
      // 执行组件水合
      await item.component.hydrate()
      
      const duration = performance.now() - start
      
      item.hydrated = true
      
      console.log(`组件水合完成: ${item.component.name}, 耗时: ${duration.toFixed(2)}ms`)
      
      // 触发水合完成事件
      item.element?.dispatchEvent(new CustomEvent('hydrated', {
        detail: { component: item.component, duration }
      }))
      
    } catch (error) {
      console.error('组件水合失败:', error)
    }
  }
  
  // 清理资源
  cleanup() {
    if (this.observer) {
      this.observer.disconnect()
      this.observer = null
    }
    
    if (this.idleCallback) {
      cancelIdleCallback(this.idleCallback)
      this.idleCallback = null
    }
    
    this.hydrationQueue = []
    this.isHydrating = false
  }
}

export const progressiveHydration = new ProgressiveHydration()

// Vue插件
export default {
  install(app) {
    app.config.globalProperties.$progressiveHydration = progressiveHydration
    
    // 在客户端启动渐进式水合
    if (typeof window !== 'undefined') {
      nextTick(() => {
        progressiveHydration.start()
      })
    }
  }
}

团队协作与开发规范

代码规范配置

// .eslintrc.js
module.exports = {
  root: true,
  env: {
    node: true,
    browser: true,
    es2022: true
  },
  extends: [
    'eslint:recommended',
    '@vue/eslint-config-typescript',
    '@vue/eslint-config-prettier',
    'plugin:vue/vue3-recommended'
  ],
  parserOptions: {
    ecmaVersion: 2022,
    sourceType: 'module'
  },
  rules: {
    // Vue相关规则
    'vue/multi-word-component-names': 'error',
    'vue/component-definition-name-casing': ['error', 'PascalCase'],
    'vue/component-name-in-template-casing': ['error', 'PascalCase'],
    'vue/prop-name-casing': ['error', 'camelCase'],
    'vue/attribute-hyphenation': ['error', 'always'],
    'vue/v-on-event-hyphenation': ['error', 'always'],
    
    // 组件组织
    'vue/order-in-components': 'error',
    'vue/component-tags-order': [
      'error',
      {
        order: ['template', 'script', 'style']
      }
    ],
    
    // 性能相关
    'vue/no-v-html': 'warn',
    'vue/require-v-for-key': 'error',
    'vue/no-use-v-if-with-v-for': 'error',
    
    // 可访问性
    'vue/require-explicit-emits': 'error',
    'vue/require-prop-types': 'error',
    'vue/require-default-prop': 'error',
    
    // 通用规则
    'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
    'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
    'no-unused-vars': 'warn',
    'prefer-const': 'error',
    'no-var': 'error'
  },
  overrides: [
    {
      files: ['**/*.test.js', '**/*.spec.js'],
      env: {
        jest: true
      }
    }
  ]
}

Git工作流配置

# .github/workflows/ci.yml
name: CI/CD Pipeline

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main, develop]

jobs:
  lint-and-test:
    runs-on: ubuntu-latest
    
    strategy:
      matrix:
        node-version: [16, 18, 20]
    
    steps:
    - name: Checkout code
      uses: actions/checkout@v3
    
    - name: Setup Node.js
      uses: actions/setup-node@v3
      with:
        node-version: ${{ matrix.node-version }}
        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 unit tests
      run: npm run test:unit
    
    - name: Run E2E tests
      run: npm run test:e2e
    
    - name: Build application
      run: npm run build
    
    - name: Upload coverage reports
      uses: codecov/codecov-action@v3
      with:
        file: ./coverage/lcov.info
        flags: unittests
        name: codecov-umbrella

  security-scan:
    runs-on: ubuntu-latest
    
    steps:
    - name: Checkout code
      uses: actions/checkout@v3
    
    - name: Run security audit
      run: npm audit --audit-level moderate
    
    - name: Run Snyk security scan
      uses: snyk/actions/node@master
      env:
        SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}

  performance-test:
    runs-on: ubuntu-latest
    needs: lint-and-test
    
    steps:
    - name: Checkout code
      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
    
    - name: Start server
      run: npm run serve &
      
    - name: Wait for server
      run: npx wait-on http://localhost:3000
    
    - name: Run Lighthouse CI
      run: npx lhci autorun
      env:
        LHCI_GITHUB_APP_TOKEN: ${{ secrets.LHCI_GITHUB_APP_TOKEN }}

组件文档规范

<!-- components/BaseButton.vue -->
<template>
  <button
    :class="buttonClasses"
    :disabled="disabled || loading"
    :type="type"
    @click="handleClick"
  >
    <BaseIcon v-if="loading" name="spinner" class="animate-spin" />
    <BaseIcon v-else-if="icon" :name="icon" />
    <slot />
  </button>
</template>

<script setup>
/**
 * 基础按钮组件
 * 
 * @component BaseButton
 * @example
 * <BaseButton variant="primary" size="lg" @click="handleClick">
 *   点击我
 * </BaseButton>
 * 
 * @example
 * <BaseButton variant="secondary" icon="plus" loading>
 *   加载中...
 * </BaseButton>
 */

import { computed } from 'vue'
import BaseIcon from './BaseIcon.vue'

/**
 * 组件属性
 */
const props = defineProps({
  /**
   * 按钮变体
   * @type {'primary' | 'secondary' | 'danger' | 'ghost'}
   * @default 'primary'
   */
  variant: {
    type: String,
    default: 'primary',
    validator: (value) => ['primary', 'secondary', 'danger', 'ghost'].includes(value)
  },
  
  /**
   * 按钮尺寸
   * @type {'sm' | 'md' | 'lg'}
   * @default 'md'
   */
  size: {
    type: String,
    default: 'md',
    validator: (value) => ['sm', 'md', 'lg'].includes(value)
  },
  
  /**
   * 按钮类型
   * @type {'button' | 'submit' | 'reset'}
   * @default 'button'
   */
  type: {
    type: String,
    default: 'button'
  },
  
  /**
   * 是否禁用
   * @type {boolean}
   * @default false
   */
  disabled: {
    type: Boolean,
    default: false
  },
  
  /**
   * 是否显示加载状态
   * @type {boolean}
   * @default false
   */
  loading: {
    type: Boolean,
    default: false
  },
  
  /**
   * 图标名称
   * @type {string}
   */
  icon: {
    type: String,
    default: ''
  }
})

/**
 * 组件事件
 */
const emit = defineEmits({
  /**
   * 点击事件
   * @param {MouseEvent} event - 鼠标事件对象
   */
  click: (event) => event instanceof MouseEvent
})

/**
 * 按钮样式类
 */
const buttonClasses = computed(() => {
  const baseClasses = [
    'inline-flex',
    'items-center',
    'justify-center',
    'font-medium',
    'rounded-md',
    'transition-colors',
    'focus:outline-none',
    'focus:ring-2',
    'focus:ring-offset-2'
  ]
  
  const variantClasses = {
    primary: [
      'bg-blue-600',
      'text-white',
      'hover:bg-blue-700',
      'focus:ring-blue-500',
      'disabled:bg-blue-300'
    ],
    secondary: [
      'bg-gray-200',
      'text-gray-900',
      'hover:bg-gray-300',
      'focus:ring-gray-500',
      'disabled:bg-gray-100'
    ],
    danger: [
      'bg-red-600',
      'text-white',
      'hover:bg-red-700',
      'focus:ring-red-500',
      'disabled:bg-red-300'
    ],
    ghost: [
      'bg-transparent',
      'text-gray-700',
      'hover:bg-gray-100',
      'focus:ring-gray-500',
      'disabled:text-gray-400'
    ]
  }
  
  const sizeClasses = {
    sm: ['px-3', 'py-1.5', 'text-sm'],
    md: ['px-4', 'py-2', 'text-base'],
    lg: ['px-6', 'py-3', 'text-lg']
  }
  
  return [
    ...baseClasses,
    ...variantClasses[props.variant],
    ...sizeClasses[props.size],
    {
      'opacity-50 cursor-not-allowed': props.disabled || props.loading
    }
  ]
})

/**
 * 处理点击事件
 * @param {MouseEvent} event - 鼠标事件对象
 */
const handleClick = (event) => {
  if (!props.disabled && !props.loading) {
    emit('click', event)
  }
}
</script>

<style scoped>
/* 组件特定样式 */
.animate-spin {
  animation: spin 1s linear infinite;
}

@keyframes spin {
  from {
    transform: rotate(0deg);
  }
  to {
    transform: rotate(360deg);
  }
}
</style>

总结

通过本教程的学习,你已经掌握了Vue SSR的核心概念和实践技巧:

核心知识点

  1. 基础概念:理解SSR的工作原理和优势
  2. 环境搭建:配置开发和生产环境
  3. 应用架构:设计同构应用结构
  4. 路由配置:实现客户端和服务端路由
  5. 状态管理:处理数据预取和状态同步
  6. 服务器配置:搭建Express服务器
  7. 性能优化:实现缓存和代码分割
  8. 部署配置:容器化和生产环境部署
  9. 故障排除:调试和监控技巧
  10. 最佳实践:代码组织和团队协作

进阶方向

  1. 微前端架构:将SSR应用拆分为微服务
  2. 边缘计算:利用CDN边缘节点进行SSR
  3. 流式渲染:实现更快的首屏渲染
  4. 静态生成:结合SSG提升性能
  5. 多端适配:支持移动端和桌面端

持续学习

  • 关注Vue.js官方文档更新
  • 参与开源项目贡献
  • 学习相关技术栈(Nuxt.js、Vite等)
  • 实践大型项目开发
  • 分享经验和最佳实践

希望这个教程能够帮助你在Vue SSR的道路上走得更远!