Vue.js 从入门到精通:技术成长之路

认识 Vue

Vue.js 是一款渐进式前端框架,核心信息如下:

  • 诞生:2014 年由尤雨溪创造。
  • 核心聚焦:专注于视图层,以简洁方式构建用户界面。
  • 核心特点:
    • 轻量级,API 简洁易懂;
    • 支持双向数据绑定,实现数据与视图联动;
    • 组件化开发,提升代码复用性;
    • 生态丰富,可灵活与其他库或现有项目集成;
    • 渐进式特性:可根据需求逐步引入功能,从简单应用扩展到复杂项目。

Vue 就像一个 "智能管家",专门帮你处理网页的交互逻辑。如果说原生 JS 操作 DOM 是 "自己动手做饭"(需要手动切菜、开火、调味,步骤繁琐),那 Vue 就是 "点外卖"------ 你只需要告诉它 "要什么"(数据和逻辑),它会自动帮你处理 "怎么做"(DOM 操作、更新视图),大大简化开发流程。

第一部分:Vue.js 入门基础

1.1 Vue.js 简介

什么是 Vue.js

Vue.js 是一套渐进式 JavaScript 框架,核心用于构建用户界面。所谓渐进式,即框架的能力可以按需分层引入:你可以仅用核心响应式能力开发简单页面,也可以逐步接入组件系统、路由、状态管理等全家桶能力,适配从简单交互页到大型单页应用(SPA)的全场景开发。

核心特点

  • 易用性:HTML/CSS/JavaScript 基础即可上手,文档友好,API 简洁直观,学习曲线平缓。
  • 灵活性:既可以嵌入现有页面增强交互,也可以基于全家桶构建完整的单页应用。
  • 高效性:基于虚拟 DOM 与差异化更新,最小化 DOM 操作,保证渲染性能。
  • 响应式数据绑定:数据与视图双向关联,数据变更自动触发视图更新,无需手动操作 DOM。
  • 组件化开发:将页面拆分为独立、可复用的组件,实现高内聚、低耦合的代码设计。

适用场景

  • 传统网页的交互增强、动态内容渲染
  • 中后台管理系统、数据可视化平台
  • 单页面应用(SPA)、移动端 H5 页面
  • 跨端应用(小程序、原生 App、桌面端应用)

Vue.js 与其他框架的简要比较

框架 核心特点 学习曲线 生态 适用场景
Vue.js 渐进式、模板与 JSX 双支持、响应式开箱即用 平缓 官方维护核心生态(路由 / 状态管理),社区丰富 中小项目到大型企业级项目,兼顾开发效率与灵活性
React 函数式编程、JSX 优先、单向数据流、灵活度极高 中等 社区生态极其庞大,方案多样 大型复杂前端项目,跨端全栈场景
Angular 大而全的全家桶方案、TypeScript 原生支持、强约束 陡峭 官方提供完整能力,企业级生态完善 大型企业级中后台项目,强规范的团队协作场景

1.2 环境搭建与第一个应用

安装方式

  1. CDN 引入(入门首选) 适合快速体验、小型项目或传统页面嵌入,直接在 HTML 中引入脚本:

    html 复制代码
    <!-- Vue 3 开发版(含完整警告和调试信息) -->
    <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
    <!-- Vue 2 开发版 -->
    <script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
  2. 包管理器安装(工程化项目) 适合大型项目开发,配合构建工具使用:

    复制代码
    # npm 安装 Vue 3
    npm install vue@3
    # yarn 安装
    yarn add vue@3
    # pnpm 安装(推荐,性能更优)
    pnpm add vue@3
  • 安装 Node.js:提供 JavaScript 运行时环境,是 Vue 开发的基础;
  • 安装 npm 或 yarn:Node.js 自带的包管理工具(npm)或替代工具(yarn),用于管理项目依赖(如 Vue 库、插件等)。

项目脚手架搭建

  1. Vite(Vue 3 官方推荐) 基于原生 ES 模块的新一代构建工具,冷启动快、热更新性能优异,是 Vue 3 项目的首选:

    复制代码
    # 创建项目
    npm create vite@latest vue-project -- --template vue
    # 进入项目目录
    cd vue-project
    # 安装依赖
    npm install
    # 启动开发服务
    npm run dev
  2. Vue CLI(Vue 2 适配) Vue 官方旧版脚手架,基于 Webpack,适配 Vue 2 项目,Vue 3 项目已不推荐:

    复制代码
    # 全局安装CLI
    npm install -g @vue/cli
    # 创建项目
    vue create vue-project

第一个 Vue 应用

Vue 3 示例(CDN 版)

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>Hello Vue 3</title>
  <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
</head>
<body>
  <!-- 挂载容器 -->
  <div id="app">{{ message }}</div>

  <script>
    // 创建Vue应用实例
    const { createApp } = Vue
    createApp({
      // 响应式数据
      data() {
        return {
          message: 'Hello Vue!'
        }
      }
    }).mount('#app') // 挂载到DOM节点
  </script>
</body>
</html>

Vue 2 示例(CDN 版)

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>Hello Vue 2</title>
  <script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
</head>
<body>
  <div id="app">{{ message }}</div>

  <script>
    // 创建Vue实例
    new Vue({
      el: '#app', // 挂载选项
      data() {
        return {
          message: 'Hello Vue!'
        }
      }
    })
  </script>
</body>
</html>

核心说明:

  • Vue 3 采用 createApp() 创建应用实例,通过 .mount() 方法挂载到 DOM;Vue 2 采用 new Vue() 创建实例,通过 el 选项或 $mount() 挂载。
  • data 选项用于定义响应式数据,模板中通过双大括号插值直接访问。

各部分介绍

html 复制代码
<!-- 视图:显示数据 -->
<div id="app">
  <p>{{ message }}</p> <!-- 双大括号是Vue的模板语法,用于显示数据 -->
  <button @click="changeMessage">点我改内容</button>
</div>

<!-- 引入Vue -->
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<script>
  // Vue实例:管理数据和逻辑
  const { createApp } = Vue;
  createApp({
    data() { // 存放数据(相当于温度计的"温度")
      return {
        message: "Hello Vue!"
      }
    },
    methods: { // 存放方法(操作数据的逻辑)
      changeMessage() {
        this.message = "Vue 真简单!"; // 修改数据
        // 不需要手动改DOM,视图会自动更新
      }
    }
  }).mount('#app'); // 把Vue实例挂载到id为app的元素上
</script>
  • 核心关系与原理
    • 实例与容器对应:一个 Vue 实例通常绑定一个 DOM 容器(通过el配置),负责管理该容器内的视图;
    • 数据绑定:Vue 的核心特性,实现数据与视图的双向同步 ------ 数据变化时视图自动更新,视图操作(如表单输入)时数据同步变化。在 Vue 中,你只需要关注 "数据",视图会自动跟随数据变化。比如:

1.3 模板语法

模板语法是框架连接数据与视图的核心桥梁,分为插值语法与指令语法 两类,前者负责数据渲染,后者处理 DOM 行为控制。模板语法可以理解为 "带动态功能的 HTML 模板",核心是用声明式的方式把数据和 DOM 关联起来,让开发者不用手动操作 DOM,只需关注 "数据是什么" 和 "页面该怎么展示"。

插值语法

  • 语法:使用双大括号 {``{ }}(Mustache 语法)
  • 功能:将数据值渲染到页面,当数据发生变化时,页面显示会同步更新(依赖框架的响应式系统)
html 复制代码
<!-- 文本插值 -->
<p>{{ message }}</p>
<!-- 支持JavaScript表达式 -->
<p>{{ number + 1 }}</p>
<p>{{ isOk ? '已完成' : '未完成' }}</p>
<p>{{ message.split('').reverse().join('') }}</p>
<!-- 一次性插值,数据变化不更新 -->
<p v-once>{{ message }}</p>

注意:双大括号内仅支持单个 JavaScript 表达式,不支持语句、循环、条件判断等复杂代码。

核心指令

指令是带有 v- 前缀的特殊属性,用于响应式地作用于 DOM,当表达式的值变化时,指令会更新 DOM。

指令 作用 核心用法
v-bind 动态绑定 HTML 属性、组件 props <img :src="imgUrl" :title="imgTitle">
v-model 表单元素双向数据绑定 <input v-model="inputValue">
v-for 列表渲染,循环遍历数组 / 对象 <li v-for="(item, index) in list" :key="index">{``{ item }}</li>
v-if / v-else / v-else-if 条件渲染,满足条件才渲染节点,不满足则销毁 / 不创建 <div v-if="type === 1">类型1</div><div v-else-if="type === 2">类型2</div><div v-else>其他类型</div>
v-show 条件展示,始终渲染节点,通过 display 控制显隐 <div v-show="isShow">展示内容</div>
v-on 事件监听,绑定 DOM 事件 <button @click="handleClick">点击按钮</button>
指令缩写

为简化开发,Vue 为高频指令提供了缩写:

  • v-bind: 缩写为 :
  • v-on: 缩写为 @
  • v-slot: 缩写为 #(插槽相关,后续详解)
具体介绍
  • v-bind:用于绑定 HTML 属性,简化属性绑定操作

    • 基础语法:v-bind:属性名="表达式",简写为 :属性名="表达式"

    • 示例:普通属性:图片路径、链接地址等

      html 复制代码
      <!--普通属性:图片路径、链接地址等-->
      <img :src="productImg" :alt="productName">
      <a :href="detailUrl" :title="productDesc">查看详情</a>

      CSS 类名:支持对象语法(条件绑定)和数组语法(多类名)

      html 复制代码
      <!-- 对象语法:isActive为true时添加active类 -->
      <div :class="{ active: isActive, disabled: isDisabled }"></div>
      <!-- 数组语法:绑定多个固定类名 -->
      <div :class="[baseClass, themeClass]"></div>

      内联样式:支持对象语法和数组语法

      html 复制代码
      <div :style="{ fontSize: '16px', color: textColor }"></div>
  • v-model:实现双向数据绑定,主要用于表单元素

    • 功能:表单元素值与数据双向同步(输入框内容变化时数据更新,数据变化时输入框内容同步更新)本质是 v-bind:value + v-on:input

    • 示例:支持元素 :input(文本 / 复选 / 单选)、textarea、select

      html 复制代码
      <!-- 文本输入框 -->
      <input v-model="username" type="text">
      <!-- 复选框 -->
      <input v-model="isAgree" type="checkbox">
      <!-- 下拉选择框 -->
      <select v-model="selectedCity">
        <option value="beijing">北京</option>
        <option value="shanghai">上海</option>
      </select>

      常用修饰符:​

      .lazy:失去焦点后同步数据(默认实时同步)

      html 复制代码
      <input v-model.lazy="username">

      number:自动转换为数字类型(避免输入框返回字符串)

      html 复制代码
      <input v-model.number="age" type="number">

      .trim:自动去除首尾空格

      html 复制代码
      <input v-model.trim="searchKey">
  • v-if,v-show用于条件渲染

      • 示例:<p v-if="isShow">显示内容</p>isShowtrue时,<p>标签及内容被渲染)
  • v-for:用于列表渲染,可遍历数组、对象、字符串等

    • 语法:

      • 遍历数组:v-for="(item, index) in array" :key="uniqueKey"

      • 遍历对象:v-for="(value, key, index) in object" :key="key"

      • 遍历数字:v-for="n in 5" :key="n"(生成 1-5 的序列)

    • 关键规则:key 属性

    • key 用于标识唯一元素,帮助框架高效更新 DOM,需遵循以下原则:​

    • 优先使用数据唯一标识(如 item.id),避免用 index(数组排序 / 删除时会导致 DOM 复用错误)

      html 复制代码
      <!-- ❌ 不推荐:index会随数组变化 -->
      <li v-for="(item, index) in list" :key="index">{{ item.name }}</li>
      <!-- ✅ 推荐:id唯一且稳定 -->
      <li v-for="item in list" :key="item.id">{{ item.name }}</li>
    • template 标签不能绑定 key,需绑定到实际 DOM 元素上

      html 复制代码
      <!-- ❌ 错误:key不能绑定在template上 -->
      <template v-for="item in list" :key="item.id">
        <div>{{ item.name }}</div>
      </template>
      <!-- ✅ 正确:key绑定到实际元素 -->
      <template v-for="item in list">
        <div :key="item.id">{{ item.name }}</div>
      </template>
    • 嵌套循环示例:

      html 复制代码
      <!-- 遍历二维数组:省份->城市 -->
      <div v-for="province in provinces" :key="province.id">
        <h3>{{ province.name }}</h3>
        <ul>
          <li v-for="city in province.cities" :key="city.id">
            {{ city.name }}
          </li>
        </ul>
      </div>

计算属性 computed

用于处理复杂的响应式计算逻辑,基于依赖缓存,只有依赖的响应式数据变化时才会重新计算,否则直接返回缓存结果。

  • 定义:通过 computed 对象定义的派生属性,依赖其他响应式数据生成新值
  • 特性:
    • 缓存机制:仅当依赖的响应式数据变化时才重新计算,重复访问直接返回缓存结果(性能优化核心)
    • 默认只读:默认只有 getter 方法,如需修改需显式定义 setter
    • 依赖追踪:自动追踪所有依赖的响应式数据,无需手动配置
    • 依赖驱动 :只依赖声明的数据源(如data中的属性),当依赖变化时自动重新计算;
    • 无副作用:只做 "计算"(纯逻辑),不应该有异步请求、修改 DOM 等操作。
javascript 复制代码
// Vue 3 选项式API示例
createApp({
  data() {
    return {
      firstName: '张',
      lastName: '三'
    }
  },
  computed: {
    // 计算属性的getter
    fullName() {
      return this.firstName + this.lastName
    }
  }
})
html 复制代码
<!-- 模板中直接使用 -->
<p>全名:{{ fullName }}</p>
  • firstNamelastName变化时,fullName会自动更新;
  • methods 的核心区别:computed 有依赖缓存,多次调用仅执行一次;methods 每次调用都会重新执行。即连续多次访问fullName(如在模板中多次使用{``{ fullName }}),只要依赖没变,只会计算一次(控制台只打印一次)。

