03-vue2

Vue2

文章目录

一:Vue2概述

1:hello world

vue是一个用于构建用户界面的渐进式框架

  • 构建用户界面:基于数据动态渲染页面
  • 渐进式:循序渐进的学习
  • 框架:一套完整的项目解决方案,提升开发效率

创建Vue实例的4步:

  1. 准备一个容器
  2. 引入vue的包
  3. 创建vue的实例 - new Vue()
  4. 指定配置项 - el指定挂载点和data提供数据
html 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <!-- 步骤1:准备容器,到时候vue配置项的数据会在这里渲染 -->
    <div id="app">
        {{ msg }}  
    </div>

    <!-- 步骤2:引入vue包 -->
    <script src="https://cdn.jsdelivr.net/npm/vue@2.7.10/dist/vue.js"></script>

    <script>
        // 步骤3:创建Vue实例
        const app = new Vue({
            el: '#app', // 指定挂载点是id=app的容器, 也就是指定vue管理的是那个盒子
            data: {
                msg: "你好,世界" // 提供对应的数据,将会渲染到上面的挂载点
            }
        })
    </script>
</body>
</html>

上面使用的{``{}}就是插值表达式,它是vue的一种模板语法,利用表达式进行插值,将数据渲染到页面中

注意插值表达式支持的只有表达式,不能用在if, for和标签属性

可以在浏览器中下载vue.js devtools这个插件,然后进入详情,打开允许访问文件地址开关,方便后面学习调试。

2:数据响应式

Vue的核心特性之一是响应式:数据变化,视图自动的更新

因为data中的数据是会被添加到实例上的,所以:

  • 对于属性的访问:实例.属性名
  • 对于属性的修改:实例.属性名 = 新值
html 复制代码
<body>
    <!-- 步骤1:准备容器,到时候vue配置项的数据会在这里渲染 -->
    <div id="app">
        {{ msg }}  
    </div>

    <!-- 步骤2:引入vue包 -->
    <script src="https://cdn.jsdelivr.net/npm/vue@2.7.10/dist/vue.js"></script>

    <script>
        // 步骤3:创建Vue实例
        const app = new Vue({
            el: '#app', // 指定挂载点是id=app的容器
            data: {
                msg: "你好,世界" // 提供对应的数据,将会渲染到上面的挂载点
            }
        })
        console.log(app.msg); // 注意不是app.data.msg, 是app.msg
        // 定时, 2秒后修改属性,可以发现视图自动变换
        setTimeout(() => {
            app.msg = "你好, vue";
        }, 2000);
    </script>
</body>

一个完整的vue2实例对象结构

js 复制代码
const vm = new Vue({
  el: '#app',
  data() {
      // 数据相关
    }
  },
  computed: {
   //计算属性
  },
  watch: {
    message(newVal, oldVal) {
      // 检测属性
    }
  },
  methods: {
    // 方法
  },
  created() {
    // 生命周期方法
  }
})
js 复制代码
const vm = new Vue({
  el: '#app',           // 挂载目标
  
  // ===================== 1. data() =====================
  data() {
    return {
      message: 'Hello',      // 响应式数据
      count: 0,              // 状态数据
      list: [],              // 数组数据
      user: {}               // 对象数据
    }
  },
  // 作用:定义组件的响应式数据
  // 特点:必须是函数(避免组件复用时的数据共享问题)
  // 数据变化会自动触发视图更新
  
  // ===================== 2. computed =====================
  computed: {
    // 计算属性 - 基于现有数据计算新值
    reversedMessage() {
      // 自动缓存,依赖的 data 变化时才重新计算
      return this.message.split('').reverse().join('')
    },
    // 带 getter/setter 的计算属性
    fullName: {
      get() { return this.firstName + ' ' + this.lastName },
      set(val) {
        const names = val.split(' ')
        this.firstName = names[0]
        this.lastName = names[1]
      }
    }
  },
  // 作用:声明式地定义派生数据
  // 特点:具有缓存、依赖追踪、可读写
  
  // ===================== 3. watch =====================
  watch: {
    // 监听数据变化,执行异步或复杂操作
    message(newVal, oldVal) {
      console.log('消息从', oldVal, '变为', newVal)
      // 适合:数据变化时执行异步请求、复杂逻辑
    },
    // 深度监听对象
    user: {
      handler(newVal) {
        console.log('用户信息变化')
      },
      deep: true,        // 深度监听对象内部变化
      immediate: true    // 立即执行一次
    },
    // 监听计算属性
    'computedProperty'(newVal) {
      console.log('计算属性变化')
    }
  },
  // 作用:响应数据变化,执行副作用
  // 特点:更灵活,适合异步操作
  
  // ===================== 4. methods =====================
  methods: {
    // 事件处理方法
    increment() {
      this.count++  // 可修改数据
    },
    // 复杂逻辑处理
    fetchData() {
      // 可以调用其他方法
      this.processData(this.data)
    },
    // 带参数的方法
    updateMessage(newMsg) {
      this.message = newMsg
    }
  },
  // 作用:定义组件方法
  // 特点:在模板中可直接调用,可修改数据,无缓存
  
  // ===================== 5. 生命周期钩子(如 created) =====================
  created() {
    // 实例创建完成后调用
    // 特点:可以访问 this,但 DOM 还未生成
    console.log('组件已创建')
    this.fetchData()     // 初始化数据
    this.setupEvents()   // 设置事件监听
  },
  
  // 其他常用生命周期钩子:
  mounted() {
    // DOM 挂载后调用
    // 适合:操作 DOM、初始化第三方库
  },
  updated() {
    // 数据变化导致 DOM 更新后调用
    // 注意:避免在此修改数据,可能导致无限循环
  },
  destroyed() {
    // 实例销毁前调用
    // 适合:清理定时器、取消事件监听
  }
})

3:vue基础指令

vue会根据不同的指令,针对标签实现不同的功能,所谓的指令,就是带有v-前缀的特殊标签属性

熟练几个常用的,剩下边用边搜,要学会使用官方文档

vue2指定官网

指令 说明
v-text 等价于插值表达式{``{}},更新元素的textContent
v-html 更新元素的 innerHTML,内容按普通 HTML 插入 - 不会作为 Vue 模板进行编译
v-show 根据表达式之真假值,切换元素的 display CSS property。
v-if 根据表达式的值的来有条件地渲染元素。在切换时元素及它的数据绑定 / 组件被销毁并重建
v-else 前一兄弟元素必须有 v-ifv-else-if
v-else-if 表示 v-ifelse if 块。可以链式调用。
v-for 基于源数据多次渲染元素或模板块。 此指令之值,必须使用特定语法 alias in expression,为当前遍历的元素提供别名
v-on 缩写是@, 绑定事件监听器。事件类型由参数指定
v-bind 缩写是:, 动态地绑定一个或多个 attribute,或一个组件 prop 到表达式
v-model 随表单控件类型不同而不同

下面是其他需要重点注意的事项

3.1:v-if/v-show
  • v-if:是"真正的"条件渲染,元素会在条件为真时被创建,条件为假时从 DOM 中完全移除
  • v-show:只是切换 CSS 的 display: none 属性,无论条件真假,元素始终存在于 DOM 中。

因此v-if的开销比较高,适用于不经常切换的场景(如一次性条件判断);而v-show:适合频繁切换的场景(如标签页切换、折叠面板)

v-if:可以在 <template> 标签上使用 / v-show:不能在 <template> 标签上使用

3.2:v-for

必须绑定:key

使用 v-for 时必须为每个项提供一个唯一的 key 属性,这有助于 Vue 高效地更新虚拟 DOM。

key必须是字符串或者数字

html 复制代码
<!-- 正确:使用唯一 key -->
<li v-for="item in items" :key="item.id">
  {{ item.name }}
</li>

<!-- 避免:使用索引作为 key(除非列表简单且不会重新排序) -->
<li v-for="(item, index) in items" :key="index">
  {{ item.name }}
</li>

在同一元素上同时使用 v-forv-if 会导致性能问题,因为 v-for 的优先级更高。

html 复制代码
<!-- 不推荐:v-for 和 v-if 在同一元素 -->
<li v-for="user in users" v-if="user.isActive" :key="user.id">
  {{ user.name }}
</li>

<!-- 推荐方案1:使用计算属性过滤 -->
<li v-for="user in activeUsers" :key="user.id">
  {{ user.name }}
</li>

<!-- 推荐方案2:使用 template 包裹 -->
<template v-for="user in users" :key="user.id">
  <li v-if="user.isActive">
    {{ user.name }}
  </li>
</template>

vue可以见到如下数组方法的变化

push, pop, shift, unshift, splice, sort, reverse, 这些方法vue都可以实时看到并响应

js 复制代码
// Vue 能检测到这些变化
this.items.push(newItem)
this.items.splice(index, 1)

// Vue 不能检测到这些变化
this.items[index] = newItem // ✗ 不会触发更新
this.items.length = 0       // ✗ 不会触发更新
3.3:v-on
html 复制代码
<!-- 完整写法 -->
<button v-on:click="handleClick">点击我</button>

<!-- 简写形式(推荐) -->
<button @click="handleClick">点击我</button>

<!-- 内联表达式 -->
<button @click="count++">增加 {{ count }}</button>

可以传递原生事件对象或者直接调用方法

html 复制代码
<!-- 直接调用方法 -->
<button @click="sayHello('小明')">打招呼</button>

<!-- 传递原生事件对象 -->
<button @click="sayHello('小明', $event)">打招呼</button>

<!-- 多个操作 -->
<button @click="count++; logClick($event)">点击</button>

vue还提供了事件修饰符来处理DOM事件的细节

事件修饰符 作用
.prevent 阻止默认行为
.stop 阻止事件冒泡
.once 事件只触发一次
.enter / .esc / .ctrl.enter /... 按键类事件
.left / .right / .middle 鼠标类事件
html 复制代码
<!-- 阻止默认行为 -->
<form @submit.prevent="onSubmit">
  <button type="submit">提交</button>
</form>

<!-- 阻止事件冒泡 -->
<div @click="outerClick">
  <button @click.stop="innerClick">点击我</button>
</div>

<!-- 事件只触发一次 -->
<button @click.once="doSomething">只执行一次</button>

<!-- 串联修饰符 -->
<form @submit.prevent.stop="onSubmit"></form>

<!-- 回车键 -->
<input @keyup.enter="submit">

<!-- 常用按键别名 -->
<input @keyup.enter="submit">     <!-- Enter -->
<input @keyup.esc="clear">        <!-- Esc -->
<input @keyup.tab="nextField">    <!-- Tab -->
<input @keyup.delete="deleteItem"><!-- Delete -->
<input @keyup.space="play">       <!-- Space -->

