工程化、测试与性能优化(全)(8,9)

Vue.js 工程化与最佳实践全面指南

这个是我大纲的内容,这篇是大纲中对8,9的部分详细整理

这个是我上一部分的链接,6,7部分的知识

第八部分:工程化和最佳实践

工程化是专业开发的分水岭,决定了项目的可维护性、团队协作效率和长期发展潜力。这部分知识将帮助你从"会写代码"升级到"会工程化开发"。


📌 26. Vue CLI 和 Vite(重要程度:⭐⭐⭐⭐⭐)

26.1 Vue CLI 项目创建

Vue CLI 是 Vue 2 时代的官方脚手架工具:

# 全局安装 Vue CLI
npm install -g @vue/cli

# 创建项目
vue create my-project

# 选择预设配置
# - Default (Vue 2, babel, eslint)
# - Default (Vue 3, babel, eslint)
# - Manually select features

# 启动开发服务器
cd my-project
npm run serve

# 构建生产版本
npm run build

手动选择功能:

? Please pick a preset: Manually select features
? Check the features needed for your project:
 ◉ Babel                       # 转译 ES6+ 代码
 ◯ TypeScript                 # TypeScript 支持
 ◯ Progressive Web App (PWA) Support
 ◉ Router                      # Vue Router
 ◉ Vuex                        # 状态管理
 ◯ CSS Pre-processors         # CSS 预处理器
 ◉ Linter / Formatter         # 代码规范检查
 ◯ Unit Testing               # 单元测试
 ◯ E2E Testing                # 端到端测试

项目结构解析:

my-project/
├── public/                    # 静态资源
│   ├── index.html            # 入口 HTML
│   └── favicon.ico           # 网站图标
├── src/                      # 源代码目录
│   ├── assets/               # 静态资源(图片、字体等)
│   ├── components/           # 组件
│   ├── views/                # 页面组件
│   ├── router/               # 路由配置
│   ├── store/                # Vuex 状态管理
│   ├── App.vue               # 根组件
│   └── main.js               # 应用入口
├── tests/                    # 测试文件
├── .browserslistrc           # 浏览器兼容配置
├── .eslintrc.js              # ESLint 配置
├── babel.config.js           # Babel 配置
├── package.json              # 项目配置
├── vue.config.js             # Vue CLI 配置
└── README.md                 # 项目说明

26.2 项目结构最佳实践

推荐的项目结构:

src/
├── api/                      # API 接口封装
│   ├── index.js              # API 实例
│   ├── modules/              # 模块化 API
│   │   ├── user.js
│   │   ├── product.js
│   │   └── order.js
│   └── interceptors.js       # 请求拦截器
├── assets/                   # 静态资源
│   ├── images/               # 图片
│   ├── fonts/                # 字体
│   └── styles/               # 全局样式
├── components/               # 公共组件
│   ├── common/               # 通用组件
│   │   ├── Button/
│   │   │   ├── Button.vue
│   │   │   └── index.js
│   │   ├── Modal/
│   │   └── Input/
│   └── layout/               # 布局组件
│       ├── Header.vue
│       ├── Sidebar.vue
│       └── Footer.vue
├── composables/              # 组合式函数(Vue 3)
│   ├── useFetch.js
│   ├── useForm.js
│   └── useAuth.js
├── directives/               # 自定义指令
│   ├── focus.js
│   ├── lazy-load.js
│   └── permission.js
├── filters/                  # 过滤器(Vue 2)
├── plugins/                  # 插件
├── router/                   # 路由
│   ├── index.js
│   ├── routes/               # 路由配置
│   │   ├── home.js
│   │   ├── user.js
│   │   └── admin.js
│   └── guards/               # 路由守卫
├── store/                    # 状态管理
│   ├── index.js
│   ├── modules/              # Vuex 模块
│   │   ├── user.js
│   │   ├── cart.js
│   │   └── product.js
│   └── types.js              # Mutation 类型常量
├── utils/                    # 工具函数
│   ├── request.js            # 请求封装
│   ├── validate.js           # 验证函数
│   ├── date.js               # 日期处理
│   └── storage.js            # 本地存储
├── views/                    # 页面组件
│   ├── Home.vue
│   ├── Login.vue
│   ├── User/
│   │   ├── Profile.vue
│   │   └── Settings.vue
│   └── Admin/
│       ├── Dashboard.vue
│       └── Users.vue
├── App.vue                   # 根组件
├── main.js                   # 应用入口
└── shims-vue.d.ts           # TypeScript 类型声明

26.3 环境变量配置

环境变量文件:

.env                       # 所有环境都会加载
.env.local                 # 本地环境,git 忽略
.env.development           # 开发环境
.env.development.local     # 本地开发环境,git 忽略
.env.production            # 生产环境
.env.production.local      # 本地生产环境,git 忽略
.env.test                  # 测试环境

环境变量示例:

# .env.development
VUE_APP_API_URL=http://localhost:3000/api
VUE_APP_TITLE=开发环境
VUE_APP_VERSION=1.0.0

# .env.production
VUE_APP_API_URL=https://api.example.com
VUE_APP_TITLE=生产环境
VUE_APP_VERSION=1.0.0

在代码中使用:

// 使用环境变量
const apiUrl = process.env.VUE_APP_API_URL
const appTitle = process.env.VUE_APP_TITLE

// 在 vue.config.js 中访问
module.exports = {
  devServer: {
    proxy: {
      '/api': {
        target: process.env.VUE_APP_API_URL,
        changeOrigin: true
      }
    }
  }
}

26.4 Vite 快速开发(Vue 3 推荐)

为什么选择 Vite?

  • ⚡ 极速启动:冷启动比 Vue CLI 快 10-100 倍
  • 🔥 即时热更新:HMR 速度极快
  • 📦 按需编译:只编译正在浏览的模块
  • 🛠️ 开箱即用:支持 TypeScript、JSX、CSS 预处理器

创建 Vite 项目:

# 使用 npm
npm create vite@latest my-vue-app -- --template vue

# 使用 yarn
yarn create vite my-vue-app --template vue

# 使用 pnpm
pnpm create vite my-vue-app --template vue

# 选择模板
? Select a framework: » Vue
? Select a variant: » JavaScript / TypeScript / Customize

Vite 项目结构:

my-vue-app/
├── public/                    # 静态资源
├── src/
│   ├── assets/               # 资源文件
│   ├── components/           # 组件
│   ├── App.vue               # 根组件
│   └── main.js               # 入口文件
├── index.html                # HTML 入口(Vite 特色)
├── vite.config.js            # Vite 配置
├── package.json
└── README.md

Vite 配置文件示例:

// vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import path from 'path'

export default defineConfig({
  plugins: [vue()],
  
  // 解析配置
  resolve: {
    alias: {
      '@': path.resolve(__dirname, './src'),
      'components': path.resolve(__dirname, './src/components')
    }
  },
  
  // 开发服务器配置
  server: {
    port: 3000,
    open: true, // 自动打开浏览器
    proxy: {
      '/api': {
        target: 'http://localhost:8080',
        changeOrigin: true,
        rewrite: (path) => path.replace(/^\/api/, '')
      }
    }
  },
  
  // 构建配置
  build: {
    outDir: 'dist',
    sourcemap: false,
    rollupOptions: {
      output: {
        // 代码分割配置
        manualChunks: {
          vue: ['vue', 'vue-router', 'vuex'],
          vendor: ['axios', 'lodash', 'dayjs']
        }
      }
    }
  },
  
  // CSS 配置
  css: {
    preprocessorOptions: {
      scss: {
        additionalData: `@import "@/styles/variables.scss";`
      }
    }
  }
})

26.5 构建优化

Vue CLI 优化配置:

// vue.config.js
module.exports = {
  // 1. 打包分析
  chainWebpack: config => {
    config
      .plugin('webpack-bundle-analyzer')
      .use(require('webpack-bundle-analyzer').BundleAnalyzerPlugin)
    
    // 2. 代码分割
    config.optimization.splitChunks({
      chunks: 'all',
      cacheGroups: {
        vue: {
          name: 'vue',
          test: /[\\/]node_modules[\\/](vue|vue-router|vuex)[\\/]/,
          priority: 20
        },
        vendors: {
          name: 'vendors',
          test: /[\\/]node_modules[\\/]/,
          priority: 10
        }
      }
    })
  },
  
  // 3. 压缩配置
  productionSourceMap: false, // 关闭 source map
  configureWebpack: {
    optimization: {
      minimizer: [
        new TerserPlugin({
          terserOptions: {
            compress: {
              drop_console: true, // 移除 console
              drop_debugger: true // 移除 debugger
            }
          }
        })
      ]
    }
  },
  
  // 4. Gzip 压缩
  configureWebpack: config => {
    if (process.env.NODE_ENV === 'production') {
      config.plugins.push(
        new CompressionPlugin({
          test: /\.(js|css|html|svg)$/,
          threshold: 10240, // 大于 10KB 才压缩
          minRatio: 0.8
        })
      )
    }
  },
  
  // 5. CDN 加速
  configureWebpack: config => {
    config.externals = {
      vue: 'Vue',
      'vue-router': 'VueRouter',
      vuex: 'Vuex',
      axios: 'axios'
    }
  }
}

Vite 优化配置:

// vite.config.js
import { defineConfig } from 'vite'
import viteCompression from 'vite-plugin-compression'

export default defineConfig({
  plugins: [
    viteCompression({
      verbose: true,
      disable: false,
      threshold: 10240, // 10KB
      algorithm: 'gzip',
      ext: '.gz'
    })
  ],
  
  build: {
    // 1. 代码分割
    rollupOptions: {
      output: {
        manualChunks(id) {
          if (id.includes('node_modules')) {
            if (id.includes('vue')) {
              return 'vue'
            }
            if (id.includes('element-plus')) {
              return 'element-plus'
            }
            return 'vendor'
          }
        }
      }
    },
    
    // 2. 构建目标
    target: 'es2015',
    
    // 3. 资源大小限制
    assetsInlineLimit: 4096, // 4KB 以下的资源转 base64
    
    // 4. 移除 console 和 debugger
    minify: 'terser',
    terserOptions: {
      compress: {
        drop_console: true,
        drop_debugger: true
      }
    }
  }
})


📌 27. 代码规范和风格指南(重要程度:⭐⭐⭐⭐⭐)

27.1 官方风格指南

Vue 官方风格指南(A、B、C 三级规则):

A 级:必要的(规避错误)

  • 组件名应为多个单词(避免与 HTML 元素冲突)
// 好
export default {
  name: 'TodoItem'
}

// 不好
export default {
  name: 'Todo'
}

  • 组件的 data 必须是一个函数
// 好
data() {
  return {
    message: 'Hello'
  }
}

// 不好
data: {
  message: 'Hello'
}
  • Prop 定义应该尽量详细
// 好
props: {
  status: {
    type: String,
    required: true,
    validator: value => ['success', 'warning', 'danger'].includes(value)
  }
}

// 不好
props: ['status']
  • v-for 设置键值
<!-- 好 -->
<li v-for="todo in todos" :key="todo.id">
  {{ todo.text }}
</li>

<!-- 不好 -->
<li v-for="todo in todos">
  {{ todo.text }}
</li>

B 级:强烈推荐(增强可读性)

  • 组件文件:单文件组件文件名应使用 PascalCase 或 kebab-case
// 好
components/
├── MyComponent.vue
└── my-component.vue

// 不好
components/
├── myComponent.vue
└── my_component.vue
  • 基础组件名:使用特定前缀(Base、App、V)
// 好
components/
├── BaseButton.vue
├── AppHeader.vue
└── VAvatar.vue

// 不好
components/
├── Button.vue
├── Header.vue
└── Avatar.vue

  • 单例组件名:以 The 前缀命名
// 好
components/
├── TheHeader.vue
└── TheSidebar.vue

  • 紧密耦合的组件名:使用父组件名作为前缀
// 好
components/
├── TodoList.vue
├── TodoListItem.vue
└── TodoListItemButton.vue

C 级:推荐(最佳实践)

  • 组件名中的单词顺序:高级别/通用化的单词排在前面
// 好
SearchButtonClear.vue
SearchButtonRun.vue
SearchInputQuery.vue

// 不好
ClearSearchButton.vue
RunSearchButton.vue
QuerySearchInput.vue

  • 自闭合组件标签:在单文件组件、字符串模板和 JSX 中使用自闭合标签
<!-- 好 -->
<MyComponent />
<MyComponent></MyComponent>

<!-- 不好 -->
<MyComponent></MyComponent>

27.2 ESLint 配置

安装和配置:

# 安装 ESLint
npm install eslint --save-dev

# 安装 Vue 插件
npm install eslint-plugin-vue --save-dev

# 安装 Vue 3 专用配置
npm install @vue/eslint-config-prettier --save-dev

ESLint 配置文件:

// .eslintrc.js
module.exports = {
  root: true,
  env: {
    node: true,
    browser: true,
    es2021: true
  },
  extends: [
    'eslint:recommended',
    'plugin:vue/vue3-recommended', // Vue 3 规则
    '@vue/prettier' // 整合 Prettier
  ],
  parserOptions: {
    parser: '@babel/eslint-parser',
    ecmaVersion: 2021,
    sourceType: 'module'
  },
  rules: {
    // 自定义规则
    'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
    'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
    'vue/multi-word-component-names': 'off', // 允许单单词组件名
    
    // Vue 3 特定规则
    'vue/script-setup-uses-vars': 'error',
    
    // 代码风格
    'vue/html-indent': ['error', 2],
    'vue/max-attributes-per-line': ['error', {
      singleline: 3,
      multiline: 1
    }],
    'vue/component-tags-order': ['error', {
      order: ['template', 'script', 'style']
    }]
  },
  globals: {
    // 全局变量
    defineProps: 'readonly',
    defineEmits: 'readonly',
    defineExpose: 'readonly',
    withDefaults: 'readonly'
  }
}

在 package.json 中添加脚本:

{
  "scripts": {
    "lint": "eslint --ext .js,.vue src",
    "lint:fix": "eslint --ext .js,.vue src --fix"
  }
}

27.3 Prettier 代码格式化

安装和配置:

npm install --save-dev prettier eslint-config-prettier eslint-plugin-prettier

Prettier 配置文件:

// .prettierrc
{
  "semi": false, // 不使用分号
  "singleQuote": true, // 使用单引号
  "tabWidth": 2, // 缩进宽度
  "trailingComma": "none", // 末尾逗号
  "printWidth": 80, // 每行最大宽度
  "bracketSpacing": true, // 对象括号空格
  "arrowParens": "avoid", // 箭头函数单参数省略括号
  
  // Vue 特定配置
  "vueIndentScriptAndStyle": true, // 缩进 <script> 和 <style> 标签
  
  // 覆盖特定文件的配置
  "overrides": [
    {
      "files": "*.vue",
      "options": {
        "parser": "vue"
      }
    }
  ]
}

与 ESLint 集成:

// .eslintrc.js
module.exports = {
  extends: [
    'plugin:vue/recommended',
    'plugin:prettier/recommended' // 必须放在最后
  ],
  rules: {
    'prettier/prettier': 'error' // 将 Prettier 错误作为 ESLint 错误显示
  }
}

VSCode 自动格式化配置:

// .vscode/settings.json
{
  "editor.formatOnSave": true,
  "editor.codeActionsOnSave": {
    "source.fixAll.eslint": true
  },
  "[vue]": {
    "editor.defaultFormatter": "esbenp.prettier-vscode"
  },
  "[javascript]": {
    "editor.defaultFormatter": "esbenp.prettier-vscode"
  },
  "vetur.format.defaultFormatter.html": "prettier",
  "vetur.format.defaultFormatter.css": "prettier",
  "vetur.format.defaultFormatter.scss": "prettier",
  "vetur.format.defaultFormatter.js": "prettier"
}

27.4 组件命名规范

组件命名规则:

// 1. 单文件组件:PascalCase
MyComponent.vue      // ✅ 推荐
my-component.vue     // ✅ 可接受
myComponent.vue      // ❌ 不推荐
my_component.vue     // ❌ 不推荐

// 2. 组件注册名:PascalCase
export default {
  name: 'MyComponent'  // ✅
}

// 3. 在模板中使用:kebab-case
<template>
  <my-component />      <!-- ✅ 推荐 -->
  <MyComponent />       <!-- ✅ 可接受 -->
</template>

组件命名前缀规范:

前缀用途示例
Base基础组件,不包含业务逻辑BaseButton, BaseInput
App应用级组件,通常为单例AppHeader, AppFooter
The单例组件,一个页面只出现一次TheHeader, TheSidebar
V通用组件库中的组件VButton, VCard
无前缀业务组件UserList, ProductCard

27.5 代码组织最佳实践

组件内部代码组织顺序:

<template>
  <!-- 1. 模板 -->
</template>

<script>
// 2. 脚本
export default {
  // 组件选项顺序
  name: 'ComponentName',
  
  // 2.1 依赖注入
  components: {},
  directives: {},
  filters: {},
  
  // 2.2 组合式 API
  mixins: [],
  provide() {},
  inject: [],
  
  // 2.3 接口
  props: {},
  emits: [],
  
  // 2.4 本地状态
  setup() {}, // Vue 3
  data() {},  // Vue 2
  
  // 2.5 计算属性
  computed: {},
  
  // 2.6 侦听器
  watch: {},
  
  // 2.7 生命周期钩子(按调用顺序)
  beforeCreate() {},
  created() {},
  beforeMount() {},
  mounted() {},
  beforeUpdate() {},
  updated() {},
  activated() {},
  deactivated() {},
  beforeUnmount() {}, // Vue 3
  unmounted() {},     // Vue 3
  errorCaptured() {},
  
  // 2.8 方法
  methods: {},
  
  // 2.9 渲染函数
  render() {}
}
</script>

<style>
/* 3. 样式 */
</style>

代码拆分策略:

// 1. 按功能拆分组件
// ❌ 不好:一个组件做太多事
<template>
  <div class="user-profile">
    <div class="user-info">...</div>
    <div class="user-orders">...</div>
    <div class="user-settings">...</div>
  </div>
</template>

// ✅ 好:拆分为多个组件
<template>
  <div class="user-profile">
    <UserInfo :user="user" />
    <UserOrders :orders="orders" />
    <UserSettings :settings="settings" />
  </div>
