Vue2

Stone大约 93 分钟

Vue2

Vue 是一款用于构建用户界面的 JavaScript 框架。它基于标准 HTML、CSS 和 JavaScript 构建,并提供了一套声明式的、组件化的编程模型,基于数据动态渲染页面,帮助你高效地开发用户界面。无论是简单还是复杂的界面,Vue 都可以胜任。

使用 VS Code,需要安装以下插件:

  • Vetur
  • vue-helper
  • ESLint

起步

使用 vue.js,创建 Vue 实例:

<!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>
  <div id="app">
    <!-- 3. 使用数据 -->
    {{ message }}
  </div>

  <!-- 1. 引入 vue.js -->
  <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script>
  <script>
    // 2. 创建 Vue 实例
    const app = new Vue({
      // 使用 el 配置选择器,指定 Vue 实例管理的容器
      el: '#app', 
      // 使用 data 指定数据
      data: {
        message: 'Hello Vue'
      }
    })
  </script>
</body>
</html>

在这个例子中,{{ message }} 显示 data 对象中 message 属性的值。

插值表达式

插值表达式是 Vue 模板语法中的一个核心特性,它允许在模板中动态地绑定和显示 Vue 实例的数据,将数据渲染到页面中。插值表达式使用双大括号 {{ }} 来包裹 JavaScript 表达式,当 Vue 实例的数据发生变化时,插值表达式的内容会自动更新。

插值表达式中可以包含任何有效的 JavaScript 表达式,不仅仅是变量或属性。但使用的数据必须存在于 data 对象中,且不能在标签属性中使用。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <!-- 
        插值表达式:Vue 的一种模板语法
     -->
    <div id="app">
        <p>{{ name }}</p>
        <p>{{ name.toUpperCase() }}</p>
        <p>{{ name + ',您好' }}</p>
        <p>{{ age >= 18 ? '未成年' : '成年' }}</p>
        <p>{{ friend.name + '爱好是' + friend.hobby }}</p>
    </div>
    <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script>
    <script>
        const app = new Vue({
            el: '#app',
            data: {
                name: 'Stone',
                age: 18,
                friend: {
                    name: 'tom',
                    hobby: 'play'
                }
            }
        })
    </script> 
</body>
</html>

响应式

Vue 的核心特性之一是它的响应式系统。这意味着当 Vue 实例的数据发生变化时,视图会自动更新。这种响应式的数据绑定是 Vue 框架的核心,它使得开发者能够更专注于业务逻辑,而不是手动操作 DOM。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <div id="app">
        <p>{{ msg }}</p>
    </div>
    <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script>
    <script>
        const app = new Vue({
            el: '#app',
            data: {
                // 响应式数据
                msg: 'stone',
            }
        })

        // 1. 访问数据:实例.属性名
        console.log(app.msg)  // 输出:stone

        // 2. 修改数据:实例.属性名 = 新值,修改后视图会自动更新
        app.msg = 'stonecoding.net'
    </script> 
</body>
</html>

开发者工具

Vue 开发者工具是用于 Vue 开发的浏览器扩展,为开发者提供了诸多有用的功能和工具,极大地便利了Vue 应用程序的开发和调试过程。

Vue 开发者工具的安装通常有两种方式,一种是通过浏览器扩展商店安装,另一种是通过下载并拖拽安装。

通过浏览器扩展商店安装:

  • 打开浏览器(如 Google Chrome 或 Mozilla Firefox),访问浏览器的扩展商店。
  • 在搜索栏中输入 "Vue.js devtools",然后搜索。
  • 在搜索结果中找到 Vue.js devtools,点击安装按钮进行安装。
  • 安装完成后,可能需要你重新加载或重启浏览器。

通过下载并拖拽安装:

  • 首先,需要从 Vue 开发者工具的官方下载页面或其他可信来源下载开发者工具的扩展文件(通常是一个 .crx 文件)。
  • 打开浏览器,并进入扩展程序页面。这通常可以通过点击浏览器菜单中的 “更多工具” 或 “扩展程序” 选项进入。
  • 在扩展程序页面中,开启开发者模式(如果尚未开启)。
  • 将下载的 .crx 文件拖拽到扩展程序页面的窗口中。
  • 浏览器会提示确认安装该扩展,点击确认或允许安装。
  • 安装完成后,Vue 开发者工具就会出现在浏览器扩展列表中。

打开 Vue 运行的页面,即可在调试中的 Vue 栏查看和修改数据,进行调试。

image-20240412142126882

指令

Vue 指令是 Vue 框架中用于操作 DOM 的特殊属性。它们以 v- 为前缀,通过绑定到 HTML 元素上,为这些元素添加一些特殊的行为。

v-html

v-html 将 Vue 实例中的数据作为 HTML 插入元素,即动态设置元素的 innerHTML 属性。

语法:

<element v-html="rawHtml"></element>

rawHtml 是 Vue 实例的一个数据属性,应该包含想要插入的 HTML 字符串。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <div id="app">
        <p v-html="msg"></p>
    </div>
    <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script>
    <script>
        const app = new Vue({
            el: '#app',
            data: {
                msg: `<h1>stonecoding.net</h1>`,
            }
        })
    </script> 
</body>
</html>

v-show

v-show 根据条件来切换元素的 CSS 属性 display

语法:

<element v-show="condition">这个元素会根据条件显示或隐藏</element>

condition 是 Vue 实例的一个数据属性,它是一个布尔值,用来决定元素是否应该被显示。当条件为真时,元素会被显示;当条件为假时,元素会被添加 style="display: none" 属性从而被隐藏。

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <style>
    .box {
      width: 200px;
      height: 100px;
      line-height: 100px;
      margin: 10px;
      border: 3px solid #000;
      text-align: center;
      border-radius: 5px;
      box-shadow: 2px 2px 2px #ccc;
    }
  </style>
</head>
<body>
  <div id="app">
    <div v-show="isVisible" class="box">stonecoding.net</div>
  </div> 
  <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script>
  <script>
    const app = new Vue({
      el: '#app',
      data: {
        isVisible: true
      }
    })
  </script>
</body>
</html>

v-if

v-if 使用条件渲染以控制元素显示和隐藏,只会在指令的表达式返回真值时才被渲染。

语法:

<div v-if="condition">这个元素会在条件为真时渲染</div>

condition 是 Vue 实例的一个数据属性,它应该是一个布尔值或者能够计算为布尔值的表达式。当 conditiontrue 时,<div> 元素会被渲染;当 conditionfalse 时,<div> 元素不会被渲染。

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <style>
    .box {
      width: 200px;
      height: 100px;
      line-height: 100px;
      margin: 10px;
      border: 3px solid #000;
      text-align: center;
      border-radius: 5px;
      box-shadow: 2px 2px 2px #ccc;
    }
  </style>
</head>
<body>
  <div id="app">
    <div v-if="isVisible" class="box">stonecoding.net</div>
  </div> 
  <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script>
  <script>
    const app = new Vue({
      el: '#app',
      data: {
        isVisible: true
      }
    })
  </script>
</body>
</html>

v-ifv-show 都是用来根据条件控制元素的显示与隐藏,但它们在实现方式上有本质的区别:

  • v-if 是 “真正” 的条件渲染,如果条件为假,那么什么都不会做,直到条件第一次变为真时,才会开始渲染条件块。每次条件变化时它可能需要销毁和重建元素或组件。
  • v-show 是不论初始条件是什么,元素总是会被渲染,只是简单地切换元素的 CSS 属性 display

这意味着 v-if 有更高的切换开销,而 v-show 有更高的初始渲染开销。因此,如果需要非常频繁地切换一个元素,使用 v-show 会更加高效;如果元素可能永远不会被显示出来,使用 v-if 会更加合适。

v-else

v-else 用于在 v-ifv-else-if 条件不满足时渲染元素。它必须紧跟在 v-ifv-else-if 元素之后,不能单独使用。

语法:

<div v-if="condition">条件为真时显示</div>  
<div v-else>条件不为真时显示</div>

condition 是 Vue 实例的数据属性。当 condition 为真时,第一个 <div> 会被渲染;当 condition 为假时,第二个 <div> 会被渲染。

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body> 
  <div id="app">
    <p v-if="gender === 1">性别:♂ 男</p>
    <p v-else>性别:♀ 女</p>
    <hr>
  </div>
  <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script>
  <script>
    const app = new Vue({
      el: '#app',
      data: {
        gender: 2
      }
    })
  </script>
</body>
</html>

v-else-if

v-else-if 是 Vue 中用于条件渲染的指令,在 v-if 条件不满足时进一步检查其他条件。v-else-if 必须紧跟在 v-if 或另一个 v-else-if 元素之后,并且它可以和 v-else 一起使用来形成完整的条件分支结构。

语法:

<div v-if="condition1">条件1为真时显示</div>  
<div v-else-if="condition2">条件2为真时显示</div>  
<div v-else-if="condition3">条件3为真时显示</div>  
<div v-else>以上条件都不为真时显示</div>

condition1condition2condition3 是 Vue 实例的数据属性。Vue 会按照顺序检查这些条件,一旦找到为真的条件,就会渲染对应的元素,并跳过后面的 v-else-ifv-else。如果没有任何条件为真,则会渲染带有 v-else 的元素。

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>  
  <div id="app">
    <p v-if="score >= 90">优秀</p>  
    <p v-else-if="score >= 80">良好</p>  
    <p v-else-if="score >= 70">中等</p>  
    <p v-else>不及格</p>
  </div>  
  <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script>
  <script>
    const app = new Vue({
      el: '#app',
      data: {
        score: 66
      }
    })
  </script>
</body>
</html>

v-on

v-on 是 Vue 中用于监听 DOM 事件并触发一些 JavaScript 代码的指令。它使得在 Vue 组件中绑定事件监听器变得非常简单和直接。

语法:

<元素 v-on:事件名="内联语句或者函数">

为了简化代码,v-on 指令有一个简写形式,使用 @ 符号代替 v-on:

<元素 @事件名="内联语句或者函数">

其中内联语句就是一段可执行的代码,例如 count--

例子:为按钮绑定点击事件处理内联语句

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <div id="app">
    <button v-on:click="count--">-</button>
    <span>{{ count }}</span>
    <button @click="count++">+</button>
  </div>
  <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script>
  <script>
    const app = new Vue({
      el: '#app',
      data: {
        count:100
      }
    })
  </script>
</body>
</html>

例子:为按钮绑定点击事件处理函数,在 methods 选项中定义

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <div id="app">
    <button @click="fn">切换显示隐藏</button>
    <h1 v-show="isShow">stonecoding.net</h1>
  </div>
  <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script>
  <script>
    const app = new Vue({
      el: '#app',
      data: {
        isShow: false
      },
      methods:{
        fn () {
          // this 指向当前 Vue 实例
          this.isShow = !this.isShow
        }
      }
    })
  </script>
</body>
</html>

例子:为按钮绑定点击事件处理函数并传递参数

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <style>
    .box {
      border: 3px solid #000000;
      border-radius: 10px;
      padding: 20px;
      margin: 20px;
      width: 200px;
    }
    h3 {
      margin: 10px 0 20px 0;
    }
    p {
      margin: 20px;
    }
  </style>
</head>
<body>

  <div id="app">
    <div class="box">
      <h3>小黑自动售货机</h3>
      <button @click="buy(5)">可乐5元</button>
      <button @click="buy(10)">咖啡10元</button>
    </div>
    <p>银行卡余额:{{ money }}元</p>
  </div>

  <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script>
  <script>
    const app = new Vue({
      el: '#app',
      data: {
        money: 100
      },
      methods: {
        buy (price) {
          this.money -= price
        }
      }
    })
  </script>
</body>
</html>

v-bind

v-bind 用于响应式地更新 HTML 元素的属性。它允许将 Vue 实例的数据绑定到元素的属性上。当 Vue 实例的数据发生变化时,这些绑定的属性也会自动更新。

语法:

<element v-bind:attribute="expression"></element>

attribute 是想要绑定的 HTML 元素的属性名称,而 expression 是一个 Vue 表达式,它将被解析为属性值。

可以使用冒号 : 来代替 v-bind:

<element :attribute="expression"></element>
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <div id="app">
    <img v-bind:src="imgUrl" :title="msg" alt="">
  </div>
  <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script>
  <script>
    const app = new Vue({
      el: '#app',
      data: {
        imgUrl: './imgs/10-02.png', 
        msg: '波仔在喝水'
      }
    })
  </script>
</body>
</html>

例子:点击按钮切换图片

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <div id="app">
    <button v-show="index > 0" @click="index--">上一页</button>
    <div>
      <img :src="list[index]" alt="">
    </div>
    <button v-show="index < list.length - 1" @click="index++">下一页</button>
  </div>
  <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script>
  <script>
    const app = new Vue({
      el: '#app',
      data: {
        index: 0,
        list: [
          './imgs/11-00.gif',
          './imgs/11-01.gif',
          './imgs/11-02.gif',
          './imgs/11-03.gif',
          './imgs/11-04.png',
          './imgs/11-05.png',
        ]
      }
    })
  </script>
</body>
</html>

可以使用对象语法或数组语法来绑定 class 用于样式控制:

  • 对象语法:键就是类名,值是布尔值。如果值为 true,则有这个类,否则没有这个类。适用于来回切换一个类名。
<div class="box" :class="{类名1: 布尔值, 类名2: 布尔值}"></div>
  • 数组语法:数组中所有的类,都会添加到元素上,本质上就是一个 class 列表。适用于批量添加或删除类。
<div class="box" :class="['类名1', '类名2']"></div>
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <style>
    .box {
      width: 200px;
      height: 200px;
      border: 3px solid #000;
      font-size: 30px;
      margin-top: 10px;
    }
    .pink {
      background-color: pink;
    }
    .big {
      width: 300px;
      height: 300px;
    }
  </style>
</head>
<body>

  <div id="app">
    <div class="box" :class="{pink: false, big: true}">黑马程序员</div>
    <div class="box" :class="['pink', 'big']">黑马程序员</div>
  </div>
  <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.min.js"></script>
  <script>
    const app = new Vue({
      el: '#app',
      data: {
      }
    })
  </script>
</body>
</html>

例子:Tab 栏点击高亮效果

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <style>
    * {
      margin: 0;
      padding: 0;
    }
    ul {
      display: flex;
      border-bottom: 2px solid #e01222;
      padding: 0 10px;
    }
    li {
      width: 100px;
      height: 50px;
      line-height: 50px;
      list-style: none;
      text-align: center;
    }
    li a {
      display: block;
      text-decoration: none;
      font-weight: bold;
      color: #333333;
    }
    li a.active {
      background-color: #e01222;
      color: #fff;
    }

  </style>
</head>
<body>

  <div id="app">
    <ul>
      <li v-for="(item, index) in list" :key="item.id" @click="activeIndex = index">
        <a :class="{ active: index === activeIndex }" href="#">{{ item.name }}</a>
      </li>
    </ul>
  </div>
  <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.min.js"></script>
  <script>
    const app = new Vue({
      el: '#app',
      data: {
        activeIndex: 0, // 记录高亮
        list: [
          { id: 1, name: '京东秒杀' },
          { id: 2, name: '每日特价' },
          { id: 3, name: '品类秒杀' }
        ]

      }
    })
  </script>
</body>
</html>

可以使用对象语法或数组语法来绑定 style 用于样式控制:

  • 语法:键就是 CSS 属性名,值是 CSS 属性值。适用于对某个具体属性进行动态设置。
<div class="box" :style="{CSS属性名1: CSS属性值, CSS属性名2: CSS属性值}"></div>
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <style>
    .box {
      width: 200px;
      height: 200px;
      background-color: rgb(187, 150, 156);
    }
  </style>
</head>
<body>
  <div id="app">
    <div class="box" :style="{ width: '400px', height: '400px', backgroundColor: 'pink' }"></div>
  </div>
  <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.min.js"></script>
  <script>
    const app = new Vue({
      el: '#app',
      data: {
      }
    })
  </script>
</body>
</html>

例子:进度条

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <style>
    .progress {
      height: 25px;
      width: 400px;
      border-radius: 15px;
      background-color: #272425;
      border: 3px solid #272425;
      box-sizing: border-box;
      margin-bottom: 30px;
    }
    .inner {
      width: 50%;
      height: 20px;
      border-radius: 10px;
      text-align: right;
      position: relative;
      background-color: #409eff;
      background-size: 20px 20px;
      box-sizing: border-box;
      transition: all 1s;
    }
    .inner span {
      position: absolute;
      right: -20px;
      bottom: -25px;
    }
  </style>
</head>
<body>
  <div id="app">
    <!-- 外层盒子底色(黑色) -->
    <div class="progress">
      <!-- 内层盒子 - 进度(蓝色) -->
      <div class="inner" :style="{ width: percent + '%' }">
        <span>{{ percent }}%</span>
      </div>
    </div>
    <button @click="percent=25">设置25%</button>
    <button @click="percent=50">设置50%</button>
    <button @click="percent=75">设置75%</button>
    <button @click="percent=100">设置100%</button>
  </div>
  <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.min.js"></script>
  <script>
    const app = new Vue({
      el: '#app',
      data: {
        percent: 0
      }
    })
  </script>
</body>
</html>

v-for

v-for 是 Vue 中用于渲染一个列表的指令。它可以根据数组或对象来渲染一个元素或一组元素多次。每次渲染都会使用数组或对象的当前项的数据。

对于数组,v-for 指令可以遍历数组中的每个元素,并为每个元素渲染一个模板:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <div id="app">
    <ul>  
      <li v-for="(item, index) in items" :key="index">  
        {{ item }}
      </li>
    </ul>
  </div>
  <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script>
  <script>
    const app = new Vue({
      el: '#app',
      data: {
        items: ['Apple', 'Banana', 'Cherry']
      }
    })
  </script>
</body>
</html>

在这个例子中,v-for 指令遍历 items 数组,并为每个元素创建一个 li 元素。item 是当前遍历到的数组元素,index 是当前元素的索引。:key 是一个绑定,用于给每个循环的元素提供一个唯一的 key,这有助于 Vue 更高效地更新虚拟 DOM。

对于对象,v-for 可以遍历对象的属性:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <div id="app">
    <ul>  
      <li v-for="(value, key, index) in object" :key="index">  
        {{ key }}: {{ value }}  
      </li>  
    </ul> 
  </div>
  <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script>
  <script>
    const app = new Vue({
      el: '#app',
      data: {
        object: {  
          firstName: 'John',  
          lastName: 'Doe',  
          age: 30  
        } 
      }
    })
  </script>