<!-- 系统修饰键 -->
<input @keyup.ctrl.enter="submit">    <!-- Ctrl + Enter -->
<input @keyup.alt.enter="submit">     <!-- Alt + Enter -->
<input @keyup.shift.enter="submit">   <!-- Shift + Enter -->
<input @keyup.meta.enter="submit">    <!-- Command/Win + Enter -->

<!-- 精确修饰符(只有该键被按下时触发) -->
<button @click.ctrl.exact="ctrlClick">Ctrl + 点击</button>
<button @click.exact="justClick">仅点击,无修饰键</button>

<!-- 鼠标按钮修饰符 -->
<button @click.left="leftClick">左键</button>
<button @click.right="rightClick">右键</button>
<button @click.middle="middleClick">中键</button>
3.4:v-bind
html 复制代码
<!-- 完整写法 -->
<a v-bind:href="url">链接</a>
<img v-bind:src="imageSrc" alt="图片">

<!-- 简写形式(推荐) -->
<a :href="url">链接</a>
<img :src="imageSrc" alt="图片">

<!-- 动态属性名 -->
<button :[attributeName]="value">按钮</button>
html 复制代码
<body>
    <div id="root">
        <button v-show="index > 0" @click="index--">上一张图片</button>
        <div>
            <img :src="image_list[index]">
        </div>
        <button v-show="index < image_list.length - 1" @click="index++">下一张图片</button>
    </div>
    <script>
        Vue.config.productionTip = false //阻止 vue 在启动时生成生产提示。
        new Vue({
            el:'#root',
            data:{
                index: 0, // 初始的index = 0
                image_list: [
                    "./image/01.png",
                    "./image/02.png",
                    "./image/03.png",
                    "./image/04.png",
                    "./image/05.png",
                ]
            }
        })
    </script>
</body>

为了方便开发者进行样式控制,Vue 扩展了 v-bind 的语法

可以针对 class 类名 和 style 行内样式 进行控制,语法是::class="对象或者数组"

html 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>:class测试</title>
    <!-- 引入vue.js -->
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <style>
        .box {
            width: 500px;
            height: 500px;
            border: 3px solid red;
            font-size: 30px;
            margin-top: 10px; /* 上侧外边距 - 10px */
        }
        .pink {
            color: pink;
        }
        .bigFont {
            font-size: 50px;
        }

    </style>
</head>
<body>
    <!-- 列表渲染,删除任务功能,添加任务功能,清空任务,底部统计 -->
    <div id="app">
<!--        <div class = "box" :class="{pink: true, bigFont: false}">-->
<!--            测试一下.class-->
<!--        </div>-->
        <div class="box" :class="['pink', 'bigFont']">
            测试一下.class
        </div>
    </div>
    
    <script>
        new Vue({
            el: '#app',
        })
    </script>
</body>
</html>
3.5:v-model

给表单元素使用,双向数据绑定

可以快速获取或设置表单元素内容:数据变化 <-> 视图变化 互相影响

html 复制代码
<!-- 文本输入框 -->
<input v-model="message" placeholder="编辑我">
<p>输入的内容是: {{ message }}</p>

<!-- 多行文本 -->
<textarea v-model="message"></textarea>

<!-- 复选框 -->
<input type="checkbox" v-model="checked">
<label>复选框: {{ checked }}</label>

<!-- 多个复选框 -->
<input type="checkbox" value="篮球" v-model="hobbies">
<input type="checkbox" value="足球" v-model="hobbies">
<input type="checkbox" value="排球" v-model="hobbies">
<p>选择的爱好: {{ hobbies }}</p>

<!-- 单选框 -->
<input type="radio" value="男" v-model="gender">
<input type="radio" value="女" v-model="gender">
<p>选择的性别: {{ gender }}</p>

<!-- 下拉选择框 -->
<select v-model="selected">
  <option disabled value="">请选择</option>
  <option value="A">选项A</option>
  <option value="B">选项B</option>
  <option value="C">选项C</option>
</select>
<p>选择的选项: {{ selected }}</p>

<!-- 多选下拉框 -->
<select v-model="selectedItems" multiple>
  <option value="A">选项A</option>
  <option value="B">选项B</option>
  <option value="C">选项C</option>
</select>
<p>选择的项目: {{ selectedItems }}</p>
js 复制代码
data() {
  return {
    message: '',        // 文本
    checked: false,     // 复选框
    hobbies: [],        // 多个复选框
    gender: '',         // 单选框
    selected: '',       // 下拉框
    selectedItems: []   // 多选下拉框
  }
}

v-model的本质是语法糖,它结合了下面两个vue指令

  • v-bind:将数据绑定到元素的 value 属性(数据 -> 视图)
  • v-on:监听输入事件来更新数据(视图 -> 数据)

对应v-bind属性和v-on事件列表如下:

元素类型 绑定的属性 监听的事件 值类型
<input type="text"> value input 字符串
<textarea> value input 字符串
<input type="checkbox"> checked change 布尔值
多个<input type="checkbox"> value change 数组
<input type="radio"> checked change 字符串
<select> value change 字符串
<select multiple> value change 数组
html 复制代码
<!-- v-model 语法糖 -->
<input v-model="message">

<!-- 等价于 -->
<input 
  :value="message" 
  @input="message = $event.target.value"
>

<!-- 对于组件 -->
<custom-input v-model="message">

<!-- 等价于 -->
<custom-input
  :value="message"
  @input="message = $event"
>

v-model常用修饰符

修饰符 说明
.lazy 将input 事件转为 change 事件
.number 自动将输入转为数字
.trim 自动去除首尾空格
html 复制代码
<!-- 默认:每次输入都更新(input事件) -->
<input v-model="message" placeholder="实时更新">

<!-- 使用 .lazy:失去焦点或回车时更新(change事件) -->
<input v-model.lazy="message" placeholder="失焦时更新">

<!-- 默认:获取的是字符串 -->
<input v-model="age" type="number">
<!-- 输入 "25" → this.age === "25"(字符串) -->

<!-- 使用 .number:转为数字类型 -->
<input v-model.number="age" type="number">
<!-- 输入 "25" → this.age === 25(数字) -->

<!-- 去除输入内容的首尾空格 -->
<input v-model.trim="username" placeholder="输入用户名">
<!-- 输入 " 张三 " → this.username === "张三" -->
3.6:小黑记事本

一个小的整合案例

html

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>小黑记事本</title>
    <script src="https://cdn.jsdelivr.net/npm/vue@2.7.10/dist/vue.js"></script>
    <link rel="stylesheet" href="test.css">
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css">
</head>
<body>
    <div id="app">
        <div class="todo-app">
            <!-- 标题 -->
            <div class="header">
                <h1><i class="fas fa-book"></i> 小黑记事本</h1>
            </div>

            <!-- 输入区域 -->
            <div class="input-section">
                <!-- type属性表示表单的类型是输入框 -->
                <!-- v-model表示双向绑定,绑定vue实例中的newTask变量 -->
                <!-- 当键盘输入enter的时候触发vue实例中的addTask方法 -->
                <!-- placeholder表示输入框的默认值 -->
                <!-- class属性用于渲染和定位 -->
                <input type="text" v-model="newTask" @keyup.enter="addTask" placeholder="请输入任务" class="task-input">
                <!-- 点击按钮也是触发vue实例中的addTask方法 -->
                <button @click="addTask" class="add-btn">
                    <i class="fas fa-plus"></i> 添加任务
                </button>
            </div>

            <!-- 任务列表 -->
            <div class="task-list" v-if="tasks.length > 0">
                <!-- v-for循环任务列表tasks, tasks是vue实例data中的列表对象, key是index -->
                <div class="task-item"  v-for="(task, index) in tasks" :key="index">
                    <span class="task-number">{{ index + 1 }}.</span>
                    <span class="task-content">{{ task }}</span>
                    <!-- 点击按钮触发删除任务的操作 -->
                    <button @click="deleteTask(index)" class="delete-btn">
                        <i class="fas fa-trash"></i>
                    </button>
                </div>
            </div>

            <!-- 空状态 -->
            <div class="empty-state" v-else>
                <i class="fas fa-clipboard-list"></i>
                <p>暂无任务,请添加任务</p>
            </div>

            <!-- 底部统计和操作 -->
            <div class="footer">
                <!-- 赋值表达式 -->
                <div class="stats">
                    合计: <span class="count">{{ tasks.length }}</span>
                </div>
                <!-- 点击的时候触发clearAll方法 -->
                <!-- 当列表长度是0的时候disabled -->
                <button @click="clearAll" class="clear-btn" :disabled="tasks.length === 0">
                    <i class="fas fa-trash-alt"></i> 清空任务
                </button>
            </div>
        </div>
    </div>

    <script>
        new Vue({
            el: '#app',
            data() {
                return {
                    newTask: '',
                    tasks: [
                        '跑步锻炼20分钟',
                        '复习数组语法'
                    ]
                }
            },
            methods: {
                // 添加任务
                addTask() {
                    if (this.newTask.trim() === '') {
                        alert('请输入任务内容!');
                        return;
                    }
                    // push方法 -> vue可见并实时响应
                    this.tasks.push(this.newTask.trim());
                    this.newTask = '';
                },
                
                // 删除单个任务
                deleteTask(index) {
                    if (confirm('确定要删除这个任务吗?')) {
                        this.tasks.splice(index, 1);
                    }
                },
                
                // 清空所有任务
                clearAll() {
                    if (this.tasks.length === 0) return;
                    
                    if (confirm('确定要清空所有任务吗?')) {
                        this.tasks = [];
                    }
                }
            },
            // 生命周期
            mounted() {
                // 页面加载后让输入框自动获得焦点
                document.querySelector('.task-input').focus();
            }
        });
    </script>
</body>
</html>

css

css 复制代码
/* 整体样式设计 */
* {
    margin: 0; /* 清除所有元素的默认外边距 */
    padding: 0; /* 清除所有元素的默认内边距 */
    box-sizing: border-box; /* 现代盒子,将内边距和边框计入元素的总宽度和高度中,简化布局计算 */
}

body {
    font-family: 'Arial', 'Microsoft YaHei', sans-serif; /* 设置字体,优先使用 Arial,其次是微软雅黑,最后是系统默认无衬线字体 */
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); /* 设置背景为135度角、从左到右(#667eea 到 #764ba2)的线性渐变 */
    min-height: 100vh; /* 最小高度为视口高度的100%,确保背景铺满全屏 */
    display: flex; /*启用弹性盒子布局 */
    justify-content: center; /* 水平居中对齐子元素*/
    align-items: center; /* 垂直居中对齐子元素 */
    padding: 20px; /* 设置内边距,在屏幕边缘留出一些空间 */
}