</template>

📌 28. 测试(重要程度:⭐⭐⭐⭐)

28.1 测试金字塔

        /\
       /  \      E2E 测试(少量)
      /----\
     /      \    集成测试(适量)
    /--------\
   /          \  单元测试(大量)
  /------------\

28.2 单元测试(Jest)

安装和配置:

# Vue CLI 创建时选择 Unit Testing -> Jest
# 或手动安装
npm install --save-dev jest @vue/test-utils vue-jest babel-jest

Jest 配置:

// jest.config.js
module.exports = {
  preset: '@vue/cli-plugin-unit-jest',
  moduleFileExtensions: ['js', 'jsx', 'json', 'vue'],
  transform: {
    '^.+\\.vue$': 'vue-jest',
    '^.+\\.jsx?$': 'babel-jest'
  },
  moduleNameMapper: {
    '^@/(.*)$': '<rootDir>/src/$1'
  },
  testMatch: [
    '**/__tests__/**/*.spec.[jt]s?(x)',
    '**/?(*.)+(spec|test).[jt]s?(x)'
  ],
  collectCoverage: true,
  collectCoverageFrom: [
    'src/**/*.{js,vue}',
    '!src/main.js',
    '!src/router/index.js',
    '!**/node_modules/**'
  ],
  coverageReporters: ['html', 'text-summary']
}

组件单元测试示例:

// components/Button.spec.js
import { mount } from '@vue/test-utils'
import Button from './Button.vue'

describe('Button 组件', () => {
  // 测试渲染
  test('渲染默认按钮', () => {
    const wrapper = mount(Button)
    expect(wrapper.classes()).toContain('btn')
    expect(wrapper.classes()).toContain('btn-default')
  })
  
  // 测试 Props
  test('渲染不同类型按钮', () => {
    const wrapper = mount(Button, {
      props: {
        type: 'primary'
      }
    })
    expect(wrapper.classes()).toContain('btn-primary')
  })
  
  // 测试事件
  test('点击按钮触发事件', async () => {
    const wrapper = mount(Button)
    await wrapper.trigger('click')
    expect(wrapper.emitted().click).toBeTruthy()
    expect(wrapper.emitted().click).toHaveLength(1)
  })
  
  // 测试插槽
  test('渲染插槽内容', () => {
    const wrapper = mount(Button, {
      slots: {
        default: '点击我'
      }
    })
    expect(wrapper.text()).toBe('点击我')
  })
  
  // 测试样式
  test('禁用状态', () => {
    const wrapper = mount(Button, {
      props: {
        disabled: true
      }
    })
    expect(wrapper.attributes('disabled')).toBe('')
    expect(wrapper.classes()).toContain('disabled')
  })
})

组合式函数测试:

// composables/useCounter.spec.js
import { renderHook } from '@testing-library/vue'
import { useCounter } from './useCounter'

describe('useCounter 组合式函数', () => {
  test('初始化计数器', () => {
    const { result } = renderHook(() => useCounter(10))
    expect(result.count.value).toBe(10)
    expect(result.double.value).toBe(20)
  })
  
  test('增加计数器', () => {
    const { result } = renderHook(() => useCounter())
    result.increment()
    expect(result.count.value).toBe(1)
    expect(result.double.value).toBe(2)
  })
  
  test('重置计数器', () => {
    const { result } = renderHook(() => useCounter(5))
    result.reset()
    expect(result.count.value).toBe(0)
  })
})

28.3 组件测试(Vue Test Utils)

常用测试工具:

import { mount, shallowMount } from '@vue/test-utils'

// mount:完整挂载组件及其子组件
const wrapper = mount(Component, {
  // 配置选项
  props: { msg: 'Hello' },
  data() { return { count: 0 } },
  slots: { default: '内容' },
  global: {
    plugins: [router], // 全局插件
    components: { Child }, // 全局组件
    mocks: { $t: (key) => key } // 模拟全局对象
  }
})

// shallowMount:只挂载当前组件,子组件被替换为存根
const wrapper = shallowMount(Component)

// 常用断言方法
wrapper.vm // 组件实例
wrapper.html() // 渲染的 HTML
wrapper.text() // 文本内容
wrapper.classes() // class 列表
wrapper.attributes() // 属性
wrapper.find('.btn') // 查找元素
wrapper.findAll('.item') // 查找所有元素
wrapper.trigger('click') // 触发事件
wrapper.emitted() // 触发的事件
wrapper.setProps() // 设置 props
wrapper.setData() // 设置 data

测试异步操作:

// 测试异步组件
test('异步组件加载', async () => {
  const wrapper = mount(AsyncComponent)
  await flushPromises() // 等待所有 Promise 完成
  expect(wrapper.find('.content').exists()).toBe(true)
})

// 测试异步方法
test('异步方法调用', async () => {
  const mockApi = jest.fn().mockResolvedValue({ data: 'success' })
  const wrapper = mount(Component, {
    global: {
      mocks: { $api: mockApi }
    }
  })
  
  await wrapper.vm.fetchData()
  expect(mockApi).toHaveBeenCalledTimes(1)
  expect(wrapper.vm.data).toBe('success')
})

28.4 E2E 测试(Cypress)

安装和配置:

# 安装 Cypress
npm install cypress --save-dev

# 打开 Cypress
npx cypress open

Cypress 配置文件:

// cypress.config.js
const { defineConfig } = require('cypress')

module.exports = defineConfig({
  e2e: {
    baseUrl: 'http://localhost:8080',
    supportFile: 'cypress/support/e2e.js',
    specPattern: 'cypress/e2e/**/*.cy.{js,jsx,ts,tsx}',
    video: false,
    screenshotOnRunFailure: true
  },
  
  component: {
    devServer: {
      framework: 'vue',
      bundler: 'vite'
    }
  }
})

E2E 测试示例:

// cypress/e2e/login.cy.js
describe('登录功能', () => {
  beforeEach(() => {
    cy.visit('/login')
  })
  
  it('成功登录', () => {
    cy.get('[data-test="username"]').type('admin')
    cy.get('[data-test="password"]').type('password123')
    cy.get('[data-test="submit"]').click()
    
    cy.url().should('include', '/dashboard')
    cy.get('[data-test="welcome-message"]').should('contain', '欢迎回来')
  })
  
  it('登录失败显示错误信息', () => {
    cy.get('[data-test="username"]').type('wrong')
    cy.get('[data-test="password"]').type('wrong')
    cy.get('[data-test="submit"]').click()
    
    cy.get('[data-test="error-message"]')
      .should('be.visible')
      .and('contain', '用户名或密码错误')
  })
  
  it('表单验证', () => {
    cy.get('[data-test="submit"]').click()
    cy.get('[data-test="username-error"]').should('contain', '请输入用户名')
    cy.get('[data-test="password-error"]').should('contain', '请输入密码')
  })
})

组件测试(Cypress Component Testing):

// cypress/component/Button.cy.js
import Button from '../../src/components/Button.vue'

describe('Button.cy.js', () => {
  it('渲染按钮', () => {
    cy.mount(Button, {
      props: {
        type: 'primary'
      },
      slots: {
        default: '点击我'
      }
    })
    
    cy.get('button').should('have.class', 'btn-primary')
    cy.get('button').should('contain', '点击我')
  })
  
  it('触发点击事件', () => {
    const onClick = cy.spy()
    cy.mount(Button, {
      props: {
        onClick
      }
    })
    
    cy.get('button').click()
    cy.wrap(onClick).should('have.been.calledOnce')
  })
})

28.5 测试覆盖率

配置测试覆盖率:

// jest.config.js
module.exports = {
  collectCoverage: true,
  collectCoverageFrom: [
    'src/**/*.{js,vue}',
    '!src/main.js',
    '!src/router/index.js',
    '!src/**/*.spec.js',
    '!**/node_modules/**'
  ],
  coverageThreshold: {
    global: {
      branches: 80,    // 分支覆盖率
      functions: 80,   // 函数覆盖率
      lines: 80,       // 行覆盖率
      statements: 80   // 语句覆盖率
    }
  },
  coverageReporters: ['html', 'text', 'text-summary', 'lcov']
}

查看覆盖率报告:

# 运行测试并生成覆盖率报告
npm test -- --coverage

# 生成的报告在 coverage/ 目录下
# 打开 HTML 报告
open coverage/lcov-report/index.html

覆盖率报告示例:

----------------|---------|----------|---------|---------|-------------------
File            | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s 
----------------|---------|----------|---------|---------|-------------------
All files       |   85.71 |    77.78 |   83.33 |   85.71 |                   
 src            |   83.33 |       75 |   83.33 |   83.33 |                   
  App.vue       |     100 |      100 |     100 |     100 |                   
  main.js       |     100 |      100 |     100 |     100 |                   
 src/components |   85.71 |    77.78 |   83.33 |   85.71 |                   
  Button.vue    |     100 |      100 |     100 |     100 |                   
  Modal.vue     |      75 |    66.67 |      75 |      75 | 25-26,35-36       
----------------|---------|----------|---------|---------|-------------------


📌 29. TypeScript 支持(重要程度:⭐⭐⭐⭐⭐)

29.1 Vue 3 + TypeScript 基础

创建 TypeScript 项目:

# Vue CLI
vue create my-project --default --typescript