</body>
</html>

在这个例子中,v-for 遍历 object 对象的属性。value 是当前属性的值,key 是属性的键名,index 是属性的索引(在 Vue 2.x 中,对于对象的 v-forindex 是基于对象属性被 Vue 检测到的顺序,这个顺序可能不是稳定的,所以通常不推荐使用 index 作为 key)。

注意:

  • 使用 v-for 时,需要为每一个循环的元素提供一个唯一的 key 属性。这有助于 Vue 跟踪每个节点的身份,从而重用和重新排序现有元素。
  • 在 Vue 2.x 中,对于数组,index 可以作为 key 使用,但对于对象,通常不推荐使用 index 作为 key,因为对象的属性顺序可能不是稳定的。对于对象,更好的做法是使用一个稳定的唯一值作为 key,比如属性的值(如果它是唯一的)或属性的键名。
  • 在 Vue 3 中,对于对象,v-for 默认不再保证顺序,因此使用对象的 v-for 时应更加小心。如果需要稳定的顺序,最好将对象转换为数组或其他可迭代的数据结构。

例子:小黑的书架

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>

  <div id="app">
    <h3>小黑的书架</h3>
    <ul>
      <li v-for="(item, index) in booksList" :key="item.id">
        <span>{{ item.name }}</span>
        <span>{{ item.author }}</span>
        <!-- 注册点击事件,通过 id 进行删除数组中的对应项 -->
        <button @click="del(item.id)">删除</button>
      </li>
    </ul>
  </div>
  <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script>
  <script>
    const app = new Vue({
      el: '#app',
      data: {
        booksList: [
          { id: 1, name: '《红楼梦》', author: '曹雪芹' },
          { id: 2, name: '《西游记》', author: '吴承恩' },
          { id: 3, name: '《水浒传》', author: '施耐庵' },
          { id: 4, name: '《三国演义》', author: '罗贯中' }
        ]
      },
      methods: {
        del (id) {
          // filter: 根据条件,保留满足条件的对应项,得到一个新数组。
          this.booksList = this.booksList.filter(item => item.id !== id)

        }
      }
    })
  </script>
</body>
</html>

v-model

v-model 是 Vue 中的一个重要指令,它提供了一种简洁的方式来实现表单输入和应用状态之间的双向绑定。这使得开发者可以非常方便地处理用户输入,并且保证视图和状态之间的同步。

v-model 用于在 inputtextarea 以及 select 等表单元素上创建双向数据绑定。确保表单元素内容与 Vue 实例中的数据保持一致,任何一方数据发生变化,对应的数据也会自动更新。

在表单元素上使用 v-model,可以将其值与 Vue 实例的某个数据进行双向绑定。当用户在表单元素中输入时,绑定的数据会自动更新;同样,当数据的值在 Vue 实例中改变时,表单元素的值也会自动更新。

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <!-- 
    v-model 可以让数据和视图,形成双向数据绑定
    (1) 数据变化,视图自动更新
    (2) 视图变化,数据自动更新
    可以快速[获取]或[设置]表单元素的内容
   -->
  <div id="app">
    账户:<input type="text" v-model="username"> <br><br>
    密码:<input type="password" v-model="password"> <br><br>
    <button @click="login">登录</button>
    <button @click="reset">重置</button>
  </div>
  <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script>
  <script>
    const app = new Vue({
      el: '#app',
      data: {
        username: '',
        password: ''
      },
      methods: {
        login() {
          console.log(this.username, this.password);
        },
        reset() {
          this.username = ''
          this.password = ''
        }
      }
    })
  </script>
</body>
</html>

例子:小黑记事本

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="stylesheet" href="./css/index.css" />
<title>记事本</title>
</head>
<body>
<!-- 主体区域 -->
<section id="app">
  <!-- 输入框 -->
  <header class="header">
    <h1>小黑记事本</h1>
    <input @keyup.enter="add" v-model="todoName" placeholder="请输入任务" class="new-todo" />
    <button @click="add" class="add">添加任务</button>
  </header>
  <!-- 列表区域 -->
  <section class="main">
    <ul class="todo-list">
      <li class="todo" v-for="(item, index) in list" :key="item.id">
        <div class="view">
          <span class="index">{{ index + 1 }}.</span> <label>{{ item.name }}</label>
          <button @click="del(item.id)" class="destroy"></button>
        </div>
      </li>
    </ul>
  </section>
  <!-- 统计和清空 如果没有任务了,底部隐藏掉 -->
  <footer class="footer" v-show="list.length > 0">
    <!-- 统计 -->
    <span class="todo-count">合 计:<strong> {{ list.length }} </strong></span>
    <!-- 清空 -->
    <button @click="reset" class="clear-completed">
      清空任务
    </button>
  </footer>
</section>

<!-- 底部 -->
<script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.min.js"></script>
<script>
  const app = new Vue({
    el: '#app',
    data: {
      todoName: '',
      list: [
        {id: 1, name: '跑步一公里'}, 
        {id: 2, name: '打游戏一小时'},
        {id: 3, name: '游泳一小时'}
      ]
    },
    methods: {
      del(id) {
        this.list = this.list.filter(item => item.id != id)
      },
      add() {
        if (this.todoName.trim() === '') {
          alert('请输入任务名称')
          return
        }
        this.list.unshift({
          id: +new Date(),
          name: this.todoName
        })
        this.todoName = ''
      },
      reset() {
        this.list = []
      }
    }
  })
</script>
</body>
</html>

例子:应用于其他表单元素

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <style>
    textarea {
      display: block;
      width: 240px;
      height: 100px;
      margin: 10px 0;
    }
  </style>
</head>
<body>

  <div id="app">
    <h3>stonecoding.net</h3>

    姓名:
      <input type="text" v-model="username"> 
      <br><br>

    是否单身:
      <input type="checkbox" v-model="isSingle"> 
      <br><br>

    <!-- 
      前置理解:
        1. name:  给单选框加上 name 属性 可以分组 → 同一组互相会互斥
        2. value: 给单选框加上 value 属性,用于提交给后台的数据
      结合 Vue 使用 → v-model
    -->
    性别: 
      <input v-model="gender" type="radio" name="gender" value="1"><input v-model="gender" type="radio" name="gender" value="2"><br><br>

    <!-- 
      前置理解:
        1. option 需要设置 value 值,提交给后台
        2. select 的 value 值,关联了选中的 option 的 value 值
      结合 Vue 使用 → v-model
    -->
    所在城市:
      <select v-model="cityId">
        <option value="101">北京</option>
        <option value="102">上海</option>
        <option value="104">成都</option>
        <option value="105">南京</option>
      </select>
      <br><br>

    自我描述:
      <textarea v-model="desc"></textarea> 

    <button>立即注册</button>
  </div>
  <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.min.js"></script>
  <script>
    const app = new Vue({
      el: '#app',
      data: {
        username: '',
        isSingle: false,
        gender: "1",
        cityId: '104',
        desc: 'stonecoding.net'
      }
    })
  </script>
</body>
</html>

v-model 的原理基于 Vue 的响应式系统和事件监听机制。具体来说,v-model 指令在内部会利用不同的元素类型(如 inputtextareaselect)自动切换为 valuecheckedselected 等属性,并监听 inputchange 或其他相应的事件。

  • 属性绑定
    • 当表单元素为 inputtextarea 时,v-model 会绑定到元素的 value 属性。
    • 当表单元素为 checkboxradio 时,v-model 会绑定到元素的 checked 属性。
    • 当表单元素为 select 时,v-model 会绑定到元素的 selected 属性。
  • 事件监听
    • v-model 会监听 inputchange 事件(取决于表单元素的类型)。当用户在表单元素中输入时,会触发相应的事件。
  • 双向绑定
    • 当用户修改表单元素的值时,会触发相应的事件,并更新组件的数据属性。
    • 同时,当组件的数据属性发生变化时,由于 Vue 的响应式系统,表单元素的值也会自动更新

v-model 本质上是一个语法糖,就是对应的属性(例如 value)与事件(例如 input)的合写:

<template>
  <div>
    <Input v-model="msg1" type="text"></Input>
    <!-- 模板中获取事件的形参 -> $event 获取 -->
    <Input :value="msg2" @input="msg2 = $event.target.value" type="text"></Input>
  </div>
</template>

除了基本的表单元素,v-model 也可以用于自定义组件。

指令修饰符

Vue 指令修饰符是 Vue 指令的附加后缀,用于修改指令的行为。它们提供了一种简洁的方式来调整指令的行为,而无需使用复杂的表达式或额外的 JavaScript 代码。

常用的指令修饰符有:

  • 按键修饰符:

    • @keyup.enter:键盘回车监听
  • v-model 修饰符:

    • .number:将用户的输入自动转换为数值类型。

    • .trim:自动过滤用户输入的首尾空白字符。

  • 事件修饰符:

    • .stop:调用 event.stopPropagation()。阻止点击事件继续传播。

    • .prevent:调用 event.preventDefault()。阻止元素的默认行为。

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <style>
    .father {
      width: 200px;
      height: 200px;
      background-color: pink;
      margin-top: 20px;
    }
    .son {
      width: 100px;
      height: 100px;
      background-color: skyblue;
    }
  </style>
</head>
<body>
  <div id="app">
    <h3>v-model修饰符 .trim .number</h3>
    姓名:<input v-model.trim="username" type="text"><br>
    年纪:<input v-model.number="age" type="text"><br>

    
    <h3>@事件名.stop     →  阻止冒泡</h3>
    <div @click="fatherFn" class="father">
      <div @click.stop="sonFn" class="son">son</div>
    </div>

    <h3>@事件名.prevent  →  阻止默认行为</h3>
    <a @click.prevent href="http://www.baidu.com">阻止默认行为</a>
  </div>
  <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.min.js"></script>
  <script>
    const app = new Vue({
      el: '#app',
      data: {
        username: '',
        age: '',
      },
      methods: {
        fatherFn () {
          alert('father')
        },
        sonFn (e) {
          // e.stopPropagation()
          alert('son')
        }
      }
    })
  </script>
</body>
</html>

计算属性

Vue 的计算属性(computed properties)是一种更为高级的数据属性,它是基于已有的数据属性通过计算得到的结果。当依赖的数据属性发生变化时,计算属性会自动更新。计算属性在处理复杂逻辑时,可以显著提高性能,因为它们被缓存了,只有在相关依赖发生改变时才会重新计算。

计算属性在 Vue 实例的 computed 选项中定义。

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <style>
    table {
      border: 1px solid #000;
      text-align: center;
      width: 240px;
    }
    th,td {
      border: 1px solid #000;
    }
    h3 {
      position: relative;
    }
  </style>
</head>
<body>

  <div id="app">
    <h3>小黑的礼物清单</h3>
    <table>
      <tr>
        <th>名字</th>
        <th>数量</th>
      </tr>
      <tr v-for="(item, index) in list" :key="item.id">
        <td>{{ item.name }}</td>
        <td>{{ item.num }}个</td>
      </tr>
    </table>

    <!-- 目标:统计求和,求得礼物总数 -->
    <p>礼物总数:{{ totalCount }} 个</p>
  </div>
  <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.min.js"></script>
  <script>
    const app = new Vue({
      el: '#app',
      data: {
        list: [
          { id: 1, name: '篮球', num: 1 },
          { id: 2, name: '玩具', num: 2 },
          { id: 3, name: '铅笔', num: 5 },
        ]
      },
      computed: {
        totalCount() {
          // 计算属性函数内部,可以直接通过 this 访问到 app 实例
          let total = this.list.reduce((sum, item) => sum + item.num, 0)
          return total
        }
      }
    })
  </script>
</body>
</html>

计算属性默认只有 Getter,不过在需要时也可以提供一个 Setter:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>

  <div id="app">
    姓:<input type="text" v-model="firstName"><br>
    名:<input type="text" v-model="lastName"><br>
    <p>姓名: {{ fullName }}</p>
    <button @click="changeName">修改姓名</button>
  </div>
  <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.min.js"></script>
  <script>
    const app = new Vue({
      el: '#app',
      data: {
        firstName: '吕',
        lastName: '布'
      },
      computed: {
        // 简写 获取
        /* fullName() {
          return this.firstName + this.lastName
        } */
        // 完整写法 获取 + 设置
        fullName: {
          // (1) 当fullName计算属性时,被获取求值时,执行get(有缓存)
          //     会将返回值作为求值的结果
          get() {
            return this.firstName + this.lastName 
          },
          // (2) 当fullName计算属性,被修改赋值时,执行set
          //     修改的值,传递给set方法的形参
          set(value) {
            this.firstName = value.slice(0, 1)
            this.lastName = value.slice(1)
          }
        }

      },
      methods: {
        changeName() {
          this.fullName='貂蝉'
        }
      }
    })
  </script>
</body>
</html>

例子:综合案例-成绩

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <link rel="stylesheet" href="./styles/index.css" />
    <title>Document</title>
  </head>
  <body>
    <div id="app" class="score-case">
      <div class="table">
        <table>
          <thead>
            <tr>
              <th>编号</th>
              <th>科目</th>
              <th>成绩</th>
              <th>操作</th>
            </tr>
          </thead>
          <tbody v-if="list.length > 0">
            <tr v-for="(item, index) in list" :key="item.id">
              <td>{{ index + 1 }}</td>
              <td>{{ item.subject }}</td>
              <!-- 需求:不及格的标红,低于 60 加 red 类 -->
              <td :class="{ red: item.score < 60 }">{{ item.score }}</td>
              <td><a @click.prevent="del(item.id)" href="#">删除</a></td>
            </tr>
          </tbody>
          <tbody v-else>
            <tr>
              <td colspan="5">
                <span class="none">暂无数据</span>
              </td>
            </tr>
          </tbody>

          <tfoot >
            <tr>
              <td colspan="5">
                <span>总分:{{ totalScore || 0 }}</span>
                <span style="margin-left: 50px">平均分:{{ averageScore }}</span>
              </td>
            </tr>
          </tfoot>
        </table>
      </div>
      <div class="form">
        <div class="form-item">
          <div class="label">科目:</div>
          <div class="input">
            <input
              type="text"
              placeholder="请输入科目"
              v-model.trim="subject"
            />
          </div>
        </div>
        <div class="form-item">
          <div class="label">分数:</div>
          <div class="input">
            <input
              type="text"
              placeholder="请输入分数"
              v-model.number="score"
            />
          </div>
        </div>
        <div class="form-item">
          <div class="label"></div>
          <div class="input">
            <button @click="add" class="submit">添加</button>
          </div>
        </div>
      </div>
    </div>
    <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.min.js"></script>
    <script>
      const app = new Vue({
        el: '#app',
        data: {
          list: [
            { id: 1, subject: '语文', score: 20 },
            { id: 7, subject: '数学', score: 99 },
            { id: 12, subject: '英语', score: 70 },
          ],
          subject: '',
          score: ''
        },
        computed: {
          totalScore() {
            let total = this.list.reduce((sum, item) => sum + item.score, 0)
            return total
          },
          averageScore() {
            if (this.list.length === 0) {
              return 0
            }
            return (this.totalScore / this.list.length).toFixed(2)
          }
        },
        methods: {
          del(id) {
            this.list = this.list.filter(item => item.id != id)
          },
          add() {
            if (!this.subject) {
              alert('请输入科目和成绩')
              return
            }
            if (typeof this.score !== 'number') {
              alert('请输入正确的成绩')
            }
            this.list.unshift({
              id: +new Date(),
              subject: this.subject,
              score: this.score
            })
            this.subject = ''
            this.score = ''
          }
        }
      })
    </script>
  </body>
</html>

侦听器

Vue 侦听器(Watchers)是 Vue 框架中的一个重要特性,它允许开发者对数据进行监视,并在数据发生变化时执行特定的操作。侦听器在 Vue 实例的 watch 选项中定义,可以监视响应式数据的变化。

new Vue({  
  el: '#app',  
  data: {  
    message: 'Hello Vue!',
    someObject: {
      someProp: ''  
    },
    deepObject: {
        
    }
  },  
  watch: {  
    // 侦听 message 的变化  
    message (newVal, oldVal) {  
      // 当 message 改变时,这个函数会被调用  
      console.log('message has changed from', oldVal, 'to', newVal);  
    },  
      
    // 也可以侦听一个对象的多个属性,使用字符串形式  
    'someObject.someProp' (newVal, oldVal) {  
      // 当 someObject.someProp 改变时,这个函数会被调用  
    },  
      
    // 深度侦听对象的变化
    deepObject: {  
      handler (newVal, oldVal) {  
        // 当 deepObject 或其任何子属性改变时,这个函数会被调用  
      },  
      deep: true // 开启深度侦听,监视对象所有属性的变化
      immediate: true, // 开启立刻执行
    }  
  }  
});