.todo-app {
    width: 100%; /* 宽度占满可用空间 */
    max-width: 500px; /* 最大宽度限制为 500px,防止在大屏幕上过宽 */
    background: white; /* 设置背景颜色为白色 */
    border-radius: 15px; /* 设置圆角边框 */
    box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2); /* 添加阴影效果,X偏移0、Y偏移10px、模糊30px、颜色为半透明黑色 */
    overflow: hidden; /* 隐藏超出容器圆角边界的内容 */
}

/* 标题样式 */
.header {
    background: linear-gradient(135deg, #2c3e50 0%, #4a6491 100%); /* 头部背景渐变(深蓝色系)*/
    color: white; /* 文字颜色设为白色 */
    padding: 25px 20px; /* 上下内边距25px,左右内边距20px */
    text-align: center; /* 文字水平居中 */
}

.header h1 {
    font-size: 24px; /* 标题字体大小 */
    font-weight: 600; /* 字体粗细为 semi-bold */
    display: flex; /* 启用弹性盒子布局 */
    align-items: center; /* 垂直居中对齐子元素 */
    justify-content: center; /* 水平居中对齐子元素 */
    gap: 10px; /* 设置子元素之间的间距为 10px */
}

.header h1 i {
    color: #ffd700; /* 图标颜色设为金色 */
}

/* 输入区域 */
.input-section {
    padding: 25px; /* 内边距 */
    border-bottom: 2px solid #f0f0f0; /* 底部边框,浅灰色 */
    display: flex; /* 启用弹性盒子布局 */
    gap: 10px; /* 子元素之间的间距 */
}

.task-input {
    flex: 1; /* 占据剩余的所有可用空间 */
    padding: 14px 18px; /* 内边距:上下14px,左右18px */
    border: 2px solid #e0e0e0; /* 边框:2px宽的浅灰色实线 */
    border-radius: 10px; /* 圆角边框 */
    font-size: 16px; /* 字体大小 */
    transition: all 0.3s; /* 所有 CSS 属性的变化在 0.3 秒内平滑过渡 */
    outline: none; /* 移除默认的聚焦轮廓线 */
}

.task-input:focus {
    border-color: #667eea; /* 聚焦时边框颜色变为主题蓝色 */
    box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1); /* 聚焦时添加淡蓝色发光效果 */
}

.add-btn {
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); /* 按钮背景渐变(主题色)*/
    color: white; /* 文字颜色 */
    border: none; /* 无边框 */
    padding: 0 25px; /* 左右内边距25px,上下为0 */
    border-radius: 10px; /* 圆角 */
    cursor: pointer; /* 鼠标悬停时显示手形指针 */
    font-size: 16px; /* 字体大小 */
    font-weight: 600; /* 字体粗细 */
    display: flex; /* 启用弹性盒子布局 */
    align-items: center; /* 垂直居中对齐子元素(图标和文字) */
    gap: 8px; /* 图标和文字之间的间距 */
    transition: all 0.3s; /* 所有属性变化过渡效果 */
}

.add-btn:hover {
    transform: translateY(-2px); /* 鼠标悬停时按钮向上轻微移动2px */
    box-shadow: 0 5px 15px rgba(102, 126, 234, 0.3); /* 鼠标悬停时添加阴影,增强立体感 */
}

.add-btn:active {
    transform: translateY(0); /* 按钮被点击时,取消向上移动,恢复原位置 */
}

/* 任务列表 */
.task-list {
    padding: 0 25px; /* 左右内边距25px,上下为0 */
}

.task-item {
    display: flex; /* 启用弹性盒子布局 /
    align-items: center; /* 垂直居中对齐子元素 */
    padding: 18px 0; /* 上下内边距18px,左右为0 */
    border-bottom: 1px solid #f0f0f0; /* 底部边框,1px宽的浅灰色实线 */
    animation: fadeIn 0.3s ease-out; /* 应用名为 'fadeIn'、时长0.3秒、缓动函数为 ease-out 的动画 */
}

.task-item:last-child {
    border-bottom: none; /* 移除最后一个任务项底部的边框 */
}

.task-number {
    color: #667eea; /* 数字颜色为主题蓝色 */
    font-weight: bold; /* 字体加粗 */
    min-width: 30px; /* 设置最小宽度,确保数字对齐 */
    font-size: 16px; /* 字体大小 */
}

.task-content {
    flex: 1; /* 占据剩余的所有可用空间 */
    font-size: 16px; /* 字体大小 */
    color: #333; /* 文字颜色为深灰色 */
    word-break: break-word; /* 允许长单词或URL在任意字符间断行,防止内容溢出 */
    padding: 0 15px; /* 左右内边距15px,上下为0 */
}

.delete-btn {
    background: #ff6b6b; /* 背景色为警告红色 */
    color: white; /* 图标颜色为白色 */
    border: none; /* 无边框 */
    width: 35px; /* 固定宽度 */
    height: 35px; /* 固定高度,形成圆形按钮 */
    border-radius: 50%; /* 50%的圆角,形成圆形 */
    cursor: pointer; /* 鼠标悬停时显示手形指针 */
    display: flex; /* 启用弹性盒子布局 */
    align-items: center; /* 垂直居中对齐子元素(图标) */
    justify-content: center; /* 水平居中对齐子元素(图标) */
    transition: all 0.3s; /* 所有属性变化过渡效果 */
}

.delete-btn:hover {
    background: #ff5252; /* 鼠标悬停时背景色变深(更深的红色) */
    transform: scale(1.1); /* 鼠标悬停时按钮放大到1.1倍 */
}

.delete-btn:active {
    transform: scale(0.95); /* 按钮被点击时缩小到0.95倍,模拟按压效果 */
}

/* 空状态(当没有任务时显示的提示) */
.empty-state {
    text-align: center; /* 文字居中 */
    padding: 50px 25px; /* 内边距:上下50px,左右25px */
    color: #999; /* 文字颜色为浅灰色 */
}

.empty-state i {
    font-size: 60px; /* 图标大小 */
    margin-bottom: 15px; /* 图标底部外边距 */
    opacity: 0.5; /* 图标半透明 */
}

.empty-state p {
    font-size: 18px; /* 提示文字大小 */
    color: #666; /* 提示文字颜色为灰色 */
}

/* 底部 */
.footer {
    background: #f8f9fa; /* 背景为浅灰色 */
    padding: 20px 25px; /* 内边距:上下20px,左右25px */
    display: flex; /* 启用弹性盒子布局 */
    justify-content: space-between; /* 子元素在主轴(水平方向)两端对齐,之间平均分配空间 */
    align-items: center; /* 垂直居中对齐子元素 */
    border-top: 2px solid #f0f0f0; /* 顶部边框,2px宽的浅灰色实线 */
}

.stats {
    font-size: 16px; /* 统计文字大小 */
    color: #666; /* 统计文字颜色为灰色 */
}

.count {
    font-weight: bold; /* 任务数量数字加粗 */
    color: #667eea; /* 数字颜色为主题蓝色 */
    font-size: 20px; /* 数字字体稍大 */
    margin-left: 5px; /* 数字左侧外边距 */
}

.clear-btn {
    background: #ff6b6b; /* 背景色为警告红色 */
    color: white; /* 文字颜色为白色 */
    border: none; /* 无边框 */
    padding: 12px 25px; /* 内边距:上下12px,左右25px */
    border-radius: 10px; /* 圆角边框 */
    cursor: pointer; /* 鼠标悬停时显示手形指针 */
    font-size: 16px; /* 字体大小 */
    font-weight: 600; /* 字体粗细 */
    display: flex; /* 启用弹性盒子布局 */
    align-items: center; /* 垂直居中对齐子元素(图标和文字) */
    gap: 8px; /* 图标和文字之间的间距 */
    transition: all 0.3s; /* 所有属性变化过渡效果 */
}

.clear-btn:hover:not(:disabled) {
    background: #ff5252; /* 鼠标悬停且按钮未禁用时,背景色变深 */
    transform: translateY(-2px); /* 鼠标悬停时向上轻微移动 */
    box-shadow: 0 5px 15px rgba(255, 107, 107, 0.3); /* 添加红色调阴影 */
}

.clear-btn:disabled {
    background: #cccccc; /* 按钮禁用时背景为灰色 */
    cursor: not-allowed; /* 按钮禁用时鼠标显示禁用样式 */
    opacity: 0.6; /* 按钮禁用时降低不透明度 */
}

.clear-btn:active:not(:disabled) {
    transform: translateY(0); /* 按钮被点击且未禁用时,取消向上移动,恢复原位置 */
}

/* 动画 */
@keyframes fadeIn {
    from {
        opacity: 0; /* 动画开始时完全透明 */
        transform: translateY(10px); /* 动画开始时向下偏移10px */
    }
    to {
        opacity: 1; /* 动画结束时完全不透明 */
        transform: translateY(0); /* 动画结束时回到原始位置 */
    }
}

/* 响应式设计 - 适配小屏幕设备(如手机) */
@media (max-width: 600px) {
    .todo-app {
        margin: 10px; /* 在小屏幕上,应用容器周围添加10px外边距 */
        border-radius: 12px; /* 稍微减小圆角半径 */
    }

    .header {
        padding: 20px 15px; /* 减少头部内边距 */
    }

    .input-section {
        padding: 20px; /* 减少输入区域内边距 */
        flex-direction: column; /* 将子元素(输入框和按钮)垂直排列 */
    }

    .add-btn {
        width: 100%; /* 按钮宽度占满容器 */
        justify-content: center; /* 按钮内容居中对齐 */
        padding: 15px; /* 调整按钮内边距 */
    }

    .task-item {
        padding: 15px 0; /* 减少任务项的内边距 */
    }

    .footer {
        flex-direction: column; /* 将底部子元素(统计和按钮)垂直排列 */
        gap: 15px; /* 子元素之间添加间距 */
        text-align: center; /* 文字居中对齐 */
    }

    .clear-btn {
        width: 100%; /* 按钮宽度占满容器 */
        justify-content: center; /* 按钮内容居中对齐 */
    }
}

4:computed/watch/生命周期

4.1:computed/watch

一些属性是需要其他属性计算出来的,此时就需要用到computed, 例如合计这个total变量属性,就需要列表变量的length确定。可以自动追踪响应式依赖,只有被使用时才会计算,可以使用getter/setter