# Vite
npm create vite@latest my-project -- --template vue-ts

项目结构:

src/
├── shims-vue.d.ts          # Vue 类型声明
├── main.ts                 # TypeScript 入口
├── App.vue
└── components/
    └── HelloWorld.vue

Vue 类型声明文件:

// shims-vue.d.ts
declare module '*.vue' {
  import { DefineComponent } from 'vue'
  const component: DefineComponent<{}, {}, any>
  export default component
}

29.2 类型定义

Props 类型定义:

<script setup lang="ts">
// 使用 defineProps 定义 props 类型
interface Props {
  title: string
  count?: number // 可选
  items: string[]
  disabled: boolean
  onAction?: () => void // 函数类型
}

const props = defineProps<Props>()

// 使用 withDefaults 定义默认值
const props = withDefaults(defineProps<Props>(), {
  title: '默认标题',
  count: 0,
  items: () => ['item1', 'item2'],
  disabled: false
})
</script>

Emits 类型定义:

<script setup lang="ts">
// 定义 emits 类型
interface Emits {
  (e: 'update:title', value: string): void
  (e: 'submit', payload: { id: number; data: any }): void
  (e: 'cancel'): void
}

const emit = defineEmits<Emits>()

// 使用 emit
const handleClick = () => {
  emit('update:title', '新标题')
  emit('submit', { id: 1, data: {} })
}
</script>

Reactive 状态类型:

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

// 用户接口
interface User {
  id: number
  name: string
  email: string
  age?: number
}

// 使用 ref 定义响应式数据
const userId = ref<number>(0) // 明确类型
const userName = ref<string>('')
const user = ref<User | null>(null) // 可空类型

// 使用 reactive 定义响应式对象
const state = reactive<{
  loading: boolean
  error: string | null
  data: User[]
}>({
  loading: false,
  error: null,
  data: []
})
</script>

29.3 组件类型

组件实例类型:

// 组件实例类型
import { ComponentPublicInstance, Ref } from 'vue'

// 组件实例
const instance: ComponentPublicInstance = getComponentInstance()

// 模板引用类型
const formRef = ref<InstanceType<typeof FormComponent>>()

// 访问组件实例的方法
const handleSubmit = () => {
  if (formRef.value) {
    formRef.value.validate()
  }
}

全局组件类型:

// 全局组件类型注册
declare module 'vue' {
  export interface GlobalComponents {
    RouterLink: typeof import('vue-router')['RouterLink']
    RouterView: typeof import('vue-router')['RouterView']
    // 自定义全局组件
    BaseButton: typeof import('./components/BaseButton.vue')['default']
    BaseInput: typeof import('./components/BaseInput.vue')['default']
  }
}

29.4 组合式 API 类型

组合式函数类型:

// composables/useFetch.ts
import { ref, Ref } from 'vue'

// 返回值类型
interface UseFetchReturn<T> {
  data: Ref<T | null>
  error: Ref<string | null>
  loading: Ref<boolean>
  execute: () => Promise<void>
}

// 参数类型
interface UseFetchOptions {
  immediate?: boolean
  onSuccess?: (data: any) => void
  onError?: (error: Error) => void
}

export function useFetch<T = any>(
  url: string,
  options?: UseFetchOptions
): UseFetchReturn<T> {
  const data = ref<T | null>(null)
  const error = ref<string | null>(null)
  const loading = ref(false)
  
  const execute = async () => {
    // 实现...
  }
  
  return {
    data,
    error,
    loading,
    execute
  }
}

泛型组合式函数:

// composables/usePagination.ts
interface PaginationOptions<T> {
  pageSize: number
  fetchData: (page: number, size: number) => Promise<{
    data: T[]
    total: number
  }>
}

interface PaginationResult<T> {
  data: Ref<T[]>
  loading: Ref<boolean>
  page: Ref<number>
  total: Ref<number>
  next: () => Promise<void>
  prev: () => Promise<void>
  goTo: (page: number) => Promise<void>
}

export function usePagination<T>(
  options: PaginationOptions<T>
): PaginationResult<T> {
  // 实现...
}

29.5 类型工具函数

常用类型工具:

// 1. 提取 Promise 的类型
type UnwrapPromise<T> = T extends Promise<infer U> ? U : T

// 2. 提取数组元素的类型
type ArrayElement<T> = T extends Array<infer U> ? U : never

// 3. 提取函数返回值的类型
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never

// 4. 提取函数参数的类型
type Parameters<T> = T extends (...args: infer P) => any ? P : never

// 5. 可选化部分属性
type PartialBy<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>

// 6. 必选化部分属性
type RequiredBy<T, K extends keyof T> = Omit<T, K> & Required<Pick<T, K>>

在 Vue 中使用类型工具:

import { computed, Ref } from 'vue'

// 计算属性的类型推断
const doubleCount = computed(() => count.value * 2) // 自动推断为 number

// 明确指定计算属性类型
const fullName = computed<string>(() => `${firstName} ${lastName}`)

// 使用类型守卫
function isUser(obj: any): obj is User {
  return obj && typeof obj.id === 'number' && typeof obj.name === 'string'
}

// 使用类型断言
const userData = data.value as User
const userData = <User>data.value

// 使用非空断言
const userId = ref.value!.id // 确保 ref.value 不为 null

29.6 第三方库类型支持

为第三方库添加类型:

// 1. 使用现有的类型定义
npm install --save-dev @types/lodash
npm install --save-dev @types/axios

// 2. 自定义类型声明
// src/types/global.d.ts
declare module 'some-library' {
  export interface SomeType {
    // 类型定义
  }
  
  export function someFunction(): void
}

Vuex + TypeScript:

// store/types.ts
export interface User {
  id: number
  name: string
  email: string
}

export interface State {
  user: User | null
  token: string | null
  loading: boolean
}

// store/index.ts
import { InjectionKey } from 'vue'
import { createStore, Store } from 'vuex'

export const key: InjectionKey<Store<State>> = Symbol()

export const store = createStore<State>({
  state: {
    user: null,
    token: null,
    loading: false
  },
  mutations: {
    SET_USER(state, payload: User) {
      state.user = payload
    }
  },
  actions: {
    async login({ commit }, credentials: { email: string; password: string }) {
      // 实现...
    }
  }
})

Pinia + TypeScript(推荐):

// stores/user.ts
import { defineStore } from 'pinia'

interface User {
  id: number
  name: string
  email: string
}

interface UserState {
  user: User | null
  token: string | null
}

export const useUserStore = defineStore('user', {
  state: (): UserState => ({
    user: null,
    token: null
  }),
  
  getters: {
    isLoggedIn: (state): boolean => !!state.token,
    userName: (state): string => state.user?.name || '访客'
  },
  
  actions: {
    async login(credentials: { email: string; password: string }) {
      // 实现...
    }
  }
})

🎯 重点总结与使用场景

必须掌握的核心知识点:

  1. 项目结构和组织(⭐⭐⭐⭐⭐)

    • 所有项目的基础
    • 团队协作和项目维护
  2. 代码规范和 ESLint(⭐⭐⭐⭐⭐)

    • 团队开发必备
    • 保证代码质量和一致性
  3. TypeScript 支持(⭐⭐⭐⭐⭐)

    • 大型项目和团队开发
    • 需要良好类型安全的项目

重要工程化实践:

  1. 测试策略(⭐⭐⭐⭐)

    • 需要稳定性和质量保证的项目
    • 企业级应用和长期维护的项目
  2. 构建优化(⭐⭐⭐⭐)

    • 生产环境部署
    • 性能敏感的应用
  3. 环境配置(⭐⭐⭐)

    • 多环境部署(开发、测试、生产)
    • 持续集成/持续部署

学习路径建议:

学习阶段重点内容项目实践
初学者Vue CLI/Vite 基础使用创建简单项目
进阶者项目结构优化、ESLint重构现有项目结构
团队开发代码规范、Git 工作流参与团队项目
专业级TypeScript、完整测试企业级项目开发
架构师构建优化、部署策略从零搭建项目架构

最佳实践总结:

  1. 始终使用版本控制:Git + 合适的 Git 工作流
  2. 代码规范先行:项目开始时配置好 ESLint + Prettier
  3. 组件设计原则:单一职责、可复用、可测试
  4. 渐进式采用:不要一次性引入所有复杂工具
  5. 文档化:README、组件文档、API 文档
  6. 持续集成:自动化测试、构建、部署

💡 实战技巧

1. 多环境部署配置

// vue.config.js
const isProduction = process.env.NODE_ENV === 'production'

module.exports = {
  publicPath: process.env.VUE_APP_PUBLIC_PATH || '/',
  
  // 不同环境的构建配置
  configureWebpack: config => {
    if (isProduction) {
      // 生产环境配置
      config.optimization.minimizer[0].options.terserOptions.compress.drop_console = true
    } else {
      // 开发环境配置
      config.devtool = 'source-map'
    }
  },
  
  chainWebpack: config => {
    // 根据环境设置 title
    config.plugin('html').tap(args => {
      args[0].title = process.env.VUE_APP_TITLE
      return args
    })
  }
}

2. 自动化部署脚本