例子:侦听翻译输入框变化

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <style>
      * {
        margin: 0;
        padding: 0;
        box-sizing: border-box;
        font-size: 18px;
      }
      #app {
        padding: 10px 20px;
      }
      .query {
        margin: 10px 0;
      }
      .box {
        display: flex;
      }
      textarea {
        width: 300px;
        height: 160px;
        font-size: 18px;
        border: 1px solid #dedede;
        outline: none;
        resize: none;
        padding: 10px;
      }
      textarea:hover {
        border: 1px solid #1589f5;
      }
      .transbox {
        width: 300px;
        height: 160px;
        background-color: #f0f0f0;
        padding: 10px;
        border: none;
      }
      .tip-box {
        width: 300px;
        height: 25px;
        line-height: 25px;
        display: flex;
      }
      .tip-box span {
        flex: 1;
        text-align: center;
      }
      .query span {
        font-size: 18px;
      }

      .input-wrap {
        position: relative;
      }
      .input-wrap span {
        position: absolute;
        right: 15px;
        bottom: 15px;
        font-size: 12px;
      }
      .input-wrap i {
        font-size: 20px;
        font-style: normal;
      }
    </style>
  </head>
  <body>
    <div id="app">
      <!-- 条件选择框 -->
      <div class="query">
        <span>翻译成的语言:</span>
        <select v-model="obj.lang">
          <option value="italy">意大利</option>
          <option value="english">英语</option>
          <option value="german">德语</option>
        </select>
      </div>

      <!-- 翻译框 -->
      <div class="box">
        <div class="input-wrap">
          <textarea v-model="obj.words"></textarea>
          <span><i>⌨️</i>文档翻译</span>
        </div>
        <div class="output-wrap">
          <div class="transbox">{{ result }}</div>
        </div>
      </div>
    </div>
    <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
    <script>
      // 接口地址:https://applet-base-api-t.itheima.net/api/translate
      // 请求方式:get
      // 请求参数:
      // (1)words:需要被翻译的文本(必传)
      // (2)lang: 需要被翻译成的语言(可选)默认值-意大利
      // -----------------------------------------------
      
      const app = new Vue({
        el: '#app',
        data: {
          // words: ''
          obj: {
            words: 'stone',
            lang: 'italy'
          },
          result: '', //翻译结果
        },
        watch: {
          obj: {
            deep: true, // 深度侦听
            immediate: true, // 立刻执行,一进入页面 handler 就立刻执行一次
            handler (newValue) {
              clearTimeout(this.timer)
              this.timer = setTimeout(async () => {
                const res = await axios({
                url: 'https://applet-base-api-t.itheima.net/api/translate',
                params: newValue
              })
              this.result = res.data.data
              console.log(res.data.data)
              }, 300);
            }
          }
           /* 'obj.words' (newValue) {
              clearTimeout(this.timer)
              this.timer = setTimeout(async () => {
                const res = await axios({
                url: 'https://applet-base-api-t.itheima.net/api/translate',
                params: {
                  words: newValue
                }
              })
              this.result = res.data.data
              console.log(res.data.data)
              }, 300);
            } */
       }
      })
    </script>
  </body>
</html>

例子:综合案例-购物车

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <link rel="stylesheet" href="./css/inputnumber.css" />
    <link rel="stylesheet" href="./css/index.css" />
    <title>购物车</title>
  </head>
  <body>
    <div class="app-container" id="app">
      <!-- 顶部banner -->
      <div class="banner-box"><img src="./img/fruit.jpg" alt="" /></div>
      <!-- 面包屑 -->
      <div class="breadcrumb">
        <span>🏠</span>
        /
        <span>购物车</span>
      </div>
      <!-- 购物车主体 -->
      <div class="main" v-if="fruitList.length > 0">
        <div class="table">
          <!-- 头部 -->
          <div class="thead">
            <div class="tr">
              <div class="th">选中</div>
              <div class="th th-pic">图片</div>
              <div class="th">单价</div>
              <div class="th num-th">个数</div>
              <div class="th">小计</div>
              <div class="th">操作</div>
            </div>
          </div>
          <!-- 身体 -->
          <div class="tbody">
            <div v-for="(item, index) in fruitList" class="tr" :key="item.id" :class="{ active: item.isChecked }">
              <div class="td"><input type="checkbox" v-model="item.isChecked" /></div>
              <div class="td"><img :src="item.icon" alt="" /></div>
              <div class="td">{{ item.price }}</div>
              <div class="td">
                <div class="my-input-number">
                  <button  :disabled="item.num <= 1" class="decrease" @click="sub(item.id)"> - </button>
                  <span class="my-input__inner">{{ item.num }}</span>
                  <button class="increase" @click="add(item.id)"> + </button>
                </div>
              </div>
              <div class="td">{{ item.price * item.num }}</div>
              <div class="td"><button @click="del(item.id)">删除</button></div>
            </div>
          </div>
        </div>
        <!-- 底部 -->
        <div class="bottom">
          <!-- 全选 -->
          <label class="check-all">
            <input type="checkbox" v-model="isAll" />
            全选
          </label>
          <div class="right-box">
            <!-- 所有商品总价 -->
            <span class="price-box">总价&nbsp;&nbsp;:&nbsp;&nbsp;¥&nbsp;<span class="price">{{ totalPrice }}</span></span>
            <!-- 结算按钮 -->
            <button class="pay">结算( {{ totalCount }} )</button>
          </div>
        </div>
      </div>
      <!-- 空车 -->
      <div class="empty" v-else>🛒空空如也</div>
    </div>
    <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.min.js"></script>
    <script>
      const defaultArr = [{
              id: 1,
              icon: './img/火龙果.png',
              isChecked: true,
              num: 2,
              price: 6,
            },
            {
              id: 2,
              icon: './img/荔枝.png',
              isChecked: false,
              num: 7,
              price: 20,
            },
            {
              id: 3,
              icon: './img/榴莲.png',
              isChecked: false,
              num: 3,
              price: 40,
            },
            {
              id: 4,
              icon: './img/鸭梨.png',
              isChecked: true,
              num: 10,
              price: 3,
            },
            {
              id: 5,
              icon: './img/樱桃.png',
              isChecked: false,
              num: 20,
              price: 34,
            },]
      const app = new Vue({
        el: '#app',
        data: { 
          // 水果列表
          fruitList: JSON.parse(localStorage.getItem('list')) || defaultArr,
        },
        computed: {
          // 默认计算属性:只能获取不能设置,要设置需要写完整写法
          /* isAll() {
            // 必须所有的小选框都选中,全选按钮才选中 -> every
            return this.fruitList.every(item => item.isChecked === true)
          } */
          // 完整写法 = get + set
          isAll: {
            get() {
              return this.fruitList.every(item => item.isChecked === true)
            },
            set(value) {
              // 基于拿到的布尔值,要让所有的小选框同步状态
              this.fruitList.forEach(item => item.isChecked = value )
            }
          },
          // 统计选中的总数 reduce
          totalCount() {
            return this.fruitList.reduce((sum, item) => {
              if (item.isChecked) {
                // 选中 -> 需要累加
                return sum + item.num
              } else {
                // 没选中 -> 不需要累加
                return sum
              }
            }, 0)
          },
          // 统计选中的总价 num * price
          totalPrice() {
            return this.fruitList.reduce((sum, item) => {
              if (item.isChecked) {
                return sum + (item.price * item.num)
              } else {
                return sum
              }
            }, 0)
          }
        },
        methods: {
          del(id) {
            this.fruitList = this.fruitList.filter(item => item.id !== id)
          },
          add(id) {
            // 1. 根据 id 找到数组中的对应项
            const fruit = this.fruitList.find(item => item.id === id)
            // 2. 操作 num 数量
            fruit.num++
          },
          sub(id) {
            // 1. 根据 id 找到数组中的对应项
            const fruit = this.fruitList.find(item => item.id === id)
            // 2. 操作 num 数量
            fruit.num--
          }
        },
        watch: {
          fruitList: {
            deep: true,
            handler(newValue) {
              // 需要将变化后的 newValue 存入本地 (转JSON)  
              localStorage.setItem('list', JSON.stringify(newValue))
            }
          }
        }
      })
    </script>
  </body>
</html>

技术点总结:

  • 渲染功能:v-ifv-elsev-for:class
  • 删除功能:点击传参,使用 filter 过滤覆盖原数组
  • 修改个数:点击传参,使用 find 查找对象
  • 全选反选:使用计算属性完整写法
  • 计算总数:使用计算属性及 reduce 进行条件求和
  • 持久化:使用侦听器,localStorageJSON.stringifyJSON.parse

生命周期

Vue 的生命周期是指 Vue 实例从创建到销毁的过程,这个过程总共分为创建阶段、挂载阶段、更新阶段和销毁阶段。

img

具体包括以下钩子函数:

  1. beforeCreate:在实例初始化之后,但在数据观测和事件配置之前被调用。此时,datamethods 等选项尚未初始化,并且无法访问 this
  2. created:在这个阶段实例已经完成以下配置:数据观测(Data Observer),属性和方法的运算,以及 Watch/Event 事件回调。然而,挂载阶段还没开始,$el 属性目前尚不可见。通常用于发送初始化渲染请求
  3. beforeMount:在挂载开始之前被调用。会进行模板编译,生成虚拟DOM。
  4. mounted:挂载完成后被调用。会将数据渲染到页面上,生成真实DOM。通常用于操作 DOM
  5. beforeUpdate:数据更新时调用,但在 DOM 重新渲染之前。在这个阶段,可以对数据进行一些处理或做一些其他操作。
  6. updated:数据更新完成时调用。此时,DOM 已经重新渲染,可以对更新后的 DOM 进行操作。
  7. beforeDestroy:实例销毁之前调用。在这个阶段,实例仍然完全可用,可以进行一些清理工作,如:清除计时器、解绑全局事件、销毁插件对象等操作。
  8. destroyed:实例销毁之后调用。调用后,Vue 实例指示的所有东西都会解绑,所有的事件监听器都会被移除,所有的子实例也会被销毁。
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <div id="app">
    <h3>{{ title }}</h3>
    <div>
      <button @click="count--">-</button>
      <span>{{ count }}</span>
      <button @click="count++">+</button>
    </div>
  </div>
  <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.min.js"></script>
  <script>
    const app = new Vue({
      el: '#app',
      data: {
        count: 100,
        title: '计数器'
      },
      // 1. 创建阶段(准备数据)
      beforeCreate() {
        console.log('beforeCreate 响应式数据准备好之前', this.count)
      },
      created() {
        console.log('created 响应式数据准备好之后', this.count)
        // this.数据名 = 请求回来的数据
        // 可以开始发送初始化渲染的请求了
      },

      // 2. 挂载阶段(渲染模板)
      beforeMount() {
        console.log('beforeMount 模板渲染之前', document.querySelector('h3').innerHTML)
      },
      mounted() {
        console.log('mounted 模板渲染之后', document.querySelector('h3').innerHTML)
        // 可以开始操作 DOM 了
      },

      // 3. 更新阶段(修改数据 -> 更新数据)
      beforeUpdate() {
        console.log('beforeUpdate 数据修改了,视图还没更新', document.querySelector('span').innerHTML)
      },
      updated() {
        console.log('updated 数据修改了,视图已经更新', document.querySelector('span').innerHTML)
      },

      // 4. 销毁阶段
      beforeDestroy() {
        console.log('beforeDestroy 销毁前')
        console.log('清除掉一些 Vue 以外的资源占用,定时器,延时器...');
      },
      destroyed() {
        console.log('destroyed 销毁后')
      }
    })
  </script>
</body>
</html>

例子:在 created 阶段发送请求获取数据

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <style>
    * {
      margin: 0;
      padding: 0;
      list-style: none;
    }
    .news {
      display: flex;
      height: 120px;
      width: 600px;
      margin: 0 auto;
      padding: 20px 0;
      cursor: pointer;
    }
    .news .left {
      flex: 1;
      display: flex;
      flex-direction: column;
      justify-content: space-between;
      padding-right: 10px;
    }
    .news .left .title {
      font-size: 20px;
    }
    .news .left .info {
      color: #999999;
    }
    .news .left .info span {
      margin-right: 20px;
    }
    .news .right {
      width: 160px;
      height: 120px;
    }
    .news .right img {
      width: 100%;
      height: 100%;
      object-fit: cover;
    }
  </style>
</head>
<body>
  <div id="app">
    <ul>
      <li  v-for="(item, index) in list" :key="item.id" class="news">
        <div class="left">
          <div class="title">{{ item.title }}</div>
          <div class="info">
            <span>{{ item.source }}</span>
            <span>{{ item.time }}</span>
          </div>
        </div>
        <div class="right">
          <img :src="item.img" alt="">
        </div>
      </li>
    </ul>
  </div>
  <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.min.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
  <script>
    // 接口地址:http://hmajax.itheima.net/api/news
    // 请求方式:get
    const app = new Vue({
      el: '#app',
      data: {
        list: []
      },
      async created() {
        // 1. 发送请求,获取数据
        const res = await axios.get('http://hmajax.itheima.net/api/news')
        // 2. 将数据更新给 data 中的 list
        this.list = res.data.data
      }
    })
  </script>
</body>
</html>

例子:在 mounted 阶段获取输入框焦点


<!DOCTYPE html>
<html lang="zh-CN">

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>示例-获取焦点</title>
  <!-- 初始化样式 -->
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/reset.css@2.0.2/reset.min.css">
  <!-- 核心样式 -->
  <style>
    html,
    body {
      height: 100%;
    }
    .search-container {
      position: absolute;
      top: 30%;
      left: 50%;
      transform: translate(-50%, -50%);
      text-align: center;
    }
    .search-container .search-box {
      display: flex;
    }
    .search-container img {
      margin-bottom: 30px;
    }
    .search-container .search-box input {
      width: 512px;
      height: 16px;
      padding: 12px 16px;
      font-size: 16px;
      margin: 0;
      vertical-align: top;
      outline: 0;
      box-shadow: none;
      border-radius: 10px 0 0 10px;
      border: 2px solid #c4c7ce;
      background: #fff;
      color: #222;
      overflow: hidden;
      box-sizing: content-box;
      -webkit-tap-highlight-color: transparent;
    }
    .search-container .search-box button {
      cursor: pointer;
      width: 112px;
      height: 44px;
      line-height: 41px;
      line-height: 42px;
      background-color: #ad2a27;
      border-radius: 0 10px 10px 0;
      font-size: 17px;
      box-shadow: none;
      font-weight: 400;
      border: 0;
      outline: 0;
      letter-spacing: normal;
      color: white;
    }
    body {
      background: no-repeat center /cover;
      background-color: #edf0f5;
    }
  </style>
</head>
<body>
<div class="container" id="app">
  <div class="search-container">
    <img src="https://www.itheima.com/images/logo.png" alt="">
    <div class="search-box">
      <input type="text" v-model="words" id="inp">
      <button>搜索一下</button>
    </div>
  </div>
</div>

<script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.min.js"></script>
<script>
  const app = new Vue({
    el: '#app',
    data: {
      words: ''
    },
    // 1. 等输入框渲染出来
    // 2. 让输入框获取焦点
    mounted() {
      // console.log(document.querySelector('#inp'))
      document.querySelector('#inp').focus()
    }
  })
</script>
</body>
</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>
    <!-- CSS only -->
    <link
      rel="stylesheet"
      href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css"
    />
    <style>
      .red {
        color: red!important;
      }
      .search {
        width: 300px;
        margin: 20px 0;
      }
      .my-form {
        display: flex;
        margin: 20px 0;
      }
      .my-form input {
        flex: 1;
        margin-right: 20px;
      }
      .table > :not(:first-child) {
        border-top: none;
      }
      .contain {
        display: flex;
        padding: 10px;
      }
      .list-box {
        flex: 1;
        padding: 0 30px;
      }
      .list-box  a {
        text-decoration: none;
      }
      .echarts-box {
        width: 600px;
        height: 400px;
        padding: 30px;
        margin: 0 auto;
        border: 1px solid #ccc;
      }
      tfoot {
        font-weight: bold;
      }
      @media screen and (max-width: 1000px) {
        .contain {
          flex-wrap: wrap;
        }
        .list-box {
          width: 100%;
        }
        .echarts-box {
          margin-top: 30px;
        }
      }
    </style>
  </head>
  <body>
    <div id="app">
      <div class="contain">
        <!-- 左侧列表 -->
        <div class="list-box">
          <!-- 添加资产 -->
          <form class="my-form">
            <input v-model.trim="name" type="text" class="form-control" placeholder="消费名称" />
            <input v-model.number="price" type="text" class="form-control" placeholder="消费价格" />
            <button @click="add" type="button" class="btn btn-primary">添加账单</button>
          </form>

          <table class="table table-hover">
            <thead>
              <tr>
                <th>编号</th>
                <th>消费名称</th>
                <th>消费价格</th>
                <th>操作</th>
              </tr>
            </thead>
            <tbody>
              <tr v-for="(item, index) in list" :key="item.id">
                <td>{{ index + 1 }}</td>
                <td>{{ item.name }}</td>
                <td :class="{red:item.price >= 500}">{{ item.price.toFixed(2) }}</td>
                <td><a @click="del(item.id)" href="javascript:;">删除</a></td>
              </tr>
            </tbody>
            <tfoot>
              <tr>
                <td colspan="4">消费总计: {{ totalPrice.toFixed(2) }}</td>
              </tr>
            </tfoot>
          </table>
        </div>        
        <!-- 右侧图表 -->
        <div class="echarts-box" id="main"></div>
      </div>
    </div>
    <script src="https://cdn.jsdelivr.net/npm/echarts@5.5.0/dist/echarts.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
    <script>
      /**
       * 接口文档地址:
       * https://www.apifox.cn/apidoc/shared-24459455-ebb1-4fdc-8df8-0aff8dc317a8/api-53371058
       * 
       * 功能需求:
       * 1. 基本渲染
       *    (1) 立刻发送请求获取数据 created,封装渲染方法
       *    (2) 拿到数据 存到 data 的响应式数据
       *    (3) 结合数据,进行渲染 v-for
       *    (4) 消费统计 => 计算属性
       * 2. 添加功能
       *    (1) 收集表单数据 v-model
       *    (2) 给添加按钮注册点击事件,发送添加请求
       *    (3) 需要重新渲染,调用渲染方法
       * 3. 删除功能
       *    (1) 注册点击事件,传参 id
       *    (2) 根据 id 发送删除请求
       *    (3) 需要重新渲染,调用渲染方法
       * 4. 饼图渲染
       *    (1) 初始化一个饼图 echarts.init(dom) mounted 钩子实现
       *    (2) 根据数据实时更新饼图 echarts.setOption({})
       */
      const app = new Vue({
        el: '#app',
        data: {
          list: [],
          name: '',
          price: ''
        },
        computed: {
          totalPrice() {
            return this.list.reduce((sum, item) => sum + item.price , 0)
          }
        },
        created() {
          /* // (1) 立刻发送请求获取数据 created
          const res = await axios.get('https://applet-base-api-t.itheima.net/bill', {
            params: {
              creator: 'stone'
            }
          })
          // console.log(res)
          this.list = res.data.data */
          this.getList()
        },
        mounted() {
          this.myChatrt = echarts.init(document.querySelector('#main'))
          this.myChatrt.setOption({
            // 大标题
            title: {
              text: '消费账单列表',
              left: 'center'
            },
            // 提示框
            tooltip: {
              trigger: 'item'
            },
            // 图例
            legend: {
              orient: 'vertical',
              left: 'left'
            },
            // 数据项
            series: [
              {
                name: '消费账单',
                type: 'pie',
                radius: '50%',
                data: [],
                emphasis: {
                  itemStyle: {
                    shadowBlur: 10,
                    shadowOffsetX: 0,
                    shadowColor: 'rgba(0, 0, 0, 0.5)'
                  }
                }
              }
            ]
          })
        },
        methods: {
          async getList() {
            const res = await axios.get('https://applet-base-api-t.itheima.net/bill', {
            params: {
              creator: 'stone'
            }
            })
            this.list = res.data.data
            
            // 更新图表
            this.myChatrt.setOption({
              // 数据项
              series: [
                {
                  /* data: [
                    { value: 1048, name: 'Search Engine' }
                  ] */
                  data: this.list.map(item => ({value: item.price, name: item.name}))
                }
              ]
            })
          },
          async add() {
            if(!this.name) {
              alert('请输入消费名称')
              return
            }
            if(typeof this.price != 'number') {
              alert('请输入正确的消费价格')
              return 
            }
            
            // 发送添加请求
            const res = await axios.post('https://applet-base-api-t.itheima.net/bill', {
              creator: 'stone',
              name: this.name,
              price: this.price
            })

            // 重新渲染一次
            this.getList()

            this.name = ''
            this.price = ''
          },
          async del(id) {
            const res = await axios.delete(`https://applet-base-api-t.itheima.net/bill/${id}`)
            // 重新渲染一次
            this.getList()
          }
        }
      })
    </script>
  </body>