而watch属性用于监听响应式数据的变化,能拿到数据变化前后的值。可以监听 data、computed、props 等

特性 Computed Watch
主要用途 计算衍生数据 监听数据变化执行操作
缓存 有缓存,依赖不变不重算 无缓存,每次变化都执行
同步/异步 只能同步 可以处理异步操作
返回值 必须返回值 无返回值
使用场景 模板显示计算值 数据变化时执行逻辑
js 复制代码
computed: {
  // 只读计算属性
  fullName() {
    return this.firstName + ' ' + this.lastName;
  },
  
  // 可读写的计算属性
  computedValue: {
    get() {
      return this.value * 2;
    },
    set(newValue) {
      this.value = newValue / 2;
    }
  }
}

watch: {
  // 监听简单值
  message(newVal, oldVal) {
    console.log('消息变化了:', newVal);
  },
  
  // 深度监听对象
  user: {
    handler(newVal) {
      console.log('用户信息变化了');
    },
    deep: true,      // 深度监听
    immediate: true  // 立即执行一次
  },
  
  // 监听对象特定属性
  'user.name': function(newVal) {
    console.log('用户名变化了:', newVal);
  }
}
4.2:生命周期

created

  • 时机:实例创建完成,数据已观测
  • 用途:数据初始化、API调用
  • 注意:$el 还不存在($el 是 Vue 实例的一个只读属性,表示 Vue 实例挂载的DOM 元素。)

mounted

  • 时机:实例已挂载到DOM
  • 用途:DOM操作、第三方库初始化

beforeDestroy

  • 时机:实例销毁之前
  • 用途:清理定时器、解绑事件

二:工程化

🔴 vue2的工程化已经渐渐淘汰,因为使用的是webpack, 现在都是vue3 + vite

核心包传统开发模式:基于html/css/js文件,直接引入核心包,开发Vue。

工程化开发模式:基于构建工具(例如:webpack)的环境中开发Vue。

1:vue cli脚手架

Vue CLI是Vue官方提供的一个全局命令工具,可以帮助我们快速创建一个开发Vue项目的标准化基础架子

Vue CLI集成了webpack配置,下面以npm为例:

1️⃣ 全局安装(一次):npm i @vue/cli -g,然后查看Vue版本:vue --version

2️⃣ 创建项目架子:先进入到指定文件夹,然后vue create project-name(项目名-不能用中文)

3️⃣ 启动项目:npm run serve(找package.json)

启动之后,就可以看到如下界面

下面是脚手架文件介绍:

2:核心文件和组件

2.1:工程核心文件

上面标绿的3个文件是核心文件,是整个工程的"根",分别是main.js / App.vue / index.html

index.html -> 模板文件

html 复制代码
<body>
    <!-- 给不支持JS的浏览器一个提示 -->
    <noscript>
      <strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
    </noscript>
    <!-- 挂载点,启动后将自动使用这个容器为vue的挂载点 -->
    <!-- Vue管理的容器,将来创建结构动态渲染 -->
    <div id="app">
     	<!-- 工程化中,这里不在直接编写 -->
        <!-- 构建的文件将被自动注入 -->
    </div>
</body>

main.js -> 入口文件,打包或者运行,第一个执行的文件

js 复制代码
// 核心作用就是导入App.vue, 根据App.vue创建结构渲染index.html
// 1:导入vue核心包
import Vue from 'vue'
// 2:导入App.vue
import App from './App.vue'

// 阻止启动生产消息
Vue.config.productionTip = false

// 创建vue实例
new Vue({
  // 提供render方法 -> 基于App.vue创建结构渲染index.html
  render: h => h(App), 
}).$mount('#app') // $mount指定挂载点为index.html中id=app的容器

App.vue -> 根组件,index.html加载这个组件,这个组件中加载其他组件

vue后缀的文件被称为vue组件,组件有三大部分构成,其实就是html + css + JS三合一文件

部分 说明
<template> 在这里写结构
<style> 在这里写样式
<script> 在这里写逻辑行为
html 复制代码
<template>
  <!-- 在template中写html结构,这里可以使用其他的组件 -->
  <div id="app">
    <img alt="Vue logo" src="./assets/logo.png">
    <!-- 步骤3:组件使用,并传递msg参数 -->
    <HelloWorld msg="hello vue2"/>
  </div>
</template>

<script>
// 在script标签中写js, 一般是声明使用的组件
// 然后使用export default引入其他的组件,可以在上面html中使用  
import HelloWorld from './components/HelloWorld.vue' // 步骤1:组件声明
// 步骤2:组件引入
// export default -> 全部导出,这样其他的文件才能用这些信息 
// 可以写components -> 引用了哪些组件(子组件有哪些)
// 可以写name, props, data, methods, computed, watch, 钩子函数... -> 自己的信息
export default {
  name: 'App',
  components: {
    HelloWorld // 声明使用的子组件
  }
}
</script>

<style>
/* 这里声明样式,指定class = app的样式如下 */  
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>

HelloWorld.vue - 子组件,注意他和根组件App.vue的配合

html 复制代码
<template>
    <div class="hello">
      <h1>{{ msg }}</h1>
    </div>
</template>

<script>
// 属性导出,暴露出去,其他组件才能使用
// 可以写data, methods, computed, watch, 钩子函数... -> 自己的信息
export default {
  name: 'HelloWorld', // 声明组件的名称
  props: { // props接收父组件传过来的参数
    msg: String // 声明属性,msg类型为String
  }
}
</script>

<style scoped>
  /* 这里写样式 */
</style>
2.2:组件化开发

通过上面可以看到,整个项目工程的根组件只有App.vue, 服务只会将App.vue渲染到index.html中

这就涉及到后面的重点,组件交互

2.3:组件注册

局部注册 -> 只能在注册的组件内使用

第一步:创建vue文件,例如模板工程的HelloWorld.vue,写好三个部分

第二步:在使用的组件内导入(import...from...)并注册(export default { components中 }),可见App.vue<script>部分

html 复制代码
<script>
// 在script标签中写js, 一般是声明使用的组件
// 然后使用export default引入其他的组件,可以在上面html中使用  
import HelloWorld from './components/HelloWorld.vue' // 步骤1:组件声明
// 步骤2:组件引入
// export default -> 全部导出,这样其他的文件才能用这些信息 
// 可以写data, methods, computed, watch, 钩子函数... -> 自己的信息
export default {
  name: 'App',
  components: {
    HelloWorld // 声明使用的子组件
  }
}
</script>

全局注册 -> 所有的组件中都能使用

第一步:创建vue文件,例如叫做CommonUseButton.vue,写好三个部分

第二步:在main.js这个入口文件中全局注册该组件,然后在任意的组件中都可以使用

js 复制代码
import Vue from 'vue'
import App from './App.vue' // 声明App.vue为App
import CommonUseButton from "@/components/CommonUseButton.vue"; // 1:导入全局组件

Vue.config.productionTip = false  // 阻止启动生产消息

// 2:进行全局注册(组件名:组件对象)
Vue.component("CommonUseButton", CommonUseButton);

new Vue({
  render: h => h(App), // 创建一个Vue实例, 将App组件渲染到id为app的div上
}).$mount('#app')

🚀 通过上面就可以得到也买你开发的思路:例如

  1. 分析页面,按模块拆分组件,搭架子(局部或全局注册)
  2. 根据设计图,编写组件html结构css样式(已准备好)
  3. 拆分封装通用小组件(局部或全局注册)
  4. 将来 → 通过jS动态渲染,实现功能

3:组件通信

组件间的数据传递是如何实现的呢?

组件的数据是独立的,无法直接访问其他组件的数据。要想要使用其他组件的数据,就是组件通信

3.1:父子组件通信

🎉 回顾下上面说的v-model语法糖是如何实现双向绑定的,父子组件通信和它很像

  • v-bind:将数据绑定到元素的 value 属性(数据 -> 视图)
  • v-on:监听输入事件来更新数据(视图 -> 数据)
html 复制代码
<template>
	<div id="app">
        <!-- 当你写了这个 -->
        <input v-model="msg" type="text">
        <!-- 等价于下面这个 -->
        <input :value="msg" @input="msg = $event.target.value" type=text>
    </div>
</template>

因为数据是在父组件中定义的,父组件调用子组件,可以将父组件类比成上面的数据,子组件类比成视图。

父组件 -> 子组件传递数据 -> v-bind

  1. 父组件在<script>data()中声明变量
  2. 在调用子组件的时候,:子组件属性名 = "父组件声明的变量"
  3. 子组件在<script>props中接收父组件的数据,也就是v-bind中指定的子组件属性名
  4. 子组件使用

子组件 -> 父组件传递数据 -> v-on

  1. 子组件通过$emit(事件名称, 更改的值)方法修改父组件的属性数据
  2. 父组件在调用子组件的时候声明事件@事件名称 = "事件方法",这样拿到子组件修改的新值就能通过方法改掉父组件对应的数据了

prop进阶

prop是自定义的属性,用于父->子,可以传递任意数量,任意类型。

prop支持校验

ts 复制代码
props: {
    type: Number, // 类型校验
    required: true, // 是否是必须传递的
    default: 0, // 默认值
    // 自定义校验,参数是属性的值,然后如果返回true,说明校验成功,否则校验失败
    validator (value) {
    	if (value >= 0  && value <= 100) {
    		return true;
    	} else {
    		return false;
    	}
    }
}

prop是单项数据流(父级prop更新,会向下流动,影响子组件,单向性)

也就是说子组件不能直接改prop中的变量

3.2:非父子组件的通信

基本废弃,非父子组件通信vue2使用vuex,vue3使用pinal

很少再使用eventbus / provide / inject

3.3:表单类组件的封装

我们经常要封装一些表单组件供其他组件使用,这时候就涉及到表达类组件的封装

父组件中可以使用v-model简化上面的操作

3.4:ref和this.$ref

想象一下你去图书馆借书:

  • 每本书就是网页上的一个元素(输入框、按钮等)
  • ref 就像是给这本书贴了个便利贴标签
  • this.$refs 就像是你的标签管理器,保存所有贴了标签的书的位置
html 复制代码
<template>
  <div>
    <!-- 给这个输入框贴个标签叫 "myInput" -->
    <input ref="myInput" type="text">
    <button @click="focusInput">点我让输入框获取焦点</button>
  </div>
</template>