// package.json
{
  "scripts": {
    "dev": "vue-cli-service serve",
    "build:dev": "vue-cli-service build --mode development",
    "build:test": "vue-cli-service build --mode test",
    "build:prod": "vue-cli-service build --mode production",
    "test:unit": "vue-cli-service test:unit",
    "test:e2e": "vue-cli-service test:e2e",
    "lint": "vue-cli-service lint",
    "deploy:dev": "npm run build:dev && scp -r dist/* user@dev-server:/path/to/app",
    "deploy:prod": "npm run build:prod && scp -r dist/* user@prod-server:/path/to/app"
  }
}

3. 性能监控集成

// src/utils/performance.js
export function trackPerformance() {
  // 监控页面加载性能
  window.addEventListener('load', () => {
    const [entry] = performance.getEntriesByType('navigation')
    console.log('页面加载时间:', entry.loadEventEnd - entry.startTime)
  })
  
  // 监控组件渲染性能(开发环境)
  if (process.env.NODE_ENV === 'development') {
    const { renderTracked, renderTriggered } = require('@vue/reactivity')
    
    renderTracked((e) => {
      console.log('依赖追踪:', e)
    })
    
    renderTriggered((e) => {
      console.log('渲染触发:', e)
    })
  }
}

工程化是 Vue.js 开发从"业余"走向"专业"的关键。这部分知识的学习曲线较陡,但回报也最大。建议从代码规范项目结构入手,逐步学习TypeScript测试,最后掌握构建优化部署

记住,好的工程化实践应该服务于项目需求,而不是为了复杂而复杂。始终以可维护性开发效率为目标来选择和实施工程化方案。

你已经掌握了 Vue.js 开发的核心技能,下一步就是将它们应用到实际项目中,通过实践不断深化理解! 🚀


Vue.js 性能优化全面指南:从监控到极致优化

第九部分:性能优化

性能优化是专业前端开发的核心竞争力,直接影响用户体验、转化率和SEO排名。这部分知识将帮助你从"能用"升级到"好用、快用"。


📌 30. 性能监控(重要程度:⭐⭐⭐⭐)

30.1 Vue Devtools 使用

Vue Devtools 是性能分析的瑞士军刀:

安装和配置:

# Chrome 商店安装 Vue Devtools
# 或手动安装
npm install -g @vue/devtools

主要功能详解:

  1. 组件树分析

    // 查看组件层级和嵌套深度
    // 过深的组件嵌套会影响性能
    // 建议:保持组件树深度在5层以内
    
  2. 性能时间线

    // 1. 开启性能记录
    // 2. 执行用户操作
    // 3. 分析时间线,找出性能瓶颈
    
    // 关键指标:
    // - Component render: 组件渲染时间
    // - Component patch: 组件更新时间
    // - Event handlers: 事件处理时间
    
  3. 事件追踪

    // 跟踪组件发出的事件
    // 分析事件触发频率和性能影响
    
  4. 状态快照

    // 保存和加载状态快照
    // 用于调试复杂状态变化
    

自定义性能监控:

// 创建性能监控插件
const performancePlugin = {
  install(app) {
    // 记录组件渲染时间
    app.config.performance = true
    
    // 自定义性能指标
    app.config.globalProperties.$perf = {
      start(name) {
        performance.mark(`${name}-start`)
      },
      end(name) {
        performance.mark(`${name}-end`)
        performance.measure(name, `${name}-start`, `${name}-end`)
        
        const measure = performance.getEntriesByName(name)[0]
        console.log(`${name}: ${measure.duration.toFixed(2)}ms`)
        
        // 清理
        performance.clearMarks(`${name}-start`)
        performance.clearMarks(`${name}-end`)
        performance.clearMeasures(name)
      }
    }
  }
}

30.2 性能测量工具

核心Web性能指标(Core Web Vitals):

// 1. LCP(最大内容绘制):< 2.5秒
// 测量视口中最大内容元素的渲染时间

// 2. FID(首次输入延迟):< 100毫秒
// 测量用户首次交互到浏览器响应的时间

// 3. CLS(累积布局偏移):< 0.1
// 测量页面视觉稳定性

测量工具矩阵:

工具用途使用场景
Lighthouse综合性能评估开发环境、构建时
WebPageTest深度性能分析竞品分析、发布前
Chrome DevTools实时性能分析开发调试
PageSpeed Insights生产环境分析线上监控
Vue.js devtoolsVue特定分析Vue应用调优

Lighthouse集成:

// 1. 命令行使用
npx lighthouse https://example.com --view

// 2. Node.js API集成
const lighthouse = require('lighthouse')
const chromeLauncher = require('chrome-launcher')

async function runLighthouse(url) {
  const chrome = await chromeLauncher.launch()
  const options = {
    output: 'html',
    onlyCategories: ['performance'],
    port: chrome.port
  }
  
  const results = await lighthouse(url, options)
  console.log('性能得分:', results.lhr.categories.performance.score * 100)
  
  await chrome.kill()
  return results
}

// 3. 在CI/CD中集成
// package.json
{
  "scripts": {
    "audit": "lighthouse-batch -f urls.txt --html"
  }
}

Chrome Performance Tab高级用法:

// 1. 录制性能时间线
// - 点击Record按钮
// - 执行用户操作
// - 停止录制并分析

// 2. 关键性能指标分析
// - FPS: 帧率(应保持在60fps)
// - CPU使用率
// - 网络请求
// - 内存使用

// 3. 性能优化建议
// - 减少强制同步布局
// - 避免长任务(>50ms)
// - 优化JavaScript执行

30.3 关键性能指标

自定义性能监控系统:

// src/utils/performance.js
class PerformanceMonitor {
  constructor() {
    this.metrics = new Map()
    this.observers = []
    
    // 监控关键性能指标
    if ('PerformanceObserver' in window) {
      this.initObservers()
    }
  }
  
  initObservers() {
    // 监控长任务(>50ms)
    this.observeLongTasks()
    
    // 监控布局偏移
    this.observeLayoutShift()
    
    // 监控最大内容绘制
    this.observeLCP()
    
    // 监控首次输入延迟
    this.observeFID()
  }
  
  observeLongTasks() {
    const observer = new PerformanceObserver((list) => {
      for (const entry of list.getEntries()) {
        console.warn('长任务检测:', {
          duration: entry.duration,
          startTime: entry.startTime,
          name: entry.name
        })
        
        // 发送到监控系统
        this.report('long-task', entry.duration)
      }
    })
    
    observer.observe({ entryTypes: ['longtask'] })
  }
  
  observeLayoutShift() {
    let cls = 0
    const observer = new PerformanceObserver((list) => {
      for (const entry of list.getEntries()) {
        if (!entry.hadRecentInput) {
          cls += entry.value
          console.warn('布局偏移:', { cls, entry })
          this.report('layout-shift', cls)
        }
      }
    })
    
    observer.observe({ entryTypes: ['layout-shift'] })
    
    // 页面可见性变化时报告CLS
    document.addEventListener('visibilitychange', () => {
      if (document.visibilityState === 'hidden') {
        this.report('final-cls', cls)
      }
    })
  }
  
  observeLCP() {
    let lcp
    const observer = new PerformanceObserver((list) => {
      const entries = list.getEntries()
      lcp = entries[entries.length - 1]
      
      console.log('LCP候选:', lcp)
    })
    
    observer.observe({ entryTypes: ['largest-contentful-paint'] })
    
    // 页面隐藏时上报最终LCP
    document.addEventListener('visibilitychange', () => {
      if (lcp && document.visibilityState === 'hidden') {
        this.report('lcp', lcp.startTime)
        observer.disconnect()
      }
    })
  }
  
  observeFID() {
    const observer = new PerformanceObserver((list) => {
      for (const entry of list.getEntries()) {
        const delay = entry.processingStart - entry.startTime
        
        console.log('FID测量:', { delay, entry })
        this.report('fid', delay)
      }
    })
    
    observer.observe({ entryTypes: ['first-input'] })
  }
  
  startMeasurement(name) {
    const start = performance.now()
    
    return {
      end: () => {
        const duration = performance.now() - start
        this.metrics.set(name, duration)
        console.log(`[性能] ${name}: ${duration.toFixed(2)}ms`)
        return duration
      }
    }
  }
  
  report(type, value) {
    // 发送到监控服务
    if (navigator.sendBeacon) {
      const data = JSON.stringify({
        type,
        value,
        timestamp: Date.now(),
        url: window.location.href,
        userAgent: navigator.userAgent
      })
      
      navigator.sendBeacon('/api/performance', data)
    }
  }
  
  // 获取性能报告
  getReport() {
    const report = {
      timestamp: new Date().toISOString(),
      url: window.location.href,
      metrics: Object.fromEntries(this.metrics),
      coreWebVitals: {
        lcp: this.getMetric('lcp'),
        fid: this.getMetric('fid'),
        cls: this.getMetric('final-cls')
      }
    }
    
    // 添加更多性能指标
    if ('memory' in performance) {
      report.memory = performance.memory
    }
    
    return report
  }
  
  getMetric(name) {
    return this.metrics.get(name)
  }
}

// 全局性能监控实例
export const perf = new PerformanceMonitor()

// Vue插件形式
export const PerformancePlugin = {
  install(app) {
    app.config.globalProperties.$perf = perf
    app.provide('performance', perf)
  }
}

在Vue组件中使用:

<script setup>
import { perf } from '@/utils/performance'