</html>

工程化

创建项目

对于正式的项目,一般使用 Vue CLI 脚手架进行工程化开发。Vue CLI 是一个基于 Vue.js 进行快速开发的完整系统,可用于快速创建一个开发 Vue 项目的标准化基础架子。

安装 Vue CLI 后,可以通过 vue create 快速搭建一个新项目,也可以使用 vue ui 图形化界面管理所有项目。

具体步骤如下:

  • 全局安装 @vue/cli
npm i @vue/cli -g

安装完成后查看版本:

vue --version
@vue/cli 5.0.8
  • 在指定目录下创建项目:
vue create vue-demo1

选择 Default ([Vue 2] babel, eslint) 然后回车进行创建。

Vue CLI v5.0.8
? Please pick a preset: Default ([Vue 2] babel, eslint)


Vue CLI v5.0.8
✨  Creating project in D:\code\vue-demo1.
🗃  Initializing git repository...
⚙️  Installing CLI plugins. This might take a while...


added 878 packages in 3m
🚀  Invoking generators...
📦  Installing additional dependencies...


added 94 packages in 26s
⚓  Running completion hooks...

📄  Generating README.md...

🎉  Successfully created project vue-demo1.
👉  Get started with the following commands:

 $ cd vue-demo1
 $ npm run serve

创建完成后,按照最后的提示,切换到项目目录,并启动项目:

$ cd vue-demo1
$ npm run serve

> vue-demo1@0.1.0 serve
> vue-cli-service serve

 INFO  Starting development server...


 DONE  Compiled successfully in 12446ms                                                                         11:31:41


  App running at:
  - Local:   http://localhost:8080/
  - Network: http://192.168.10.2:8080/

  Note that the development build is not optimized.
  To create a production build, run npm run build.

启动后,即可访问输出的地址,可以看到项目创建并启动成功:

image-20240416113400065

项目目录如下:

vue-demo1
├── node_modules            依赖的第三方模块
├── public                  公共文件
│   ├── favicon.ico         HTML 模板文件
│   └── index.html          网站s
├── src                     项目源代码
│   ├── assets              静态资源文件,如图片、字体等
│   ├── components          Vue 组件
│   ├── App.vue             根组件
│   └── main.js             项目入口文件
├── .gitignore              Git 忽略文件配置
├── babel.config.js         Babel 配置文件
├── jsconfig.json           JS 配置文件
├── package.json            项目配置和依赖
├── package-lock.json       依赖版本锁定文件
├── README.md               项目说明文件
└── vue.config.js           Vue CLI 配置文件

对于项目入口文件 main.js,用于导入 App.vue 根组件,基于 App.vue 创建结构渲染 index.html

// 1. 导入 Vue 核心包
import Vue from 'vue'
// 2. 导入 App.vue 根组件
import App from './App.vue'

// 3. 提示:当前处于什么环境(生产环境/开发环境)
Vue.config.productionTip = false

// 4. Vue 实例化,提供 render 方法,基于 App.vue 创建结构渲染到 index.html 容器中
new Vue({
  // el: '#app', 作用:和 $mount('#app') 作用一致,将这个 Vue 实例挂载到 HTML 模板中的 <div id="app"></div> 元素上。
  // render 函数指定了根组件为 App,h 是 createElement 的简写,用于创建虚拟 DOM 节点
  render: h => h(App),
}).$mount('#app')

对于根组件文件 App.vue,是应用最上层的组件,是所有其他组件的容器,并可能包含一些全局的样式和脚本:

<template>
  <div id="app">
    <img alt="Vue logo" src="./assets/logo.png">
    <HelloWorld msg="Welcome to Your Vue.js App"/>
  </div>
</template>

<script>
import HelloWorld from './components/HelloWorld.vue'

export default {
  name: 'App',
  components: {
    HelloWorld
  }
}
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>

自定义创建项目

在前面创建项目时,选择了 Default ([Vue 2] babel, eslint) 默认配置。但在实际开发中,往往会自定义创建项目,例如:

  • 在指定目录下创建项目:
vue create vue-demo2

选择 Manually select features 然后回车进行创建:

? Please pick a preset:
  Default ([Vue 3] babel, eslint)
  Default ([Vue 2] babel, eslint)
> Manually select features

根据需求,选择相应模块:

? Check the features needed for your project: (Press <space> to select, <a> to toggle all, <i> to invert selection, and
<enter> to proceed)
 (*) Babel
 ( ) TypeScript
 ( ) Progressive Web App (PWA) Support
 (*) Router
 (*) Vuex
>(*) CSS Pre-processors
 (*) Linter / Formatter
 ( ) Unit Testing
 ( ) E2E Testing

选择 Vue 版本:

? Choose a version of Vue.js that you want to start the project with
  3.x
> 2.x

暂不为路由采用历史模式:

? Use history mode for router? (Requires proper server setup for index fallback in production) (Y/n) n

选择 Less 作为 CSS 预处理器:

? Pick a CSS pre-processor (PostCSS, Autoprefixer and CSS Modules are supported by default):
  Sass/SCSS (with dart-sass)
> Less
  Stylus

选择 ESLint + Standard config 作为 Linter 的标准:

? Pick a linter / formatter config:
  ESLint with error prevention only
  ESLint + Airbnb config
> ESLint + Standard config
  ESLint + Prettier

在保存时进行代码格式校验:

? Pick additional lint features: (Press <space> to select, <a> to toggle all, <i> to invert selection, and <enter> to
proceed)
>(*) Lint on save
 ( ) Lint and fix on commit

将配置放在单独的文件中:

? Where do you prefer placing config for Babel, ESLint, etc.? (Use arrow keys)
> In dedicated config files
  In package.json

是否需要保存上面的配置:

? Save this as a preset for future projects? (y/N) n

开始创建项目:

Vue CLI v5.0.8
✨  Creating project in D:\code\vue-demo2.
🗃  Initializing git repository...
⚙️  Installing CLI plugins. This might take a while...


added 878 packages in 4m
🚀  Invoking generators...
📦  Installing additional dependencies...


added 192 packages in 1m
⚓  Running completion hooks...

📄  Generating README.md...

🎉  Successfully created project vue-demo2.
👉  Get started with the following commands:

 $ cd vue-demo2
 $ npm run serve

项目中选择了 Standard config 即 JavaScript Standard Style 作为代码规范,这个规范的目标是提供一个简单、一致的代码风格,以减少在代码库中的不一致性和混乱。它遵循严格的错误检查规则,以捕捉常见的 JavaScript 错误。

以下是一些 JavaScript Standard Style 的主要规则和特点:

  • 使用两个空格进行缩进:而不是使用制表符或四个空格。
  • 使用单引号:字符串应使用单引号,而不是双引号。
  • 不使用分号:JavaScript Standard Style 遵循 ASCII Art 规则,即在语句末尾省略分号。这可以减少错误,并使代码更简洁。
  • 函数声明后应有空格:例如,function name (arg) { ... } 而不是 function name(arg) { ... }
  • 变量和函数名使用驼峰命名法:例如 myVariableNamemyFunctionName
  • 始终使用 ===!== 而不是 ==!=:以避免类型强制转换带来的潜在问题。
  • 对象字面量中的键和字符串值之间应有空格:例如 { key: 'value' } 而不是 {key:'value'}
  • 数组和对象末尾不应有逗号:这是为了兼容旧版本的 JavaScript 引擎。
  • 使用模板字符串进行多行字符串:而不是使用字符串连接或 \ 进行转义。
  • 避免使用 var,优先使用 constlet:以提供更清晰的变量作用域和避免变量提升问题。

要使用 JavaScript Standard Style,可以使用 ESLint 插件,该插件会检查代码是否符合这些规范open in new window。在项目中安装并配置 ESLint 和相关插件后,可以在运行代码或提交代码到版本控制系统时自动检查代码风格问题。

    "editor.formatOnSave": false,
    "editor.codeActionsOnSave": {
      "source.fixAll": true
    },

组件化开发

在 Vue 项目中,采用组件化开发将复杂的用户界面拆分成更小、更易于管理和重用的组件。每个组件都负责渲染页面上的特定部分,并且具有自己的状态和行为。通过组件化开发,可以提高代码的可读性、可维护性和可重用性。

Vue 提供了单文件组件的开发模式,使得组件的结构、逻辑和样式可以在一个文件中组织起来,从上面的根组件文件 App.vue 可以看出,一个 .vue 文件通常包含三个部分:

  • <template>:组件的 HTML 结构,有且只能有一个根元素
  • <script>:组件的 JavaScript 逻辑
  • <style>:组件的样式,默认全局样式,通常加上 scoped 属性让样式只作用于当前组件

一个简单的 .vue 组件文件示例:

<template>  
  <div class="my-component">  
    <h2>{{ title }}</h2>  
    <button @click="incrementCounter">点击增加</button>  
    <p>已经点击了 {{ counter }} 次</p>  
  </div>  
</template>  
  
<script>  
export default {  
  name: 'MyComponent',  
  data() {  
    return {  
      title: '我的组件',  
      counter: 0  
    };  
  },  
  methods: {  
    incrementCounter() {  
      this.counter++;  
    }  
  }  
};  
</script>  
  
<style scoped>  
.my-component {  
  border: 1px solid #ddd;  
  padding: 1em;  
  margin-bottom: 1em;  
}  
  
.my-component button {  
  margin-top: 0.5em;  
}  
</style>

其中 <style scoped> 中的 scoped 属性确保了这些样式只应用于当前组件,不会影响到其他组件或全局样式。原理是给当前组件的所有元素都添加一个自定义属性 data-v-hash值 以区别其他组件,CSS 选择器都会被添加 [data-v-hash值] 属性选择器。

如果需要让样式支持 LESS,需要先使用 npm i less less-loader -D 安装 LESS,并配置 <style lang="less"> 以支持 LESS。

对于 <script> 中的 data 选项,必须是一个函数,以保证每个组件实例维护独立的一份数据对象。每次创建新的组件实例都会重新执行一次 data 函数,得到一个新对象。

<template>
  <div class="base-count">
    <button @click="count--">-</button>
    <span>{{ count }}</span>
    <button @click="count++">+</button>
  </div>
</template>

<script>
export default {
  data() {
    // data必须是一个函数 -> 保证每个组件实例,维护独立的一个数据对象
    return {
      count: 100,
    };
  },
};
</script>

<style>
.base-count {
  margin: 20px;
}
</style>

组件注册

在 Vue 项目中,组件注册是使组件能够在其他组件或实例的模板中使用的关键步骤。Vue 提供了全局注册和局部注册两种方式,一般使用局部注册,如果发现确实是通用组件,在抽离到全局。

局部注册

局部注册的组件只能在注册它的父组件的模板中使用。

src/components 目录下创建组件,组件使用大驼峰命名:

HmHeader.vue 组件:

<template>
  <div class="header">我是header</div>
</template>

<script>
export default {};
</script>

<style>
.header {
  height: 100px;
  line-height: 100px;
  text-align: center;
  font-size: 30px;
  background-color: #8064a2;
  color: white;
}
</style>

HmMain.vue 组件:

<template>
  <div class="main">我是main</div>
</template>

<script>
export default {};
</script>

<style>
.main {
  height: 400px;
  line-height: 400px;
  text-align: center;
  font-size: 30px;
  background-color: #f79646;
  color: white;
  margin: 20px 0;
}
</style>

HmFooter.vue 组件:

<template>
  <div class="footer">我是footer</div>
</template>

<script>
export default {};
</script>

<style>
.footer {
  height: 100px;
  line-height: 100px;
  text-align: center;
  font-size: 30px;
  background-color: #4f81bd;
  color: white;
}
</style>

src/App.vue 引入组件并注册组件,然后就可以像使用 HTML 标签一样使用组件:

<template>
  <div class="App">
    <!-- 头部组件 -->
    <HmHeader></HmHeader>
    <!-- 主体组件 -->
    <HmMain></HmMain>
    <!-- 底部组件 -->
    <HmFooter></HmFooter>
  </div>
</template>

<script>
import HmHeader from "./components/HmHeader.vue";
import HmMain from "./components/HmMain.vue";
import HmFooter from "./components/HmFooter.vue";
export default {
  components: {
    // 组件名: 组件对象,同名可以省略组件对象
    HmHeader: HmHeader,
    HmMain,
    HmFooter,
  },
};
</script>

<style>
.App {
  width: 600px;
  height: 700px;
  background-color: #87ceeb;
  margin: 0 auto;
  padding: 20px;
}
</style>

如果在使用组件时,无法使用 TAB 键进行补全,则需要在 VS Code 的设置中勾选 Trigger Expansion On Tab

全局注册

全局注册的组件可以在任何 Vue 实例的模板中使用。通常在项目的入口文件 main.js 中进行全局注册。

src/components 目录下创建组件 HmButton.vue

<template>
  <button class="btn">通用按钮</button>
</template>

<script>
export default {};
</script>

<style>
.btn {
  height: 50px;
  line-height: 50px;
  padding: 0 20px;
  background-color: #3bae56;
  border-radius: 10px;
  border: none;
  color: white;
  vertical-align: middle;
  cursor: pointer;
}
</style>

main.js 引入组件并注册组件:

import Vue from 'vue'
import App from './App.vue'
// 全局导入组件
import HmButton from './components/HmButton.vue'
// 全局注册组件,语法:Vue.component('组件名',组件对象)
Vue.component('HmButton', HmButton)

Vue.config.productionTip = false

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

然后就可以在项目的任何 Vue 模板中使用该组件。

动态组件

Vue 还提供了动态组件的功能,允许在同一个挂载点动态切换多个组件。这可以通过 <component> 标签和 is 属性来实现。

<template>  
  <div>  
    <component :is="currentComponent"></component>  
  </div>  
</template>  
  
<script>  
import Vue from 'vue';  
import MyComponent from './components/MyComponent.vue';  
import AnotherComponent from './components/AnotherComponent.vue';  
  
export default {  
  data() {  
    return {  
      currentComponent: 'my-component'  
    };  
  },  
  components: {  
    'my-component': MyComponent,  
    'another-component': AnotherComponent  
  }  
};  
</script>

在上面的代码中,currentComponent 数据属性用于控制当前显示的组件。可以通过修改 currentComponent 的值来动态切换组件。

组件通信

Vue 组件通信是指如何在组件之间传递数据和消息。

组件的关系及对应的组件通信解决方案:

  • 父子关系:

    • props$emit
  • 非父子关系:

    • provideinject
    • eventbus
  • 通用方案:

    • vuex

父子通信

父组件通过自定义属性向下传递数据给子组件的 props,子组件通过 $emit 向父组件发送消息触发父组件调用方法修改父组件中的数据。即单向数据流,子组件的 props 是只读的,在子组件内部不能修改父组件的数据,谁的数据谁负责。

父组件:

<template>
  <div class="app" style="border: 3px solid #000; margin: 10px">
    我是APP组件
    <!-- 父传子1. 给组件标签,添加属性方式 传值 -->
    <!-- 子传父2. 父组件,对消息进行监听 -->
    <Son :title="myTitle" @changeTitle="handleChange"></Son>
  </div>
</template>

<script>
import Son from "./components/Son.vue";
export default {
  name: "App",
  components: {
    Son,
  },
  data() {
    return {
      myTitle: "Hello, Vue",
    };
  },
  methods: {
    // 子传父3. 提供处理函数,提供逻辑
    handleChange(newTitle) {
      // console.log(newTitle);
      this.myTitle = newTitle;
    },
  },
};
</script>

<style>
</style>

子组件:

<template>
  <div class="son" style="border: 3px solid #000; margin: 10px">
    <!-- 父传子3. 渲染使用 -->
    我是Son组件 {{ title }}
    <button @click="changeFn">修改title</button>
  </div>
</template>

<script>
export default {
  name: "Son-Child",
  // 父传子2. 通过 props 来接收
  props: ["title"],
  methods: {
    changeFn() {
      // 子传父1. 通过 $emit 向父组件发送消息通知
      this.$emit("changeTitle", "stone");
    },
  },
};
</script>

<style>
</style>

父传子步骤:

  • 在父组件中给子组件标签添加属性传值,属性名自定义(示例中为 title),属性值为需要传递给子组件的值(示例中为 myTitle 的值)。
  • 子组件使用 props 接收父组件中自定义的属性名。
  • 在模块中使用该属性。

子传父步骤:

  • 在子组件中定义方法,在方法中通过 $emit 向父组件发送消息通知。
  • 在父组件中的子组件标签上绑定事件,事件名为 $emit 的第一个参数(示例中为 changeTitle),并指定处理函数(示例中为 handleChange)。
  • 父组件提供处理函数,接收子组件传递过来的数据,数据为 $emit 的第二个参数(示例中为 stone)。

为了确保组件接收到的 props 数据是符合预期的,Vue 允许在组件定义中指定 props 的期望类型、默认值或其他验证规则。当父组件向子组件传递 props 时,Vue 会根据这些规则进行校验。

语法:

  props: {
    校验的属性名:类型,
    校验的属性名: {  
      type: 类型,  
      required: true, 
      default: 默认值
      validator (value) {
        // 自定义校验逻辑
        return 是否通过校验;  
      }  
    }  
  } 