<script>
export default {
  methods: {
    focusInput() {
      // 通过标签找到这个输入框,然后让它获取焦点
      this.$refs.myInput.focus();
    }
  },
  mounted() {
    // 页面加载完自动让输入框获取焦点
    this.$refs.myInput.focus();
  }
}
</script>
html 复制代码
<template>
  <input ref="username"> <!-- 贴标签:username -->
  <button ref="submitBtn">提交</button>
  <my-child ref="childComp"></my-child>  <!-- 贴标签:childComp -->
</template>

<script>
    // 那么 this.$refs 里面就有:
    this.$refs = {
        username: <input 元素>,     // 真实的输入框
        submitBtn: <button 元素>,   // 真实的按钮
        childComp: <子组件实例>     // 子组件的全部内容
    }
</script>

4:插槽

想象一下你要买一个手机壳:

  • 普通手机壳:图案、颜色固定,不能改
  • DIY手机壳:有个"插槽"可以放自己的照片

插槽就是:父组件可以在子组件中插入自定义内容的地方

4.1:默认插槽

默认插槽就是留一个坑,等着其他的父组件去填充,这在当代页面开发非常常用

例如一个页面,顶部和底部一直都是不变的,只有中间的内容会变,此时就可以使用插槽。

4.2:具名插槽

vue2.6+

子组件的插槽有名称,通过<slot name="插槽名称">,这样一个子组件中可以定义多个插槽,挖好几个坑,分别插旗命名

父组件中使用<template v-slot:插槽名称>来填充对应的插槽

html 复制代码
<!-- 子组件 BlogPost.vue -->
<template>
  <div class="blog-post">
    <!-- 有名字的插槽 -->
    <slot name="header"></slot>
    <div class="date">{{ date }}</div>
    <!-- 另一个有名字的插槽 -->
    <slot name="content"></slot>
    <!-- 没名字的默认插槽 -->
    <slot></slot>
    <!-- 第三个有名字的插槽 -->
    <slot name="footer"></slot>
  </div>
</template>
html 复制代码
<!-- 父组件使用(Vue 2.6+ 新语法 v-slot) -->
<template>
  <BlogPost date="2024-01-15">
    <!-- 方式1:v-slot:名字 -->
    <template v-slot:header>
      <h1>文章标题</h1>
    </template>
    
    <!-- 方式2:#名字 是简写 -->
    <template #content>
      <p>这里是文章正文内容...</p>
    </template>
    
    <!-- 这个会放到默认插槽(没名字的那个) -->
    <p>默认插槽的内容</p>
    
    <template #footer>
      <div>作者:张三</div>
      <div>阅读量:1000</div>
    </template>
  </BlogPost>
</template>
4.3:作用域插槽

这是最神奇的插槽!子组件可以把数据传给父组件。

html 复制代码
<!-- 子组件 UserList.vue -->
<template>
  <div class="user-list">
    <!-- 把 user 数据通过 slot 传给父组件 -->
    <div v-for="user in users" :key="user.id">
      <slot :user-data="user"></slot>
    </div>
  </div>
</template>

<script>
export default {
  data() {
    return {
      users: [
        { id: 1, name: '小明', age: 20 },
        { id: 2, name: '小红', age: 22 },
        { id: 3, name: '小刚', age: 25 }
      ]
    }
  }
}
</script>
html 复制代码
<!-- 父组件使用 -->
<template>
  <UserList>
    <!-- 接收子组件传来的数据 -->
    <!-- slotProps 可以随便取名,这里收到的是 { user-data: user } -->
    <template v-slot="slotProps">
      <div class="user-item">
        姓名:{{ slotProps['user-data'].name }}
        年龄:{{ slotProps['user-data'].age }}
      </div>
    </template>
  </UserList>
</template>

作用域插槽可以简写

html 复制代码
<!-- 直接解构 -->
<template v-slot="{ userData }">
  <div>姓名:{{ userData.name }}</div>
</template>

<!-- 或者 -->
<template #default="{ userData }">
  <div>姓名:{{ userData.name }}</div>
</template>

5:路由和VueRouter

5.1:使用说明

基础步骤,操作一次即可,配置vueRouter

1️⃣ 下载 - 下载VueRouter模块到当前工程,版本3.6.5

  • vue2 + vueRouter3 + vueX3;
  • vue3 + vueRouter4 + vueX4
bash 复制代码
# 打开工程终端,然后输入(只在当前工程安装vue-router,如果是要再全局安装,后面加上 -g)
npm install vue-router@3.6.5

# 如果使用的yarn
yarn add vue-router@3.6.5

2️⃣ 再main.js中

引入VueRouter -> 安装注册VueRouter -> 创建路由对象 -> 注入

js 复制代码
// 核心作用就是导入App.vue, 根据App.vue创建结构渲染index.html
// 1:导入vue核心包
import Vue from 'vue'
// 2:导入App.vue
import App from './App.vue'

import VueRouter from "vue-router"; // 步骤1:引入VueRouter

// 阻止启动生产消息
Vue.config.productionTip = false

Vue.use(VueRouter); // 步骤2:安装注册VueRouter
const router = new VueRouter(); // 步骤3:创建路由对象

// 创建vue实例
new Vue({
  // 提供render方法 -> 基于App.vue创建结构渲染index.html
  render: h => h(App), 
  router, // 步骤4:将路由对象注入new Vue()实例中,建立关联
}).$mount('#app') // $mount指定挂载点为index.html中id=app的容器

核心步骤 - 使用vue-router

1️⃣ 创建所需要的组件(和视图切换相关的组件,最好放置在views文件夹下),配置路由的规则

假设现在已经有三个组件,分别是Find.vue, My.vue, Friend.vue

main.js中(这里先放在这里,正常不会放在main.js)

js 复制代码
import Find from './views/Find.vue'
import My from './views/My.vue'
import Firend from './views/Friend.vue'

const router = new VueRouter({
    routes: [
        {path: '/find', component: Find},
        {path: '/my', component: My},
        {path: '/friend', component: Friend}
    ]
})

2️⃣ 配置导航,配置路由出口(路径匹配的组件显示的位置)

vue-router提供了一个全局组件router-link来取代a标签,有下面两个作用

  • 能够跳转 - 配置to属性指定路径,to属性无需#,其实本质还是a标签
  • 能够高亮 - 默认就会提供高亮类名(自带激活时的类名-router-link-active),可以直接设置高亮样式
html 复制代码
<div class="my_music">
    <router-link to='/find'>发现音乐</router-link>
    <router-link to='/my'>我的音乐</router-link>
    <router-link to='/friend'>朋友</router-link>
</div>
<div class="top">
    <!-- 
	导航出口配置router-view, router-view是控制组件所展示的位置
	如果在导航html部分的下面写,说明导航在上,切换的内容在下
	如果在导航html部分的上面写,说明导航在下,切换的内容在上
	-->
    <router-view></router-view>
</div>
5.2:router抽离

显然所有的路由配置都堆在main.js中不合适,所以我们应该将路由模块抽离出来

新建src/router文件夹,创建index.js

js 复制代码
// 1:引入视图相关的组件,准备映射
// 小技巧:因为vue的位置层级可能比较深,为了不来回改动,不推荐使用相对路径
// 这里使用绝对路径 - @ - @表示的就是当前工程的src目录
import MyHeader from "@/views/MyHeader.vue";
import MyBody from "@/views/MyBody.vue";
import MyFooter from "@/views/MyFooter.vue";

// 2:引入vue & vue-router
import Vue from 'vue'
import VueRouter from "vue-router";

// 3: 注册vue-router 
Vue.use(VueRouter)

// 创建路由实例,并声明映射
const router = new VueRouter({
    routes: [
        {path: '/header', component: MyHeader},
        {path: '/body', component: MyBody},
        {path: '/footer', component: MyFooter}
    ]
})

// 暴露出去,为了外面的main.js能够使用
export default router

然后main.js中引入这个就可以了

js 复制代码
import Vue from 'vue'
import App from './App.vue' // 声明App.vue为App
import router from "@/router/index.js"; // 导入index.js

Vue.config.productionTip = false  // 阻止启动生产消息

new Vue({
  render: h => h(App), // 创建一个Vue实例, 将App组件渲染到id为app的div上
  router, // 将路由信息放入vue实例中
}).$mount('#app')

在根组件或者指定组件中的<template>中使用

html 复制代码
<div class="my_music">
    <router-link to='/header'>发现音乐</router-link>
    <router-link to='/body'>我的音乐</router-link>
    <router-link to='/footer'>朋友</router-link>
</div>
<div class="top">
    <!-- 
    导航出口配置router-view
    router-view是控制组件所展示的位置
    如果在导航html部分的下面写,说明导航在上,切换的内容在下
    如果在导航html部分的上面写,说明导航在下,切换的内容在上
    -->
    <router-view></router-view>
</div>
5.3:路由模式

hash路由:默认的路由模式,例如http://localhost:8080/#/home

history路由:常用,例如http://localhost:8080/home, 没有了/#这层

js 复制代码
const router = new VueRouter({
    routes,
    mode: "history"
})
5.4:重定向和404

网页打开的时候,url打开的默认是/根路径,此时如果根路径没有配置和任何组件的映射,就会产生一进入导航栏下面空白的情况。而重定向的作用就是,匹配到指定的path之后,强制跳转path的路径

对于404,就是所有的路由都没有配置成功,这个时候就要404页面组件了,404路由一定要配置在所有的路由的最后面,同时指定path:'*'

js 复制代码
// 创建路由实例,并声明映射
const router = new VueRouter({
    routes: [
        // 注意这一行.... 匹配到根路径之后,也就是一进入系统页面,就自动跳转到/header路径,然后触发/header路径对应的组件
        {path: '/', redirect: '/header'},
        {path: '/header', component: MyHeader},
        {path: '/body', component: MyBody},
        {path: '/footer', component: MyFooter},
        // 404,一定要放在最后,否则会匹配到其他路径,path : '*' 
        {path: '*', component: NoFoundError}
    ]
})
5.5:编程式导航和路由传参

基本跳转

可以使用path方式进行跳转

js 复制代码
// 等效于 router-link 的点击
this.$router.push('/user/123')
// 替换当前路由(不会添加历史记录)
this.$router.push({ path: '/login', replace: true })
// 或使用 replace 方法
this.$router.replace('/login')

还可以根据name命名路由跳转(适合path路径长的场景)

js 复制代码
this.$router.push({
    name: '路由名称'
})

// 同时在路径组件映射文件中指定name - router/index.js
{name: "路由名称", path: '路径', component: 组件},

可以在历史记录中前进或者后退 - router.go()

