Vue.js 工程化与最佳实践全面指南
第八部分:工程化和最佳实践
工程化是专业开发的分水岭,决定了项目的可维护性、团队协作效率和长期发展潜力。这部分知识将帮助你从"会写代码"升级到"会工程化开发"。
📌 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 }) {
// 实现...
}
}
})
🎯 重点总结与使用场景
必须掌握的核心知识点:
-
项目结构和组织(⭐⭐⭐⭐⭐)
- 所有项目的基础
- 团队协作和项目维护
-
代码规范和 ESLint(⭐⭐⭐⭐⭐)
- 团队开发必备
- 保证代码质量和一致性
-
TypeScript 支持(⭐⭐⭐⭐⭐)
- 大型项目和团队开发
- 需要良好类型安全的项目
重要工程化实践:
-
测试策略(⭐⭐⭐⭐)
- 需要稳定性和质量保证的项目
- 企业级应用和长期维护的项目
-
构建优化(⭐⭐⭐⭐)
- 生产环境部署
- 性能敏感的应用
-
环境配置(⭐⭐⭐)
- 多环境部署(开发、测试、生产)
- 持续集成/持续部署
学习路径建议:
| 学习阶段 | 重点内容 | 项目实践 |
|---|---|---|
| 初学者 | Vue CLI/Vite 基础使用 | 创建简单项目 |
| 进阶者 | 项目结构优化、ESLint | 重构现有项目结构 |
| 团队开发 | 代码规范、Git 工作流 | 参与团队项目 |
| 专业级 | TypeScript、完整测试 | 企业级项目开发 |
| 架构师 | 构建优化、部署策略 | 从零搭建项目架构 |
最佳实践总结:
- 始终使用版本控制:Git + 合适的 Git 工作流
- 代码规范先行:项目开始时配置好 ESLint + Prettier
- 组件设计原则:单一职责、可复用、可测试
- 渐进式采用:不要一次性引入所有复杂工具
- 文档化:README、组件文档、API 文档
- 持续集成:自动化测试、构建、部署
💡 实战技巧
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
主要功能详解:
-
组件树分析
// 查看组件层级和嵌套深度 // 过深的组件嵌套会影响性能 // 建议:保持组件树深度在5层以内 -
性能时间线
// 1. 开启性能记录 // 2. 执行用户操作 // 3. 分析时间线,找出性能瓶颈 // 关键指标: // - Component render: 组件渲染时间 // - Component patch: 组件更新时间 // - Event handlers: 事件处理时间 -
事件追踪
// 跟踪组件发出的事件 // 分析事件触发频率和性能影响 -
状态快照
// 保存和加载状态快照 // 用于调试复杂状态变化
自定义性能监控:
// 创建性能监控插件
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 devtools | Vue特定分析 | 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 列表优化
关键优化技巧:
-
始终使用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> -
避免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> -
冻结大数据
// 大数据列表冻结,避免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 } -
分页和无限滚动
<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 计算属性缓存
计算属性高级优化:
-
缓存策略
const expensiveData = computed(() => { // 复杂计算 const result = heavyCalculation(data.value) // 缓存中间结果 const cacheKey = JSON.stringify(data.value) if (!cache[cacheKey]) { cache[cacheKey] = result } return cache[cacheKey] || result }) -
惰性计算
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 } } } -
记忆化计算属性
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) }) -
防抖计算属性
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')
🎯 重点总结与使用场景
必须掌握的核心优化:
-
组件懒加载(⭐⭐⭐⭐⭐)
- 所有路由级组件
- 大型弹窗/模态框
- 不常用的功能模块
-
代码分割(⭐⭐⭐⭐⭐)
- 生产环境构建必备
- 第三方库分离
- 公共代码提取
-
虚拟滚动(⭐⭐⭐⭐)
- 数据表格、长列表
- 聊天记录
- 商品列表
重要性能监控:
-
核心Web性能指标(⭐⭐⭐⭐)
- LCP、FID、CLS监控
- 生产环境性能追踪
-
Vue Devtools(⭐⭐⭐)
- 开发环境性能分析
- 组件渲染时间分析
SSR使用场景:
| 项目类型 | 推荐方案 | 理由 |
|---|---|---|
| 内容型网站 | SSR/Nuxt.js | SEO友好,首屏快 |
| 后台管理系统 | CSR | 不需要SEO,快速开发 |
| 电商网站 | SSR + CSR混合 | 产品页SSR,后台CSR |
| 移动端H5 | CSR | 体积小,加载快 |
优化策略优先级:
// 优化金字塔(从基础到高级)
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()
})
性能优化是一个持续的过程,需要测量->优化->验证的循环。记住优化原则:
- 不要过早优化:先确保功能正确
- 基于数据优化:用性能数据指导优化方向
- 权衡利弊:每个优化都有代价(复杂度、可维护性)
从最简单的优化开始(如懒加载、代码分割),逐步深入更高级的优化技巧。性能优化的最终目标是提供优秀的用户体验。
恭喜你完成了Vue.js性能优化的学习!这是成为高级Vue开发者的重要一步。🎉

被折叠的 条评论
为什么被折叠?