其中:

  • type:指定类型。
  • required:表示是否必须传递该属性。
  • default:指定默认值。
  • validator:是一个自定义的校验函数,如果校验失败,Vue 会在控制台输出警告信息。
<template>  
  <div>  
    <p>{{ message }}</p>  
  </div>  
</template>  
  
<script>  
export default {  
  name: 'MyComponent',  
  props: {  
    // 基础类型检查 (`null` 和 `undefined` 会通过任何类型验证)  
    propA: Number,  
      
    // 多种类型  
    propB: [String, Number],  
      
    // 必传且是字符串  
    propC: {  
      type: String,  
      required: true  
    },  
      
    // 数字,有默认值  
    propD: {  
      type: Number,  
      default: 100  
    },  
      
    // 自定义验证函数  
    propE: {  
      validator (value) {  
        // 这个值必须匹配下列字符串中的一个  
        return ['success', 'warning', 'danger'].indexOf(value) !== -1  
      }  
    }  
  }  
}  
</script>

例子:小黑记事本组件版

父组件:

<template>
  <!-- 主体区域 -->
  <section id="app">
    <TodoHeader @add="handleAdd"></TodoHeader>
    <TodoMain @del="handleDel" :list="list"></TodoMain>
    <TodoFooter @clear="headleClear" :list="list"></TodoFooter>
  </section>
</template>

<script>
import TodoHeader from "./components/TodoHeader.vue";
import TodoMain from "./components/TodoMain.vue";
import TodoFooter from "./components/TodoFooter.vue";

// 渲染功能:
// 1. 提供数据 -> 提供在公共的父组件 App.vue
// 2. 通过父传子,将数据传递给 ToMain
// 3. 子组件利用 v-for 渲染

// 添加功能:
// 1. 收集表单数据 -> v-model
// 2. 监听事件(回车 + 点击 都要进行添加)
// 3. 子传父,将任务名称传递给父组件 App.vue
// 4. 父组件进行添加 unshift (自己的数据自己负责)

// 删除功能:
// 1. 监听事件(监听删除的点击) 携带 id
// 2. 子传父,将删除的 id 传递给父组件 App.vue
// 3. 父组件进行删除 filter(自己的数据自己负责)

// 底部合计:父传子 list -> 渲染

// 清空功能:子传父, 通知到父组件 -> 父组件进行清空操作

// 持久化存储:Watch 深度监视 list 的变化 -> 往本地存储 -> 一进入页面优先读取本地

export default {
  components: {
    TodoHeader,
    TodoMain,
    TodoFooter,
  },
  data() {
    return {
      list: JSON.parse(localStorage.getItem("list")) || [
        { id: 1, name: "打篮球" },
        { id: 2, name: "看电影" },
        { id: 3, name: "逛街" },
      ],
    };
  },
  methods: {
    handleAdd(todoName) {
      this.list.unshift({
        id: +new Date(),
        name: todoName,
      });
    },
    handleDel(id) {
      this.list = this.list.filter((item) => item.id !== id);
    },
    headleClear() {
      this.list = [];
    },
  },
  watch: {
    list: {
      deep: true,
      handler(newValue) {
        localStorage.setItem("list", JSON.stringify(newValue));
      },
    },
  },
};
</script>

<style>
</style>

头部子组件:

<template>
  <!-- 输入框 -->
  <header class="header">
    <h1>小黑记事本</h1>
    <input
      @keyup.enter="handleAdd"
      v-model="todoName"
      placeholder="请输入任务"
      class="new-todo"
    />
    <button @click="handleAdd" class="add">添加任务</button>
  </header>
</template>

<script>
export default {
  data() {
    return {
      todoName: "",
    };
  },
  methods: {
    handleAdd() {
      if (this.todoName.trim() === "") {
        alert("任务名不能为空");
        return;
      }
      this.$emit("add", this.todoName);
      this.todoName = "";
    },
  },
};
</script>

<style>
</style>

主体子组件:

<template>
  <!-- 列表区域 -->
  <section class="main">
    <ul class="todo-list">
      <li v-for="(item, index) in list" :key="item.id" class="todo">
        <div class="view">
          <span class="index">{{ index + 1 }}.</span>
          <label>{{ item.name }}</label>
          <button @click="handleDel(item.id)" class="destroy"></button>
        </div>
      </li>
    </ul>
  </section>
</template>

<script>
export default {
  props: {
    list: Array,
  },
  methods: {
    handleDel(id) {
      this.$emit("del", id);
    },
  },
};
</script>

<style>
</style>

底部子组件:

<template>
  <!-- 统计和清空 -->
  <footer class="footer">
    <!-- 统计 -->
    <span class="todo-count">
      合 计:<strong> {{ list.length }} </strong>
    </span>
    <!-- 清空 -->
    <button @click="clear" class="clear-completed">清空任务</button>
  </footer>
</template>

<script>
export default {
  props: {
    list: Array,
  },
  methods: {
    clear() {
      this.$emit("clear");
    },
  },
};
</script>

<style>
</style>

非父子通信

Event Bus

可以使用事件总线(Event Bus)实现非父子组件之间的通信。事件总线是一个创建在 Vue 实例上的全局事件监听器,允许组件通过 $emit$on$off 方法触发、监听和取消监听全局事件。这使得任何组件都可以发送和接收这些事件,从而实现跨组件通信。

  1. 创建事件总线。在 utils/EventBus.js 创建一个新的 Vue 实例作为事件总线。
// 1. 创建一个都能访问到的事件总线(空的 Vue 实例)
import Vue from 'vue'

const Bus  =  new Vue()

export default Bus
  1. 发送组件触发事件
<template>
  <div class="base-b">
    <div>我是B组件(发布方)</div>
    <button @click="sendMsgFn">发送消息</button>
  </div>
</template>

<script>
import Bus from '../utils/EventBus'
export default {
  methods: {
    // 3. B 组件(发送方)触发事件的方式传递参数(发布消息)
    sendMsgFn() {
      Bus.$emit('sendMsg', '今天天气不错,适合旅游')
    },
  },
}
</script>

<style scoped>
.base-b {
  width: 200px;
  height: 200px;
  border: 3px solid #000;
  border-radius: 3px;
  margin: 10px;
}
</style>
  1. 接收组件监听事件
<template>
  <div class="base-a">
    我是A组件(接受方)
    <p>{{msg}}</p>  
  </div>
</template>

<script>
import Bus from '../utils/EventBus'
export default {
  data() {
    return {
      msg: '',
    }
  },
  created() {
    // 2. 在 A 组件(接收方),进行监听 Bus 的事件(订阅消息)
    Bus.$on('sendMsg', (msg) => { 
      // console.log(msg)
      this.msg = msg
    })
  },
}
</script>

<style scoped>
.base-a {
  width: 200px;
  height: 200px;
  border: 3px solid #000;
  border-radius: 3px;
  margin: 10px;
}
</style>
<template>
  <div class="base-c">
    我是C组件(接受方)
    <p>{{msg}}</p>  
  </div>
</template>

<script>
import Bus from '../utils/EventBus'
export default {
  data() {
    return {
      msg: '',
    }
  },
  created() {
    Bus.$on('sendMsg', (msg) => {
      // console.log(msg)
      this.msg = msg
    })
  },
}
</script>

<style scoped>
.base-c {
  width: 200px;
  height: 200px;
  border: 3px solid #000;
  border-radius: 3px;
  margin: 10px;
}
</style>

根组件:

<template>
  <div class="app">
    <BaseA></BaseA>
    <BaseB></BaseB>
    <BaseC></BaseC>
  </div>
</template>

<script>
import BaseA from './components/BaseA.vue'
import BaseB from './components/BaseB.vue'
import BaseC from './components/BaseC.vue'
export default {
  components:{
    BaseA,
    BaseB,
    BaseC
  }
}
</script>

<style>

</style>

Provide Inject

Provide/Inject 是 Vue 提供的一种依赖注入机制,允许祖先组件向其所有子孙组件提供一个依赖。祖先组件通过 provide 选项来声明要提供的数据或方法,子孙组件则可以通过 inject 选项来接收这些数据或方法。这种方式适用于跨越多层嵌套的组件通信,可以简化组件间的依赖关系。

根组件:

<template>
  <div class="app">
    我是APP组件
    <button @click="change">修改数据</button>
    <SonA></SonA>
    <SonB></SonB>
  </div>
</template>

<script>
import SonA from "./components/SonA.vue";
import SonB from "./components/SonB.vue";
export default {
  provide() {
    return {
      // 简单类型 是非响应式的
      color: this.color,
      // 复杂类型 是响应式的,推荐使用
      userInfo: this.userInfo,
    };
  },
  data() {
    return {
      color: "pink", //简单类型
      userInfo: {
        // 复杂类型
        name: "zs",
        age: 18,
      },
    };
  },
  methods: {
    change() {
      this.color = "red";
      this.userInfo.name = "ls";
    },
  },
  components: {
    SonA,
    SonB,
  },
};
</script>

<style>
.app {
  border: 3px solid #000;
  border-radius: 6px;
  margin: 10px;
}
</style>

子组件:

<template>
  <div class="SonA">
    我是SonA组件
    <GrandSon></GrandSon>
  </div>
</template>

<script>
import GrandSon from "../components/GrandSon.vue";
export default {
  components: {
    GrandSon,
  },
};
</script>

<style>
.SonA {
  border: 3px solid #000;
  border-radius: 6px;
  margin: 10px;
  height: 200px;
}
</style>

孙子组件:

<template>
  <div class="grandSon">
    我是GrandSon
    {{ color }} -{{ userInfo.name }} -{{ userInfo.age }}
  </div>
</template>

<script>
export default {
  inject: ["color", "userInfo"],
};
</script>

<style>
.grandSon {
  border: 3px solid #000;
  border-radius: 6px;
  margin: 10px;
  height: 100px;
}
</style>

表单封装

从前面 v-model 的原理可知,v-model 本质上是一个语法糖,就是对应的属性(例如 value)与事件(例如 input)的合写:

<template>
  <div>
    <Input v-model="msg1" type="text"></Input>
    <!-- 模板中获取事件的形参 -> $event 获取 -->
    <Input :value="msg2" @input="msg2 = $event.target.value" type="text"></Input>
  </div>
</template>

如果在表单子组件中,在 props 中使用 value 接收父组件数据,在方法中指定传递的事件名为 input,则在父组件中就可以使用 v-model 给表单子组件标签直接绑定数据,以实现表单子组件与父组件的双向绑定。

注意:

不能在子组件的表单上使用 v-model 绑定来自于父组件的数据。因为子组件不能直接修改父组件的数据。

父组件:

<template>
  <div class="app">
    <!-- v-model => :value + @input -->
    <!-- <BaseSelect :value="selectId" @input="selectId = $event"></BaseSelect> -->
    <BaseSelect v-model="selectId"></BaseSelect>
  </div>
</template>

<script>
import BaseSelect from "./components/BaseSelect.vue";
export default {
  data() {
    return {
      selectId: "102",
    };
  },
  components: {
    BaseSelect,
  },
};
</script>

<style>
</style>

子组件:

<template>
  <div>
    <select :value="value" @change="handleChange">
      <option value="101">北京</option>
      <option value="102">上海</option>
      <option value="103">武汉</option>
      <option value="104">广州</option>
      <option value="105">深圳</option>
    </select>
  </div>
</template>

<script>
export default {
  props: {
    value: String,
  },
  methods: {
    handleChange(e) {
      this.$emit("input", e.target.value);
    },
  },
};
</script>

<style>
</style>

在上面的例子中,子组件必须要使用名称为 value 的属性名来接收父组件的数据,可以使用 .sync 修饰父组件中子组件标签的自定义属性,这样在子组件的 props 就不必使用名称为 value 的属性名来接收父组件的数据,而是使用自定义的属性来接收父组件的数据,以实现子组件和父组件的双向数据绑定。

.sync 修饰本质就是 :属性名@update:属性名 的合写:

<template>
  <div>
    <BaseDialog :visible.sync="isShow"></BaseDialog>
    <!-- <BaseDialog :visible="isShow" @update:visible="isShow = $event"></BaseDialog> -->
  </div>
</template>

假设有一个父组件和一个子组件,子组件接受一个名为 visible 属性,并且想让父组件能够响应 visible 的变化。

父组件:

<template>
  <div class="app">
    <button @click="isShow = true">退出按钮</button>
    <BaseDialog :visible.sync="isShow"></BaseDialog>
    <!-- <BaseDialog :visible="isShow" @update:visible="isShow = $event"></BaseDialog> -->
  </div>
</template>

<script>
import BaseDialog from "./components/BaseDialog.vue";
export default {
  data() {
    return {
      isShow: false,
    };
  },
  methods: {},
  components: {
    BaseDialog,
  },
};
</script>

<style>
</style>

子组件:

<template>
  <div v-show="visible" class="base-dialog-wrap">
    <div class="base-dialog">
      <div class="title">
        <h3>温馨提示:</h3>
        <button @click="close" class="close">x</button>
      </div>
      <div class="content">
        <p>你确认要退出本系统么?</p>
      </div>
      <div class="footer">
        <button>确认</button>
        <button @click="close">取消</button>
      </div>
    </div>
  </div>
</template>

<script>
export default {
  props: {
    visible: Boolean
  },
  methods: {
    close() {
      this.$emit('update:visible', false)
    }
  }
}
</script>

<style scoped>
.base-dialog-wrap {
  width: 300px;
  height: 200px;
  box-shadow: 2px 2px 2px 2px #ccc;
  position: fixed;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  padding: 0 10px;
}
.base-dialog .title {
  display: flex;
  justify-content: space-between;
  align-items: center;
  border-bottom: 2px solid #000;
}
.base-dialog .content {
  margin-top: 38px;
}
.base-dialog .title .close {
  width: 20px;
  height: 20px;
  cursor: pointer;
  line-height: 10px;
}
.footer {
  display: flex;
  justify-content: flex-end;
  margin-top: 26px;
}
.footer button {
  width: 80px;
  height: 40px;
}
.footer button:nth-child(1) {
  margin-right: 10px;
  cursor: pointer;
}
</style>

在上面的例子中,子组件使用 visible 来接收父组件传递的数据。当点击按钮后,close 方法会被调用,并发出一个名为 update:visible 的事件,携带新的值。父组件使用 .sync 修饰符来监听这个事件,并自动更新 isShow 的值。这样,父组件和子组件之间的 visible 就实现了双向绑定。

ref 和 $refs

ref 是一个特殊的属性,可以附加到任何元素或子组件上。当元素或组件被渲染时,ref 会被注册为一个引用信息。引用信息将会注册在父组件的 $refs 对象上。如果在普通的 DOM 元素上使用,引用指向的就是 DOM 元素;如果用在子组件上,引用就指向组件实例。

$refs 是一个对象,用于存储注册过 ref 特性的所有 DOM 元素和子组件实例。这些 $refs 只在组件渲染完成后填充,并且它们是响应式的,但不会被 Vue 的响应式系统追踪依赖。它们应当仅作为直接的子组件的访问方式,而不应用于任何形式的响应式状态更新。

ref$refs 的常见使用场景包括:

  • 需要在父组件中直接操作子组件的方法或属性。
  • 需要直接访问和操作 DOM 元素,例如设置焦点、获取输入值等。

ref$refs 的查找范围为当前组件内,相比 querySeletor 在整个页面范围进行查找,更精确稳定。

在 Vue 中,使用 ref$refs 的基本步骤如下:

  1. 在模板中为元素或组件设置 ref:可以为任何元素或组件添加一个 ref 属性,为其指定一个唯一的名称。这个名称之后将用于在 Vue 实例中通过 $refs 访问该元素或组件。
<template>  
  <div>  
    <!-- 为普通 DOM 元素设置 ref -->  
    <input ref="myInput" type="text" />  
  
    <!-- 为子组件设置 ref -->  
    <MyComponent ref="myComponentInstance" />  
  </div>  
</template>
  1. 在 Vue 实例中通过 $refs 访问元素或组件:一旦元素或组件被渲染,就可以通过 this.$refs 在 Vue 实例的方法或计算属性中访问它们。注意,$refs 只在组件渲染完成后被填充,并且它不会触发视图的更新。
<script>  
import MyComponent from './MyComponent.vue';  
  
export default {  
  components: {  
    MyComponent  
  },  
  mounted() {  
    // 访问 DOM 元素  
    const inputElement = this.$refs.myInput;  
    inputElement.focus(); // 例如,将焦点设置到输入框  
  
    // 访问子组件实例  
    const componentInstance = this.$refs.myComponentInstance;  
    componentInstance.someMethod(); // 调用子组件的方法  
  }  
}  
</script>

注意:

  • $refs 只在组件渲染完成后才可用,因此只能在生命周期钩子(如 mountedupdated)或方法中访问它。
  • $refs 不是响应式的,也就是说,它不会触发视图的更新。因此,不应该在模板或计算属性中直接使用它。
  • 不要在 $refs 上进行任何形式的响应式操作,它只应该被用来直接访问 DOM 元素或子组件实例。
  • 过度依赖 $refs 可能会使代码难以测试和维护。在可能的情况下,优先考虑使用 propsevents 来实现父子组件间的通信。

例子:获取 DOM

子组件:

<template>
  <div ref="mychart" class="base-chart-box">子组件</div>
</template>

<script>
// 安装 echarts 包:npm install echarts
import * as echarts from "echarts";

export default {
  mounted() {
    // 基于准备好的 DOM,初始化 echarts 实例
    const myChart = echarts.init(this.$refs.mychart);
    // 绘制图表
    myChart.setOption({
      title: {
        text: "ECharts 入门示例",
      },
      tooltip: {},
      xAxis: {
        data: ["衬衫", "羊毛衫", "雪纺衫", "裤子", "高跟鞋", "袜子"],
      },
      yAxis: {},
      series: [
        {
          name: "销量",
          type: "bar",
          data: [5, 20, 36, 10, 10, 20],
        },
      ],
    });
  },
};
</script>

<style scoped>
.base-chart-box {
  width: 400px;
  height: 300px;
  border: 3px solid #000;
  border-radius: 6px;
}
</style>

父组件:

<template>
  <div class="app">
    <BaseChart></BaseChart>
  </div>
</template>