js 复制代码
// 前进 1 步,相当于 history.forward()
this.$router.go(1)
// 后退 1 步,相当于 history.back()
this.$router.go(-1)
// 后退 3 步
this.$router.go(-3)

导航传参

如果是静态路由传参,可以使用下面的方式

js 复制代码
this.$router.push("/路径?参数名1=参数值1&参数名2=参数值2")
// 或者
this.$router.push({
    path: "/路径",
    query: {
        参数名1: '参数值1',
        参数名2: '参数值2'
    }
})
// 或者
this.$router.push({
    name: "路由名字",
    query: {
        参数名1: '参数值1',
        参数名2: '参数值2'
    }
})

此时,router/index.js应该是类似于这样的

js 复制代码
// src/router/index.js
import Vue from 'vue'
import VueRouter from 'vue-router'

Vue.use(VueRouter)

// 定义路由
const routes = [
  {
    path: '/',  // 首页
    name: 'Home', // 名称,根据name命名路由跳转的时候用的就是这个
    component: () => import('../views/Home.vue') // 声明对应的组件是那个
  },
  {
    path: '/search',  // 搜索页
    name: 'Search',
    component: () => import('../views/Search.vue')
  },
  {
    path: '/user',  // 用户详情页
    name: 'User',
    component: () => import('../views/User.vue')
  }
]

// 创建路由实例
const router = new VueRouter({
  mode: 'hash',  // 使用 hash 模式,最简单,不用配服务器
  routes  // 简写,相当于 routes: routes
})

export default router

下游组件通过$router.query.参数名就能拿到对应的值

html 复制代码
<!-- 方式1:在组件中直接使用 $route.query -->
<template>
  <div>
    <!-- 模板中使用 -->
    <p>关键词: {{ $route.query.keyword }}</p>
    <p>页码: {{ $route.query.page }}</p>
  </div>
</template>

<script>
export default {
  created() {
    // JS中获取查询参数
    const keyword = this.$route.query.keyword
    const page = this.$route.query.page
    console.log('搜索参数:', keyword, page)
    
    // 获取路径参数(如果有的话)
    const userId = this.$route.params.id
    console.log('用户ID:', userId)
  }
}
</script>
html 复制代码
<!-- 方式2:使用计算属性 -->
<template>
  <div>
    <p>关键词: {{ keyword }}</p>
    <p>页码: {{ page }}</p>
  </div>
</template>

<script>
export default {
  computed: {
    // 将查询参数转为计算属性
    keyword() {return this.$route.query.keyword || ''},
    page() {return this.$route.query.page || 1},
    // 路径参数
    userId() {return this.$route.params.id}
  },
  
  created() {
    // 直接使用计算属性
    console.log('搜索:', this.keyword, '页码:', this.page)
  }
}
</script>

如果是动态路由传参,可以在上游组件这么写

js 复制代码
this.$router.push("/路径/参数值")
this.$router.push({
    path: "/路径/参数值"
})

this.$router.push({
    name: "路由名称",
    params: {
        参数名: "参数值"
    }
})

此时,router/index.js应该是类似于这样的

js 复制代码
// src/router/index.js
import Vue from 'vue'
import VueRouter from 'vue-router'

Vue.use(VueRouter)

const routes = [
  {
    path: '/',  // 首页
    name: 'Home',
    component: () => import('../views/Home.vue')
  },
  {
    path: '/user/:id',  // 动态路由,:id 是参数占位符
    name: 'User',
    component: () => import('../views/User.vue')
  },
  {
    path: '/article/:category/:id',  // 多个参数
    name: 'Article',
    component: () => import('../views/Article.vue')
  }
]

const router = new VueRouter({
  mode: 'hash',
  routes
})

export default router

下游组件使用同静态路径传参

html 复制代码
<template>
  <div>
    <h2>用户详情</h2>
    <p>用户ID: {{ userId }}</p>
    
    <!-- 显示参数 -->
    <p>动态参数: {{ $route.params.id }}</p>
    
    <!-- 多个参数 -->
    <div v-if="$route.params.category">
      <p>分类: {{ $route.params.category }}</p>
      <p>文章ID: {{ $route.params.id }}</p>
    </div>
  </div>
</template>

<script>
export default {
  name: 'UserDetail',
  
  computed: {
    // 推荐:使用计算属性
    userId() {
      return this.$route.params.id
    },
    category() {
      return this.$route.params.category
    },
    articleId() {
      return this.$route.params.id  // 注意:这是文章ID,不是用户ID
    }
  },
  
  created() {
    // 方法1:直接通过 $route.params 获取
    console.log('用户ID:', this.$route.params.id)
    
    // 方法2:使用计算属性
    console.log('用户ID:', this.userId)
    
    // 多个参数
    console.log('参数对象:', this.$route.params)
  },
  
  watch: {
    // 监听参数变化
    '$route'(to, from) {
      if (to.params.id !== from.params.id) {
        this.loadUserData(to.params.id)
      }
    }
  },
  
  methods: {
    loadUserData(userId) {
      console.log('加载用户数据:', userId)
      // 调用 API 获取用户信息
    }
  }
}
</script>
5.6:路由配置思路

🎉 二级路由对应的组件渲染到哪个一级路由下,children就配置到哪个路由下边

js 复制代码
...
import Article from '@/views/Article.vue'
import Collect from '@/views/Collect.vue'
import Like from '@/views/Like.vue'
import User from '@/views/User.vue'
...

const router = new VueRouter({
  routes: [
    {path: '/', redirect: '/home/article'}, // 首页的重定向
    // 二级路由属于谁,就是谁的children
    {path: '/home', component: Layout, children: [
        {path: 'article', component: Article},
        {path:'collect', component:Collect},
        {path:'like', component:Like},
        {path:'user', component:User}
    ]},
    {path: '/detail', component:ArticleDetail},
    {path: "*", component:PageError} // 404
  ]
})
5.7:路由守卫和配置

路由守卫是在路由跳转前、跳转后或跳转过程中执行的钩子函数,用于控制路由的访问权限、参数验证等

  • to -> 要跳转到的路径
  • from -> 跳转前的路径
  • next -> 是否放行或者跳转到指定的path

全局前置守卫 - router.beforeEach

js 复制代码
const router = new VueRouter({ ... })

// 全局前置守卫
router.beforeEach((to, from, next) => {
  console.log('全局前置守卫')
  console.log('从', from.path, '到', to.path)
  
  // 1. 验证登录状态
  const isAuthenticated = localStorage.getItem('token')
  
  if (to.meta.requiresAuth && !isAuthenticated) {
    // 需要登录但未登录,跳转到登录页
    next({
      path: '/login', // 要跳转到的页面
      query: { redirect: to.fullPath }  // 保存目标路径,登录后跳转回来
    })
  } else if (to.path === '/login' && isAuthenticated) {
    // 已登录但访问登录页,跳转到首页
    next('/')
  } else {
    // 放行
    next()
  }
})

全局解析守卫 - router.beforeResolve

js 复制代码
// 在导航被确认之前,同时在所有组件内守卫和异步路由组件被解析之后调用
router.beforeResolve((to, from, next) => {
    // 可以在这里进行一些全局的数据预取或权限验证
    if (to.meta.requiresData) {
        // 预取数据
        fetchInitialData().then(() => {
            next()
        }).catch(() => {
            next(false)  // 取消导航
        })
    } else {
        next()
    }
})

全局后置钩子 - router.afterEach

js 复制代码
// 导航完成后调用,没有 next 函数
router.afterEach((to, from) => {
    // 修改页面标题
    document.title = to.meta.title || '默认标题'
    // 页面访问统计
    console.log(`用户从 ${from.path} 访问到 ${to.path}`)
    // 滚动到顶部
    window.scrollTo(0, 0)
})

路由独享守卫

js 复制代码
const routes = [
    {
        path: '/user/:id',
        name: 'UserProfile',
        component: UserProfile,
        // 路由独享守卫
        beforeEnter: (to, from, next) => {
            // 验证参数格式
            if (!/^\d+$/.test(to.params.id)) {
                next({ name: 'NotFound' })  // 参数格式错误,跳转到404
                return
            }
            // 检查权限
            const userRole = localStorage.getItem('role')
            if (to.meta.roles && !to.meta.roles.includes(userRole)) {
                next({ path: '/403' })  // 无权限页面
                return
            }
            // 验证用户是否存在
            checkUserExists(to.params.id).then(exists => {
                if (exists) {
                    next()
                } else {
                    next({ name: 'UserNotFound' })
                }
            })
        },

        meta: {
            title: '用户详情',
            roles: ['admin', 'user']  // 允许访问的角色
        }
    }
]

自定义 query 序列化/反序列化