侦听器 watch

用于监听响应式数据的变化,当数据变化时执行异步操作、复杂逻辑或副作用代码。

  • 定义:通过 watch 选项定义,用于监听数据变化

  • 特点

    • 目标明确 :只监听指定的数据(可以是datacomputed甚至嵌套属性);
    • 副作用优先:适合执行 "非计算" 操作(如异步请求、打印日志、修改其他数据等);
    • 无缓存:每次监听的数据变化时,都会执行回调函数(哪怕变化后的值和之前一样)。
javascript 复制代码
new Vue({
  data() {
    return {
      userId: 1,
      userInfo: null
    }
  },
  watch: {
    // 监听userId的变化
    userId(newVal, oldVal) {
      console.log(`userId从${oldVal}变成了${newVal}`);
      // 副作用操作:发请求(异步)
      fetch(`/api/user/${newVal}`)
        .then(res => res.json())
        .then(data => this.userInfo = data);
    }
  }
})
  • userId从 1 变成 2 时,会自动执行回调,打印日志并请求新用户的数据;
  • 即使userId被重复赋值为 1(比如this.userId = 1),只要发生了 "赋值" 操作,回调依然会执行(无缓存)。
  • computed 的适用场景:computed 用于数据派生、有返回值的同步计算;watch 用于数据变化触发的副作用、异步操作,无强制返回值要求。

计算属性 vs 侦听器对比:​