<script>
import BaseChart from "./components/BaseChart.vue";
export default {
  components: {
    BaseChart,
  },
};
</script>

<style>
.base-chart-box {
  width: 200px;
  height: 100px;
}
</style>

例子:获取组件实例

子组件:

<template>
  <div class="app">
    <div>
      账号: <input v-model="username" type="text">
    </div>
     <div>
      密码: <input v-model="password" type="password">
    </div>
    <div>
      
    </div>
  </div>
</template>

<script>
export default {
  data() {
    return {
      username: '',
      password: '',
    }
  },
  methods: {
    getFormData() {
      return {
        username: this.username,
        password: this.password,
      }
    },
    resetFormData() {
      this.username = ''
      this.password = ''
      console.log('重置表单数据成功');
    },
  }
}
</script>

<style scoped>
.app {
  border: 2px solid #ccc;
  padding: 10px;
}
.app div{
  margin: 10px 0;
}
.app div button{
  margin-right: 8px;
}
</style>

父组件:

<template>
  <div class="app">
    <BaseForm ref="baseForm"></BaseForm>
    <button @click="haandleGet">获取数据</button>
    <button @click="handleReset">重置数据</button>
  </div>
</template>

<script>
import BaseForm from "./components/BaseForm.vue";
export default {
  components: {
    BaseForm,
  },
  methods: {
    haandleGet() {
      console.log(this.$refs.baseForm.getFormData());
    },
    handleReset() {
      this.$refs.baseForm.resetFormData();
    },
  },
};
</script>

<style>
</style>

$nextTick

Vue 的 $nextTick 方法是一个重要的工具,用于在 DOM 更新完成后执行延迟回调。当需要等待 Vue 完成其当前的 DOM 更新周期后执行代码时,可以使用这个方法。

Vue 的异步更新队列意味着当修改数据后,视图不会立即更新。Vue 会等到当前事件循环结束后,再执行视图更新。这样可以提高性能,因为如果有多个数据变动,Vue 只会执行一次 DOM 更新。但是,在某些情况下,可能需要在视图更新完成后立即执行某些操作。这就是 $nextTick 的用武之地。

使用场景:

  • 需要在视图更新后获取某个元素的尺寸或位置。
  • 需要在视图更新后手动设置焦点到某个元素。
  • 需要在视图更新后执行某些依赖于最终 DOM 状态的操作。

$nextTick 接受一个回调函数作为参数,并在 DOM 更新完成后执行该回调。

语法:

this.$nextTick(function () {  
  // 这里可以访问更新后的 DOM  
  console.log('DOM updated');  
});

在 Vue 组件的方法或生命周期钩子中,可以使用 $nextTick

<template>  
  <div ref="myDiv">Hello Vue</div>  
</template>  
  
<script>  
export default {  
  mounted() {  
    this.myDivWidth = this.$refs.myDiv.offsetWidth;  
      
    // 由于 Vue 的异步更新队列,此时 myDiv 可能还未渲染完成  
    // 使用 $nextTick 来确保获取到的是更新后的宽度  
    this.$nextTick(() => {  
      this.myDivWidthAfterUpdate = this.$refs.myDiv.offsetWidth;  
      console.log('Width after update:', this.myDivWidthAfterUpdate);  
    });  
  }  
}  
</script>

注意:

  • $nextTick 的回调会在 DOM 更新后同步执行,所以如果在其中执行了改变状态的操作,这些改变不会触发新的 DOM 更新周期。
  • $nextTick 主要用于确保对 DOM 的访问或操作是在 Vue 完成其 DOM 更新之后进行的。
  • 不要在 $nextTick 的回调中执行耗时操作或大量计算,因为这可能会阻塞浏览器并导致不良的用户体验。

例子:显示输入框后并让输入框获取焦点

<template>
  <div class="app">
    <div v-if="isShowEdit">
      <input type="text" v-model="editValue" ref="inp" />
      <button>确认</button>
    </div>
    <div v-else>
      <span>{{ title }}</span>
      <button @click="handleEdit">编辑</button>
    </div>
  </div>
</template>

<script>
export default {
  data() {
    return {
      title: "大标题",
      isShowEdit: false,
      editValue: "",
    };
  },
  methods: {
    handleEdit() {
      //  显示输入框(异步 DOM 更新)
      this.isShowEdit = true;
      //  让输入框获取焦点($nextTick 等 DOM 更新完,立刻执行准备的函数体)
      this.$nextTick(() => {
        this.$refs.inp.focus();
      });
    },
  },
};
</script>

<style>
</style>

自定义指令

Vue 自定义指令是自己定义的指令。通过自定义指令,可以封装一些 DOM 操作,从而扩展额外的功能。

语法:

  • 全局注册:在 main.js 或其他全局入口文件中使用 Vue.directive 方法进行注册。
Vue.directive('指令名', {  
  // 钩子函数  
  bind(el, binding, vnode) {  
    // 指令第一次绑定到元素时调用  
    // 在这里可以进行一次性的初始化设置  
  },  
  inserted(el) {  
    // 被绑定元素插入父节点时调用  
    // 例如,聚焦元素  
    el.focus();  
  },  
  update(el, binding) {  
    // 组件的 VNode 更新时调用  
    // 根据组件的值更新绑定的元素  
  },  
  componentUpdated(el, binding) {  
    // 组件的 VNode 及其子 VNode 全部更新后调用  
  },  
  unbind(el) {  
    // 只调用一次,指令与元素解绑时调用  
    // 清理工作,比如移除事件监听器  
  }  
});
  • 局部注册:在 Vue 组件的配置项中的 directives 选项中进行注册。
export default {  
  directives: {  
    '指令名': {  
      // 钩子函数  
      bind(el, binding, vnode) {  
        // ...  
      },  
      // 其他钩子函数...  
    }  
  }  
}
  • 使用指令:在模板中,通过 v-指令名 的形式来使用自定义指令。
<template>  
  <div v-指令名=""></div>  
</template>

例子:在 main.js 中全局注册指令

import Vue from 'vue'
import App from './App.vue'

Vue.config.productionTip = false

// 1. 全局注册指令
Vue.directive('focus', {
  // inserted 会在指令所在的元素被插入到页面中时触发
  inserted(el) {
    // el 就是指令所绑定的元素
    el.focus()
  }
})

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

例子:在组件文件中局部注册指令并使用

<template>
  <div>
    <h1>自定义指令</h1>
    <input v-focus ref="inp" type="text" />
  </div>
</template>

<script>
export default {
  /* mounted() {
    this.$refs.inp.focus()
  } */

  // 2. 局部注册指令
  directives: {
    // 指令名:指令的配置项
    focus: {
      inserted(el) {
        el.focus();
      },
    },
  },
};
</script>

<style>
</style>

例子:使用 binding.value 获取指令值,使用 update 钩子监听指令值变化

<template>
  <div>
    <h1 v-color="color1">指令的值1测试</h1>
    <h1 v-color="color2">指令的值2测试</h1>
  </div>
</template>

<script>
export default {
  data() {
    return {
      color1: "red",
      color2: "blue",
    };
  },
  directives: {
    color: {
      // 1. inserted 提供的是元素被添加到页面中的逻辑
      inserted(el, binding) {
        // console.log(el, binding.value)
        // binding.value 就是指令的值
        el.style.color = binding.value;
      },
      // 2. update 指令的值修改的时候触发,提供值变化后,DOM 更新的逻辑
      update(el, binding) {
        el.style.color = binding.value;
      },
    },
  },
};
</script>

<style>
</style>

例子:封装 v-loading 指令,实现加载中效果。思路:

  • 准备一个 loading 类,通过伪元素定位,设置宽高,实现蒙层。
  • 开启关闭 loading 状态(添加移除蒙层),本质只需要添加移除类即可。
  • 结合自定义指定的语法进行封装复用。
<template>
  <div class="main">
    <div class="box" v-loading="isLoading">
      <ul>
        <li v-for="item in list" :key="item.id" class="news">
          <div class="left">
            <div class="title">{{ item.title }}</div>
            <div class="info">
              <span>{{ item.source }}</span>
              <span>{{ item.time }}</span>
            </div>
          </div>

          <div class="right">
            <img :src="item.img" alt="" />
          </div>
        </li>
      </ul>
    </div>
  </div>
</template>

<script>
// 安装 axios:npm i axios
import axios from "axios";

// 接口地址:http://hmajax.itheima.net/api/news
// 请求方式:get
export default {
  data() {
    return {
      list: [],
      isLoading: true,
    };
  },
  async created() {
    // 1. 发送请求获取数据
    const res = await axios.get("http://hmajax.itheima.net/api/news");

    setTimeout(() => {
      // 2. 更新到 list 中
      this.list = res.data.data;
      this.isLoading = false;
    }, 2000);
  },
  directives: {
    loading: {
      inserted(el, binding) {
        binding.value ? el.classList.add("loading") : el.classList.remove("loading");
      },
      update(el, binding) {
        binding.value ? el.classList.add("loading") : el.classList.remove("loading");
      },
    },
  },
};
</script>

<style>
/* 伪类 - 蒙层效果 */
.loading:before {
  content: "";
  position: absolute;
  left: 0;
  top: 0;
  width: 100%;
  height: 100%;
  background: #fff url("./assets/loading.gif") no-repeat center;
}

.box {
  width: 800px;
  min-height: 500px;
  border: 3px solid orange;
  border-radius: 5px;
  position: relative;
}
.news {
  display: flex;
  height: 120px;
  width: 600px;
  margin: 0 auto;
  padding: 20px 0;
  cursor: pointer;
}
.news .left {
  flex: 1;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  padding-right: 10px;
}
.news .left .title {
  font-size: 20px;
}
.news .left .info {
  color: #999999;
}
.news .left .info span {
  margin-right: 20px;
}
.news .right {
  width: 160px;
  height: 120px;
}
.news .right img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}
</style>

插槽

Vue的插槽(Slot)是一种强大的功能,它允许开发者在封装组件时,把不确定的、希望由用户指定的部分定义为插槽。这样,组件的使用者就可以为这些插槽提供具体的内容,从而实现组件的高度可定制性。

插槽可以分为三种类型:默认插槽、具名插槽和作用域插槽。

  • 默认插槽
  • 具名插槽
  • 作用域插槽

通过使用插槽,Vue 的组件可以变得更加灵活和可复用,因为开发者可以根据需要为组件提供特定的内容或行为。这大大提高了 Vue 应用的开发效率和可维护性。

默认插槽

在封装组件时,可以通过 <slot> 元素定义插槽,从而为用户预留内容占位符。如果没有为插槽指定名称,那么它就是默认插槽。默认插槽只能有一个。

使用步骤:

  • 在子组件中,使用 <slot></slot> 标签来定义一个默认插槽。这个标签表示插槽的占位符,当父组件没有提供具名插槽内容时,子组件的内容会被插入到这个默认插槽中。
<template>  
  <div>  
    <h2>子组件标题</h2>  
    <slot></slot> <!-- 定义默认插槽 -->  
  </div>  
</template>  
  
<script>  
export default {  
  name: 'MyComponent'  
}  
</script>
  • 在父组件中,使用子组件标签(例如 <MyComponent></MyComponent>)。在这个标签内部,可以直接写入需要插入到子组件默认插槽中的内容。
<template>  
  <div>  
    <my-component>  
      <p>这是父组件传递给默认插槽的内容。</p> <!-- 父组件内容 -->  
    </my-component>  
  </div>  
</template>  
  
<script>  
import MyComponent from './MyComponent.vue'  
  
export default {  
  components: {  
    MyComponent  
  }  
}  
</script>

在这个示例中,<p>这是父组件传递给默认插槽的内容。</p> 这段内容会被渲染到子组件 MyComponent 中的 <slot></slot> 位置。因此,最终的渲染结果将是在子组件的 <h2> 标题下方显示这段内容。

例子:给确认对话框组件传入不同的提示内容

子组件使用 <slot></slot> 指定提示内容位置:

<template>
  <div class="dialog">
    <div class="dialog-header">
      <h3>友情提示</h3>
      <span class="close">✖️</span>
    </div>

    <div class="dialog-content">
      <!-- 1. 在需要定制的位置,使用 slot 占位 -->
      <slot></slot>
    </div>
    <div class="dialog-footer">
      <button>取消</button>
      <button>确认</button>
    </div>
  </div>
</template>

<script>
export default {
  data() {
    return {};
  },
};
</script>

<style scoped>
* {
  margin: 0;
  padding: 0;
}
.dialog {
  width: 470px;
  height: 230px;
  padding: 0 25px;
  background-color: #ffffff;
  margin: 40px auto;
  border-radius: 5px;
}
.dialog-header {
  height: 70px;
  line-height: 70px;
  font-size: 20px;
  border-bottom: 1px solid #ccc;
  position: relative;
}
.dialog-header .close {
  position: absolute;
  right: 0px;
  top: 0px;
  cursor: pointer;
}
.dialog-content {
  height: 80px;
  font-size: 18px;
  padding: 15px 0;
}
.dialog-footer {
  display: flex;
  justify-content: flex-end;
}
.dialog-footer button {
  width: 65px;
  height: 35px;
  background-color: #ffffff;
  border: 1px solid #e1e3e9;
  cursor: pointer;
  outline: none;
  margin-left: 10px;
  border-radius: 3px;
}
.dialog-footer button:last-child {
  background-color: #007acc;
  color: #fff;
}
</style>

父组件在子组件标签中传入不同的提示内容:

<template>
  <div>
    <!-- 2. 在使用组件时,组件标签内填入内容 -->
    <MyDialog> 你确认要删除吗? </MyDialog>
    <MyDialog> 你确认要退出么 </MyDialog>
  </div>
</template>

<script>
import MyDialog from "./components/MyDialog.vue";
export default {
  data() {
    return {};
  },
  components: {
    MyDialog,
  },
};
</script>

<style>
body {
  background-color: #b3b3b3;
}
</style>

在 Vue 中,默认插槽的后备内容(Fallback Content)是指在父组件没有提供具体插槽内容的情况下,子组件中应该显示的默认内容。这通常是在 <slot></slot> 标签内部定义的内容,当父组件没有向该插槽传递任何内容时,这部分后备内容就会被渲染出来。

后备内容提供了一种机制,使得子组件在没有得到父组件的特定内容时,仍然能够显示一些有意义或占位的内容,从而保证了组件的完整性和可读性。

<template>  
  <div>  
    <h2>子组件标题</h2>  
    <slot>  
      <!-- 这里是后备内容,当父组件没有提供插槽内容时显示 -->  
      <p>没有提供内容时显示这里。</p>  
    </slot>  
  </div>  
</template>  
  
<script>  
export default {  
  name: 'ChildComponent'  
}  
</script>

在这个子组件中,在 <slot></slot> 标签内部定义了一段后备内容 <p>没有提供内容时显示这里。</p>。如果父组件没有提供任何插槽内容,子组件的后备内容 <p>没有提供内容时显示这里。</p> 将会被渲染出来。

<template>  
  <div>  
    <ChildComponent></ChildComponent> <!-- 没有提供插槽内容 -->  
  </div>  
</template>  
  
<script>  
import ChildComponent from './ChildComponent.vue'  
  
export default {  
  components: {  
    ChildComponent  
  }  
}  
</script>

具名插槽

如果需要在组件中预留多个插槽节点,那么应该为每个插槽指定具体的名称,这种带有具体名称的插槽就叫做具名插槽。它允许在子组件中定义多个插槽,并通过不同的名称来区分它们。这样,父组件就可以根据需要向子组件的特定插槽传递内容。

在子组件中,使用 <slot> 标签的 name 属性来定义具名插槽。例如:

<!-- ChildComponent.vue -->  
<template>  
  <div>  
    <h2>子组件标题</h2>  
    <slot name="header"></slot> <!-- 具名插槽 header -->  
    <slot name="content"></slot> <!-- 具名插槽 content -->  
    <slot name="footer"></slot> <!-- 具名插槽 footer -->  
  </div>  
</template>  
  
<script>  
export default {  
  name: 'ChildComponent'  
}  
</script>