js 复制代码
const router = new VueRouter({
    mode: 'history',
    routes,

    /**
   * stringifyQuery - 将查询对象转换为 URL 查询字符串
   * @param {Object} query - 查询参数对象
   * @returns {string} - 查询字符串
   */
    stringifyQuery(query) {
        // 默认实现(Vue Router 内部实现)
        // 你可以自定义序列化逻辑
        const parts = []
        for (const key in query) {
            const value = query[key]
            if (value === null || value === undefined) {
                continue
            }

            // 处理数组
            if (Array.isArray(value)) {
                value.forEach(item => {
                    parts.push(
                        `${encodeURIComponent(key)}[]=${encodeURIComponent(item)}`
                    )
                })
            } 
            // 处理对象
            else if (typeof value === 'object') {
                parts.push(
                    `${encodeURIComponent(key)}=${encodeURIComponent(JSON.stringify(value))}`
                )
            }
            // 处理基本类型
            else {
                parts.push(
                    `${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`
                )
            }
        }
        return parts.length ? `?${parts.join('&')}` : ''
    },

    /**
   * parseQuery - 将 URL 查询字符串解析为对象
   * @param {string} queryString - 查询字符串(包含 ?)
   * @returns {Object} - 查询参数对象
   */
    parseQuery(queryString) {
        const query = {}
        if (!queryString) return query
        // 移除开头的 ? 或 #
        const search = queryString.replace(/^[?#]/, '')
        if (!search) return query
        // 分割参数
        const pairs = search.split('&')
        for (const pair of pairs) {
            if (!pair) continue
            let [key, value = ''] = pair.split('=')
            // URL 解码
            key = decodeURIComponent(key)
            value = decodeURIComponent(value)
            // 处理数组参数:key[]
            if (key.endsWith('[]')) {
                const realKey = key.slice(0, -2)
                if (!query[realKey]) {
                    query[realKey] = []
                }
                query[realKey].push(value)
            }
            // 处理 JSON 字符串
            else if (value.startsWith('{') || value.startsWith('[')) {
                try {
                    query[key] = JSON.parse(value)
                } catch {
                    query[key] = value
                }
            }
            // 普通参数
            else {
                query[key] = value
            }
        }
        return query
    }
})

滚动行为配置

js 复制代码
const router = new VueRouter({
    mode: 'history',
    routes,

    /**
   * scrollBehavior - 控制路由跳转时的滚动位置
   * @param {Route} to - 目标路由
   * @param {Route} from - 来源路由
   * @param {Object|null} savedPosition - 浏览器前进/后退时保存的位置
   */
    scrollBehavior(to, from, savedPosition) {
        // 1. 浏览器前进/后退时,恢复到之前的位置
        if (savedPosition) {
            return savedPosition
        }

        // 2. 如果有 hash,滚动到对应元素
        if (to.hash) {
            return {
                selector: to.hash,
                behavior: 'smooth',  // 平滑滚动
                offset: { x: 0, y: 100 }  // 偏移量
            }
        }

        // 3. 如果路由有 meta 属性,根据 meta 处理
        if (to.meta.scrollToTop === false) {
            // 某些页面不需要滚动到顶部
            return false
        }

        // 4. 默认滚动到顶部
        return { x: 0, y: 0 }

        // 5. 异步滚动(返回 Promise)
        // return new Promise(resolve => {
        //   setTimeout(() => {
        //     resolve({ x: 0, y: 0 })
        //   }, 500)
        // })
    }
})
5.8:基础案例关键点

整合下的上面的知识点,整个面试经验页面跳转的设计关键点

路由配置思路:二级路由对应的组件渲染到哪个一级路由下,children就配置到哪个路由下边

js 复制代码
...
import Article from '@/views/Article.vue'
import Collect from '@/views/Collect.vue'
import Like from '@/views/Like.vue'
import User from '@/views/User.vue'
...

const router = new VueRouter({
      routes: [
            { path: '/', redirect: '/home/article' }, // 首页的重定向
            // 二级路由属于谁,就是谁的children
            { path: '/home', component: Layout, children: [
                    { path:'article', component:Article },
                    { path:'collect', component:Collect },
                    { path:'like', component:Like },
                    { path:'user', component:User }
            ]},
            { path: '/detail', component: ArticleDetail },
            { path: "*", component: PageError }
      ]
})
html 复制代码
<!-- 导航栏 -->
<nav class="tabbar">
    <router-link to="/home/article">面经</router-link>
    <router-link to="/home/collect">收藏</router-link>
    <router-link to="/home/like">喜欢</router-link>
    <router-link to="/home/user">我的</router-link>
</nav>

<!-- 高亮设置 -->
<style>
    a.router-link-active {
        color: orange;
    }
</style>

文章页面的请求和渲染

js 复制代码
data() {
    return {
        articleList: [], // 初始化文章列表为空,从后端获取
    }
},
async created() {
    // 调用后端,拿到其中的rows,然后赋值给articleList
    const rows = await axios.get(
        'https://mock.boxuegu.com/mock/3083/articles'
    )
    this.articleList = rows
},
html 复制代码
<template>
    <div class="article-page">
        <!-- 使用v-for渲染 -->
        <div class="article-item" v-for="item in articelList" :key="item.id">
            <div class="head">
                <img :src="item.creatorAvatar" alt="" />
                <div class="con">
                    <p class="title">{{ item.stem }}</p>
                    <p class="other">{{ item.creatorName }} | {{ item.createdAt }}</p>
                </div>
            </div>
            <div class="body">
                {{item.content}}
            </div>
            <div class="foot">点赞 {{item.likeCount}} | 浏览 {{item.views}}</div>
        </div>
    </div>
</template>

详情页跳转思路,跳转详情页需要把当前点击的文章id传给详情页,获取数据

  • 查询参数传参 - this.$router.push('/detail?参数1=参数值&参数2=参数值')
  • 动态路由传参 - 先改造路由在传参this.$router.push('/detail/参数值')
html 复制代码
<template>
  <div class="article-page">
    <div class="article-item" 
      	 v-for="item in articelList" 
         :key="item.id" 
       	 @click="$router.push(`/detail?id=${item.id}`)">
    </div>
  </div>
</template>

详情页通过this.$route.query.id使用

动态路由传参 - 需要该index.js路由映射为动态路由

json 复制代码
{
    path: '/detail/:id',
    component: ArticleDetail
}
html 复制代码
<div class="article-item" 
     v-for="item in articelList" :key="item.id" 
     @click="$router.push(`/detail/${item.id}`)">
    ....
</div>

详情页通过this.$route.params.id使用

点击回退跳转到上一页。在详情页中,可以设置点击事件$router.back()回到上一步

html 复制代码
<template>
  <div class="article-detail-page">
    <nav class="nav"><span class="back" @click="$router.back()">&lt;</span> 面经详情</nav>
     ....
  </div>
</template>

详情页渲染,先通过axios携带id发送请求获得文章详情,然后传递给当前组件的属性

js 复制代码
data() {
    return {
        articleDetail:{}
    }
},
async created() {
    const id = this.$route.params.id
    const {data:{result}} = await axios.get(
        `https://mock.boxuegu.com/mock/3083/articles/${id}`
    )
    this.articleDetail = result
},

拿到数据之后,进行页面渲染就可以了

html 复制代码
<template>
    <div class="article-detail-page">
        <!-- 回到上一页面 -->
        <nav class="nav">
            <span class="back" @click="$router.back()">&lt;</span> 面经详情
        </nav>
        <!-- 头部渲染 -->
        <header class="header">
            <h1>{{articleDetail.stem}}</h1>
            <p>{{articleDetail.createAt}} | {{articleDetail.views}} 浏览量 | {{articleDetail.likeCount}} 点赞数</p>
            <p>
                <img
                     :src="articleDetail.creatorAvatar"
                     alt=""
                     />
                <span>{{articleDetail.creatorName}}</span>
            </p>
        </header>
        <!-- 文章主体渲染 -->
        <main class="body">
            {{articleDetail.content}}
        </main>
    </div>
</template>

缓存组件和keep-alive

从面经列表 点到 详情页,又点返回,数据重新加载了 → 希望回到原来的位置

当路由被跳转后,原来所看到的组件就被销毁了,重新返回后组件又被重新创建了,所以数据被加载了,所以可以利用keep-alive把原来的组件给缓存下来

keep-alive 是 Vue 的内置组件,当它包裹动态组件时,会缓存不活动的组件实例,而不是销毁它们。

keep-alive 是一个抽象组件:它自身不会渲染成一个 DOM 元素,也不会出现在父组件中。

html 复制代码
<template>
    <div class="h5-wrapper">
        <!-- 将文章列表的数据进行keep-alive,防止被销毁 -->
        <keep-alive>
            <router-view></router-view>
        </keep-alive>
    </div>
</template>

优点

在组件切换过程中把切换出去的组件保留在内存中,防止重复渲染DOM,

减少加载时间及性能消耗,提高用户体验性。

缺点

缓存了所有被切换的组件

keep-alive的三个属性

  • include : 组件名数组,只有匹配的组件会被缓存
  • exclude : 组件名数组,任何匹配的组件都不会被缓存
  • max : 最多可以缓存多少组件实例
html 复制代码
<template>
    <div class="h5-wrapper">
        <!-- 只有LayoutPage这个组件会被缓存 -->
        <keep-alive :include="['LayoutPage']">
            <router-view></router-view>
        </keep-alive>
    </div>
</template>

keep-alive的使用会触发两个生命周期函数

  • activated 当组件被激活(使用)的时候触发 → 进入这个页面的时候触发
  • deactivated 当组件不被使用的时候触发 → 离开这个页面的时候触发

组件缓存后就不会执行组件的created, mounted, destroyed 等钩子了

所以其提供了actived 和deactived钩子,帮我们实现业务需求。

6:数据共享vuex

Vuex 是一个 Vue 的 状态管理工具,状态就是数据。可以帮我们管理 Vue 通用的数据 (多组件共享的数据)。例如:购物车数据个人信息数据

⚠️ 不是所有的场景都适用于vuex,只有在必要的时候才使用vuex

⚠️ 使用了vuex之后,会附加更多的框架中的概念进来,增加了项目的复杂度

⚠️ vue3中已被pinal取代

6.1:使用说明
bash 复制代码
npm i vuex@3

为了维护项目目录的整洁,在src目录下新建一个store目录其下放置一个index.js文件

js 复制代码
// 导入vue & vuex
import Vue from 'vue'
import Vuex from 'vuex'
// vuex也是vue的插件, 需要use一下, 进行插件的安装初始化
Vue.use(Vuex)

// 创建vuex实例,并导出
export default new Vuex.Store({
    // =============== 这五部分也会是vuex的五大核心概念 ==================
    state: {
        // ~ data
        // state => State提供唯一的公共数据源
        // 所有共享的数据都要统一放到Store中的State中存储。
    },
    getters: {
        // ~ computed
        // getters => Getters是Store的计算属性,可以理解为Store的计算属性,
        // 除了state之外,有时我们还需要从state中筛选出符合条件的一些数据
        // 这些数据是依赖state的,此时会用到getters
    },
    mutations: {
        // mutations => Mutations是Store的变更处理器,
        // 所有对state的修改,都要通过mutations来完成
    },
    actions: {
        // actions => Actions是Store的异步变更处理器,
    },
    modules: {
        // modules => 模块化
    }
})

main.js中引入并挂载vuex到vue实例上

js 复制代码
import Vue from 'vue'
import App from './App.vue'
import store from './store'

Vue.config.productionTip = false

new Vue({
    render: h => h(App),
    store
}).$mount('#app')

🎉 如果在自定义工程的时候勾选了vuex,上面的都不用做,已经创建好对应的模板了

6.2:共享数据-state

提供数据

State提供唯一的公共数据源,所有共享的数据都要统一放到Store中的State中存储。

打开项目中的store/index.js文件,在state对象中可以添加我们要共享的数据。

js 复制代码
// 导入vue & vuex
import Vue from 'vue'
import Vuex from 'vuex'
// vuex也是vue的插件, 需要use一下, 进行插件的安装初始化
Vue.use(Vuex)

// 创建vuex实例,并导出
export default new Vuex.Store({
    state: {
        // state => State提供唯一的公共数据源
        // 所有共享的数据都要统一放到Store中的State中存储。
        count: 100
    }
})

访问数据

s t o r e − > 指向了 s t o r e 仓库的位置,可以帮助我们获取数据,所以我们可以通过 store -> 指向了store仓库的位置,可以帮助我们获取数据,所以我们可以通过 store−>指向了store仓库的位置,可以帮助我们获取数据,所以我们可以通过store访问到vuex的数据

  • <template>中,也就是html使用的话 -> {``{$store.state.变量名}}
  • 在组件的<script>中,this.$store.state.变量名,如果是单个的JS中,使用store.state.变量名

还可以通过辅助函数mapState映射计算属性,帮助我们使用共享数据

1️⃣ 在当前要使用共享数据的组件下导入mapState

2️⃣ 通过es6的展开运算符(...)声明当前组件要使用的共享数据,放到计算属性中

js 复制代码
computed: {
    // 2: 通过es6中的导出运算符号映射state中的count属性
    // 然后就可以直接通过共享数据的名称在当前组件中进行操作了
    ...mapState(['变量名'])
},

⚠️ vuex中的state在组件中直接修改是不生效的,因为state遵循单项数据流

6.3:同步修改-mutations

声明同步方法

Vuex中mutations中要求不能写异步代码,如果有异步的ajax请求,应该放置在actions中

mutations是一个对象,对象中存放修改state的方法

js 复制代码
mutations: {
    // mutations => Mutations是Store的变更处理器,
    // 所有对state的修改,都要通过mutations来完成
    addCount(state) {
        state.count++
    },
    minusCount(state) {
        state.count--
    }
},

使用同步方法

在组件中可以使用this.$store.commit()提交

js 复制代码
handle () {
  this.$store.commit('addCount', 10) // 如果是一个要提交的参数
}

// 如果是多个
this.$store.commit('addCount', {
  count: 10,
  title: '小标题'
})

还可以使用辅助函数mapMutations把位于mutations中的方法提取了出来,我们可以将它导入

js 复制代码
import  { mapMutations } from 'vuex'
methods: {
    // 使用es6中的...
    ...mapMutations(['addCount'])
}
6.4:异步变更-actions

定义异步方法

actions则负责进行异步操作

js 复制代码
mutations: {
  changeCount (state, newCount) {
    state.count = newCount
  }
},

actions: {
  setAsyncCount (context, num) {
    // 一秒后, 给一个数, 去修改 num
    setTimeout(() => {
      context.commit('changeCount', num)
    }, 1000)
  }
},

使用异步方法

组件中通过this.$store.dispatch进行调用

js 复制代码
setAsyncCount () {
  this.$store.dispatch('setAsyncCount', 666)
}

当然也提供了辅助函数辅助mapActions, 同mapMutations一样,放在下游组件的methods中

js 复制代码
import { mapActions } from 'vuex'
methods: {
   ...mapActions(['changeCountAction'])
}

//mapActions映射的代码 本质上是以下代码的写法
//methods: {
//  changeCountAction (n) {
//    this.$store.dispatch('changeCountAction', n)
//  },
//}
6.5:数据筛选-getter

除了state之外,有时我们还需要从state中筛选出符合条件的一些数据

这些数据是依赖state的,此时会用到getters

定义getter

js 复制代码
getters: {
    // getters函数的第一个参数是 state
    // 必须要有返回值
    filterList:  state =>  state.list.filter(item => item > 5)
}

使用getter

可以使用辅助函数mapGetters,将其放在下游组件的computed中

html 复制代码
computed: {
    ...mapGetters(['filterList'])
}

<div>{{ filterList }}</div>
6.6:统一例子实例
js 复制代码
// store/index.js
// 导入vue & vuex
import Vue from 'vue'
import Vuex from 'vuex'
// vuex也是vue的插件, 需要use一下, 进行插件的安装初始化
Vue.use(Vuex)

// 创建vuex实例,并导出
export default new Vuex.Store({

    // 严格模式,严格模式下,不允许在mutations之外进行state的修改
    strict: true,

    state: {
        // state => State提供唯一的公共数据源
        // 所有共享的数据都要统一放到Store中的State中存储。
        count: 100,
        list: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
    },
    getters: {
        // getters => Getters是Store的计算属性,可以理解为Store的计算属性,
        // 除了state之外,有时我们还需要从state中筛选出符合条件的一些数据
        // 这些数据是依赖state的,此时会用到getters
        filterList: state => state.list.filter(item => item > 5)
    },
    mutations: {
        // mutations => Mutations是Store的变更处理器,
        // 所有对state的修改,都要通过mutations来完成
        addCount(state) {
            state.count++
        },
        minusCount(state) {
            state.count--
        }
    },
    actions: {
        // actions => Actions是Store的异步变更处理器
        setAsyncCount(context, num) {
            // 一秒后, 给一个数, 去修改 num
            setTimeout(() => {
                // 提交一个mutations
                context.commit('addCount', num)
            }, 1000)
        }
    },
    modules: {
        // modules => 模块化
    }
})
html 复制代码
<template>
  <div class="box">
    <!-- 3: 模板中的使用 -->
    <h2>{{count}}</h2>
    <button @click="addCount">点我 + 1</button>
    <button @click="setAsyncCount">异步 + 1</button>
    <!-- 遍历filterList -->
    <div v-for="item in filterList" :key="item">
      {{item}}
    </div>
  </div>
</template>

<script>

// 引入mapState & mapMutations & mapActions & mapGetters
import { mapState } from "vuex";
import { mapMutations } from "vuex";
import { mapActions } from "vuex";
import { mapGetters } from "vuex";

export default {
  name: 'Son1Com',
  computed: {
    ...mapState(['count']),
    ...mapGetters(['filterList'])
  },
  methods: {
    ...mapMutations(['addCount']),
    ...mapActions(['setAsyncCount'])
  }
}
</script>

<style lang="css" scoped>
.box {
  border: 3px solid #ccc;
  width: 400px;
  padding: 10px;
  margin: 20px;
}

h2 {
  margin-top: 10px;
}
</style>
6.7:模块-module

由于使用单一状态树,应用的所有状态会集中到一个比较大的对象。当应用变得非常复杂时,store 对象就有可能变得相当臃肿。也就是说,如果把所有的状态都放在state中,当项目变得越来越大的时候,Vuex会变得越来越难以维护

此时就可以写好几个store的文件,然后暴露出去,最后导入到index.js中由index.js统一暴露

/store/modules/user.js

js 复制代码
const state = {
    userInfo: {
        name: 'zs',
        age: 18
    }
}

const mutations = {}

const actions = {}

const getters = {}

// 将自己的属性都暴露出去,为了后面在store/index.js中集成
export default {
    state,
    mutations,
    actions,
    getters
}

/store/modules/setting.js

js 复制代码
const state = {
    theme: 'dark',
    desc: '描述真呀真不错'
}

const mutations = {}

const actions = {}

const getters = {}

export default {
    state,
    mutations,
    actions,
    getters
}

/store/index.js

js 复制代码
// 导入vue & vuex
import Vue from 'vue'
import Vuex from 'vuex'

import user from '@/modules/user'
import setting from '@/modules/setting'

// vuex也是vue的插件, 需要use一下, 进行插件的安装初始化
Vue.use(Vuex)

// 创建vuex实例,并导出
export default new Vuex.Store({

    // 严格模式,严格模式下,不允许在mutations之外进行state的修改
    strict: true,

    state: {
        // state => State提供唯一的公共数据源
        // 所有共享的数据都要统一放到Store中的State中存储。
        count: 100,
        list: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
    },
    getters: {
        // getters => Getters是Store的计算属性,可以理解为Store的计算属性,
        // 除了state之外,有时我们还需要从state中筛选出符合条件的一些数据
        // 这些数据是依赖state的,此时会用到getters
        filterList: state => state.list.filter(item => item > 5)
    },
    mutations: {
        // mutations => Mutations是Store的变更处理器,
        // 所有对state的修改,都要通过mutations来完成
        addCount(state) {
            state.count++
        },
        minusCount(state) {
            state.count--
        }
    },
    actions: {
        // actions => Actions是Store的异步变更处理器
        setAsyncCount(context, num) {
            // 一秒后, 给一个数, 去修改 num
            setTimeout(() => {
                // 提交一个mutations
                context.commit('addCount', num)
            }, 1000)
        }
    },
    modules: {
        // modules => 模块化
        user,
        setting
    }
})

在下游组件,就可以通过辅助函数轻松获取到每一个模块的指定内容:

  • state -> 辅助函数mapState('模块名', ['xxx'])
  • geeter -> 辅助函数mapGetters('模块名', ['xxx'])
  • mutations -> 辅助函数mapMutations('模块名', ['xxx'])
  • actions -> 辅助函数mapActions('模块名', ['xxx'])

或者直接使用:

  • state -> $store.state.模块名.数据项名
  • getters -> $store.getters['模块名/属性名']
  • mutations -> $store.commit('模块名/方法名', 其他参数)
  • actions -> $store.dispatch('模块名/方法名', 其他参数)

7:element组件库的使用

所谓组件库就是封装好了很多很多的组件,整合到一起就是一个组件库

bash 复制代码
npm i element-ui -S

在main.js中全局导入

js 复制代码
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import ElementUI from 'element-ui'; // 引入elementUI
import 'element-ui/lib/theme-chalk/index.css'; // element ui的css
 
Vue.config.productionTip = false // 生产提示关闭
 
Vue.use(ElementUI, { size: "small" }); // 初始化ElementUI
 
new Vue({
  router, // vue路由
  render: h => h(App) // 渲染到模板
}).$mount('#app') // 渲染位置 id = app

剩下看文档就行https://element.eleme.cn/#/zh-CN。其实就是提供了布局方案和一些设计好的标签

相关推荐
北辰alk2 小时前
Vue 自定义指令生命周期钩子完全指南
前端·vue.js
学习非暴力沟通的程序员2 小时前
Karabiner-Elements 豆包语音输入一键启停操作手册
前端
Jing_Rainbow2 小时前
【 前端三剑客-39 /Lesson65(2025-12-12)】从基础几何图形到方向符号的演进与应用📐➡️🪜➡️🥧➡️⭕➡️🛞➡️🧭
前端·css·html
刘羡阳2 小时前
使用Web Worker的经历
前端·javascript
发现一只大呆瓜2 小时前
JS-类型转换:从显式“强制”到隐式“魔法”
javascript
!执行2 小时前
高德地图 JS API 在 Linux 系统的兼容性解决方案
linux·前端·javascript
Gooooo2 小时前
现代浏览器的工作原理
前端
发现一只大呆瓜2 小时前
JS-ES6新特性
javascript
kk晏然3 小时前
TypeScript 错误类型检查,前端ts错误指南
前端·react native·typescript·react