维度 计算属性(computed 侦听器(watch
核心作用 派生新数据(如拼接、过滤、计算) 执行副作用(如异步请求、日志、DOM 操作)
依赖处理 自动追踪所有依赖(无需手动指定) 需手动指定监听的目标数据
缓存 有(依赖不变则不重新计算) 无(数据变化就执行)
返回值 必须有返回值(用于模板渲染或其他逻辑) 无需返回值(专注于过程)
使用场景 当你需要 "从现有数据算出一个新数据"(如fullNamefilteredList),用computed 当你需要 "数据变了之后做一件事"(如发请求、存本地存储),用watch

1.4 事件处理

Vue 中的事件处理,本质是让页面元素(如按钮、输入框)在发生特定动作(点击、输入、滚动等)时,能精准触发 Vue 实例中的逻辑(方法),实现 "用户操作→程序响应" 的交互闭环。它就像给元素装了个 "智能感应器":当用户做了预设动作(比如点按钮),感应器就会 "通知" 对应的函数执行操作(比如修改数据、发请求)。

基础事件绑定v-on指令)

框架通过 v-on 指令统一管理 DOM 事件,支持事件绑定、修饰符、参数传递等功能。

核心语法v-on:事件名="处理函数",简写为 @事件名="处理函数"

html 复制代码
<!-- 完整写法 -->
<button v-on:click="handleClick">点我</button>

<!-- 缩写(推荐) -->
<button @click="handleClick">点我</button>

绑定方式

  • 直接绑定 methods 方法(推荐)

  • 内联事件表达式(适用于简单逻辑

  1. 示例:

    javascript 复制代码
    new Vue({
      methods: {
        handleClick() {
          console.log('按钮被点击了!');
          // 可以在这里修改数据(响应式更新)
          this.message = '按钮被点了~';
        }
      }
    })

传递参数

如果需要在事件触发时传递额外数据(比如列表项的 ID),可以直接在方法后加参数:

html 复制代码
<!-- 传递静态值 -->
<button @click="sayHello('小明')">问候小明</button>

<!-- 传递动态数据(来自data) -->
<ul>
  <li v-for="item in list" :key="item.id">
    <button @click="deleteItem(item.id)">删除</button>
    {{ item.name }}
  </li>
</ul>

方法接收参数:

javascript 复制代码
methods: {
  sayHello(name) {
    console.log(`你好,${name}!`); // 点击后打印"你好,小明!"
  },
  deleteItem(id) {
    console.log(`要删除的ID是:${id}`);
    // 实际逻辑:从list中删除对应项
    this.list = this.list.filter(item => item.id !== id);
  }
}

新手坑 :如果需要同时传递参数和事件对象(event),要手动传入$event(Vue 提供的特殊变量,代表原生事件对象):

html 复制代码
<!-- 正确:同时传参数和事件对象 -->
<button @click="handleClick('参数', $event)">点击</button>
javascript 复制代码
handleClick(arg, e) {
  console.log(arg); // 打印"参数"
  console.log(e.target); // 打印触发事件的按钮元素(原生event属性)
}

事件修饰符

有时需要对事件做额外处理(比如阻止冒泡、阻止默认行为),Vue 提供了事件修饰符 (以.开头),直接在事件后添加即可,无需在方法里写e.preventDefault()e.stopPropagation()

修饰符 功能说明 示例
.stop 阻止事件冒泡 @click.stop="handleClick"
.prevent 阻止默认行为(如表单提交、链接跳转) @submit.prevent="handleSubmit"
.self 仅元素自身触发时执行(排除冒泡 / 捕获事件) @click.self="handleClick"
.once 事件仅触发一次 @click.once="handleClick"
.passive 优化滚动性能(不阻止默认滚动) @scroll.passive="handleScroll"
.native 监听自定义组件的原生事件 <MyBtn @click.native="handleClick">

组合使用示例

html 复制代码
<!-- 同时阻止冒泡和默认行为 -->
<a @click.stop.prevent="handleCustomClick" href="/detail">自定义链接</a>

按键修饰符

对于键盘事件(keyupkeydown),可以用按键修饰符指定 "只有按特定键时才触发",比如只响应回车键、ESC 键。

修饰符 对应按键
.enter 回车键
.esc ESC 键
.tab Tab 键
.space 空格键
.up/.down/.left/.right 方向键

示例

html 复制代码
<!-- 输入框按回车触发搜索 -->
<input @keyup.enter="handleSearch" v-model="searchQuery">

<!-- 按ESC关闭弹窗 -->
<div @keyup.esc="closeModal" class="modal"></div>

1.5 组件基础

组件概念

组件是可复用的 Vue 实例,将页面的独立模块(按钮、弹窗、表单、页面)封装为独立单元,每个组件包含自己的模板、逻辑、样式,实现代码复用与解耦。

一个完整的 Vue 应用,本质是一棵由嵌套组件构成的组件树。就像 "拼乐高"------ 把复杂的网页拆成一个个独立的 "小模块"(组件),每个模块有自己的结构、样式和功能,就像乐高的 "轮子""车身""车窗",可以单独设计、重复使用,最后拼出完整的 "汽车"(网页)。

这种思路能解决两大问题:

  • 大型页面代码混乱(拆成小模块,每个模块逻辑清晰);
  • 重复功能反复开发(一个组件写一次,到处能用)。
什么是 "组件"?

组件是 "独立的、可复用的代码片段",包含 3 部分核心内容(类比一个 "小网页"):

  • 结构(HTML):组件的外观(比如按钮的形状、文字);
  • 样式(CSS):组件的样式(比如按钮的颜色、大小);
  • 逻辑(JS):组件的功能(比如按钮点击后做什么)。

举个例子:一个 "商品卡片" 组件,可能包含:

  • 结构:图片、标题、价格、购买按钮;
  • 样式:卡片边框、阴影、文字大小;
  • 逻辑:点击购买按钮弹出提示。
组件化的核心好处(为什么要用组件?)
  1. 复用性:一个组件可以在多个地方重复使用。比如电商网站的 "商品卡片",首页、列表页、推荐页都能用,不用写 3 次代码。

  2. 维护性:修改一个组件,所有使用它的地方都会同步更新。比如想把所有商品卡片的边框颜色从灰色改成蓝色,只需要改一次组件代码,不用逐个页面修改。

  3. 分工明确:多人开发时,可按组件分工(A 写头部组件,B 写列表组件),互不干扰。

总结

组件化的核心是 "拆分与复用":把页面拆成独立的小模块,每个模块封装自己的结构、样式和逻辑,通过 "父子通信" 协同工作。就像工厂生产手机:屏幕、电池、摄像头都是标准化组件,单独生产、测试,最后组装成整机。这种模式让开发更高效、维护更简单,尤其适合大型项目。入门时,先学会定义简单组件、复用组件、处理父子通信,再逐步深入复杂组件和状态管理(如 Vuex)。

Vue 中的组件基础(以 Vue 为例,最常用)

Vue 是组件化的典型代表,它的组件系统让 "拆模块" 变得简单。下面用具体例子说明。

1. 定义一个简单组件(以 Vue 3 为例)

一个组件通常是一个.vue文件(单文件组件),包含 3 个部分:<template>(结构)、<script>(逻辑)、<style>(样式)。

比如定义一个Button.vue组件(自定义按钮):

html 复制代码
<!-- Button.vue -->
<template>
  <!-- 结构:按钮的HTML -->
  <button class="my-btn" @click="handleClick">
    {{ text }} <!-- 显示传入的文字 -->
  </button>
</template>

<script>
// 逻辑:组件的数据和方法
export default {
  // props:父组件传给子组件的数据(类似"参数")
  props: {
    text: {
      type: String, // 类型是字符串
      default: "按钮" // 默认文字
    }
  },
  methods: {
    handleClick() {
      // 子组件触发事件,通知父组件"被点击了"
      this.$emit("btn-click");
    }
  }
};
</script>

<style scoped>
/* 样式:只作用于当前组件(scoped确保不污染其他组件) */
.my-btn {
  padding: 8px 16px;
  background: #42b983;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}
</style>
2. 使用组件(复用组件)

定义好的组件需要 "引入" 到其他组件(或页面)中才能使用,就像把乐高零件拼到主体上。

比如在App.vue(根组件)中使用Button组件:

html 复制代码
<!-- App.vue -->
<template>
  <div>
    <!-- 使用Button组件,传入text属性,监听btn-click事件 -->
    <Button text="登录" @btn-click="login" />
    <Button text="注册" @btn-click="register" />
    <!-- 不传入text,使用默认值"按钮" -->
    <Button @btn-click="other" />
  </div>
</template>

<script>
// 1. 引入Button组件
import Button from "./Button.vue";

export default {
  // 2. 注册组件(声明要使用的组件)
  components: {
    Button // 键和值相同,可简写
  },
  methods: {
    login() {
      alert("登录按钮被点击");
    },
    register() {
      alert("注册按钮被点击");
    },
    other() {
      alert("默认按钮被点击");
    }
  }
};
</script>

效果 :页面会显示 3 个按钮,点击分别触发不同的提示 ------ 这就是组件的 "复用性":写一次Button.vue,能在多个地方用,且各自的文字和点击逻辑可定制。

3. 组件的 "父子关系" 和通信

组件之间通常有层级关系(比如App是父组件,Button是子组件),就像 "大盒子套小盒子"。父子组件需要传递数据或事件,这就是 "组件通信"。

  • 父传子(父给子数据) :通过props(就像给子组件传 "参数")。比如上面的Button组件通过props: { text }接收父组件传入的text属性。

  • 子传父(子通知父发生了什么) :通过$emit触发事件(就像子组件给父组件 "发消息")。比如Button组件用this.$emit("btn-click")告诉父组件 "我被点击了",父组件通过@btn-click监听并处理。

组件注册

组件分为全局注册和局部注册,区别在于作用域不同。引入组件后,必须在components中注册,否则会报错 "组件未定义"。

  1. 全局注册:组件在整个项目中都能直接使用(不用每次引入)。适合高频使用的组件(如按钮、输入框):

    javascript 复制代码
    // Vue 3 全局注册
    const app = createApp(App)
    app.component('MyButton', {
      template: `<button class="my-btn">全局按钮</button>`
    })
    app.mount('#app')
    
    // Vue 2 全局注册
    Vue.component('MyButton', {
      template: `<button class="my-btn">全局按钮</button>`
    })
  2. 局部注册:组件只在引入它的父组件中可用。优点:避免不必要的资源加载(不用的组件不加载)。

    javascript 复制代码
    // 单文件组件中局部注册
    import MyButton from './MyButton.vue'
    
    export default {
      components: {
        MyButton // 注册后,模板中可使用 <MyButton />
      }
    }

组件的 data 选项

组件的 data 必须是一个函数,且返回一个对象

javascript 复制代码
export default {
  data() {
    return {
      count: 0,
      btnText: '点击计数'
    }
  }
}

核心原因:每个组件实例需要独立的响应式数据对象。如果 data 是一个普通对象,所有组件实例会共享同一个对象,导致一个实例修改数据,其他实例全部受影响;使用函数返回对象,每个实例都会创建独立的对象,实现数据隔离。

第二部分:核心概念深入

2.1 组件化开发进阶

Props 详解

props 支持完整的类型校验、默认值、必填校验、自定义验证,提升组件的健壮性。

javascript 复制代码
export default {
  props: {
    // 基础类型校验
    title: String,
    // 多种类型支持
    count: [Number, String],
    // 必填项校验
    userId: {
      type: Number,
      required: true
    },
    // 带默认值
    status: {
      type: String,
      default: 'normal'
    },
    // 对象/数组的默认值必须用函数返回
    userInfo: {
      type: Object,
      default: () => {
        return { name: '未知', age: 0 }
      }
    },
    list: {
      type: Array,
      default: () => []
    },
    // 自定义验证函数
    type: {
      type: String,
      validator: (value) => {
        // 必须是以下值中的一个
        return ['primary', 'warning', 'danger'].includes(value)
      }
    }
  }
}

单向数据流

Vue 严格遵循单向数据流原则:

  • 父组件的 props 更新会向下流动到子组件,子组件会自动更新
  • 子组件绝对不能直接修改 props,否则会触发控制台警告
  • 若子组件需要修改 props 传递的值,有两种合法方案:
    1. 将 props 赋值给本地 data ,作为初始值,后续修改本地 data
    2. 通过计算属性,基于 props 派生新值

自定义事件 $emit:子传父

子组件向父组件传递数据,通过 $emit 触发自定义事件,父组件通过 v-on/@ 监听事件并接收数据。

  1. 子组件触发事件

    html 复制代码
    <!-- Child.vue -->
    <template>
      <button @click="handleSend">向父组件传值</button>
    </template>
    
    <script>
    export default {
      data() {
        return {
          childMsg: '来自子组件的消息'
        }
      },
      methods: {
        handleSend() {
          // 触发自定义事件,第一个参数是事件名,后续参数是传递的数据
          this.$emit('send-message', this.childMsg, 200)
        }
      }
    }
    </script>
  2. 父组件监听事件

    html 复制代码
    <!-- Parent.vue -->
    <template>
      <Child @send-message="handleReceive" />
    </template>
    
    <script>
    import Child from './Child.vue'
    export default {
      components: { Child },
      methods: {
        handleReceive(msg, code) {
          console.log(msg, code) // 输出:来自子组件的消息 200
        }
      }
    }
    </script>

插槽 slot:内容分发

插槽用于实现组件的内容分发,父组件可以向子组件的模板中传递 HTML 内容,子组件通过 <slot> 标签作为内容的占位符。

  1. 默认插

    html 复制代码
    <!-- MyCard.vue 子组件 -->
    <template>
      <div class="card">
        <div class="card-header">卡片标题</div>
        <!-- 插槽占位符,父组件传递的内容会渲染在这里 -->
        <slot></slot>
        <div class="card-footer">卡片底部</div>
      </div>
    </template>
    html 复制代码
    <!-- 父组件使用 -->
    <MyCard>
      <p>这是卡片的主体内容</p>
      <p>可以是任意HTML结构</p>
    </MyCard>
  2. 具名插槽 当组件需要多个插槽时,通过 name 属性给插槽命名,父组件通过 v-slot:name 对应传递内容。

    html 复制代码
    <!-- MyLayout.vue 子组件 -->
    <template>
      <div class="layout">
        <header><slot name="header"></slot></header>
        <main><slot></slot></main> <!-- 不带name的是默认插槽,name为default -->
        <footer><slot name="footer"></slot></footer>
      </div>
    </template>
    html 复制代码
    <!-- 父组件使用,缩写 # 代替 v-slot: -->
    <MyLayout>
      <template #header>
        <h1>页面标题</h1>
      </template>
      <template #default>
        <p>页面主体内容</p>
      </template>
      <template #footer>
        <p>页面版权信息</p>
      </template>
    </MyLayout>
  3. 作用域插槽让父组件的插槽内容可以访问子组件内部的数据,实现子组件向父组件插槽传递数据。

    html 复制代码
    <!-- MyList.vue 子组件 -->
    <template>
      <ul>
        <li v-for="item in list" :key="item.id">
          <!-- 向插槽传递item数据 -->
          <slot :item="item" :index="item.id"></slot>
        </li>
      </ul>
    </template>
    
    <script>
    export default {
      data() {
        return {
          list: [
            { id: 1, name: 'Vue' },
            { id: 2, name: 'React' },
            { id: 3, name: 'Angular' }
          ]
        }
      }
    }
    </script>
    html 复制代码
    <!-- 父组件使用,接收子组件传递的数据 -->
    <MyList>
      <template #default="{ item, index }">
        <p>{{ index }} - {{ item.name }}</p>
      </template>
    </MyList>

动态组件与 keep-alive

动态组件用于在多个组件之间动态切换,通过 <component> 标签的 :is 属性指定要渲染的组件。

html 复制代码
<template>
  <div>
    <button @click="currentTab = 'Home'">首页</button>
    <button @click="currentTab = 'About'">关于</button>
    <!-- 动态渲染组件 -->
    <component :is="currentTab"></component>
  </div>
</template>

<script>
import Home from './Home.vue'
import About from './About.vue'
export default {
  components: { Home, About },
  data() {
    return {
      currentTab: 'Home'
    }
  }
}
</script>

默认情况下,动态组件切换时,被隐藏的组件会被销毁,再次切换会重新创建,导致状态丢失、性能损耗。此时可以用 <keep-alive> 包裹动态组件,缓存不活动的组件实例,避免重复销毁和创建。

html 复制代码
<!-- 缓存动态组件 -->
<keep-alive>
  <component :is="currentTab"></component>
</keep-alive>

<keep-alive> 支持三个核心属性:

  • include:字符串 / 正则 / 数组,只有名称匹配的组件会被缓存
  • exclude:字符串 / 正则 / 数组,名称匹配的组件不会被缓存
  • max:数字,最多缓存的组件实例数量,超出会采用 LRU 策略销毁最久未使用的实例

2.2 深入响应式原理

响应式是 Vue 的核心特性,实现了数据与视图的自动同步,Vue 2 与 Vue 3 采用了完全不同的底层实现方案。

Vue 2:Object.defineProperty 数据劫持

Vue 2 的响应式核心是 Object.defineProperty(),通过劫持对象属性的 gettersetter 方法,在属性被访问时收集依赖,属性被修改时派发更新,通知视图更新。

核心实现逻辑:

  1. 遍历 data 中的所有属性,通过 Object.defineProperty() 为每个属性添加 gettersetter
  2. 当组件渲染时,会访问属性触发 getter,将当前组件的渲染 Watcher 收集为依赖
  3. 当属性被修改时,触发 setter,通知所有收集的 Watcher,触发组件重新渲染

核心缺陷

  • 无法监听对象新增 / 删除的属性,只能劫持初始化时已存在的属性
  • 无法监听数组通过下标修改元素修改 length的操作
  • 无法原生支持 Map、Set、WeakMap、WeakSet 等数据结构

为解决这些问题,Vue 2 提供了 Vue.set / this.$set 方法,用于给对象新增响应式属性、修改数组元素。

javascript

运行

复制代码
// 给对象新增属性
this.$set(this.userInfo, 'age', 18)
// 按下标修改数组元素
this.$set(this.list, 0, '新内容')

Vue 3:Proxy 代理

Vue 3 重构了响应式系统,使用 ES6 的 Proxy 代理整个对象,而非单个属性,彻底解决了 Vue 2 响应式的缺陷。

核心优势:

  • 可以监听对象新增 / 删除的属性,无需提前遍历
  • 可以监听数组的下标修改length 修改,无需重写数组方法
  • 原生支持 Map、Set、WeakMap、WeakSet 等数据结构
  • 性能更优,初始化时无需遍历所有属性,懒执行依赖收集
  • 支持嵌套对象的深层代理,可按需递归

依赖收集与派发更新

完整的响应式流程分为两个核心阶段:

  1. 依赖收集(track):当响应式数据被访问时,记录哪些组件 / 函数依赖了这个数据
  2. 派发更新(trigger):当响应式数据被修改时,通知所有依赖该数据的组件 / 函数执行更新

整个流程的核心角色:

  • 响应式数据:被 Proxy/Object.defineProperty 劫持的对象 / 属性
  • 副作用函数(Effect):组件的渲染函数、watch、computed 等,依赖响应式数据的函数
  • 依赖集合(Dep):管理每个响应式数据对应的所有副作用函数

响应式数据的注意事项

  1. Vue 2 注意事项

    • 新增对象属性必须使用 $set,删除属性使用 $delete
    • 数组修改优先使用 push/pop/shift/unshift/splice/sort/reverse 7 个变异方法,下标修改使用 $set
    • 初始化时必须在 data 中声明所有需要响应式的属性,避免后续新增属性无响应式
  2. Vue 3 注意事项

    • reactive 仅对引用类型(对象 / 数组 / Map 等)生效,对基本类型(字符串 / 数字 / 布尔值)无效,基本类型请使用 ref
    • 直接解构 reactive 对象会丢失响应式,需使用 toRefs/toRef 转换后再解构
    • reactive 对象直接赋值一个新对象,会丢失原对象的响应式代理,需使用 Object.assign 或修改属性值

2.3 生命周期钩子

Vue 组件从创建到销毁的整个过程,称为组件的生命周期。Vue 在生命周期的关键节点,提供了生命周期钩子函数,允许开发者在特定阶段执行自定义代码。

生命周期整体流程

组件生命周期分为 4 个核心阶段:

  1. 创建阶段:组件实例初始化,数据、事件初始化,未挂载到 DOM
  2. 挂载阶段:组件模板编译、DOM 渲染完成,挂载到页面
  3. 更新阶段:组件响应式数据变化,触发视图重新渲染
  4. 销毁阶段:组件实例销毁,清理事件监听、定时器、副作用

Vue 2 与 Vue 3 生命周期钩子对应表

表格

Vue 2 选项式钩子 Vue 3 选项式钩子 Vue 3 setup 组合式钩子 执行时机 核心使用场景
beforeCreate beforeCreate 无需对应,setup 替代 实例初始化之后,data/methods 等配置未初始化 几乎不用,Vue3 中 setup 执行时机早于该钩子
created created 无需对应,setup 替代 实例创建完成,data/methods 已初始化,未挂载 DOM 数据初始化、异步接口请求、全局事件监听注册
beforeMount beforeMount onBeforeMount 挂载开始前,模板编译完成,DOM 未生成 挂载前的最后数据修改,不会触发更新
mounted mounted onMounted 组件挂载完成,DOM 已渲染到页面 DOM 操作、第三方库初始化、异步接口请求、获取 DOM 元素
beforeUpdate beforeUpdate onBeforeUpdate 数据变化,视图更新前触发 获取更新前的 DOM 状态、更新前的数据备份
updated updated onUpdated 数据变化,视图重新渲染完成 操作更新后的 DOM、更新后的状态同步
beforeDestroy beforeUnmount onBeforeUnmount 组件销毁前触发,实例仍完全可用 清除定时器、取消事件监听、取消接口请求、解绑第三方库
destroyed unmounted onUnmounted 组件销毁完成,实例、DOM 已完全卸载 销毁后的收尾操作、日志上报

核心说明:Vue 3 的 setup 函数执行时机在 beforeCreate 之前,因此组合式 API 中无需 beforeCreatecreated 钩子,初始化逻辑直接写在 setup 中即可。

生命周期使用示例(Vue 3 组合式 API)

html 复制代码
<template>
  <div id="box">{{ count }}</div>
</template>

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

const count = ref(0)
let timer = null

// 挂载完成后执行
onMounted(() => {
  // 操作DOM
  const box = document.getElementById('box')
  console.log(box)
  // 启动定时器
  timer = setInterval(() => {
    count.value++
  }, 1000)
})

// 组件销毁前执行
onBeforeUnmount(() => {
  // 清除定时器,避免内存泄漏
  clearInterval(timer)
})
</script>

2.4 路由管理 (Vue Router)

Vue Router 是 Vue 官方的路由插件,核心作用是 在单页应用(SPA)中,实现 URL 与组件的精准映射,并且做到 "无刷新切换页面"

通俗比喻:把 SPA 当成 "大型商场"

  • 整个 Vue 应用 = 一座商场(只有 1 个 "大门",对应 1 个 HTML 文件,不会重新开门);
  • 每个组件 = 商场里的 "店铺"(比如 "首页店""商品详情店""我的订单店");
  • Vue Router = 商场的 "导航系统"(导览图 + 指路牌 + 电梯);
  • URL 地址 = 店铺的 "门牌号"(比如 /home 是首页店,/goods/123 是 123 号商品店);
  • 路由切换 = 顾客在商场内走电梯 / 指路牌找店铺(不用出商场大门,直接到达目标店铺,无刷新 = 不用重新进商场)。

没有 Vue Router 的 SPA,就像商场没有导航:顾客(用户)不知道怎么找店铺,只能在 "大厅"(根组件)里打转;有了 Vue Router,用户只需输入 "门牌号"(URL)或点击 "指路牌"(导航按钮),就能直接切换到对应 "店铺"(组件)。

当前版本对应关系:

  • Vue 3 → Vue Router 4
  • Vue 2 → Vue Router 3

安装与基本配置

  1. 安装依赖

    复制代码
    # Vue 3 安装
    npm install vue-router@4
    # Vue 2 安装
    npm install vue-router@3
  2. 基础配置(Vue 3 + Vite)新建 src/router/index.js 路由配置文件:

    javascript 复制代码
    // 从vue-router导入核心方法
    import { createRouter, createWebHistory } from 'vue-router'
    // 导入页面组件
    import Home from '@/views/Home.vue'
    import About from '@/views/About.vue'
    
    // 定义路由规则
    const routes = [
      {
        path: '/', // 路由路径
        name: 'Home', // 路由名称(可选)
        component: Home, // 对应的组件
        meta: { title: '首页' } // 路由元信息(可选,存储自定义数据)
      },
      {
        path: '/about',
        name: 'About',
        component: () => import('@/views/About.vue') // 路由懒加载
      }
    ]
    
    // 创建路由实例
    const router = createRouter({
      // 路由模式
      history: createWebHistory(import.meta.env.BASE_URL),
      // 路由规则
      routes
    })
    
    // 导出路由实例
    export default router
  3. 全局挂载路由在 src/main.js 中挂载路由到 Vue 应用:

    javascript 复制代码
    import { createApp } from 'vue'
    import App from './App.vue'
    import router from './router' // 导入路由实例
    
    // 挂载路由
    createApp(App).use(router).mount('#app')
  4. 路由视图与导航链接在根组件 App.vue 中添加路由视图和导航:

    html 复制代码
    <template>
      <div id="app">
        <!-- 导航链接,替代a标签,避免页面刷新 -->
        <router-link to="/">首页</router-link>
        <router-link to="/about">关于</router-link>
        <!-- 路由视图:匹配到的路由组件会渲染在这里 -->
        <router-view></router-view>
      </div>
    </template>

路由模式

Vue Router 提供两种核心路由模式,区别在于 URL 表现、实现原理、后端配置要求:

模式 实现原理 URL 表现 兼容性 后端配置
hash 模式 基于 URL 的 hash 值(#),hash 变化不会触发页面刷新,通过 hashchange 事件监听 http://localhost:5173/#/about 兼容所有浏览器,包括低版本 IE 无需后端配置,刷新不会 404
history 模式 基于 HTML5 History API,实现 URL 无刷新跳转 http://localhost:5173/about 兼容现代浏览器,IE10+ 必须配置后端,将所有路由请求 fallback 到 index.html,否则刷新页面会 404

核心路由能力

动态路由参数

用于匹配动态路径,比如详情页**/detail/1001,** 通过**:参数名**定义:

核心本质

URL 中携带动态参数(如商品 ID、用户 ID),让同一个组件适配不同 "参数场景"(无需为每个场景创建单独组件)。

通俗比喻

商场里的 "商品详情店":所有商品共用一个 "详情店铺",门牌号用 "商品 ID" 区分(比如 /goods/123 是 123 号商品,/goods/456 是 456 号商品),进店后根据门牌号展示对应商品信息。

javascript 复制代码
// 路由配置
const routes = [
  {
    path: '/detail/:id',
    name: 'Detail',
    component: () => import('@/views/Detail.vue')
  }
]

组件中获取参数:

javascript 复制代码
// 选项式API
this.$route.params.id

// 组合式API
import { useRoute } from 'vue-router'
const route = useRoute()
console.log(route.params.id)

新手容易误解的点

  • 坑 1:动态参数名写错(如路由配置是 :goodsId,组件内写 $route.params.id),导致参数获取为 undefined
  • 坑 2:动态路由的参数在刷新页面后仍存在(hash/history 模式均支持),但如果用 params 传递非动态参数(如 push({ name: 'goods', params: { name: '手机' } })),history 模式下刷新会丢失;
  • 坑 3:子路由的动态参数路径带 /,比如父路由 path: '/user',子路由写 path: '/:id',会导致 URL 变成 /id(而非 /user/id),正确写法是子路由 path: ':id'(不带 /
嵌套路由

用于实现页面的嵌套布局,比如后台管理系统的侧边栏 + 主体内容,通过 children 属性配置子路由:

核心本质

路由的层级结构对应组件的嵌套结构,实现 "父页面包含子页面" 的效果(如 "我的订单" 页面包含 "待付款""已完成" 子页面)。

通俗比喻

商场里的 "我的订单店"(父店铺),里面分 "待付款分区""已完成分区"(子店铺):用户进入 "我的订单店" 后,可在店内切换不同分区,无需离开父店铺(URL 也会变成 parent/child 层级形式)。

javascript 复制代码
const routes = [
  {
    path: '/admin',
    component: () => import('@/views/AdminLayout.vue'),
    children: [
      // 子路由path不以/开头,会拼接父路由path,最终路径 /admin/dashboard
      {
        path: 'dashboard',
        component: () => import('@/views/admin/Dashboard.vue')
      },
      {
        path: 'user',
        component: () => import('@/views/admin/User.vue')
      },
      // 空路径,匹配父路由 /admin 时默认渲染
      {
        path: '',
        redirect: '/admin/dashboard'
      }
    ]
  }
]

父组件 AdminLayout.vue 中需要添加 <router-view> 作为子路由的渲染出口。

编程式导航 除了 <router-link> 声明式导航,Vue Router 提供了编程式导航 API,用于在 JS 代码中控制页面跳转:

javascript 复制代码
// 选项式API
// 跳转到指定路径,新增历史记录
this.$router.push('/about')
this.$router.push({ path: '/detail', query: { id: 1001 } })
this.$router.push({ name: 'Detail', params: { id: 1001 } })

// 替换当前历史记录,不新增,点击后退不会回到该页面
this.$router.replace('/login')

// 前进/后退历史记录
this.$router.go(-1) // 后退一页
this.$router.go(1) // 前进一页

// 组合式API
import { useRouter } from 'vue-router'
const router = useRouter()
router.push('/about')

新手容易误解的点

  • 坑 1:父组件忘记写 <router-view>,导致子组件无法渲染(最常见);
  • 坑 2:子路由 path/,变成绝对路径(如子路由 path: '/unpaid',URL 直接是 /unpaid,而非 /order/unpaid);
  • 坑 3:嵌套路由的导航路径必须写完整路径(如 to="/order/unpaid"),不能只写 to="unpaid"(除非用相对路径,但新手不推荐)。

编程式导航

核心本质

通过 JavaScript 代码控制路由跳转(替代 <router-link> 标签),适用于 "逻辑触发后跳转" 场景(如登录成功、表单提交后)。

通俗比喻

顾客在商场里,不是通过 "指路牌"(router-link)找店铺,而是通过 "导购员"(代码)直接带到目标店铺(如登录成功后,导购员直接把顾客带到首页)。

核心方法($router 实例的 3 个核心方法)

方法 作用 类比场景
push(path/obj) 跳转到目标路由,添加历史记录 正常走路到店铺,可后退
replace(path/obj) 跳转到目标路由,替换历史记录 坐电梯到店铺,不可后退
go(n) 前进 / 后退 n 步(n 为数字) 按浏览器前进 / 后退按钮

代码示例

html 复制代码
<template>
  <button @click="handleLogin">登录</button>
  <button @click="goBack">返回上一页</button>
</template>

<script>
export default {
  methods: {
    handleLogin() {
      // 模拟登录成功
      const isLogin = true;
      if (isLogin) {
        // 1. 跳转到首页(简单路径)
        this.$router.push('/home');

        // 2. 跳转到动态路由(带参数)
        this.$router.push({ path: '/goods', params: { id: 123 } });

        // 3. 跳转到带查询参数的路由(?name=xxx)
        this.$router.push({ path: '/search', query: { keyword: '手机' } });

        // 4. 替换历史记录(登录后不能后退到登录页)
        this.$router.replace('/home');
      }
    },
    goBack() {
      // 后退 1 步(相当于浏览器后退)
      this.$router.go(-1);
      // 前进 1 步:this.$router.go(1)
    }
  }
}
</script>

新手容易误解的点

  • 坑 1:混淆 $route$router:用 this.$route.push(...) 报错(正确是 $router$route 是当前路由信息对象,没有导航方法);
  • 坑 2:push 传递 params 时用 path 字段(正确:用 name 或直接拼接路径,如 push({ name: 'goods', params: { id: 123 } }),或 push('/goods/123'));
  • 坑 3:go(-1) 在没有历史记录时会报错(可先判断 history.length,如 if (window.history.length > 1) this.$router.go(-1))。

路由守卫

路由守卫用于拦截导航过程,实现权限控制、登录拦截、页面标题修改、跳转确认等功能,分为三类:全局守卫、路由独享守卫、组件内守卫。

通俗比喻

顾客进入 "VIP 店铺"(需要权限的路由)前,安检员(路由守卫)检查是否有 VIP 卡(登录状态):有则放行,无则引导到办卡处(登录页);或进入店铺后,通知店员准备服务(路由进入后执行逻辑)。

  1. 全局守卫对所有路由生效,在路由实例上配置:

    javascript 复制代码
    // 全局前置守卫:每次导航跳转前执行,最常用,用于登录拦截
    router.beforeEach((to, from, next) => {
      // to:即将进入的目标路由对象
      // from:当前导航正要离开的路由对象
      // next:放行函数,必须调用一次才能完成导航
      
      // 登录拦截逻辑
      const token = localStorage.getItem('token')
      // 目标路由需要登录权限
      if (to.meta.requiresAuth && !token) {
        // 跳转到登录页
        next('/login')
      } else {
        // 放行
        next()
      }
    })
    
    // 全局后置钩子:导航完成后执行,无next函数,不改变导航
    router.afterEach((to, from) => {
      // 修改页面标题
      document.title = to.meta.title || 'Vue应用'
    })
  2. 路由独享守卫仅对单个路由生效,写在路由配置中:

    javascript 复制代码
    const routes = [
      {
        path: '/admin',
        component: () => import('@/views/Admin.vue'),
        beforeEnter: (to, from, next) => {
          // 管理员权限校验
          const isAdmin = localStorage.getItem('isAdmin')
          if (!isAdmin) {
            next('/403')
          } else {
            next()
          }
        }
      }
    ]
  3. 组件内守卫在组件内部定义,仅对当前组件生效:

    javascript 复制代码
    // 选项式API
    export default {
      // 进入组件前执行,此时组件实例还未创建,无法访问this
      beforeRouteEnter(to, from, next) {
        next(vm => {
          // 回调中可访问组件实例vm
        })
      },
      // 路由参数变化,组件复用时执行(比如 /detail/1001 → /detail/1002)
      beforeRouteUpdate(to, from, next) {
        // 可访问this,重新获取数据
        this.fetchData(to.params.id)
        next()
      },
      // 离开组件前执行,用于阻止未保存的表单页面离开
      beforeRouteLeave(to, from, next) {
        if (this.isFormChanged) {
          const isConfirm = window.confirm('内容未保存,确定要离开吗?')
          next(isConfirm)
        } else {
          next()
        }
      }
    }

新手容易误解的点

  • 坑 1:忘记调用 next(),导致路由切换卡住(页面无响应);
  • 坑 2:next() 调用多次(如 if (a) next(); next();),导致报错;
  • 坑 3:beforeRouteEnter 中直接用 this(此时组件未实例化,thisundefined,需用 next(vm => { ... }));
  • 坑 4:全局守卫中判断路径时用 to.path === '/order'(如果路由有子路由,如 /order/unpaid,会判断失败,推荐用 to.path.startsWith('/order'))。

路由参数传递

核心本质

通过路由 URL 传递数据,实现 "无父子关系" 的组件通信(常用两种方式:paramsquery)。

通俗比喻

  • params:相当于 "店铺门牌号的一部分"(如 /goods/123 中的 123),隐藏在路径中,更优雅;
  • query:相当于 "店铺门口贴的通知"(如 /search?keyword=手机),显式拼接在 URL 后,支持多参数。

两种参数对比与代码示例

特性 params(动态参数) query(查询参数)
URL 表现 /goods/123 /search?keyword=手机&price=5000
配置方式 路由路径需定义 :paramName 无需配置路由,直接拼接
获取方式 $route.params.paramName $route.query.queryName
刷新后是否丢失 不丢失(动态路由)/ 丢失(非动态) 不丢失(URL 中可见)
适用场景 唯一标识(商品 ID、用户 ID) 搜索条件、筛选参数(可分享 URL)

代码示例

javascript 复制代码
// 1. 传递 params(动态路由)
this.$router.push('/goods/123'); // 直接拼接
this.$router.push({ name: 'goods', params: { id: 123 } }); // 命名路由方式(推荐)

// 2. 传递 query
this.$router.push({ path: '/search', query: { keyword: '手机', price: 5000 } });

// 3. 组件内获取
console.log(this.$route.params.id); // 123
console.log(this.$route.query.keyword); // 手机
console.log(this.$route.query.price); // 5000

新手容易误解的点

  • 坑 1:用 path 传递 params(如 push({ path: '/goods', params: { id: 123 } })),params 会丢失,正确用 name 或直接拼接路径;
  • 坑 2:query 参数是字符串类型(如 price=5000 实际是 '5000'),需要手动转成数字类型;
  • 坑 3:params 传递非动态参数(如 push({ name: 'goods', params: { name: '手机' } })),history 模式下刷新会丢失,需用 query 或本地存储替代。

重定向与别名

核心本质

  • 重定向(redirect):访问 A 路由时,自动跳转到 B 路由(如访问 / 跳转到 /home);
  • 别名(alias):给路由起 "小名",访问小名和原名都能指向同一个组件(如 /main/home 的别名,访问 /main 也能看到首页)。

通俗比喻

  • 重定向:商场 "大门"(/)直接通向 "首页店"(/home),顾客不用手动找;
  • 别名:"首页店" 的门牌号除了 /home,还有 /main,两个门牌号都能找到同一店铺。

代码示例

javascript 复制代码
// src/router/index.js
const routes = [
  // 1. 重定向:访问 / 跳转到 /home
  { path: '/', redirect: '/home' },
  // 重定向到动态路由(如访问 /product/123 跳转到 /goods/123)
  { path: '/product/:id', redirect: '/goods/:id' },

  // 2. 别名:/main 是 /home 的别名,访问 /home 或 /main 都渲染 Home 组件
  { path: '/home', component: () => import('@/views/Home.vue'), alias: '/main' }
]

新手容易误解的点

  • 坑 1:重定向用 component 替代 redirect(如 { path: '/', component: '/home' }),导致报错,正确用 redirect

  • 坑 2:别名 alias 写绝对路径时,要和父路由层级匹配(如嵌套路由 path: 'unpaid',别名不能写 /unpaid,要写 'unpaid-alias');

  • 坑 3:重定向和别名混淆:重定向会改变 URL(如访问 / 会变成 /home),别名不会改变 URL(访问 /main 仍显示 /main)。

路由懒加载

核心本质

默认情况下,所有路由组件会在项目启动时一次性加载(首屏加载慢);路由懒加载让组件 "按需加载"------ 只有访问该路由时,才下载组件代码,减少首屏加载时间。

通俗比喻

商场里的 "冷门店铺"(如 "售后服务店"),平时不打开大门(不加载代码),只有顾客要去时才开门(加载代码),节省商场的电力(项目性能)。

代码示例

javascript 复制代码
// src/router/index.js
const routes = [
  // 1. 箭头函数 + import()(推荐,简洁)
  { path: '/home', component: () => import('@/views/Home.vue') },
  // 2. 定义变量(可命名,便于维护)
  const GoodsDetail = () => import('@/views/GoodsDetail.vue');
  { path: '/goods/:id', component: GoodsDetail }
]

新手容易误解的点

  • 坑 1:把懒加载和异步组件搞混(路由懒加载是异步组件的一种特殊场景,专门用于路由);
  • 坑 2:懒加载组件时忘记加 ()(如 component: import('@/views/Home.vue')),导致报错,正确是 () => import(...)
  • 坑 3:认为懒加载会让页面切换变慢(实际首屏加载更快,切换时的加载时间可通过骨架屏优化,整体体验更好)。

2.5 状态管理 (Vuex/Pinia)

状态管理用于管理 Vue 应用中的全局共享状态,解决跨组件、跨层级的状态共享问题,比如用户登录信息、购物车数据、全局配置等,实现状态的统一管理、可追踪、可预测。

为什么需要状态管理

  • 多层嵌套组件的传参极其繁琐,事件冒泡难以维护
  • 兄弟组件、无关联组件之间无法便捷共享状态
  • 多个组件依赖同一个状态,修改逻辑分散,难以维护
  • 状态变更无追踪,出现 bug 难以定位原因

Vuex 核心概念

Vuex 是 Vue 官方的传统状态管理库,专为 Vue 设计,遵循单向数据流、状态唯一修改途径的设计原则。

当前版本对应:

  • Vue 3 → Vuex 4
  • Vue 2 → Vuex 3

Vuex 有 5 个核心概念:

把 Vuex 比作大型超市的仓储管理体系,5 个核心概念对应超市的不同岗位 / 区域:

概念 超市类比 核心本质 关键规则
State 超市总货架 全局唯一的原始数据仓库 只读,不能直接改
Getter 超市导购员 State 数据的「加工 / 过滤器」 只读取、不修改数据
Mutation 货架理货员 修改 State 的唯一入口 必须是同步函数
Action 超市采购员 处理异步操作(如进货 / 网络请求) 不能直接改 State,只能调用 Mutation
Module 超市分区(零食区 / 生鲜区) 大型项目的状态拆分 每个模块独立管理自己的数据
State(总货架):

单一状态树,应用的唯一数据源,所有全局状态都存储在 State 中,类似组件的 data。

  • 作用 :存储整个应用的全局原始数据,是一个对象。
  • 本质 :Vuex 的唯一数据源,所有共享数据都存在这里。
  • 新手误区 :直接在组件里修改 this.$store.state.xxx ❌ 禁止直接修改!必须通过 Mutation。
javascript 复制代码
// store/index.js
import { createStore } from 'vuex'

export default createStore({
  state: {
    userInfo: null,
    token: '',
    cartCount: 0
  }
})

组件中访问:

javascript 复制代码
// 选项式API
this.$store.state.userInfo
// 组合式API
import { useStore } from 'vuex'
const store = useStore()
console.log(store.state.userInfo)
Getters(导购员):

派生状态,基于 State 计算得到,类似组件的 computed,有依赖缓存,只有依赖的 State 变化时才会重新计算。

  • 作用 :对 State 数据做加工、过滤、计算 ,类似 Vue 组件的 computed 计算属性。
  • 本质:State 的「派生数据」,复用数据处理逻辑,不用每个组件都写一遍。
javascript 复制代码
createStore({
  state: {
    goodsList: [
      { id: 1, name: '商品1', price: 100, checked: true },
      { id: 2, name: '商品2', price: 200, checked: false }
    ]
  },
  getters: {
    // 计算选中商品的总价
    checkedTotalPrice(state) {
      return state.goodsList
        .filter(item => item.checked)
        .reduce((sum, item) => sum + item.price, 0)
    }
  }
})

组件中访问:

javascript 复制代码
this.$store.getters.checkedTotalPrice
Mutations(理货员):

唯一修改 State 的合法途径 ,必须是同步函数,通过 commit 触发。

  • 作用唯一能修改 State 的地方,修改逻辑写在这里。
  • 本质:同步修改 State 的工具。
  • 铁律 :必须是同步函数!(理货员只能当场理货,不能等半天)
  • 新手误区:把异步请求(axios)写在 Mutation 里 ❌ 绝对不行!
javascript 复制代码
createStore({
  state: {
    cartCount: 0
  },
  mutations: {
    // 第一个参数是state,后续参数是传递的载荷payload
    setCartCount(state, count) {
      state.cartCount = count
    },
    incrementCartCount(state, step = 1) {
      state.cartCount += step
    }
  }
})

组件中触发:

javascript 复制代码
// 提交mutation
this.$store.commit('setCartCount', 10)
this.$store.commit('incrementCartCount', 2)
Actions(采购员):

用于处理异步操作,不能直接修改 State,必须通过 commit 提交 Mutation 修改 State,通过 dispatch 触发。

  • 作用 :处理异步操作(如网络请求、定时器),拿到结果后,调用 Mutation 修改 State。
  • 本质:异步操作的中转站,自己不碰货架,只指挥理货员。
  • 铁律 :不能直接修改 State,必须通过 commit 触发 Mutation。
javascript 复制代码
createStore({
  state: {
    userInfo: null
  },
  mutations: {
    setUserInfo(state, userInfo) {
      state.userInfo = userInfo
    }
  },
  actions: {
    // 第一个参数是context对象,包含commit、dispatch、state、getters
    // 后续参数是载荷payload
    async fetchUserInfo(context, userId) {
      // 异步接口请求
      const res = await getUserInfoApi(userId)
      // 提交mutation修改state
      context.commit('setUserInfo', res.data)
    }
  }
})

组件中触发:

javascript 复制代码
this.$store.dispatch('fetchUserInfo', 1001)
Modules(分区货架):

模块化,将庞大的 Store 拆分为多个独立的模块,每个模块拥有自己的 State、Getters、Mutations、Actions,甚至嵌套子模块,解决单状态树的臃肿问题。

  • 作用:大型项目中,把 Vuex 拆分成多个小模块,避免所有数据挤在一起。
  • 本质:模块化拆分,让代码更易维护。
  • 场景:电商项目 → 用户模块、商品模块、订单模块,互不干扰。
javascript 复制代码
// 用户模块
const userModule = {
  namespaced: true, // 开启命名空间,避免命名冲突
  state: { userInfo: null, token: '' },
  mutations: {
    setToken(state, token) {
      state.token = token
    }
  },
  actions: {
    async login(context, loginForm) {
      // 登录逻辑
    }
  }
}

// 购物车模块
const cartModule = {
  namespaced: true,
  state: { cartList: [] },
  mutations: {},
  actions: {}
}

// 根store注册模块
export default createStore({
  modules: {
    user: userModule,
    cart: cartModule
  }
})

命名空间模块的访问与触发:

javascript 复制代码
// 访问state
this.$store.state.user.token
// 提交mutation
this.$store.commit('user/setToken', 'xxx')
// 触发action
this.$store.dispatch('user/login', loginForm)

Vuex 完整使用流程(代码 + 讲解)

步骤 1:安装
复制代码
npm install vuex --save
步骤 2:配置 Store(储物柜核心代码)

这是标准模板,我加了注释,逐行看懂:

javascript 复制代码
// 1. 引入Vue和Vuex
import Vue from 'vue';
import Vuex from 'vuex';

// 2. 给Vue安装Vuex插件
Vue.use(Vuex);

// 3. 创建Vuex仓库实例
const store = new Vuex.Store({
  // 👉 State:总货架(原始数据)
  state: {
    count: 0 // 共享数据:数字计数器
  },

  // 👉 Getter:导购员(加工数据)
  getters: {
    // 加工:给count加个前缀
    getCounter: (state) => {
      return `当前计数:${state.count}`;
    }
  },

  // 👉 Mutation:理货员(同步修改State)
  mutations: {
    // 定义修改方法:数字+1
    increment(state) {
      state.count++;
    }
  },

  // 👉 Action:采购员(异步操作)
  actions: {
    // 模拟异步:延迟1秒后修改
    asyncIncrement({ commit }) {
      setTimeout(() => {
        commit('increment'); // 调用Mutation修改数据
      }, 1000);
    }
  }
});

// 4. 导出仓库,给Vue项目使用
export default store;
步骤 3:在 Vue 组件中使用(存取数据)
html 复制代码
<template>
  <div>
    <!-- 直接读取State -->
    <p>原始数据:{{ $store.state.count }}</p>
    <!-- 读取Getter加工后的数据 -->
    <p>加工数据:{{ $store.getters.getCounter }}</p>

    <!-- 同步修改:触发Mutation -->
    <button @click="$store.commit('increment')">同步+1</button>
    <!-- 异步修改:触发Action -->
    <button @click="$store.dispatch('asyncIncrement')">异步+1</button>
  </div>
</template>

Pinia 简介与核心概念

Pinia 是 Vue 官方推荐的新一代状态管理库,被称为 Vuex 5,完全替代 Vuex,专为 Vue 3 设计,完美支持组合式 API 和 TypeScript。

核心优势

  • 去掉了 Mutations,仅保留 State、Getters、Actions,同步异步都可在 Actions 中处理,API 更简洁
  • 原生完美支持 TypeScript,类型推导友好,无需复杂的类型声明
  • 支持组合式 API,写法更灵活,和组件的组合式 API 风格统一
  • 无命名空间的烦恼,每个 Store 都是独立的模块,天然隔离
  • 体积更小,调试更友好,支持 Vue Devtools,状态变更可追踪
  • 兼容 Vue 2 和 Vue 3
Pinia 基础使用
安装依赖
复制代码
   npm install pinia
全局挂载
javascript 复制代码
// main.js
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'

createApp(App).use(createPinia()).mount('#app')
定义 Store新建 src/stores/user.js
javascript 复制代码
import { defineStore } from 'pinia'

// 定义并导出Store,第一个参数是唯一的store id
export const useUserStore = defineStore('user', {
  // 状态:类似Vuex的State,必须是函数返回对象
  state: () => {
    return {
      userInfo: null,
      token: localStorage.getItem('token') || '',
      userId: 0
    }
  },
  // 派生状态:类似Vuex的Getters,有缓存
  getters: {
    isLogin: (state) => !!state.token
  },
  // 方法:类似Vuex的Actions,同步异步都可处理,直接通过this访问state
  actions: {
    setToken(token) {
      this.token = token
      localStorage.setItem('token', token)
    },
    async fetchUserInfo() {
      if (!this.token) return
      // 异步接口请求
      const res = await getUserInfoApi()
      this.userInfo = res.data
      this.userId = res.data.id
    },
    logout() {
      this.token = ''
      this.userInfo = null
      localStorage.removeItem('token')
    }
  }
})
组件中使用
html 复制代码
<template>
  <div>
    <p v-if="userStore.isLogin">欢迎你,{{ userStore.userInfo?.name }}</p>
    <button @click="handleLogout" v-if="userStore.isLogin">退出登录</button>
  </div>
</template>

<script setup>
// 导入定义的store
import { useUserStore } from '@/stores/user'
// 创建store实例
const userStore = useUserStore()

// 调用actions方法
const handleLogout = () => {
  userStore.logout()
}
</script>

推荐用法:Pinia 优先支持组合式 API 写法,也可以使用 mapStatemapActions 等辅助函数适配选项式 API,是 Vue 3 项目的首选状态管理方案。

第三部分:实战与进阶

3.1 Composition API (Vue 3 核心)

Composition API(组合式 API)是 Vue 3 引入的核心新特性,彻底改变了 Vue 组件的代码组织方式,解决了 Vue 2 选项式 API(Options API)在大型项目中逻辑分散、复用困难的问题,同时提供了完美的 TypeScript 支持。

Options API vs Composition API

特性 Options API(选项式) Composition API(组合式)
代码组织 按选项拆分代码,data、methods、computed、watch 分块书写 按业务逻辑组织代码,相关的响应式数据、方法、计算属性写在一起
逻辑复用 依赖 mixin,存在命名冲突、来源不明、数据追踪困难的问题 依赖组合式函数(Composables),无命名冲突,来源清晰,类型安全
TypeScript 支持 支持差,需要复杂的类型声明,类型推导困难 原生完美支持,类型推导友好,无需额外声明
适用场景 小型简单组件,逻辑单一 中大型复杂组件,多逻辑耦合,需要逻辑复用
执行上下文 所有选项都挂载到 this 上,this 指向固定 无 this,基于函数式编程,代码更易压缩,tree-shaking 友好

setup () 函数

setup() 是组合式 API 的入口函数,是组件中使用组合式 API 的唯一位置。

核心特性

  • 执行时机:在组件实例创建之前,beforeCreate 钩子之前执行,因此没有 this
  • 接收两个参数:
    1. props:父组件传递的 props,是响应式的,不能解构,解构会丢失响应式
    2. context:上下文对象,包含 attrs(透传属性)、slots(插槽)、emit(触发事件)、expose(暴露组件方法给父组件)
  • 返回值:返回一个对象,对象中的属性和方法可以在模板中直接使用;也可以返回渲染函数

基础示例:

html 复制代码
<template>
  <div>
    <p>计数:{{ count }}</p>
    <button @click="increment">+1</button>
    <p>双倍计数:{{ doubleCount }}</p>
  </div>
</template>

<script>
import { ref, computed } from 'vue'
export default {
  setup(props, context) {
    // 定义响应式数据
    const count = ref(0)
    // 定义方法
    const increment = () => {
      count.value++
    }
    // 定义计算属性
    const doubleCount = computed(() => count.value * 2)

    // 返回给模板使用
    return {
      count,
      increment,
      doubleCount
    }
  }
}
</script>

更简洁的写法:<script setup> 语法糖 <script setup> 是单文件组件中使用组合式 API 的语法糖,大幅简化代码,无需写 setup 函数和 return 语句,顶层的变量、方法、导入的组件都可以直接在模板中使用,是 Vue 3 官方推荐的写法。

html 复制代码
<template>
  <div>
    <p>计数:{{ count }}</p>
    <button @click="increment">+1</button>
    <p>双倍计数:{{ doubleCount }}</p>
  </div>
</template>

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

// 直接定义,无需return,模板可直接使用
const count = ref(0)
const increment = () => {
  count.value++
}
const doubleCount = computed(() => count.value * 2)
</script>

核心响应式 API

  1. ref() 用于定义任意类型的响应式数据,优先用于基本类型(字符串、数字、布尔值),也支持引用类型(对象、数组)。

    • 核心特点:通过 .value 属性访问和修改值,模板中使用会自动解包,无需写 .value
    • 底层实现:基本类型基于 Object.defineProperty,引用类型会自动调用 reactive 做深层代理
    javascript 复制代码
    import { ref } from 'vue'
    
    // 基本类型
    const count = ref(0)
    console.log(count.value) // 0
    count.value = 1
    console.log(count.value) // 1
    
    // 引用类型
    const userInfo = ref({ name: '张三', age: 18 })
    userInfo.value.age = 20
  2. reactive() 用于定义引用类型(对象、数组、Map、Set 等)的响应式数据,不能用于基本类型。

    • 核心特点:无需 .value,直接访问和修改属性,深层响应式,嵌套对象也有响应式
    • 缺陷:直接解构会丢失响应式;直接给对象赋值新对象会丢失原代理
    javascript 复制代码
    import { reactive } from 'vue'
    
    const userInfo = reactive({
      name: '张三',
      age: 18,
      address: {
        city: '北京'
      }
    })
    
    // 直接修改,无需.value
    userInfo.age = 20
    userInfo.address.city = '上海'
  3. toRefs() / toRef() 解决 reactive 解构丢失响应式的问题。

    • toRefs():将 reactive 对象的所有属性转为 ref 对象,解构后仍保持响应式
    • toRef():将 reactive 对象的单个属性 转为 ref 对象
    javascript 复制代码
    import { reactive, toRefs, toRef } from 'vue'
    
    const userInfo = reactive({
      name: '张三',
      age: 18
    })
    
    // 解构后仍有响应式
    const { name, age } = toRefs(userInfo)
    console.log(age.value) // 18
    age.value = 20 // 修改会同步到原reactive对象
    
    // 单个属性转ref
    const userName = toRef(userInfo, 'name')
  4. **computed()**组合式 API 中的计算属性,支持只读和可写两种模式。

    javascript 复制代码
    import { ref, computed } from 'vue'
    
    const firstName = ref('张')
    const lastName = ref('三')
    
    // 只读模式(默认)
    const fullName = computed(() => {
      return firstName.value + lastName.value
    })
    
    // 可写模式
    const fullNameWritable = computed({
      get: () => firstName.value + lastName.value,
      set: (newVal) => {
        const [first, last] = newVal.split('')
        firstName.value = first
        lastName.value = last
      }
    })
  5. **watch()**侦听器,监听一个或多个响应式数据源,数据变化时执行回调函数,惰性执行(默认只有数据变化才执行),可获取新旧值。

    javascript 复制代码
    import { ref, watch } from 'vue'
    
    const count = ref(0)
    const userInfo = reactive({
      name: '张三',
      age: 18
    })
    
    // 监听单个ref
    watch(count, (newVal, oldVal) => {
      console.log('count变化', newVal, oldVal)
    })
    
    // 监听多个数据源
    watch([count, () => userInfo.age], ([newCount, newAge], [oldCount, oldAge]) => {
      console.log('数据变化')
    })
    
    // 深度监听:监听reactive对象的深层属性变化
    watch(
      () => userInfo,
      (newVal) => {
        console.log('userInfo深层变化')
      },
      { deep: true }
    )
    
    // 立即执行:页面初始化就执行一次回调
    watch(
      count,
      (newVal) => {
        console.log('立即执行', newVal)
      },
      { immediate: true }
    )
  6. **watchEffect()**立即执行的副作用函数,自动收集依赖,依赖的响应式数据变化时自动重新执行,无需指定监听源。

    javascript 复制代码
    import { ref, watchEffect } from 'vue'
    
    const count = ref(0)
    const name = ref('张三')
    
    // 立即执行一次,自动收集count和name为依赖
    watchEffect(() => {
      console.log(`count: ${count.value}, name: ${name.value}`)
    })
    
    count.value++ // 依赖变化,重新执行
    name.value = '李四' // 依赖变化,重新执行

生命周期钩子在 setup 中的使用

组合式 API 中的生命周期钩子,需要先从 vue 中导入,以 on 开头,在 setup 中注册,执行时机和选项式 API 一致。

javascript 复制代码
<script setup>
import {
  onBeforeMount,
  onMounted,
  onBeforeUpdate,
  onUpdated,
  onBeforeUnmount,
  onUnmounted
} from 'vue'

onMounted(() => {
  console.log('组件挂载完成')
})

onBeforeUnmount(() => {
  console.log('组件即将销毁')
})
</script>

自定义组合式函数 (Composables)

组合式函数(Composables)是利用 Vue 组合式 API 封装和复用状态逻辑的函数,是 Vue 3 中逻辑复用的核心方案,彻底替代 Vue 2 中的 mixin。

命名规范 :以 use 开头的驼峰命名,比如 useMouseuseFetchuseLocalStorage

示例 1:封装鼠标位置逻辑

javascript 复制代码
// src/composables/useMouse.js
import { ref, onMounted, onBeforeUnmount } from 'vue'

// 导出组合式函数
export function useMouse() {
  // 封装内部响应式状态
  const x = ref(0)
  const y = ref(0)

  // 封装内部方法
  const updateMouse = (e) => {
    x.value = e.pageX
    y.value = e.pageY
  }

  // 生命周期管理
  onMounted(() => {
    window.addEventListener('mousemove', updateMouse)
  })

  onBeforeUnmount(() => {
    window.removeEventListener('mousemove', updateMouse)
  })

  // 暴露给外部的状态和方法
  return { x, y }
}

组件中使用

html 复制代码
<template>
  <p>鼠标X坐标:{{ x }}</p>
  <p>鼠标Y坐标:{{ y }}</p>
</template>

<script setup>
import { useMouse } from '@/composables/useMouse'
// 复用逻辑,解构获取状态
const { x, y } = useMouse()
</script>

核心优势

  • 无命名冲突:每个组件调用组合式函数,都会创建独立的响应式状态,互不影响
  • 来源清晰:状态和方法的来源明确,直接看导入和调用即可
  • 类型安全:完美支持 TypeScript,类型自动推导
  • 按需复用:只导入需要的逻辑,tree-shaking 友好
  • 逻辑内聚:相关的状态、方法、生命周期都封装在一个函数中,维护方便

3.2 项目结构与工程化

Vue 项目工程化是大型项目开发的基础,合理的项目结构、规范的代码组织、标准化的开发流程,能大幅提升团队协作效率、降低维护成本、提升项目稳定性。

目录结构最佳实践(Vue 3 + Vite)

以下是企业级 Vue 项目的标准目录结构,兼顾可扩展性、可维护性、团队协作:

复制代码
vue-project/
├── public/                 # 静态资源目录,不会被构建工具处理,直接复制到打包根目录
│   ├── favicon.ico         # 网站图标
│   └── static/             # 不参与打包的静态资源,比如大图片、第三方库
├── src/                    # 项目源码核心目录
│   ├── api/                # 接口请求目录,统一管理所有API接口
│   │   ├── modules/        # 按业务模块拆分接口
│   │   │   ├── user.js     # 用户相关接口
│   │   │   └── goods.js    # 商品相关接口
│   │   └── request.js      # Axios实例封装,请求/响应拦截器
│   ├── assets/             # 参与构建的静态资源,会被Vite处理、压缩、哈希命名
│   │   ├── images/         # 图片资源
│   │   ├── fonts/          # 字体文件
│   │   └── styles/         # 全局样式文件
│   │       ├── index.scss  # 全局样式入口
│   │       ├── variables.scss # 全局SCSS变量
│   │       └── reset.scss  # 样式重置
│   ├── components/         # 组件目录
│   │   ├── common/         # 全局通用基础组件,比如按钮、输入框、弹窗
│   │   └── business/       # 业务通用组件,多个页面复用的业务组件
│   ├── composables/        # 组合式函数目录,封装可复用的业务逻辑
│   ├── directives/         # 全局自定义指令
│   ├── plugins/            # Vue插件目录,全局注册组件、指令、原型方法
│   ├── router/             # 路由配置目录
│   │   ├── index.js        # 路由入口文件
│   │   └── modules/        # 按业务模块拆分路由配置
│   ├── stores/             # Pinia状态管理目录,按业务模块拆分store
│   ├── utils/              # 工具函数目录,封装通用工具方法
│   │   ├── auth.js         # 权限、token相关工具
│   │   ├── validate.js     # 表单校验工具
│   │   └── format.js       # 数据格式化工具
│   ├── views/              # 页面组件目录,对应路由的页面
│   │   ├── home/           # 首页模块
│   │   ├── login/          # 登录页
│   │   └── 404.vue         # 404页面
│   ├── App.vue             # 根组件
│   └── main.js             # 项目入口文件
├── .env                    # 全局环境变量
├── .env.development        # 开发环境变量
├── .env.production         # 生产环境变量
├── .eslintrc.js            # ESLint配置
├── .prettierrc             # Prettier配置
├── vite.config.js          # Vite配置文件
├── package.json            # 项目依赖、脚本配置
└── README.md               # 项目说明文档

模块化与组件划分原则

  1. 单一职责原则一个组件 / 模块只负责一件事,功能边界清晰,避免一个组件承载过多逻辑,通常单个组件代码不超过 500 行,超出则考虑拆分。

  2. 分层设计原则组件分为三层,从上到下依赖,禁止反向依赖:

    • 页面组件(Page):对应路由页面,负责页面布局、数据获取、状态管理,不包含复杂的 UI 逻辑,只调用业务组件和基础组件
    • 业务组件(Business Component):封装特定业务逻辑的可复用组件,比如商品卡片、订单列表、用户信息卡片,只接收 props、触发事件,不直接调用接口
    • 基础组件(Common Component):无业务逻辑的通用 UI 组件,比如按钮、输入框、弹窗、表格,只负责 UI 渲染和基础交互,可在全项目复用
  3. 可复用性原则重复出现 2 次及以上的 UI 和逻辑,必须抽离为组件 / 组合式函数;组件设计要考虑通用性,通过 props 配置不同行为,避免硬编码业务逻辑。

  4. 数据流向原则严格遵循单向数据流,数据通过 props 向下传递,事件向上传递,禁止子组件直接修改 props、禁止直接修改父组件数据、禁止组件之间直接互相修改状态。

  5. 避免过度封装不要为了封装而封装,简单的逻辑无需过度拆分,平衡复用性和可维护性。

代码风格与规范

统一的代码规范是团队协作的基础,推荐使用 ESLint + Prettier 组合,实现代码检查和自动格式化,配合 Vue 官方的规范插件。

  1. 核心依赖安装

    复制代码
    # ESLint 核心
    npm install eslint --save-dev
    # Vue ESLint 插件
    npm install eslint-plugin-vue --save-dev
    # Prettier 核心
    npm install prettier --save-dev
    # ESLint 与 Prettier 兼容插件
    npm install eslint-config-prettier eslint-plugin-prettier --save-dev
  2. 基础规范约定

    • 命名规范:
      • 组件名:大驼峰命名(PascalCase),比如 UserCard.vueMyButton.vue
      • 组合式函数:小驼峰命名,以 use 开头,比如 useFetchuseLocalStorage
      • 变量、方法:小驼峰命名(camelCase),语义化,避免拼音、缩写
      • 常量:全大写,下划线分隔,比如 MAX_PAGE_SIZEBASE_API_URL
    • 模板规范:
      • 多个属性换行,每行一个属性
      • 指令缩写统一,: 代替 v-bind:@ 代替 v-on:
      • 模板中避免复杂逻辑,复杂计算抽离到 computed
    • 脚本规范:
      • 导入顺序:内置模块 → 第三方依赖 → 自定义组件 / 工具
      • 避免 this 滥用,组合式 API 中无 this
      • 回调函数优先使用箭头函数,避免 this 指向混乱
      • 避免魔法数字,抽离为常量
    • 样式规范:
      • 单文件组件样式默认添加 scoped,避免样式污染
      • 通用样式抽离到全局样式文件,避免重复代码
      • 统一使用 SCSS/Less 预处理器,使用变量管理颜色、尺寸

环境变量与配置

Vite 内置了环境变量支持,通过 .env 文件管理不同环境的配置,避免硬编码,实现开发、测试、生产环境的隔离。

  1. 环境变量文件规则

    • .env:所有环境都会加载的全局环境变量
    • .env.development:开发环境(npm run dev)加载
    • .env.production:生产环境(npm run build)加载
    • .env.test:测试环境加载,需配合 --mode test 指定模式
  2. 环境变量定义规则

    • Vite 环境变量必须以 VITE_ 开头,否则不会暴露给客户端

      .env.development 开发环境

      VITE_BASE_API = /api
      VITE_APP_TITLE = Vue开发环境

      .env.production 生产环境

      VITE_BASE_API = https://api.xxx.com
      VITE_APP_TITLE = Vue生产环境

  3. 环境变量使用

    • 项目中通过 import.meta.env.变量名 访问
    javascript 复制代码
    // request.js
    const service = axios.create({
      baseURL: import.meta.env.VITE_BASE_API,
      timeout: 10000
    })

构建工具简介与优化

Vue 项目主流构建工具分为 Vite 和 Webpack(Vue CLI),Vite 是 Vue 3 官方推荐的构建工具,基于 ESBuild 和 Rollup,开发体验远超 Webpack。

  1. Vite 核心优势

    • 极速的冷启动:基于浏览器原生 ES 模块,无需打包整个项目,启动时间几乎与项目复杂度无关
    • 即时热更新(HMR):修改代码后,只更新修改的模块,无需刷新页面,更新速度不随项目变大而变慢
    • 开箱即用:内置对 TypeScript、JSX、CSS 预处理器、静态资源的原生支持,无需复杂配置
    • 优化的构建:基于 Rollup 构建,自动 tree-shaking、代码分割、压缩,打包产物更优
  2. Vite 基础优化配置

    javascript 复制代码
    // vite.config.js
    import { defineConfig } from 'vite'
    import vue from '@vitejs/plugin-vue'
    import { resolve } from 'path'
    // 包体积分析插件
    import { visualizer } from 'rollup-plugin-visualizer'
    
    export default defineConfig({
      // 插件配置
      plugins: [
        vue(),
        visualizer({ open: true }) // 打包后自动打开包体积分析页面
      ],
      // 路径别名配置
      resolve: {
        alias: {
          '@': resolve(__dirname, 'src') // @ 指向src目录
        }
      },
      // 开发服务配置
      server: {
        port: 3000, // 端口号
        open: true, // 启动自动打开浏览器
        cors: true, // 开启跨域
        // 代理配置,解决开发环境跨域问题
        proxy: {
          '/api': {
            target: 'https://api.xxx.com', // 目标接口地址
            changeOrigin: true, // 开启跨域
            rewrite: (path) => path.replace(/^\/api/, '') // 路径重写
          }
        }
      },
      // 构建配置
      build: {
        outDir: 'dist', // 打包输出目录
        assetsDir: 'assets', // 静态资源目录
        sourcemap: false, // 生产环境关闭sourcemap,减小包体积
        // 分包配置
        rollupOptions: {
          output: {
            // 分包策略:将第三方依赖单独打包
            manualChunks: {
              vue: ['vue', 'vue-router', 'pinia'],
              ui: ['element-plus'],
              utils: ['axios', 'dayjs']
            }
          }
        }
      }
    })

3.3 性能优化

Vue 项目性能优化分为运行时性能优化打包体积优化两大方向,核心目标是提升首屏加载速度、降低页面卡顿、提升用户体验。

虚拟 DOM 与 Diff 算法理解

Vue 的渲染核心是虚拟 DOM 和 Diff 算法,理解其原理是性能优化的基础。

  • 虚拟 DOM:用 JavaScript 对象描述真实 DOM 的结构,避免直接操作 DOM。每次数据变化,生成新的虚拟 DOM 树,对比新旧虚拟 DOM 的差异,只把差异部分更新到真实 DOM,大幅减少 DOM 操作次数。
  • Diff 算法 :Vue 的 Diff 算法采用同层比较 策略,只对比同一层级的节点,不跨层级对比,时间复杂度从 O (n³) 优化到 O (n);采用双端对比 算法,高效处理列表节点的移动、新增、删除;通过key给节点唯一标识,避免就地复用导致的渲染错误和性能损耗。

优化关键点

  • 列表渲染必须添加key,且key必须是唯一稳定的(比如 id),禁止使用 index 作为 key,否则节点顺序变化时,Diff 算法会出现错误复用,导致性能损耗和渲染 bug。
  • 避免嵌套过深的 DOM 结构,减少虚拟 DOM 的对比层级,提升 Diff 效率。

避免不必要的渲染

  1. v-once 指令只渲染元素和组件一次,后续数据变化不会重新渲染,适用于静态内容、无数据变化的节点,减少渲染开销。

    html 复制代码
    <p v-once>这是静态内容,不会重新渲染</p>
  2. 合理使用 v-if 和 v-show

    • v-if:不满足条件时,节点不会渲染,直接销毁,切换时会触发组件的销毁和重建,开销大。适用于切换频率低的场景
    • v-show:始终渲染节点,通过display: none控制显隐,切换开销极小。适用于切换频率高的场景 。 错误用法:频繁切换的弹窗、tab 使用 v-if,导致每次切换都重新创建组件,性能极差。
  3. 避免模板中的复杂计算 模板中写复杂的计算逻辑,每次组件重新渲染都会重新执行,导致性能损耗。应将复杂计算抽离到computed,利用缓存特性,依赖不变时不会重新计算。

    html 复制代码
    <!-- 错误写法:模板中复杂计算 -->
    <p>{{ list.filter(item => item.checked).reduce((sum, item) => sum + item.price, 0) }}</p>
    
    <!-- 正确写法:抽离到computed -->
    <p>{{ totalPrice }}</p>
    <script setup>
    const totalPrice = computed(() => {
      return list.filter(item => item.checked).reduce((sum, item) => sum + item.price, 0)
    })
    </script>

计算属性和侦听器的优化

  • 优先使用computed做数据派生,利用缓存特性,减少重复计算。

  • 避免滥用watch,只在必要的异步操作、副作用场景使用。

  • 避免不必要的深度监听deep: true,深度监听会遍历对象的所有属性,性能损耗大,尽量精准监听需要的属性。

    javascript 复制代码
    // 错误写法:深度监听整个对象
    watch(
      userInfo,
      () => {
        console.log('name变化')
      },
      { deep: true }
    )
    
    // 正确写法:只监听需要的属性
    watch(
      () => userInfo.name,
      () => {
        console.log('name变化')
      }
    )

组件懒加载 / 路由懒加载

懒加载也叫按需加载,核心是将代码分包,首屏不加载非必要的代码,访问对应页面 / 组件时再加载,大幅减小首屏包体积,提升首屏加载速度。

  1. 路由懒加载(最常用)

    javascript 复制代码
    // router/index.js
    const routes = [
      {
        path: '/',
        name: 'Home',
        component: () => import('@/views/Home.vue') // 懒加载
      },
      {
        path: '/about',
        name: 'About',
        component: () => import('@/views/About.vue') // 懒加载
      }
    ]
  2. 组件懒加载对于大组件、非首屏的组件,使用动态导入实现懒加载:

    javascript 复制代码
    <template>
      <div>
        <!-- 点击时才加载组件 -->
        <button @click="showBigComponent = true">显示大组件</button>
        <BigComponent v-if="showBigComponent" />
      </div>
    </template>
    
    <script setup>
    import { ref } from 'vue'
    const showBigComponent = ref(false)
    // 动态导入,懒加载组件
    const BigComponent = defineAsyncComponent(() => import('@/components/BigComponent.vue'))
    </script>

使用 keep-alive 缓存组件状态

<keep-alive> 可以缓存不活动的组件实例,避免重复创建和销毁,大幅提升组件切换的性能,同时保留组件的状态(比如表单输入内容、滚动位置)。

优化场景

  • tab 切换、页面跳转时,保留页面状态,无需重新请求接口、重新渲染
  • 列表页跳转到详情页,返回列表页时,保留之前的筛选条件、滚动位置、分页状态

使用示例

html 复制代码
<!-- 缓存所有路由页面 -->
<keep-alive>
  <router-view></router-view>
</keep-alive>

<!-- 只缓存指定名称的组件 -->
<keep-alive include="Home,List">
  <router-view></router-view>
</keep-alive>

<!-- 排除指定名称的组件 -->
<keep-alive exclude="Detail,Edit">
  <router-view></router-view>
</keep-alive>

打包体积优化

  1. 按需引入第三方依赖避免全量引入第三方库,只引入需要的模块,减小打包体积。

    • UI 组件库:Element Plus、Ant Design Vue 等都支持按需引入,自动导入使用的组件,不打包未使用的组件。

    • 工具库:比如 Lodash,避免全量引入,只引入需要的函数:

      javascript 复制代码
      // 错误写法:全量引入,打包整个Lodash
      import _ from 'lodash'
      
      // 正确写法:按需引入,只打包用到的函数
      import { debounce, cloneDeep } from 'lodash-es'
  2. Tree-Shaking 优化利用构建工具的 Tree-Shaking 能力,剔除未使用的代码(死代码)。

    • 优先使用 ES 模块规范(import/export),避免使用 CommonJS(require)
    • 避免副作用代码,确保纯函数
    • 生产环境开启压缩,剔除 console、debugger 等调试代码
    javascript 复制代码
    // vite.config.js 生产环境剔除console
    build: {
      minify: 'terser',
      terserOptions: {
        compress: {
          drop_console: true,
          drop_debugger: true
        }
      }
    }
  3. 静态资源优化

    • 图片优化:小图片转为 base64 内联,大图片使用 webp/avif 等高效格式,图片压缩后再引入项目
    • 字体优化:字体文件子集化,只保留项目用到的字符,减小字体包体积
    • 避免在项目中存放大体积静态资源,使用 CDN 托管
  4. CDN 加速将不常更新的第三方依赖(Vue、Vue Router、Element Plus 等)通过 CDN 引入,不打包到项目中,减小打包体积,同时利用 CDN 的边缘节点加速加载。

性能分析工具

  • Vue Devtools:Vue 官方调试工具,可查看组件树、组件渲染时间、性能瓶颈、状态变化,是 Vue 性能优化的首选工具。
  • Chrome DevTools Performance:分析页面运行时性能,定位卡顿、长任务、重排重绘的瓶颈。
  • Chrome DevTools Lighthouse:全面检测页面的性能、可访问性、SEO,给出优化评分和具体建议。
  • rollup-plugin-visualizer / webpack-bundle-analyzer:包体积分析工具,可视化展示打包产物的构成,定位大体积依赖,针对性优化。

3.4 测试

前端测试是保障项目质量、减少线上 bug、便于重构的核心手段,Vue 项目测试分为单元测试端到端(E2E)测试两大类。

测试的重要性与策略

  • 提升代码质量,提前发现潜在 bug,减少线上问题
  • 便于代码重构,有测试覆盖的代码,重构时可快速验证是否影响原有功能
  • 保障核心业务流程的稳定性,避免迭代中出现核心功能故障
  • 提升团队协作效率,测试用例是最好的功能文档

测试策略

  • 单元测试:覆盖核心业务逻辑、工具函数、通用组件,保障代码单元的正确性
  • E2E 测试:覆盖核心业务流程(登录、下单、支付等),模拟用户真实操作,保障全链路的正确性
  • 避免过度测试:无需追求 100% 测试覆盖率,优先保障核心功能、高频使用场景、复杂逻辑的测试覆盖

单元测试

单元测试是对软件中最小可测试单元的验证,Vue 项目中,单元测试的核心对象是:工具函数、组合式函数、通用组件、Vuex/Pinia 的 store。

推荐技术栈

  • Vitest:Vue 3 首选测试框架,和 Vite 生态完美兼容,API 兼容 Jest,执行速度极快,对 Vue 组件支持友好
  • Vue Test Utils:Vue 官方的组件测试工具库,提供了组件挂载、交互、属性访问的 API,是 Vue 组件测试的标准工具

基础示例

  1. 安装依赖

    复制代码
    npm install vitest @vue/test-utils happy-dom --save-dev
  2. 配置 Vitest

    javascript 复制代码
    // vite.config.js
    import { defineConfig } from 'vite'
    import vue from '@vitejs/plugin-vue'
    
    export default defineConfig({
      plugins: [vue()],
      test: {
        environment: 'happy-dom', // 模拟浏览器DOM环境
        globals: true, // 全局导入describe、it、expect等API
      }
    })
  3. 组件测试示例待测试组件 MyButton.vue

    javascript 复制代码
    <template>
      <button @click="handleClick" :disabled="disabled">
        {{ text }}
      </button>
    </template>
    
    <script setup>
    const props = defineProps({
      text: {
        type: String,
        default: '按钮'
      },
      disabled: {
        type: Boolean,
        default: false
      }
    })
    const emit = defineEmits(['click'])
    const handleClick = () => {
      emit('click')
    }
    </script>

    测试用例 MyButton.test.js

    javascript 复制代码
    import { describe, it, expect } from 'vitest'
    import { mount } from '@vue/test-utils'
    import MyButton from './MyButton.vue'
    
    // 测试套件
    describe('MyButton组件', () => {
      // 测试用例1:渲染正确的文本
      it('渲染正确的文本', () => {
        const wrapper = mount(MyButton, {
          props: { text: '测试按钮' }
        })
        // 断言:按钮文本正确
        expect(wrapper.text()).toBe('测试按钮')
      })
    
      // 测试用例2:disabled状态生效
      it('disabled状态禁用按钮', () => {
        const wrapper = mount(MyButton, {
          props: { disabled: true }
        })
        // 断言:按钮被禁用
        expect(wrapper.attributes('disabled')).toBeDefined()
      })
    
      // 测试用例3:点击触发click事件
      it('点击按钮触发click事件', () => {
        const wrapper = mount(MyButton)
        // 触发点击事件
        wrapper.trigger('click')
        // 断言:事件被触发
        expect(wrapper.emitted()).toHaveProperty('click')
      })
    })
  4. 执行测试在 package.json 中添加脚本:

    javascript 复制代码
    {
      "scripts": {
        "test": "vitest",
        "test:coverage": "vitest --coverage"
      }
    }

    执行 npm run test 运行测试用例,npm run test:coverage 生成测试覆盖率报告。

端到端 (E2E) 测试

端到端测试是模拟用户真实操作,从用户视角测试整个应用的完整流程,验证应用的功能是否符合预期,不关注内部实现,只关注最终的页面表现。

推荐技术栈

  • Cypress:开箱即用,API 简洁,调试友好,支持实时预览,是前端 E2E 测试的首选
  • Playwright:微软开发的 E2E 测试框架,支持多浏览器、多平台,性能优异,适合复杂场景

核心测试场景

  • 用户登录、注册流程
  • 商品列表、详情、下单、支付的完整购物流程
  • 表单提交、校验、数据回显
  • 页面跳转、权限控制、异常场景处理

3.5 高级特性

Teleport(传送门)

Teleport 是 Vue 3 新增的内置组件,用于将组件的模板内容渲染到 DOM 树的指定位置,不受父组件 DOM 结构的限制,解决了弹窗、tooltip、下拉菜单等组件的层级(z-index)、overflow:hidden 导致的遮挡问题。

基础用法

html 复制代码
<template>
  <div class="container">
    <button @click="showModal = true">打开弹窗</button>
    <!-- 弹窗内容,通过to属性指定渲染到body标签下 -->
    <teleport to="body">
      <div v-if="showModal" class="modal">
        <p>这是弹窗内容,渲染到body下,不受父组件样式影响</p>
        <button @click="showModal = false">关闭弹窗</button>
      </div>
    </teleport>
  </div>
</template>

<script setup>
import { ref } from 'vue'
const showModal = ref(false)
</script>

<style scoped>
.container {
  overflow: hidden;
  position: relative;
  z-index: 1;
}
.modal {
  position: fixed;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  z-index: 9999;
  background: #fff;
  padding: 20px;
}
</style>

核心说明:即使父组件有 overflow: hidden 和低 z-index,弹窗也会渲染到 body 下,不会被遮挡,层级正常生效。

Fragments(片段)

Fragments 是 Vue 3 新增的特性,支持组件模板有多个根节点,解决了 Vue 2 中组件必须有一个根 div 的限制,减少了不必要的 DOM 嵌套,优化了 DOM 结构。

Vue 2 错误写法(会报错)

html 复制代码
<template>
  <header>标题</header>
  <main>内容</main>
  <footer>底部</footer>
</template>

Vue 3 正确写法(多根节点)

html 复制代码
<template>
  <header>标题</header>
  <main>内容</main>
  <footer>底部</footer>
</template>

注意:多根节点组件使用透传属性时,需要手动指定属性绑定的节点,否则 Vue 会发出警告。

自定义指令

自定义指令用于对普通 DOM 元素进行底层操作,封装可复用的 DOM 交互逻辑,是 Vue 组件逻辑复用的补充。

自定义指令分为全局注册局部注册,包含一组生命周期钩子函数,对应元素的不同生命周期阶段。

核心钩子函数

  • created:元素属性或事件监听器应用前调用
  • beforeMount:元素被挂载到 DOM 前调用
  • mounted:元素被挂载到 DOM 后调用(最常用)
  • beforeUpdate:元素更新前调用
  • updated:元素更新后调用
  • beforeUnmount:元素卸载前调用
  • unmounted:元素卸载后调用

示例 1:自动聚焦指令 v-focus

javascript 复制代码
// 全局注册
// main.js
app.directive('focus', {
  // 元素挂载后自动聚焦
  mounted(el) {
    el.focus()
  }
})
html 复制代码
<!-- 组件中使用 -->
<input v-focus type="text" placeholder="自动聚焦输入框">

示例 2:图片懒加载指令 v-lazy

javascript 复制代码
app.directive('lazy', {
  mounted(el, binding) {
    // 图片默认占位图
    el.src = '/images/loading.gif'
    // 创建交叉观察器,监听元素是否进入视口
    const observer = new IntersectionObserver((entries) => {
      if (entries[0].isIntersecting) {
        // 进入视口,加载真实图片
        el.src = binding.value
        // 加载完成后停止观察
        observer.unobserve(el)
      }
    })
    // 观察元素
    observer.observe(el)
  }
})
javascript 复制代码
<!-- 组件中使用 -->
<img v-lazy="https://example.com/real-image.jpg" alt="懒加载图片">

渲染函数 & JSX

Vue 的模板最终会被编译器编译为渲染函数,渲染函数是 Vue 组件渲染的底层实现,用 JavaScript 完全描述 DOM 结构,比模板更灵活,适合复杂动态渲染的场景。

Vue 3 完美支持 JSX 语法,JSX 是 JavaScript 的语法扩展,允许在 JS 中写 HTML 标签,写渲染函数比原生 h () 函数更简洁直观。

渲染函数基础示例

javascript 复制代码
<script>
import { h } from 'vue'
export default {
  props: {
    level: {
      type: Number,
      default: 1
    },
    title: {
      type: String,
      default: ''
    }
  },
  // 渲染函数
  render() {
    // h函数创建虚拟DOM,三个参数:标签名、属性、子节点
    return h(`h${this.level}`, { class: 'title' }, this.title)
  }
}
</script>

JSX 示例

javascript 复制代码
<script setup>
const props = defineProps({
  level: {
    type: Number,
    default: 1
  },
  title: {
    type: String,
    default: ''
  }
})

// JSX直接返回
const renderTitle = () => {
  const Tag = `h${props.level}`
  return <Tag class="title">{props.title}</Tag>
}
</script>

<template>
  <renderTitle />
</template>

适用场景:动态渲染层级不确定的组件、复杂的条件渲染、高阶组件封装、需要完全控制渲染逻辑的场景。

插件开发

插件是为 Vue 添加全局功能的扩展方式,用于封装全局组件、全局指令、原型方法、全局资源、自定义功能等,是 Vue 生态的核心组成部分。

Vue 3 插件是一个包含 install 方法的对象,或者直接是一个函数,接收两个参数:app 应用实例、options 插件配置选项。

插件开发示例

javascript 复制代码
// plugins/my-plugin.js
// 定义插件
const MyPlugin = {
  // 必须的install方法
  install(app, options) {
    // 1. 全局注册组件
    app.component('MyButton', () => import('@/components/MyButton.vue'))
    
    // 2. 全局注册指令
    app.directive('focus', {
      mounted(el) {
        el.focus()
      }
    })
    
    // 3. 全局注入属性,所有组件可通过inject访问
    app.provide('globalConfig', options)
    
    // 4. 挂载全局方法,所有组件可通过app.config.globalProperties访问
    app.config.globalProperties.$message = (text) => {
      alert(text)
    }
  }
}

export default MyPlugin

插件使用

javascript

运行

复制代码
// main.js
import { createApp } from 'vue'
import App from './App.vue'
import MyPlugin from './plugins/my-plugin'

const app = createApp(App)
// 安装插件,传递配置选项
app.use(MyPlugin, {
  baseUrl: '/api',
  timeout: 10000
})
app.mount('#app')

持续学习与社区

Vue 技术生态在持续迭代,想要精通 Vue,需要建立持续学习的习惯,紧跟官方更新,融入社区,不断提升技术深度。

官方文档

官方文档是最权威、最准确的学习资料,是学习 Vue 的首选:

核心 RFCs

Vue 的所有重大变更、新特性、设计决策,都通过 RFC(Request For Comments)流程公开讨论,所有 RFC 都托管在 GitHub 仓库:https://github.com/vuejs/rfcs

阅读 RFC 可以:

  • 了解 Vue 新特性的设计背景、解决的问题、使用方式
  • 理解 Vue 团队的设计理念和技术演进方向
  • 参与社区讨论,提出自己的建议和意见
  • 提前了解 Vue 未来的版本更新,做好技术储备

优质博客与社区

  • GitHub :Vue 官方仓库 https://github.com/vuejs/core,关注 issue、PR、release,了解最新更新,参与开源贡献
  • 掘金:国内优质的前端技术社区,有大量 Vue 相关的优质教程、实战经验、源码解析文章
  • InfoQ:关注前端技术前沿,有 Vue 相关的深度技术文章、行业实践分享
  • 知乎:Vue 相关的话题、专栏,有很多技术大佬的深度分享
  • Dev.to / Medium:国外优质技术社区,有很多 Vue 的国际前沿技术分享
  • Vue 官方论坛https://forum.vuejs.org/,官方维护的论坛,可提问、交流、分享

关注 Vue 3 及未来版本的新特性

Vue 团队的迭代节奏稳定,会持续发布新特性、性能优化、bug 修复,关注新特性可以:

  • 及时使用新 API,优化代码,提升开发效率
  • 了解 Vue 的技术演进方向,提前做好技术规划
  • 避免使用废弃 API,减少技术债务

关注渠道:

  • Vue 官方博客:https://blog.vuejs.org/
  • Vue 官方 Twitter/X:@vuejs
  • Vue 官方 GitHub Release 页面
  • 尤雨溪(Vue 作者)的社交媒体账号

附录

常用工具库

  1. Axios:基于 Promise 的 HTTP 请求库,是 Vue 项目接口请求的首选方案,支持请求 / 响应拦截器、取消请求、JSON 自动转换、客户端 XSRF 防护等能力。
  2. Lodash :JavaScript 实用工具库,提供了大量数组、对象、函数、字符串的处理方法,比如防抖节流、深拷贝、类型判断、集合处理等,是前端开发的瑞士军刀,推荐使用支持 ES 模块的 lodash-es 版本,支持按需引入。
  3. Day.js:轻量级日期处理库,API 和 Moment.js 兼容,体积仅 2KB,是 Moment.js 的完美替代品,支持日期格式化、解析、计算、时区等能力。
  4. Vue I18n:Vue 官方的国际化解决方案,支持多语言切换、复数、日期时间格式化、组件内本地化等能力,完美支持 Vue 3 和 Nuxt。
  5. VueUse:基于 Vue 组合式 API 的实用工具函数库,提供了 200 + 常用的组合式函数,比如 useMouse、useLocalStorage、useFetch、useDebounce 等,开箱即用,是 Vue 项目的必备工具库。

UI 组件库

PC 端组件库
  1. Element Plus:饿了么团队开发的基于 Vue 3 的 PC 端组件库,是国内使用率最高的 Vue PC 组件库,提供了 60 + 常用组件,文档友好,生态完善,适合中后台管理系统开发。
  2. Ant Design Vue:蚂蚁集团开发的企业级 UI 组件库,基于 Ant Design 设计规范,功能丰富,严谨专业,提供了 80 + 组件,适合大型企业级中后台系统开发,完美支持 Vue 3 和 TypeScript。
  3. Vuetify:基于 Material Design 3 的 Vue 组件库,样式美观,功能丰富,内置 100 + 组件,支持响应式布局、主题定制,无需 CSS 基础即可快速开发美观的页面,完美支持 Vue 3。
  4. Quasar Framework:高性能的 Vue 跨端框架,一套代码可发布到 PC、H5、移动端 App、小程序、桌面端等多个平台,内置 200 + 组件,性能优异,功能全面,适合多端项目开发。
移动端组件库
  1. Vant:有赞团队开发的轻量级移动端 Vue 组件库,国内移动端首选,内置 60 + 常用组件,性能优异,文档完善,完美支持 Vue 3 和 SSR。
  2. NutUI:京东团队开发的移动端 Vue 组件库,基于京东 APP 设计规范,内置 70 + 组件,支持 Vue 3、多端适配、主题定制。
  3. Varlet:基于 Material Design 3 的 Vue 3 移动端组件库,完全使用 TypeScript 开发,性能优异,支持按需引入、主题定制、SSR。

调试工具

Vue Devtools:Vue 官方开发的浏览器调试插件,是 Vue 开发的必备工具,支持 Vue 2 和 Vue 3,核心能力:

  • 组件树查看:查看应用的组件层级结构,组件的 props、data、computed、事件等状态
  • 状态管理调试:查看 Pinia/Vuex 的状态变化、mutation/action 触发记录,支持时间旅行、状态回滚
  • 路由调试:查看路由配置、导航记录、路由参数、元信息
  • 性能分析:分析组件的渲染时间、更新次数,定位性能瓶颈
  • 事件追踪:追踪组件的事件触发、自定义事件传递
  • 源码定位:快速定位组件对应的源码文件,方便调试

支持 Chrome、Edge、Firefox 等主流浏览器,可直接在浏览器应用商店搜索安装。

相关推荐
恋猫de小郭12 小时前
Swift 6.3 正式发布支持 Android ,它能在跨平台发挥什么优势?
android·前端·flutter
i-阿松!16 小时前
PCB板子+ flutter前端 + go后端
物联网·flutter·pcb工艺·go1.19
恋猫de小郭16 小时前
Flutter 3.41.6 版本很重要,你大概率需要更新一下
android·前端·flutter
亚历克斯神1 天前
Flutter for OpenHarmony: Flutter 三方库 mutex 为鸿蒙异步任务提供可靠的临界资源互斥锁(并发安全基石)
android·数据库·安全·flutter·华为·harmonyos
钛态1 天前
Flutter 三方库 smartstruct 鸿蒙化字段映射适配指南:介入静态预编译引擎扫除视图及数据模型双向强转类型错乱隐患,筑稳如磐石的企业级模型治理防线-适配鸿蒙 HarmonyOS ohos
flutter·华为·harmonyos
键盘鼓手苏苏1 天前
Flutter 组件 csv2json 适配鸿蒙 HarmonyOS 实战:高性能异构数据转换,构建 CSV 流式解析与全栈式数据映射架构
flutter·harmonyos·鸿蒙·openharmony
左手厨刀右手茼蒿1 天前
Flutter 组件 http_requests 适配鸿蒙 HarmonyOS 实战:极简网络请求,构建边缘端轻量级 RESTful 通讯架构
网络·flutter·http
雷帝木木1 天前
Flutter 三方库 hrk_logging 的鸿蒙化适配指南 - 实现标准化分层日志记录、支持多目的地输出与日志分级过滤
flutter·harmonyos·鸿蒙·openharmony·hrk_logging
左手厨刀右手茼蒿1 天前
Flutter 三方库 dio_compatibility_layer 的鸿蒙化适配指南 - 实现 Dio 跨主版本的平滑迁移、支持遗留拦截器兼容与网络请求架构稳定升级
flutter·harmonyos·鸿蒙·openharmony·dio_compatibility_layer