// 监控组件渲染性能
const measure = perf.startMeasurement('ComponentRender')

onMounted(() => {
  const duration = measure.end()
  
  if (duration > 100) {
    console.warn('组件渲染时间过长:', duration)
  }
})

// 监控方法执行时间
function complexCalculation() {
  const measure = perf.startMeasurement('ComplexCalculation')
  
  // 复杂计算逻辑
  // ...
  
  measure.end()
}
</script>

📌 31. 优化技巧(重要程度:⭐⭐⭐⭐⭐)

31.1 组件懒加载(重要)

路由懒加载(必须掌握):

// router/index.js
// ❌ 静态导入(所有路由组件打包到主包)
import Home from '@/views/Home.vue'
import About from '@/views/About.vue'

// ✅ 动态导入(按需加载)
const Home = () => import('@/views/Home.vue')
const About = () => import('@/views/About.vue')

// 带加载状态的懒加载
const UserProfile = () => ({
  component: import('@/views/UserProfile.vue'),
  loading: LoadingComponent,  // 加载中组件
  error: ErrorComponent,      // 错误组件
  timeout: 10000             // 超时时间
})

const routes = [
  { path: '/', component: Home },
  { path: '/about', component: About },
  { path: '/profile', component: UserProfile }
]

组件级懒加载:

<template>
  <div>
    <!-- 按需加载大型组件 -->
    <UserList v-if="showList" />
    <ProductGrid v-if="showGrid" />
  </div>
</template>

<script setup>
import { defineAsyncComponent, ref } from 'vue'

// 异步组件
const UserList = defineAsyncComponent(() => import('./UserList.vue'))
const ProductGrid = defineAsyncComponent({
  loader: () => import('./ProductGrid.vue'),
  delay: 200, // 延迟显示加载组件
  timeout: 3000, // 超时时间
  suspensible: true // 支持Suspense
})

const showList = ref(false)
const showGrid = ref(false)

// 需要时加载
function loadComponents() {
  showList.value = true
  showGrid.value = true
}
</script>

基于Webpack的优化:

// 魔法注释(Webpack特性)
const Home = () => import(
  /* webpackChunkName: "home" */
  /* webpackPrefetch: true */  // 空闲时预加载
  /* webpackPreload: true */   // 优先级更高
  '@/views/Home.vue'
)

// 分组打包
const Home = () => import(/* webpackChunkName: "group-home" */ './Home.vue')
const About = () => import(/* webpackChunkName: "group-home" */ './About.vue')
const Contact = () => import(/* webpackChunkName: "group-home" */ './Contact.vue')
// 这三个组件会打包到同一个chunk

31.2 代码分割

Vue CLI代码分割配置:

// vue.config.js
module.exports = {
  configureWebpack: {
    optimization: {
      splitChunks: {
        chunks: 'all',
        minSize: 20000, // 最小大小(20KB)
        maxSize: 0,
        minChunks: 1, // 最少被引用次数
        maxAsyncRequests: 30,
        maxInitialRequests: 30,
        automaticNameDelimiter: '~',
        cacheGroups: {
          // Vue相关库
          vue: {
            name: 'vue',
            test: /[\\/]node_modules[\\/](vue|vue-router|vuex)[\\/]/,
            priority: 40,
            chunks: 'all'
          },
          // UI组件库
          elementUI: {
            name: 'element-ui',
            test: /[\\/]node_modules[\\/]element-plus[\\/]/,
            priority: 30
          },
          // 公共库
          vendors: {
            name: 'vendors',
            test: /[\\/]node_modules[\\/]/,
            priority: 20
          },
          // 公共代码
          common: {
            name: 'common',
            minChunks: 2, // 被2个以上chunk引用
            priority: 10,
            reuseExistingChunk: true
          }
        }
      }
    }
  }
}

Vite代码分割配置:

// vite.config.js
export default defineConfig({
  build: {
    rollupOptions: {
      output: {
        // 手动配置代码分割
        manualChunks: {
          // Vue生态
          'vue-vendor': ['vue', 'vue-router', 'vuex', 'pinia'],
          
          // UI库
          'ui-library': ['element-plus'],
          
          // 工具库
          'utils': ['axios', 'lodash-es', 'dayjs'],
          
          // 根据目录自动分割
          'components': (id) => {
            if (id.includes('node_modules')) return
            if (id.includes('/components/')) {
              return 'components'
            }
          },
          
          // 动态分割策略
          'pages': (id) => {
            if (id.includes('node_modules')) return
            if (id.includes('/views/')) {
              // 进一步按页面类型分割
              if (id.includes('/views/admin/')) return 'admin-pages'
              if (id.includes('/views/user/')) return 'user-pages'
              return 'common-pages'
            }
          }
        },
        
        // 优化chunk命名
        chunkFileNames: 'assets/js/[name]-[hash].js',
        entryFileNames: 'assets/js/[name]-[hash].js',
        assetFileNames: 'assets/[ext]/[name]-[hash].[ext]'
      }
    },
    
    // 开启压缩
    minify: 'terser',
    terserOptions: {
      compress: {
        drop_console: true,
        drop_debugger: true
      }
    },
    
    // 启用CSS代码分割
    cssCodeSplit: true,
    
    // 资产大小限制
    assetsInlineLimit: 4096 // 4KB以下转base64
  }
})

31.3 虚拟滚动(核心优化)

虚拟滚动原理:

<template>
  <div class="virtual-scroll" ref="container" @scroll="handleScroll">
    <!-- 占位元素,撑开滚动高度 -->
    <div class="scroll-phantom" :style="{ height: totalHeight + 'px' }"></div>
    
    <!-- 可见区域 -->
    <div class="scroll-content" :style="{ transform: `translateY(${offsetY}px)` }">
      <div v-for="item in visibleItems" :key="item.id" class="scroll-item">
        {{ item.content }}
      </div>
    </div>
  </div>
</template>

<script setup>
import { ref, computed, onMounted, onUnmounted } from 'vue'

const props = defineProps({
  items: { type: Array, required: true },
  itemHeight: { type: Number, default: 50 },
  buffer: { type: Number, default: 5 } // 缓冲项数
})

const container = ref(null)
const scrollTop = ref(0)

// 总高度
const totalHeight = computed(() => props.items.length * props.itemHeight)

// 可见项数
const visibleCount = computed(() => {
  if (!container.value) return 0
  return Math.ceil(container.value.clientHeight / props.itemHeight)
})

// 开始索引
const startIndex = computed(() => {
  return Math.max(0, Math.floor(scrollTop.value / props.itemHeight) - props.buffer)
})

// 结束索引
const endIndex = computed(() => {
  return Math.min(
    props.items.length,
    startIndex.value + visibleCount.value + props.buffer * 2
  )
})

// 可见项
const visibleItems = computed(() => {
  return props.items.slice(startIndex.value, endIndex.value)
})

// Y轴偏移
const offsetY = computed(() => {
  return startIndex.value * props.itemHeight
})

// 滚动处理
function handleScroll() {
  if (container.value) {
    scrollTop.value = container.value.scrollTop
  }
}

// 性能优化:节流滚动
let scrollTimeout
function throttledScroll() {
  clearTimeout(scrollTimeout)
  scrollTimeout = setTimeout(handleScroll, 16) // 约60fps
}

// 虚拟滚动库推荐
// - vue-virtual-scroller
// - vue-virtual-scroll-list
// - vue-virtual-scroll-grid
</script>

<style scoped>
.virtual-scroll {
  position: relative;
  height: 500px;
  overflow-y: auto;
}

.scroll-phantom {
  position: absolute;
  left: 0;
  top: 0;
  right: 0;
  z-index: -1;
}

.scroll-content {
  position: absolute;
  left: 0;
  right: 0;
  top: 0;
}

.scroll-item {
  height: 50px;
  display: flex;
  align-items: center;
  border-bottom: 1px solid #eee;
}
</style>

使用虚拟滚动库:

<!-- 使用 vue-virtual-scroller -->
<template>
  <RecycleScroller
    class="scroller"
    :items="items"
    :item-size="itemHeight"
    key-field="id"
    v-slot="{ item }"
  >
    <div class="item">
      {{ item.content }}
    </div>
  </RecycleScroller>
</template>

<script setup>
import { RecycleScroller } from 'vue-virtual-scroller'
import 'vue-virtual-scroller/dist/vue-virtual-scroller.css'

const items = ref([
  /* 大量数据 */
])
const itemHeight = 50
</script>

<style scoped>
.scroller {
  height: 500px;
}
</style>

31.4 列表优化