在父组件中,使用 <template> 标签配合 v-slot 指令来向子组件的具名插槽传递内容。v-slot 后面跟上插槽的名称(或使用 #插槽名),用来指定内容应该插入到哪个具名插槽中。例如:

<!-- ParentComponent.vue -->  
<template>  
  <div>  
    <ChildComponent>  
      <template v-slot:header>  
        <h3>这是头部内容</h3>  
      </template>  
      <template v-slot:content>  
        <p>这是主体内容。</p>  
      </template>  
      <template #footer>  
        <p>这是页脚内容。</p>  
      </template>  
    </ChildComponent>  
  </div>  
</template>  
  
<script>  
import ChildComponent from './ChildComponent.vue'  
  
export default {  
  components: {  
    ChildComponent  
  }  
}  
</script>

在这个父组件中,使用了三个 <template> 标签,每个标签都通过 v-slot 指令指定了内容应该插入到子组件的哪个具名插槽中。这样,当父组件渲染时,每个 <template> 标签中的内容都会被插入到对应的具名插槽位置。

作用域插槽

Vue 的作用域插槽(Scoped Slots)是 Vue 的高级插槽机制,其本质是带数据的插槽,子组件提供给父组件的参数,父组件根据子组件传过来的插槽数据来进行不同的展现和填充内容。在标签中,通过 #插槽名="obj"(默认插槽为 #default)来接收这些数据。

使用步骤:

  • 子组件定义插槽并绑定数据:在子组件的模板中,使用 <slot> 标签定义插槽,并通过属性(如 v-bind 或简写 :)将需要传递的数据绑定到插槽上,所有添加的属性都会被收集到一个对象中。
<template>  
  <div>  
    <!-- 定义插槽并传递数据 -->  
    <slot :user="user" :post="post"></slot>  
  </div>  
</template>  
  
<script>  
export default {  
  data() {  
    return {  
      user: { name: '张三' },  
      post: { title: '我的文章' }  
    }  
  }  
}  
</script>
  • 父组件接收数据并自定义渲染:在父组件的模板中,引入并使用子组件。使用 <template> 标签包裹作用域插槽的内容,并使用 #插槽名="obj"(默认插槽为 #default)来接收子组件传递的数据。然后在 <template> 标签内部,根据接收到的数据,使用插值表达式(如 {{ obj.user.name }})来渲染自定义内容。
<template>  
  <div>  
    <ChildComponent>  
      <!-- 接收数据并自定义渲染 -->  
      <template #default="obj">  
        <div>用户:{{ obj.user.name }}</div>  
        <div>文章:{{ obj.post.title }}</div>  
      </template>  
    </ChildComponent>  
  </div>  
</template>  
  
<script>  
import ChildComponent from './ChildComponent.vue'  
  
export default {  
  components: {  
    ChildComponent  
  }  
}  
</script>

例子:父组件根据子组件传递的数据进行操作

子组件 MyTable:

<template>
  <table class="my-table">
    <thead>
      <tr>
        <th>序号</th>
        <th>姓名</th>
        <th>年纪</th>
        <th>操作</th>
      </tr>
    </thead>
    <tbody>
      <tr v-for="(item, index) in data" :key="item.id">
        <td>{{ index + 1 }}</td>
        <td>{{ item.name }}</td>
        <td>{{ item.age }}</td>
        <td>
          <!-- 1. 给 slot 标签,添加属性的方式传值 -->
          <slot :row="item" msg="测试文本"></slot>

          <!-- 2. 所有的属性被自动添加到动态对象 -->
          <!-- 
            {
              row: {id: 1, name: 'stone', age: 18}
              msg: '测试文本'
            }
           -->
        </td>
      </tr>
    </tbody>
  </table>
</template>

<script>
export default {
  props: {
    data: Array,
  },
};
</script>

<style scoped>
</style>

父组件:

<template>
  <div>
    <MyTable :data="list">
      <!-- 3. 通过 template #插槽名="变量名" 接收d -->
      <template #default="obj">
        <button @click="del(obj.row.id)">删除</button>
      </template>
    </MyTable>
    <MyTable :data="list2">
      <template #default="{ row }">
        <button @click="show(row)">查看</button>
      </template>
    </MyTable>
  </div>
</template>

<script>
import MyTable from "./components/MyTable.vue";
export default {
  data() {
    return {
      list: [
        { id: 1, name: "张小花", age: 18 },
        { id: 2, name: "孙大明", age: 19 },
        { id: 3, name: "刘德忠", age: 17 },
      ],
      list2: [
        { id: 1, name: "赵小云", age: 18 },
        { id: 2, name: "刘蓓蓓", age: 19 },
        { id: 3, name: "姜肖泰", age: 17 },
      ],
    };
  },
  methods: {
    del(id) {
      // console.log(id)
      this.list = this.list.filter((item) => item.id !== id);
    },
    show(row) {
      // console.log(row)
      alert(`姓名:${row.name}; 年纪:${row.age}`);
    },
  },
  components: {
    MyTable,
  },
};
</script>

例子:商品列表

标签子组件:

<template>
  <div class="my-tag">
    <input
      v-if="isEdit"
      v-focus
      ref="inp"
      class="input"
      type="text"
      placeholder="输入标签"
      :value="value"
      @blur="isEdit = false"
      @keyup.enter="handleEnter"
    />
    <div @dblclick="handleClick" class="text" v-else>
      {{ value }}
    </div>
  </div>
</template>

<script>
export default {
  props: {
    value: String,
  },
  data() {
    return {
      isEdit: false,
    };
  },
  methods: {
    handleClick() {
      // 双击后,切换到输入框状态(Vue 是异步 DOM 更新)
      this.isEdit = true;
      // 等 DOM 更新完,再获取焦点
      /* this.$nextTick(() => {
                // 立刻获取焦点
                this.$refs.inp.focus()
            }) */
    },
    handleEnter(e) {
      // 非空处理
      if (e.target.value.trim() === "") return alert("标签内容不能为空");
      // 子传父,将回车时,输入框的内容提交给父组件更新
      // 由于父组件是 v-model,触发事件,需要触发 input 事件
      this.$emit("input", e.target.value);
      // 提交完成,关闭输入状态
      this.isEdit = false;
    },
  },
};
</script>

<style  lang="less" scoped>
.my-tag {
  cursor: pointer;
  .input {
    appearance: none;
    outline: none;
    border: 1px solid #ccc;
    width: 100px;
    height: 40px;
    box-sizing: border-box;
    padding: 10px;
    color: #666;
    &::placeholder {
      color: #666;
    }
  }
}
</style>

表格子组件:

<template>
      <table class="my-table">
      <thead>
        <tr>
            <slot name="head"></slot>
        </tr>
      </thead>
      <tbody>
        <tr v-for="(item, index) in data" :key="item.id">
          <slot name="body" :item="item" :index="index"></slot>
        </tr>
      </tbody>
    </table>
</template>

<script>
export default {
    props: {
        data: {
           type: Array,
           require: true
        }
    }
}
</script>

<style lang="less" scoped>
.my-table {
    width: 100%;
    border-spacing: 0;
    img {
      width: 100px;
      height: 100px;
      object-fit: contain;
      vertical-align: middle;
    }
    th {
      background: #f5f5f5;
      border-bottom: 2px solid #069;
    }
    td {
      border-bottom: 1px dashed #ccc;
    }
    td,
    th {
      text-align: center;
      padding: 10px;
      transition: all 0.5s;
      &.red {
        color: red;
      }
    }
    .none {
      height: 100px;
      line-height: 100px;
      color: #999;
    }
  }
</style>

父组件:

<template>
  <div class="table-case">
    <MyTable :data="goods">
      <template #head>
        <th>编号</th>
        <th>名称</th>
        <th>图片</th>
        <th width="100px">标签</th>
      </template>
      <template #body="{ item, index }">
        <td>{{ index + 1 }}</td>
        <td>{{ item.name }}</td>
        <td>
          <img :src="item.picture" />
        </td>
        <td>
          <MyTag v-model="item.tag"></MyTag>
        </td>
      </template>
    </MyTable>
  </div>
</template>

<script>
// my-tag 标签组件的封装
// 1. 创建组件 - 初始化
// 2. 实现功能
//    (1) 双击显示,并且自动聚焦
//         v-if v-else @dblclick
//         自动聚焦:
//         1. $nextTick => $refs 获取到 DOM,进行 focus 获取焦点
//         2. 封装 v-focus 指令
//    (2) 失去焦点,隐藏输入框
//         @blur 操作 isEdit 即可
//    (3) 回显标签信息
//        回显的标签信息是父组件传递过来的
//        v-model 实现功能(简化代码) => v-model => :value 和 @input
//        组件内部通过 props 接收, :value 设置给输入框
//    (4) 内容修改了,回车 => 修改标签信息
//        @keyup.enter, 触发事件 $emit('input', e.target.value)

// -----------------------------------------------------------

// my-table 表格组件的封装
// 1. 数据不能写死,动态传递表格渲染的数据 【props】
// 2. 结构不能写死 - 多处结构自定义【具名插糟】
//    (1) 表头支持自定义
//    (2) 主体支持自定义

import MyTag from "./components/MyTag.vue";
import MyTable from "./components/MyTable.vue";
export default {
  name: "TableCase",
  components: {
    MyTag,
    MyTable,
  },
  data() {
    return {
      // 测试组件功能的临时数据
      tempText: "茶壶",
      goods: [
        {
          id: 101,
          picture:
            "https://yanxuan-item.nosdn.127.net/f8c37ffa41ab1eb84bff499e1f6acfc7.jpg",
          name: "梨皮朱泥三绝清代小品壶经典款紫砂壶",
          tag: "茶具",
        },
        {
          id: 102,
          picture:
            "https://yanxuan-item.nosdn.127.net/221317c85274a188174352474b859d7b.jpg",
          name: "全防水HABU旋钮牛皮户外徒步鞋山宁泰抗菌",
          tag: "男鞋",
        },
        {
          id: 103,
          picture:
            "https://yanxuan-item.nosdn.127.net/cd4b840751ef4f7505c85004f0bebcb5.png",
          name: "毛茸茸小熊出没,儿童羊羔绒背心73-90cm",
          tag: "儿童服饰",
        },
        {
          id: 104,
          picture:
            "https://yanxuan-item.nosdn.127.net/56eb25a38d7a630e76a608a9360eec6b.jpg",
          name: "基础百搭,儿童套头针织毛衣1-9岁",
          tag: "儿童服饰",
        },
      ],
    };
  },
};
</script>

<style lang="less" scoped>
.table-case {
  width: 1000px;
  margin: 50px auto;
  img {
    width: 100px;
    height: 100px;
    object-fit: contain;
    vertical-align: middle;
  }

  .my-table {
    width: 100%;
    border-spacing: 0;
    img {
      width: 100px;
      height: 100px;
      object-fit: contain;
      vertical-align: middle;
    }
    th {
      background: #f5f5f5;
      border-bottom: 2px solid #069;
    }
    td {
      border-bottom: 1px dashed #ccc;
    }
    td,
    th {
      text-align: center;
      padding: 10px;
      transition: all 0.5s;
      &.red {
        color: red;
      }
    }
    .none {
      height: 100px;
      line-height: 100px;
      color: #999;
    }
  }
}
</style>

路由

Vue Router 是 Vue 官方提供的路由管理器,它和 Vue 深度集成,可以非常方便地构建单页面应用(SPA)。通过 Vue Router,可以定义多个路由规则,即访问路径和组件的映射关系,并根据不同的 URL 显示不同的组件。这样,可以在一个页面中通过 URL 导航来切换不同的视图,而不需要重新加载整个页面。

Vue 路由的主要功能包括:

  • 定义路由规则:可以使用 Vue Router 来定义路由规则,每个规则对应一个 URL 路径和一个组件。当用户访问某个 URL 时,Vue Router 会根据定义的规则找到对应的组件并渲染到页面上。
  • 声明式导航:可以使用 <router-link> 组件来创建导航链接,当用户点击链接时,会触发路由跳转并更新页面内容。
  • 动态路由匹配:可以使用动态路由匹配来创建具有可变部分的 URL 路径。例如,可以定义一个路径 /user/:id,其中 :id 是一个动态部分,可以匹配任何值。
  • 嵌套路由:可以在路由规则中嵌套其他路由规则,以创建具有层次结构的页面布局。
  • 编程式导航:除了使用 <router-link> 组件进行导航外,还可以使用 Vue Router 的 API 进行编程式导航,例如在 JavaScript 代码中直接跳转到某个路由。

基本使用

Vue Router 使用步骤:

  • 安装 Vue Router(Vue 2 对应的版本为 Vue Router 3):
npm i vue-router@3
  • main.js 中引入,注册,创建路由对象,并注入到 Vue 实例中:
import Vue from 'vue'
import App from './App.vue'

// 引入 Vue Router
import VueRouter from 'vue-router'
// 注册 Vue Router
Vue.use(VueRouter)
// 创建 Vue Router 对象,对象名为 router
const router = new VueRouter()

Vue.config.productionTip = false

new Vue({
  render: h => h(App),
  // 将 Vue Router 对象注入到 Vue 实例,对象名为 router 才可以简写
  router
}).$mount('#app')
  • views 目录创建组件文件(配合路由使用的页面组件放在 src/views 目录,复用组件放在 src/components 目录),在 main.js 中配置路由规则:
import Vue from 'vue'
import App from './App.vue'

import Find from './views/Find.vue'
import My from './views/My.vue'
import Friend from './views/Friend.vue'

// 引入 Vue Router
import VueRouter from 'vue-router'
// 注册 Vue Router
Vue.use(VueRouter)
// 创建 Vue Router 对象
const router = new VueRouter({
  // 配置路由规则
  routes: [
    { path: '/find', component: Find},
    { path: '/my', component: My},
    { path: '/friend', component: Friend}
  ]
})

Vue.config.productionTip = false

new Vue({
  render: h => h(App),
  // 将 Vue Router 对象注入到 Vue 实例
  router
}).$mount('#app')

  • 在 Vue 组件中,可以使用 <router-view> 来显示当前路由对应的组件,使用 <router-link> 来创建导航链接:
<template>
  <div>
    <div class="footer_wrap">
      <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>
    </div>
  </div>
</template>

这样,当用户点击链接时,页面内容会根据定义的路由规则进行更新。同时,<router-view> 会自动显示当前路由对应的组件。

为保持代码的整洁性和可维护性,通常会对路由模块进行封装:

  • 创建 src/router/index.js 文件,并将路由相关代码从 main.js 中抽离到 src/router/index.js 文件中,引入组件时使用 @ 指代 src 目录,使用绝对路径,最后将 Vue Router 对象导出:
import Vue from 'vue'
// 引入 Vue Router
import VueRouter from 'vue-router'
// 注册 Vue Router
Vue.use(VueRouter)

import Find from '@/views/Find'
import My from '@/views/My'
import Friend from '@/views/Friend'

// 创建 Vue Router 对象
const router = new VueRouter({
  // 配置路由规则
  routes: [
    { path: '/find', component: Find},
    { path: '/my', component: My},
    { path: '/friend', component: Friend}
  ]
})

export default router
  • main.js 中引入 Vue Router 对象,并对象注入到 Vue 实例:
import Vue from 'vue'
import App from './App.vue'
import router from './router/index';

Vue.config.productionTip = false

new Vue({
  render: h => h(App),
  // 将 Vue Router 对象注入到 Vue 实例
  router
}).$mount('#app')

声明式导航

在 Vue 中,声明式导航主要指的是使用 <router-link> 组件来创建导航链接。这种方式允许在模板中直接声明链接,并且 Vue Router 会自动处理点击链接时的路由跳转。

导航链接

Vue Router 提供了一个全局组件 <router-link> 来实现导航链接,以取代 a 标签。<router-link> 组件必须配置 to 属性指定路径,最终会被渲染成一个 <a> 标签。

<template>
  <div>
    <div class="footer_wrap">
      <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>
    </div>
  </div>
</template>

<script>
export default {};
</script>

<style>
body {
  margin: 0;
  padding: 0;
}
.footer_wrap {
  position: relative;
  left: 0;
  top: 0;
  display: flex;
  width: 100%;
  text-align: center;
  background-color: #333;
  color: #ccc;
}
.footer_wrap a {
  flex: 1;
  text-decoration: none;
  padding: 20px 0;
  line-height: 20px;
  background-color: #333;
  color: #ccc;
  border: 1px solid black;
}
.footer_wrap a:hover {
  background-color: #555;
}
.footer_wrap a.router-link-active {
  background-color: purple;
}
/* .footer_wrap a.router-link-exact-active {
  background-color: pink;
} */
</style>

<router-link> 会自动给当前导航添加两个类名:

  • router-link-active:模糊匹配,例如 to="/my" 可以匹配 /my/my/a/my/b 等等
  • router-link-exact-active:精确匹配,例如 to="/my" 只能匹配 /my

还可以在路由配置文件 src/router/index.js 的 Vue Router 对象中自定义这两个类名:

import Vue from 'vue'
// 引入 Vue Router
import VueRouter from 'vue-router'
// 注册 Vue Router
Vue.use(VueRouter)

import Find from '@/views/Find'
import My from '@/views/My'
import Friend from '@/views/Friend'

// 创建 Vue Router 对象
const router = new VueRouter({
  // 配置路由规则
  routes: [
    { path: '/find', component: Find},
    { path: '/my', component: My},
    { path: '/friend', component: Friend}
  ],
  linkActiveClass:'active', // 配置模糊匹配的类名
  linkExactActiveClass:'exact-active' // 配置精确匹配的类名
})

export default router

那么组件中的类名也要做相应修改:

<style>
.footer_wrap a.active {
  background-color: purple;
}
/* .footer_wrap a.exact-active {
  background-color: pink;
} */
</style>

跳转传参

使用 Vue Router 进行声明式导航时,可以通过路由参数(params)或查询参数(query)来传递参数。这些参数可以在目标组件中通过 $route 对象来访问。

  • 查询参数传参,比较适合传多个参数:

    • 传递参数:to='/path?参数名=值' 或者 :to="{ path: '/user', query: { id: userId } }"

    • 获取参数:$route.query.参数名

在模板中指定导航链接,并配置查询参数:

<template>
  <div class="home">
    <div class="logo-box"></div>
    <div class="search-box">
      <input type="text">
      <button>搜索一下</button>
    </div>
    <div class="hot-link">
      热门搜索:
      <router-link to="/search?key=黑马程序员">黑马程序员</router-link>
      <router-link to="/search?key=前端培训">前端培训</router-link>
      <router-link to="/search?key=如何成为前端大牛">如何成为前端大牛</router-link>
    </div>
  </div>
</template>

获取参数:

<template>
  <div class="search">
    <p>搜索关键字: {{ $route.query.key }} </p>
    <p>搜索结果: </p>
    <ul>
      <li>.............</li>
      <li>.............</li>
      <li>.............</li>
      <li>.............</li>
    </ul>
  </div>
</template>

<script>
export default {
  name: 'MyFriend',
  created () {
    // 在created中,获取路由参数
    // this.$route.query.参数名 获取
    console.log(this.$route.query.key);
  }
}
</script>
  • 动态路由传参,比较适合传单个参数:
    • 配置动态路由:path: '/path/:参数名?',其中可选符 ? 表示没有传参也可以匹配,如果不加上可选符 ? 则必须传参
    • 配置导航链接:to='/path/值' 或者 :to="{ name: 'user', params: { userId: 123 }}"
    • 获取参数:$route.params.参数名

在路由配置文件 src/router/index.js 的 Vue Router 对象中配置动态路由:

// 创建 Vue Router 对象
const router = new VueRouter({
  // 配置路由规则
  routes: [
    { path: '/home', component: Home },
    { path: '/search/:words?', component: Search }
  ]
})

配置导航链接:

<template>
  <div class="home">
    <div class="logo-box"></div>
    <div class="search-box">
      <input type="text">
      <button>搜索一下</button>
    </div>
    <div class="hot-link">
      热门搜索:
      <router-link to="/search/黑马程序员">黑马程序员</router-link>
      <router-link to="/search/前端培训">前端培训</router-link>
      <router-link to="/search/如何成为前端大牛">如何成为前端大牛</router-link>
    </div>
  </div>
</template>

获取参数:

<template>
  <div class="search">
    <p>搜索关键字: {{ $route.params.words }}</p>
    <p>搜索结果:</p>
    <ul>
      <li>.............</li>
      <li>.............</li>
      <li>.............</li>
      <li>.............</li>
    </ul>
  </div>
</template>

<script>
export default {
  name: "MyFriend",
  created() {
    // 在created中,获取路由参数
    // this.$route.query.参数名 获取查询参数
    // this.$route.params.参数名 获取动态路由参数
    console.log(this.$route.params.words);
  },
};
</script>

路由重定向

在 Vue Router 中,路由重定向允许将一个路由路径重定向到另一个路由路径。这在某些情况下非常有用,比如当用户访问一个旧的 URL 或者一个不存在的 URL 时,可以将其重定向到一个新的或者默认的 URL。

在路由配置文件 src/router/index.js 的 Vue Router 对象中使用 redirect 属性设置路由重定向。redirect 可以是一个字符串,表示要重定向到的路径;也可以是一个函数,根据当前路由信息动态地返回重定向的目标路径。

语法:{ path: 匹配路径, redirect: 重定向到的路径}

例子:根路径重定向

// 创建 Vue Router 对象
const router = new VueRouter({
  // 配置路由规则
  routes: [
    { path: '/', redirect: '/home'},
    { path: '/home', component: Home },
    { path: '/search/:words?', component: Search }
  ]
})

例子:404 页面重定向,用于当路径找不到匹配的组件时,提供一个提示页面,需要配置在路由规则最后

// 创建 Vue Router 对象
const router = new VueRouter({
  // 配置路由规则
  routes: [
    { path: '/', redirect: '/home'},
    { path: '/home', component: Home },
    { path: '/search/:words?', component: Search },
    { path: '*', component: NotFound }
  ]
})

路由模式

在 Vue Router 中,有两种模式用于处理 URL:hash 模式和 history 模式。默认情况下,Vue Router 使用 hash 模式,它会利用 URL 的 Hash(#)来模拟一个完整的 URL,而不会重新加载页面。然而,在某些情况下,可能希望使用更 “正常” 的 URL,这时可以使用 history 模式。

  • hash 模式:在 hash 模式下,URL 会包含一个 #。例如,访问 /app/#/home 时,浏览器实际上不会向服务器发送请求,因为 # 之后的内容被视为 URL 的片段(fragment),不会包含在 HTTP 请求中。这意味着所有的路由都是在客户端处理的,不会触发页面的重新加载。
  • history 模式:history 模式则利用了 HTML5 History API 来实现 URL 的变化而不重新加载页面。它允许使用没有 # 的 URL,如 /app/home,并且看起来和普通的网站 URL 一样。但请注意,由于这种模式不会向服务器发送请求,所以需要确保服务器能够正确地处理所有路由。

要在 Vue Router 中启用 history 模式,只需在创建 router 实例时设置 mode'history' 即可:

import Vue from 'vue'  
import Router from 'vue-router'  
  
Vue.use(Router)  
  
const router = new Router({  
  mode: 'history',  
  routes: [  
    // ...你的路由配置  
  ]  
})

使用 history 模式时,需要确保服务器配置能够处理所有可能的路由。当用户直接访问一个深层链接时(例如 /app/user/123),服务器应该能够返回 Vue 应用的入口文件(通常是 index.html),而不是返回 404 错误。这是因为 Vue Router 会在客户端接管路由,并在内部处理这些路径。

例如对于 Nginxopen in new window 服务器,配置如下:

location / {  
  try_files $uri $uri/ /index.html;  
}

在上面器配置中,所有未匹配到实际文件或目录的请求都会被重定向到 index.html,这样 Vue Router 就可以接管并处理这些路由了。

编程式导航

编程式导航使用 JavaScript 代码进行跳转。在组件中通过 this.$router 对象来访问路由实例,并使用它的 push 方法来执行编程式导航。

语法:

  • 使用路径跳转:
// 字符串  
this.$router.push('路由路径')  
  
// 对象  
this.$router.push({ path: '路由路径' })
  • 使用命名路由跳转,适合路径比较长的场景,需要先在路由配置文件中为路由规则指定名称:
{ name: '路由名', path: '路径', component: 组件 }
this.$router.push({ name: '路由名' }) 

路由配置文件:

import Vue from 'vue'
// 引入 Vue Router
import VueRouter from 'vue-router'
// 注册 Vue Router
Vue.use(VueRouter)

import Home from "@/views/Home"
import Search from "@/views/Search"
import NotFound from "@/views/NotFound"

// 创建 Vue Router 对象
const router = new VueRouter({
  // 配置路由规则
  routes: [
    { path: '/', redirect: '/home'},
    { path: '/home', component: Home },
    { name: 'search', path: '/search/:words?', component: Search },
    { path: '*', component: NotFound }
  ]
})

export default router

组件:

<template>
  <div class="home">
    <div class="logo-box"></div>
    <div class="search-box">
      <input type="text">
      <button @click="goSearch">搜索一下</button>
    </div>
    <div class="hot-link">
      热门搜索:
      <router-link to="/search/黑马程序员">黑马程序员</router-link>
      <router-link to="/search/前端培训">前端培训</router-link>
      <router-link to="/search/如何成为前端大牛">如何成为前端大牛</router-link>
    </div>
  </div>
</template>

<script>
export default {
  name: 'FindMusic',
  methods: {
    goSearch() {
      // 1. 通过路径的方式跳转
      // (1) this.$router.push('路由路径')[简写]
      // this.$router.push('/search')

      // (2) this.$router.push({ [完整写法]
                // path:'路径'
      // })


      // 2. 通过命名路由的方式跳转(需要给路由名字),适合路径长
      // this.$router.push({
          // name:'路由名'
      // })
      this.$router.push({
        name:'search'
      })
    }
  }
}
</script>

对于以上两种编程式导航语法,都可以使用查询参数传参和动态路由传参。

  • 使用路径跳转:

    • 查询参数传参:
    this.$router.push('路由路径?参数名1=参数值1&参数名2=参数值2')  
    this.$router.push({ path: '路由路径', query: { 参数名1: '参数值1', 参数名2: '参数值2' }})
    
    • 动态路由传参
    this.$router.push('路由路径/参数值')  
    this.$router.push({ path: '路由路径/参数值 }})
    
  • 使用命名路由跳转:

    • 查询参数传参:
    this.$router.push({ name: '路由名', query: { 参数名1: '参数值1', 参数名2: '参数值2' }})
    
    • 动态路由传参:
    this.$router.push({ name: '路由名', params: { 参数名: '参数值' }})
    

传递参数:

<template>
  <div class="home">
    <div class="logo-box"></div>
    <div class="search-box">
      <input v-model="inpValue" type="text" />
      <button @click="goSearch">搜索一下</button>
    </div>
    <div class="hot-link">
      热门搜索:
      <router-link to="/search/黑马程序员">黑马程序员</router-link>
      <router-link to="/search/前端培训">前端培训</router-link>
      <router-link to="/search/如何成为前端大牛">如何成为前端大牛</router-link>
    </div>
  </div>
</template>

<script>
export default {
  name: "FindMusic",
  data() {
    return {
      inpValue: "",
    };
  },
  methods: {
    goSearch() {
      // 1. 通过路径的方式跳转
      // (1) this.$router.push('路由路径')[简写]
      //     this.$router.push('路由路径?参数名=参数值')
      //     this.$router.push('/search')
      //     this.$router.push(`/search?key=${this.inpValue}`)
      //     this.$router.push(`/search/${this.inpValue}`)

      // (2) this.$router.push({ [完整写法] 更适合传参
      //        path:'路径',
      //        query: {
      //          参数名: 参数值,
      //          参数名: 参数值,
      //        }
      //     })
      /* this.$router.push({
            path: '/search',
            query: {
              key: this.inpValue,
            }
          }) */
      /* this.$router.push({
            path: `/search/${this.inpValue}`
          })
      */

      // 2. 通过命名路由的方式跳转(需要给路由名字),适合路径长
      //    this.$router.push({
      //       name:'路由名'
      //       query: {参数名: 参数值}
      //    })
      //    this.$router.push({
      //       name:'路由名'
      //       params: {参数名: 参数值}
      //    })
      this.$router.push({
        name: "search",
        /* query: {
          key: this.inpValue
        } */
        params: {
          words: this.inpValue,
        },
      });
    },
  },
};
</script>

接收参数:

<template>
  <div class="search">
    <p>搜索关键字: {{ $route.params.words }} </p>
    <p>搜索结果: </p>
    <ul>
      <li>.............</li>
      <li>.............</li>
      <li>.............</li>
      <li>.............</li>
    </ul>
  </div>
</template>

<script>
export default {
  name: 'MyFriend',
  created () {
    // 在created中,获取路由参数
    // this.$route.query.参数名 获取查询参数
    // this.$route.params.参数名 获取动态路由参数
    console.log(this.$route.params.words);
  }
}
</script>

还可以使用 $router.go() 方法在浏览器历史中前进或后退多少步:

this.$router.go(1); // 相当于 history.forward()
this.$router.go(-1); // 相当于 history.back()

嵌套路由

嵌套路由允许在一个路由组件内部定义另一组路由,用于展示多级导航或嵌套页面。

在路由配置中添加一个 children 数组来配置嵌套路由:

import Vue from 'vue';  
import Router from 'vue-router';  
import ParentComponent from './components/ParentComponent.vue';  
import ChildComponent from './components/ChildComponent.vue';  
  
Vue.use(Router);  
  
const router = new Router({  
  routes: [  
    {  
      path: '/parent',  
      component: ParentComponent,  
      children: [  
        {  
          // 当 /parent/child 匹配成功,  
          // ChildComponent 会被渲染在 ParentComponent 的 <router-view> 中  
          path: 'child',  
          component: ChildComponent  
        }  
        // 可以在这里添加更多的子路由  
      ]  
    }  
  ]  
});  
  
export default router;

在父组件中使用 <router-view> 标签指定子路由组件应该渲染的位置:

<!-- ParentComponent.vue -->  
<template>  
  <div>  
    <h1>Parent Component</h1>  
    <router-view></router-view> <!-- 子路由组件会在这里渲染 -->  
  </div>  
</template>

可以使用 <router-link> 组件或编程式导航 $router.push 来导航到嵌套路由:

<!-- 使用 <router-link> -->  
<router-link to="/parent/child">Go to Child</router-link>
this.$router.push('/parent/child');

面经案例

配置路由:

import Vue from 'vue'
import VueRouter from "vue-router";
Vue.use(VueRouter)

import Layout from "@/views/Layout";
import Article from "@/views/Article";
import Collect from "@/views/Collect";
import Like from "@/views/Like";
import User from "@/views/User";
import ArticleDetail from "@/views/ArticleDetail";

const router = new VueRouter({
  routes: [
    {
      path: '/',
      component:Layout,
      // 通过 children 配置项,可以以配置嵌套子路由
      // 1.在 children 配置项中,配规则
      // 2.准备二级路由出口
      redirect: '/article',//重定向 article。相当于默认打开 article
      children:[
        { path: '/article', component: Article },
        { path: '/collect', component: Collect },
        { path: '/like', component: Like },
        { path: '/user', component: User }
      ]
    },
    { path: '/detail/:id', component: ArticleDetail }
  ]
})

export default router

App.vue 组件:

<template>
  <div class="h5-wrapper">
    <!-- 包裹了keep-alive 一级路由匹配的组件都会被缓存
         Layout 组件和 ArticleDetail 组件都会被缓存
         缓存后进入界面不再执行 created、mounted、destroyed。但可以使用 activated、deactivated 来配合
         Layout 组件(被缓存) - 多两个生命周期钩子
            activated 激活时,组件被看到时触发
            deactivated 失活时,离开页面组件看不见触发

         需求: 只希望 Layout 组件被缓存,include 配置:include="组件名数组" -->
    <keep-alive :include="keepArr">
      <router-view></router-view>
    </keep-alive>
  </div>
</template>

<script>
export default {
  name: "h5-wrapper",
  data() {
    return {
      // 缓存组件名的数组
      keepArr: ["LayoutPage"],
    };
  },
};
</script>

Layout.vue 组件:

<template>
  <div class="h5-wrapper">
    <div class="content">
      <router-view></router-view>
    </div>
    <nav class="tabbar">
      <!-- 导航高亮
           1. 将 a 标签,替换成 router-link (to) 
           2. 结合高亮类名 router-link-active 实现高亮效果 
      -->
      <router-link to="/article">面经</router-link>
      <router-link to="/collect">收藏</router-link>
      <router-link to="/like">喜欢</router-link>
      <router-link to="/user">我的</router-link>
    </nav>
  </div>
</template>

<script>
export default {
  // 组件名(如果没有配置 name,才会找文件名作为组件名)
  name: "LayoutPage",
  created() {
    console.log("created 组件被加载了");
  },
  mounted() {
    console.log("mounted dom 渲染完了");
  },
  destroyed() {
    console.log("destroyed 组件被销毁了");
  },
  activated() {
    console.log("activated 组件激活,看到组件了");
  },
  deactivated() {
    console.log("deactivated 组件失活,看不见组件了");
  },
};
</script>

Article.vue 组件:

<template>
  <div class="article-page">
    <div
      class="article-item"
      v-for="item in articles"
      :key="item.id"
      @click="$router.push(`/detail/${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>

<script>
// 请求地址: https://mock.boxuegu.com/mock/3083/articles
// 请求方式: get

// 跳转详情页传参
// 1. 查询参数传参 ?参数=参数值=> this.$route.query.参数名
// 2. 动态路由传参 改造路由 => /路径/参数 => this.$route.params.参数名
import axios from "axios";
export default {
  name: "ArticlePage",
  data() {
    return {
      articles: [],
    };
  },
  async created() {
    const res = await axios.get("https://mock.boxuegu.com/mock/3083/articles");
    this.articles = res.data.result.rows;
  },
};
</script>

ArticleDetail.vue 组件:

<template>
  <!-- v-if="articles.id" 当数据渲染完毕后展示 -->
  <div class="article-detail-page" v-if="articles.id">
    <nav class="nav">
      <span class="back" @click="$router.back()">&lt;</span> 面经详情
    </nav>
    <header class="header">
      <h1>{{ articles.stem }}</h1>
      <p>
        {{ articles.createdAt }} | {{ articles.views }} 浏览量 |
        {{ articles.likeCount }} 点赞数
      </p>
      <p>
        <img :src="articles.creatorAvatar" alt="" />
        <span>{{ articles.creatorName }}</span>
      </p>
    </header>
    <main class="body">
      {{ articles.content }}
    </main>
  </div>
</template>

<script>
import axios from "axios";
// 请求地址: https://mock.boxuegu.com/mock/3083/articles/:id
// 请求方式: get
export default {
  name: "ArticleDetailPage",
  data() {
    return {
      articles: [],
    };
  },
  async created() {
    const id = this.$route.params.id;
    const { data } = await axios.get(
      `https://mock.boxuegu.com/mock/3083/articles/${id}`
    );
    this.articles = data.result;
  },
};
</script>

路由守卫

VueRouter 的路由守卫是一种控制路由跳转的机制,它允许在路由跳转之前、之后或导航确认时执行用户自定义逻辑。这种机制有助于在页面跳转过程中实现权限控制、页面加载前的拦截处理等功能,是 Vue 开发中非常重要的一部分。

VueRouter 的路由守卫主要包括全局前置守卫、全局解析守卫、全局后置守卫、路由独享的守卫以及组件内的守卫。

其中,常用的全局前置守卫(beforeEach)在路由跳转前触发,参数包括 tofromnext 三个。这个钩子作用主要是用于登录验证等场景,确保在路由跳转前满足某些条件。

全局前置守卫(beforeEach)语法是:

const router = new VueRouter({ ... })

// 所有的路由在真正被访问到之前,即解析渲染对应组件前,都会先经过全局前置守卫
// 只有全局前置守卫放行了,才会到达对应的页面
router.beforeEach((to, from, next) => {
  // 在这里执行你的守卫逻辑  
  // to:     到哪里去的完整路由信息对象 
  // from:   从哪里来的完整路由信息对象
  // next(): 是否放行 
  //   (1)next()     直接放行,放行到 to 要去的路径
  //   (2)next(路径)  进行拦截,拦截到 next 里面配置的路径
})

面是一个使用全局前置守卫来实现简单的登录验证的示例:

import Vue from 'vue'  
import VueRouter from 'vue-router'  
  
// 假设有一个用于检查用户是否登录的函数  
function isUserLoggedIn() {  
  // 这里你可以根据实际情况来判断用户是否已登录  
  // 比如检查本地存储的 token 等  
  // 这里简单模拟一下,假设用户已登录  
  return true;  
}  
  
Vue.use(VueRouter)  
  
const router = new VueRouter({  
  routes: [  
    // 定义你的路由  
    { path: '/login', component: Login },  
    { path: '/dashboard', component: Dashboard, meta: { requiresAuth: true } },  
    // ... 其他路由  
  ]  
})  
  
router.beforeEach((to, from, next) => {  
  // 检查目标路由是否需要验证登录  
  if (to.matched.some(record => record.meta.requiresAuth)) {  
    // 如果用户未登录,则重定向到登录页面  
    if (!isUserLoggedIn()) {  
      next({  
        path: '/login',  
        query: { redirect: to.fullPath } // 将当前路由地址保存到查询参数中,登录后重定向  
      })  
    } else {  
      next() // 用户已登录,继续导航  
    }  
  } else {  
    next() // 无需验证登录的路由,直接继续导航  
  }  
})  
  
export default router

在这个示例中,定义了一个全局前置守卫,它检查即将进入的路由是否需要登录验证(通过 to.matched.some(record => record.meta.requiresAuth))。如果需要,则检查用户是否已登录。如果用户未登录,则将其重定向到登录页面,并将当前路由地址保存到查询参数中,以便登录后能够重定向回原来的页面。如果用户已登录或路由不需要验证登录,则调用 next() 继续导航。

路由懒加载

路由懒加载是一种优化手段,它允许你在访问某个路由时,才加载该路由对应的组件,而不是在应用启动时一次性加载所有组件。这可以显著提高应用的初始加载速度,特别是当应用有很多路由和大型组件时。

可以使用动态导入 import() 语法来实现路由的懒加载:

// 将
// import UserDetails from './views/UserDetails.vue'
// 替换成
const UserDetails = () => import('./views/UserDetails.vue')

const router = createRouter({
  // ...
  routes: [
    { path: '/users/:id', component: UserDetails }
    // 或在路由定义里直接使用它
    { path: '/users/:id', component: () => import('./views/UserDetails.vue') },
  ],
})
上次编辑于:
贡献者: stonebox,stone