关键优化技巧:

  1. 始终使用key

    <!-- ❌ 错误 -->
    <div v-for="item in items">
      {{ item.name }}
    </div>
    
    <!-- ✅ 正确 -->
    <div v-for="item in items" :key="item.id">
      {{ item.name }}
    </div>
    
    <!-- ✅ 复杂场景使用组合key -->
    <div v-for="item in items" :key="`${item.id}-${item.version}`">
      {{ item.name }}
    </div>
    
  2. 避免v-for和v-if一起使用

    <!-- ❌ 错误:每次渲染都会重新计算 -->
    <div v-for="user in users" v-if="user.isActive">
      {{ user.name }}
    </div>
    
    <!-- ✅ 正确:使用计算属性过滤 -->
    <template>
      <div v-for="user in activeUsers" :key="user.id">
        {{ user.name }}
      </div>
    </template>
    
    <script setup>
    const activeUsers = computed(() => {
      return users.value.filter(user => user.isActive)
    })
    </script>
    
    <!-- ✅ 另一种方案:使用template -->
    <template v-for="user in users" :key="user.id">
      <div v-if="user.isActive">
        {{ user.name }}
      </div>
    </template>
    
  3. 冻结大数据

    // 大数据列表冻结,避免Vue响应式开销
    import { markRaw } from 'vue'
    
    const largeList = ref(
      Object.freeze( // Object.freeze防止修改
        markRaw( // markRaw跳过响应式转换
          Array.from({ length: 10000 }, (_, i) => ({
            id: i,
            name: `Item ${i}`,
            // 注意:内部对象不会被冻结
            data: markRaw({ value: i * 2 })
          }))
        )
      )
    )
    
    // 使用shallowRef进一步优化
    const largeList = shallowRef([])
    
    // 更新时直接替换整个数组
    function loadMoreData() {
      const newData = [...largeList.value, ...fetchNewData()]
      largeList.value = newData
    }
    
  4. 分页和无限滚动

    <template>
      <div class="infinite-list">
        <div v-for="item in visibleItems" :key="item.id">
          {{ item.content }}
        </div>
        
        <div v-if="loading" class="loading">
          加载中...
        </div>
        
        <div v-if="hasMore" class="load-more" ref="loader">
          <button @click="loadMore">加载更多</button>
        </div>
      </div>
    </template>
    
    <script setup>
    import { ref, computed, onMounted, onUnmounted } from 'vue'
    import { useIntersectionObserver } from '@vueuse/core'
    
    const items = ref([])
    const page = ref(1)
    const loading = ref(false)
    const hasMore = ref(true)
    const loader = ref(null)
    
    // 可见项(分页)
    const visibleItems = computed(() => {
      return items.value.slice(0, page.value * 20)
    })
    
    // 加载更多
    async function loadMore() {
      if (loading.value || !hasMore.value) return
      
      loading.value = true
      try {
        const newItems = await fetchItems(page.value + 1)
        
        if (newItems.length === 0) {
          hasMore.value = false
        } else {
          items.value = [...items.value, ...newItems]
          page.value++
        }
      } catch (error) {
        console.error('加载失败:', error)
      } finally {
        loading.value = false
      }
    }
    
    // 使用IntersectionObserver自动加载
    onMounted(() => {
      if (loader.value) {
        useIntersectionObserver(
          loader,
          ([{ isIntersecting }]) => {
            if (isIntersecting) {
              loadMore()
            }
          }
        )
      }
    })
    </script>
    

31.5 计算属性缓存

计算属性高级优化:

  1. 缓存策略

    const expensiveData = computed(() => {
      // 复杂计算
      const result = heavyCalculation(data.value)
      
      // 缓存中间结果
      const cacheKey = JSON.stringify(data.value)
      if (!cache[cacheKey]) {
        cache[cacheKey] = result
      }
      
      return cache[cacheKey] || result
    })
    
  2. 惰性计算

    import { computed, reactive } from 'vue'
    
    function useLazyComputed(getter) {
      const result = reactive({
        value: null,
        dirty: true // 脏检查标记
      })
      
      const computedValue = computed(getter)
      
      // 监听依赖变化,标记为脏
      computedValue.effect.onStop(() => {
        result.dirty = true
      })
      
      return {
        get value() {
          if (result.dirty) {
            result.value = computedValue.value
            result.dirty = false
          }
          return result.value
        },
        
        // 手动标记为脏
        markDirty() {
          result.dirty = true
        }
      }
    }
    
  3. 记忆化计算属性

    import { ref, computed } from 'vue'
    import memoize from 'lodash/memoize'
    
    // 使用lodash的memoize
    const expensiveComputation = memoize((input) => {
      // 昂贵计算
      return input * 2
    })
    
    const inputValue = ref(0)
    const computedValue = computed(() => {
      return expensiveComputation(inputValue.value)
    })
    
  4. 防抖计算属性

    import { ref, customRef } from 'vue'
    
    // 防抖ref
    function useDebouncedRef(value, delay = 200) {
      let timeout
      return customRef((track, trigger) => {
        return {
          get() {
            track()
            return value
          },
          set(newValue) {
            clearTimeout(timeout)
            timeout = setTimeout(() => {
              value = newValue
              trigger()
            }, delay)
          }
        }
      })
    }
    
    // 防抖计算属性
    const searchQuery = useDebouncedRef('')
    const searchResults = computed(() => {
      // 这个计算属性只有在searchQuery稳定后才会重新计算
      return fetchSearchResults(searchQuery.value)
    })
    

📌 32. 服务端渲染(重要程度:⭐⭐⭐)

32.1 SSR 概念

SSR核心原理:

传统SPA渲染流程:
1. 浏览器请求HTML
2. 服务器返回空HTML和JS
3. 浏览器下载并执行JS
4. Vue应用挂载,渲染界面

SSR渲染流程:
1. 浏览器请求HTML
2. 服务器运行Vue应用,渲染为完整HTML
3. 服务器返回完整HTML
4. 浏览器立即显示内容
5. Vue应用在客户端"激活"

SSR vs CSR对比:

方面客户端渲染(CSR)服务端渲染(SSR)
首屏加载慢(需要下载所有JS)快(立即显示HTML)
SEO差(搜索引擎难抓取)好(完整HTML内容)
服务器负载低(静态文件)高(动态渲染)
用户体验初始白屏时间长立即显示内容
开发复杂度简单复杂(需要考虑同构)

32.2 Nuxt.js 介绍

Nuxt.js项目结构:

my-nuxt-app/
├── pages/          # 自动生成路由
│   ├── index.vue   # /
│   ├── about.vue   # /about
│   └── user/
│       ├── index.vue     # /user
│       └── [id].vue      # /user/:id
├── components/     # 组件
├── layouts/        # 布局
├── middleware/     # 中间件
├── plugins/        # 插件
├── static/         # 静态文件
├── store/          # Vuex状态管理
├── nuxt.config.js  # Nuxt配置
└── package.json

基础配置:

// nuxt.config.js
export default {
  // 渲染模式
  ssr: true, // 开启SSR(默认)
  
  // 目标部署平台
  target: 'server', // 或 'static'(静态站点生成)
  
  // 应用头
  head: {
    title: '我的Nuxt应用',
    meta: [
      { charset: 'utf-8' },
      { name: 'viewport', content: 'width=device-width, initial-scale=1' }
    ]
  },
  
  // CSS配置
  css: ['@/assets/css/main.css'],
  
  // 插件
  plugins: [
    '@/plugins/axios.js',
    '@/plugins/vant.js'
  ],
  
  // 构建配置
  build: {
    // 优化配置
    optimizeCSS: true,
    extractCSS: true,
    
    // 代码分割
    splitChunks: {
      layouts: true,
      pages: true,
      commons: true
    },
    
    // 扩展webpack配置
    extend(config, { isClient, isServer }) {
      if (isServer) {
        // 服务器端特定配置
      }
    }
  },
  
  // 模块
  modules: [
    '@nuxtjs/axios',
    '@nuxtjs/auth'
  ],
  
  // 环境变量
  env: {
    API_BASE_URL: process.env.API_BASE_URL || 'http://localhost:3000'
  }
}

Nuxt.js页面组件:

<!-- pages/index.vue -->
<template>
  <div>
    <h1>欢迎来到Nuxt.js</h1>
    <p>当前时间: {{ currentTime }}</p>
  </div>
</template>

<script>
export default {
  // 异步数据获取(在服务器端执行)
  async asyncData({ $axios }) {
    try {
      const data = await $axios.get('/api/data')
      return { data }
    } catch (error) {
      return { data: null }
    }
  },
  
  // 在服务器端和客户端都会执行
  data() {
    return {
      currentTime: null
    }
  },
  
  // 只会在客户端执行
  mounted() {
    this.currentTime = new Date().toLocaleString()
  },
  
  // 头部配置(支持SEO)
  head() {
    return {
      title: '首页',
      meta: [
        { hid: 'description', name: 'description', content: '这是首页描述' }
      ]
    }
  }
}
</script>

32.3 同构应用

同构应用注意事项:

// 1. 避免全局副作用
// ❌ 错误:在组件外使用全局变量
let count = 0

// ✅ 正确:在生命周期钩子中使用
export default {
  data() {
    return { count: 0 }
  }
}

// 2. 特定API的平台兼容性
export default {
  mounted() {
    // 只在客户端执行的代码
    if (process.client) {
      window.addEventListener('resize', this.handleResize)
    }
  },
  
  methods: {
    handleResize() {
      // 客户端特定逻辑
    }
  }
}

// 3. 数据预取
export default {
  async asyncData({ params, $axios }) {
    // 服务器端数据预取
    const { data } = await $axios.get(`/api/user/${params.id}`)
    return { user: data }
  },
  
  data() {
    return {
      user: null
    }
  },
  
  mounted() {
    // 客户端数据补全
    if (!this.user) {
      this.fetchUserData()
    }
  }
}

Nuxt.js生命周期:

服务器端生命周期:
1. nuxtServerInit (store) - 初始化store
2. middleware - 路由中间件
3. validate - 路由参数验证
4. asyncData / fetch - 数据预取
5. beforeCreate
6. created

客户端生命周期:
1. beforeCreate (再次执行)
2. created (再次执行)
3. beforeMount
4. mounted

32.4 Hydration

Hydration过程详解:

// 1. 服务器端渲染生成HTML
const { renderToString } = require('@vue/server-renderer')

async function renderApp(app) {
  const html = await renderToString(app)
  return `
    <!DOCTYPE html>
    <html>
      <head>
        <title>SSR App</title>
      </head>
      <body>
        <div id="app">${html}</div>
        <script src="/client.js"></script>
      </body>
    </html>
  `
}

// 2. 客户端激活
import { createSSRApp } from 'vue'

const app = createSSRApp(App)

// 重要:确保客户端和服务端状态一致
if (window.__INITIAL_STATE__) {
  app.config.globalProperties.$store.replaceState(window.__INITIAL_STATE__)
}

// 挂载到已有HTML上(激活)
app.mount('#app')

// 3. 常见的hydration错误
// ❌ 服务器端和客户端渲染的HTML不一致
// 原因:
// - 使用不同数据
// - 使用平台特定API(window、document)
// - 时间差异

// 解决方案:
// - 确保数据一致性
// - 使用process.client/process.server判断
// - 使用Date.now()而不是new Date()

自定义SSR实现:

// server.js
import express from 'express'
import { createSSRApp } from 'vue'
import { renderToString } from '@vue/server-renderer'
import App from './App.vue'

const server = express()

server.use('*', async (req, res) => {
  // 创建Vue应用实例
  const app = createSSRApp(App)
  
  // 服务器端数据预取
  const context = { url: req.url }
  
  // 渲染为HTML
  const html = await renderToString(app, context)
  
  // 注入初始状态
  const initialState = JSON.stringify(context.initialState || {}).replace(
    /</g,
    '\\u003c'
  )
  
  // 返回完整HTML
  res.send(`
    <!DOCTYPE html>
    <html>
      <head>
        <title>Vue SSR</title>
        <script>window.__INITIAL_STATE__ = ${initialState}</script>
      </head>
      <body>
        <div id="app">${html}</div>
        <script src="/dist/client.js"></script>
      </body>
    </html>
  `)
})

// client.js
import { createSSRApp } from 'vue'
import App from './App.vue'

// 创建应用
const app = createSSRApp(App)

// 恢复状态
if (window.__INITIAL_STATE__) {
  // 恢复应用状态
  app.config.globalProperties.$store?.replaceState?.(window.__INITIAL_STATE__)
}

// 挂载(激活)
app.mount('#app')

🎯 重点总结与使用场景

必须掌握的核心优化:

  1. 组件懒加载(⭐⭐⭐⭐⭐)

    • 所有路由级组件
    • 大型弹窗/模态框
    • 不常用的功能模块
  2. 代码分割(⭐⭐⭐⭐⭐)

    • 生产环境构建必备
    • 第三方库分离
    • 公共代码提取
  3. 虚拟滚动(⭐⭐⭐⭐)

    • 数据表格、长列表
    • 聊天记录
    • 商品列表

重要性能监控:

  1. 核心Web性能指标(⭐⭐⭐⭐)

    • LCP、FID、CLS监控
    • 生产环境性能追踪
  2. Vue Devtools(⭐⭐⭐)

    • 开发环境性能分析
    • 组件渲染时间分析

SSR使用场景:

项目类型推荐方案理由
内容型网站SSR/Nuxt.jsSEO友好,首屏快
后台管理系统CSR不需要SEO,快速开发
电商网站SSR + CSR混合产品页SSR,后台CSR
移动端H5CSR体积小,加载快

优化策略优先级:

// 优化金字塔(从基础到高级)
1. ✅ 代码级优化(计算属性、v-for key)
2. ✅ 组件级优化(懒加载、冻结数据)
3. ✅ 构建优化(代码分割、压缩)
4. ✅ 运行时优化(虚拟滚动、防抖)
5. ⭐ 架构优化(SSR、微前端)

💡 实战技巧

1. 渐进式图片加载

<template>
  <div class="image-container">
    <!-- 占位图 -->
    <div v-if="!loaded" class="placeholder">
      <div class="spinner"></div>
    </div>
    
    <!-- 缩略图(快速显示) -->
    <img
      v-show="!highResLoaded"
      :src="thumbnailSrc"
      @load="onThumbnailLoad"
      :class="{ 'blur': !highResLoaded }"
      loading="lazy"
    />
    
    <!-- 高清图(延迟加载) -->
    <img
      v-show="highResLoaded"
      :src="highResSrc"
      @load="onHighResLoad"
      loading="lazy"
    />
  </div>
</template>

<script setup>
import { ref } from 'vue'

const props = defineProps({
  thumbnailSrc: String,
  highResSrc: String
})

const thumbnailLoaded = ref(false)
const highResLoaded = ref(false)
const loaded = computed(() => thumbnailLoaded.value || highResLoaded.value)

function onThumbnailLoad() {
  thumbnailLoaded.value = true
  // 开始加载高清图
  loadHighResImage()
}

async function loadHighResImage() {
  const img = new Image()
  img.src = props.highResSrc
  img.onload = () => {
    highResLoaded.value = true
  }
}

// 使用IntersectionObserver懒加载
import { useIntersectionObserver } from '@vueuse/core'
const container = ref(null)

useIntersectionObserver(
  container,
  ([{ isIntersecting }]) => {
    if (isIntersecting && !thumbnailLoaded.value) {
      // 开始加载图片
    }
  }
)
</script>

2. 请求缓存和重复请求合并

// utils/request-cache.js
class RequestCache {
  constructor(options = {}) {
    this.cache = new Map()
    this.pendingRequests = new Map()
    this.ttl = options.ttl || 5 * 60 * 1000 // 5分钟
  }
  
  async request(key, requestFn) {
    const now = Date.now()
    
    // 检查缓存
    const cached = this.cache.get(key)
    if (cached && now - cached.timestamp < this.ttl) {
      return Promise.resolve(cached.data)
    }
    
    // 检查是否有相同的请求正在进行
    if (this.pendingRequests.has(key)) {
      return this.pendingRequests.get(key)
    }
    
    // 发起新请求
    const requestPromise = requestFn()
      .then(data => {
        // 缓存结果
        this.cache.set(key, {
          data,
          timestamp: Date.now()
        })
        
        // 清理pending
        this.pendingRequests.delete(key)
        
        return data
      })
      .catch(error => {
        // 清理pending
        this.pendingRequests.delete(key)
        throw error
      })
    
    // 记录pending请求
    this.pendingRequests.set(key, requestPromise)
    
    return requestPromise
  }
  
  clear() {
    this.cache.clear()
    this.pendingRequests.clear()
  }
  
  invalidate(key) {
    this.cache.delete(key)
  }
}

// 全局缓存实例
export const requestCache = new RequestCache()

// 在Vue中使用
import { requestCache } from '@/utils/request-cache'

async function fetchUserData(userId) {
  return requestCache.request(
    `user:${userId}`, // 缓存key
    () => axios.get(`/api/user/${userId}`) // 请求函数
  )
}

3. 基于Web Workers的复杂计算

// worker.js
self.onmessage = function(event) {
  const { type, data } = event.data
  
  switch (type) {
    case 'CALCULATE_STATS':
      const result = calculateComplexStats(data)
      self.postMessage({ type: 'STATS_RESULT', data: result })
      break
      
    case 'PROCESS_DATA':
      const processed = processLargeData(data)
      self.postMessage({ type: 'DATA_PROCESSED', data: processed })
      break
  }
}

function calculateComplexStats(data) {
  // 复杂计算,不阻塞主线程
  return data.reduce((acc, item) => {
    // ...
  }, {})
}

// main.js
const worker = new Worker('./worker.js')

// 发送计算任务
worker.postMessage({
  type: 'CALCULATE_STATS',
  data: largeDataset
})

// 接收结果
worker.onmessage = function(event) {
  const { type, data } = event.data
  
  switch (type) {
    case 'STATS_RESULT':
      // 更新Vue状态
      stats.value = data
      break
  }
}

// 组件中使用
function calculateInBackground() {
  isLoading.value = true
  
  worker.postMessage({
    type: 'PROCESS_DATA',
    data: largeData.value
  })
  
  worker.onmessage = (event) => {
    if (event.data.type === 'DATA_PROCESSED') {
      processedData.value = event.data.data
      isLoading.value = false
    }
  }
}

// 清理
onUnmounted(() => {
  worker.terminate()
})

性能优化是一个持续的过程,需要测量->优化->验证的循环。记住优化原则:

  1. 不要过早优化:先确保功能正确
  2. 基于数据优化:用性能数据指导优化方向
  3. 权衡利弊:每个优化都有代价(复杂度、可维护性)

从最简单的优化开始(如懒加载、代码分割),逐步深入更高级的优化技巧。性能优化的最终目标是提供优秀的用户体验

恭喜你完成了Vue.js性能优化的学习!这是成为高级Vue开发者的重要一步。🎉

这里是我下一部分的知识链接,也是最后一部分,10,11,12的内容

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值