Vue基础

目录


参考

API参考

插件参考

组件库参考

  • 参考:Vant组件库:轻量、可靠的移动端 Vue 组件库
  • 参考:Vue Tables-2:提供功能齐全的工具及以便Vue创建漂亮而实用的数据表格
  • 参考:Handsontable:页面端的表格交互插件,以及加载显示表格内容
  • 参考:Ag-Grid Vue:基于Vue的数据表格组件
  • 参考:Vue-Easytable:强大的Vue表格组件之一
  • 参考:Vue-Good-Table:基于Vue的数据表格组件,简单干净,具有排序、列过滤、分页等基本功能
  • 参考:Vue Toastification:提供轻巧、简单和漂亮的吐司提示,支持Vue 3
  • 参考:Vue Toasted:Vue最好的toast(提示)插件之一
  • 参考:Vue Notifaction
  • 参考:Vue Awesome Notifaction:轻量级,完全可自定义
  • 参考:Vue Wait:用于Vue、VueX等应用的复杂装载器和进度管理插件
  • 参考:Vue Content Loader:基于Vue.js的SVG占位符加载,可自定义SVG组件
  • 参考:Epic Spinners:纯CSS打造的网页Loading效果
  • 参考:Vue Radial Progress:径向进度条效果的加载器组件
  • 参考:Vue Feather Icons:开源图标库
  • 参考:Vue Awesome:流行的图标字体库
  • 参考:Vue Material Design Icons:单文件组件的SVG Material Design图标集合
  • 参考:Vue Apexcharts:现代的JS图表库
  • 参考:Vue-ECharts:基于Vue2.0和EChart封装的图标组件
  • 参考:Vue Timer Hook:计时器模块

调试参考


日志

  • 2022年03月08日19:55:26 初始版本
  • 2022年06月14日10:18:04 提取【模板】内容

内部模板参考

  • [新建Vue CLI工程(基础工程)【模板】](# 新建Vue CLI工程(基础工程)【模板】)
  • [新建Vue CLI工程(带路由工程)【模板】](# 新建Vue CLI工程(带路由工程)【模板】)
  • [Vant组件库基本使用【模板】](# Vant组件库基本使用【模板】)
  • [Vite创建Vue3.x项目【模板】](# Vite创建Vue3.x项目【模板】)
  • [Vite安装和配置less【模板】](# Vite安装和配置less【模板】)
  • [Vue CLI创建Vue3.x项目【模板】](# Vue CLI创建Vue3.x项目【模板】)
  • [Vite创建Vue3.x项目【模板】](# Vite创建Vue3.x项目【模板】)
  • [UI库ElementUI【模板】](# UI库ElementUI【模板】)
  • [UI库AntDesign【模板】](# UI库AntDesign【模板】)
  • [Vite安装和配置Router【模板】](# Vite安装和配置Router【模板】)

vue-devtools调试工具

安装完成后需要进行配置。

  • “管理扩展程序 > Vue.js devtools”,“有权访问的网站”配置为“在所有网站上”。
  • “管理扩展程序 > Vue.js devtools”,“允许访问文件网址”配置为开启。

Vue简介

什么是Vue

  1. 构建用户界面
    • 用 vue 往 html 页面中填充数据,非常的方便
  2. 框架
    • 框架是一套现成的解决方案,程序员只能遵守框架的规范,去编写自己的业务功能!
    • 要学习 vue,就是在学习 vue 框架中规定的用法!

vue全家桶

  • vue(核心库)vu
  • vue-router(路由方案)
  • vuex(状态管理方案)
  • vue组件库(快速搭建页面UI效果的方案)

以及辅助 vue 项目开发的一系列工具

  • vue-cli(npm 全局包:一键生成工程化的 vue 项目 - 基于 webpack、大而全)
  • vite(npm 全局包:一键生成工程化的 vue 项目 - 小而巧)
  • vue-devtools(浏览器插件:辅助调试的工具)
  • vetur(vscode 插件:提供语法高亮和智能提示)

模块化、组件化、规范化、自动化

Vue的两个特性

  1. 数据驱动视图:在使用了 vue 的页面中,vue 会监听数据的变化,从而自动重新渲染页面的结构。

    • 数据的变化会驱动视图自动更新。
    • 好处:程序员只管把数据维护好,那么页面结构会被 vue 自动渲染出来!
  2. 双向数据绑定: 在填写表单时,双向数据绑定可以辅助开发者在不操作 DOM 的前提下,自动把用户填写的内容同步到数据源 中。

    在网页中,form 表单负责采集数据,Ajax 负责提交数据

    • js 数据的变化,会被自动渲染到页面上
    • 页面上表单采集的数据发生变化的时候,会被 vue 自动获取到,并更新到JS数据中

注意:数据驱动视图和双向数据绑定的底层原理是 MVVM(Mode 数据源、View 视图、ViewModel 就是 vue 的实例)

MVVM

MVVM 是 vue 实现数据驱动视图和双向数据绑定的核心原理。MVVM 指的是 Model、View 和 ViewModel,它把每个 HTML 页面都拆分成了这三个部分。

在 MVVM 概念中:

  • Model 表示当前页面渲染时所依赖的数据源。
  • View 表示当前页面所渲染的 DOM 结构。
  • ViewModel 表示 vue 的实例,它是 MVVM 的核心。

MVVM 的工作原理

ViewModel 作为 MVVM 的核心,是它把当前页面的数据源(Model)和页面的结构(View)连接在了一起。

  • 当数据源发生变化时,会被 ViewModel 监听到,VM 会根据最新的数据源自动更新页面的结构
  • 当表单元素的值发生变化时,也会被 VM 监听到,VM 会把变化过后最新的值自动同步到 Model 数据源中

HTML中引入Vue

<!-- Vue.js 2.X -->
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>

<!-- Vue.js 3.X -->
<script src="https://cdn.jsdelivr.net/npm/vue"></script>

Vue的基本使用

  1. 导入 vue.js 的 script 脚本文件
  2. 在页面中声明一个将要被 vue 所控制的 DOM 区域
  3. 创建 vm 实例对象(vue 实例对象)

在VS Code中新增一个自定义模板“template-vue2.x”:

  1. Ctrl + Alt + P调出控制台,输入“snippets”。

  2. 在弹出的选项中选择“Preferences:Configure User Snippets”。

  3. 选择“新建全局代码片段文件”。

  4. 输入名称“template-vue2.x”。

  5. 将以下代码形成一个自定义模板。

    <!DOCTYPE html>
    <html>
      <head>
        <meta charset="utf-8" />
        <title>My test page</title>
        <!-- 1. 导入Vue库文件,再Windows全局就有了Vue这个构造函数 -->
        <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
      </head>
    
      <body>
        <!-- 希望Vue能够控制下面的这个div,帮我们再把数据填充到div内部 -->
        <div id="app">
          <p>{{ message1 }}</p>
          <p>{{ message2 }}</p>
        </div>
    
        <script>
          // 创建Vue实例对象
          new Vue({
            // el属性是固定的写法,表示当前VM实例要控制页面的哪个区域,接收的值是一个选择器
            el: "#app",
            // data对象为需要渲染到页面的数据
            data: {
              message1: "Hello Vue.js来了!",
              message2: "This is my first project!",
            },
          });
        </script>
      </body>
    </html>
    
    
  6. 模板中填写内容。

    {
    	"Print to console": {
    		"prefix": "tepmlate-Vue2.x",
    		"body": [
    			"<!DOCTYPE html>",
    			"<html>",
    			"  <head>",
    			"    <meta charset='utf-8' />",
    			"    <title>My test page</title>",
    			"    <!-- 1. 导入Vue库文件,再Windows全局就有了Vue这个构造函数 -->",
    			"    <script src='https://cdn.jsdelivr.net/npm/vue/dist/vue.js'></script>",
    			"  </head>",
    			"",
    			"  <body>",
    			"    <!-- 希望Vue能够控制下面的这个div,帮我们再把数据填充到div内部 -->",
    			"    <div id='app'>",
    			"      <p>{{ message1 }}</p>",
    			"      <p>{{ message2 }}</p>",
    			"    </div>",
    			"",
    			"    <script>",
    			"      // 创建Vue实例对象",
    			"      new Vue({",
    			"        // el属性是固定的写法,表示当前VM实例要控制页面的哪个区域,接收的值是一个选择器",
    			"        el: '#app',",
    			"        // data对象为需要渲染到页面的数据",
    			"        data: {",
    			"          message1: 'Hello Vue.js来了!',",
    			"          message2: 'This is my first project!',",
    			"        },",
    			"      });",
    			"    </script>",
    			"  </body>",
    			"</html>",
    			"",
    		],
    		"description": "A Vue2.x file template.",
    	}
    }
    

解析:

  • data指向的对象,就是Model数据源。
  • el指向的选择器,就是View视图区域。
  • new Vue()构造函数,得到的vm实例对象,就是ViewModel。

Vue的指令

指令(Directives)是 vue 为开发者提供的模板语法,用于辅助开发者渲染页面的基本结构。

vue 中的指令按照不同的用途可以分为如下 6 大类:

  1. 内容渲染指令
  2. 属性绑定指令
  3. 事件绑定指令
  4. 双向绑定指令
  5. 条件渲染指令
  6. 列表渲染指令

注意:指令是 vue 开发中最基础、最常用、最简单的知识点。

内容渲染指令

内容渲染指令用来辅助开发者渲染 DOM 元素的文本内容。常用的内容渲染指令有如下 3 个:

  • v-text
  • {{ }}
  • v-html

说明:

  1. v-text 指令的缺点:会覆盖元素内部原有的内容!
  2. {{ }} 插值表达式:在实际开发中用的最多,只是内容的占位符,不会覆盖原有的内容!
  3. v-html 指令的作用:可以把带有标签的字符串,渲染成真正的 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>
  <!-- 希望 Vue 能够控制下面的这个 div,帮我们在把数据填充到 div 内部 -->
  <div id="app">
    <p v-text="username"></p>
    <p v-text="gender">性别:</p>

    <hr>

    <p>姓名:{{ username }}</p>
    <p>性别:{{ gender }}</p>

    <hr>

    <div v-text="info"></div>
    <div>{{ info }}</div>
    <div v-html="info"></div>
  </div>

  <!-- 1. 导入 Vue 的库文件,在 window 全局就有了 Vue 这个构造函数 -->
  <script src="./lib/vue-2.6.12.js"></script>
  <!-- 2. 创建 Vue 的实例对象 -->
  <script>
    // 创建 Vue 的实例对象
    const vm = new Vue({
      // el 属性是固定的写法,表示当前 vm 实例要控制页面上的哪个区域,接收的值是一个选择器
      el: '#app',
      // data 对象就是要渲染到页面上的数据
      data: {
        username: 'zhangsan',
        gender: '女',
        info: '<h4 style="color: red; font-weight: bold;">欢迎大家来学习 vue.js</h4>'
      }
    })
  </script>
</body>

</html>

v-text

<!-- 把username对应的值,渲染到第一个p标签中 -->
<p v-text="username"></p>

<!-- 把gender对应的值,渲染到第二个p标签中 -->
<!-- 注意:第二个p标签中,默认的文本“性别”会被gender的值覆盖掉 -->
<p v-text="gender">性别:</p>

注意:v-text 指令会覆盖元素内默认的值。

{{ }} 语法

vue 提供的 {{ }} 语法,专门用来解决 v-text 会覆盖默认文本内容的问题。这种 {{ }} 语法的专业名称是插值表达式(英文名为:Mustache)。

<!-- 使用{{}}插值表达式,将对应的值渲染到元素的内容节点中 -->
<!-- 同时保留元素自身的默认值 -->
<p>姓名:{{username}}</p>
<p>性别:{{gender}}</p>

注意:相对于 v-text 指令来说,插值表达式在开发中更常用一些!因为它不会覆盖元素中默认的文本内容。

v-html

v-text 指令和插值表达式只能渲染纯文本内容。如果要把包含 HTML 标签的字符串渲染为页面的 HTML 元素, 则需要用到 v-html 这个指令:

<!-- 假设data中定义了名为info的数据,数据的值为包含HTML标签的字符串 -->
<!-- '<h4 style="color: red; font-weight: bold;">欢迎大家来学习 vue.js</h4>' -->
<div v-html="info"></div>

属性绑定指令(v-bind/:)

如果需要为元素的属性动态绑定属性值,则需要用到 v-bind 属性绑定指令。

注意:插值表达式只能用在元素的内容节点中,不能用在元素的属性节点中!

  • 在 vue 中,可以使用 v-bind: 指令,为元素的属性动态绑定值;

  • 简写是英文的 :

  • 在使用 v-bind 属性绑定期间,如果绑定内容需要进行动态拼接,则字符串的外面应该包裹单引号,例如:

    <!DOCTYPE html>
    <html>
      <head>
        <meta charset="utf-8" />
        <title>My test page</title>
        <!-- 1. 导入Vue库文件,再Windows全局就有了Vue这个构造函数 -->
        <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
      </head>
    
      <body>
        <!-- 希望 Vue 能够控制下面的这个 div,帮我们在把数据填充到 div 内部 -->
        <div id="app">
          <input type="text" :placeholder="tips" />
          <hr />
          <!-- vue 规定 v-bind: 指令可以简写为 : -->
          <img :src="photo" alt="" style="width: 150px" />
    
          <hr />
          <div>1 + 2 的结果是:{{ 1 + 2 }}</div> <!-- 1 + 2 的结果是:3 -->>
          <div>
            {{ tips }} 反转的结果是:{{ tips.split('').reverse().join('') }} <!-- 请输入用户名 反转的结果是:名户用入输请 -->>
          </div>
          <div :title="'box' + index">这是一个 div</div>
          <a :href="url">百度一下</a>
        </div>
    
        <!-- 2. 创建 Vue 的实例对象 -->
        <script>
          // 创建 Vue 的实例对象
          const vm = new Vue({
            // el 属性是固定的写法,表示当前 vm 实例要控制页面上的哪个区域,接收的值是一个选择器
            el: "#app",
            // data 对象就是要渲染到页面上的数据
            data: {
              tips: "请输入用户名",
              photo: "https://cn.vuejs.org/images/logo.svg",
              index: 3,
              url: "www.baidu.com",
            },
          });
        </script>
      </body>
    </html>
    

使用 Javascript 表达式

在 vue 提供的模板渲染语法中,除了支持绑定简单的数据值之外,还支持 Javascript 表达式的运算,例如:

{{ number + 1 }}
{{ message.split('').reverse().join('') }}
{{ ok ? 'YES' : 'NO' }}

<div v-bind:id>"'list-' + id"</div>

Class与Style绑定

绑定class

  1. 直接绑定方式
  2. 绑定数组方式
  3. 绑定对象方式

举例

  • 方式一:直接绑定方式

    <!DOCTYPE html>
    <html>
      <head>
        <meta charset="utf-8" />
        <meta http-equiv="Content-Type" content="text/html; charset=gb2312" />
        <meta http-equiv="Content-Language" content="zh-cn" />
        <title>My test page</title>
        <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    
        <style>
          .class-active {
            color: red;
          }
          .class-font-size {
            font-size: 75px;
          }
        </style>
      </head>
    
      <body>
        <div id="app">
          <div
            v-bind:class="{'class-active':isActive, 'class-font-size':isFontSet}"
          >
            我是一些文字的内容
          </div>
        </div>
    
        <script>
          var app = new Vue({
            el: "#app",
            data: {
              isActive: true,
              isFontSet: false,
            },
          });
        </script>
      </body>
    </html>
    
    
  • 方式二(1):绑定数组方式

    <!DOCTYPE html>
    <html>
      <head>
        <meta charset="utf-8" />
        <meta http-equiv="Content-Type" content="text/html; charset=gb2312" />
        <meta http-equiv="Content-Language" content="zh-cn" />
        <title>My test page</title>
        <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    
        <style>
          .class-active {
            color: red;
          }
    
          .class-font-size {
            font-size: 75px;
          }
        </style>
      </head>
    
      <body>
        <div id="app">
          <div v-bind:class="className">我是一些文字的内容</div>
        </div>
    
        <script>
          var app = new Vue({
            el: "#app",
            data: {
              className: ["class-active", "class-font-size"],
            },
          });
        </script>
      </body>
    </html>
    
    
  • 方式二(2):绑定数组方式

    <!DOCTYPE html>
    <html>
      <head>
        <meta charset="utf-8" />
        <meta http-equiv="Content-Type" content="text/html; charset=gb2312" />
        <meta http-equiv="Content-Language" content="zh-cn" />
        <title>My test page</title>
        <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    
        <style>
          .class-active {
            color: red;
          }
    
          .class-font-size {
            font-size: 75px;
          }
        </style>
      </head>
    
      <body>
        <div id="app">
          <div v-bind:class="[classActive, classFontSize]">我是一些文字的内容</div>
          <!-- classActive 和 classFontSize 都是变量 -->
        </div>
    
        <script>
          var app = new Vue({
            el: "#app",
            data: {
              classActive: "class-active",
              classFontSize: "class-font-size",
            },
          });
        </script>
      </body>
    </html>
    
    
  • 方式三:绑定对象方式

    <!DOCTYPE html>
    <html>
      <head>
        <meta charset="utf-8" />
        <meta http-equiv="Content-Type" content="text/html; charset=gb2312" />
        <meta http-equiv="Content-Language" content="zh-cn" />
        <title>My test page</title>
        <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    
        <style>
          .class-active {
            color: red;
          }
    
          .class-font-size {
            font-size: 75px;
          }
        </style>
      </head>
    
      <body>
        <div id="app">
          <div v-bind:class="classObject">我是一些文字的内容</div>
        </div>
    
        <script>
          var app = new Vue({
            el: "#app",
            data: {
              classObject: {
                "class-active": true,
                "class-font-size": true,
              },
            },
          });
        </script>
      </body>
    </html>
    
    

绑定内联样式

  • 方式一(1):对象方式

    <!DOCTYPE html>
    <html>
      <head>
        <meta charset="utf-8" />
        <meta http-equiv="Content-Type" content="text/html; charset=gb2312" />
        <meta http-equiv="Content-Language" content="zh-cn" />
        <title>My test page</title>
        <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
      </head>
    
      <body>
        <div id="app">
          <div v-bind:style="{color: theColor, 'font-size': fontSize + 'px'}">
            我是一些文字的内容
          </div>
        </div>
    
        <script>
          var app = new Vue({
            el: "#app",
            data: {
              theColor: "red",
              fontSize: 40,
            },
          });
        </script>
      </body>
    </html>
    
    
  • 方式一(2):对象方式

    <!DOCTYPE html>
    <html>
      <head>
        <meta charset="utf-8" />
        <title>My test page</title>
        <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
      </head>
    
      <body>
        <div id="app">
          <div :style="styleObj" @click="handleDivClick">我是一些文字的内容</div>
        </div>
    
        <script>
          var app = new Vue({
            el: "#app",
            data: {
              styleObj: {
                color: "black",
                "font-size": "35px",
              },
            },
            methods: {
              handleDivClick: function () {
                this.styleObj.color =
                  this.styleObj.color === "black" ? "red" : "black";
              },
            },
          });
        </script>
      </body>
    </html>
    
    
  • 方式二:数组方式

    <!DOCTYPE html>
    <html>
      <head>
        <meta charset="utf-8" />
        <title>My test page</title>
        <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
      </head>
    
      <body>
        <div id="app">
          <div :style="[styleObj, {'font-size': '35px'}]" @click="handleDivClick">
            我是一些文字的内容
          </div>
        </div>
    
        <script>
          var app = new Vue({
            el: "#app",
            data: {
              styleObj: {
                color: "black",
              },
            },
            methods: {
              handleDivClick: function () {
                this.styleObj.color =
                  this.styleObj.color === "black" ? "red" : "black";
              },
            },
          });
        </script>
      </body>
    </html>
    
    

事件绑定指令(v-on/@)

vue 提供了 v-on 事件绑定指令,用来辅助程序员为 DOM 元素绑定事件监听。

通过 v-on 绑定的事件处理函数,需要在 methods 节点中进行声明。

由于 v-on 指令在开发中使用频率非常高,因此,vue 官方为其提供了简写形式(简写为英文的 @ )。

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>My test page</title>
    <!-- 1. 导入Vue库文件,再Windows全局就有了Vue这个构造函数 -->
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
  </head>

  <body>
    <!-- 希望 Vue 能够控制下面的这个 div,帮我们在把数据填充到 div 内部 -->
    <div id="app">
      <p>count 的值是:{{ count }}</p>
      <!-- 在绑定事件处理函数的时候,可以使用 () 传递参数 -->
      <!-- v-on: 指令可以被简写为 @ -->
      <button @click="add(1)">+1</button>
      <button @click="sub">-1</button>
    </div>

    <!-- 2. 创建 Vue 的实例对象 -->
    <script>
      // 创建 Vue 的实例对象
      const vm = new Vue({
        // el 属性是固定的写法,表示当前 vm 实例要控制页面上的哪个区域,接收的值是一个选择器
        el: "#app",
        // data 对象就是要渲染到页面上的数据
        data: {
          count: 0,
        },
        // methods 的作用,就是定义事件的处理函数
        methods: {
          add(n) {
            // 在 methods 处理函数中,this 就是 new 出来的 vm 实例对象
            // console.log(vm === this)
            console.log(vm);
            // vm.count += 1
            this.count += n;
          },
          sub() {
            // console.log('触发了 sub 处理函数')
            this.count -= 1;
          },
        },
      });
    </script>
  </body>
</html>

注意:原生 DOM 对象有 onclick、oninput、onkeyup 等原生事件,替换为 vue 的事件绑定形式后, 分别为:v-on:click、v-on:input、v-on:keyup

事件参数对象

在原生的 DOM 事件绑定中,可以在事件处理函数的形参处,接收事件参数对象 event。

同理,在 v-on 指令 (简写为 @ )所绑定的事件处理函数中,同样可以接收到事件参数对象 event,

绑定事件并传参

在使用 v-on 指令绑定事件时,可以使用 ( ) 进行传参。

事件对象$event

$event 是 vue 提供的特殊变量,用来表示原生的事件参数对象 event。$event 可以解决事件参数对象 event 被覆盖的问题。

$event 的应用场景:如果默认的事件对象 e 被覆盖了,则可以手动传递一个$event。

需求:单机button时候,count数值+1,当count数值为偶数时候,改变button的背景颜色。

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>My test page</title>
    <!-- 1. 导入Vue库文件,再Windows全局就有了Vue这个构造函数 -->
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
  </head>

  <body>
    <!-- 希望 Vue 能够控制下面的这个 div,帮我们在把数据填充到 div 内部 -->
    <div id="app">
      <p>count 的值是:{{ count }}</p>
      <!-- 在绑定事件处理函数的时候,可以使用 () 传递参数 -->
      <!-- v-on: 指令可以被简写为 @ -->
      <!-- vue 提供了内置变量,名字叫做 $event,它就是原生 DOM 的事件对象 e -->
      <button @click="add($event, 1)">+1</button>
      <!-- 未传递参数时,默认接收到参数e -->
      <button @click="sub">-1</button>
    </div>

    <!-- 2. 创建 Vue 的实例对象 -->
    <script>
      // 创建 Vue 的实例对象
      const vm = new Vue({
        // el 属性是固定的写法,表示当前 vm 实例要控制页面上的哪个区域,接收的值是一个选择器
        el: "#app",
        // data 对象就是要渲染到页面上的数据
        data: {
          count: 0,
        },
        // methods 的作用,就是定义事件的处理函数
        methods: {
          add(e, n) {
            // 在 methods 处理函数中,this 就是 new 出来的 vm 实例对象
            // console.log(vm === this)
            console.log(vm);
            // vm.count += 1;
            this.count += n;
            e.target.style.backgroundColor = this.count % 2 === 0 ? "blue" : "";
          },
          sub(e) {
            // console.log('触发了 sub 处理函数')
            console.log(e);
            this.count -= 1;
            e.target.style.backgroundColor = this.count % 2 === 0 ? "red" : "";
          },
        },
      });
    </script>
  </body>
</html>

事件修饰符

在事件处理函数中调用 event.preventDefault() 或 event.stopPropagation() 是非常常见的需求。

因此, vue 提供了事件修饰符的概念,来辅助程序员更方便的对事件的触发进行控制。

常用的 5 个事件修饰符如下:

事件修饰符 说明
.prevent 阻止默认行为(例如:阻止 a 连接的跳转、阻止表单的提交等)
.stop 阻止事件冒泡
.capture 以捕获模式触发当前的事件处理函数
.once 绑定的事件只触发1次
.self 只有在 event.target 是当前元素自身时触发事件处理函数

语法格式如下:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>My test page</title>
    <!-- 1. 导入Vue库文件,再Windows全局就有了Vue这个构造函数 -->
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
  </head>

  <body>
    <!-- 希望 Vue 能够控制下面的这个 div,帮我们在把数据填充到 div 内部 -->
    <div id="app">
      <a href="https://www.baidu.com" @click.prevent="show">跳转到百度首页</a>
      <hr />
      <div
        style="
          height: 150px;
          background-color: orange;
          padding-left: 100px;
          line-height: 150px;
        "
        @click="divHandler"
      >
        <button @click.stop="btnHandler">按钮</button>
      </div>
    </div>

    <!-- 2. 创建 Vue 的实例对象 -->
    <script>
      // 创建 Vue 的实例对象
      const vm = new Vue({
        // el 属性是固定的写法,表示当前 vm 实例要控制页面上的哪个区域,接收的值是一个选择器
        el: "#app",
        // data 对象就是要渲染到页面上的数据
        data: {},
        // methods 的作用,就是定义事件的处理函数
        methods: {
          show() {
            console.log("单击了a链接");
          },
          divHandler() {
            console.log("divHandler");
          },
          btnHandler() {
            console.log("btnHandler");
          },
        },
      });
    </script>
  </body>
</html>

按键修饰符

在监听键盘事件时,我们经常需要判断详细的按键。此时,可以为键盘相关的事件添加按键修饰符,例如:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>My test page</title>
    <!-- 1. 导入Vue库文件,再Windows全局就有了Vue这个构造函数 -->
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
  </head>

  <body>
    <!-- 希望 Vue 能够控制下面的这个 div,帮我们在把数据填充到 div 内部 -->
    <div id="app">
      <input type="text" @keyup.esc="clearInput" @keyup.enter="commitAjax">
    </div>

    <!-- 2. 创建 Vue 的实例对象 -->
    <script>
      // 创建 Vue 的实例对象
      const vm = new Vue({
        // el 属性是固定的写法,表示当前 vm 实例要控制页面上的哪个区域,接收的值是一个选择器
        el: "#app",
        // data 对象就是要渲染到页面上的数据
        data: {},
        // methods 的作用,就是定义事件的处理函数
        methods: {
          clearInput(e) {
            console.log('触发了clearInput方法');
            e.target.value = '';
          },
          commitAjax() {
            console.log('触发了commitAjax方法');
          },
        },
      });
    </script>
  </body>
</html>

示例2(按键修饰符、系统修饰健、鼠标按钮修饰符)

<!DOCTYPE html>
<html>

<head>
    <meta charset="utf-8">
    <title>My test page</title>
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
</head>

<body>
    <div id="app">
        <!-- 1、按键修饰符 -->
        <!-- Vue 允许为 v-on 在监听键盘事件时添加按键修饰符 -->

        <!-- 只有在 `key` 是 `Enter` 时调用 `vm.submit()` -->
        <input v-on:keyup.enter="submit1">

        <!-- 可以直接将 KeyboardEvent.key 暴露的任意有效按键名转换为 kebab-case 来作为修饰符 -->
        <!-- 示例中,处理函数只会在 $event.key 等于 PageDown 时被调用 -->
        <input v-on:keyup.page-down="onPageDown">

        <!-- **按键码,keyCode 的事件用法已经被废弃了并可能不会被最新的浏览器支持** -->
        <input v-on:keyup.13="submit2">

        <!-- 按键码 -->
        <!-- .enter
             .tab
             .delete (捕获“删除”和“退格”键)
             .esc
             .space
             .up
             .down
             .left
             .right -->

        <!-- 2、系统修饰键 -->
        <!-- 2.1.0 新增 -->
        <!-- .ctrl
             .alt
             .shift
             .meta -->

        <!-- Alt + C -->
        <input @keyup.alt.67="clear">

        <!-- Ctrl + Click -->
        <div @click.ctrl="doSomething">Do something</div>

        <!-- 请注意修饰键与常规按键不同,在和 keyup 事件一起用时,事件触发时修饰键必须处于按下状态。
             换句话说,只有在按住 ctrl 的情况下释放其它按键,才能触发 keyup.ctrl。
             而单单释放 ctrl 也不会触发事件。如果你想要这样的行为,请为 ctrl 换用 keyCode:keyup.17 -->

        <!-- 2.5.0 新增 -->
        <!-- .exact 修饰符允许你控制由精确的系统修饰符组合触发的事件。 -->
        <!-- 即使 Alt 或 Shift 被一同按下时也会触发 -->
        <button @click.ctrl="onClick1">A</button>

        <!-- 有且只有 Ctrl 被按下的时候才触发 -->
        <button @click.ctrl.exact="onCtrlClick">A</button>

        <!-- 没有任何系统修饰符被按下的时候才触发 -->
        <button @click.exact="onClick2">A</button>

        <!-- 3、鼠标按钮修饰符 -->
        <!-- 2.2.0 新增 -->
        <!-- .left
             .right
             .middle -->
        <button @click.left="onClickLeft">Left</button>
        <button @click.right="onClickRight">Right</button>
        <button @click.middle="onClickMiddle">Middle</button>

    </div>

    <script>
        var app = new Vue({
            el: '#app',
            data: {
                name: 'Vue.js',
            },
            computed: {
            },
            methods: {
                submit1: function () {
                    alert("submit1");
                },
                onPageDown: function () {
                    alert("onPageDown");
                },
                submit2: function () {
                    alert("submit2");
                },
                clear: function () {
                    alert("clear");
                },
                doSomething: function () {
                    alert("doSomething");
                },
                onClick1: function () {
                    alert("onClick1");
                },
                onCtrlClick: function () {
                    alert("onCtrlClick");
                },
                onClick2: function () {
                    alert("onClick2");
                },
                onClickLeft: function () {
                    alert("onClickLeft");
                },
                onClickRight: function () {
                    alert("onClickRight");
                },
                onClickMiddle: function () {
                    alert("onClickMiddle");
                },
            }
        })
    </script>
</body>

</html>

双向绑定指令v-model

vue 提供了 v-model 双向数据绑定指令,用来辅助开发者在不操作 DOM 的前提下,快速获取表单的数据。

v-model其实是一个语法糖,通过props和emit组合而成。

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>My test page</title>
    <!-- 1. 导入Vue库文件,再Windows全局就有了Vue这个构造函数 -->
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
  </head>

  <body>
    <!-- 希望 Vue 能够控制下面的这个 div,帮我们在把数据填充到 div 内部 -->
    <div id="app">
      <p>用户的名字是:{{ username }}</p>
      <input type="text" v-model="username" />
      <hr />
      <input type="text" :value="username" />
      <hr />
      <p>选中的城市是{{ city }}</p>
      <select v-model="city">
        <option value="">请选择城市</option>
        <option value="1">北京</option>
        <option value="2">上海</option>
        <option value="3">广州</option>
      </select>
    </div>

    <!-- 2. 创建 Vue 的实例对象 -->
    <script>
      // 创建 Vue 的实例对象
      const vm = new Vue({
        // el 属性是固定的写法,表示当前 vm 实例要控制页面上的哪个区域,接收的值是一个选择器
        el: "#app",
        // data 对象就是要渲染到页面上的数据
        data: {
          username: "ZhangSan",
          city: "2",
        },
      });
    </script>
  </body>
</html>

v-model指令的修饰符

为了方便对用户输入的内容进行处理,vue 为 v-model 指令提供了 3 个修饰符,分别是:

修饰符 作用 示例
.number 自动将用户的输入值转为数值类型。 <input v-model.number="age" />
.trim 自动过滤用户输入的首尾空白字符。 <input v-model.trim="msg" />
.lazy 在“change”时而非“input”时更新,例如在输入框中的行为初始不会同步到数据源,直到失去鼠标焦点,才会把数据源的数据同步到数据源。 <input v-model.lazy="msg" />
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>My test page</title>
    <!-- 1. 导入Vue库文件,再Windows全局就有了Vue这个构造函数 -->
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
  </head>

  <body>
    <!-- 希望 Vue 能够控制下面的这个 div,帮我们在把数据填充到 div 内部 -->
    <div id="app">
      <input type="text" v-model.number="n1" /> +
      <input type="text" v-model.number="n2" /> =
      <span>{{ n1 + n2 }}</span>
      <hr />
      <input type="text" v-model.trim="username" />
      <button @click="getUserName">获取用户名</button>
      <hr />
      <input type="text" v-model.lazy="username">
    </div>

    <!-- 2. 创建 Vue 的实例对象 -->
    <script>
      // 创建 Vue 的实例对象
      const vm = new Vue({
        // el 属性是固定的写法,表示当前 vm 实例要控制页面上的哪个区域,接收的值是一个选择器
        el: "#app",
        // data 对象就是要渲染到页面上的数据
        data: {
          username: "ZhangSan",
          n1: 1,
          n2: 2,
        },
        methods: {
          getUserName() {
            console.log("获取到的用户名为:'" + this.username + "'");
          },
        },
      });
    </script>
  </body>
</html>

条件渲染指令

条件渲染指令用来辅助开发者按需控制 DOM 的显示与隐藏。

条件渲染指令有如下两个,分别是:

  • v-if
  • v-show

v-if 和 v-show 的区别

实现原理不同:

  • v-if 指令会动态地创建或移除 DOM 元素,从而控制元素在页面上的显示与隐藏;
  • v-show 指令会动态为元素添加或移除 style="display: none;" 样式,从而控制元素的显示与隐藏;

性能消耗不同:

v-if 有更高的切换开销,而 v-show 有更高的初始渲染开销。因此:

  • 如果需要非常频繁地切换,则使用 v-show 较好
  • 如果在运行时条件很少改变,则使用 v-if 较好

v-else

v-if 可以单独使用,或配合 v-else 指令一起使用:

注意:v-else 指令必须配合 v-if 指令一起使用,否则它将不会被识别!

v-else-if

v-else-if 指令,顾名思义,充当 v-if 的“else-if 块”,可以连续使用:

注意:v-else-if 指令必须配合 v-if 指令一起使用,否则它将不会被识别!

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>My test page</title>
    <!-- 1. 导入Vue库文件,再Windows全局就有了Vue这个构造函数 -->
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
  </head>

  <body>
    <!-- 希望 Vue 能够控制下面的这个 div,帮我们在把数据填充到 div 内部 -->
    <div id="app">
      <button @click="toggleFlagValue">切换flag的值</button>
      <p v-if="flag">这是被 v-if 控制的元素</p>
      <p v-show="flag">这是被 v-show 控制的元素</p>
      <hr />
      <div v-if="type === 'A'">优秀</div>
      <div v-else-if="type === 'B'">良好</div>
      <div v-else-if="type === 'C'">一般</div>
      <div v-else>差</div>
    </div>

    <!-- 2. 创建 Vue 的实例对象 -->
    <script>
      // 创建 Vue 的实例对象
      const vm = new Vue({
        // el 属性是固定的写法,表示当前 vm 实例要控制页面上的哪个区域,接收的值是一个选择器
        el: "#app",
        // data 对象就是要渲染到页面上的数据
        data: {
          // 如果 flag 为 true,则显示被控制的元素;如果为 false 则隐藏被控制的元素
          flag: true,
          type: "A",
        },
        methods: {
          toggleFlagValue() {
            this.flag = !this.flag;
          },
        },
      });
    </script>
  </body>
</html>

v-for列表渲染指令

vue 提供了 v-for 列表渲染指令,用来辅助开发者基于一个数组来循环渲染一个列表结构。

v-for 指令需要使 用 item in items 形式的特殊语法,其中:

  • items 是待循环的数组
  • item 是被循环的每一项

v-for中的索引

v-for 指令还支持一个可选的第二个参数,即当前项的索引。语法格式为 (item, index) in items。

注意:v-for指令中的item项和index索引都是形参,可以根据需要进行重命名。例如(user, i) in userlist。

使用key维护列表的状态

当列表的数据变化时,默认情况下,vue 会尽可能的复用已存在的 DOM 元素,从而提升渲染的性能。但这种默认的性能优化策略,会导致有状态的列表无法被正确更新。

为了给 vue 一个提示,以便它能跟踪每个节点的身份,从而在保证有状态的列表被正确更新的前提下,提升渲 染的性能。

此时,需要为每项提供一个唯一的key属性。

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>My test page</title>
    <!-- 1. 导入Vue库文件,再Windows全局就有了Vue这个构造函数 -->
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
  </head>

  <body>
    <!-- 希望 Vue 能够控制下面的这个 div,帮我们在把数据填充到 div 内部 -->
    <div id="app">
      <table class="table table-bordered table-hover table-striped">
        <thead>
          <th>索引</th>
          <th>ID</th>
          <th>姓名</th>
        </thead>
        <tbody>
          <!-- 官方建议:只要用到了 v-for 指令,那么一定要绑定一个 :key 属性 -->
          <!-- 而且,尽量把 id 作为 key 的值 -->
          <!-- 官方对 key 的值类型,是有要求的:字符串或数字类型 -->
          <!-- key 的值是千万不能重复的,否则会终端报错:Duplicate keys detected -->
          <!-- <tr v-for="(item, index) in list" :key="item.id" :title="item.name + index"> -->
          <tr v-for="(item, index) in list" :key="item.id">
            <td>{{ index }}</td>
            <td>{{ item.id }}</td>
            <td>{{ item.name }}</td>
          </tr>
        </tbody>
      </table>
    </div>

    <!-- 2. 创建 Vue 的实例对象 -->
    <script>
      // 创建 Vue 的实例对象
      const vm = new Vue({
        // el 属性是固定的写法,表示当前 vm 实例要控制页面上的哪个区域,接收的值是一个选择器
        el: "#app",
        // data 对象就是要渲染到页面上的数据
        data: {
          list: [
            { id: 1, name: "张三" },
            { id: 2, name: "李四" },
            { id: 3, name: "王五" },
            { id: 4, name: "张三" },
          ],
        },
      });
    </script>
  </body>
</html>

key 的注意事项

  1. key 的值只能是字符串或数字类型。
  2. key 的值必须具有唯一性(即:key 的值不能重复)。
  3. 建议把数据项 id 属性的值作为 key 的值(因为 id 属性的值具有唯一性)。
  4. 使用 index 的值当作 key 的值没有任何意义(因为 index 的值不具有唯一性,例如在新增条目的状态下,索引不具有对应条目的唯一性)。
  5. 建议使用 v-for 指令时一定要指定 key 的值(既提升性能、又防止列表状态紊乱)。
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>My test page</title>
    <!-- 1. 导入Vue库文件,再Windows全局就有了Vue这个构造函数 -->
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
  </head>

  <body>
    <!-- 希望 Vue 能够控制下面的这个 div,帮我们在把数据填充到 div 内部 -->
    <div id="app">
      <input type="text" v-model="name" />
      <button @click="addNewUser">添加用户</button>

      <!-- 用户列表区域 -->
      <ul>
        <li v-for="(user, index) in userList" :id="user.id">
          <input type="checkbox" name="" id="" />姓名:{{ user.name }}
        </li>
      </ul>
    </div>

    <!-- 2. 创建 Vue 的实例对象 -->
    <script>
      // 创建 Vue 的实例对象
      const vm = new Vue({
        // el 属性是固定的写法,表示当前 vm 实例要控制页面上的哪个区域,接收的值是一个选择器
        el: "#app",
        // data 对象就是要渲染到页面上的数据
        data: {
          // 输入的用户名
          name: "",
          // 用户列表
          userList: [
            { id: 1, name: "ZhangSan" },
            { id: 2, name: "LiSi" },
          ],
          // 下一个可用的ID值
          nextId: 3,
        },
        methods: {
          addNewUser() {
            this.userList.unshift({ id: this.nextId, name: this.name });
            this.name = "";
            this.nextId++;
          },
        },
      });
    </script>
  </body>
</html>

Label的for属性

for 属性规定 label 与哪个表单元素绑定。

品牌列表案例

添加品牌

品牌名称:“请输入品牌名称” 【添加品牌】

# 品牌名称 状态 创建时间 操作
1 宝马 已启用 2020-11-03 11:03:56 删除
2 奥迪 已禁用 2020-11-03 11:03:56 删除
3 奔驰 已启用 2020-11-03 11:03:56 删除
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>My test page</title>
    <!-- 1. 导入Vue库文件,再Windows全局就有了Vue这个构造函数 -->
    <!-- 新 Bootstrap 核心 CSS 文件 -->
    <link
      rel="stylesheet"
      href="https://cdn.jsdelivr.net/npm/bootstrap@3.3.7/dist/css/bootstrap.min.css"
    />
    <link rel="stylesheet" href="./css/brandlist.css" />
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
  </head>

  <body>
    <!-- 希望 Vue 能够控制下面的这个 div,帮我们在把数据填充到 div 内部 -->
    <div id="app">
      <!-- 卡片区域 -->
      <div class="card">
        <div class="card-header">添加品牌</div>
        <div class="card-body">
          <!-- 添加品牌的表单区域 -->
          <!-- form 表单元素有 submit 事件 -->
          <form @submit.prevent="add">
            <div class="form-row align-items-center">
              <div class="col-auto">
                <div class="input-group mb-2">
                  <div class="input-group-prepend">
                    <div class="input-group-text">品牌名称</div>
                  </div>
                  <input
                    type="text"
                    class="form-control"
                    placeholder="请输入品牌名称"
                    v-model.trim="brand"
                  />
                </div>
              </div>
              <div class="col-auto">
                <button type="submit" class="btn btn-primary mb-2">添加</button>
              </div>
            </div>
          </form>
        </div>
      </div>

      <!-- 表格区域 -->
      <table class="table table-bordered table-hover table-striped">
        <thead>
          <tr>
            <th scope="col">#</th>
            <th scope="col">品牌名称</th>
            <th scope="col">状态</th>
            <th scope="col">创建时间</th>
            <th scope="col">操作</th>
          </tr>
        </thead>
        <tbody>
          <tr v-for="item in list" :key="item.id">
            <td>{{ item.id }}</td>
            <td>{{ item.name }}</td>
            <td>
              <div class="custom-control custom-switch">
                <!-- 使用 v-model 实现双向数据绑定 -->
                <input
                  type="checkbox"
                  class="custom-control-input"
                  :id="'cb' + item.id"
                  v-model="item.status"
                />
                <!-- 使用 v-if 结合 v-else 实现按需渲染 -->
                <label
                  class="custom-control-label"
                  :for="'cb' + item.id"
                  v-if="item.status"
                >
                  已启用
                </label>
                <label
                  class="custom-control-label"
                  :for="'cb' + item.id"
                  v-else
                >
                  已禁用
                </label>
              </div>
            </td>
            <td>{{ item.time }}</td>
            <td>
              <a href="javascript:;" @click="remove(item.id)">删除</a>
            </td>
          </tr>
        </tbody>
      </table>
    </div>

    <!-- 2. 创建 Vue 的实例对象 -->
    <script>
      // 创建 Vue 的实例对象
      const vm = new Vue({
        // el 属性是固定的写法,表示当前 vm 实例要控制页面上的哪个区域,接收的值是一个选择器
        el: "#app",
        // data 对象就是要渲染到页面上的数据
        data: {
          // 用户输入的品牌名称
          brand: "",
          // nextId 是下一个,可用的 id
          nextId: 4,
          // 品牌列表的数据
          list: [
            { id: 1, name: "宝马", status: true, time: new Date() },
            { id: 2, name: "奔驰", status: false, time: new Date() },
            { id: 3, name: "奥迪", status: true, time: new Date() },
          ],
        },
        methods: {
          // 点击链接,删除对应的品牌信息
          remove(id) {
            this.list = this.list.filter((item) => item.id !== id);
          },
          // 阻止表单的默认提交行为之后,触发 add 方法
          add() {
            // 如果判断到 brand 的值为空字符串,则 return 出去
            if (this.brand === "") {
              return alert("必须填写品牌名称!");
            }

            // 如果没有被 return 出去,应该执行添加的逻辑
            // 1. 先把要添加的品牌对象,整理出来
            const obj = {
              id: this.nextId,
              name: this.brand,
              status: true,
              time: new Date(),
            };
            // 2. 往 this.list 数组中 push 步骤 1 中得到的对象
            this.list.push(obj);
            // 3. 清空 this.brand;让 this.nextId 自增 +1
            this.brand = "";
            this.nextId++;
          },
        },
      });
    </script>
  </body>
</html>

Vue过滤器

Vue过滤器适用于Vue2.x,而Vue3.x不再支持该功能。

过滤器定义

过滤器可以用在两个地方:插值表达式v-bind 属性绑定。

过滤器应该被添加在 JavaScript 表达式的尾部,由“管道符”进行调用。

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>My test page</title>
    <!-- 1. 导入Vue库文件,再Windows全局就有了Vue这个构造函数 -->
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
  </head>

  <body>
    <!-- 希望 Vue 能够控制下面的这个 div,帮我们在把数据填充到 div 内部 -->
    <div id="app">
      <p>message的值是:{{ message | capi }}</p>
    </div>

    <!-- 2. 创建 Vue 的实例对象 -->
    <script>
      // 创建 Vue 的实例对象
      const vm = new Vue({
        // el 属性是固定的写法,表示当前 vm 实例要控制页面上的哪个区域,接收的值是一个选择器
        el: "#app",
        // data 对象就是要渲染到页面上的数据
        data: {
          message: "hello, vue.js.",
        },
        // 过滤器函数,必须被定义到filters节点之下
        // 过滤器本质上是函数
        // 在过滤器的形参中就可以获取到管道符前面的值
        filters: {
          capi(val) {
            console.log(val);
            const firstChar = val.charAt(0).toUpperCase();
            const otherChar = val.slice(1);
            // 强调:过滤器中,一定要有返回值
            // 过滤器函数形参中的val一定是过滤器前面的值
            return firstChar + otherChar;
          },
        },
      });
    </script>
  </body>
</html>

私有过滤器和全局过滤器

在 filters 节点下定义的过滤器,称为“私有过滤器”,因为它只能在当前 vm 实例所控制的 el 区域内使用。 如果希望在多个 vue 实例之间共享过滤器,则可以按照如下的格式定义全局过滤器:

注意:如果全局过滤器和私有过滤器名字一致,此时按照“就近原则”,调用的是“私有过滤器”。

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>My test page</title>
    <!-- 1. 导入Vue库文件,再Windows全局就有了Vue这个构造函数 -->
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
  </head>

  <body>
    <!-- 希望 Vue 能够控制下面的这个 div,帮我们在把数据填充到 div 内部 -->
    <div id="app1">
      <p>message的值是:{{ message | capi }}</p>
    </div>

    <div id="app2">
      <p>message的值是:{{ message | capi }}</p>
    </div>

    <!-- 2. 创建 Vue 的实例对象 -->
    <script>
      // 使用Vue.filter()定义全局过滤器
      Vue.filter("capi", (val) => {
        console.log(val);
        const firstChar = val.charAt(0).toUpperCase();
        const otherChar = val.slice(1);
        // 强调:过滤器中,一定要有返回值
        // 过滤器函数形参中的val一定是过滤器前面的值
        return firstChar + otherChar;
      });

      // 创建 Vue 的实例对象
      const vm1 = new Vue({
        // el 属性是固定的写法,表示当前 vm 实例要控制页面上的哪个区域,接收的值是一个选择器
        el: "#app1",
        // data 对象就是要渲染到页面上的数据
        data: {
          message: "hello, vue.js.",
        },
      });

      // 创建 Vue 的实例对象
      const vm2 = new Vue({
        // el 属性是固定的写法,表示当前 vm 实例要控制页面上的哪个区域,接收的值是一个选择器
        el: "#app2",
        // data 对象就是要渲染到页面上的数据
        data: {
          message: "hello, world.",
        },
      });
    </script>
  </body>
</html>

连续调用多个过滤器

过滤器可以串联地进行调用。

过滤器传参

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>My test page</title>
    <!-- 1. 导入Vue库文件,再Windows全局就有了Vue这个构造函数 -->
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
  </head>

  <body>
    <!-- 希望 Vue 能够控制下面的这个 div,帮我们在把数据填充到 div 内部 -->
    <div id="app">
      <p>
        message的值是:{{ message | capitalize("参数1", "参数2") | maxLength }}
      </p>
    </div>

    <!-- 2. 创建 Vue 的实例对象 -->
    <script>
      // 全局过滤器,使用Vue.filter()定义全局过滤器
      Vue.filter("capitalize", (val, arg1, arg2) => {
        console.log(
          "filter of capitalize: " + val + ", arg1: " + arg1 + ", arg2: " + arg2
        );
        const firstChar = val.charAt(0).toUpperCase();
        const otherChar = val.slice(1);
        // 强调:过滤器中,一定要有返回值
        // 过滤器函数形参中的val一定是过滤器前面的值
        return firstChar + otherChar;
      });

      // 全局过滤器,使用Vue.filter()定义全局过滤器
      // 控制文本的最大长度
      Vue.filter("maxLength", (val) => {
        console.log("filter of maxLength: " + val);
        if (val.length <= 0) {
          return val;
        }
        // 强调:过滤器中,一定要有返回值
        // 过滤器函数形参中的val一定是过滤器前面的值
        return val.slice(0, 11) + "...";
      });

      // 创建 Vue 的实例对象
      const vm = new Vue({
        // el 属性是固定的写法,表示当前 vm 实例要控制页面上的哪个区域,接收的值是一个选择器
        el: "#app",
        // data 对象就是要渲染到页面上的数据
        data: {
          message: "hello, vue.js. hello, vue.js.",
        },
      });
    </script>
  </body>
</html>

过滤器的兼容性

过滤器仅在 vue 2.x 和 1.x 中受支持,在 vue 3.x 的版本中剔除了过滤器相关的功能。

在企业级项目开发中:

  • 如果使用的是 2.x 版本的 vue,则依然可以使用过滤器相关的功能。
  • 如果项目已经升级到了 3.x 版本的 vue,官方建议使用计算属性或方法代替被剔除的过滤器功能。

具体的迁移指南,请参考 vue 3.x 的官方文档给出的说明。

Vue侦听器

什么是侦听器

watch 侦听器允许开发者监视数据的变化,从而针对数据的变化做特定的操作。

  • 方法格式的侦听器:
    • 缺点1:无法在刚进入页面的时候,自动触发。
    • 缺点2:如果侦听的是一个对象,如果对象中的属性发生了变化,不会触发侦听器
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>My test page</title>
    <!-- 1. 导入Vue库文件,再Windows全局就有了Vue这个构造函数 -->
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
  </head>

  <body>
    <!-- 希望 Vue 能够控制下面的这个 div,帮我们在把数据填充到 div 内部 -->
    <div id="app">
      <input type="text" v-model="username" />
      <p>用户名:{{username}}</p>
    </div>

    <!-- 2. 创建 Vue 的实例对象 -->
    <script>
      // 创建 Vue 的实例对象
      const vm = new Vue({
        // el 属性是固定的写法,表示当前 vm 实例要控制页面上的哪个区域,接收的值是一个选择器
        el: "#app",
        // data 对象就是要渲染到页面上的数据
        data: {
          username: "",
        },
        // 所有的侦听器,都应该被定义到watch节点下
        watch: {
          // 侦听器本质上是一个函数,要监视那个数据的变化,就把数据名作为方法名即可
          // 新值在前,旧值在后
          username(newVal, oldVal) {
            console.log(
              `监听到了username的变化, newVal: ${newVal}, oldVal: ${oldVal}`
            );
          },
        },
      });
    </script>
  </body>
</html>

判断用户是否被占用

// 监听 username 值的变化,并使用 axios 发起 Ajax 请求,检测当前输入的用户名是否可用:
watch: {
    // 监听 username 值的变化
    async username(newVal) {
        if (newVal === '') return;
        // 使用 axios 发起请求,判断用户名是否可用
        const { data: res } = await axios.get('https://www.escook.cn/api/finduser/' + newVal);
        console.log(res);
    }
}

侦听器-immediate选项

默认情况下,组件在初次加载完毕后不会调用 watch 侦听器。如果想让 watch 侦听器立即被调用,则需要使 用 immediate 选项。

对象格式的侦听器:可以通过immediate选项,在刚进入页面的时候,让侦听器自动触发一次。

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>My test page</title>
    <!-- 1. 导入Vue库文件,再Windows全局就有了Vue这个构造函数 -->
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
  </head>

  <body>
    <!-- 希望 Vue 能够控制下面的这个 div,帮我们在把数据填充到 div 内部 -->
    <div id="app">
      <input type="text" v-model="username" />
      <p>用户名:{{username}}</p>
    </div>

    <!-- 2. 创建 Vue 的实例对象 -->
    <script>
      // 创建 Vue 的实例对象
      const vm = new Vue({
        // el 属性是固定的写法,表示当前 vm 实例要控制页面上的哪个区域,接收的值是一个选择器
        el: "#app",
        // data 对象就是要渲染到页面上的数据
        data: {
          username: "ZhangSan",
        },
        // 所有的侦听器,都应该被定义到watch节点下
        watch: {
          // 侦听器本质上是一个函数,要监视那个数据的变化,就把数据名作为方法名即可
          // 新值在前,旧值在后
          username: {
            handler(newVal, oldVal) {
              console.log(
                `监听到了username的变化, newVal: ${newVal}, oldVal: ${oldVal}`
              );
            },
            // immediate选项的默认值是false
            // immediate的作用是:控制侦听器是否自动触发一次
            immediate: true,
          },
        },
      });
    </script>
  </body>
</html>

侦听器-deep选项(深度监听,监听对象属性)

对象格式的侦听器:可以通过deep选项,让侦听器深度监听对象中每个属性的变化。

方式一:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>My test page</title>
    <!-- 1. 导入Vue库文件,再Windows全局就有了Vue这个构造函数 -->
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
  </head>

  <body>
    <!-- 希望 Vue 能够控制下面的这个 div,帮我们在把数据填充到 div 内部 -->
    <div id="app">
      <input type="text" v-model="info.username" />
      <p>用户名:{{info.username}}</p>
    </div>

    <!-- 2. 创建 Vue 的实例对象 -->
    <script>
      // 创建 Vue 的实例对象
      const vm = new Vue({
        // el 属性是固定的写法,表示当前 vm 实例要控制页面上的哪个区域,接收的值是一个选择器
        el: "#app",
        // data 对象就是要渲染到页面上的数据
        data: {
          info: {
            username: "ZhangSan",
          },
        },
        // 所有的侦听器,都应该被定义到watch节点下
        watch: {
          info: {
            handler(newVal) {
              console.log(`监听到了username的变化, newVal: ${newVal.username}`);
            },
            // 开启深度监听,只要对象中任何一个属性变化了,都会触发对象侦听器
            deep: true,
          },
        },
      });
    </script>
  </body>
</html>

方式二:直接侦听子属性的变化

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>My test page</title>
    <!-- 1. 导入Vue库文件,再Windows全局就有了Vue这个构造函数 -->
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
  </head>

  <body>
    <!-- 希望 Vue 能够控制下面的这个 div,帮我们在把数据填充到 div 内部 -->
    <div id="app">
      <input type="text" v-model="info.username" />
      <p>用户名:{{info.username}}</p>
    </div>

    <!-- 2. 创建 Vue 的实例对象 -->
    <script>
      // 创建 Vue 的实例对象
      const vm = new Vue({
        // el 属性是固定的写法,表示当前 vm 实例要控制页面上的哪个区域,接收的值是一个选择器
        el: "#app",
        // data 对象就是要渲染到页面上的数据
        data: {
          info: {
            username: "ZhangSan",
          },
        },
        // 所有的侦听器,都应该被定义到watch节点下
        watch: {
          // 如果要侦听的对象的子属性的变化,则必须包裹一层单引号
          "info.username": {
            handler(newVal, oldVal) {
              console.log(
                `监听到了username的变化, newVal: ${newVal}, oldVal: ${oldVal}`
              );
            },
          },
        },
      });
    </script>
  </body>
</html>

Vue计算属性

计算属性指的是通过一系列运算之后,最终得到一个属性值。

这个动态计算出来的属性值可以被模板结构或 methods 方法使用。

计算属性的特点

  1. 虽然计算属性在声明的时候被定义为方法,但是计算属性的本质是一个属性。
  2. 计算属性会缓存计算的结果,只有计算属性依赖的数据变化时,才会重新进行运算。

优点

  1. 实现了代码的复用。
  2. 只要计算属性中依赖的数据源变化了,则计算属性会自动重新求值。

原始代码:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>My test page</title>
    <!-- 1. 导入Vue库文件,再Windows全局就有了Vue这个构造函数 -->
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <style>
      .box {
        width: 200px;
        height: 200px;
        border: 1px solid #ccc;
      }
    </style>
  </head>

  <body>
    <!-- 希望 Vue 能够控制下面的这个 div,帮我们在把数据填充到 div 内部 -->
    <div id="app">
      <div>
        <span>R:</span>
        <input type="text" v-model.number="r" />
      </div>
      <div>
        <span>G:</span>
        <input type="text" v-model.number="g" />
      </div>
      <div>
        <span>B:</span>
        <input type="text" v-model.number="b" />
      </div>
      <hr />

      <!-- 专门用户呈现颜色的 div 盒子 -->
      <div class="box" :style="{ backgroundColor: `rgb(${r}, ${g}, ${b})` }">
        {{ `rgb(${r}, ${g}, ${b})` }}
      </div>
      <button @click="show">按钮</button>
    </div>

    <!-- 2. 创建 Vue 的实例对象 -->
    <script>
      // 创建 Vue 实例,得到 ViewModel
      var vm = new Vue({
        el: "#app",
        data: {
          // 红色
          r: 0,
          // 绿色
          g: 0,
          // 蓝色
          b: 0,
        },
        methods: {
          // 点击按钮,在终端显示最新的颜色
          show() {
            console.log(`rgb(${this.r}, ${this.g}, ${this.b})`);
          },
        },
      });
    </script>
  </body>
</html>

计算属性改造案例,优化之后的代码:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>My test page</title>
    <!-- 1. 导入Vue库文件,再Windows全局就有了Vue这个构造函数 -->
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <style>
      .box {
        width: 200px;
        height: 200px;
        border: 1px solid #ccc;
      }
    </style>
  </head>

  <body>
    <!-- 希望 Vue 能够控制下面的这个 div,帮我们在把数据填充到 div 内部 -->
    <div id="app">
      <div>
        <span>R:</span>
        <input type="text" v-model.number="r" />
      </div>
      <div>
        <span>G:</span>
        <input type="text" v-model.number="g" />
      </div>
      <div>
        <span>B:</span>
        <input type="text" v-model.number="b" />
      </div>
      <hr />

      <!-- 专门用户呈现颜色的 div 盒子 -->
      <div class="box" :style="{ backgroundColor: rgb }">{{ rgb }}</div>
      <button @click="show">按钮</button>
    </div>

    <!-- 2. 创建 Vue 的实例对象 -->
    <script>
      // 创建 Vue 实例,得到 ViewModel
      var vm = new Vue({
        el: "#app",
        data: {
          // 红色
          r: 0,
          // 绿色
          g: 0,
          // 蓝色
          b: 0,
        },
        // 所有的计算属性都要定义到computed中
        // computed属性要定义程方法格式
        computed: {
          // rgb作为一个计算属性,被定义成了方法格式
          // 最终,在这个方法中,要返回一个生成好的rgb(x, x, x)的字符串
          rgb() {
            return `rgb(${this.r}, ${this.g}, ${this.b})`;
          },
        },
        methods: {
          // 点击按钮,在终端显示最新的颜色
          show() {
            console.log(this.rgb);
          },
        },
      });
    </script>
  </body>
</html>

axios

说明:

axios专注于网络请求的库。

参考的请求URL,可以从F12找到一个网络请求即可(通过Open in new tab)。

axios基本使用

在Vue、React中都会用到axios来请求数据。

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>My test page</title>
    <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
  </head>

  <body>
    <script>
      // http://www.liulongbin.top:3006/api/getbooks

      // 1. 调用 axios 方法得到的返回值是 Promise 对象
      axios({
        // 请求方式
        method: "GET",
        // 请求的地址
        url: "http://www.liulongbin.top:3006/api/getbooks",
        // URL 中的查询参数(GET请求)
        params: {
          id: 1,
        },
        // 请求体参数(PUT请求)
        data: {},
      }).then(function (result) {
        console.log(result);
      });
    </script>
  </body>
</html>

axios发起POST请求

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>My test page</title>
    <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
  </head>

  <body>
    <button id="btnPOST">发起POST请求</button>
    <button id="btnGET">发起GET请求</button>

    <script>
      document
        .querySelector("#btnPOST")
        .addEventListener("click", async function () {
          // 如果调用某个方法的返回值是 Promise 实例,则前面可以添加 await!
          // await 只能用在被 async “修饰”的方法中
          const { data } = await axios({
            method: "POST",
            url: "http://www.liulongbin.top:3006/api/post",
            data: {
              name: "zs",
              age: 20,
            },
          });

          console.log(data);
        });

      document
        .querySelector("#btnGET")
        .addEventListener("click", async function () {
          // 解构赋值的时候,使用 : 进行重命名
          // 1. 调用 axios 之后,使用 async/await 进行简化
          // 2. 使用解构赋值,从 axios 封装的大对象中,把 data 属性解构出来
          // 3. 把解构出来的 data 属性,使用 冒号 进行重命名,一般都重命名为 { data: res }
          const { data: res } = await axios({
            method: "GET",
            url: "http://www.liulongbin.top:3006/api/getbooks",
          });

          console.log(res.data);
        });

      // $.ajax()   $.get()  $.post()
      // axios()    axios.get()    axios.post()    axios.delete()   axios.put()
    </script>
  </body>
</html>

axios直接发起GET和POST请求(推荐)

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>My test page</title>
    <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
  </head>

  <body>
    <button id="btnPOST">发起POST请求</button>
    <button id="btnGET">发起GET请求</button>

    <script>
      document
        .querySelector("#btnGET")
        .addEventListener("click", async function () {
          // axios.get("url地址", {
          //   // GET 参数
          //   params: {},
          // });
          const { data: res } = await axios.get(
            "http://www.liulongbin.top:3006/api/getbooks",
            {
              params: { id: 1 },
            }
          );
          console.log(res);
        });

      document
        .querySelector("#btnPOST")
        .addEventListener("click", async function () {
          // axios.post('url', { /* POST 请求体数据 */ })
          const { data: res } = await axios.post(
            "http://www.liulongbin.top:3006/api/post",
            { name: "zs", gender: "女" }
          );
          console.log(res);
        });
    </script>
  </body>
</html>

Vue CLI中axios的使用

建议先看完Vue CLI,再看本章节内容。

axios的基本用法

  • 参考:“VUE CLI > 新建一个工程(基础工程)”

新建一个工程,包含一个App.vue根组件和CompLeft.vue、Compight.vue两个子组件。

  1. 安装axios
npm install axios --save
  1. 在CompLeft.vue组件中发起GET请求。

    <template>
      <div class="left-container">
        <h3>Left组件</h3>
        <button @click="getInfo">发起GET请求</button>
      </div>
    </template>
    
    <script>
    import axios from 'axios'
    
    export default {
      methods: {
        async getInfo() {
          const { data: res } = await axios.get(
            "http://liulongbin.top:3006/api/get"
          );
          console.log("GET request res: " + JSON.stringify(res));
        },
      },
    };
    </script>
    
    <style lang="less" scoped>
    .left-container {
      background-color: lightgreen;
      min-height: 200px;
      flex: 1;
    }
    </style>
    
    
  2. 在Compight.vue组件中发起POST请求。

    <template>
      <div class="right-container">
        <h3>Right组件</h3>
        <button @click="postInfo">发起POST请求</button>
      </div>
    </template>
    
    <script>
    import axios from 'axios'
    
    export default {
        methods: {
        async postInfo() {
          const { data: res } = await axios.post(
            "http://liulongbin.top:3006/api/post"
          );
          console.log("POST request res: " + JSON.stringify(res));
        },
      },
    };
    </script>
    
    <style lang="less" scoped>
    .right-container {
      background-color: lightcoral;
      min-height: 200px;
      flex: 1;
    }
    </style>
    
    
  3. App.vue

    <template>
      <div id="app">
        <h1>App根组件</h1>
        <div class="box">
          <Left></Left>
          <right></right>
        </div>
      </div>
    </template>
    
    <script>
    import Left from "@/components/CompLeft.vue";
    import Right from "@/components/CompRight.vue";
    
    export default {
      name: "App",
      components: {
        Left,
        Right,
      },
    };
    </script>
    
    <style lang="less" scoped>
    .box {
      display: flex;
    }
    </style>
    
    

把axios挂载到Vue原型上并配置请求根路径

改装以上工程。

  1. 在main.js中,把axios中挂载到Vue原型上,自定义一个属性:$http,并给axios配置一个默认的URL根路径。

    import Vue from 'vue'
    import App from './App.vue'
    import axios from 'axios'
    
    Vue.config.productionTip = false
    
    // 全局配置axios的请求根路径
    axios.defaults.baseURL = 'http://liulongbin.top:3006'
    // 把axios挂载到Vue.prototype上,供每个.vue组件的实例直接使用
    // 后期在每个.vue组件要发起请求,直接调用this.$http.get / this.$http.post即可
    //  但是,把axios挂载到Vue原型上,无法实现API接口的复用。
    //  即如果某个API在多个组件中都有使用,需要在每个组件中都需要重新调用该API接口。
    Vue.prototype.$http = axios
    
    new Vue({
      render: h => h(App)
    }).$mount('#app')
    
    
  2. CompLeft.vue组件修改

    <template>
      <div class="left-container">
        <h3>Left组件</h3>
        <button @click="getInfo">发起GET请求</button>
      </div>
    </template>
    
    <script>
    export default {
      methods: {
        async getInfo() {
          const { data: res } = await this.$http.get("/api/get");
          console.log("GET request res: " + JSON.stringify(res));
        },
      },
    };
    </script>
    
    <style lang="less" scoped>
    .left-container {
      background-color: lightgreen;
      min-height: 200px;
      flex: 1;
    }
    </style>
    
    
  3. CompRight.vue组件修改

    <template>
      <div class="right-container">
        <h3>Right组件</h3>
        <button @click="postInfo">发起POST请求</button>
      </div>
    </template>
    
    <script>
    export default {
      methods: {
        async postInfo() {
          const { data: res } = await this.$http.post("/api/post");
          console.log("POST request res: " + JSON.stringify(res));
        },
      },
    };
    </script>
    
    <style lang="less" scoped>
    .right-container {
      background-color: lightcoral;
      min-height: 200px;
      flex: 1;
    }
    </style>
    
    

Vue CLI

单页面应用程序

Single Page Application,顾名思义,指的是一个Web网站中只有唯一一个HTML页面,所有的功能与交互都在这唯一的一个页面内完成。

什么是vue-cli

vue-cli 是 Vue.js 开发的标准工具。

它简化了程序员基于 webpack 创建工程化的 Vue 项目的过程。

引用自 vue-cli 官网上的一句话: 程序员可以专注在撰写应用上,而不必花好几天去纠结 webpack 配置的问题。

安装和使用

vue-cli 是 npm 上的一个全局包,使用 npm install 命令,即可方便的把它安装到自己的电脑上:

npm install -g @vue/cli

安装完成后,执行以下命令查看是否安装成功。

vue -V

# 返回结果示例
@vue/cli 5.0.1

在XX目录/文件夹中,基于 vue-cli 快速生成工程化的 Vue 项目:

vue create 项目名称

新建一个Vue CLI工程 *

新建Vue CLI工程(基础工程)【模板】

  1. 执行命令,新建一个工程

    vue create demo-1
    
  2. 手动配置工程

    # Manually select features
    # 新增勾选“CSS Pre-processors”
    # 以2.x为例,选择“2.x”
    # 选择“less”CSS预处理器
    # 选择“ESLint with error prevention only”或者“ESLint + Standard config”,根据需要选择
    # 最后咨询是否保存为模板,这里选择“N”
    

新建Vue CLI工程(带路由工程)【模板】

说明:建议看完路由章节内容,再关注本章节,带路由的工程创建。

  1. 执行命令,新建一个工程

    vue create demo-1
    
  2. 手动配置工程

    # Manually select features
    # 新增勾选“Router”和“CSS Pre-processors”
    # 以2.x为例,选择“2.x”
    # 选择“less”CSS预处理器
    # 选择“ESLint with error prevention only”或者“ESLint + Standard config”,根据需要选择
    # 最后咨询是否保存为模板,这里选择“N”
    

说明:与路由切换相关的组件,建议放到views文件夹中。反之,建议放到components文件夹中。

新建两个子组件

  1. 新建组件CompLeft.vue组件内容:

    <template>
      <div class="left-container">
        <h3>Left组件</h3>
      </div>
    </template>
    
    <script>
    export default {};
    </script>
    
    <style lang="less" scoped>
    .left-container {
      background-color: lightgreen;
      min-height: 200px;
      flex: 1;
    }
    </style>
    
    
  2. 新建组件CompRight.vue组件内容:

    <template>
      <div class="right-container">
        <h3>Right组件</h3>
      </div>
    </template>
    
    <script>
    export default {};
    </script>
    
    <style lang="less" scoped>
    .right-container {
      background-color: lightcoral;
      min-height: 200px;
      flex: 1;
    }
    </style>
    
    
  3. 删除components文件夹中的HelloWorld.vue组件。

  4. 修改App.vue组件内容:

    <template>
      <div id="app">
        <h1>App根组件</h1>
        <div class="box">
          <Left></Left>
          <right></right>
        </div>
      </div>
    </template>
    
    <script>
    import Left from "@/components/CompLeft.vue";
    import Right from "@/components/CompRight.vue";
    
    export default {
      name: "App",
      components: {
        Left,
        Right,
      },
    };
    </script>
    
    <style lang="less" scoped>
    .box {
      display: flex;
    }
    </style>
    
    

说明:如需要关闭ESLint,可以在vue.config.js文件中增加以下一条语句:

lintOnSave: false // 关闭ESLint检查

Vue项目的运行流程

在工程化的项目中,vue 要做的事情很单纯:通过 main.js 把 App.vue 渲染到 index.html 的指定区域中。

其中:

  1. App.vue 用来编写待渲染的模板结构
  2. index.html 中需要预留一个 el 区域
  3. main.js 把 App.vue 渲染到了 index.html 所预留的区域中

目录结构

│  .gitignore
│  babel.config.js				# Babel插件配置文件
│  jsconfig.json
│  package-lock.json
│  package.json
│  README.md
│  vue.config.js
│
├─node_modules					# 依赖包目录
├─public
│      favicon.ico
│      index.html				# 主页,生成的文件会被注入到此文件中
│
└─src							# 源代码目录
    │  App.vue					# 项目的根组件
    │  main.js					# 项目的入口文件,整个项目的运行,要先执行main.js
    │
    ├─assets					# 静态资源目录,例如图片资源、CSS样式表
    │      logo.png
    │
    └─components				# 组件,封装的、可复用的组件
            HelloWorld.vue

main.js文件解析

// 导入Vue包,得到Vue构造函数
import Vue from 'vue'
// 导入App.vue根组件,将来要把App.vue中的模板结构,渲染到HTML页面中
import App from './App.vue'

Vue.config.productionTip = false

// 创建Vue的实例对象
new Vue({
  // 把render函数指定的组件,渲染到HTML页面中
  render: h => h(App),
}).$mount('#app')

// 使用$mount方法指定到app区域,作用与el属性完全一样。两种方式选择其一即可。

测试$mount方法

等价于:el: '#app'

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>My test page</title>
    <!-- 1. 导入Vue库文件,再Windows全局就有了Vue这个构造函数 -->
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <style>
      .box {
        width: 200px;
        height: 200px;
        border: 1px solid #ccc;
      }
    </style>
  </head>

  <body>
    <div id="app">
      <input type="text" v-model="username" />
    </div>
    <!-- 2. 创建 Vue 的实例对象 -->
    <script>
      // 创建 Vue 实例,得到 ViewModel
      var vm = new Vue({
        data: {
          username: "ZhangSan",
        },
      });
      vm.$mount("#app");
    </script>
  </body>
</html>

Vue组件

什么是组件化开发

组件化开发指的是:根据封装的思想,把页面上可重用的 UI 结构封装为组件,从而方便项目的开发和维护。

Vue是一个支持组件化开发的前端框架。

Vue中规定:组件的后缀名是 .vue。之前接触到的 App.vue 文件本质上就是一个 vue 的组件。

Vue组件的三个组成部分

每个 .vue 组件都由 3 部分构成,分别是:

  • template:组件的模板结构。
  • script:组件的 JavaScript 行为。
  • style:组件的样式 其中,每个组件中必须包含 template 模板结构,而 script 行为和 style 样式是可选的组成部分。

template

vue 规定:每个组件对应的模板结构,需要定义到template节点中。

注意:

  • template 是 vue 提供的容器标签,只起到包裹性质的作用,它不会被渲染为真正的 DOM 元素
  • template 中只能包含唯一的根节点

script

vue 规定:开发者可以在script节点中封装组件的JavaScript业务逻辑。

.vue 组件中的 data 必须是函数

vue 规定:.vue 组件中的 data 必须是一个函数,不能直接指向一个数据对象。

style

vue 规定:组件内的style节点是可选的,开发者可以在style节点中编写样式美化当前组件的 UI 结构。

新建文件Test.vue

<template>
  <div class="text-box">
    <h3>这是一个自定义组件--{{ username }}</h3>
  </div>
</template>

<script>
// 默认导出,固定写法
export default {
  // data数据源
  // 注意:.vue组件中的data不能像之前一样,不能指向对象
  // 注意:组件中的data必须是一个函数
  data() {
    // return出去的对象中,可以定义数据
    return {
      username: "admin",
    };
  },
};
</script>

<style>
</style>

在组件中定义methods方法

<template>
  <div class="text-box">
    <h3>这是一个自定义组件--{{ username }}</h3>
    <button @click="changeName">修改名字</button>
  </div>
</template>

<script>
// 默认导出,固定写法
export default {
  // data数据源
  // 注意:.vue组件中的data不能像之前一样,不能指向对象
  // 注意:组件中的data必须是一个函数
  data() {
    // return出去的对象中,可以定义数据
    return {
      username: "admin",
    };
  },
  methods: {
    // 在组件中,this就表示当前组件的实例对象
    changeName() {
      this.username = "娃哈哈";
    },
    // 当前组件中的侦听器
    watch: {},
    // 当前组件中的计算属性
    computed: {},
    // 当前组件中的过滤器
    filters: {},
  },
};
</script>

<style>
.text-box {
  background-color: pink;
}
</style>

让style中支持 less 语法

在style标签上添加 lang="less" 属性,即可使用 less 语法编写组件的样式。

<style lang="less">
.test-box {
  background-color: pink;
  h3 {
    color: red;
  }
}
</style>

使用组件的三个步骤

  • 步骤1:使用 import 语法导入需要的组件。

    import Left from '@/components/Left.vue'
    
  • 步骤2:使用components节点注册组件。

    export default {
    	components: {
    		Left
    	}
    }
    
  • 步骤3:以标签形式使用刚才注册的组件。

    <div class="box">
        <Left></Left>
    </div>
    

通过components注册的是私有子组件

在组件 A 的 components 节点下,注册了组件 F。

则组件 F 只能用在组件 A 中;不能被用在组件 C 中。

请大家思考两个问题:

① 为什么 F 不能用在组件 C 中?

② 怎样才能在组件 C 中使用 F?

注册全局组件

方式一(main.js中定义)

在 vue 项目的 main.js 入口文件中,通过 Vue.component() 方法,可以注册全局组件。

// 导入需要被全局注册的组件
import TestName from '@/components/TestName'

// 参数1:字符串格式,表示组件的“注册名称”
// 参数2:需要被全局注册的对应组件
Vue.component('MyTestName', TestName);

在使用时候,只需要将注册的全局组件当成一个常规组件使用即可。

例如在App.vue中需要使用该组件。

<template>
  <div id="app">
    <MyTestName></MyTestName>
  </div>
</template>

<script>
export default {
  name: "App",
};
</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>

方式二(App.vue中定义,推荐)

App.vue

<template>
  <div id="app">
    <MyTestName :init="9"></MyTestName>
  </div>
</template>

<script>
// 1. 导入需要使用的.vue组件 import TestName from '@/components/TestName.vue'
import MyTestName from "@/components/TestName";

export default {
  name: "App",
  // 2. 注册组件
  components: {
    MyTestName,
  },
};
</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>

组件的自定义属性props

props 是组件的自定义属性,在封装通用组件的时候,合理地使用 props 可以极大的提高组件的复用性!

语法使用如下:

export default {
    // 组件的自定义属性
    props: ['自定义属性A', '自定义属性B', '其他自定义属性...'],
    
    // 组件的私有属性
    data() {
        return {},
    }
}

举例说明。

TestName.vue

<template>
  <div class="test-container">
    <h3 id="myh3">Test.vue 组件 --- {{ books.length }} 本图书</h3>
    <p id="pppp">message 的值是:{{ message }}</p>
    <button @click="message += '~'">修改 message 的值</button>
  </div>
</template>

<script>
export default {
  // props是自定义属性,允许使用者通过自定义属性,为当前组件指定初始值
  // 自定义属性的名字,是封装者自定义的(只要名称合法即可)
  props: ["info"],
  data() {
    return {
      message: "hello vue.js",
      // 定义 books 数组,存储的是所有图书的列表数据。默认为空数组!
      books: [],
    };
  },
};
</script>

<style></style>

调用的组件中,通过属性的方式传递参数值即可。

App.vue

<template>
  <div id="app">
    <MyTestName int="9"></MyTestName>
  </div>
</template>

<script>
export default {
  name: "App",
};
</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>

结合v-bind使用自定义属性

默认传递过去的是字符串数值。如需要传递数字值,使用v-bind属性即可。

<template>
  <div id="app">
    <MyTestName :int="9"></MyTestName>
  </div>
</template>

props是只读的

vue 规定:组件中封装的自定义属性是只读的,程序员不能直接修改 props 的值。否则会直接报错。

要想修改 props 的值,可以把 props 的值转存到 data 中,因为 data 中的数据都是可读可写的!

export default {
  	// props是自定义属性,允许使用者通过自定义属性,为当前组件指定初始值
  	// 自定义属性的名字,是封装者自定义的(只要名称合法即可)
    // props中的数据,可以直接在末班结构中被使用
    // 注意:props中的数据是只读的,不要直接修改props的值,否则终端会报错!
    props: ['init'],
    
    // 组件的私有属性
    data() {
        return {
            count: this.init, // 把this.init的值转存到count
        },
    }
}

TestName.vue

<template>
  <div class="test-container">
    <p id="pppp">count 的值是:{{ count }}</p>
    <input type="text" v-model="count" />
    <br />
    <br />
    <button @click="countPlusOne">Count+1</button>
  </div>
</template>

<script>
export default {
  props: ["init"],
  data() {
    return {
      count: this.init,
    };
  },
  methods: {
    countPlusOne() {
      console.log(this);
      this.count++;
    },
  },
};
</script>

<style></style>

组件调用方,App.vue中,以属性形式传递参数。

<template>
  <div id="app">
    <MyTestName :init="9"></MyTestName>
  </div>
</template>

<script>
export default {
  name: "App",
};
</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>

props中default默认值

在声明自定义属性时,可以通过 default 来定义属性的默认值。

export default {
  	// props是自定义属性,允许使用者通过自定义属性,为当前组件指定初始值
  	// 自定义属性的名字,是封装者自定义的(只要名称合法即可)
    // props中的数据,可以直接在末班结构中被使用
    // 注意:props中的数据是只读的,不要直接修改props的值,否则终端会报错!
    props: {
        // 自定义属性A:{ /* 配置项 */ }
        // 自定义属性B:{ /* 配置项 */ }
        // 自定义属性C:{ /* 配置项 */ }
        init: {
            // 如果外界使用TestName组件的时候,没有传递init属性,则默认值生效
            // 用default属性定义属性的默认值
            default: 0,
        },
    },
    
    // 组件的私有属性
    data() {
        return {
            count: this.init, // 把this.init的值转存到count
        },
    }
}

props中type值类型

在声明自定义属性时,可以通过 type 来定义属性的值类型。

export default {
  	// props是自定义属性,允许使用者通过自定义属性,为当前组件指定初始值
  	// 自定义属性的名字,是封装者自定义的(只要名称合法即可)
    // props中的数据,可以直接在末班结构中被使用
    // 注意:props中的数据是只读的,不要直接修改props的值,否则终端会报错!
    props: {
        // 自定义属性A:{ /* 配置项 */ }
        // 自定义属性B:{ /* 配置项 */ }
        // 自定义属性C:{ /* 配置项 */ }
        init: {
            // 如果外界使用TestName组件的时候,没有传递init属性,则默认值生效
            // 用default属性定义属性的默认值
            default: 0,
            // 用type属性定义属性的值类型
            // 如果传递过来的值不符合此类型,则会在终端报错
            type: Number,
        },
    },
    
    // 组件的私有属性
    data() {
        return {
            count: this.init, // 把this.init的值转存到count
        },
    }
}

props的required必填项

在声明自定义属性时,可以通过 required 选项,将属性设置为必填项,强制用户必须传递属性的值。

export default {
  	// props是自定义属性,允许使用者通过自定义属性,为当前组件指定初始值
  	// 自定义属性的名字,是封装者自定义的(只要名称合法即可)
    // props中的数据,可以直接在末班结构中被使用
    // 注意:props中的数据是只读的,不要直接修改props的值,否则终端会报错!
    props: {
        // 自定义属性A:{ /* 配置项 */ }
        // 自定义属性B:{ /* 配置项 */ }
        // 自定义属性C:{ /* 配置项 */ }
        init: {
            // 如果外界使用TestName组件的时候,没有传递init属性,则默认值生效
            // 用default属性定义属性的默认值
            default: 0,
            // 用type属性定义属性的值类型
            // 如果传递过来的值不符合此类型,则会在终端报错
            type: Number,
            // 必填项校验
            required: true,
        },
    },
    
    // 组件的私有属性
    data() {
        return {
            count: this.init, // 把this.init的值转存到count
        },
    }
}

组件之间的样式冲突问题

默认情况下,写在 .vue 组件中的样式会全局生效,因此很容易造成多个组件之间的样式冲突问题。

导致组件之间样式冲突的根本原因是:

① 单页面应用程序中,所有组件的 DOM 结构,都是基于唯一的 index.html 页面进行呈现的

② 每个组件中的样式,都会影响整个 index.html 页面中的 DOM 元素

如何解决组件样式冲突的问题

为每个组件分配唯一的自定义属性,在编写组件样式时,通过属性选择器来控制样式的作用域。

为了提高开发效率和开发体验,vue 为 style 节点提供了 scoped 属性,从而防止组件之间的样式冲突问题

TestName.vue

<template>
  <div class="test-container">
    <p id="pppp">count 的值是:{{ count }}</p>
    <input type="text" v-model="count" />
    <br />
    <br />
    <button @click="countPlusOne">Count+1</button>
  </div>
</template>

<script>
export default {
  props: {
    init: {
      default: 15,
      type: Number,
      required: true,
    },
  },
  data() {
    return {
      count: this.init,
    };
  },
  methods: {
    countPlusOne() {
      console.log(this);
      this.count++;
    },
  },
};
</script>

<style scoped>
/* style节点的scoped属性,用来自动为每个组件分配一个唯一的“自定义属性”
    并自动为当前组件的DOM标签和style样式应用这个自定义属性,防止组件的样式冲突问题 */
p {
  color: blue;
}
</style>

生成的代码:

<div data-v-7fcd5826="" class="test-container">
  <p data-v-7fcd5826="" id="pppp">count 的值是:9</p>
  <input data-v-7fcd5826="" type="text" /><br data-v-7fcd5826="" /><br
    data-v-7fcd5826=""
  /><button data-v-7fcd5826="">Count+1</button>
</div>

/deep/ 样式穿透

如果给当前组件的 style 节点添加了 scoped 属性,则当前组件的样式对其子组件是不生效的。如果想让某些样

式对子组件生效,可以使用 /deep/ 深度选择器。

Vue组件的实例对象

  • 组件定义的是一个模板结构。

  • 实际组件使用的场景,即为一个组件实例对象。

Vue生命周期

生命周期&生命周期函数

生命周期(Life Cycle)是指一个组件从创建 -> 运行 -> 销毁的整个阶段,强调的是一个时间段。

生命周期函数:是由 vue 框架提供的内置函数,会伴随着组件的生命周期,自动按次序执行。

注意:生命周期强调的是时间段,生命周期函数强调的是时间点。

生命周期图示

新建一个组件:TestLife.vue

<template>
  <div class="test-container">
    <h3>Test.vue组件</h3>
  </div>
</template>

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

<style scoped>
.test-container {
  background-color: aqua;
  height: 100px;
}
</style>

在App.vue中注册全局组件,并使用。

<template>
  <div id="app">
    <MyTestLife></MyTestLife>
    <MyTestName :init="9"></MyTestName>
  </div>
</template>

<script>
// 1. 导入需要使用的.vue组件 import TestName from '@/components/TestName.vue'
import MyTestName from "@/components/TestName";
import MyTestLife from "@/components/TestLife";

export default {
  name: "App",
  // 2. 注册组件
  components: {
    MyTestName,
    MyTestLife,
  },
};
</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>

  • 了解beforeCreate和created生命周期的特点
  • 了解beforeMount和mounted生命周期函数
  • 组件运行阶段的生命周期函数
  • 组件销毁阶段的生命周期函数

新建一个测试生命周期的组件TestLife.vue。

<template>
  <div class="test-container">
    <h3 id="myh3">Test.vue组件——{{ books.length }}本图书</h3>
    <p v-for="item in books" :key="item.id">{{ item.name }}</p>
    <p id="myId">{{ message }}</p>
    <button @click="changeMessage">改变message的值</button>
  </div>
</template>

<script>
export default {
  // 组件的自定义属性
  props: ["info"],
  // 组件的私有属性
  data() {
    return {
      message: "hello, vue.js",
      books: [
        { id: 1, name: "张三书" },
        { id: 2, name: "李四书" },
      ],
    };
  },
  methods: {
    show() {
      console.log("调用了TestLife组件的show()方法");
    },
    initBookList() {},
    changeMessage() {
      this.message += "~";
    },
  },

  /***** 1. 组件创建阶段 *****/
  // -->>(过程)初始化事件和生命周期函数
  // 组件的props/data/methods尚未被创建,都处于不可用状态
  beforeCreate() {
    console.log("--生命周期: beforeCreate()");
    // console.log(this.info); // 不可用
    // console.log(this.message); // 不可用
    // this.show(); // 不可用
  },
  // -->>(过程)初始化props、data、methods
  // 组件的props/data/methods已创建好,都处于可用状态
  // 但是组件的模板结构尚未生成,不能操作DOM元素
  // created生命周期函数,非常常用
  // 经常在它里面,调用methods中的方法,气你跪求服务器的数据
  // 并且,把请求到的数据,转存到data中,供template模板渲染的时候使用
  created() {
    console.log("--生命周期: create() **");
    console.info(this.info); // 可用
    console.info(this.message); // 可用
    this.show(); // 可用

    // const dom = document.querySelector('#myh3'); // 不可用
    // console.log(dom); // 不可用
  },
  // -->>(过程)基于数据和模板,在内存中编译生成HTML结构
  // 将要把内存中编译好的HTML结构渲染到浏览器中
  // 此时浏览器中还没有当前组件的DOM结构
  beforeMount() {
    console.log("--生命周期: beforeMount()");
    // const dom = document.querySelector('#myh3'); // 不可用
    // console.log(dom); // 不可用
  },
  // -->>(过程)用内存中编译生成的HTML结构,替换掉el属性指定的DOM元素
  // 已经把内存中的HTML结构,成功的渲染到了浏览器之中。此时浏览器中已然包含了当前组件的DOM结构
  mounted() {
    console.log("--生命周期: mounted() **"); // 可用
    const dom = document.querySelector("#myh3"); // 可用
    console.log(dom);
  },
  /***** 2. 组件运行阶段 *****/
  /*****  此阶段数据最少执行0次(无数据变化) *****/
  /*****  此阶段数据最多执行N次(数据变化N次) *****/
  /*****  当数据变化之后,为了能够操作最新的DOM结构,必须把代码写到updated生命周期中 *****/
  // 将要根据变化过后,最新的数据,重新渲染组件的模板结构
  // 此时数据已经变化,但DOM元素内容未更新
  beforeUpdate() {
    console.log("--生命周期: beforeUpdate()");
    console.log("数据值(已更新): " + this.message);
    const domId = document.querySelector("#myId"); // 可用
    console.log("DOM元素值(未更新): " + domId.innerHTML);
  },
  // -->>(过程)根据最新的数据,重新渲染组件的DOM结构
  // 已经根据最新的数据,完成了组件DOM结构的重新渲染
  updated() {
    console.log("--生命周期: updated() **");
    console.log("数据值(已更新): " + this.message);
    const domId = document.querySelector("#myId");
    console.log("DOM元素值(已更新): " + domId.innerHTML);
  },
  /***** 3. 组件销毁阶段 *****/
  // 将要销毁此组件,此时尚未销毁,组件处于正常工作的状态
  beforeDestroy() {
    console.log("--生命周期: beforeDestroy()");
  },
  // -->>(过程)销毁当前组件的数据侦听器、子组件、事件监听
  // 组件已经被销毁,此组件在浏览器中对应的DOM结构已被完全移除
  destroyed() {
    console.log("--生命周期: destroyed()");
  },
};
</script>

<style scoped>
.test-container {
  background-color: aqua;
  height: 180px;
}
</style>

在App.vue组件中进行传参和测试组件的销毁阶段。

<template>
  <div id="app">
    <h1>App根组件</h1>
    <button @click="flag = !flag">Toggle TestLife Flag</button>

    <MyTestLife info="你好" v-if="flag"></MyTestLife>
    <MyTestName :init="9"></MyTestName>
  </div>
</template>

<script>
// 1. 导入需要使用的.vue组件 import TestName from '@/components/TestName.vue'
import MyTestName from "@/components/TestName";
import MyTestLife from "@/components/TestLife";

export default {
  name: "App",
  data() {
    return {
      flag: true,
    };
  },
  // 2. 注册组件
  components: {
    MyTestName,
    MyTestLife,
  },
};
</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>

组件之间的数据共享

组件之间的关系

最常见的组件之间的关系

  • 父子关系
  • 兄弟关系

父组件向子组件共享数据(自定义属性)

父组件App.vue:

  • 简单类型是传递值到子组件
  • 复杂类型传递的是对象的引用到子组件
<template>
  <div id="app">
    <h1>App根组件</h1>
    <MyTestName :msg="message" :user="userInfo"></MyTestName>
  </div>
</template>

<script>
// 1. 导入需要使用的.vue组件 import TestName from '@/components/TestName.vue'
import MyTestName from "@/components/TestName";

export default {
  name: "App",
  // 组件的自定义属性
  props: [],
  // 组件的私有属性
  data() {
    return {
      message: "hello, vue.js",
      userInfo: { name: "张三", age: 20 },
    };
  },
  // 2. 注册组件
  components: {
    MyTestName,
  },
};
</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>

子组件TestName.vue:

子组件中自定义属性(props)接收数据

说明:不要直接修改props中的值。可以在子组件中定义私有属性进行接收和改变。

<template>
  <div class="test-container">
    <h5>Son组件</h5>
    <p>父组件传递过来的msg值是: {{ msg }}</p>
    <p>父组件传递过来的user值是: {{ user }}</p>
  </div>
</template>

<script>
export default {
  // 组件的自定义属性
  props: ["msg", "user"],
  // 组件的私有属性
  data() {
    return {
      msgIn: this.msg,
      userIn: this.user,
    };
  },
};
</script>

<style scoped>
p {
  color: blue; /* 不加/deep/时,生成的选择器格式为 p[data-v-052242de] */
}
</style>

子组件向父组件传递数据(自定义事件)

子组件向父组件共享数据使用自定义事件。示例代码:

子组件触发XX自定义事件:

<script>
export default {
  // 组件的自定义属性
  props: [],
  // 组件的私有属性
  data() {
    return {
      count: 0, // 需要传递给父组件的值
    };
  },
  methods() {
      add() {
          this.count += 1;
          // 修改数据时,通过$emit()触发自定义事件
          this.$emit('numChange', this.count);
      }
  }
};
</script>

父组件监听XX自定义事件:

<Son @numChange="getNewCount"></Son>

<script>
export default {
  // 组件的自定义属性
  props: [],
  // 组件的私有属性
  data() {
    return {
      countFromSon: 0,
    };
  },
  methods() {
      getNewCount(val) {
          this.countFromSon = val;
      }
  }
};
</script>

举例说明

App.vue

<template>
  <div id="app">
    <h1>App根组件</h1>
    <p >子组件传递过来的数据值:{{ countFromSon }}</p>
    <MyTestName :msg="message" :user="userInfo" @numChange="getNewCount"></MyTestName>
  </div>
</template>

<script>
// 1. 导入需要使用的.vue组件 import TestName from '@/components/TestName.vue'
import MyTestName from "@/components/TestName";

export default {
  name: "App",
  // 组件的自定义属性
  props: [],
  // 组件的私有属性
  data() {
    return {
      message: "hello, vue.js",
      userInfo: { name: "张三", age: 20 },
      countFromSon: 0,
    };
  },
  methods: {
    getNewCount(val) {
      this.countFromSon = val;
    },
  },
  // 2. 注册组件
  components: {
    MyTestName,
  },
};
</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>

TestName.vue

<template>
  <div class="test-container">
    <h5>Son组件</h5>
    <p>父组件传递过来的msg值是: {{ msg }}</p>
    <p>父组件传递过来的user值是: {{ user }}</p>
    <p>需要传递给父组件的count值:{{ count }}</p>
    <button @click="add()">count数值+1</button>
  </div>
</template>

<script>
export default {
  // 组件的自定义属性
  props: ["msg", "user"],
  // 组件的私有属性
  data() {
    return {
      msgIn: this.msg,
      userIn: this.user,
      count: 0, // 需要传递给父组件的值
    };
  },
  methods: {
    add() {
      this.count += 1;
      // 修改数据时,通过$emit()触发自定义事件
      this.$emit('numChange', this.count);
    }
  },
};
</script>

<style scoped>
p {
  color: blue; /* 不加/deep/时,生成的选择器格式为 p[data-v-052242de] */
}
</style>

兄弟组件之间的数据共享

在 Vue2.x 中,兄弟组件之间数据共享的方案是 EventBus。

在components文件夹中,新建一个EventBus的JS文件(eventBus.js):

import Vue from 'vue'
// 向外共享 Vue 的实例对象
export default new Vue();

兄弟组件A(数据发送方):

// 1. 导入eventBus.js模块
import bus from './eventBus.js'

<script>
export default {
    data() {
        return {
            msg: 'hello vue.js'
        }
    },
    methods: {
        sendMsg() {
            // 2. 通过eventBus触发自定义事件,发送数据
            bus.$emit('share', this.msg)
        }
    }
}
</script>

兄弟组件C(数据接收方):

// 1. 导入eventBus.js模块
import bus from './eventBus.js'

<script>
export default {
    data() {
        return {
            msgFromLeft: '',
        }
    },
    created() {
        // 2. 通过eventBus监听自定义事件,接收数据
        bus.$on('share', val => {
            this.msgFromLeft = val;
        })
    }
}
</script>

EventBus 的使用步骤

① 创建 eventBus.js 模块,并向外共享一个 Vue 的实例对象

② 在数据发送方,调用 bus.$emit('事件名称', 要发送的数据) 方法触发自定义事件

③ 在数据接收方,调用 bus.$on('事件名称', 事件处理函数) 方法注册一个自定义事件

Ref引用

什么是ref引用

ref 用来辅助开发者在不依赖于 jQuery 的情况下,获取 DOM 元素或组件的引用。

每个 vue 的组件实例上,都包含一个 $refs 对象,里面存储着对应的 DOM 元素或组件的引用。默认情况下,

组件的 $refs 指向一个空对象。

使用ref引用DOM元素

如果想要使用 ref 引用页面上的 DOM 元素,则可以按照如下的方式进行操作:

App.vue

<template>
  <div id="app">
    <h1 ref="myh1">App根组件</h1>
    <p>子组件传递过来的数据值:{{ countFromSon }}</p>
    <button @click="getRef">切换H1的颜色</button>
  </div>
</template>

<script>
// 1. 导入需要使用的.vue组件 import TestName from '@/components/TestName.vue'

export default {
  name: "App",
  // 组件的自定义属性
  props: [],
  // 组件的私有属性
  data() {
    return {
      message: "hello, vue.js",
      userInfo: { name: "张三", age: 20 },
      countFromSon: 0,
    };
  },
  methods: {
    getNewCount(val) {
      console.log("numChange事件被触发了");
      this.countFromSon = val;
    },
    getRef() {
      console.log(this);
      this.$refs.myh1.style.color = "red";
    },
  },
  // 2. 注册组件
  components: {},
};
</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>

使用ref进行父组件调用子组件的方法

TestName.vue

<template>
  <div class="test-container">
    <h5>Son组件</h5>
    <p>父组件传递过来的msg值是: {{ msg }}</p>
    <p>父组件传递过来的user值是: {{ user }}</p>
    <p>子组件的count值:{{ count }}</p>
    <button @click="add">count数值+1</button>
    <button @click="resetCount">count数值充值</button>
  </div>
</template>

<script>
export default {
  // 组件的自定义属性
  props: ["msg", "user"],
  // 组件的私有属性
  data() {
    return {
      msgIn: this.msg,
      userIn: this.user,
      count: 0,
    };
  },
  methods: {
    add() {
      this.count += 1;
      // 修改数据时,通过$emit()触发自定义事件
      this.$emit("numChange", this.count);
    },
    resetCount() {
      this.count = 0;
    },
  },
};
</script>

<style scoped>
p {
  color: blue; /* 不加/deep/时,生成的选择器格式为 p[data-v-052242de] */
}
</style>

App.vue,通过ref属性调用子组件的方法

<template>
  <div id="app">
    <h1 ref="myH1">App根组件</h1>
    <button @click="toggleH1Color">切换H1标题颜色</button>
    <button @click="resetSubComCount">重置子组件的count数值</button>
    <MyTestName
      :msg="message"
      :user="userInfo"
      ref="componentTestName"
    ></MyTestName>
  </div>
</template>

<script>
// 1. 导入需要使用的.vue组件 import TestName from '@/components/TestName.vue'
import MyTestName from "@/components/TestName";

export default {
  name: "App",
  // 组件的自定义属性
  props: [],
  // 组件的私有属性
  data() {
    return {
      message: "hello, vue.js",
      userInfo: { name: "张三", age: 20 },
      countFromSon: 0,
    };
  },
  methods: {
    toggleH1Color() {
      console.log(this);
      this.$refs.myH1.style.color = "red";
    },
    resetSubComCount() {
      console.log(this);
      this.$refs.componentTestName.resetCount();
    },
  },
  // 2. 注册组件
  components: {
    MyTestName,
  },
};
</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>

动态组件

什么是动态组件

动态组件指的是动态切换组件的显示与隐藏。

如何实现动态组件渲染

vue 提供了一个内置的<component>组件(只是一个占位符),专门用来实现动态组件的渲染。示例代码如下:

data() {
	// 1. 当前要渲染的组件名称
	return { comName: 'Left' }
}

<!-- 2. 通过is属性,动态指定要渲染的组件 -->
<component :is="comName"></component>

<!-- 3. 点击按钮,动态切换组件的名称 -->
<button @click="comName = 'Left'">展示Left组件</button>
<button @click="comName = 'Right'">展示Right组件</button>
  • component标签是Vue内置的,作用:组件的占位符
  • is属性的值,表示要渲染的组件的名字

举例说明,前提子组件TestName.vue已经创建。App.vue组件:

<template>
  <div id="app">
    <h1 ref="myH1">App根组件</h1>
    <button @click="componentToggle">展示组件TestName</button>
     <component :is="componentId"></component>
  </div>
</template>

<script>
// 1. 导入需要使用的.vue组件 import TestName from '@/components/TestName.vue'
import MyTestName from "@/components/TestName";

export default {
  name: "App",
  // 组件的自定义属性
  props: [],
  // 组件的私有属性
  data() {
    return {
      componentId: "",
    };
  },
  methods: {
    componentToggle() {
      console.log(this.componentId);
      if (this.componentId !== "MyTestName") {
        this.componentId = "MyTestName";
      } else {
        this.componentId = "";
      }
    },
  },
  // 2. 注册组件
  components: {
    MyTestName,
  },
};
</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>

使用keep-alive保持状态

默认情况下,切换动态组件时无法保持组件的状态。

此时可以使用 vue 内置的 <keep-alive> 组件保持动态组件的状态。

<keep-alive>可以把内部的组件进行缓存,而不是销毁组件。

示例代码如下:

<keep-alive>
    <component :is="componentId"></component>
</keep-alive>

举例,App.vue

<template>
  <div id="app">
    <h1 ref="myH1">App根组件</h1>
    <button @click="componentToggle">展示组件TestName</button>
    <keep-alive>
      <component :is="componentId"></component>
    </keep-alive>
  </div>
</template>

<script>
// 1. 导入需要使用的.vue组件 import TestName from '@/components/TestName.vue'
import MyTestName from "@/components/TestName";

export default {
  name: "App",
  // 组件的自定义属性
  props: [],
  // 组件的私有属性
  data() {
    return {
      componentId: "",
    };
  },
  methods: {
    componentToggle() {
      console.log(this.componentId);
      if (this.componentId !== "MyTestName") {
        this.componentId = "MyTestName";
      } else {
        this.componentId = "";
      }
    },
  },
  // 2. 注册组件
  components: {
    MyTestName,
  },
};
</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>

keep-alive对应的生命周期函数

当组件被缓存时,会自动触发组件的deactivated生命周期函数。

当组件被激活时,会自动触发组件的activated生命周期函数。

<template>
  <div id="app">
    <h1 ref="myH1">App根组件</h1>
    <button @click="componentToggle">展示组件TestName</button>
    <keep-alive>
      <component :is="componentId"></component>
    </keep-alive>
  </div>
</template>

<script>
// 1. 导入需要使用的.vue组件 import TestName from '@/components/TestName.vue'
import MyTestName from "@/components/TestName";

export default {
  name: "App",
  // 组件的自定义属性
  props: [],
  // 组件的私有属性
  data() {
    return {
      componentId: "",
    };
  },
  methods: {
    componentToggle() {
      console.log(this.componentId);
      if (this.componentId !== "MyTestName") {
        this.componentId = "MyTestName";
      } else {
        this.componentId = "";
      }
    },
  },
  // 2. 注册组件
  components: {
    MyTestName,
  },
  created() {
    console.log("TestName组件被创建了");
  },
  destroyed() {
    console.log("TestName组件被销毁了");
  },
  // 当组件第一次被创建的时候,既会执行created生命周期,也会执行activated生命周期
  // 当组件被激活的时候,只会触发activated生命周期,不再触发created生命周期,因为组件没有被重新创建
  activated() {
    console.log("组件被激活了,activated");
  },
  deactivated() {
    console.log("组件被去激活了,deactivated");
  },
};
</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>

keep-alive的include/exclude属性

include 属性用来指定:只有名称匹配的组件会被缓存。多个组件名之间使用英文的逗号分隔:

<keep-alive include="Left,Right">
    <component :is="componentId"></component>
</keep-alive>

exclude 属性用来指定:除了名称匹配的其他的组件会被缓存。多个组件名之间使用英文的逗号分隔:

<keep-alive exclude="Right">
    <component :is="componentId"></component>
</keep-alive>
  • 在使用keep-alive时候,可以通过include属性指定哪些组件需要被缓存。
  • 在使用keep-alive时候,可以通过exclude属性指定哪些组件不需要被缓存。
  • 但是,不要同时使用include和exclude这两个属性。

了解组件注册名称和组件声明时name的区别

  • 组件的“注册名称”主要应用场景:以标签的形式,把注册好的组件,渲染和使用到页面结构之中。
  • 组件声明时候的“name”名称的主要应用场景:结合<keep-alive>标签实现组件缓存功能;以及在调试工具中看到组件的name名称。
<script>
export default {
  // 当提供了name属性之后,组件的名称,就是name属性的值
  name: 'MyComponent',
  // 组件的自定义属性
  props: [],
  // 组件的私有属性
  data() {};
  },
  methods: {},
};
</script>

components属性中名称为组件的注册名称

<script>
export default {
  // 组件的自定义属性
  props: [],
  // 组件的私有属性
  data() {};
  },
  components: {
    // 如果在声明组件的时候,没有为组件指定name名称,则组件的名称默认就是“注册时候的名称”
    TestName,
  }
  methods: {},
};
</script>

插槽

什么是插槽

插槽(slot)是 vue 为组件的封装者提供的能力。允许开发者在封装组件时,把不确定的、希望由用户指定的

部分定义为插槽。

可以把插槽认为是组件封装期间,为用户预留的内容的占位符。以实现组件的复用。

体验插槽的基础用法

在封装组件时,可以通过 <slot> 元素定义插槽,从而为用户预留内容占位符。

父组件,App.vue:

<template>
  <div id="app">
    <h1>App根组件</h1>
    <MyTestName>
      <p>这是子组件的内容区域,声明在父组件的p标签中</p>
    </MyTestName>
  </div>
</template>

<script>
// 导入需要使用的.vue组件 import TestName from '@/components/TestName.vue'
import MyTestName from "@/components/TestName";

export default {
  name: "App",
  // 组件的自定义属性
  props: [],
  // 组件的私有属性
  data() {
    return {
      componentId: "",
    };
  },
  methods: {},
  // 注册组件
  components: {
    MyTestName,
  },
};
</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>

子组件,TestName.vue:

<template>
  <div class="test-container">
    <h5>Son组件</h5>
    <!-- 声明一个插槽区域 -->
    <!-- Vue官方规定:每一个slot插槽,都要有一个name名称 -->
    <!-- 如果省略了slot的name属性,则有一个默认名称,叫做default -->
    <slot name="default"></slot>
  </div>
</template>

<script>
export default {
  name: "MyComponent",
  // 组件的自定义属性
  props: [],
  // 组件的私有属性
  data() {
    return {};
  },
  methods: {},
};
</script>

<style scoped>
p {
  color: blue; /* 不加/deep/时,生成的选择器格式为 p[data-v-052242de] */
}
</style>

v-slot指令以及插槽的后备内容

在向具名插槽提供内容的时候,我们可以在一个 <template> 元素上使用 v-slot 指令,并以 v-slot 的参数的

形式提供其名称。

具名插槽的定义和使用

如果在封装组件时需要预留多个插槽节点,则需要为每个 <slot> 插槽指定具体的 name 名称。这种带有具体

名称的插槽叫做“具名插槽”。

父组件,App.vue:

<template>
  <div id="app">
    <h1>App根组件</h1>
    <MyTestName>
      <!-- 1. 如果要把内容填充到指定名称的插槽中,需要使用“v-slot:”这个指令 -->
      <!-- 2. v-slot: 后面要跟上插槽的名称 -->
      <!-- 3. v-slot: 指令不能直接用在元素身上,必须用在template标签上 -->
      <!-- 4. template这个标签,它是一个虚拟的标签,只起到包裹性质的作用,但是,不会被渲染为任何实质性的html元素 -->
      <!-- 5. v-slot: 指令的间歇形式是“#” -->
      <template v-slot:header>
        <h>滕王阁序</h>
      </template>
	
	  <!-- v-slot: 指令的间歇形式是“#” -->
      <template #default>
        <p>豫章故郡,洪都新府。</p>
        <p>星分翼轸,地接衡庐。</p>
        <p>襟三江而带五湖,控蛮荆而引瓯越。</p>
      </template>

      <template v-slot:footer>
        <p>落款:王勃</p>
      </template>
    </MyTestName>
  </div>
</template>

<script>
// 导入需要使用的.vue组件 import TestName from '@/components/TestName.vue'
import MyTestName from "@/components/TestName";

export default {
  name: "App",
  // 组件的自定义属性
  props: [],
  // 组件的私有属性
  data() {
    return {
      componentId: "",
    };
  },
  methods: {},
  // 注册组件
  components: {
    MyTestName,
  },
};
</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>

子组件,TestName.vue,包含了插槽的后备内容。

<template>
  <div class="test-container">
    <h5>Son组件</h5>
    <!-- 声明一个插槽区域 -->
    <!-- Vue官方规定:每一个slot插槽,都要有一个name名称 -->
    <!-- 如果省略了slot的name属性,则有一个默认名称,叫做default -->
    <div class="header-box">
      <slot name="header"></slot>
    </div>
    <div class="content-box">
      <!-- 在声明插槽时候,提供了一个默认的内容 -->
      <!-- 当用户没有指定内容时候,该后备内容即会生效 -->
      <slot name="default">这是default插槽的默认内容</slot>
    </div>
    <div class="footer-box">
      <slot name="footer"></slot>
    </div>
  </div>
</template>

<script>
export default {
  name: "MyComponent",
  // 组件的自定义属性
  props: [],
  // 组件的私有属性
  data() {
    return {};
  },
  methods: {},
};
</script>

<style lang="less" scoped>
.test-container {
  > div {
    min-height: 30px;
  }
  .header-box {
    background-color: pink;
    height: 10px;
  }
  .content-box {
    background-color: lightblue;
    height: 100px;
  }
  .footer-box {
    background-color: lightgreen;
    height: 10px;
  }
}
</style>

作用域插槽的基本用法

在封装组件的过程中,可以为预留的 <slot> 插槽绑定 props 数据,这种带有 props 数据的 <slot> 叫做“作用域插槽”。

父组件,App.vue

<template>
  <div id="app">
    <h1>App根组件</h1>
    <MyTestName>
      <!-- 1. 如果要把内容填充到指定名称的插槽中,需要使用“v-slot:”这个指令 -->
      <!-- 2. v-slot: 后面要跟上插槽的名称 -->
      <!-- 3. v-slot: 指令不能直接用在元素身上,必须用在template标签上 -->
      <!-- 4. template这个标签,它是一个虚拟的标签,只起到包裹性质的作用,但是,不会被渲染为任何实质性的html元素 -->
      <!-- 5. v-slot: 指令的间歇形式是“#” -->
      <template v-slot:header>
        <h>滕王阁序</h>
      </template>

      <template #default="scope">
        <p>豫章故郡,洪都新府。</p>
        <p>星分翼轸,地接衡庐。</p>
        <p>襟三江而带五湖,控蛮荆而引瓯越。</p>
        <p>{{ scope.msg }}</p>
      </template>

      <template v-slot:footer>
        <p>落款:王勃</p>
      </template>
    </MyTestName>
  </div>
</template>

<script>
// 导入需要使用的.vue组件 import TestName from '@/components/TestName.vue'
import MyTestName from "@/components/TestName";

export default {
  name: "App",
  // 组件的自定义属性
  props: [],
  // 组件的私有属性
  data() {
    return {
      componentId: "",
    };
  },
  methods: {},
  // 注册组件
  components: {
    MyTestName,
  },
};
</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>

子组件,TestName.vue

<template>
  <div class="test-container">
    <h5>Son组件</h5>
    <!-- 声明一个插槽区域 -->
    <!-- Vue官方规定:每一个slot插槽,都要有一个name名称 -->
    <!-- 如果省略了slot的name属性,则有一个默认名称,叫做default -->
    <div class="header-box">
      <slot name="header"></slot>
    </div>
    <div class="content-box">
      <!-- 在封装组件时,为预留的<slot>提供属性对应的值,这种用法,叫作作用域插槽 -->
      <slot name="default" msg="hello vue.js"></slot>
    </div>
    <div class="footer-box">
      <slot name="footer"></slot>
    </div>
  </div>
</template>

<script>
export default {
  name: "MyComponent",
  // 组件的自定义属性
  props: [],
  // 组件的私有属性
  data() {
    return {};
  },
  methods: {},
};
</script>

<style lang="less" scoped>
.test-container {
  > div {
    min-height: 30px;
  }
  .header-box {
    background-color: pink;
    height: 10px;
  }
  .content-box {
    background-color: lightblue;
    height: 130px;
  }
  .footer-box {
    background-color: lightgreen;
    height: 10px;
  }
}
</style>

作用域插槽的结构解析

作用域插槽对外提供的数据对象,可以使用解构赋值简化数据的接收过程。

父组件,App.vue

<template>
  <div id="app">
    <h1>App根组件</h1>
    <MyTestName>
      <!-- 1. 如果要把内容填充到指定名称的插槽中,需要使用“v-slot:”这个指令 -->
      <!-- 2. v-slot: 后面要跟上插槽的名称 -->
      <!-- 3. v-slot: 指令不能直接用在元素身上,必须用在template标签上 -->
      <!-- 4. template这个标签,它是一个虚拟的标签,只起到包裹性质的作用,但是,不会被渲染为任何实质性的html元素 -->
      <!-- 5. v-slot: 指令的间歇形式是“#” -->
      <template v-slot:header>
        <h>滕王阁序</h>
      </template>

      <!-- 作用域插槽对外提供的数据对象,可以通过“结构赋值”简化接收的过程 -->
      <template #default="{ msg, user }">
        <p>豫章故郡,洪都新府。</p>
        <p>星分翼轸,地接衡庐。</p>
        <p>襟三江而带五湖,控蛮荆而引瓯越。</p>
        <p>{{ msg }}</p>
        <p>{{ user }}</p>
      </template>

      <template v-slot:footer>
        <p>落款:王勃</p>
      </template>
    </MyTestName>
  </div>
</template>

<script>
// 导入需要使用的.vue组件 import TestName from '@/components/TestName.vue'
import MyTestName from "@/components/TestName";

export default {
  name: "App",
  // 组件的自定义属性
  props: [],
  // 组件的私有属性
  data() {
    return {
      componentId: "",
    };
  },
  methods: {},
  // 注册组件
  components: {
    MyTestName,
  },
};
</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>

子组件,TestName.vue

<template>
  <div class="test-container">
    <h5>Son组件</h5>
    <!-- 声明一个插槽区域 -->
    <!-- Vue官方规定:每一个slot插槽,都要有一个name名称 -->
    <!-- 如果省略了slot的name属性,则有一个默认名称,叫做default -->
    <div class="header-box">
      <slot name="header"></slot>
    </div>
    <div class="content-box">
      <!-- 在封装组件时,为预留的<slot>提供属性对应的值,这种用法,叫作作用域插槽 -->
      <slot name="default" msg="hello vue.js" :user="userInfo"></slot>
    </div>
    <div class="footer-box">
      <slot name="footer"></slot>
    </div>
  </div>
</template>

<script>
export default {
  name: "MyComponent",
  // 组件的自定义属性
  props: [],
  // 组件的私有属性
  data() {
    return {
      userInfo: {
        name: "张三",
        age: 24,
      },
    };
  },
  methods: {},
};
</script>

<style lang="less" scoped>
.test-container {
  > div {
    min-height: 30px;
  }
  .header-box {
    background-color: pink;
    height: 10px;
  }
  .content-box {
    background-color: lightblue;
    height: 170px;
  }
  .footer-box {
    background-color: lightgreen;
    height: 10px;
  }
}
</style>

自定义指令

什么是自定义指令

vue 官方提供了 v-text、v-for、v-model、v-if 等常用的指令。除此之外 vue 还允许开发者自定义指令。

vue 中的自定义指令分为两类,分别是:

  • 私有自定义指令

  • 全局自定义指令

私有自定义指令

在每个 vue 组件中,可以在 directives 节点下声明私有自定义指令。

App.vue

<template>
  <div id="app">
    <h1 v-color>App根组件</h1>
    <p>测试内容</p>
  </div>
</template>

<script>
export default {
  name: "App",
  // 组件的自定义属性
  props: [],
  // 组件的私有属性
  data() {
    return {};
  },
  // 私有自定义指令的节点
  directives: {
    // 定义名为color的指令,指向一个配置对象
    color: {
      // 当指令第一次被绑定到元素上的时候,会立即触发bind函数
      // 形参中el表示当前指令所绑定到的哪个DOM对象
      bind(el) {
        console.log(el);
        el.style.color = 'red';
      }
    },
  },
  methods: {},
  // 注册组件
  components: {},
};
</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>

使用binding.value获取指令绑定的值

App.vue

<template>
  <div id="app">
    <h1 v-color>App根组件</h1>
    <p v-color="'red'">测试内容</p>
  </div>
</template>

<script>
export default {
  name: "App",
  // 组件的自定义属性
  props: [],
  // 组件的私有属性
  data() {
    return {};
  },
  // 私有自定义指令的节点
  directives: {
    // 定义名为color的指令,指向一个配置对象
    color: {
      // 当指令第一次被绑定到元素上的时候,会立即触发bind函数
      // 形参中el表示当前指令所绑定到的哪个DOM对象
      bind(el, binding) {
        console.log(el);
        console.log(binding);
        el.style.color = binding.value ? binding.value : "blue";
      },
    },
  },
  methods: {},
  // 注册组件
  components: {},
};
</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>

update函数

App.vue

<template>
  <div id="app">
    <h1 v-color="color">App根组件</h1>
    <p v-color="'red'">测试内容</p>
    <button @click="color = 'yellow'">改变color的颜色值</button>
  </div>
</template>

<script>
export default {
  name: "App",
  // 组件的自定义属性
  props: [],
  // 组件的私有属性
  data() {
    return {
      color: "blue",
    };
  },
  // 私有自定义指令的节点
  directives: {
    // 定义名为color的指令,指向一个配置对象
    color: {
      // 当指令第一次被绑定到元素上的时候,会立即触发bind函数
      // 形参中el表示当前指令所绑定到的哪个DOM对象
      // binding函数只调用1次
      // 当指令第一次绑定到元素时调用,当DOM更新时bind函数不会被触发
      bind(el, binding) {
        // console.log(el);
        console.log(binding);
        el.style.color = binding.value;
        console.log("el.style.color: " + el.style.color);
      },
      // update函数会在每次DOM更新时被调用
      update(el, binding) {
        el.style.color = binding.value;
      },
    },
  },
  methods: {},
  // 注册组件
  components: {},
};
</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>

自定义指令-函数简写

如果 bind 和update 函数中的逻辑完全相同,则对象格式的自定义指令可以简写成函数格式:

<template>
  <div id="app">
    <h1 v-color="color">App根组件</h1>
    <p v-color="'red'">测试内容</p>
    <button @click="color = 'yellow'">改变color的颜色值</button>
  </div>
</template>

<script>
export default {
  name: "App",
  // 组件的自定义属性
  props: [],
  // 组件的私有属性
  data() {
    return {
      color: "blue",
    };
  },
  // 私有自定义指令的节点
  directives: {
    // bind 和update 函数中的逻辑完全相同,可以缩写至一个方法中
    color(el, binding) {
      // console.log(el);
      console.log(binding);
      console.log("触发了v-color的bind函数");
      el.style.color = binding.value;
    },
  },
  methods: {},
  // 注册组件
  components: {},
};
</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>

全局自定义指令(推荐)

全局共享的自定义指令需要通过“Vue.directive()”进行声明。

在main.js中进行全局声明。main.js

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

Vue.config.productionTip = false

// 私有自定义指令的节点
Vue.directive('color', (el, binding) => {
  el.style.color = binding.value;
});

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

App.vue

<template>
  <div id="app">
    <h1 v-color="color">App根组件</h1>
    <p v-color="'red'">测试内容</p>
    <button @click="color = 'yellow'">改变color的颜色值</button>
  </div>
</template>

<script>
export default {
  name: "App",
  // 组件的自定义属性
  props: [],
  // 组件的私有属性
  data() {
    return {
      color: "blue",
    };
  },
  methods: {},
  // 注册组件
  components: {},
};
</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>

路由

路由(英文:router)就是对应关系。

SPA 与前端路由

SPA 指的是一个 web 网站只有唯一的一个 HTML 页面,所有组件的展示与切换都在这唯一的一个页面内完成。

此时,不同组件之间的切换需要通过前端路由来实现。

结论:在 SPA 项目中,不同功能之间的切换,要依赖于前端路由来完成!

什么是前端路由

通俗易懂的概念:Hash 地址与组件之间的对应关系。

前端路由的工作方式

① 用户点击了页面上的路由链接

② 导致了 URL 地址栏中的 Hash 值发生了变化

③ 前端路由监听了到 Hash 地址的变化

④ 前端路由把当前 Hash 地址对应的组件渲染都浏览器中

结论:前端路由,指的是 Hash 地址与组件之间的对应关系!

实现简易的前端路由

  1. 通过 <component> 标签,结合 comName 动态渲染组件。示例代码如下:

    <!-- 通过is属性,指定要展示的组件的名称 -->
    <component :is="compName"></component>
    
    <script>
    export default {
        name: 'App',
        data() {
            return {
                // 要展示的组件的名称
                compName: 'Home'
            }
        }
    }
    </script>
    
  2. 在 App.vue 组件中,为 <a> 链接添加对应的 hash 值

    <a href="#/home">Home</a>&nbsp;
    <a href="#/movie">Movie</a>&nbsp;
    <a href="#/about">About</a>
    
  3. 在 created 生命周期函数中,监听浏览器地址栏中 hash 地址的变化,动态切换要展示的组件的名称。

    <script>
    export default {
        created() {
            window.onhashchange = () => {
                switch (location.hash) {
                    case '#/home': // 单击了“首页”的链接
                        this.compName = 'Home';
                        break;
    				case '#/movie': // 单击了“电影”的链接
                        this.compName = 'Movie';
                        break;
    				case '#/about': // 单击了“关于”的链接
                        this.compName = 'About';
                        break;  
                }
            }
        }
    }
    </script>
    

举例说明

  1. 在src/components文件夹中,新建子组件CompHome.vue

    <template>
      <div class="home-container">
        <h3>Home组件</h3>
      </div>
    </template>
    
    <script>
    export default {
      methods: {},
    };
    </script>
    
    <style lang="less" scoped>
    .home-container {
      background-color: skyblue;
      min-height: 200px;
    }
    </style>
    
    
  2. 在src/components文件夹中,新建子组件CompMovie.vue

    <template>
      <div class="movie-container">
        <h3>Movie组件</h3>
      </div>
    </template>
    
    <script>
    export default {
      methods: {},
    };
    </script>
    
    <style lang="less" scoped>
    .movie-container {
      background-color: lightgreen;
      min-height: 200px;
    }
    </style>
    
    
  3. 在src/components文件夹中,新建子组件CompAbout.vue

    <template>
      <div class="about-container">
        <h3>About组件</h3>
      </div>
    </template>
    
    <script>
    export default {
      methods: {},
    };
    </script>
    
    <style lang="less" scoped>
    .about-container {
      background-color: orange;
      min-height: 200px;
    }
    </style>
    
    
  4. 修改App.vue组件

    <template>
      <div id="app">
        <h1>App根组件</h1>
        <div class="app-container">
          <a href="#/home">首页</a>
          <a href="#/movie">电影</a>
          <a href="#/about">关于</a>
          <hr />
    
          <component :is="compName"></component>
        </div>
      </div>
    </template>
    
    <script>
    import Home from "@/components/CompHome.vue";
    import Movie from "@/components/CompMovie.vue";
    import About from "@/components/CompAbout.vue";
    
    export default {
      name: "App",
      components: {
        Home,
        Movie,
        About,
      },
      data() {
        return {
          compName: '',
        }
      },
      created() {
        window.onhashchange = () => {
          switch (location.hash) {
            case "#/home": // 单击了“首页”的链接
              this.compName = "Home";
              break;
            case "#/movie": // 单击了“电影”的链接
              this.compName = "Movie";
              break;
            case "#/about": // 单击了“关于”的链接
              this.compName = "About";
              break;
          }
        };
      },
    };
    </script>
    
    <style lang="less" scoped>
    .app-container {
      background-color: #efefef;
      overflow: hidden;
      margin: 10px;
      padding: 15px;
      > a {
        margin-right: 10px;
      }
    }
    </style>
    
    

vue-router的基本用法

vue-router 是 vue.js 官方给出的路由解决方案。它只能结合 vue 项目进行使用,能够轻松的管理 SPA 项目

中组件的切换。

vue-router 的官方文档地址:https://router.vuejs.org/zh/

vue-router安装和配置的步骤

① 安装 vue-router 包

② 创建路由模块

③ 导入并挂载路由模块

④ 声明路由链接和占位符

  1. 项目中安装vue-router。可以执行以下命令安装:

    npm i vue-router -S
    
  2. 创建路由模块。在src源代码目录下,新建router/index.js 路由模块,并初始化如下的代码:

    // src/router/index.js 就是当前项目的路由模块
    // 1. 导入Vue和VueRouter的包
    import Vue from 'vue'
    import VueRouter from 'vue-router'
    
    // 2. 把 VueRouter 安装为 Vue 项目的插件
    // Vue.use() 函数的作用,就是来安装插件的
    Vue.use(VueRouter)
    
    // 3. 创建路由的实例对象
    const router = new VueRouter()
    
    // 4. 向外共享路由的实例对象
    export default router
    
    

    如果需要加上组件的路由,可以在创建路由的实例对象中,增加一个routes属性,定义hash地址和组件之间的对应关系

    // src/router/index.js 就是当前项目的路由模块
    // 导入Vue和VueRouter的包
    import Vue from 'vue'
    import VueRouter from 'vue-router'
    
    // 导入需要路由的组件
    import Home from '@/components/CompHome.vue'
    import Movie from '@/components/CompMovie.vue'
    import About from '@/components/CompAbout.vue'
    
    // 把 VueRouter 安装为 Vue 项目的插件
    // Vue.use() 函数的作用,就是来安装插件的
    Vue.use(VueRouter)
    
    // 创建路由的实例对象
    const router = new VueRouter({
        // routes是一个数组,作用:定义“hash地址”与“组件”之间的对应关系
        routes: [
    		// 重定向的路由规则
            { path: '/', component: Home },
    		// 路由规则
            { path: '/home', component: Home },
            { path: '/movie', component: Movie },
            { path: '/about', component: About }
        ]
    })
    
    // 向外共享路由的实例对象
    export default router
    
    
  3. 导入并挂载路由模块。在src/main.js 入口文件中,导入并挂载路由模块。示例代码如下:

    import Vue from 'vue'
    import App from './App2.vue'
    // 导入路由模块,目的:拿到路由的实例对象
    // 在进行模块化导入的时候,如果给定的是文件夹,则默认导入这个文件夹下,名字叫做 index.js 的文件,因此此处可以直接引导到文件夹,不用引导到具体的文件中
    import router from '@/router'
    
    Vue.config.productionTip = false
    
    new Vue({
      render: h => h(App),
      // 在 Vue 项目中,要想把路由用起来,必须把路由实例对象,通过下面的方式进行挂载
      // router: 路由的实例对象
      router
    }).$mount('#app')
    
    

声明路由链接和占位符

在src/App.vue 组件中,使用vue-router 提供的<router-link><router-view> 声明路由链接和占位符:

<template>
  <div id="app">
    <h1>App根组件</h1>
    <div class="app-container">
      <!-- <a href="#/home">首页</a>> -->
      <router-link to="/home">首页</router-link>>
      <router-link to="/movie">电影</router-link>>
      <router-link to="/about">关于</router-link>>
      <hr />

      <!-- 只要在项目中安装和配置了vue-router,就可以使用router-view这个组件了 -->
      <!-- 它的作用很单纯,就是占位符 -->
      <router-view></router-view>
    </div>
  </div>
</template>

路由重定向

路由重定向指的是:用户在访问地址A 的时候,强制用户跳转到地址C ,从而展示特定的组件页面。

通过路由规则的redirect属性,指定一个新的路由地址,可以很方便地设置路由的重定向:

router/index.js中修改如下:

// src/router/index.js 就是当前项目的路由模块
// 导入Vue和VueRouter的包
import Vue from 'vue'
import VueRouter from 'vue-router'

// 导入需要路由的组件
import Home from '@/components/CompHome.vue'
import Movie from '@/components/CompMovie.vue'
import About from '@/components/CompAbout.vue'

// 把 VueRouter 安装为 Vue 项目的插件
// Vue.use() 函数的作用,就是来安装插件的
Vue.use(VueRouter)

// 创建路由的实例对象
const router = new VueRouter({
    // routes是一个数组,作用:定义“hash地址”与“组件”之间的对应关系
    routes: [
		// 重定向的路由规则
        { path: '/', component: Home },
		// 路由规则
        { path: '/home', component: Home },
        { path: '/movie', component: Movie },
        { path: '/about', component: About }
    ]
})

// 向外共享路由的实例对象
export default router

嵌套路由

通过路由实现组件的嵌套展示,叫作嵌套路由。

声明子路由链接和子路由占位符

在 About.vue 组件中,声明 tab1 和 tab2 的子路由链接以及子路由占位符。

<template>
    <div class="ablut-container">
        <h3>About组件</h3>
        <!-- 1. 在关于页面中,声明两个子路由链接 -->
        <router-link to="/about/tab1">tab1</router-link>
        <router-link to="/about/tab2">tab2</router-link>

        <hr />

        <!-- 2. 在关于页面中,声明子路由的占位符 -->
        <router-view></router-view>
    </div>
</template>

通过 children 属性声明子路由规则

在 src/router/index.js 路由模块中,导入需要的组件,并使用 children 属性声明子路由规则:

import Tab1 from '@components/tabs.Tab1.vue'
import Tab2 from '@components/tabs.Tab2.vue'

const router = new VueRouter({
    routes: [
        { // about页面的路由规则(父级路由规则)
            path: '/about',
            component: About,
            children: [ // 1. 通过children属性,嵌套声明子级路由规则
                { path: 'tab1', components: Tab1 }, // 2. 访问/about/tab1时,展示Tab1组件
                { path: 'tab2', components: Tab2 }, // 2. 访问/about/tab2时,展示Tab2组件
            ]
        }
    ]
})

完整示例 *

  1. 新建Vue CLI工程,参见[新建Vue CLI工程(带路由工程)【模板】](# 新建Vue CLI工程(带路由工程)【模板】)。

  2. 在src/views文件夹中,新建子组件CompHome.vue

    <template>
      <div class="home-container">
        <h3>Home组件</h3>
      </div>
    </template>
    
    <script>
    export default {
      methods: {},
    };
    </script>
    
    <style lang="less" scoped>
    .home-container {
      background-color: skyblue;
      min-height: 200px;
    }
    </style>
    
    
  3. 在src/views文件夹中,新建子组件CompMovie.vue

    <template>
      <div class="movie-container">
        <h3>Movie组件</h3>
      </div>
    </template>
    
    <script>
    export default {
      methods: {},
    };
    </script>
    
    <style lang="less" scoped>
    .movie-container {
      background-color: lightgreen;
      min-height: 200px;
    }
    </style>
    
    
  4. 在src/views文件夹中,新建子组件CompAbout.vue

    <template>
      <div class="about-container">
        <h3>About组件</h3>
        <!-- 子级路由链接 -->
        <router-link to="/about/tab1">Tab1</router-link> |
        <router-link to="/about/tab2">Tab2</router-link>
        <hr />
        <!-- 子级路由占位符 -->
        <router-view></router-view>
      </div>
    </template>
    
    <script>
    export default {
      methods: {},
    };
    </script>
    
    <style lang="less" scoped>
    .about-container {
      background-color: orange;
      min-height: 200px;
    }
    </style>
    
    
  5. 在src/views文件夹中,新建文件夹tabs。

  6. 在tabs文件夹中新建子组件CompTab1.vue。

    <template>
      <div class="tab1-container">
        <h3>About中的Tab1组件</h3>
      </div>
    </template>
    
    <script>
    export default {
      methods: {},
    };
    </script>
    
    <style lang="less" scoped>
    .tab1-container {
      background-color: royalblue;
      min-height: 100px;
    }
    </style>
    
    
  7. 在tabs文件夹中新建子组件CompTab2.vue。

    <template>
      <div class="about-container">
        <h3>About中的Tab2组件</h3>
      </div>
    </template>
    
    <script>
    export default {
      methods: {},
    };
    </script>
    
    <style lang="less" scoped>
    .about-container {
      background-color: rebeccapurple;
      min-height: 100px;
    }
    </style>
    
    
  8. 在router/indes.js中,创建路由规则。

    import Vue from 'vue'
    import VueRouter from 'vue-router'
    import CompHome from '@/views/CompHome.vue'
    import CompMovie from '@/views/CompMovie.vue'
    import CompAbout from '@/views/CompAbout.vue'
    import CompTab1 from '@/views/tabs/CompTab1'
    import CompTab2 from '@/views/tabs/CompTab2'
    
    Vue.use(VueRouter)
    
    const routes = [
      // 重定向的路由规则
      { path: '/', name: 'CompHome', component: CompHome },
      // 路由规则
      { path: '/home', name: 'CompHome', component: CompHome },
      { path: '/movie', name: 'CompMovie', component: CompMovie },
      {
        path: '/about',
        name: 'CompAbout',
        component: CompAbout,
        // redirect: '/about/tab1', // 默认子路由方式一:增加重定向功能
        children: [
          // 子路由规则,注意path不能加/前缀
          // 默认子路由方式二:如果children数组中,某个路由规则的path值为空字符串,则这条路由规则路由为默认子路由
          { path: '', name: 'CompTab1', component: CompTab1 },
          { path: 'tab1', name: 'CompTab1', component: CompTab1 },
          { path: 'tab2', name: 'CompTab2', component: CompTab2 },
        ]
      },
    ]
    
    const router = new VueRouter({
      routes
    })
    
    export default router
    
    
  9. 修改App.vue根组件内容。

    <template>
      <div id="app">
        <nav>
          <router-link to="/home">Home</router-link> |
          <router-link to="/movie">Movie</router-link> |
          <router-link to="/about">About</router-link>
        </nav>
        <router-view/>
      </div>
    </template>
    
    <style lang="less">
    #app {
      font-family: Avenir, Helvetica, Arial, sans-serif;
      -webkit-font-smoothing: antialiased;
      -moz-osx-font-smoothing: grayscale;
      text-align: center;
      color: #2c3e50;
    }
    
    nav {
      padding: 30px;
    
      a {
        font-weight: bold;
        color: #2c3e50;
    
        &.router-link-exact-active {
          color: #42b983;
        }
      }
    }
    </style>
    
    

动态路由

动态路由的概念

动态路由指的是:把 Hash 地址中可变的部分定义为参数项,从而提高路由规则的复用性。
在 vue-router 中使用英文的冒号(:)来定义路由的参数项。router/index.js示例代码如下:

// 路由中的动态参数以:进行声明,冒号后面的是动态参数的名称,id为自定义的一个参数
{ path: 'movie/:id', component: Movie }

// 将以下3个路由规则,合并成了一个,提高了路由规则的复用性
{ path: 'movie/1', component: Movie }
{ path: 'movie/2', component: Movie }
{ path: 'movie/3', component: Movie }

示例,以嵌套路由中的完整示例修改说明。

  1. App.vue修改为如下所示。

    <template>
      <div id="app">
        <nav>
          <router-link to="/home">Home</router-link> |
          <!-- 注意1:在hash地址中,/后面的参数项,叫作路径参数 -->
          <!-- 在路由参数对象中,需要使用 this.$route.param来访问路径参数 -->
          <!-- 注意2:在hash地址中,?后面的参数项,叫作查询参数 -->
          <!-- 在查询参数对象中,需要使用 this.$route.query来访问查询参数 -->
          <!-- 注意3:在this.$route中,path只是路径部分,fullPath是完整的地址 -->
          <router-link to="/movie/1">Movie-洛神</router-link> |
          <router-link to="/movie/2?name=zs&age=24">Movie-雷神</router-link> |
          <router-link to="/movie/3">Movie-复联</router-link> |
          <router-link to="/about">About</router-link>
        </nav>
        <router-view/>
      </div>
    </template>
    
    <style lang="less">
    #app {
      font-family: Avenir, Helvetica, Arial, sans-serif;
      -webkit-font-smoothing: antialiased;
      -moz-osx-font-smoothing: grayscale;
      text-align: center;
      color: #2c3e50;
    }
    
    nav {
      padding: 30px;
    
      a {
        font-weight: bold;
        color: #2c3e50;
    
        &.router-link-exact-active {
          color: #42b983;
        }
      }
    }
    </style>
    
    
  2. 在router/indes.js中,修改为如下所示。

    import Vue from 'vue'
    import VueRouter from 'vue-router'
    import CompHome from '@/views/CompHome.vue'
    import CompMovie from '@/views/CompMovie.vue'
    import CompAbout from '@/views/CompAbout.vue'
    import CompTab1 from '@/components/tabs/CompTab1'
    import CompTab2 from '@/components/tabs/CompTab2'
    
    Vue.use(VueRouter)
    
    const routes = [
      // 重定向的路由规则
      { path: '/', name: 'CompHome', component: CompHome },
      // 路由规则
      { path: '/home', name: 'CompHome', component: CompHome },
      // 需求:在Movie组件中,希望根据mid的值,展示对应电影的详情信息
      { path: '/movie/:mid', name: 'CompMovie', component: CompMovie },
      {
        path: '/about',
        name: 'CompAbout',
        component: CompAbout,
        // redirect: '/about/tab1', // 默认子路由方式一:增加重定向功能
        children: [
          // 子路由规则,注意path不能加/前缀
          // 默认子路由方式二:如果children数组中,某个路由规则的path值为空字符串,则这条路由规则路由为默认子路由
          { path: '', name: 'CompTab1', component: CompTab1 },
          { path: 'tab1', name: 'CompTab1', component: CompTab1 },
          { path: 'tab2', name: 'CompTab2', component: CompTab2 },
        ]
      },
    ]
    
    const router = new VueRouter({
      routes
    })
    
    export default router
    
    

方式一:$route.params 参数对象

在动态路由渲染出来的组件中,可以使用 this.$route.params 对象访问到动态匹配的参数值。
在CompMovie.vue组件中接收参数。

<template>
  <div class="movie-container">
    <h3>Movie组件</h3>
    <h3>this.$route.params.xx方式:路径参数 -- {{ this.$route.params.mid }}</h3>
    <h3>this.$route.query.xx方式:路径参数 -- {{ this.$route.query }}</h3>
    <button @click="showThis">打印this</button>
  </div>
</template>

<script>
export default {
  methods: {
    showThis() {
      console.log(this);
    }
  },
};
</script>

<style lang="less" scoped>
.movie-container {
  background-color: lightgreen;
  min-height: 200px;
}
</style>

方式二:使用props接收路由参数

  1. 修改router/index.js,为Movie组件开启props传参。

    import Vue from 'vue'
    import VueRouter from 'vue-router'
    import CompHome from '@/views/CompHome.vue'
    import CompMovie from '@/views/CompMovie.vue'
    import CompAbout from '@/views/CompAbout.vue'
    import CompTab1 from '@/components/tabs/CompTab1'
    import CompTab2 from '@/components/tabs/CompTab2'
    
    Vue.use(VueRouter)
    
    const routes = [
      // 重定向的路由规则
      { path: '/', name: 'CompHome', component: CompHome },
      // 路由规则
      { path: '/home', name: 'CompHome', component: CompHome },
      // 为Movie组件开启props传参,从而方便的拿到动态参数的值
      { path: '/movie/:mid', name: 'CompMovie', component: CompMovie, props: true },
      {
        path: '/about',
        name: 'CompAbout',
        component: CompAbout,
        // redirect: '/about/tab1', // 默认子路由方式一:增加重定向功能
        children: [
          // 子路由规则,注意path不能加/前缀
          // 默认子路由方式二:如果children数组中,某个路由规则的path值为空字符串,则这条路由规则路由为默认子路由
          { path: '', name: 'CompTab1', component: CompTab1 },
          { path: 'tab1', name: 'CompTab1', component: CompTab1 },
          { path: 'tab2', name: 'CompTab2', component: CompTab2 },
        ]
      },
    ]
    
    const router = new VueRouter({
      routes
    })
    
    export default router
    
    
  2. 在CompMovie.vue组件中通过props自定义属性接收参数。

    <template>
      <div class="movie-container">
        <h3>Movie组件 -- {{ mid }}</h3>
        <h3>this.$route.params.xx:路径参数 -- {{ this.$route.params.mid }}</h3>
        <h3>this.$route.query.xx:查询参数 -- {{ this.$route.query }}</h3>
        <h3>this.$route.fullPath:完整地址 -- {{ this.$route.fullPath }}</h3>
        <button @click="showThis">打印this</button>
      </div>
    </template>
    
    <script>
    export default {
      props: ["mid"],
      methods: {
        showThis() {
          console.log(this);
        }
      },
    };
    </script>
    
    <style lang="less" scoped>
    .movie-container {
      background-color: lightgreen;
      min-height: 200px;
    }
    </style>
    
    

声明式导航&编程式导航

  • 在浏览器中,点击链接实现导航的方式,叫做声明式导航。例如:
    普通网页中点击 <a> 链接、vue 项目中点击 <router-link> 都属于声明式导航。

  • 在浏览器中,调用 API 方法实现导航的方式,叫做编程式导航。例如:
    普通网页中调用 location.href 跳转到新页面的方式,属于编程式导航。

vue-router中的编程式导航API

vue-router 提供了许多编程式导航的 API,其中最常用的导航 API 分别是:

  • this.$router.push('hash 地址')
    • 跳转到指定 hash 地址,并增加一条历史记录
  • this.$router.replace('hash 地址')
    • 跳转到指定的 hash 地址,并替换掉当前的历史记录
  • this.$router.go(数值 n)
    • 实现导航历史前进、后退

$router.push跳转指定页面

调用 this.$router.push() 方法,可以跳转到指定的 hash 地址,从而展示对应的组件页面。

会增加一个历史记录,即可以返回上一个网址中。

修改上述示例的CompHome.vue组件为例:

<template>
  <div class="home-container">
    <h3>Home组件</h3>
    <button @click="gotoMovie">跳转到“洛神”页面</button>
  </div>
</template>

<script>
export default {
  methods: {
    gotoMovie() {
      // 通过编程式导航跳转到指定页面
      this.$router.push('/movie/1');
    }
  },
};
</script>

<style lang="less" scoped>
.home-container {
  background-color: skyblue;
  min-height: 200px;
}
</style>

$router.replace跳转指定页面

调用 this.$router.replace() 方法,可以跳转到指定的 hash 地址,从而展示对应的组件页面。

修改上述示例的CompHome.vue组件为例:

<template>
  <div class="home-container">
    <h3>Home组件</h3>
    <button @click="gotoMovie">跳转到“洛神”页面</button>
  </div>
</template>

<script>
export default {
  methods: {
    gotoMovie() {
      // 通过编程式导航跳转到指定页面
      this.$router.replace('/movie/1');
    }
  },
};
</script>

<style lang="less" scoped>
.home-container {
  background-color: skyblue;
  min-height: 200px;
}
</style>

$router.go()

调用 this.$router.go() 方法,可以在浏览历史中前进和后退。

<template>
  <div class="home-container">
    <h3>Home组件</h3>
    <button @click="goBack">后退</button>
  </div>
</template>

<script>
export default {
  methods: {
    gotoMovie() {
      // 后退到之前的组件页面
      // 如果后退的层数超过上限,则原地不动
      this.$router.goBack('-1');
    }
  },
};
</script>

$router.go 的简化用法:

在实际开发中,一般只会前进和后退一层页面。因此 vue-router 提供了如下两个便捷方法:

  • $router.back():在历史记录中,后退到上一个页面
  • $router.forward():在历史记录中,前进到下一个页面

导航守卫

导航守卫可以控制路由的访问权限。

全局前置守卫

每次发生路由的导航跳转时,都会触发全局前置守卫。因此,在全局前置守卫中,程序员可以对每个路由进行访问权限的控制(router/index.js):

// 创建路由实例对象
const router = new VueRouter({ ... });
                              
// 调用路由实例对象的beforeEach方法,即可声明“全局前置守卫”
// 每次发生路由导航跳转的时候,都会自动触发fn这个“回调函数”
router.beforeEach(fn);

守卫方法的3个形参(控制后台主页的访问权限)

router/index.js:

// 为 router 实例对象,声明全局前置导航守卫
// 只要发生了路由的跳转,必然会触发 beforeEach 指定的 function 回调函数
router.beforeEach(function(to, from, next) {
  // to 表示将要访问的路由的信息对象
  // from 表示将要离开的路由的信息对象
  // next() 函数表示放行的意思
  // 分析:
  // 1. 要拿到用户将要访问的 hash 地址
  // 2. 判断 hash 地址是否等于 /main。
  // 2.1 如果等于 /main,证明需要登录之后,才能访问成功
  // 2.2 如果不等于 /main,则不需要登录,直接放行  next()
  // 3. 如果访问的地址是 /main。则需要读取 localStorage 中的 token 值
  // 3.1 如果有 token,则放行
  // 3.2 如果没有 token,则强制跳转到 /login 登录页
  if (to.path === '/main') {
    // 要访问后台主页,需要判断是否有 token
    const token = localStorage.getItem('token')
    if (token) {
      next()
    } else {
      // 没有登录,强制跳转到登录页
      next('/login')
    }
  } else {
    next()
  }
})

next函数的 3 种调用方式

  • 当前用户拥有后台主页的访问权限,直接放行:next()
  • 当前用户没有后台主页的访问权限,强制其跳转到登录页面:next('/login')
  • 当前用户没有后台主页的访问权限,不允许跳转到后台主页:next(false)

Vant组件库

Vant组件库基本使用【模板】

  • 参考:Vant组件库:轻量、可靠的移动端 Vue 组件库。
  1. 新建VUE CLI工程。

  2. 参考快速上手 - Vant 3。包含安装Vant、安装vite-plugin-style-import插件(自动按需引入组件的样式)、配置插件。

  3. 以Button按钮为例说明。

    1. 在main.js中注册全局组件,增加以下代码。

      import { Button } from "vant";
      
      Vue.use(Button);
      
    2. 在应用组件中直接使用该组件即可。例如在App.vue中使用Button组件。

      <van-button type="primary">主要按钮</van-button>
      
    3. 重新编译代码

      npm run serve
      

案例说明

  1. 新建VUE CLI工程,带路由和less功能。

  2. 配置初始工程。

    1. 删除components文件夹中的文件。
    2. 删除views文件夹中的文件。
    3. 删除router/index.js中的默认路由配置。
    4. F12切换浏览器为移动端的浏览效果。
  3. 安装和导入Vant组件以及vite-plugin-style-import插件及插件配置。

  4. 在views文件夹,新建Home文件夹和CompHome.vue文件。

    <template>
      <div class="home-container">
        <h3>Home组件</h3>
      </div>
    </template>
    
    <script>
    export default {};
    </script>
    
    <style lang="less" scoped></style>
    
    
  5. 在views文件夹,新建User文件夹和CompUser.vue文件。

    <template>
      <div class="user-container">
        <h3>User组件</h3>
      </div>
    </template>
    
    <script>
    export default {};
    </script>
    
    <style lang="less" scoped></style>
    
    
  6. main.js中引入组件。

    import Vue from 'vue'
    import App from './App.vue'
    import router from './router'
    import { Button } from 'vant';
    import { Tabbar, TabbarItem } from 'vant';
    
    Vue.use(Button);
    Vue.use(Tabbar);
    Vue.use(TabbarItem);
    
    Vue.config.productionTip = false
    
    new Vue({
      router,
      render: h => h(App)
    }).$mount('#app')
    
    
  7. 使用Tabbar 标签栏组件,并开启路由模式。App.vue

    <template>
      <div id="app">
        <h1>App根组件</h1>
        <van-button type="primary">主要按钮</van-button>
        <van-button type="success">成功按钮</van-button>
    
        <router-view />
    
        <van-tabbar route>
          <van-tabbar-item replace to="/home" icon="home-o">首页</van-tabbar-item>
          <van-tabbar-item replace to="/user" icon="user-o">我的</van-tabbar-item>
        </van-tabbar>
      </div>
    </template>
    
    <style lang="less" scoped>
    #app {
      font-family: Avenir, Helvetica, Arial, sans-serif;
      -webkit-font-smoothing: antialiased;
      -moz-osx-font-smoothing: grayscale;
      text-align: center;
      color: #2c3e50;
    }
    </style>
    
    
  8. 配置路由规则。router/index.js。

    import Vue from 'vue'
    import VueRouter from 'vue-router'
    import Home from '@/views/Home/CompHome.vue'
    import User from '@/views/User/CompUser.vue'
    
    Vue.use(VueRouter)
    
    // 路由规则配置
    const routes = [
      // 定义“首页”的路由规则
      { path: '/', component: Home },
      { path: '/home', component: Home },
      // 定义“我的”的路由规则
      { path: '/user', component: User },
    ]
    
    const router = new VueRouter({
      routes
    })
    
    export default router
    
    

VueX使用

Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式

  1. Vuex 的状态存储是响应式的。当 Vue 组件从 store 中读取状态的时候,若 store 中的状态发生变化,那么相应的组件也会相应地得到高效更新。
  2. 你不能直接改变 store 中的状态。改变 store 中的状态的唯一途径就是显式地提交 (commit) mutation。这样使得我们可以方便地跟踪每一个状态的变化,从而让我们能够实现一些工具帮助我们更好地了解我们的应用。

操作步骤:

  1. 新建VUE CLI工程,带路由、Vuex和less功能。

  2. 配置初始工程。

    1. components/HelloWorld.vue中,删除多余的展示内容,只保留msg的信息。
    2. views/HomeView.vue中,只保留<HelloWorld msg="Welcome to Your Vue.js App"/>的展示内容。

state

  1. store/index.js中新增一个状态num。

    import Vue from 'vue'
    import Vuex from 'vuex'
    
    Vue.use(Vuex)
    
    export default new Vuex.Store({
      // state相当于组件中的data,专门用来存放全局的数据
      state: { // 用来存放数据,类似于组件中的data
        num: 0,
      },
      getters: { // 计算属性,类似于组件中的computed
      },
      mutations: { // 存放方法,类似于组件中的methods
      },
      actions: { // Action类似于mutation,不同在于:Action提交的是mutation,而不是直接变更状态
      },
      modules: {
      }
    })
    
    
  2. 修改App.vue内容。

    <template>
      <div id="app">
        <h3>App根组件的数字:{{ num }}</h3>
        <nav>
          <router-link to="/">Home</router-link> |
          <router-link to="/about">About</router-link>
        </nav>
        <router-view />
      </div>
    </template>
    
    <script>
    import store from "./store";
    export default {
      computed: {
        num() {
          return store.state.num;
        }
      },
    };
    </script>
    
    <style lang="less">
    #app {
      font-family: Avenir, Helvetica, Arial, sans-serif;
      -webkit-font-smoothing: antialiased;
      -moz-osx-font-smoothing: grayscale;
      text-align: center;
      color: #2c3e50;
    }
    
    nav {
      padding: 30px;
    
      a {
        font-weight: bold;
        color: #2c3e50;
    
        &.router-link-exact-active {
          color: #42b983;
        }
      }
    }
    </style>
    
    
  3. 修改components/HomeView.vue内容。

    <template>
      <div class="home">
        <HelloWorld msg="Welcome to Your Vue.js App"/>
        <p>Home页面的数字:{{ $store.state.num }}</p>
      </div>
    </template>
    
    <script>
    // @ is an alias to /src
    import HelloWorld from '@/components/HelloWorld.vue'
    
    export default {
      name: 'HomeView',
      components: {
        HelloWorld
      }
    }
    </script>
    
    
  4. 修改components/AboutView.vue内容(通过计算属性的方式进行展示)。

    <template>
      <div class="about">
        <h1>This is an about page</h1>
        <p>About页面的数字:{{ num }}</p>
      </div>
    </template>
    
    <script>
    export default {
      data() {
        return {}
      },
      computed: {
        num() {
          return this.$store.state.num;
        },
      },
    };
    </script>
    
    

getters

将组件中统一使用的computed都放到getters里面来操作。

  1. 修改store/index.js内容。

    import Vue from 'vue'
    import Vuex from 'vuex'
    
    Vue.use(Vuex)
    
    export default new Vuex.Store({
      // state相当于组件中的data,专门用来存放全局的数据
      state: {
        num: 10,
        toDos: [
          { id: 1, text: 'text1...', done: true },
          { id: 2, text: 'text2...', done: false },
          { id: 3, text: 'text3...', done: true },
        ],
      },
      // getters相当于组件中的computed,getters是全局的,computed是组件内部使用的
      getters: {
        // 通过属性访问
        getNum(state) {
          return state.num;
        },
        // 通过属性访问
        doneTodos(state) {
          return state.toDos.filter((todo) => todo.done);
        },
     // 通过属性访问
        // Getter 也可以接受其他 getter 作为第二个参数
        getTodosCount(state, getters) {
          return getters.doneTodos.length;
        },
        // 通过方法访问
        getTodoById: (state) => (id) => {
          return state.toDos.filter((todo) => todo.id === id);
        }
      },
      mutations: {
      },
      actions: {
      },
      modules: {
      }
    })
    
    
  2. 修改App.vue内容。

    <template>
      <div id="app">
        <h3>App根组件的数字:{{ $store.getters.getNum }}</h3>
        <nav>
          <router-link to="/">Home</router-link> |
          <router-link to="/about">About</router-link>
        </nav>
        <router-view />
      </div>
    </template>
    
    <script>
    
    export default {};
    </script>
    
    <style lang="less">
    #app {
      font-family: Avenir, Helvetica, Arial, sans-serif;
      -webkit-font-smoothing: antialiased;
      -moz-osx-font-smoothing: grayscale;
      text-align: center;
      color: #2c3e50;
    }
    
    nav {
      padding: 30px;
    
      a {
        font-weight: bold;
        color: #2c3e50;
    
        &.router-link-exact-active {
          color: #42b983;
        }
      }
    }
    </style>
    
    
  3. 修改components/HomeView.vue内容。

    <template>
      <div class="home">
        <HelloWorld msg="Welcome to Your Vue.js App" />
        <p>Home页面的数字:{{ $store.getters.getNum }}</p>
        <p>doneTodos:{{ $store.getters.doneTodos }}</p>
        <p>getTodosCount:{{ $store.getters.getTodosCount }}</p>
        <p>getTodoById:{{ $store.getters.getTodoById(2) }}</p>
      </div>
    </template>
    
    <script>
    // @ is an alias to /src
    import HelloWorld from "@/components/HelloWorld.vue";
    
    export default {
      name: "HomeView",
      components: {
        HelloWorld,
      },
    };
    </script>
    
    
  4. 修改components/AboutView.vue内容(通过计算属性的方式进行展示)。

    <template>
      <div class="about">
        <h1>This is an about page</h1>
        <p>About页面的数字:{{ num }}</p>
        <p>getTodosCount:{{ getTodosCount }}</p>
      </div>
    </template>
    
    <script>
    export default {
      data() {
        return {};
      },
      computed: {
        num() {
          return this.$store.getters.getNum;
        },
        getTodosCount() {
          return this.$store.getters.getTodosCount;
        },
      },
    };
    </script>
    
    

mutations

更改 Vuex 的 store 中的状态的唯一方法是提交 mutation。Vuex 中的 mutation 非常类似于事件:每个 mutation 都有一个字符串的事件类型 (type)和一个回调函数 (handler) 。这个回调函数就是我们实际进行状态更改的地方,并且它会接受 state 作为第一个参数。

一条重要的原则就是要记住 mutation 必须是同步函数

提交负荷

  1. 修改store/index.js内容。

    import Vue from 'vue'
    import Vuex from 'vuex'
    
    Vue.use(Vuex)
    
    export default new Vuex.Store({
      // state相当于组件中的data,专门用来存放全局的数据
      state: {
        num: 10,
        toDos: [
          { id: 1, text: 'text1...', done: true },
          { id: 2, text: 'text2...', done: false },
          { id: 3, text: 'text3...', done: true },
        ],
      },
      // getters相当于组件中的computed,getters是全局的,computed是组件内部使用的
      getters: {
        // 通过属性访问
        getNum(state) {
          return state.num;
        },
        // 通过属性访问
        doneTodos(state) {
          return state.toDos.filter((todo) => todo.done);
        },
        // 通过属性访问
        // Getter 也可以接受其他 getter 作为第二个参数
        getTodosCount(state, getters) {
          return getters.doneTodos.length;
        },
        // 通过方法访问
        getTodoById: (state) => (id) => {
          return state.toDos.filter((todo) => todo.id === id);
        }
      },
      mutations: {
        numPlusOne(state) {
          state.num++;
        },
        // 可以向 store.commit 传入额外的参数,即 mutation 的载荷(payload)
        numPlusX(state, x) {
          // x (payload)是一个形参,如果组件在commit时,有传这个参数过来,就存在,如果没有传过来,就是undefined
          state.num += x ? x : 1;
        },
        // 在大多数情况下,载荷应该是一个对象,这样可以包含多个字段并且记录的 mutation 会更易读
        numPlusObjectX(state, payload) {
          state.num += payload.amount;
        }
      },
      actions: {
      },
      modules: {
      }
    })
    
    
  2. 修改views/HomeView.vue内容。

    <template>
      <div class="home">
        <HelloWorld msg="Welcome to Your Vue.js App" />
        <p>Home页面的数字:{{ $store.getters.getNum }}</p>
        <p>doneTodos:{{ $store.getters.doneTodos }}</p>
        <p>getTodosCount:{{ $store.getters.getTodosCount }}</p>
        <p>getTodoById:{{ $store.getters.getTodoById(2) }}</p>
    
        <button @click="numPlusOne">num数值+1</button>
        <button @click="$store.commit('numPlusX', 2)">num数值+X</button>
        <button @click="$store.commit('numPlusObjectX', { amount: 3 })">num数值+ObjX</button>
        <button @click="$store.commit({type: 'numPlusObjectX', amount: 3 })">num数值+ObjX(对象风格提交方式)</button>
      </div>
    </template>
    
    <script>
    // @ is an alias to /src
    import HelloWorld from "@/components/HelloWorld.vue";
    
    export default {
      name: "HomeView",
      components: {
        HelloWorld,
      },
      methods: {
        numPlusOne() {
          this.$store.commit("numPlusOne");
        },
      },
    };
    </script>
    
    

actions

actions是store中专门用来处理异步的,实际修改状态值的,还是mutations。

Action类似于mutation,不同在于:

  • Action提交的是mutation,而不是直接变更状态。
  • Action可以包含任意异步操作。

Action 函数接受一个与 store 实例具有相同方法和属性的context对象,因此你可以调用context.commit 提交一个mutation,或者通过context.statecontext.getters 来获取state和getters。

  1. 修改store/index.js内容。

    import Vue from 'vue'
    import Vuex from 'vuex'
    
    Vue.use(Vuex)
    
    export default new Vuex.Store({
      // state相当于组件中的data,专门用来存放全局的数据
      state: {
        num: 10,
        toDos: [
          { id: 1, text: 'text1...', done: true },
          { id: 2, text: 'text2...', done: false },
          { id: 3, text: 'text3...', done: true },
        ],
      },
      // getters相当于组件中的computed,getters是全局的,computed是组件内部使用的
      getters: {
        // 通过属性访问
        getNum(state) {
          return state.num;
        },
        // 通过属性访问
        doneTodos(state) {
          return state.toDos.filter((todo) => todo.done);
        },
        // 通过属性访问
        // Getter 也可以接受其他 getter 作为第二个参数
        getTodosCount(state, getters) {
          return getters.doneTodos.length;
        },
        // 通过方法访问
        getTodoById: (state) => (id) => {
          return state.toDos.filter((todo) => todo.id === id);
        }
      },
      mutations: {
        numPlusOne(state) {
          state.num++;
        },
        // 可以向 store.commit 传入额外的参数,即 mutation 的载荷(payload)
        numPlusX(state, x) {
          // x (payload)是一个形参,如果组件在commit时,有传这个参数过来,就存在,如果没有传过来,就是undefined
          state.num += x ? x : 1;
        },
        // 在大多数情况下,载荷应该是一个对象,这样可以包含多个字段并且记录的 mutation 会更易读
        numPlusObjectX(state, payload) {
          state.num += payload.amount;
        },
      },
      actions: {
        // Action 函数接受一个与 store 实例具有相同方法和属性的 context 对象
        // 因此可以调用 context.commit 提交一个 mutation,或者通过 context.state 和 context.getters 来获取 state 和 getters
        numPlusOneDelayNSec(context, nSec) {
          console.log("numPlusOneDelayNSec ing.");
          setTimeout(() => {
            context.commit('numPlusOne');
            console.log("numPlusOnesDelayNSec done.");
          }, nSec)
        },
        numPlusObjectXDelayNSec(context, payload) {
          console.log("numPlusObjectXDelayNSec ing.");
          setTimeout(() => {
            console.log('numPlusObjectXDelayNSec ing payload: ' + JSON.stringify(payload));
            console.log("numPlusObjectXDelayNSec done.");
          }, payload.nSec)
        }
      },
      modules: {
      }
    })
    
    
  2. 修改views/HomeView.vue内容。

    <template>
      <div class="home">
        <HelloWorld msg="Welcome to Your Vue.js App" />
        <p>Home页面的数字:{{ $store.getters.getNum }}</p>
        <p>doneTodos:{{ $store.getters.doneTodos }}</p>
        <p>getTodosCount:{{ $store.getters.getTodosCount }}</p>
        <p>getTodoById:{{ $store.getters.getTodoById(2) }}</p>
    
        <button @click="numPlusOne">num数值+1</button>
        <button @click="$store.commit('numPlusX', 2)">num数值+X</button>
        <button @click="$store.commit('numPlusObjectX', { amount: 3 })">
          num数值+ObjX
        </button>
        <button @click="$store.commit({ type: 'numPlusObjectX', amount: 3 })">
          num数值+ObjX(对象风格提交方式)
        </button>
    
        <button @click="$store.dispatch('numPlusOneDelayNSec', 2000)">
          numPlusOneDelayNSec(actions异步实现)
        </button>
        <!-- 以载荷形式分发 -->
        <button
          @click="
            $store.dispatch('numPlusObjectXDelayNSec', {
              nSec: 2000,
              amount: 10,
            })
          "
        >
          numPlusObjectXDelayNSec(actions异步实现)
        </button>
        <!-- 以对象形式分发 -->
        <button
          @click="
            $store.dispatch({
              type: 'numPlusObjectXDelayNSec',
              nSec: 2000,
              amount: 10,
            })
          "
        >
          numPlusObjectXDelayNSec(actions异步实现)
        </button>
      </div>
    </template>
    
    <script>
    // @ is an alias to /src
    import HelloWorld from "@/components/HelloWorld.vue";
    
    export default {
      name: "HomeView",
      components: {
        HelloWorld,
      },
      methods: {
        numPlusOne() {
          this.$store.commit("numPlusOne");
        },
      },
    };
    </script>
    
    

辅助函数 *

mapState和mapGetters在组件中都是写在computed里面。

mapMutations和mapActions在组件中都是写在methods里面。

  1. store/index.js内容不变。

    import Vue from 'vue'
    import Vuex from 'vuex'
    
    Vue.use(Vuex)
    
    export default new Vuex.Store({
      // state相当于组件中的data,专门用来存放全局的数据
      state: {
        num: 10,
        toDos: [
          { id: 1, text: 'text1...', done: true },
          { id: 2, text: 'text2...', done: false },
          { id: 3, text: 'text3...', done: true },
        ],
      },
      // getters相当于组件中的computed,getters是全局的,computed是组件内部使用的
      getters: {
        // 通过属性访问
        getNum(state) {
          return state.num;
        },
        // 通过属性访问
        doneTodos(state) {
          return state.toDos.filter((todo) => todo.done);
        },
        // 通过属性访问
        // Getter 也可以接受其他 getter 作为第二个参数
        getTodosCount(state, getters) {
          return getters.doneTodos.length;
        },
        // 通过方法访问
        getTodoById: (state) => (id) => {
          return state.toDos.filter((todo) => todo.id === id);
        }
      },
      mutations: {
        numPlusOne(state) {
          state.num++;
        },
        // 可以向 store.commit 传入额外的参数,即 mutation 的载荷(payload)
        numPlusX(state, x) {
          // x (payload)是一个形参,如果组件在commit时,有传这个参数过来,就存在,如果没有传过来,就是undefined
          state.num += x ? x : 1;
        },
        // 在大多数情况下,载荷应该是一个对象,这样可以包含多个字段并且记录的 mutation 会更易读
        numPlusObjectX(state, payload) {
          state.num += payload.amount;
        },
      },
      actions: {
        // Action 函数接受一个与 store 实例具有相同方法和属性的 context 对象
        // 因此可以调用 context.commit 提交一个 mutation,或者通过 context.state 和 context.getters 来获取 state 和 getters
        numPlusOneDelayNSec(context, nSec) {
          console.log("numPlusOneDelayNSec ing.");
          setTimeout(() => {
            context.commit('numPlusOne');
            console.log("numPlusOnesDelayNSec done.");
          }, nSec)
        },
        numPlusObjectXDelayNSec(context, payload) {
          console.log("numPlusObjectXDelayNSec ing.");
          setTimeout(() => {
            console.log('numPlusObjectXDelayNSec ing payload: ' + JSON.stringify(payload));
            console.log("numPlusObjectXDelayNSec done.");
          }, payload.nSec)
        }
      },
      modules: {
      }
    })
    
    
  2. 修改views/HomeView.vue内容。

    <template>
      <div class="home">
        <HelloWorld msg="Welcome to Your Vue.js App" />
        <p>Home页面的数字:{{ getNum }}</p>
        <p>doneTodos:{{ doneTodos }}</p>
        <p>getTodosCount:{{ getTodosCount }}</p>
        <p>getTodoById:{{ getTodoById(2) }}</p>
    
        <button @click="numPlusOne">num数值+1</button>
        <button @click="numPlusX(2)">num数值+X</button>
        <button @click="numPlusObjectX({ amount: 3 })">num数值+ObjX</button>
        <button @click="numPlusObjectX({ amount: 3 })">
          num数值+ObjX(对象风格提交方式)
        </button>
    
        <button @click="numPlusOneDelayNSec(2000)">
          numPlusOneDelayNSec(actions异步实现)
        </button>
        <button @click="numPlusOneDelayNSecAlias(2000)">
          numPlusOneDelayNSecAlias(actions异步实现)
        </button>
        <button
          @click="
            numPlusObjectXDelayNSec({
              nSec: 2000,
              amount: 10,
            })
          "
        >
          numPlusObjectXDelayNSec(actions异步实现)
        </button>
      </div>
    </template>
    
    <script>
    // @ is an alias to /src
    import HelloWorld from "@/components/HelloWorld.vue";
    
    import { mapState, mapGetters } from "vuex";
    import { mapMutations, mapActions } from "vuex";
    
    export default {
      name: "HomeView",
      components: {
        HelloWorld,
      },
      computed: {
        ...mapState(["num"]),
        ...mapGetters(["getNum", "doneTodos", "getTodosCount", "getTodoById"]),
      },
      methods: {
        ...mapMutations(["numPlusOne", "numPlusX", "numPlusObjectX"]),
        ...mapActions([
          "numPlusOneDelayNSec", // 将 `this.incrementBy(amount)` 映射为 `this.$store.dispatch('incrementBy', amount)`
          "numPlusObjectXDelayNSec",
        ]),
        ...mapActions({
          numPlusOneDelayNSecAlias: "numPlusOneDelayNSec", // 将 `this.add()` 映射为 `this.$store.dispatch('increment')`
        }),
    
        numPlusOne() {
          this.$store.commit("numPlusOne");
        },
      },
    };
    </script>
    
    
  3. 修改views/AboutView.vue内容。

    <template>
      <div class="about">
        <h1>This is an about page</h1>
        <p>About页面的数字:{{ num }}</p>
        <p>getTodosCount:{{ getTodosCount }}</p>
      </div>
    </template>
    
    <script>
    import { mapState, mapGetters } from "vuex";
    import { mapMutations, mapActions } from "vuex";
    
    export default {
      data() {
        return {};
      },
      computed: {
        ...mapState(["num"]),
        ...mapGetters(["getNum", "doneTodos", "getTodosCount", "getTodoById"]),
      },
      methods: {
        ...mapMutations(["numPlusOne", "numPlusX", "numPlusObjectX"]),
        ...mapActions(["numPlusOneDelayNSec", "numPlusObjectXDelayNSec"]),
      },
    };
    </script>
    
    

拆分写法以及使用常量替代Mutation事件类型*

  1. 在store目录下新建文件mutations_type.js。

    export const MUTATIONS_TYPE = {
        NUM_PLUS_ONE: 'NUM_PLUS_ONE',
        NUM_PLUS_X: 'NUM_PLUS_X',
        NUM_PLUS_OBJECT_X: 'NUM_PLUS_OBJECT_X',
    }
    
    export default {
        [MUTATIONS_TYPE.NUM_PLUS_ONE](state) {
            state.num++;
        },
        // 可以向 store.commit 传入额外的参数,即 mutation 的载荷(payload)
        [MUTATIONS_TYPE.NUM_PLUS_X](state, x) {
            // x (payload)是一个形参,如果组件在commit时,有传这个参数过来,就存在,如果没有传过来,就是undefined
            state.num += x ? x : 1;
        },
        // 在大多数情况下,载荷应该是一个对象,这样可以包含多个字段并且记录的 mutation 会更易读
        [MUTATIONS_TYPE.NUM_PLUS_OBJECT_X](state, payload) {
            state.num += payload.amount;
        }
    }
    
    
  2. 在store目录下新建文件actions.js。

    /* eslint-disable */
    
    const actions = {
        // Action 函数接受一个与 store 实例具有相同方法和属性的 context 对象
        // 因此可以调用 context.commit 提交一个 mutation,或者通过 context.state 和 context.getters 来获取 state 和 getters
        numPlusOneDelayNSec(context, nSec) {
            console.log("numPlusOneDelayNSec ing.");
            setTimeout(() => {
                context.commit('NUM_PLUS_ONE');
                console.log("numPlusOnesDelayNSec done.");
            }, nSec)
        },
        numPlusObjectXDelayNSec(context, payload) {
            console.log("NUM_PLUS_OBJECT_X ing.");
            setTimeout(() => {
                console.log('NUM_PLUS_OBJECT_X ing payload: ' + JSON.stringify(payload));
                console.log("NUM_PLUS_OBJECT_X done.");
            }, payload.nSec)
        },
    
        // 组合 Action
        // store.dispatch 可以处理被触发的 action 的处理函数返回的 Promise,并且 store.dispatch
        makeUpActionA({ commit }) {
            // 返回一个 promise 对象
            return new Promise((resolve, reject) => {
                setTimeout(() => {
                    commit('NUM_PLUS_ONE');
                    resolve(); // 跟一般 promise 的使用差别不大
                }, 1000);
            })
        },
        makeUpActionB({ commit, dispatch }) {
            console.log("actions of makeUpActionA ing.")
            return dispatch('makeUpActionA').then(() => {
                // commit('someOtherMutation');
                console.log("actions of makeUpActionA done.")
            })
        },
    
        // 如果我们利用 async / await,我们可以如下组合 action
        // 一个 store.dispatch 在不同模块中可以触发多个 action 函数。
        // 在这种情况下,只有当所有触发函数完成后,返回的 Promise 才会执行。
        async actionA({ commit }) {
            commit('gotData', await getData());
        },
        async actionB({ dispatch, commit }) {
            await dispatch('actionA'); // 等待 actionA 完成
            commit('gotOtherData', await getOtherData());
        }
    };
    
    export default actions;
    
    
  3. 修改store/index.js内容。

    import Vue from 'vue'
    import Vuex from 'vuex'
    
    import mutations from '@/store/mutation-types'
    import actions from '@/store/actions'
    
    Vue.use(Vuex)
    
    export default new Vuex.Store({
      // state相当于组件中的data,专门用来存放全局的数据
      state: {
        num: 10,
        toDos: [
          { id: 1, text: 'text1...', done: true },
          { id: 2, text: 'text2...', done: false },
          { id: 3, text: 'text3...', done: true },
        ],
      },
      // getters相当于组件中的computed,getters是全局的,computed是组件内部使用的
      getters: {
        // 通过属性访问
        getNum(state) {
          return state.num;
        },
        // 通过属性访问
        doneTodos(state) {
          return state.toDos.filter((todo) => todo.done);
        },
        // 通过属性访问
        // Getter 也可以接受其他 getter 作为第二个参数
        getTodosCount(state, getters) {
          return getters.doneTodos.length;
        },
        // 通过方法访问
        getTodoById: (state) => (id) => {
          return state.toDos.filter((todo) => todo.id === id);
        }
      },
      mutations,
      actions,
      modules: {
      }
    })
    
    
  4. 修改App.vue文件。

    <template>
      <div id="app">
        <h3>App根组件的数字:{{ getNum }}</h3>
        <nav>
          <router-link to="/">Home</router-link> |
          <router-link to="/about">About</router-link>
        </nav>
        <router-view />
      </div>
    </template>
    
    <script>
    import { mapState, mapGetters } from "vuex";
    
    export default {
      computed: {
        ...mapState(['num']),
        ...mapGetters(['getNum', 'doneTodos', 'getTodosCount', 'getTodoById']),
      },
    };
    </script>
    
    <style lang="less">
    #app {
      font-family: Avenir, Helvetica, Arial, sans-serif;
      -webkit-font-smoothing: antialiased;
      -moz-osx-font-smoothing: grayscale;
      text-align: center;
      color: #2c3e50;
    }
    
    nav {
      padding: 30px;
    
      a {
        font-weight: bold;
        color: #2c3e50;
    
        &.router-link-exact-active {
          color: #42b983;
        }
      }
    }
    </style>
    
    
  5. 修改views/HomeView.vue文件。

    <template>
      <div class="home">
        <HelloWorld msg="Welcome to Your Vue.js App" />
        <p>Home页面的数字:{{ getNum }}</p>
        <p>doneTodos:{{ doneTodos }}</p>
        <p>getTodosCount:{{ getTodosCount }}</p>
        <p>getTodoById:{{ getTodoById(2) }}</p>
    
        <button @click="NUM_PLUS_ONE">num数值+1</button>
        <button @click="NUM_PLUS_X(2)">num数值+X</button>
        <button @click="NUM_PLUS_OBJECT_X({ amount: 3 })">num数值+ObjX</button>
        <button @click="NUM_PLUS_OBJECT_X({ amount: 3 })">
          num数值+ObjX(对象风格提交方式)
        </button>
    
        <button @click="numPlusOneDelayNSec(2000)">
          numPlusOneDelayNSec(actions异步实现)
        </button>
        <button @click="numPlusOneDelayNSecAlias(2000)">
          numPlusOneDelayNSecAlias(actions异步实现)
        </button>
        <button
          @click="
            numPlusObjectXDelayNSec({
              nSec: 2000,
              amount: 10,
            })
          "
        >
          numPlusObjectXDelayNSec(actions异步实现)
        </button>
        <button @click="makeUpActionB">组合Action</button>
      </div>
    </template>
    
    <script>
    // @ is an alias to /src
    import HelloWorld from "@/components/HelloWorld.vue";
    
    import { mapState, mapGetters } from "vuex";
    import { mapMutations, mapActions } from "vuex";
    
    export default {
      name: "HomeView",
      components: {
        HelloWorld,
      },
      computed: {
        ...mapState(['num']),
        ...mapGetters(['getNum', 'doneTodos', 'getTodosCount', 'getTodoById']),
      },
      methods: {
        ...mapMutations(['NUM_PLUS_ONE', 'NUM_PLUS_X', 'NUM_PLUS_OBJECT_X']),
        ...mapActions([
          'numPlusOneDelayNSec', // 将 `this.incrementBy(amount)` 映射为 `this.$store.dispatch('incrementBy', amount)`
          'numPlusObjectXDelayNSec',
          'makeUpActionB'
        ]),
        ...mapActions({
          numPlusOneDelayNSecAlias: 'numPlusOneDelayNSec', // 将 `this.add()` 映射为 `this.$store.dispatch('increment')`
        }),
    
        numPlusOne() {
          this.$store.commit('numPlusOne');
        },
      },
    };
    </script>
    
    
  6. 修改views/AboutView.vue文件。

    <template>
      <div class="about">
        <h1>This is an about page</h1>
        <p>About页面的数字:{{ num }}</p>
        <p>getTodosCount:{{ getTodosCount }}</p>
      </div>
    </template>
    
    <script>
    import { mapState, mapGetters } from "vuex";
    import { mapMutations, mapActions } from "vuex";
    
    export default {
      data() {
        return {};
      },
      computed: {
        ...mapState(['num']),
        ...mapGetters(['getNum', 'doneTodos', 'getTodosCount', 'getTodoById']),
      },
      methods: {
        ...mapMutations(['NUM_PLUS_ONE', 'NUM_PLUS_X', 'NUM_PLUS_OBJECT_X']),
        ...mapActions(['numPlusOneDelayNSec', 'numPlusObjectXDelayNSec']),
      },
    };
    </script>
    
    

module

默认情况下,模块内部的 action 和 mutation 仍然是注册在全局命名空间的——这样使得多个模块能够对同一个 action 或 mutation 作出响应。Getter 同样也默认注册在全局命名空间,但是目前这并非出于功能上的目的(仅仅是维持现状来避免非兼容性变更)。必须注意,不要在不同的、无命名空间的模块中定义两个相同的 getter 从而导致错误。

如果希望你的模块具有更高的封装度和复用性,你可以通过添加 namespaced: true 的方式使其成为带命名空间的模块。当模块被注册后,它的所有 getter、action 及 mutation 都会自动根据模块注册的路径调整命名。

操作步骤:

  1. 新建VUE CLI工程,带路由、Vuex和less功能。

  2. 配置初始工程。

    1. components/HelloWorld.vue中,删除多余的展示内容,只保留msg的信息。
    2. views/HomeView.vue中,只保留<HelloWorld msg="Welcome to Your Vue.js App"/>的展示内容。
  3. 在store目录下新建文件moduleA.js。

    const moduleA = {
        namespaced: true,
        state: {
            goods: [1, 2, 3, 4]
        },
        getters: {
        },
        mutations: {
            ADD_GOODS(state) {
                state.goods.push(state.goods.length + 1);
            }
        },
        actions: {
            addGoodsDelay(context) {
                setTimeout(() => {
                    context.commit('ADD_GOODS');
                }, 1000);
            }
        }
    };
    
    export default moduleA;
    
  4. 在store目录下新建文件moduleB.js。

    const moduleB = {
        namespaced: true,
        state: {
            list: ['A', 'B', 'C']
        },
        getters: {
        },
        mutations: {
            ADD_LIST(state) {
                state.list.push(state.list.length + 1);
            }
        },
        actions: {
            addListDelay(context) {
                setTimeout(() => {
                    context.commit('ADD_LIST');
                }, 1000);
            }
        }
    };
    
    export default moduleB;
    
  5. 修改store/index.js内容。

    import Vue from 'vue'
    import Vuex from 'vuex'
    import moduleA from '@/store/moduleA'
    import moduleB from '@/store/moduleB'
    
    Vue.use(Vuex)
    
    export default new Vuex.Store({
      state: {
      },
      getters: {
      },
      mutations: {
      },
      actions: {
      },
      modules: {
        moduleA,
        moduleB
      }
    })
    
    
  6. 修改views/HomeView.vue内容。

    <template>
      <div class="home">
        <HelloWorld msg="Welcome to Your Vue.js App" />
        <p v-for="item in goods" :key="item">{{ item }}</p>
        <button @click="ADD_GOODS">moduleA ADD_GOODS</button>
        <button @click="addGoodsDelay">moduleA addGoodsDelay</button>
      </div>
    </template>
    
    <script>
    // @ is an alias to /src
    import HelloWorld from "@/components/HelloWorld.vue";
    import { mapState } from "vuex";
    import { mapMutations, mapActions } from "vuex";
    
    export default {
      name: "HomeView",
      components: {
        HelloWorld,
      },
      computed: {
        ...mapState("moduleA", ["goods"]),
      },
      methods: {
        ...mapMutations("moduleA", ["ADD_GOODS"]),
        ...mapActions("moduleA", ["addGoodsDelay"]),
      },
    };
    </script>
    
    
  7. 修改views/AboutView.vue内容。

    <template>
      <div class="about">
        <h1>This is an about page</h1>
        <p v-for="item in list" :key="item">{{ item }}</p>
        <button @click="ADD_LIST">moduleA ADD_LIST</button>
        <button @click="addListDelay">moduleA addListDelay</button>
      </div>
    </template>
    
    <script>
    import { mapState } from "vuex";
    import { mapMutations, mapActions } from "vuex";
    
    export default {
      name: "AboutView",
      computed: {
        ...mapState("moduleB", ["list"]),
      },
      methods: {
        ...mapMutations("moduleB", ["ADD_LIST"]),
        ...mapActions({
          addListDelay: "moduleB/addListDelay", // 对象格式
        }),
      },
    };
    </script>
    
    

mutations结合axios示例

需求背景:

初始加载Home页面发起一个请求,并将请求的结果保存下来。

进入About页面时候,展示在Home页面请求数据的结果内容。

操作步骤:

  1. 新建VUE CLI工程,带路由、Vuex和less功能。

  2. 配置初始工程。

    1. components/HelloWorld.vue中,删除多余的展示内容,只保留msg的信息。
    2. views/HomeView.vue中,只保留<HelloWorld msg="Welcome to Your Vue.js App"/>的展示内容。
  3. 安装axios(npm run axios)。

  4. 在views/HomeView.vue中导入axios请求。

    ...
    <script>
    import axios from 'axios'
    ...
    </script>
    
  5. 修改store/index.js内容。

    import Vue from 'vue'
    import Vuex from 'vuex'
    
    Vue.use(Vuex)
    
    export default new Vuex.Store({
      state: {
        a: 1,
        axiosRes: {},
      },
      getters: {
      },
      mutations: {
        INIT_DATA(state, payload) {
          state.axiosRes = payload;
        },
        CHANGE_A2B(state) {
          state.a = 'BCD';
        },
      },
      actions: {
      },
      modules: {
      }
    })
    
    
  6. 修改views/HomeView.vue内容。

    <template>
      <div class="home">
        <HelloWorld msg="Welcome to Your Vue.js App" />
        <p>a: {{ a }}</p>
        <p>axiosRes: {{ axiosRes }}</p>
    
        <button @click="CHANGE_A2B">CHANGE_A2B</button>
      </div>
    </template>
    
    <script>
    // @ is an alias to /src
    import HelloWorld from "@/components/HelloWorld.vue";
    import axios from "axios";
    import { mapState } from "vuex";
    import { mapMutations } from "vuex";
    
    export default {
      name: "HomeView",
      components: {
        HelloWorld,
      },
      created() {
        this.getData();
      },
      computed: {
        ...mapState(["a", "axiosRes"]),
      },
      methods: {
        ...mapMutations(["INIT_DATA", "CHANGE_A2B"]),
        getData() {
          axios
            .post("http://liulongbin.top:3006/api/post", { page: 1, sie: 3 })
            .then((res) => {
              console.log(res);
              this.INIT_DATA(res);
            });
        },
      },
    };
    </script>
    
    
  7. 修改views/AboutView.vue内容。

    <template>
      <div class="about">
        <h1>This is an about page</h1>
        <p>axiosRes: {{ axiosRes }}</p>
      </div>
    </template>
    
    <script>
    import { mapState } from "vuex";
    
    export default {
      computed: {
        ...mapState(["axiosRes"]),
      },
    };
    </script>
    
    

actions实现全选示例

操作步骤:

  1. 新建VUE CLI工程,带Vuex和less功能。

  2. 配置初始工程。

    1. components/HelloWorld.vue中,删除多余的展示内容,只保留msg的信息。
    2. App.vue中,只保留<HelloWorld msg="Welcome to Your Vue.js App"/>的展示内容。
  3. 修改store/index.js内容。

    import Vue from 'vue'
    import Vuex from 'vuex'
    
    Vue.use(Vuex)
    
    export default new Vuex.Store({
      state: {
        list: [
          { checked: false },
          { checked: false },
          { checked: false },
          { checked: false },
          { checked: false },
          { checked: false },
          { checked: false },
        ],
        selectedList: [],
      },
      getters: {
        isCheckedAll(state) {
          return state.list.length === state.selectedList.length;
        }
      },
      mutations: {
        CHECK_ALL(state) {
          state.selectedList = state.list.map(v =>
            v.checked = true // 全部赋值为true
          )
        },
        UNCHECK_ALL(state) {
          state.selectedList = state.list.map(v =>
            v.checked = false // 全部赋值为false
          );
          state.selectedList = []; // 恢复默认值
        },
      },
      actions: {
        toggleCheckedAllFn({ commit, getters }) {
          getters.isCheckedAll ? commit('UNCHECK_ALL') : commit('CHECK_ALL');
        }
      },
      modules: {
      }
    })
    
    
  4. 修改components/HelloWorld.vue内容。

    <template>
      <div class="hello">
        <h1>{{ msg }}</h1>
        <ul>
          <li v-for="(item, index) in list" :key="index">
            <input type="radio" :checked="item.checked" /> {{ index }}
          </li>
        </ul>
        <label @click="toggleCheckedAllFn">
          <input type="radio" :checked="isCheckedAll" />
        </label>
      </div>
    </template>
    
    <script>
    import { mapState, mapGetters } from "vuex";
    import { mapMutations, mapActions } from "vuex";
    
    export default {
      name: "HelloWorld",
      props: {
        msg: String,
      },
      computed: {
        ...mapState(["list", "selectedList"]),
        ...mapGetters(["isCheckedAll"]),
      },
      methods: {
        ...mapMutations(["CHECK_ALL", "UNCHECK_ALL"]),
        ...mapActions(["toggleCheckedAllFn"]),
      },
    };
    </script>
    
    <!-- Add "scoped" attribute to limit CSS to this component only -->
    <style scoped lang="less">
    h3 {
      margin: 40px 0 0;
    }
    ul {
      list-style-type: none;
      padding: 0;
    }
    li {
      display: inline-block;
      margin: 0 10px;
    }
    a {
      color: #42b983;
    }
    </style>
    
    

/---- Vue3.x ----/

Vue3.x和Vue2.x版本的对比

Vue2.x 中绝大多数的 API 与特性,在 Vue3.x 中同样支持。同时,Vue3.x 中还新增了 3.x 所特有的功能、并
废弃了某些 2.x 中的旧功能:

新增的功能例如:

组合式 API、多根节点组件、更好的 TypeScript 支持等

废弃的旧功能如下:

过滤器、不再支持 $on,$off 和 $once 实例方法等

详细的变更信息,请参考官方文档给出的迁移指南:

https://v3.vuejs.org/guide/migration/introduction.html

Vue3.x新特性介绍

新特性介绍

  • 重写双向数据绑定
  • VDOM性能瓶颈
  • Fragments
  • Tree-Shaking的支持
  • Composition API

Vite

备注:目前只支持Vue3.x的项目。

Vite创建Vue3.x项目【模板】

Vite特征特点

  1. Vite冷服务启动,ES6 import。
  2. 开发中热更新。
  3. 按需进行编译,不会刷新全部DOM。

操作步骤

  1. 新建项目文件夹ViteDemo(Vite项目工程存放的位置)。

  2. 执行以下命令,创建Vite工程项目。

    # 方式一
    npm create vite@latest <project-name>
    
    # 方式二(vite2.0版本)
    npm init @vitejs/app <project-name>
    
    # 方式三(vite1.0版本)
    npm init vite-app <project-name> # 项目名称不写时,默认为vite
    
    # 直接回车,按照提示操作即可
    
  3. 使用VS Code打开工程目录。

  4. 执行以下命令安装相关依赖。

    npm install
    
  5. 执行以下命令运行代码。

    npm run dev
    

Vite对Typescript、CSS和JSON的支持

  1. 使用Vite搭建Vue3开发环境,参见[Vite创建Vue3.x项目【模板】](# Vite创建Vue3.x项目【模板】)。

  2. 修改文件App.vue。验证Vite对Typescript的支持。

    <script lang="ts">
    // This starter template is using Vue 3 <script setup> SFCs
    // Check out https://vuejs.org/api/sfc-script-setup.html#script-setup
    import HelloWorld from "./components/HelloWorld.vue";
    const tsVal: string = "Hello TS";
    
    export default {
      name: 'app',
      components: {
        HelloWorld,
      },
      mounted() {
        console.log(tsVal);
      },
    };
    </script>
    
    <template>
      <img alt="Vue logo" src="./assets/logo.png" />
      <HelloWorld msg="Hello Vue 3 + Vite" />
    </template>
    
    <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>
    
    
  3. 在src\assets\目录中,新建文件app.css。验证Vite对CSS文件的支持。

    body {
      background-color: aqua;
    }
    
    
  4. 修改文件App.vue。

    <script lang="ts">
    // This starter template is using Vue 3 <script setup> SFCs
    // Check out https://vuejs.org/api/sfc-script-setup.html#script-setup
    import HelloWorld from "./components/HelloWorld.vue";
    import "./assets/app.css";
    
    const tsVal: string = "Hello TS";
    
    export default {
      name: "app",
      components: {
        HelloWorld,
      },
      mounted() {
        console.log(tsVal);
      },
    };
    </script>
    
    <template>
      <img alt="Vue logo" src="./assets/logo.png" />
      <HelloWorld msg="Hello Vue 3 + Vite" />
    </template>
    
    <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>
    
    
  5. 在src\assets\目录中,新建文件config.json。验证Vite对JSON文件的支持。

    {
        "name": "张三",
        "website": "www.baidu.com"
    }
    
  6. 修改文件App.vue。

    <script lang="ts">
    // This starter template is using Vue 3 <script setup> SFCs
    // Check out https://vuejs.org/api/sfc-script-setup.html#script-setup
    import HelloWorld from "./components/HelloWorld.vue";
    import "./assets/app.css";
    import data from "./assets/config.jso
        n";
    
    const tsVal: string = "Hello TS";
    
    export default {
      name: "app",
      components: {
        HelloWorld,
      },
      mounted() {
        console.log(tsVal);
        console.log(`${data.name} | ${data.website}`);
      },
    };
    </script>
    
    <template>
      <img alt="Vue logo" src="./assets/logo.png" />
      <HelloWorld msg="Hello Vue 3 + Vite" />
    </template>
    
    <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>
    
    

Vite配置文件和别名设置

基于上述的工程进行适配修改。

在项目根目录中新建配置文件vite.config.js。

const { resolve } = require('path');

export default {
    alias: {
        '/@/': resolve(__dirname, 'src'),
    }
}

Vite安装和配置less【模板】

  1. 搭建完成Vite项目。

  2. 安装依赖包。

    npm i -D less-loader less
    
  3. 使用示例,新建XX.vue。

    <template>
        <div class="content-home">
            <h3>我是Home组件</h3>
        </div>
    </template>
    
    <script setup lang="ts">
    
    </script>
    
    <style lang="less" scoped>
    .content-home {
        width: 200px;
        height: 200px;
        background-color: aqua;
    }
    </style>
    
    

Vue3.x组合式API

创建Vue3.x项目

Vue CLI创建Vue3.x项目【模板】

  1. 执行命令,新建一个工程

    vue create vue3-project-1
    
  2. 手动配置工程

    # Manually select features
    # 新增勾选“Typescript”、“Router”、“Vuex”和“CSS Pre-processors”
    # 以3.x为例,选择“3.x”
    # Use Babel alongside TypeScript,选择“Yes”
    # 选择“less”CSS预处理器
    # 选择“ESLint with error prevention only”或者“ESLint + Standard config”,根据需要选择
    # 最后咨询是否保存为模板,这里选择“N”
    

Vite创建Vue3.x项目【模板】

参见[Vite创建Vue3.x项目【模板】](# Vite创建Vue3.x项目【模板】)。

setup函数

新的 setup 选项在组件被创建之前执行,一旦 props 被解析完成,它就将被作为组合式 API 的入口。

说明:在 setup 中你应该避免使用 this,因为它不会找到组件实例。setup 的调用发生在 data property、computed property 或 methods 被解析之前,所以它们无法在 setup 中被获取。

带ref的响应式变量

在 Vue 3.0 中,我们可以通过一个新的 ref 函数使任何响应式变量在任何地方起作用,如下所示:

import { ref } from 'vue'

const counter = ref(0)

ref 接收参数并将其包裹在一个带有 value property 的对象中返回,然后可以使用该 property 访问或更改响应式变量的值:

import { ref } from 'vue'

const counter = ref(0)

console.log(counter) // { value: 0 }
console.log(counter.value) // 0

counter.value++
console.log(counter.value) // 1

操作步骤

  1. 创建Vue3.x项目,参见[Vite创建Vue3.x项目【模板】](# Vite创建Vue3.x项目【模板】)。

  2. 修改文件src\views\AboutView.vue。

    <template>
      <div class="about">
        <h1>This is an about page</h1>
        <h3>01. setup函数介绍和使用</h3>
        <h3>
          Vue2.x中的数据:{{ title01 }}----
          <button @click="getVue3Data">获取一下setup中的数据</button>
        </h3>
        <h3>
          Vue3.x中的数据:{{ title02 }}
          <button @click="getVue2Data">获取一下data中的数据</button>
        </h3>
        <h3>
          <button @click="addCount">count++ == {{ count }}</button>
        </h3>
      </div>
    </template>
    
    <script>
    import { ref } from "vue";
    export default {
      // 1. setup执行的时期:在beforeCreate和created之前执行
      // 2. setup和options api对比
      // 3. 在Vue3.x中仍然可以使用Vue2.x相关语法
      // 4. 在Vue2.x中可以获取到setup中的数据
      // 5. 在setup中无法获取到Vue2.x中的数据,没有this,this的值是undefined
      // 6. 如果setup中的方法或者数据和Vue2.x中的方法和数据冲突了,优先使用setup中的数据或方法
      data() {
        return {
          title01: "我是Vue2中的data中的数据!",
          // count: 1,
        };
      },
      methods: {
        // addCount() {
        //   console.log("methods--addCount");
        //   this.count++;
        // },
        getVue3Data() {
          console.log("methods--getVue3Data, title02: " + this.title02);
        },
      },
      beforeCreate() {
        console.log("Life cycle--beforeCreate");
      },
      created() {
        console.log("Life cycle--created");
      },
      // setup是在created和beforeCreate生命周期之前触发
      setup(props) {
        // props是一个对象,组件外部传递过来的值,并且这个值是在组件内接收了的
        console.log("setup, props: ", props);
        let title02 = ref("我是setup中的数据!");
    
        // setup中无法获取到Vue2.x中的数据
        // setup中打印this也会是undefined
        function getVue2Data() {
          console.log("setup, function--getVue2Data, title01: " + this.title01);
        }
    
        let count = ref(0);
        function addCount() {
          count.value++;
        }
    
        // 这里返回的任何内容都可以用于组件的其余部分
        return {
          title02,
          getVue2Data,
          addCount,
          count,
        };
      },
    };
    </script>
    
    

    说明:

    1. 在Vue2.x中可以获取到setup中的数据。

    2. 在setup中无法获取到Vue2.x中的数据(在setup中获取this,打印出来是undefined)。

    3. Vue2.x中的方法如果跟setup中的方法冲突,优先选择setup中的方法。

    4. 上述的方式可以使用setup的语法糖形式。不需要进行导出操作。

      <script setup>
      // 使用了setup的语法糖
      import { ref, reactive } from "vue";
      
      const coffee = ref("卡布奇洛");
      const movie = reactive({
        name: "<strong>蝙蝠侠</strong>",
        price: 55,
      });
      
      const params = reactive({
        username: "admin",
        password: "123",
      });
      
      const gender = ref("Female");
      
      const isAgree = ref(false);
      </script>
      
  3. 验证setup中props参数使用场景。

    1. 在components目录下,新建文件TestSetUpProps.vue。

      <template>
        <div>
          <h1>测试子组件</h1>
        </div>
      </template>
      
      <script lang="ts">
      export default {
      
      }
      </script>
      
      <!-- Add "scoped" attribute to limit CSS to this component only -->
      <style scoped lang="less"></style>
      
      
    2. 修改文件src\views\AboutView.vue。补充两个父组件传递给子组件的内容。

      <template>
        <div class="about">
          <h1>This is an about page</h1>
          <h3>01. setup函数介绍和使用</h3>
          <h3>
            Vue2.x中的数据:{{ title01 }} ----
            <button @click="getVue3Data">获取一下setup中的数据</button>
          </h3>
          <h3>
            Vue3.x中的数据:{{ title02 }}
            <button @click="getVue2Data">获取一下data中的数据</button>
          </h3>
          <h3>
            <button @click="addCount">count++ == {{ count }}</button>
          </h3>
      
          <hr />
      
          <TestSetUpProps :title03="title03" :cookie="cookie"></TestSetUpProps>
        </div>
      </template>
      
      <script>
      import { ref } from "vue";
      import TestSetUpProps from "../components/TestSetUpProps.vue";
      
      export default {
        // 1. setup执行的时期:在beforeCreate和created之前执行
        // 2. setup和options api对比
        // 3. 在Vue3.x中仍然可以使用Vue2.x相关语法
        // 4. 在Vue2.x中可以获取到setup中的数据
        // 5. 在setup中无法获取到Vue2.x中的数据,没有this,this的值是undefined
        // 6. 如果setup中的方法或者数据和Vue2.x中的方法和数据冲突了,优先使用setup中的数据或方法
        data() {
          return {
            title01: "我是Vue2中的data中的数据!",
            // count: 1,
          };
        },
        components: {
          TestSetUpProps,
        },
        methods: {
          // addCount() {
          //   console.log("methods--addCount");
          //   this.count++;
          // },
          getVue3Data() {
            console.log("methods--getVue3Data, title02: " + this.title02);
          },
        },
        beforeCreate() {
          console.log("Life cycle--beforeCreate");
        },
        created() {
          console.log("Life cycle--created");
        },
        // setup是在created和beforeCreate生命周期之前触发
        setup(props) {
          // props是一个对象,组件外部传递过来的值,并且这个值是在组件内接收了的
          console.log("setup, props: ", props);
          let title02 = ref("我是setup中的数据!");
      
          // setup中无法获取到Vue2.x中的数据
          // setup中打印this也会是undefined
          function getVue2Data() {
            console.log("setup, function--getVue2Data, title01: " + this.title01);
          }
      
          let count = ref(0);
          function addCount() {
            count.value++;
          }
      
          let title03 = ref("给子组件传递的数据!");
          let cookie = ref("奥利奥!");
      
          // 这里返回的任何内容都可以用于组件的其余部分
          return {
            title02,
            getVue2Data,
            addCount,
            count,
            title03,
            cookie,
          };
        },
      };
      </script>
      
      
    3. 修改文件components/TestSetUpProps.vue,接收和显示父组件传递过来的数据内容。

      <template>
        <div>
          <h1>测试子组件</h1>
          <h3>接收来自父组件传递过来的参数 ---- {{ newVal }}</h3>
        </div>
      </template>
      
      <script lang="ts">
      import { ref } from "vue";
      
      export default {
        // 子组件接收父组件传递值,一定要用props自定义属性进行接收
        props: {
          title03: {
            type: String,
            require: true,
          },
        },
        setup(props) {
          console.log(props);
      
          const newVal = ref(props.title03 + " + 经过setup处理之后的值!");
      
          return {
            newVal,
          };
        },
      };
      </script>
      
      <!-- Add "scoped" attribute to limit CSS to this component only -->
      <style scoped lang="less"></style>
      
      
  4. 验证setup中context参数使用场景,先说明context参数的attrs和slots属性。

    1. 修改文件components/TestSetUpProps.vue。

      <template>
        <div>
          <h1>测试子组件</h1>
          <h3>接收来自父组件传递过来的参数 ---- {{ newVal }}</h3>
          <slot name="one"></slot>
        </div>
      </template>
      
      <script lang="ts">
      import { ref } from "vue";
      
      export default {
        // 子组件接收父组件传递值,一定要用props自定义属性进行接收
        props: {
          title03: {
            type: String,
            require: true,
          },
        },
        setup(props, context) {
          // context表示上下文对象,包含attrs, slots, emit
          // 	attrs,外部传递过来的属性,并且没有在props中定义
          // 	slots,插槽
          // 	emit,分发自定义的事件,相当于$emit
          console.log("setup ing..");
          console.log(props);
          console.log(context);
          console.log(JSON.stringify(context.attrs)); // {"cookie":"奥利奥!"}
          console.log(context.slots); //
      
          const newVal = ref(props.title03 + " + 经过setup处理之后的值!");
      
          return {
            newVal,
          };
        },
      };
      </script>
      
      <!-- Add "scoped" attribute to limit CSS to this component only -->
      <style scoped lang="less"></style>
      
      
    2. 修改文件src\views\AboutView.vue。

      <template>
        <div class="about">
          <h1>This is an about page</h1>
          <h3>01. setup函数介绍和使用</h3>
          <h3>
            Vue2.x中的数据:{{ title01 }} ----
            <button @click="getVue3Data">获取一下setup中的数据</button>
          </h3>
          <h3>
            Vue3.x中的数据:{{ title02 }}
            <button @click="getVue2Data">获取一下data中的数据</button>
          </h3>
          <h3>
            <button @click="addCount">count++ == {{ count }}</button>
          </h3>
      
          <hr />
      
          <TestSetUpProps :title03="title03" :cookie="cookie">
            <template v-slot:one>
              <h3>Hello demo, 自定义插槽!!</h3>
            </template>
          </TestSetUpProps>
        </div>
      </template>
      
      <script>
      import { ref } from "vue";
      import TestSetUpProps from "../components/TestSetUpProps.vue";
      
      export default {
        // 1. setup执行的时期:在beforeCreate和created之前执行
        // 2. setup和options api对比
        // 3. 在Vue3.x中仍然可以使用Vue2.x相关语法
        // 4. 在Vue2.x中可以获取到setup中的数据
        // 5. 在setup中无法获取到Vue2.x中的数据,没有this,this的值是undefined
        // 6. 如果setup中的方法或者数据和Vue2.x中的方法和数据冲突了,优先使用setup中的数据或方法
        data() {
          return {
            title01: "我是Vue2中的data中的数据!",
            // count: 1,
          };
        },
        components: {
          TestSetUpProps,
        },
        methods: {
          // addCount() {
          //   console.log("methods--addCount");
          //   this.count++;
          // },
          getVue3Data() {
            console.log("methods--getVue3Data, title02: " + this.title02);
          },
        },
        beforeCreate() {
          console.log("Life cycle--beforeCreate");
        },
        created() {
          console.log("Life cycle--created");
        },
        // setup是在created和beforeCreate生命周期之前触发
        setup(props) {
          // props是一个对象,组件外部传递过来的值,并且这个值是在组件内接收了的
          console.log("setup, props: ", props);
          let title02 = ref("我是setup中的数据!");
      
          // setup中无法获取到Vue2.x中的数据
          // setup中打印this也会是undefined
          function getVue2Data() {
            console.log("setup, function--getVue2Data, title01: " + this.title01);
          }
      
          let count = ref(0);
          function addCount() {
            count.value++;
          }
      
          let title03 = ref("给子组件传递的数据!");
          let cookie = ref("奥利奥!");
      
          // 这里返回的任何内容都可以用于组件的其余部分
          return {
            title02,
            getVue2Data,
            addCount,
            count,
            title03,
            cookie,
          };
        },
      };
      </script>
      
      
  5. 验证setup中context参数使用场景,说明context参数的emit属性。

    1. 修改文件components/TestSetUpProps.vue。

      <template>
        <div>
          <h1>测试子组件</h1>
          <h3>接收来自父组件传递过来的参数 ---- {{ newVal }}</h3>
          <slot name="one"></slot>
          <h3><button @click="send">子向父传值</button></h3>
        </div>
      </template>
      
      <script lang="ts">
      import { ref } from "vue";
      
      export default {
        // 子组件接收父组件传递值,一定要用props自定义属性进行接收
        props: {
          title03: {
            type: String,
            require: true,
          },
        },
        setup(props, context) {
          // context表示上下文对象,包含attrs, slots, emit
          // 	attrs,外部传递过来的属性,并且没有在props中定义
          // 	slots,插槽
          // 	emit,分发自定义的事件,相当于$emit
          console.log("setup ing..");
          console.log(props);
          console.log(context);
          console.log(JSON.stringify(context.attrs)); // {"cookie":"奥利奥!"}
          console.log(context.slots); //
      
          const newVal = ref(props.title03 + " + 经过setup处理之后的值!");
      
          const price = ref("100元"); // 传递给父组件的值内容
          function send() {
            context.emit("sendPrice", price);
          }
      
          return {
            newVal,
            price,
            send,
          };
        },
      };
      </script>
      
      <!-- Add "scoped" attribute to limit CSS to this component only -->
      <style scoped lang="less"></style>
      
      
    2. 修改文件src\views\AboutView.vue。

      <template>
        <div class="about">
          <h1>This is an about page</h1>
          <h3>01. setup函数介绍和使用</h3>
          <h3>
            Vue2.x中的数据:{{ title01 }} ----
            <button @click="getVue3Data">获取一下setup中的数据</button>
          </h3>
          <h3>
            Vue3.x中的数据:{{ title02 }}
            <button @click="getVue2Data">获取一下data中的数据</button>
          </h3>
          <h3>
            <button @click="addCount">count++ == {{ count }}</button>
          </h3>
      
          <br />
          <h3>接收子组件传递出来的数据 ---- {{ gift }}</h3>
      
          <hr />
      
          <TestSetUpProps
            :title03="title03"
            :cookie="cookie"
            @sendPrice="receiveFromSubComp"
          >
            <template v-slot:one>
              <h3>Hello demo, 自定义插槽!!</h3>
            </template>
          </TestSetUpProps>
        </div>
      </template>
      
      <script>
      import { ref } from "vue";
      import TestSetUpProps from "../components/TestSetUpProps.vue";
      
      export default {
        // 1. setup执行的时期:在beforeCreate和created之前执行
        // 2. setup和options api对比
        // 3. 在Vue3.x中仍然可以使用Vue2.x相关语法
        // 4. 在Vue2.x中可以获取到setup中的数据
        // 5. 在setup中无法获取到Vue2.x中的数据,没有this,this的值是undefined
        // 6. 如果setup中的方法或者数据和Vue2.x中的方法和数据冲突了,优先使用setup中的数据或方法
        data() {
          return {
            title01: "我是Vue2中的data中的数据!",
            // count: 1,
          };
        },
        components: {
          TestSetUpProps,
        },
        methods: {
          // addCount() {
          //   console.log("methods--addCount");
          //   this.count++;
          // },
          getVue3Data() {
            console.log("methods--getVue3Data, title02: " + this.title02);
          },
        },
        beforeCreate() {
          console.log("Life cycle--beforeCreate");
        },
        created() {
          console.log("Life cycle--created");
        },
        // setup是在created和beforeCreate生命周期之前触发
        setup(props) {
          // props是一个对象,组件外部传递过来的值,并且这个值是在组件内接收了的
          console.log("setup, props: ", props);
          let title02 = ref("我是setup中的数据!");
      
          // setup中无法获取到Vue2.x中的数据
          // setup中打印this也会是undefined
          function getVue2Data() {
            console.log("setup, function--getVue2Data, title01: " + this.title01);
          }
      
          let count = ref(0);
          function addCount() {
            count.value++;
          }
      
          let title03 = ref("给子组件传递的数据!");
          let cookie = ref("奥利奥!");
      
          let gift = ref("0元");
          function receiveFromSubComp(val) {
            console.log("子组件传递过来的数据, val", val);
            console.log("子组件传递过来的数据, val.value", val.value);
            gift.value = val.value;
            console.log("gift", gift);
          }
      
          // 这里返回的任何内容都可以用于组件的其余部分
          return {
            title02,
            getVue2Data,
            addCount,
            count,
            title03,
            cookie,
            receiveFromSubComp,
            gift,
          };
        },
      };
      </script>
      
      

ref

说明:处理基本数据类型的数据。

接受一个内部值并返回一个响应式且可变的ref对象。ref对象仅有一个.value property,指向该内部值。

  1. 创建Vue3.x项目,参见[Vite创建Vue3.x项目【模板】](# Vite创建Vue3.x项目【模板】)。

  2. 修改文件src\views\AboutView.vue。

    <template>
      <div class="about">
        <h1>This is an about page</h1>
        <h3>02. ref</h3>
        <p>当前的电影是:{{ title }}</p>
        <p>当前电影的价格是:{{ price }}</p>
        <p>电影的详情是: {{ info }}</p>
        <button @click="getMovieInfo">单击查看详情</button>
    
        <br /><br />
        <input type="text" v-model="mobile" />
        <p>mobile: {{ mobile }}</p>
    
        <br />
        <p>当前咖啡商品的名字是:{{ coffee.name }}</p>
        <p>当前咖啡商品的价格是:{{ coffee.price }}</p>
      </div>
    </template>
    
    <script>
    import { ref } from "vue";
    
    export default {
      // 1. setup执行的时期:在beforeCreate和created之前执行
      // 2. setup和options api对比
      // 3. 在Vue3.x中仍然可以使用Vue2.x相关语法
      // 4. 在Vue2.x中可以获取到setup中的数据
      // 5. 在setup中无法获取到Vue2.x中的数据,没有this,this的值是undefined
      // 6. 如果setup中的方法或者数据和Vue2.x中的方法和数据冲突了,优先使用setup中的数据或方法
      data() {
        return {};
      },
      components: {},
      methods: {},
      // setup是在created和beforeCreate生命周期之前触发
      setup(props, context) {
        // props是一个对象,组件外部传递过来的值,并且这个值是在组件内接收了的
        // context表示上下文对象,包含attrs, slots, emit
        // 	attrs,外部传递过来的属性,并且没有在props中定义
        // 	slots,插槽
        // 	emit,分发自定义的事件,相当于$emit
        // 这里返回的任何内容都可以用于组件的其余部分
        console.log(props);
        console.log(context);
    
        let title = "长津湖";
        let price = 56;
        let info = ref("抗美援朝的故事");
        function getMovieInfo() {
          info.value = `当前电影的名字是:${title},当前电影的价格是:${price},详情是:${info.value}`;
        }
    
        // ref实现数据的双向绑定,实际上是通过set和get的方法来实现的
        let mobile = ref("123");
    
        // 对象类型
        const coffee = ref({
          name: "瑞纳冰拿铁",
          price: 48,
        });
        console.log("coffee.value.name", coffee.value.name);
    
        return {
          title,
          price,
          info,
          getMovieInfo,
          mobile,
          coffee,
        };
      },
    };
    </script>
    
    
  3. 模拟登录注册的效果。

    1. 修改文件src\views\AboutView.vue。编写登录框架内容。

      <template>
        <div class="about">
          <h1>This is an about page</h1>
          <h3>02. ref</h3>
          <p>当前的电影是:{{ title }}</p>
          <p>当前电影的价格是:{{ price }}</p>
          <p>电影的详情是: {{ info }}</p>
          <button @click="getMovieInfo">单击查看详情</button>
      
          <br /><br />
          <input type="text" v-model="mobile" />
          <p>mobile: {{ mobile }}</p>
      
          <br />
          <p>当前咖啡商品的名字是:{{ coffee.name }}</p>
          <p>当前咖啡商品的价格是:{{ coffee.price }}</p>
      
          <br />
          <p>用户名:<input type="text" v-model="loginParams.username" /></p>
          <p>密码:<input type="text" v-model="loginParams.password" /></p>
          <button @click="doLogin">登录</button>
          <p>login info: {{ loginParams }}</p>
        </div>
      </template>
      
      <script>
      import { ref } from "vue";
      
      export default {
        // 1. setup执行的时期:在beforeCreate和created之前执行
        // 2. setup和options api对比
        // 3. 在Vue3.x中仍然可以使用Vue2.x相关语法
        // 4. 在Vue2.x中可以获取到setup中的数据
        // 5. 在setup中无法获取到Vue2.x中的数据,没有this,this的值是undefined
        // 6. 如果setup中的方法或者数据和Vue2.x中的方法和数据冲突了,优先使用setup中的数据或方法
        data() {
          return {};
        },
        components: {},
        methods: {},
        // setup是在created和beforeCreate生命周期之前触发
        setup(props, context) {
          // props是一个对象,组件外部传递过来的值,并且这个值是在组件内接收了的
          // context表示上下文对象,包含attrs, slots, emit
          // 	attrs,外部传递过来的属性,并且没有在props中定义
          // 	slots,插槽
          // 	emit,分发自定义的事件,相当于$emit
          // 这里返回的任何内容都可以用于组件的其余部分
          console.log(props);
          console.log(context);
      
          let title = "长津湖";
          let price = 56;
          let info = ref("抗美援朝的故事");
          function getMovieInfo() {
            info.value = `当前电影的名字是:${title},当前电影的价格是:${price},详情是:${info.value}`;
          }
      
          // ref实现数据的双向绑定,实际上是通过set和get的方法来实现的
          let mobile = ref("123");
      
          // 对象类型
          const coffee = ref({
            name: "瑞纳冰拿铁",
            price: 48,
          });
          console.log("coffee.value.name", coffee.value.name);
      
          const loginParams = ref({
            username: "",
            password: "",
          });
      
          function doLogin() {
            console.log(loginParams.value);
          }
      
          return {
            title,
            price,
            info,
            getMovieInfo,
            mobile,
            coffee,
            loginParams,
            doLogin,
          };
        },
      };
      </script>
      
      
    2. 在根目录中执行以下命令,安装axios和qs。

      npm i -D axios qs
      
    3. 修改main.ts,挂在全局对象。

      import { createApp } from "vue";
      import App from "./App.vue";
      import router from "./router";
      import store from "./store";
      import axios from "axios";
      import qs from "qs";
      
      const app = createApp(App);
      
      // 挂载全局对象
      app.config.globalProperties.foo = "Hello world!";
      app.config.globalProperties.$http = axios;
      axios.defaults.baseURL = "http://bufantec.com/api/";
      
      // 请求拦截器
      axios.interceptors.request.use(
        (config) => {
          if (config.method?.toLocaleLowerCase() === "post") {
            config.data = qs.stringify(config.data);
          }
          return config;
        },
        (err) => {
          return Promise.reject(err);
        }
      );
      
      app.use(store).use(router).mount("#app");
      
      
    4. 修改文件src\views\AboutView.vue。编写请求内容。

      <template>
        <div class="about">
          <h1>This is an about page</h1>
          <h3>02. ref</h3>
          <p>当前的电影是:{{ title }}</p>
          <p>当前电影的价格是:{{ price }}</p>
          <p>电影的详情是: {{ info }}</p>
          <button @click="getMovieInfo">单击查看详情</button>
      
          <br /><br />
          <input type="text" v-model="mobile" />
          <p>mobile: {{ mobile }}</p>
      
          <br />
          <p>当前咖啡商品的名字是:{{ coffee.name }}</p>
          <p>当前咖啡商品的价格是:{{ coffee.price }}</p>
      
          <br />
          <p>用户名:<input type="text" v-model="loginParams.username" /></p>
          <p>密码:<input type="text" v-model="loginParams.password" /></p>
          <button @click="doLogin">登录</button>
          <p>login info: {{ loginParams }}</p>
        </div>
      </template>
      
      <script>
      // ref:创建一个包含响应式数据的引用对象,xxx.value
      // ref可以接收基本数据类型和对象类型
      import { getCurrentInstance, ref } from "vue";
      
      export default {
        // 1. setup执行的时期:在beforeCreate和created之前执行
        // 2. setup和options api对比
        // 3. 在Vue3.x中仍然可以使用Vue2.x相关语法
        // 4. 在Vue2.x中可以获取到setup中的数据
        // 5. 在setup中无法获取到Vue2.x中的数据,没有this,this的值是undefined
        // 6. 如果setup中的方法或者数据和Vue2.x中的方法和数据冲突了,优先使用setup中的数据或方法
        data() {
          return {};
        },
        components: {},
        methods: {},
        // setup是在created和beforeCreate生命周期之前触发
        setup(props, context) {
          // props是一个对象,组件外部传递过来的值,并且这个值是在组件内接收了的
          // context表示上下文对象,包含attrs, slots, emit
          // 	attrs,外部传递过来的属性,并且没有在props中定义
          // 	slots,插槽
          // 	emit,分发自定义的事件,相当于$emit
          // 这里返回的任何内容都可以用于组件的其余部分
          console.log(props);
          console.log(context);
      
          // 接收挂载的全局变量
          // 例如proxy.poo就表示"Hello world!"
          // 例如proxy.$http就表示axios请求
          const { proxy } = getCurrentInstance();
      
          let title = "长津湖";
          let price = 56;
          let info = ref("抗美援朝的故事");
          function getMovieInfo() {
            info.value = `当前电影的名字是:${title},当前电影的价格是:${price},详情是:${info.value}`;
          }
      
          // ref实现数据的双向绑定,实际上是通过set和get的方法来实现的
          let mobile = ref("123");
      
          // 对象类型
          const coffee = ref({
            name: "瑞纳冰拿铁",
            price: 48,
          });
          console.log("coffee.value.name", coffee.value.name);
      
          const loginParams = ref({
            username: "",
            password: "",
          });
      
          function doLogin() {
            console.log("setup doLogin ing..");
            console.log(loginParams.value);
            proxy.$http.post("test/user/doLogin", loginParams.value).then((res) => {
              console.log(res);
            });
          }
      
          return {
            title,
            price,
            info,
            getMovieInfo,
            mobile,
            coffee,
            loginParams,
            doLogin,
          };
        },
      };
      </script>
      
      

reactive

说明:处理对象数据类型的数据。

修改文件src\views\AboutView.vue。修改对象的处理内容。修改咖啡的信息为reactive方式进行处理。

<template>
  <div class="about">
    <h1>This is an about page</h1>
    <h3>02. ref</h3>
    <p>当前的电影是:{{ title }}</p>
    <p>当前电影的价格是:{{ price }}</p>
    <p>电影的详情是: {{ info }}</p>
    <button @click="getMovieInfo">单击查看详情</button>

    <br /><br />
    <input type="text" v-model="mobile" />
    <p>mobile: {{ mobile }}</p>

    <br />
    <p>当前咖啡商品的名字是:{{ coffee.name }}</p>
    <p>当前咖啡商品的价格是:{{ coffee.price }}</p>

    <br />
    <p>用户名:<input type="text" v-model="loginParams.username" /></p>
    <p>密码:<input type="text" v-model="loginParams.password" /></p>
    <button @click="doLogin">登录</button>
    <p>login info: {{ loginParams }}</p>
  </div>
</template>

<script>
// ref:创建一个包含响应式数据的引用对象,xxx.value
// ref可以接收基本数据类型和对象类型
import { getCurrentInstance, ref, reactive } from "vue";

export default {
  // 1. setup执行的时期:在beforeCreate和created之前执行
  // 2. setup和options api对比
  // 3. 在Vue3.x中仍然可以使用Vue2.x相关语法
  // 4. 在Vue2.x中可以获取到setup中的数据
  // 5. 在setup中无法获取到Vue2.x中的数据,没有this,this的值是undefined
  // 6. 如果setup中的方法或者数据和Vue2.x中的方法和数据冲突了,优先使用setup中的数据或方法
  data() {
    return {};
  },
  components: {},
  methods: {},
  // setup是在created和beforeCreate生命周期之前触发
  setup(props, context) {
    // props是一个对象,组件外部传递过来的值,并且这个值是在组件内接收了的
    // context表示上下文对象,包含attrs, slots, emit
    // 	attrs,外部传递过来的属性,并且没有在props中定义
    // 	slots,插槽
    // 	emit,分发自定义的事件,相当于$emit
    // 这里返回的任何内容都可以用于组件的其余部分
    console.log(props);
    console.log(context);

    // 接收挂载的全局变量
    // 例如proxy.poo就表示"Hello world!"
    // 例如proxy.$http就表示axios请求
    const { proxy } = getCurrentInstance();

    let title = "长津湖";
    let price = 56;
    let info = ref("抗美援朝的故事");
    function getMovieInfo() {
      info.value = `当前电影的名字是:${title},当前电影的价格是:${price},详情是:${info.value}`;
    }

    // ref实现数据的双向绑定,实际上是通过set和get的方法来实现的
    let mobile = ref("123");

    // 对象类型
    const coffee = reactive({
      name: "瑞纳冰拿铁",
      price: 48,
    });
    console.log(coffee); // 直接就是Proxy的处理方式
    console.log("coffee.name", coffee.name, "coffee.price", coffee.price);

    const loginParams = reactive({
      username: "",
      password: "",
    });

    function doLogin() {
      console.log("setup doLogin ing..");
      console.log(loginParams);
      proxy.$http.post("test/user/doLogin", loginParams).then((res) => {
        console.log(res);
      });
    }

    return {
      title,
      price,
      info,
      getMovieInfo,
      mobile,
      coffee,
      loginParams,
      doLogin,
    };
  },
};
</script>

options-api和composition-api

Vue2.x实现一个成员的添加和删减方式

options-api

修改文件AboutView.vue。

<template>
  <div class="about">
    <h1>This is an about page</h1>
    <h3>Vue2.x实现成员添加和删减</h3>
    <p><input type="text" v-model="user.name" /></p>
    <p><input type="text" v-model="user.age" /></p>
    <p><button @click="addItem">添加一个成员</button></p>

    <ul>
      <li v-for="(item, index) in list" :key="index" @click="removeItem(index)">
        No.{{ index + 1 }} ---- 姓名:{{ item.name }} ---- 年龄:{{ item.age }}
      </li>
    </ul>
  </div>
</template>

<script>
export default {
  data() {
    return {
      list: [
        {
          name: "张三",
          age: 17,
        },
        {
          name: "李四",
          age: 19,
        },
        {
          name: "王五",
          age: 18,
        },
      ],
      user: {
        name: "LeMo",
        age: 20,
      },
    };
  },
  components: {},
  methods: {
    addItem() {
      if (
        this.user.name &&
        this.user.name !== "" &&
        this.user.age &&
        this.user.age !== ""
      ) {
        this.list.push(Object.assign({}, this.user));
        this.user.name = "";
        this.user.age = "";
      }
    },
    removeItem(i) {
      this.list = this.list.filter((val, index) => index !== i);
    },
  },
};
</script>

Vue3.x实现一个成员的添加和删减方式

composition-api

修改文件AboutView.vue。

<template>
  <div class="about">
    <h1>This is an about page</h1>
    <h3>Vue3.x实现成员添加和删减</h3>
    <p><input type="text" v-model="params.item.name" /></p>
    <p><input type="text" v-model="params.item.age" /></p>
    <p><button @click="addItem">添加一个成员</button></p>

    <ul>
      <li
        v-for="(item, index) in list.items"
        :key="index"
        @click="removeItem(index)"
      >
        No.{{ index + 1 }} ---- 姓名:{{ item.name }} ---- 年龄:{{ item.age }}
      </li>
    </ul>
  </div>
</template>

<script>
import { reactive } from "vue";

export default {
  data() {
    return {};
  },
  components: {},
  methods: {},
  setup(props) {
    console.log(props);

    let list = reactive({
      items: [
        {
          name: "张三",
          age: 17,
        },
        {
          name: "李四",
          age: 19,
        },
        {
          name: "王五",
          age: 18,
        },
      ],
    });
    let params = reactive({
      item: {
        name: "阿三",
        age: 20,
      },
    });

    function addItem() {
      if (
        params.item.name &&
        params.item.name !== "" &&
        params.item.age &&
        params.item.age !== ""
      ) {
        list.items.push(Object.assign({}, params.item));
        params.item.name = "";
        params.item.age = "";
      }
    }

    function removeItem(i) {
      list.items = list.items.filter((val, index) => index !== i);
    }

    return {
      list,
      params,
      addItem,
      removeItem,
    };
  },
};
</script>

Vue3.x实现一个成员的添加和删减方式优化1

修改文件AboutView.vue。

<template>
  <div class="about">
    <h1>This is an about page</h1>
    <h3>Vue3.x实现成员添加和删减</h3>
    <p><input type="text" v-model="params.item.name" /></p>
    <p><input type="text" v-model="params.item.age" /></p>
    <p><button @click="addItem">添加一个成员</button></p>

    <ul>
      <li
        v-for="(item, index) in list.items"
        :key="index"
        @click="removeItem(index)"
      >
        No.{{ index + 1 }} ---- 姓名:{{ item.name }} ---- 年龄:{{ item.age }}
      </li>
    </ul>
  </div>
</template>

<script>
import { reactive } from "vue";

export default {
  data() {
    return {};
  },
  components: {},
  methods: {},
  setup(props) {
    console.log(props);

    let { list, removeItem } = originRemoveItem();
    let { params, addItem } = originAddItem(list);

    return {
      params,
      addItem,
      list,
      removeItem,
    };
  },
};

// 原始数据和删除方法
function originRemoveItem() {
  // 原始数据
  let list = reactive({
    items: [
      {
        name: "张三",
        age: 17,
      },
      {
        name: "李四",
        age: 19,
      },
      {
        name: "王五",
        age: 18,
      },
    ],
  });
  // 删除一条数据
  function removeItem(i) {
    list.items = list.items.filter((val, index) => index !== i);
  }

  return {
    list,
    removeItem,
  };
}

// 默认数据和添加方法
function originAddItem(list) {
  let params = reactive({
    item: {
      name: "阿三",
      age: 20,
    },
  });

  function addItem() {
    if (
      params.item.name &&
      params.item.name !== "" &&
      params.item.age &&
      params.item.age !== ""
    ) {
      list.items.push(Object.assign({}, params.item));
      params.item.name = "";
      params.item.age = "";
    }
  }

  return {
    params,
    addItem,
  };
}
</script>

Vue3.x实现一个成员的添加和删减方式优化2

将原始的数据删除和数据添加的方法,分别挪到独立的JS文件中。

  1. 在src目录中,新建一个目录hooks。

  2. 在hooks目录中,新建文件del.js。

    import { reactive } from 'vue'
    
    // 按需导出
    // 原始数据和删除方法
    export function originRemoveItem() {
        // 原始数据
        let list = reactive({
            items: [
                {
                    name: "张三",
                    age: 17,
                },
                {
                    name: "李四",
                    age: 19,
                },
                {
                    name: "王五",
                    age: 18,
                },
            ],
        });
        // 删除一条数据
        function removeItem(i) {
            list.items = list.items.filter((val, index) => index !== i);
        }
    
        return {
            list,
            removeItem,
        };
    }
    
    
  3. 在hooks目录中,新建文件add.js。

    import { reactive } from 'vue'
    
    // 按需导出
    // 默认数据和添加方法
    export function originAddItem(list) {
        let params = reactive({
            item: {
                name: "阿三",
                age: 20,
            },
        });
    
        function addItem() {
            if (
                params.item.name &&
                params.item.name !== "" &&
                params.item.age &&
                params.item.age !== ""
            ) {
                list.items.push(Object.assign({}, params.item));
                params.item.name = "";
                params.item.age = "";
            }
        }
    
        return {
            params,
            addItem,
        };
    }
    
    
  4. 修改文件AboutView.vue。

    <template>
      <div class="about">
        <h1>This is an about page</h1>
        <h3>Vue3.x实现成员添加和删减</h3>
        <p><input type="text" v-model="params.item.name" /></p>
        <p><input type="text" v-model="params.item.age" /></p>
        <p><button @click="addItem">添加一个成员</button></p>
    
        <ul>
          <li
            v-for="(item, index) in list.items"
            :key="index"
            @click="removeItem(index)"
          >
            No.{{ index + 1 }} ---- 姓名:{{ item.name }} ---- 年龄:{{ item.age }}
          </li>
        </ul>
      </div>
    </template>
    
    <script>
    import { originRemoveItem } from "../hooks/del.js";
    import { originAddItem } from "../hooks/add.js";
    
    export default {
      data() {
        return {};
      },
      components: {},
      methods: {},
      setup(props) {
        console.log(props);
        console.log(originRemoveItem);
        console.log(originAddItem);
    
        let { list, removeItem } = originRemoveItem();
        let { params, addItem } = originAddItem(list);
    
        return {
          params,
          addItem,
          list,
          removeItem,
        };
      },
    };
    </script>
    
    

Vue3.x原理

  • 在Vue2.x中,如果属性在data()方法中未定义,随后在相应的方法中,新增属性,此时新增的属性实际上是生效的,但不会渲染到页面中(非响应式)。Vue2.x中的双向数据绑定会存在一些缺陷。
    Vue2.x使用的是 Object.prototype 方式。

    <script>
        var VM = new Vue({
            el: '#app',
            data: {
                person: {
                    name: "张三",
                    hobby: ["打篮球", "游泳", "健身"],
                },
            },
            methods: {
                changePerson() {
                    // this.person.age = 18; // 不生效
                    // this.$set(this.person, "age", 19); // 生效
                    this.person = Object.assign({}, this.person, {age: 20}); // 生效
    
                    // 数组下标的方式修改元素也是不生效的
                    this.person.habby[0] = "踢足球"; // 不生效
                    this.$set(this.person.hobby, 0, "吃饭"); // 生效
                }
            },
        })
    </script>
    
  • Vue3.x绑定原理。不存在该问题。
    Vue3.x使用的是Proxy方式。

    <template>
      <div class="about">
        <h1>This is an about page</h1>
        <h3>Vue3.x的绑定原理</h3>
        <p><button @click="changeObj">修改当前对象</button></p>
        <p>{{ person }}</p>
      </div>
    </template>
    
    <script>
    import { reactive } from "vue";
    
    export default {
      setup(props) {
        console.log(props);
    
        var person = reactive({
          name: "张三",
          hobby: ["打篮球", "游泳", "健身"],
        });
    
        function changeObj() {
          person.age = 20; // 生效
          person.hobby[0] = "读书"; // 生效
        }
    
        return {
          person,
          changeObj,
        };
      },
    };
    </script>
    
    

computed

示例:

<template>
  <div class="about">
    <h1>This is an about page</h1>
    <h3>Vue3.x的computed</h3>

    <table border="1" cellpadding="0" cellspacing="0">
      <tr>
        <th><input type="checkbox" name="" id="" v-model="isAllChecked" /></th>
        <th>商品名字</th>
        <th>商品价格</th>
        <th>购买数量</th>
      </tr>
      <tr v-for="item in iceCream" :key="item.id">
        <td>
          <input type="checkbox" name="" id="" v-model="item.isChecked" />
        </td>
        <td>{{ item.name }}</td>
        <td>{{ item.price }}</td>
        <td>{{ item.num }}</td>
      </tr>
      <tr>
        <td>总计:</td>
        <td colspan="3">{{ totalPrice }}</td>
      </tr>
    </table>
  </div>
</template>

<script>
import { computed, reactive } from "vue";

export default {
  setup(props) {
    console.log(props);

    var { iceCream } = reactive({
      iceCream: [
        {
          id: 101,
          name: "哈根达斯",
          price: 98,
          num: 10,
          isChecked: true,
        },
        {
          id: 100,
          name: "DQ",
          price: 67,
          num: 15,
          isChecked: false,
        },
        {
          id: 112,
          name: "八喜",
          price: 48,
          num: 28,
          isChecked: false,
        },
        {
          id: 117,
          name: "蒙牛",
          price: 3,
          num: 34,
          isChecked: false,
        },
      ],
    });

    var isAllChecked = computed({
      get() {
        return iceCream.every((el) => el.isChecked);
      },
      set(value) {
        iceCream.forEach((el) => (el.isChecked = value));
      },
    });

    var totalPrice = computed(() => {
      return iceCream.reduce((cur, val) => {
        if (val.isChecked) {
          return cur + val.num * val.price;
        } else {
          return cur;
        }
      }, 0);
    });

    return {
      iceCream,
      isAllChecked,
      totalPrice,
    };
  },
};
</script>

<style lang="less" scoped>
table {
  width: 500px;
  margin: 0 auto;

  td {
    text-align: center;
  }
}
</style>

watch

<template>
  <div class="about">
    <h1>This is an about page</h1>
    <h3>Vue3.x的watch函数</h3>
    <h3>1. 监听ref的响应式数据</h3>
    <p>
      当前的年龄是:{{ ageRef }}
      <br />
      出生日期:<input type="text" v-model.lazy="birth" />
    </p>

    <br />
    <h3>2. 监听多个ref的响应式数据</h3>
    <p>
      当前的姓名是:{{ fullName }}
      <input type="text" v-model.lazy="firstName" />
      <input type="text" v-model.lazy="lastName" />
    </p>

    <br />
    <h3>3. 监听reactive的响应式数据 ---- {{ film }}</h3>
    <p>当前的电影票价是:<input type="number" v-model.lazy="movie.price" /></p>
    <p>当前的电影是:<input type="text" v-model.lazy="movie.title" /></p>
    <p>movie: {{ movie }}</p>

    <br />
    <h3>4. 监听reactive数据中的某个属性</h3>
    <p>当前的学生是:<input type="text" v-model.lazy="student.name" /></p>
    <p>student: {{ student }}</p>
  </div>
</template>

<script>
import { watch, ref, reactive } from "vue";

export default {
  setup(props) {
    console.log(props);

    // 1. 监听ref的响应式数据,newVal和oldVal都可以正常使用
    var birth = ref("2000-01-04");
    var ageRef = ref(0);
    watch(
      birth, // 被监听的数据
      (newVal, oldVal) => {
        console.log(newVal, oldVal);
        ageRef.value =
          new Date().getFullYear() - new Date(birth.value).getFullYear();
      },
      {
        immediate: true, // 初始状态也生效
      }
    );

    // 2. 监听多个ref的响应式数据
    var firstName = ref("张");
    var lastName = ref("三");
    var fullName = ref("");
    watch(
      [firstName, lastName],
      (newVal, oldVal) => {
        console.log(newVal, oldVal);
        fullName.value = newVal[0] + newVal[1];
      },
      {
        immediate: true, // 初始状态也生效
      }
    );

    // 3. 监听reactive的响应式数据
    // watch监听reactive响应式数据,无法正确获取oldVal
    // 默认开启的是深度监听
    var movie = reactive({
      title: "长津湖",
      price: 48,
    });
    var film = reactive({});
    watch(
      movie,
      (newVal, oldVal) => {
        console.log(newVal, oldVal);
        film.title = newVal.title;
        film.price = newVal.price;
      },
      { immediate: true }
      // { deep: false }
    );

    // 4. 监听reactive数据中的某个属性
    // watch监听reactive响应式数据的某个属性,可以正确获取oldVal
    var student = reactive({
      name: "李晓华",
      age: 12,
    });
    watch(
      // () => student.name,
      student.name,
      (newVal, oldVal) => {
        console.log(newVal, oldVal);
      }
    );

    return {
      birth,
      ageRef,
      firstName,
      lastName,
      fullName,
      movie,
      film,
      student,
    };
  },
};
</script>

<style lang="less" scoped></style>

数据绑定指令(举例说明)

指令说明

  • v-text指令:向其所在的节点中渲染文本内容,放入标签则也会被当成文本解析。

  • {{}}插值表达式:基本上和v-text的作用差不多,可以理解为v-text的简写。

    v-text与差值表达式的区别:v-text会替换掉节点中的内容,原来的内容会被代替,无法与原来的内容一起出现。{{}}插值表达式则可以。

    在Vue3.x中直接在包含内容的标签上使用v-text会报错。

  • v-html指令:向指定节点中渲染包含html结构的内容,更新元素的innerHTML。

    内容按普通HTML插入,不会作为Vue模板进行编译。如果试图使用v-html组合模板,可以重新考虑通过使用组件来替代。

操作步骤

  1. 创建Vue3.x项目,参见[Vite创建Vue3.x项目【模板】](# Vite创建Vue3.x项目【模板】)。

  2. 在src\views目录下,新建文件InstructionView.vue。

    <template>
      <div>
        <h3>1. 数据绑定指令</h3>
        <h3>今日份咖啡:{{ coffee }}</h3>
      </div>
    </template>
    
    <script setup>
    // 使用了setup的语法糖
    import { ref, reactive } from "vue";
    
    const coffee = ref("卡布奇洛");
    const movie = reactive({
      name: "蝙蝠侠",
      price: 55,
    });
    
    const params = reactive({
      username: "admin",
      password: "123",
    });
    </script>
    
    
  3. 修改文件src\router\index.ts。

    import { createRouter, createWebHashHistory, RouteRecordRaw } from "vue-router";
    import HomeView from "../views/HomeView.vue";
    import InstructionView from "../views/InstructionView.vue";
    
    const routes: Array<RouteRecordRaw> = [
      {
        path: "/",
        name: "home",
        component: HomeView,
      },
      {
        path: "/about",
        name: "about",
        // route level code-splitting
        // this generates a separate chunk (about.[hash].js) for this route
        // which is lazy-loaded when the route is visited.
        component: () =>
          import(/* webpackChunkName: "about" */ "../views/AboutView.vue"),
      },
      {
        path: "/instruction",
        name: "instruction",
        component: InstructionView,
      },
    ];
    
    const router = createRouter({
      history: createWebHashHistory(),
      routes,
    });
    
    export default router;
    
    
  4. 修改文件src\App.vue。

    <template>
      <nav>
        <router-link to="/">Home</router-link> |
        <router-link to="/about">About</router-link> |
        <router-link to="/instruction">instruction</router-link>
      </nav>
      <router-view />
    </template>
    
    <style lang="less">
    #app {
      font-family: Avenir, Helvetica, Arial, sans-serif;
      -webkit-font-smoothing: antialiased;
      -moz-osx-font-smoothing: grayscale;
      text-align: center;
      color: #2c3e50;
    }
    
    nav {
      padding: 30px;
    
      a {
        font-weight: bold;
        color: #2c3e50;
    
        &.router-link-exact-active {
          color: #42b983;
        }
      }
    }
    </style>
    
    
  5. 在src\components目录下,新建子组件InstructionComp.vue。

    <template>
      <div>
     <h3>1. 数据绑定指令</h3>
        <!-- {{}}插值表达式指令,计算表达式等均可 -->
        <p>今日份咖啡:{{ coffee }}</p>
    
        <hr />
        <!-- v-text指令,只能渲染文本 -->
        <p v-text="coffee"></p>
        <!-- v-text指令,<strong>蝙蝠侠</strong>,不会解析strong元素 -->
        <p v-text="movie.name"></p>
    
        <hr />
        <!-- v-html指令,蝙蝠侠,会解析strong元素 -->
        <!-- v-html指令,不建议使用,存在安全风险,容易造成XSS攻击 -->
        <p v-html="movie.name"></p>
    
        <hr />
        <!-- v-model指令,双向数据绑定,作用于表单元素 -->
        <p>用户名:<input type="text" v-model="params.username" /></p>
        <p>密码:<input type="text" v-model="params.password" /></p>
        <p>params: {{ params }}</p>
        <!-- v-model指令,语法糖解析 -->
        <input
          type="text"
          :value="params.username"
          @input="params.username = $event.target.value"
        />
    
        <hr />
        <!-- v-model指令,双向数据绑定,作用于表单元素,单选框举例 -->
        <p>单选按钮:</p>
        <label for="">Male</label>
        <input type="radio" name="" id="" value="Male" v-model="gender" /> |
        <label for="">Female</label>
        <input type="radio" name="" id="" value="Female" v-model="gender" /> |
        <label for="">Other</label>
        <input type="radio" name="" id="" value="Other" v-model="gender" />
        <p>当前选择的性别是:{{ gender }}</p>
    
        <hr />
        <!-- v-model指令,双向数据绑定,作用于表单元素,单选框举例 -->
        <p>单选框:</p>
        <input type="checkbox" v-model="isAgree" />大熊协议
        <p>您当前的选择是:{{ isAgree ? "同意" : "不同意" }}</p>
    
        <hr />
        <!-- v-model指令,双向数据绑定,作用于表单元素,多选框举例 -->
        <!-- 多选框时候,存储数据的变量要以数组的形成呈现 -->
        <p>多选框:</p>
        <input type="checkbox" value="足球" v-model="hobbit" />足球
        <input type="checkbox" value="游泳" v-model="hobbit" />游泳
        <input type="checkbox" value="乒乓球" v-model="hobbit" />乒乓球
        <input type="checkbox" value="篮球" v-model="hobbit" /> 篮球
        <p>您当前的爱好是:{{ hobbit }}</p>
    
        <hr />
        <!-- v-model指令,双向数据绑定,作用于表单元素,下拉框举例 -->
        <p>select下拉框</p>
        <select name="" id="" v-model="level">
          <option value="黑铁">黑铁</option>
          <option value="青铜">青铜</option>
          <option value="白银">白银</option>
          <option value="黄金">黄金</option>
          <option value="铂金">铂金</option>
          <option value="钻石">钻石</option>
          <option value="荣耀">荣耀</option>
          <option value="王者">王者</option>
        </select>
        <p>您当前的段位是:{{ level }}</p>
    
        <hr />
        <!-- v-model指令,双向数据绑定,作用于表单元素,xx.number修饰符,数值类型字符串转换为数值 -->
        <input type="text" v-model.number="num1" /> +
        <input type="text" v-model.number="num2" /> =
        <input type="text" :value="num1 + num2" />
        <p>typeof num1: {{ typeof num1 }}</p>
    
        <hr />
        <!-- v-model指令,双向数据绑定,作用于表单元素,xx.lazy修饰符,鼠标失去焦点的时候才会触发数据的同步 -->
        <input type="text" v-model.lazy="mobile" />
        <p>mobile: {{ mobile }}</p>
    
        <hr />
        <!-- v-model指令,双向数据绑定,作用于表单元素,xx.trim修饰符,自动去掉字符串前后的空格 -->
        <input type="text" v-model.trim="params.username" />
        <p>params.username.length: {{ params.username.length }}</p>
      </div>
    </template>
    
    <script setup>
    // 使用了setup的语法糖
    import { ref, reactive } from "vue";
    
    const coffee = ref("卡布奇洛");
    const movie = reactive({
      name: "<strong>蝙蝠侠</strong>",
      price: 55,
    });
    
    const params = reactive({
      username: "admin",
      password: "123",
    });
    
    const gender = ref("Female");
    const isAgree = ref(false);
    const hobbit = ref([]);
    const level = ref("黑铁");
    
    const num1 = ref(0);
    const num2 = ref(0);
    
    const mobile = ref(0);
    </script>
    
    

属性绑定指令

参见[属性绑定指令(v-bind/:)](# 属性绑定指令(v-bind/:))内容进行改造成Vue3.x版本的内容。

条件判断指令

参见[条件渲染指令](# 条件渲染指令)内容进行改造成Vue3.x版本的内容。

<template>
  <div>
    <h3>条件判断指令</h3>
    <div>
      <p v-if="cartList.length !== 0">
        您当前加入购物车的商品一共有:{{ cartList.length }}件
      </p>
      <p v-else>您的购物车空空如也,快去购物吧</p>
    </div>

    <hr />
    <div>
      多分支条件语句,根据分数判断当前考试等级
      <p>您当前的考试等级是:</p>
      <p v-if="score >= 90">A</p>
      <p v-else-if="score >= 80">B</p>
      <p v-else-if="score >= 60">C</p>
      <p v-else>D</p>
    </div>
  </div>
</template>

<script setup>
// 使用了setup的语法糖
import { ref, reactive } from "vue";

const { cartList } = reactive({
  // cartList: [],
  cartList: [
    {
      id: 1001,
      name: "苹果",
    },
  ],
});

const score = ref(10);
</script>

v-for列表渲染指令

参见[v-for列表渲染指令](# v-for列表渲染指令)内容进行改造成Vue3.x版本的内容。

<template>
  <div>
    <h3>v-for列表渲染指令</h3>
    <!-- 在不做增删改查的时候,key绑定下标没问题 -->
    <!--  -->
    <p v-for="(item, index) in books" :key="index">
      {{ index + 1 }} ---- {{ item }}
    </p>

    <hr />
    <!-- 遍历对象内容 -->
    <!-- value表示对象的值 -->
    <!-- attr表示对象的属性名(key值) -->
    <p v-for="(value, attr) in actor" :key="attr">
      {{ attr }} ---- {{ value }}
    </p>
  </div>

  <hr />
  <!-- 循环嵌套 -->
  <div v-for="item in beverages" :key="item.id">
    <h3>{{ item.category }}</h3>
    <ul>
      <li v-for="(good, index) in item.commodity" :key="index">{{ good }}</li>
    </ul>
  </div>

  <hr />
  <h3>key绑定index和id的情况</h3>
  <p><button @click="addItem">添加一项数据</button></p>
  <ul>
    <!-- 不推荐如此使用,这种情况是存在问题的 -->
    <li v-for="(item, index) in todos" :key="index">
      当前的内容是:{{ item.text }} <input type="text" />
    </li>
  </ul>

  <!-- 
    分析:

    绑定index的旧虚拟DOM:
      <li :key="0">当前的内容是:吃饭 <input type=""></li>
      <li :key="1">当前的内容是:睡觉 <input type=""></li>
      <li :key="2">当前的内容是:打豆豆 <input type=""></li>

      绑定index的新虚拟DOM:
      index在头部添加一项数据的虚拟DOM:

      <li :key="0">当前的内容是:打游戏 <input type=""></li> ————key没有变化,input内容认为没有变化,实际上input内容需要跟随变化
      <li :key="1">当前的内容是:吃饭 <input type=""></li>
      <li :key="2">当前的内容是:睡觉 <input type=""></li>
      <li :key="3">当前的内容是:打豆豆 <input type=""></li>
   -->

  <!--
     绑定id的旧虚拟DOM:
      <li :key="1001">当前的内容是:吃饭 <input type=""></li>
      <li :key="1002">当前的内容是:睡觉 <input type=""></li>
      <li :key="1003">当前的内容是:打豆豆 <input type=""></li>

    绑定index的新虚拟DOM:
      <li :key="1004">当前的内容是:打游戏 <input type=""></li> ————key新增DOM
      <li :key="1001">当前的内容是:吃饭 <input type=""></li> ————key无变化
      <li :key="1002">当前的内容是:睡觉 <input type=""></li> ————key无变化
      <li :key="1003">当前的内容是:打豆豆 <input type=""></li> ————key无变化
   -->

  <hr />
  <h3>key绑定index和id的情况</h3>
  <p><button @click="addItem">添加一项数据</button></p>
  <ul>
    <!-- 推荐 -->
    <li v-for="item in todos" :key="item.id">
      当前的内容是:{{ item.text }} <input type="text" />
    </li>
  </ul>
</template>

<script setup>
// 使用了setup的语法糖
import { reactive } from "vue";

// v-for:用于循环列表数据
// 可遍历:数组、对象、字符串、指定次数
// v-for指令:"item in data",data为数据源
// v-for还可以接收第二个参数,指的是当前项的索引
//  语法:v-for="(item, index) in data"

// key的作用:
// key是虚拟DOM的标识,当数据发生变化的时候,Vue会根据新数据生成新的虚拟DOM节点
// 随后Vue进行新虚拟DOM和旧虚拟DOM的对比,对比规则:
// 1)旧虚拟DOM找到新虚拟DOM的key值
//  如果虚拟DOM内容没有变化,直接使用之前的真实DOM
//  如果虚拟DOM内容发生了变化,则生成新的真实DOM,随后替换掉页面之前的真实DOM
// 2)旧虚拟DOM未找到与新的虚拟DOM相同的key,会创建真实的DOM,随后渲染到页面

const { books, actor, beverages } = reactive({
  books: [
    "西游记",
    "水浒传",
    "三国演义",
    "人性的弱点",
    "晚熟的人",
    "人类简史",
    "今日简史",
    "未来简史",
    "三体",
  ],
  actor: {
    name: "沈腾",
    age: 40,
    work: "演员",
  },
  beverages: [
    {
      id: 10011,
      category: "咖啡",
      commodity: ["拿铁", "厚乳", "瑞纳水", "卡布奇诺", "美式"],
    },
    {
      id: 10015,
      category: "茶叶",
      commodity: ["西湖龙井", "太平猴魁", "黄山毛峰", "庐山雨雾", "银针白毫"],
    },
    {
      id: 10019,
      category: "奶茶",
      commodity: ["珍珠奶茶", "燕麦奶茶", "波霸奶茶", "蓝莓奶茶", "椰果奶茶"],
    },
  ],
});

// 验证key绑定index和id的区别
const { todos } = reactive({
  todos: [
    {
      id: 1001,
      text: "吃饭",
      done: false,
    },
    {
      id: 1002,
      text: "睡觉",
      done: true,
    },
    {
      id: 1003,
      text: "打豆豆",
      done: false,
    },
  ],
});

function addItem() {
  todos.unshift({
    id: 1004,
    text: "打游戏",
    done: false,
  });
}
</script>

/---- Vue3.x+TS ----/

Vue+TS

模板语法&Vue指令

模板插值语法

  1. 搭建Vue3.x+TS环境。

  2. 修改文件AblutView.vue。验证模板插值语法。

    <template>
      <div class="about">
        <h1>This is an about page</h1>
        <p>{{ msg }}</p>
        <p>{{ msg.split(",") }}</p>
      </div>
    </template>
    
    <script setup lang="ts">
    let msg: string = "Hello, TS.";
    </script>
    
    

指令

  • v-text:显示文本。

    <template>
      <div class="about">
        <h1>This is an about page</h1>
        <p v-text="msg"></p>
      </div>
    </template>
    
    <script setup lang="ts">
    let msg: string = "Hello, TS.";
    </script>
    
    
  • v-html:展示富文本。

    <template>
      <div class="about">
        <h1>This is an about page</h1>
        <p v-html="msg"></p>
      </div>
    </template>
    
    <script setup lang="ts">
    let msg: string = "<div>Hello, TS.</div>";
    </script>
    
    
  • v-if: 用来控制元素的显示隐藏(切换真假DOM)。

    v-else-if:表示 v-if 的“else if 块”。可以链式调用。

    v-else:v-if条件收尾语句。

    <template>
      <div class="about">
        <h1>This is an about page</h1>
        <p v-if="flag === 'A'">A</p>
        <p v-else-if="flag === 'B'">B</p>
        <p v-else-if="flag === 'C'">C</p>
        <p v-else>D</p>
      </div>
    </template>
    
    <script setup lang="ts">
    let flag: string = "B";
    </script>
    
    
  • v-show:用来控制元素的显示隐藏(display none block ccss切换)。

    <template>
      <div class="about">
        <h1>This is an about page</h1>
        <p v-show="flag">A</p>
        <p>B</p>
      </div>
    </template>
    
    <script setup lang="ts">
    let flag: boolean = false;
    </script>
    
    
  • v-on:简写@用来给元素添加事件。

    <template>
      <div class="about">
        <h1>This is an about page</h1>
        <button @click="clickEvent">按键事件</button>
      </div>
    </template>
    
    <script setup lang="ts">
    const clickEvent = () => {
      console.log("触发了点击事件");
    };
    </script>
    
    
    • v-on修饰符:冒泡举例。

      <template>
        <div class="about">
          <h1>This is an about page</h1>
          <div @click="parent">
            <button @click.stop="clickEvent">按键事件</button>
          </div>
        </div>
      </template>
      
      <script setup lang="ts">
      const parent = () => {
        console.log("我是父级的内容");
      };
      
      const clickEvent = () => {
        console.log("我是子级的内容");
      };
      </script>
      
      

      说明:如果子级没有添加.stop的话,会同时触发子级和父级的点击事件。

    • v-on修饰符:阻止表单提交案例。

      <template>
        <div class="about">
          <h1>This is an about page</h1>
          <form action="/">
            <button @click.prevent="clickEvent" type="submit">提交</button>
          </form>
        </div>
      </template>
      
      <script setup lang="ts">
      const clickEvent = () => {
        console.log("我是子级的内容");
      };
      </script>
      
      
  • v-bind:简写:用来绑定元素的属性Attr。

    • 绑定样式举例,对象格式。

      <template>
        <div class="about">
          <h1>This is an about page</h1>
      
          <div :style="style">我是一个测试数据</div>
        </div>
      </template>
      
      <script setup lang="ts">
      type Style = {
        color: string;
        height: string;
      };
      
      const style: Style = {
        color: "blue",
        height: "200px",
      };
      </script>
      
      
    • 绑定样式举例,数组格式。

      <template>
        <div class="about">
          <h1>This is an about page</h1>
      
          <div :class="['a', 'b']">我是一个测试数据</div>
        </div>
      </template>
      
      <script setup lang="ts"></script>
      
      <style>
      .a {
        color: red;
      }
      
      .b {
        height: 300px;
      }
      </style>
      
      
    • 绑定样式举例,数组格式,增加条件表达式。

      <template>
        <div class="about">
          <h1>This is an about page</h1>
      
          <div :class="[flag ? 'a' : 'b']">我是一个测试数据</div>
        </div>
      </template>
      
      <script setup lang="ts">
      const flag: boolean = true;
      </script>
      
      <style>
      .a {
        color: red;
      }
      
      .b {
        color: blue;
      }
      </style>
      
      
    • 绑定样式举例,对象格式。

      <template>
        <div class="about">
          <h1>This is an about page</h1>
      
          <div :class="cls">我是一个测试数据</div>
        </div>
      </template>
      
      <script setup lang="ts">
      type Cls = {
        a: boolean;
        b: boolean;
      };
      
      const cls = {
        a: true,
        b: true,
      };
      </script>
      
      <style>
      .a {
        color: red;
      }
      
      .b {
        height: 200px;
      }
      </style>
      
      
  • v-for:用来遍历元素。

    • 遍历数组格式。

      <template>
        <div class="about">
          <h1>This is an about page</h1>
      
          <div v-for="item in arr" :key="item">{{ item }}</div>
        </div>
      </template>
      
      <script setup lang="ts">
      const arr: Array<number> = [1, 2, 3, 4, 5];
      </script>
      
      <style></style>
      
      
    • 遍历对象格式。

      <template>
        <div class="about">
          <h1>This is an about page</h1>
      
          <div v-for="item in arr" :key="item.id">{{ item.name }}</div>
        </div>
      </template>
      
      <script setup lang="ts">
      const arr: Array<any> = [
        {
          id: 1,
          name: "张三",
        },
        {
          id: 2,
          name: "李四",
        },
      ];
      </script>
      
      <style></style>
      
      
  • v-model:数据的双向绑定。

    <template>
      <div class="about">
        <h1>This is an about page</h1>
    
        <input v-model="coffee" type="text" />
        <p>{{ coffee }}</p>
      </div>
    </template>
    
    <script setup lang="ts">
    import { ref } from "vue";
    
    const coffee = ref("卡布奇洛");
    </script>
    
    <style></style>
    
    

ref全家桶

  • ref使用,方式一

    <template>
      <div class="about">
        <h1>This is an about page</h1>
    
        <p>{{ msg }}</p>
        <button @click="changeMsg">修改msg值</button>
      </div>
    </template>
    
    <script setup lang="ts">
    import { ref, Ref } from "vue";
    
    let msg: Ref<string> = ref("卡布奇洛");
    
    function changeMsg(): void {
      msg.value = "Hello TS";
    }
    
    </script>
    
    <style>
    </style>
    
    

    说明:注意被ref包装之后需要.value 来进行赋值。

  • ref使用,方式二

    <template>
      <div class="about">
        <h1>This is an about page</h1>
    
        <p>{{ msg }}</p>
        <button @click="changeMsg">修改msg值</button>
      </div>
    </template>
    
    <script setup lang="ts">
    import { ref } from "vue";
    
    let msg = ref<string | number>("卡布奇洛");
    
    function changeMsg(): void {
      msg.value = "Hello TS";
    }
    
    </script>
    
    <style>
    </style>
    
    
  • isRef:判断是不是一个Ref对象。

    <template>
      <div class="about">
        <h1>This is an about page</h1>
    
        <p>{{ msg }}</p>
        <button @click="changeMsg">修改msg值</button>
      </div>
    </template>
    
    <script setup lang="ts">
    import { ref, Ref, isRef } from "vue";
    
    let msg: Ref<string> = ref("卡布奇洛");
    let notRef: number = 10;
    
    function changeMsg(): void {
      msg.value = "Hello TS";
    
      console.log(isRef(msg)); // true
      console.log(isRef(notRef)); // false
    }
    
    </script>
    
    <style>
    </style>
    
    
  • shallowRef:创建一个跟踪自身.value变化的ref,但不会使其值也变成响应式的。

    <template>
      <div class="about">
        <h1>This is an about page</h1>
    
        <p>{{ msg }}</p>
        <button @click="changeMsg">修改msg值</button>
      </div>
    </template>
    
    <script setup lang="ts">
    import { ref, Ref, isRef, shallowRef } from "vue";
    
    type Obj = {
      name: string
    }
    let msg: Ref<Obj> = shallowRef({
      name: "张三"
    });
    
    function changeMsg(): void {
      // msg.value.name = "Hello TS"; // 不会生效
      msg.value = { name: "Hello Ts" }; // 生效
    }
    
    </script>
    
    <style>
    </style>
    
    
  • triggerRef:强制刷新页面DOM。

    <template>
      <div class="about">
        <h1>This is an about page</h1>
    
        <p>{{ msg }}</p>
        <button @click="changeMsg">修改msg值</button>
      </div>
    </template>
    
    <script setup lang="ts">
    import { ref, Ref, isRef, shallowRef, triggerRef } from "vue";
    
    type Obj = {
      name: string
    }
    let msg: Ref<Obj> = shallowRef({
      name: "张三"
    });
    
    function changeMsg(): void {
      msg.value.name = "Hello TS"; // 不会生效
      triggerRef(msg); // 强制刷新页面DOM
    }
    
    </script>
    
    <style>
    </style>
    
    

    示例2:triggerRef存在视图更新的问题。

    <template>
      <div class="about">
        <h1>This is an about page</h1>
    
        <p>{{ r }}</p>
        <p>{{ msg }}</p>
        <button @click="changeMsg">修改msg值1</button>
        <button @click="changeMsg2">修改msg值2</button>
      </div>
    </template>
    
    <script setup lang="ts">
    import { ref, Ref, shallowRef } from "vue";
    
    type Obj = {
      name: string
    }
    let msg: Ref<Obj> = shallowRef({
      name: "张三",
      bar: "哈哈"
    });
    
    let r = ref<string>("前缀");
    
    function changeMsg(): void {
      console.log("changeMsg ing.");
    
      msg.value.name = "Hello TS"; // 不会生效
    }
    
    function changeMsg2(): void {
      console.log("changeMsg2 ing.");
    
      r.value = "前缀被修改";
      msg.value.name = "Hello TS"; // 上行代码的修改会触发triggerRef方法导致shallowRef失效
    }
    
    </script>
    
    <style>
    </style>
    
    
  • customerRef:自定义ref。
    customRef是个工厂函数要求我们返回一个对象,并且实现get和set。

    <template>
      <div class="about">
        <h1>This is an about page</h1>
    
        <p>{{ msg }}</p>
        <button @click="changeMsg">修改msg值</button>
      </div>
    </template>
    
    <script setup lang="ts">
    import { Ref, shallowRef, triggerRef, customRef } from 'vue'
    
    function MyRef<T>(value: T) {
      return customRef((track, trigger) => {
        return {
          get() {
            track();
            return value;
          },
          set(newVal: T) {
            console.log('set');
            value = newVal;
            trigger();
          }
        }
      })
    }
    
    let msg = MyRef('张三');
    const changeMsg = () => {
      msg.value = '李四';
    }
    </script>
    
    <style>
    </style>
    
    

reacive全家桶

用来绑定复杂的数据类型,例如对象和数组。

  • reactive

    <template>
      <div class="about">
        <h1>This is an about page</h1>
    
        <p>{{ obj }}</p>
        <p>{{ msg1 }}</p>
        <p>{{ msg2 }}</p>
    
      </div>
    </template>
    
    <script setup lang="ts">
    import { reactive } from 'vue';
    
    type Obj = {
      name: string,
      age: number
    }
    let obj = reactive<Obj>({
      name: "张三",
      age: 15
    });
    obj.name = "李四";
    
    let msg1 = reactive<number[]>([]);
    let arr1: Array<number> = [1, 2, 3, 4, 5];
    msg1 = arr1;
    
    let msg2 = reactive<number[]>([]);
    let arr2: Array<number> = [2, 3, 4, 5, 6];
    msg2.push(...arr2);
    
    </script>
    
    <style>
    </style>
    
    
  • readonly:拷贝一份proxy对象将其设置为只读。

    <template>
      <div class="about">
        <h1>This is an about page</h1>
    
        <p>{{ obj }}</p>
      </div>
    </template>
    
    <script setup lang="ts">
    import { reactive, readonly } from 'vue';
    
    type Obj = {
      name: string,
      age: number,
      count: number
    }
    let obj = reactive<Obj>({
      name: "张三",
      age: 15,
      count: 0
    });
    
    const copy = readonly(obj);
    // copy.count++; // 报错,无法分配到 "count" ,因为它是只读属性
    
    obj.count++; // 没问题
    obj.name = "李四"; // 没问题
    
    </script>
    
    <style>
    </style>
    
    
  • shallowReactive

    • 直接代码触发方法,会导致页面的渲染修改。
      在DOM挂载阶段,是会生效的。如果挂载之后再去修改数据,是不会被改变的。

      <template>
        <div class="about">
          <h1>This is an about page</h1>
      
          <p>{{ msg }}</p>
        </div>
      </template>
      
      <script setup lang="ts">
      import { shallowReactive } from 'vue';
      
      let msg = shallowReactive({
        name: "张三",
        age: 15,
        count: 0,
        nav: {
          bar: {
            text: "Hello"
          }
        }
      });
      
      const change1 = () => {
        msg.name = "张三改成了李四";
        console.log(msg);
      }
      
      const change2 = () => {
        msg.nav.bar.text = "我被改啦";
        console.log(msg);
      }
      
      change1();
      change2(); // 会触发页面的内容渲染修改
      
      </script>
      
      <style>
      </style>
      
      
    • 如果挂载之后再去修改数据,是不会被改变的。

      <template>
        <div>
          <div>{{ state }}</div>
          <button @click="change1">test1</button>
          <button @click="change2">test2</button>
        </div>
      </template>
      
      <script setup lang="ts">
      import { shallowReactive } from 'vue'
      
      const obj = {
        a: 1,
        first: {
          b: 2,
          second: {
            c: 3
          }
        }
      }
      
      const state = shallowReactive(obj)
      
      function change1() {
        state.a = 7
      }
      function change2() {
        state.first.b = 8
        state.first.second.c = 9
        console.log(state);
      }
      </script>
      
      <style>
      </style>
      

to系列全家桶(toRef/toRefs/toRaw)

  • toRef:针对原始对象是否是响应式的对象使用效果有所差异。

    • 如果原始对象是非响应式的就不会更新视图,数据是会变的。toRef引用对象,会对自身造成影响,也会对原始对象造成影响,同时页面上的视图是不会发生变化的。

      <template>
        <div class="about">
          <h1>This is an about page</h1>
      
          <button @click="change">修改</button>
          <p>{{ msg }}</p>
        </div>
      </template>
      
      <script setup lang="ts">
      import { toRef } from 'vue';
      
      let msg = {
        foo: 1,
        bar: 1
      };
      
      let state = toRef(msg, "foo");
      
      const change = () => {
        state.value++;
        console.log("原始对象:", msg);
        console.log("引用对象:", state);
      }
      
      </script>
      
      <style>
      </style>
      
      
    • 如果原始对象是一个proxy代理的响应式对象。引用对象、原始对象以及页面上的视图都是会发生变化。

      <template>
        <div class="about">
          <h1>This is an about page</h1>
      
          <button @click="change">修改</button>
          <p>{{ msg }}</p>
        </div>
      </template>
      
      <script setup lang="ts">
      import { toRef, reactive } from 'vue';
      
      let msg = reactive({
        foo: 1,
        bar: 1
      });
      
      let state = toRef(msg, "foo");
      
      const change = () => {
        state.value++;
        console.log("原始对象:", msg);
        console.log("引用对象:", state);
      }
      
      </script>
      
      <style>
      </style>
      
      
  • toRefs:可以帮我们批量创建ref对象主要是方便我们解构使用。
    非响应式举例:

    <template>
      <div class="about">
        <h1>This is an about page</h1>
    
        <button @click="change">修改</button>
        <p>{{ msg }}</p>
      </div>
    </template>
    
    <script setup lang="ts">
    import { toRefs, reactive } from 'vue';
    
    let msg = reactive({
      foo: 1,
      bar: 1
    });
    
    let { foo, bar } = msg;
    
    const change = () => {
      console.log(foo, bar); // 非响应式
    }
    
    </script>
    
    <style>
    </style>
    
    

    使用toRefs后,响应式举例:

    <template>
      <div class="about">
        <h1>This is an about page</h1>
    
        <button @click="change">修改</button>
        <p>{{ msg }}</p>
      </div>
    </template>
    
    <script setup lang="ts">
    import { toRefs, reactive } from 'vue';
    
    let msg = reactive({
      foo: 1,
      bar: 1
    });
    
    let { foo, bar } = toRefs(msg);
    
    const change = () => {
      foo.value++;
      bar.value++;
    }
    
    </script>
    
    <style>
    </style>
    
    
  • toRaw:将响应式对象转换为普通对象(节约内存)。

    <template>
      <div class="about">
        <h1>This is an about page</h1>
    
        <button @click="change1">修改msg</button>
        <p>msg -- {{ msg }}</p>
    
        <button @click="change2">修改raw</button>
        <p>raw -- {{ raw }}</p>
      </div>
    </template>
    
    <script setup lang="ts">
    import { toRaw, reactive } from 'vue';
    
    let msg = reactive({
      foo: 1,
      bar: 1
    });
    
    let raw = toRaw(msg);
    
    const change1 = () => {
      console.log("msg", msg); // msg Proxy {foo: 7, bar: 7},视图也会变化
      msg.foo++;
      msg.bar++;
    }
    
    const change2 = () => {
      console.log("raw", raw); // raw {foo: 8, bar: 8},视图不会变化
      raw.foo = 15;
      raw.bar++;
    }
    
    </script>
    
    <style>
    </style>
    
    

computed计算属性

  • 以姓名为例说明(常规的计算方式)

    <template>
      <div class="about">
        <h1>This is an about page</h1>
    
        <input type="text" v-model="firstName">
        <input type="text" v-model="lastName">
    
        <p>{{ firstName }} -- {{ lastName }}</p>
      </div>
    </template>
    
    <script setup lang="ts">
    import { computed, ref } from 'vue';
    
    let firstName = ref<string>("");
    let lastName = ref<string>("");
    
    </script>
    
    <style>
    </style>
    
    
  • 计算属性(方式一,函数形式),以计算属性的方式计算姓名全称。

    <template>
      <div class="about">
        <h1>This is an about page</h1>
    
        <input type="text" v-model="firstName">
        <input type="text" v-model="lastName">
    
        <p>{{ name }}</p>
      </div>
    </template>
    
    <script setup lang="ts">
    import { computed, ref } from 'vue';
    
    let firstName = ref<string>("");
    let lastName = ref<string>("");
    
    const name = computed(() => {
      return `${firstName.value} -- ${lastName.value}`;
    });
    
    </script>
    
    <style>
    </style>
    
    
  • 计算属性(方式二,对象形式),以计算属性的方式计算姓名全称。

    <template>
      <div class="about">
        <h1>This is an about page</h1>
    
        <input type="text" v-model="firstName">
        <input type="text" v-model="lastName">
    
        <p>{{ name }}</p>
      </div>
    </template>
    
    <script setup lang="ts">
    import { computed, ref } from 'vue';
    
    let firstName = ref<string>("");
    let lastName = ref<string>("");
    
    const name = computed({
      get() {
        return `${firstName.value} -- ${lastName.value}`;
      },
      set() {
        `${firstName.value} -- ${lastName.value}`;
      }
    });
    
    </script>
    
    <style>
    </style>
    
    

computed购物车案例

<template>
  <div>
    <table style="width:800px" border>
      <thead>
        <tr>
          <th>名称</th>
          <th>数量</th>
          <th>价格</th>
          <th>操作</th>
        </tr>
      </thead>
      <tbody>
        <tr :key="index" v-for="(item, index) in data">
          <td align="center">{{ item.name }}</td>
          <td align="center">
            <button @click="AddAnbSub(item, false)">-</button>
            {{ item.num }}
            <button @click="AddAnbSub(item, true)">+</button>
          </td>
          <td align="center">{{ item.num * item.price }}</td>
          <td align="center">
            <button @click="del(index)">删除</button>
          </td>
        </tr>
      </tbody>
      <tfoot>
        <tr>
          <td></td>
          <td></td>
          <td></td>
          <td align="center">总价:{{ $total }}</td>
        </tr>
      </tfoot>
    </table>
  </div>
</template>

<script setup lang="ts">
import { computed, reactive, ref } from 'vue'
type Shop = {
  name: string,
  num: number,
  price: number
}
let $total = ref<number>(0)
const data = reactive<Shop[]>([
  {
    name: "XX的袜子",
    num: 1,
    price: 100
  },
  {
    name: "XX的裤子",
    num: 1,
    price: 200
  },
  {
    name: "XX的衣服",
    num: 1,
    price: 300
  },
  {
    name: "XX的毛巾",
    num: 1,
    price: 400
  }
])

const AddAnbSub = (item: Shop, type: boolean = false): void => {
  if (item.num > 1 && !type) {
    item.num--
  }
  if (item.num <= 99 && type) {
    item.num++
  }
}
const del = (index: number) => {
  data.splice(index, 1)
}

$total = computed<number>(() => {
  return data.reduce((prev, next) => {
    return prev + (next.num * next.price)
  }, 0)
})

</script>

<style>
</style>

watch侦听器

<template>
  <div class="about">
    <h1>This is an about page</h1>
    <h3>Vue3.x的watch函数</h3>
    <h3>1. 监听ref的响应式数据</h3>
    <p>
      当前的年龄是:{{ ageRef }}
      <br />
      出生日期:<input type="text" v-model.lazy="birth" />
    </p>

    <br />
    <h3>2. 监听多个ref的响应式数据</h3>
    <p>
      当前的姓名是:{{ fullName }}
      <input type="text" v-model.lazy="firstName" />
      <input type="text" v-model.lazy="lastName" />
    </p>

    <br />
    <h3>3. 监听reactive的响应式数据 ---- {{ movie }}</h3>
    <p>当前的电影票价是:<input type="number" v-model.lazy="movie.price" /></p>
    <p>当前的电影是:<input type="text" v-model.lazy="movie.title" /></p>
    <p>movie: {{ movie }}</p>

    <br />
    <h3>4. 监听reactive数据中的某个属性</h3>
    <p>当前的学生是:<input type="text" v-model.lazy="student.name" /></p>
    <p>student: {{ student }}</p>
  </div>
</template>

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

// 1. 监听ref的响应式数据,newVal和oldVal都可以正常使用
var birth = ref<string>("2000-01-04");
var ageRef = ref<number>(0);
// 被监听的数据
watch(birth, (newVal, oldVal) => {
  console.log(newVal, oldVal);
  ageRef.value =
    new Date().getFullYear() - new Date(birth.value).getFullYear();
}, {
  immediate: true, // 初始状态也生效
});

// 2. 监听多个ref的响应式数据
var firstName = ref<string>("张");
var lastName = ref<string>("三");
var fullName = ref<string>("");
watch([firstName, lastName], (newVal, oldVal) => {
  console.log(newVal, oldVal);
  fullName.value = newVal[0] + newVal[1];
}, {
  immediate: true, // 初始状态也生效
});

// 3. 监听reactive的响应式数据
// watch监听reactive响应式数据,无法正确获取oldVal
// 默认开启的是深度监听
var movie = reactive({
  title: "长津湖",
  price: 48,
});
watch(movie, (newVal, oldVal) => {
  console.log(newVal, oldVal);
}, { immediate: true }
  // { deep: false }
);

// 4. 监听reactive数据中的某个属性
// watch监听reactive响应式数据的某个属性,可以正确获取oldVal
var student = reactive({
  name: "李晓华",
  age: 12,
});
watch(() => student.name, (newVal, oldVal) => {
  console.log(newVal, oldVal);
});

</script>

<style lang="less" scoped>
</style>

watchEffect高级侦听器

立即执行传入的一个函数,同时响应式追踪其依赖,并在其依赖变更时重新运行该函数。

如果用到message就只会监听messag,就是用到几个监听几个,而且是非惰性 会默认调用一次。

  • watchEffect使用

    <template>
      <div class="about">
        <h1>This is an about page</h1>
    
        <input type="text" v-model="msg1">
        <input type="text" v-model="msg2">
      </div>
    </template>
    
    <script setup lang="ts">
    import { watchEffect, ref } from "vue";
    
    let msg1 = ref<string | number>("");
    let msg2 = ref<string | number>("");
    
    watchEffect(() => {
      console.log("----");
      console.log("msg1: ", msg1.value);
      console.log("msg2: ", msg2.value);
    });
    </script>
    
    <style lang="less" scoped>
    </style>
    
    
  • 消除副作用:就是在触发监听之前会调用一个函数可以处理你的逻辑例如防抖。

    <template>
      <div class="about">
        <h1>This is an about page</h1>
    
        <input type="text" v-model.lazy="msg1">
      </div>
    </template>
    
    <script setup lang="ts">
    import { watchEffect, ref } from "vue";
    
    let msg1 = ref<string | number>("");
    
    watchEffect((before) => {
      before(()=> {
        console.log("before");
      })
      console.log("----");
      console.log("msg1: ", msg1.value);
    });
    </script>
    
    <style lang="less" scoped>
    </style>
    
    
  • 停止跟踪:watchEffect返回一个函数,调用后停止更新

    <template>
      <div class="about">
        <h1>This is an about page</h1>
    
        <input type="text" v-model.lazy="msg">
        <button @click="stop">停止侦听</button>
      </div>
    </template>
    
    <script setup lang="ts">
    import { watchEffect, ref } from "vue";
    
    let msg = ref<string | number>("");
    
    const stop = watchEffect((before) => {
      before(()=> {
        console.log("before");
      })
      console.log("----");
      console.log("msg: ", msg.value);
    });
    </script>
    
    <style lang="less" scoped>
    </style>
    
    
  • 消除副作用的刷新机制。副作用刷新时机flush一般使用post。

    pre sync post
    更新时机 组件更新前执行 强制效果始终同步触发 组件更新后执行
    <template>
      <div class="about">
        <h1>This is an about page</h1>
    
        <input type="text" v-model.lazy="msg">
        <button @click="stop">停止侦听</button>
      </div>
    </template>
    
    <script setup lang="ts">
    import { watchEffect, ref } from "vue";
    
    let msg = ref<string | number>("");
    
    const stop = watchEffect((before) => {
      before(() => {
        console.log("before");
      })
      console.log("----");
      console.log("msg: ", msg.value);
    }, {
      flush: "post"
    });
    </script>
    
    <style lang="less" scoped>
    </style>
    
    
  • onTrigger可以帮我们调试watchEffect。

    <template>
      <div class="about">
        <h1>This is an about page</h1>
    
        <input type="text" v-model.lazy="msg">
        <button @click="stop">停止侦听</button>
      </div>
    </template>
    
    <script setup lang="ts">
    import { watchEffect, ref } from "vue";
    
    let msg = ref<string | number>("");
    
    const stop = watchEffect((before) => {
      before(() => {
        console.log("before");
      })
      console.log("----");
      console.log("msg: ", msg.value);
    }, {
      flush: "post",
      onTrigger(e) {
        debugger
      }
    });
    </script>
    
    <style lang="less" scoped>
    </style>
    
    

认识组件和生命周期

<template>
  <div class="about">
    <h1>This is an about page</h1>

    <div id="hello">我是Hello组件</div>
    <br />
    <p>count: {{ count }}</p>
    <button @click="count++">更新Count值</button>
  </div>
</template>

<script setup lang="ts">
import { onBeforeMount, onMounted, onBeforeUpdate, onUpdated, onBeforeUnmount, onUnmounted, ref } from "vue";

let count = ref<number>(1);

onBeforeMount(() => {
  let div = document.querySelector("#hello"); // 获取到的是null
  console.log("创建之前 -->> onBeforeMount", div);
});

onMounted(() => {
  let div = document.querySelector("#hello"); // 可以获取到元素值
  console.log("创建完成 -->> onMounted", div);
});

// onBeforeUpdate和onUpdated生命周期
// 例如组件发生了更新/变化,会进入这两个生命周期
onBeforeUpdate(() => {
  console.log("更新之前 -->> onBeforeUpdate");
});

onUpdated(() => {
  console.log("更新完成 -->> onUpdated");
});

// 组件卸载之前和卸载完成
// 可以在组件被调用的位置加上一个v-if属性进行模拟组件卸载的效果
onBeforeUnmount(() => {
  console.log("卸载之前 -->> onBeforeUnmount");
});

onUnmounted(() => {
  console.log("卸载完成 -->> onUnmounted");
});

</script>

<style lang="less" scoped>
</style>

父子组件传参

以views/HomeView.vue组件和components/HelloWorld.vue组件的传值为例说明。

父组件给子组件传参

通过自定义属性进行传值。

  1. src\views\HomeView.vue父组件传递参数值。
    传递字符串类型可以不用加v-bind。
    传递非字符串类型,需要加v-bind。

    <template>
      <div class="home">
     <!-- 父组件传子组件,文本类型的参数直接传参即可,其他类型参数需要在传参前面加上一个v-bind -->
        <HelloWorld msg="Welcome to Your Vue.js + TypeScript App" :dataArr="dataArr" />
      </div>
    </template>
    
    <script setup lang="ts">
    import { reactive } from 'vue';
    import HelloWorld from '../components/HelloWorld.vue'; // @ is an alias to /src
    
    let dataArr = reactive<number[]>([1, 2, 3, 4]);
    
    
    </script>
    
    
  2. src\components\HelloWorld.vue子组件接收传值。
    通过defineProps来接收。defineProps是无须引入,直接使用即可。
    如果使用的是TypeScript,可以使用传递字面量类型的纯类型语法作为参数。
    TypeScript接收方式。

    <template>
      <div class="hello">
        <h1>{{ msg }}</h1>
        <p>{{ dataArr }}</p>
      </div>
    </template>
    
    <script setup lang="ts">
    
    type Props = {
      msg: string,
      dataArr: number[]
    }
    defineProps<Props>();
    
    </script>
    
    <!-- Add "scoped" attribute to limit CSS to this component only -->
    <style scoped lang="less">
    </style>
    
    

    TypeScript特有的默认值方式。
    withDefault是个函数也是无须引入开箱即用,接收参数中,第一个参数props函数,第二个参数是一个对象,设置默认值。

    <template>
      <div class="hello">
        <h1>{{ msg }}</h1>
        <p>{{ dataArr }}</p>
      </div>
    </template>
    
    <script setup lang="ts">
    
    type Props = {
      msg?: string,
      dataArr?: number[]
    }
    withDefaults(defineProps<Props>(), {
      msg: "张三", // 配置默认值
      dataArr: () => [6, 6, 6] // 复杂函数需要使用函数返回值得方式使用
    });
    
    </script>
    
    <!-- Add "scoped" attribute to limit CSS to this component only -->
    <style scoped lang="less">
    </style>
    
    

子组件给父组件传参

通过自定义事件进行传值。

  1. 修改文件src\components\HelloWorld.vue。

    <template>
      <div class="hello">
        <h1>{{ msg }}</h1>
        <p>{{ dataArr }}</p>
    
        <div>
          <button @click="clickTip1"> 子组件派发值到父组件1</button>
          <button @click="clickTip2"> 子组件派发值到父组件2</button>
        </div>
      </div>
    </template>
    
    <script setup lang="ts">
    import { reactive } from "@vue/reactivity";
    
    
    // 子组件接收父组件的传值
    type Props = {
      msg?: string,
      dataArr?: number[]
    }
    withDefaults(defineProps<Props>(), {
      msg: "张三", // 配置默认值
      dataArr: () => [6, 6, 6] // 复杂函数需要使用函数返回值得方式使用
    });
    
    
    // 子组件向父组件传值
    let list = reactive<number[]>([8, 8, 8]);
    let flag = true;
    const emit = defineEmits(['sunToFather1', 'sunToFather2']); // 自定义属性
    const clickTip1 = () => {
      emit('sunToFather1', list, flag);
    }
    const clickTip2 = () => {
      emit('sunToFather2', list);
    }
    
    </script>
    
    <!-- Add "scoped" attribute to limit CSS to this component only -->
    <style scoped lang="less">
    </style>
    
    
  2. src\views\HomeView.vue接收子组件的传参。

    <template>
      <div class="home">
        <!-- 父组件传子组件,自定义属性,文本类型的参数直接传参即可,其他类型参数需要在传参前面加上一个v-bind -->
        <!-- 父组件接收子组件传值,自定义事件 -->
        <HelloWorld msg="Welcome to Your Vue.js + TypeScript App" :dataArr="dataArr" @sunToFather1="getList1"
          @sunToFather2="getList2" />
      </div>
    </template>
    
    <script setup lang="ts">
    import { reactive } from 'vue';
    import HelloWorld from '../components/HelloWorld.vue'; // @ is an alias to /src
    
    let dataArr = reactive<number[]>([1, 2, 3, 4]);
    
    const getList1 = (list: number[], flag: boolean) => {
      console.log(list, flag, "我是子组件传递过来的值");
    }
    
    const getList2 = (list: number[]) => {
      console.log(list, "我是子组件传递过来的值");
    }
    
    </script>
    
    

子组件暴露给父组件的内部属性

通过defineExpress。

  1. 子组件通过defineExpress暴露属性给父组件。

    <script setup lang="ts">
        const list = reactive<number[]>([4, 5, 6]);
        defineExpress({
            list,
        });
    </script>
    
  2. 父组件获取子组件实例通过ref。

    <template>
      <div class="home">
        <HelloWorld ref="helloWorld" />
      </div>
    </template>
    
    <script setup lang="ts">
    import { reactive, ref } from 'vue';
    import HelloWorld from '../components/HelloWorld.vue'; // @ is an alias to /src
    
    const helloWorld = ref(null);
    
    </script>
    
    

举例说明。

  1. 修改文件src\components\HelloWorld.vue。

    <template>
      <div class="hello">
        <h1>{{ msg }}</h1>
        <p>{{ dataArr }}</p>
    
        <div>
          <button @click="clickTip1"> 子组件派发值到父组件1</button>
        </div>
      </div>
    </template>
    
    <script setup lang="ts">
    import { reactive } from "@vue/reactivity";
    import { ref } from "vue";
    
    
    // 子组件接收父组件的传值
    type Props = {
      msg?: string,
      dataArr?: number[]
    }
    withDefaults(defineProps<Props>(), {
      msg: "张三", // 配置默认值
      dataArr: () => [6, 6, 6] // 复杂函数需要使用函数返回值得方式使用
    });
    
    
    // 子组件向父组件传值
    let list = reactive<number[]>([8, 8, 8]);
    let flag = ref(true);
    const emit = defineEmits(['sunToFather1']); // 自定义属性
    const clickTip1 = () => {
      emit('sunToFather1', list, flag);
    }
    
    
    // 子组件暴露给父组件内部属性
    defineExpose({
      list,
    })
    
    </script>
    
    <!-- Add "scoped" attribute to limit CSS to this component only -->
    <style scoped lang="less">
    </style>
    
    
  2. 修改文件。

    <template>
      <div class="home">
        <!-- 父组件传子组件,自定义属性,文本类型的参数直接传参即可,其他类型参数需要在传参前面加上一个v-bind -->
        <!-- 父组件接收子组件传值,自定义事件 -->
        <HelloWorld ref="helloWorld" @sunToFather1="getList1" />
      </div>
    </template>
    
    <script setup lang="ts">
    import { reactive, ref } from 'vue';
    import HelloWorld from '../components/HelloWorld.vue'; // @ is an alias to /src
    
    const helloWorld = ref(null);
    const getList1 = () => {
      console.log(helloWorld.value); // 在Proxy > Target > Target中能拿到list的值
    }
    
    </script>
    
    

v-model实现父子组件的双向绑定

直接使用默认值

  1. 新建一个Vite项目。

  2. 修改文件App.vue。

    <template>
      <h3>我是父组件</h3>
      <button @click="flag = !flag">修改Flag值</button>
      <p>{{ flag }}</p>
      <HelloWorld v-model="flag" />
    </template>
    
    <script setup lang="ts">
    import { ref } from '@vue/reactivity'
    import HelloWorld from './components/HelloWorld.vue'
    
    let flag = ref<boolean>(true);
    let title = ref<string>("我是一条狗");
    </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>
    
    
  3. 修改文件HelloWorld.vue。

    
    <template>
      <div v-if="modelValue" class="hello">
        <h3>我是子组件</h3>
        <p>{{ modelValue }}</p>
      </div>
    
    </template>
    
    <script setup lang="ts">
    import { ref } from 'vue'
    
    type Props = {
      modelValue: boolean,
    }
    defineProps<Props>();
    
    </script>
    
    <style scoped>
    .hello {
      background-color: aqua;
      width: 200px;
      height: 200px;
    }
    </style>
    
    

子组件修改父组件值

修改子组件HelloWorld.vue。


<template>
  <div v-if="modelValue" class="hello">
    <h3>我是子组件</h3>
    <p>{{ modelValue }}</p>
    <button @click="close">Close</button>
  </div>

</template>

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

type Props = {
  modelValue: boolean,
}
defineProps<Props>();


const emit = defineEmits(['update:modelValue']); // 自定义属性
const close = () => {
  emit('update:modelValue', false);
}
</script>

<style scoped>
.hello {
  background-color: aqua;
  width: 200px;
  height: 200px;
}
</style>

自定义名称及实现双向绑定 *

  1. 修改父组件文件src\App.vue。

    <template>
      <h3>我是父组件</h3>
      <button @click="flag = !flag">修改Flag值</button>
      <p>{{ flag }}</p>
      <p>{{ title }}</p>
      <!-- 支持传递绑定多个v-model参数,以及支持自定义名称 -->
      <HelloWorld v-model:title="title" v-model="flag" />
    </template>
    
    <script setup lang="ts">
    import { ref } from '@vue/reactivity'
    import HelloWorld from './components/HelloWorld.vue'
    
    let flag = ref<boolean>(true);
    let title = ref<string>("我是一条狗");
    </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>
    
    
  2. 修改子组件文件src\components\HelloWorld.vue。

    
    <template>
      <div v-if="modelValue" class="hello">
        <h3>我是子组件</h3>
        <p>Flag: {{ modelValue }}</p>
        <p>Title: {{ title }}</p>
        <button @click="close">CloseAndChangeTitle</button>
      </div>
    
    </template>
    
    <script setup lang="ts">
    import { ref } from 'vue'
    
    type Props = {
      modelValue: boolean,
      title: string,
    }
    defineProps<Props>();
    
    const emit = defineEmits(['update:modelValue', 'update:title']); // 自定义属性
    const close = () => {
      emit('update:modelValue', false);
      emit('update:title', '我是一只猫');
    }
    </script>
    
    <style scoped>
    .hello {
      background-color: aqua;
      width: 200px;
      height: 200px;
    }
    </style>
    
    

支持自定义修饰符

  1. 修改父组件文件src\App.vue。

    <template>
      <h3>我是父组件</h3>
      <button @click="flag = !flag">修改Flag值</button>
      <p>{{ flag }}</p>
      <p>{{ title }}</p>
      <!-- 支持传递绑定多个v-model参数,以及支持自定义名称 -->
      <HelloWorld v-model:title.selfAA="title" v-model.bb="flag" />
    </template>
    
    <script setup lang="ts">
    import { ref } from '@vue/reactivity'
    import HelloWorld from './components/HelloWorld.vue'
    
    let flag = ref<boolean>(true);
    let title = ref<string>("我是一条狗");
    </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>
    
    
  2. 修改子组件文件src\components\HelloWorld.vue。

    
    <template>
      <div v-if="modelValue" class="hello">
        <h3>我是子组件</h3>
        <p>Flag: {{ modelValue }}</p>
        <p>Title: {{ title }}</p>
        <button @click="close">CloseAndChangeTitle</button>
      </div>
    
    </template>
    
    <script setup lang="ts">
    import { ref } from 'vue'
    
    type Props = {
      modelValue: boolean,
      title: string,
      modelModifiers?: {
        bb: boolean,
      },
      titleModifiers?: {
        selfAA: boolean
      }
    }
    const PropsData = defineProps<Props>();
    
    const emit = defineEmits(['update:modelValue', 'update:title']); // 自定义属性
    const close = () => {
      console.log("自定义修饰符", PropsData);
    
      if (PropsData.modelModifiers?.bb) {
        emit('update:title', '我是一只猫');
      } else {
        emit('update:title', '我是一只狗');
      }
    
      emit('update:modelValue', false);
    }
    </script>
    
    <style scoped>
    .hello {
      background-color: aqua;
      width: 200px;
      height: 200px;
    }
    </style>
    
    

全局组件、局部组件和递归组件

全局组件

例如组件使用频率非常高(table,Input,button,等)这些组件,几乎每个页面都在使用便可以封装成全局组件。

  1. 新建组件src\components\Cart.vue。

    <template>
        <div class="card">
            <div class="card-header">
                <div>标题</div>
                <div>副标题</div>
            </div>
            <div v-if='content' class="card-content">
                {{ content }}
            </div>
        </div>
    </template>
    
    <script setup lang="ts">
    type Props = {
        content: string
    }
    defineProps<Props>()
    
    </script>
    
    <style scoped lang='less'>
    @border: #ccc;
    
    .card {
        width: 300px;
        border: 1px solid @border;
        border-radius: 3px;
    
        &:hover {
            box-shadow: 0 0 10px @border;
        }
    
        &-content {
            padding: 10px;
        }
    
        &-header {
            display: flex;
            justify-content: space-between;
            padding: 10px;
            border-bottom: 1px solid @border;
        }
    }
    </style>
    
  2. 在src\main.ts中引入组件,跟随在createApp(App)后面,切记不能放到mount后面。

    import { createApp } from "vue";
    import App from "./App.vue";
    import router from "./router";
    import store from "./store";
    import Cart from "./components/Cart.vue";
    
    createApp(App).component("Cart", Cart).use(store).use(router).mount("#app");
    
    
  3. 使用时直接引用该组件即可。

    <template>
      <div class="home">
        <Cart />
      </div>
    </template>
    

局部组件

就是在组件A通过import引入别的组件B,称之为局部组件。

  1. 新建组件src\components\Cart.vue。
    同上。

  2. 在src\views\HomeView.vue中引入Cart.vue组件。

    <template>
      <div class="home">
        <Cart content=""/>
      </div>
    </template>
    
    <script setup lang="ts">
    import { reactive, ref } from 'vue';
    import HelloWorld from '../components/HelloWorld.vue'; // @ is an alias to /src
    import Cart from '../components/Cart.vue'
    
    const helloWorld = ref(null);
    const getList1 = () => {
      console.log(helloWorld.value); // 在Proxy > Target > Target中能拿到list的值
    }
    
    </script>
    
    

递归组件(未完成)

原理跟我们写JS递归是一样的,自己调用自己,通过一个条件来结束递归,否则导致内存泄漏。

  1. 新建文件src\components\Tree.vue。

    <template>
        <div class="tree">
            <h3>我是Tree组件</h3>
        </div>
    </template>
    
    <script setup lang="ts">
    </script>
    
    <style scoped lang='less'>
    </style>
    
  2. 在文件src\views\HomeView.vue中,

    <template>
      <div class="home">
        <!-- 将参数传递给子组件 -->
        <Tree :data="data"></Tree>
      </div>
    </template>
    
    <script setup lang="ts">
    import Tree from '../components/Tree.vue'
    import { reactive } from "vue";
    type TreeList = {
      name: string;
      icon?: string;
      children?: TreeList[] | [];
    };
    const data = reactive<TreeList[]>([
      {
        name: "no.1",
        children: [
          {
            name: "no.1-1",
            children: [
              {
                name: "no.1-1-1",
              },
            ],
          },
        ],
      },
      {
        name: "no.2",
        children: [
          {
            name: "no.2-1",
          },
        ],
      },
      {
        name: "no.3",
      },
    ]);
    
    </script>
    
    
  3. 子组件接收参数。修改文件src\components\Tree.vue。

动态组件

  1. 新建组件src\components\A.vue。

    <template>
        <div class="a">
            <h3>我是组件A</h3>
        </div>
    </template>
    
    <script setup lang="ts">
    
    </script>
    
    <style scoped lang='less'>
    .a {
        background-color: orangered;
        height: 100px;
        width: 200px;
        margin: auto;
    }
    
```
  1. 新建组件src\components\B.vue。

    <template>
     <div class="b">
         <h3>我是组件B</h3>
     </div>
    </template>
    
    <script setup lang="ts">
    
    </script>
    
    <style scoped lang='less'>
    .b {
     background-color: blue;
     height: 100px;
     width: 200px;
     margin: auto;
    }
    </style>
    
  2. 新建组件src\components\C.vue。

    <template>
     <div class="c">
         <h3>我是组件C</h3>
     </div>
    </template>
    
    <script setup lang="ts">
    
    </script>
    
    <style scoped lang='less'>
    .c {
     background-color: green;
     height: 100px;
     width: 200px;
     margin: auto;
    }
    </style>
    
  3. 修改文件src\views\HomeView.vue。

    <template>
      <div class="home">
        <button v-for="item in data" :key="item.name" @click="switchComp(item)">{{ item.name }}</button>
    
        <component :is="cur.compName"></component>
      </div>
    </template>
    
    <script setup lang="ts">
    import { markRaw, reactive } from '@vue/reactivity'
    import A from '../components/A.vue'
    import B from '../components/B.vue'
    import C from '../components/C.vue'
    
    type Tabs = {
      name: string,
      compName: any
    }
    const data = reactive<Tabs[]>([
      {
        name: "我是A组件",
        compName: markRaw(A),
      },
      {
        name: "我是B组件",
        compName: markRaw(B),
      },
      {
        name: "我是C组件",
        compName: markRaw(C),
      },
    ]);
    
    let cur = reactive({
      compName: data[0].compName, // 定义一个默认值
    })
    
    const switchComp = (item: Tabs) => {
      cur.compName = item.compName;
    }
    
    </script>
    
    

插槽

匿名插槽

  1. 子组件放置一个插槽。新建组件src\components\A.vue。

    <template>
        <div class="a">
            <h3>我是组件A</h3>
    
            <slot></slot>
        </div>
    </template>
    
    <script setup lang="ts">
    
    </script>
    
    <style scoped lang='less'>
    .a {
        background-color: orangered;
        height: 100px;
        width: 200px;
        margin: auto;
    }
    </style>
    
  2. 父组件使用插槽。在父组件给这个插槽填充内容。修改文件src\views\HomeView.vue。

    <template>
      <div class="home">
        <h1>我是HomeView页面</h1>
        <A>
          <template v-slot>
            <h>滕王阁序</h>
          </template>
        </A>
      </div>
    </template>
    
    <script setup lang="ts">
    import { reactive } from '@vue/reactivity'
    import A from '../components/A.vue'
    </script>
    
    

具名插槽

具名插槽其实就是给插槽取个名字。一个子组件可以放多个插槽,而且可以放在不同的地方,而父组件填充内容时,可以根据这个名字把内容填充到对应插槽中。

  1. 新建组件src\components\A.vue。

    <template>
        <div class="a">
            <h3>我是组件A</h3>
    
            <slot name="header"></slot>
        </div>
    </template>
    
    <script setup lang="ts">
    
    </script>
    
    <style scoped lang='less'>
    .a {
        background-color: orangered;
        height: 100px;
        width: 200px;
        margin: auto;
    }
    </style>
    
  2. 新建组件src\components\B.vue。

    <template>
     <div class="b">
         <h3>我是组件B</h3>
    
         <slot name="middle"></slot>
     </div>
    </template>
    
    <script setup lang="ts">
    
    </script>
    
    <style scoped lang='less'>
    .b {
     background-color: blue;
     height: 100px;
     width: 200px;
     margin: auto;
    }
    </style>
    
  3. 新建组件src\components\C.vue。

    <template>
     <div class="c">
         <h3>我是组件C</h3>
    
         <slot name="footer"></slot>
     </div>
    </template>
    
    <script setup lang="ts">
    
    </script>
    
    <style scoped lang='less'>
    .c {
     background-color: green;
     height: 100px;
     width: 200px;
     margin: auto;
    }
    </style>
    
  4. 修改文件src\views\HomeView.vue。

    <template>
      <div class="home">
        <h1>我是HomeView页面</h1>
        <A>
          <template #header>
            <h>滕王阁序</h>
          </template>
        </A>
        <B>
          <template #middle>
            <h>正文,哈哈</h>
          </template>
        </B>
        <C>
          <template #footer>
            <h>结尾了哦</h>
          </template>
        </C>
      </div>
    </template>
    
    <script setup lang="ts">
    import { reactive } from '@vue/reactivity'
    import A from '../components/A.vue'
    import B from '../components/B.vue'
    import C from '../components/C.vue'
    </script>
    
    

作用域插槽

  1. 子组件放置一个插槽。新建组件src\components\A.vue。

    <template>
        <div class="a">
            <h3>我是组件A</h3>
            <div v-for="(item, index) in data" :key="item.name">
                <slot :data="item" :index="index"></slot>
            </div>
        </div>
    </template>
    
    <script setup lang="ts">
    import { reactive } from "vue";
    
    type Names = {
        name: string,
        age: number,
    }
    const data = reactive<Names[]>([
        {
            name: "张三",
            age: 16,
        },
        {
            name: "李四",
            age: 15,
        },
        {
            name: "王五",
            age: 18,
        },
    ]);
    </script>
    
    <style scoped lang='less'>
    .a {
        background-color: orangered;
        height: 280px;
        width: 200px;
        margin: auto;
    }
    </style>
    
  2. 父组件使用插槽。在父组件给这个插槽填充内容。修改文件src\views\HomeView.vue。

    <template>
      <div class="home">
        <h1>我是HomeView页面</h1>
    
        <A>
          <template #default="scoop">
            <h>滕王阁序</h>
            <p>{{ scoop.data.name }} -- {{ scoop.index }}</p>
          </template>
        </A>
    
        <!-- 结构解析 -->
        <A>
          <template #default="{ data, index }">
            <h>滕王阁序</h>
            <p>{{ data.name }} -- {{ index }}</p>
          </template>
        </A>
      </div>
    </template>
    
    <script setup lang="ts">
    import { reactive } from '@vue/reactivity'
    import A from '../components/A.vue'
    </script>
    
    

动态插槽

  1. 新建组件src\components\A.vue。

    <template>
        <div class="a">
            <h3>我是组件A</h3>
    
            <slot name="header"></slot>
            <p>header -- default</p>
            <slot></slot> <!-- name="default" -->
            <p>default -- footer</p>
            <slot name="footer"></slot>
        </div>
    </template>
    
    <script setup lang="ts">
    
    </script>
    
    <style scoped lang='less'>
    .a {
        background-color: orangered;
        height: 200px;
        width: 200px;
        margin: auto;
    }
    </style>
    
    
  2. 修改文件src\views\HomeView.vue。

    <template>
      <div class="home">
        <h1>我是HomeView页面</h1>
    
        <A>
          <template #[name]>
            <h>滕王阁序</h>
          </template>
        </A>
      </div>
    </template>
    
    <script setup lang="ts">
    import { reactive, ref } from '@vue/reactivity'
    import A from '../components/A.vue'
    
    const name = ref("footer");
    </script>
    
    

异步组件&代码分包&suspense

异步组件

在大型应用中,我们可能需要将应用分割成小一些的代码块,并且减少主包的体积。

这时候就可以使用异步组件。

Teleport传送组件

Teleport Vue 3.0新特性之一。

Teleport 是一种能够将我们的模板渲染至指定DOM节点,不受父级style、v-show等属性影响,但data、prop数据依旧能够共用的技术;类似于 React 的 Portal。

主要解决的问题 因为Teleport节点挂载在其他指定的DOM节点下,完全不受父级style样式影响

使用方法

  • 通过to属性 插入指定元素位置 to="body" 便可以将Teleport 内容传送到指定位置。

    <template>
      <div class="home">
        <h1>我是HomeView页面</h1>
    
        <Teleport to='body'>
          Hello teleport
        </Teleport>
      </div>
    </template>
    
    <script setup lang="ts">
    </script>
    
    
  • 可以自定义传送位置,支持class id等选择器。可以使用多个。

<template>
  <div class="home">
    <h1>我是HomeView页面</h1>

    <Teleport to='#app'>
      Hello teleport
    </Teleport>
    <Teleport to='#app'>
      Hello teleport
    </Teleport>

  </div>
</template>

<script setup lang="ts">
</script>

keep-alive缓存组件

有时候我们不希望组件被重新渲染影响使用体验;或者处于性能考虑,避免多次重复渲染降低性能。而是希望组件可以缓存下来,维持当前的状态。这时候就需要用到keep-alive组件。

开启keep-alive生命周期的变化

  • 初次进入时:onMounted> onActivated
  • 退出后触发deactivated
  • 再次进入:
  • 只会触发onActivated
  • 事件挂载的方法等,只执行一次的放在 onMounted中;组件每次进去执行的方法放在onActivated中。

操作步骤

  1. 新建文件src\components\A.vue。

    <template>
        <div class="a">
            <h3>我是组件A</h3>
            <table>
                <tr>
                    <td>帐号</td>
                    <td><input type="text" v-model="form.login"></td>
                </tr>
                <tr>
                    <td>密码</td>
                    <td><input type="text" v-model="form.psd"></td>
                </tr>
                <tr>
                    <td>验证码</td>
                    <td><input type="text" v-model="form.code"></td>
                </tr>
            </table>
            <button @click="submit">登录</button>
        </div>
    </template>
    
    <script setup lang="ts">
    import { reactive } from "@vue/reactivity";
    import { onActivated, onDeactivated, onMounted, onUnmounted } from "@vue/runtime-core";
    
    const form = reactive({
        login: "",
        psd: "",
        code: "",
    });
    
    const submit = () => {
        console.log(form);
    };
    
    
    onMounted(() => {
        console.log("Register onMounted.");
    });
    
    onUnmounted(() => {
        console.log("Register onUnmounted.");
    });
    
    onActivated(() => {
        console.log("Register onActivated.");
    });
    
    onDeactivated(() => {
        console.log("Register onDeactivated.");
    });
    
    </script>
    
    <style scoped lang='less'>
    .a {
        background-color: olive;
        height: 200px;
        width: 300px;
        margin: auto;
    }
    </style>
    
    
  2. 新建文件src\components\B.vue。

    <template>
        <div class="a">
            <h3>我是组件B</h3>
            <table>
                <tr>
                    <td>帐号</td>
                    <td><input type="text" v-model="form.login"></td>
                </tr>
                <tr>
                    <td>密码</td>
                    <td><input type="text" v-model="form.psd"></td>
                </tr>
            </table>
            <button @click="register">注册</button>
        </div>
    </template>
    
    <script setup lang="ts">
    import { reactive } from "@vue/reactivity";
    
    const form = reactive({
        login: "",
        psd: "",
    });
    
    const register = () => {
        console.log(form);
    }
    </script>
    
    <style scoped lang='less'>
    .a {
        background-color: green;
        height: 200px;
        width: 300px;
        margin: auto;
    }
    </style>
    
    
  3. 修改文件src\views\HomeView.vue。

    <template>
      <div class="home">
        <h1>我是HomeView页面</h1>
        <button @click="switchComp">切换</button>
    
        <keep-alive>
          <A v-if="flag"></A>
          <B v-else></B>
        </keep-alive>
      </div>
    </template>
    
    <script setup lang="ts">
    import { ref } from 'vue';
    import A from '../components/A.vue'
    import B from '../components/B.vue'
    
    let flag = ref<boolean>(true);
    
    const switchComp = () => {
      flag.value = !flag.value;
    }
    
    </script>
    
    

    参数介绍

    include和exclude prop允许组件有条件地缓存。二者都可以用逗号分隔字符串、正则表达式或一个数组来表示。

    <keep-alive :include="" :exclude="" :max=""></keep-alive>
    
    1. src\components\B.vue新建文件src\components\A.vue。

      ...
      
      <script lang="ts">
      export default {
          name: 'A'
      }
      </script>
      
    2. 修改文件src\components\B.vue。

      ...
      
      <script lang="ts">
      export default {
          name: 'B'
      }
      </script>
      
    3. 修改文件src\views\HomeView.vue。

      <template>
        <div class="home">
          <h1>我是HomeView页面</h1>
          <button @click="switchComp">切换</button>
      
          <keep-alive :include="['A', 'B']">
            <A v-if="flag"></A>
            <B v-else></B>
          </keep-alive>
        </div>
      </template>
      
      <script setup lang="ts">
      import { ref } from 'vue';
      import A from '../components/A.vue'
      import B from '../components/B.vue'
      
      let flag = ref<boolean>(true);
      
      const switchComp = () => {
        flag.value = !flag.value;
      }
      
      </script>
      
      

transition动画组件

基本使用

修改文件src\views\HomeView.vue。

<template>
  <div class="home">
    <h1>我是HomeView页面</h1>
    <button @click="flag = !flag">切换</button>
    <transition name="fade">
      <div class="box" v-if="flag"></div>
    </transition>
  </div>
</template>

<script setup lang="ts">
import { ref } from "vue";

let flag = ref<boolean>(true);
</script>

<style scoped>
.box {
  width: 200px;
  height: 200px;
  background-color: red;
}

.fade-enter-from {
  background: red;
  width: 0px;
  height: 0px;
  transform: rotate(360deg)
}

.fade-enter-active {
  transition: all 2.5s linear;
}

.fade-enter-to {
  background: yellow;
  width: 200px;
  height: 200px;
}

.fade-leave-from {
  width: 200px;
  height: 200px;
  transform: rotate(360deg)
}

.fade-leave-active {
  transition: all 1s linear;
}

.fade-leave-to {
  width: 0px;
  height: 0px;
}
</style>

自定义过渡类名并结合Animate三方库使用

transition props

  • enter-from-class
  • enter-active-class
  • enter-to-class
  • leave-from-class
  • leave-active-class
  • leave-to-class

自定义过渡时间(单位:毫秒),或者分别指定进入和离开的持续时间。

<transition :duration="1000">...</transition>

<transition :duration="{ enter: 1000, leave: 800 }">...</transition>

通过自定义class结合CSS动画库Animate CSS。

  1. 安装依赖库。

    npm i -D animate.css
    
  2. 引入import "animate.css",并使用动画库。
    修改文件src\views\HomeView.vue。

    <template>
      <div class="home">
        <h1>我是HomeView页面</h1>
        <button @click="flag = !flag">切换</button>
        <transition
          leave-active-class="animate__animated animate__fadeInDownBig"
          enter-active-class="animate__animated animate__bounceInRight"
        >
          <div v-if="flag" class="box"></div>
        </transition>
      </div>
    </template>
    
    <script setup lang="ts">
    import { ref } from "vue";
    import "animate.css";
    
    let flag = ref<boolean>(true);
    </script>
    
    <style scoped>
    .box {
      width: 200px;
      height: 200px;
      background-color: red;
    }
    </style>
    

transition的8个生命周期结合GSAP三方库使用

@before-enter="beforeEnter" // 对应enter-from
@enter="enter" // 对应enter-active
@after-enter="afterEnter" // 对应enter-to
@enter-cancelled="enterCancelled" // 显示过度打断
@before-leave="beforeLeave" // 对应leave-from
@leave="leave" // 对应enter-active
@after-leave="afterLeave" // 对应leave-to
@leave-cancelled="leaveCancelled" // 离开过度打断

修改文件src\views\HomeView.vue。

<template>
  <div class="home">
    <h1>我是HomeView页面</h1>
    <button @click="flag = !flag">切换</button>
    <transition
      leave-active-class="animate__animated animate__fadeInDownBig"
      enter-active-class="animate__animated animate__bounceInRight"
      @before-enter="enterFrom"
      @enter="enterActive"
      @after-enter="enterTo"
      @enter-cancelled="enterCancelled"
      @before-leave="leaveFrom"
      @leave="leaveActive"
      @after-leave="leaveTo"
      @leave-cancelled="leaveCancelled"
    >
      <div v-if="flag" class="box"></div>
    </transition>
  </div>
</template>

<script setup lang="ts">
import { ref } from "vue";
import "animate.css";

let flag = ref<boolean>(true);

const enterFrom = (el: Element) => {
  console.log("进入之前", el);
};

const enterActive = (el: Element, done: Function) => {
  console.log("过渡曲线", el);
  setTimeout(() => {
    done();
  }, 3000);
};

const enterTo = (el: Element) => {
  console.log("过渡完成", el);
};

const enterCancelled = (el: Element) => {
  console.log("过渡被打断", el);
};

const leaveFrom = (el: Element) => {
  console.log("过渡完成", el);
};

const leaveActive = (el: Element, done: Function) => {
  console.log("离开之前", el);
  setTimeout(() => {
    done();
  }, 5000);
};

const leaveTo = (el: Element) => {
  console.log("过渡曲线", el);
};

const leaveCancelled = (el: Element) => {
  console.log("离开被打断", el);
};
</script>

<style scoped>
.box {
  width: 200px;
  height: 200px;
  background-color: red;
}
</style>

结合GSAP三方库使用

  1. 安装依赖库。

    npm i -D gsap
    
  2. 引入import gsap "gsap",并使用动画库。
    修改文件src\views\HomeView.vue。

    <template>
      <div class="home">
        <h1>我是HomeView页面</h1>
        <button @click="flag = !flag">切换</button>
        <transition
          leave-active-class="animate__animated animate__fadeInDownBig"
          enter-active-class="animate__animated animate__bounceInRight"
          @before-enter="enterFrom"
          @enter="enterActive"
          @leave="leaveActive"
        >
          <div v-if="flag" class="box"></div>
        </transition>
      </div>
    </template>
    
    <script setup lang="ts">
    import { ref } from "vue";
    import gsap from "gsap";
    
    let flag = ref<boolean>(true);
    
    const enterFrom = (el: Element) => {
      console.log("进入之前", el);
      gsap.set(el, {
        width: 0,
        height: 0,
      });
    };
    
    const enterActive = (el: Element, done: gsap.Callback) => {
      console.log("过渡曲线", el);
      gsap.to(el, {
        width: 200,
        height: 200,
        onComplete: done,
      });
    };
    
    const leaveActive = (el: Element, done: gsap.Callback) => {
      console.log("离开之前", el);
      gsap.to(el, {
        width: 0,
        height: 0,
        onComplete: done,
      });
    };
    </script>
    
    <style scoped>
    .box {
      width: 200px;
      height: 200px;
      background-color: red;
    }
    </style>
    
    

appear属性页面加载完成的开始动画

通过这个属性可以设置初始节点过度,就是页面加载完成就开始动画,对应三个状态。

只是针对加载完成的开始动画。

appear
appear-from-class=""
appear-active-class=""
appear-to-class=""

修改文件src\views\HomeView.vue。

<template>
  <div class="home">
    <h1>我是HomeView页面</h1>
    <button @click="flag = !flag">切换</button>
    <transition
      appear
      appear-from-class="from"
      appear-active-class="active"
      appear-to-class="to"
    >
      <div v-if="flag" class="box"></div>
    </transition>
  </div>
</template>

<script setup lang="ts">
import { ref } from "vue";

let flag = ref<boolean>(true);
</script>

<style scoped>
.box {
  width: 200px;
  height: 200px;
  background-color: red;
}

.from {
  width: 0;
  height: 0;
}

.active {
  transition: all 2s ease;
}

.to {
  width: 200px;
  height: 200px;
}
</style>

也可以结合Animate三方库一起使用。

修改文件src\views\HomeView.vue。

<template>
  <div class="home">
    <h1>我是HomeView页面</h1>
    <button @click="flag = !flag">切换</button>
    <transition
      appear
      appear-active-class="animate__animated animate__fadeInDownBig"
    >
      <div v-if="flag" class="box"></div>
    </transition>
  </div>
</template>

<script setup lang="ts">
import { ref } from "vue";
import "animate.css";

let flag = ref<boolean>(true);
</script>

<style scoped>
.box {
  width: 200px;
  height: 200px;
  background-color: red;
}
</style>

transition-group过渡列表

过渡列表

<template>
  <div class="home">
    <h1>我是HomeView页面</h1>
    <button @click="addItem">ADD</button>
    <button @click="deleteItem">POP</button>
    <div class="wraps">
      <!-- tag="section"为可选配置,配置之后,会多包裹一层div -->
      <transition-group
        enter-active-class="animate__animated animate__fadeInDownBig"
        leave-active-class="animate__animated animate__hinge"
      >
        <div class="item" v-for="(item, index) in list" :key="index">
          {{ item }}
        </div>
      </transition-group>
    </div>
  </div>
</template>

<script setup lang="ts">
import { reactive } from "vue";
import "animate.css";

let list = reactive<number[]>([1, 2, 3, 5, 6, 6, 4]);
const addItem = () => {
  list.push(list.length + 1);
};

const deleteItem = () => {
  list.pop();
};
</script>

<style lang="less" scoped>
.wraps {
  display: flex;
  flex-wrap: wrap;
  word-break: break-all;
  border: 1px solid #ccc;
  .item {
    margin: 10px;
    font-size: 30px;
  }
}
</style>

列表的移动过渡

参见参考内容。

列表的状态过渡

参见参考内容。

依赖注入(Provide/Inject)

通常,当我们需要从父组件向子组件传递数据时,我们使用 props。想象一下这样的结构:有一些深度嵌套的组件,而深层的子组件只需要父组件的部分内容。在这种情况下,如果仍然将 prop 沿着组件链逐级传递下去,可能会很麻烦。

官网的解释很让人疑惑,那我翻译下这几句话:

provide 可以在祖先组件中指定我们想要提供给后代组件的数据或方法,而在任何后代组件中,我们都可以使用 inject 来接收 provide 提供的数据或方法。

  1. 修改文件src\views\HomeView.vue。

    <template lang="">
        <div class="content-home">
            <h3>我是HomeView组件</h3>
            <p>{{ flag }}</p>
            <A></A>
        </div>
    </template>
    
    <script setup lang="ts">
    import { ref } from '@vue/reactivity';
    import { provide } from '@vue/runtime-core';
    import A from '../components/A.vue';
    // 使用ref或者reactive才能将传递的参数变成响应式的参数
    let flag = ref<number>(1);
    provide('flag', flag);
    </script>
    
    <style deep lang="less">
    .content-home {
      background: blue;
      color: #fff;
      width: 200px;
      height: 200px;
    }
    </style>
    
    
  2. 新建文件src\components\A.vue。

    <template lang="">
      <div class="content-a">
        <h3>我是A组件</h3>
        <p>{{ flag }}</p>
        <button @click="change">Change</button>
      </div>
    </template>
    
    <script setup lang="ts">
    import { inject, ref } from "@vue/runtime-core";
    import { Ref } from "vue";
    
    const flag = inject<Ref<number>>('flag', ref(1));
    
    const change = () => {
      flag.value += 1;
    };
    </script>
    
    <style deep lang="less">
    .content-a {
      background: green;
      color: #fff;
      width: 200px;
      height: 100px;
    }
    </style>
    
    

兄弟组件传参(Mitt)

结合Mitt库使用Event Bus实现兄弟组件的传参。

  1. 安装依赖库。

    npm i -D mitt
    
  2. 修改文件src\main.ts。
    全局总线,vue入口文件main.js中挂载全局属性。

    import { createApp } from "vue";
    import App from "./App.vue";
    import router from "./router";
    import store from "./store";
    import mitt from "mitt";
    
    const Mit = mitt();
    
    // TypeScript注册
    // 由于必须要拓展ComponentCustomProperties类型才能获得类型提示
    declare module "vue" {
      export interface ComponentCustomProperties {
        $Bus: typeof Mit;
      }
    }
    
    const app = createApp(App);
    // Vue3挂载全局API
    app.config.globalProperties.$Bus = Mit;
    
    app.use(store).use(router).mount("#app");
    
    
  3. 修改文件src\components\A.vue。使用方法通过emit派发。

    <template>
      <div class="a">
        <h3>我是组件A</h3>
        <button @click="emit1">emit1</button>
        <button @click="emit2">emit2</button>
      </div>
    </template>
    
    <script setup lang="ts">
    import { getCurrentInstance } from 'vue';
    
    const instance = getCurrentInstance();
    const emit1 = () => {
      instance?.proxy?.$Bus.emit("on-num1", 100);
    };
    const emit2 = () => {
      instance?.proxy?.$Bus.emit("on-num2", 500);
    };
    </script>
    
    <style scoped lang='less'>
    .a {
      background-color: orange;
      height: 200px;
      width: 300px;
      margin: auto;
    }
    </style>
    
    <script lang="ts">
    export default {
      name: 'A'
    }
    </script>
    
    
  4. 修改文件src\components\B.vue。注册监听事件。

    <template>
        <div class="b">
            <h3>我是组件B</h3>
    
        </div>
    </template>
    
    <script setup lang="ts">
    import { getCurrentInstance } from 'vue';
    
    const instance = getCurrentInstance();
    instance?.proxy?.$Bus.on('on-num1', (num) => {
        console.log("组件B on-num1", num);
    });
    
    instance?.proxy?.$Bus.on('on-num2', (num) => {
        console.log("组件B on-num2", num);
    });
    
    </script>
    
    <style scoped lang='less'>
    .b {
        background-color: green;
        height: 200px;
        width: 300px;
        margin: auto;
    }
    </style>
    
    <script lang="ts">
    export default {
        name: 'B'
    }
    </script>
    
    

拓展

  • 监听所有事件。

    <script>
        ...
        // 监听所有事件
        instance?.proxy?.$Bus.on('*', (type, num) => {
            console.log("组件B", type, num);
        });
    </script>
    
  • 移除监听事件。

    <script>
        ...
        const Fn = (num: any) => {
        	console.log(num, '===========>B')
        }
        instance?.proxy?.$Bus.on('on-num',Fn); // listen
        instance?.proxy?.$Bus.off('on-num',Fn); // unListen
    </script>
    
  • 清空所有监听。

    <script>
        ...
        instance?.proxy?.$Bus.all.clear();
    </script>
    

TSX

unplugin-auto-import/vite无须import

Vite项目使用。

  1. 安装依赖包。

    npm i -D unplugin-auto-import
    
  2. 在main.ts中导入。

    import AutoImport from 'unplugin-auto-import/vite'
    

自定义指令directive

Vue3指令的钩子函数

  • created:元素初始化的时候
  • beforeMount:指令绑定到元素后调用,只调用一次
  • mounted:元素插入父级dom调用
  • beforeUpdate:元素被更新之前调用
  • update:这个周期方法被移除 改用updated
  • beforeUnmount:在元素被移除前调用
  • unmounted:指令被移除后调用 只调用一次

setup中定义局部指令

这里有一个需要注意的限制:必须以 vNameOfDirective 的形式来命名本地自定义指令,以使得它们可以直接在模板中使用。

  1. 修改文件src\App.vue。

    <template>
      <h3>我是App组件</h3>
      <button @click="show = !show">开关{{ show }} ----- {{ title }}</button>
      <!-- 可以自定义参数aaa和增加修饰符bcd -->
      <HelloWorld v-if="show" v-move:aaa.bcd="{ background: 'green', flag: show }"></HelloWorld>
    </template>
    
    <script setup lang="ts">
    import { Directive, DirectiveBinding, ref } from "vue";
    import HelloWorld from "./components/HelloWorld.vue";
    
    let show = ref<boolean>(true);
    let title = ref<string>("Hello Vue+TS");
    
    type Dir = {
      background: string,
    }
    
    const vMove: Directive = {
      created: () => {
        console.log("生命周期created:初始化");
      },
      beforeMount(...args: Array<any>) {
        // 在元素上做些操作
        console.log("生命周期beforeMount:初始化一次", args);
      },
      // 可以增加一个类型推导(可选)<Dir>
      mounted(el: any, dir: DirectiveBinding<Dir>) {
        el.style.background = dir.value.background;
        console.log("生命周期mounted:初始化");
      },
      beforeUpdate() {
        console.log("生命周期beforeUpdate:更新之前");
      },
      updated() {
        console.log("生命周期updated:更新结束");
      },
      beforeUnmount(...args: Array<any>) {
        console.log("生命周期beforeUnmount:卸载之前", args);
      },
      unmounted(...args: Array<any>) {
        console.log("生命周期unmounted:卸载完成", args);
      },
    };
    </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>
    
    
  2. 修改文件src\components\HelloWorld.vue。

    <template>
      <div class="hello">
        <h3>我是A组件</h3>
      </div>
    
    </template>
    
    <script setup lang="ts">
    
    </script>
    
    <style scoped>
    .hello {
      background-color: aqua;
      width: 200px;
      height: 200px;
    }
    </style>
    
    

函数简写

修改文件src\App.vue。

<template>
  <div>
    <h3>我是App组件</h3>
    <input type="text" v-model="val">
    <HelloWorld v-move="{ background: val }"></HelloWorld>
  </div>
</template>

<script setup lang="ts">
import { Directive, DirectiveBinding, ref } from "vue";
import HelloWorld from "./components/HelloWorld.vue";

let val = ref<string>("");

type Dir = {
  background: string,
}

const vMove: Directive = (el: any, dir: DirectiveBinding<Dir>) => {
  el.style.background = dir.value.background;
};
</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>

自定义指令案例(拖拽窗口)

参见参考内容。

自定义Hooks(VueUse开源库)

自定义全局函数和变量

操作步骤

  1. 修改文件src\main.ts。

    import { createApp } from 'vue'
    import App from './App.vue'
    
    let app = createApp(App);
    
    // 自定义了一个全局函数$filter
    app.config.globalProperties.$filters = {
        format<T>(str: T): string {
            return `真-${str}`;
        }
    }
    
    // 自定义了一个全局变量$env
    app.config.globalProperties.$env = "dev";
    
    // 声明定义的全局函数和变量
    // 声明要扩充@vue/runtime-core包的声明.
    // 这里扩充"ComponentCustomProperties"接口, 因为他是Vue3中实例的属性的类型
    type Filter = {
        format: <T extends any>(str: T) => T
    };
    
    declare module '@vue/runtime-core' {
        export interface ComponentCustomProperties {
            $filters: any,
            $env: string
        }
    }
    
    app.mount('#app')
    
    
  2. 修改文件src\App.vue。

    <template>
      <div>
        <h3>我是App组件</h3>
        <div>{{ $filters.format("张三") }}</div>
        <div>{{ $env }}</div>
      </div>
    </template>
    
    <script setup lang="ts"></script>
    
    <style lang="less"></style>
    
    

自定义Vue3插件

UI库ElementUI【模板】

开箱即用。

以表格举例说明。

  1. 安装依赖库。

    npm i -D element-plus
    
  2. 修改文件src\main.ts。

    import { createApp } from 'vue'
    import App from './App.vue'
    import ElementPlus from 'element-plus'
    import 'element-plus/dist/index.css'
    
    const app = createApp(App);
    
    app.use(ElementPlus);
    app.mount('#app')
    
    
  3. 复制代码使用即可。

UI库AntDesign【模板】

操作步骤

  1. 安装依赖库。

    npm i -D ant-design-vue
    
  2. 修改文件src\main.ts。

    import { createApp } from 'vue'
    import App from './App.vue'
    import DatePicker from 'ant-design-vue';
    import 'ant-design-vue/dist/antd.css';
    
    const app = createApp(App);
    
    app.use(DatePicker);
    app.mount('#app')
    
    
  3. 复制代码使用即可。

    <template>
      <a-timeline>
        <a-timeline-item>Create a services site 2015-09-01</a-timeline-item>
        <a-timeline-item>Solve initial network problems 2015-09-01</a-timeline-item>
        <a-timeline-item>Technical testing 2015-09-01</a-timeline-item>
        <a-timeline-item>Network problems being solved 2015-09-01</a-timeline-item>
      </a-timeline>
    </template>
    
    

详解Scoped和样式穿透

修改文件App.vue。

<template>
  <div style="margin: 200px">
    <el-input class="ipt"></el-input>
  </div>
</template>

<script setup lang="ts"></script>

<style scoped lang="less">
.ipt {
  :deep(input) {
    background-color: red;
  }
}
</style>

CSS完整新特性

插槽选择器

  1. 修改文件src\App.vue。

    <template lang="">
    <div>
      <h3>我是App组件</h3>
      <HelloWorld>
        <p class="helloSlot">我是父组件给子组件的内容</p>
      </HelloWorld>
    </div>
    </template>
    
    <script setup lang="ts">
    import HelloWorld from './components/HelloWorld.vue'
    
    </script>
    
    <style lang="less">
    
    </style>
    
    
  2. 修改文件src\components\HelloWorld.vue。
    在子组件中添加插槽选择器即可控制修改父组件的插槽内容的样式。

    <template>
      <div class="hello">
        <h3>我是HelloWorld组件</h3>
        <slot></slot>
      </div>
    
    </template>
    
    <script setup lang="ts">
    
    </script>
    
    <style scoped>
    .hello {
      background-color: aqua;
      width: 200px;
      height: 200px;
    }
    
    :slotted(.helloSlot) {
      color: red;
    }
    </style>
    
    

全局选择器

之前我们想加入全局样式,通常都是新建一个style标签,不加scoped。
现在有更优雅的解决方案。

<style scoped lang="less">
.hello {
  background-color: aqua;
  width: 200px;
  height: 200px;
}

:global(h3) {
  color: red;
}
</style>

动态CSS

  • 单文件组件的<style>标签可以通过v-bind这一CSS函数将CSS的值关联到动态的组件状态上。
  • 如果是对象参数,v-bind参数中需要加上引号。

修改文件src\App.vue。

<template lang="">
<div>
  <h3>我是App组件</h3>
  <p><input v-model.lazy="fontColor" /></p>
  <p><input v-model.lazy="fontBackground.color" /></p>
  <p class="change">动态变化</p>
</div>
</template>

<script setup lang="ts">
import { reactive, ref } from "@vue/reactivity";

type BG = {
  color: string
}

let fontColor = ref<string>('red');
let fontBackground = reactive<BG>({
  color: "blue",
})

</script>

<style scoped lang="less">
.change {
  color: v-bind('fontColor');
  background-color: v-bind('fontBackground.color');
} 
</style>

CSS Module

  • <style module>标签会编译为CSS Modules,并且将生成的CSS类作为$style对象的键暴露给组件。

    <template lang="">
    <div>
      <h3>我是App组件</h3>
      <p :class="$style.aaa">我是测试样式数据</p>
    </div>
    </template>
    
    <script setup lang="ts">
    
    </script>
    
    <style module>
    .aaa {
      color: red;
      font-size: 20px;
    }
    </style>
    
    
  • 自定义注入名称(多个情况下可以用数组)。
    示例1:

    <template lang="">
    <div>
      <h3>我是App组件</h3>
      <p :class="zs.aaa">我是测试样式数据</p>
    </div>
    </template>
    
    <script setup lang="ts">
    
    </script>
    
    <style module="zs">
    .aaa {
      color: red;
      font-size: 20px;
    }
    </style>
    
    

    示例2:

    <template lang="">
    <div>
      <h3>我是App组件</h3>
      <p :class="[zs.aaa, zs.bbb]">我是测试样式数据</p>
    </div>
    </template>
    
    <script setup lang="ts">
    
    </script>
    
    <style module="zs">
    .aaa {
      color: red;
      font-size: 20px;
    }
    
    .bbb {
      border: 1px solid #ccc;
    }
    </style>
    
    

Vue3集成Tailwind CSS *

操作步骤

  1. 安装依赖包。

    npm install -D tailwindcss@latest postcss@latest autoprefixer@latest
    
  2. 创建配置文件。
    生成 tailwind.config.jspostcss.config.js 文件。

    npx tailwindcss init -p
    
  3. 修改配置文件tailwind.config.js

    • V2.6版本。

      module.exports = {
        purge: ['./index.html', './src/**/*.{vue,js,ts,jsx,tsx}'],
        theme: {
          extend: {},
        },
        plugins: [],
      }
      
    • V3.0版本。

      module.exports = {
        content: ['./index.html', './src/**/*.{vue,js,ts,jsx,tsx}'],
        theme: {
          extend: {},
        },
        plugins: [],
      }
      
  4. 创建文件src\assets\tailwind.css。

    @tailwind base;
    @tailwind components;
    @tailwind utilities;
    
  5. 在文件src\main.ts中引入CSS文件。

    import { createApp } from 'vue'
    import App from './App.vue'
    import './assets/tailwind.css'
    
    createApp(App).mount('#app')
    
    
  6. 在XX.vue组件中直接使用该样式即可。

  7. 运行项目。

    npm run dev
    

Pinia(适用Vue2.x和Vue3.x)

安装Pinia(Vue3.x为例)

  1. 创建Vue3.x项目工程。

  2. 安装依赖包。

    npm i -D pinia
    
  3. 修改文件src\main.ts。引入注册。

    import { createApp } from 'vue'
    import App from './App.vue'
    import { createPinia } from 'pinia'
    const pinia = createPinia();
    
    const app = createApp(App);
    
    app.use(pinia);
    
    app.mount('#app')
    
    

初始化仓库Store

  1. 新建文件夹src\store。

  2. 新建文件src\store\store-name.ts。

    export const enum Names {
        TEST = 'TEST',
    }
    
  3. 修建文件src\store\index.ts。

    import { defineStore } from "pinia";
    import { Names } from "./store-name";
    
    // 当前仓库名称为Names.TEST
    export const userStore = defineStore(Names.TEST, {
        state: () => {
            return {
                count: 10,
                name: '张三',
            }
        },
    
        // computed计算属性
        getters: {
        },
    
        // methods,可以使用同步/异步方法,提交state
        actions: {
        },
    });
    
    
  4. 修改文件src\App.vue。

    <template>
      <div>
        pinia -- {{ TEST.count }} -- {{ TEST.name }}
      </div>
    </template>
    
    <script setup lang="ts">
    // 可以根据需要导入需要的数据仓库
    import { userStore } from './store'
    
    const TEST = userStore();
    </script>
    
    <style scoped lang="">
    </style>
    
    
  5. State的修改方式。修改文件src\App.vue。

    • 方式一:获取后直接修改。

      <template>
        <div>
          pinia -- {{ TEST.count }} -- {{ TEST.name }}
        </div>
        <button @click="countAddOne">Count+1</button>
      </template>
      
      <script setup lang="ts">
      // 可以根据需要导入需要的数据仓库
      import { userStore } from './store'
      
      const TEST = userStore();
      
      function countAddOne() {
        TEST.count++;
      }
      
      </script>
      
      <style scoped lang="">
      </style>
      
      
    • 方式二:数据结构后结合响应式的声明进行修改。

      <template>
        <div>
          pinia -- {{ TEST.count }} -- {{ TEST.name }}
        </div>
        <button @click="countAddOne">Count+1</button>
      </template>
      
      <script setup lang="ts">
      // 可以根据需要导入需要的数据仓库
      import { storeToRefs } from 'pinia';
      import { userStore } from './store'
      
      const TEST = userStore();
      let { count } = storeToRefs(TEST);
      
      function countAddOne() {
        // TEST.count++;
        count.value++;
      }
      
      </script>
      
      <style scoped lang="">
      </style>
      
      
    • 方式三:使用$patch进行修改。
      举例1:

      <template>
        <div>
          pinia -- {{ TEST.count }} -- {{ TEST.name }}
        </div>
        <button @click="countAddOne">Count+1</button>
        <button @click="patchClick">Count+5</button>
      </template>
      
      <script setup lang="ts">
      // 可以根据需要导入需要的数据仓库
      import { storeToRefs } from 'pinia';
      import { userStore } from './store'
      
      const TEST = userStore();
      let { count } = storeToRefs(TEST);
      
      function countAddOne() {
        // TEST.count++;
        count.value++;
      }
      
      // $patch的方式修改
      function patchClick() {
        TEST.$patch({
          count: TEST.count + 5,
        })
      }
      
      </script>
      
      <style scoped lang="">
      </style>
      
      

      举例2:

      修改文件src\store\index.ts。

      import { defineStore } from "pinia";
      import { Names } from "./store-name";
      
      // 当前仓库名称为Names.TEST
      export const userStore = defineStore(Names.TEST, {
          state: () => {
              return {
                  count: 10,
                  name: '张三',
                  list: [{
                      name: "iPhone",
                      price: 5888,
                  },
                  {
                      name: "Mate40 Pro",
                      price: 6888,
                  },]
              }
          },
      
          // computed计算属性
          getters: {
          },
      
          // methods,可以使用同步/异步方法,提交state
          actions: {
          },
      });
      
      

      修改文件src\App.vue。

      <template>
        <div>
          pinia -- {{ TEST.count }} -- {{ TEST.name }}
        </div>
        <button @click="countAddOne">Count+1</button>
        <button @click="patchClick">Count+5</button>
      
        <br>
        <h3>商品列表:</h3>
        <ul>
          <li v-for="(item, index) in TEST.list" :key="index">{{ item }}</li>
        </ul>
        <button @click="listAddOne">ListAddOne</button>
      </template>
      
      <script setup lang="ts">
      // 可以根据需要导入需要的数据仓库
      import { storeToRefs } from 'pinia';
      import { userStore } from './store'
      
      const TEST = userStore();
      let { count } = storeToRefs(TEST);
      
      function countAddOne() {
        // TEST.count++;
        count.value++;
      }
      
      // $patch的方式修改
      function patchClick() {
        TEST.$patch({
          count: TEST.count + 5,
        })
      }
      
      // $patch的方式修改
      function listAddOne() {
        TEST.list.push({
          name: "Oppo",
          price: 2000
        })
        TEST.$patch({
          list: TEST.list
        })
      }
      </script>
      
      <style scoped lang="">
      </style>
      
      
    • 方式四(推荐):使用$patch进行修改。

      <template>
        <div>
          pinia -- {{ TEST.count }} -- {{ TEST.name }}
        </div>
        <button @click="countAddOne">Count+1</button>
        <button @click="patchClick">Count+5</button>
      
        <br>
        <h3>商品列表:</h3>
        <ul>
          <li v-for="(item, index) in TEST.list" :key="index">{{ item }}</li>
        </ul>
        <button @click="listAddOne">ListAddOne</button>
        <button @click="recommendPatch">RecommendPatch</button>
      </template>
      
      <script setup lang="ts">
      // 可以根据需要导入需要的数据仓库
      import { storeToRefs } from 'pinia';
      import { userStore } from './store'
      
      const TEST = userStore();
      let { count } = storeToRefs(TEST);
      
      function countAddOne() {
        // TEST.count++;
        count.value++;
      }
      
      // $patch的方式修改
      function patchClick() {
        TEST.$patch({
          count: TEST.count + 5,
        })
      }
      
      // $patch的方式修改
      function listAddOne() {
        TEST.list.push({
          name: "Oppo",
          price: 2000
        })
        TEST.$patch({
          list: TEST.list
        })
      }
      
      // $patch推荐的修改方式
      function recommendPatch() {
        TEST.$patch((state) => {
          state.list.push({
            name: "Xiaomi",
            price: 2001,
          });
          state.count += 10;
        }
      }
      </script>
      
      <style scoped lang="">
      </style>
      重置State。
      

替换State值

修改文件src\App.vue。

<template>
  <div>
    pinia -- {{ TEST.count }} -- {{ TEST.name }}
  </div>
  <button @click="countAddOne">Count+1</button>
  <button @click="patchClick">Count+5</button>

  <br>
  <h3>商品列表:</h3>
  <ul>
    <li v-for="(item, index) in TEST.list" :key="index">{{ item }}</li>
  </ul>
  <button @click="listAddOne">ListAddOne</button>
  <button @click="recommendPatch">RecommendPatch</button>
  <button @click="toggleState">重置State状态</button>
</template>

<script setup lang="ts">
// 可以根据需要导入需要的数据仓库
import { storeToRefs } from 'pinia';
import { userStore } from './store'

const TEST = userStore();
let { count } = storeToRefs(TEST);

function countAddOne() {
  // TEST.count++;
  count.value++;
}

// $patch的方式修改
function patchClick() {
  TEST.$patch({
    count: TEST.count + 5,
  })
}

// $patch的方式修改
function listAddOne() {
  TEST.list.push({
    name: "Oppo",
    price: 2000
  })
  TEST.$patch({
    list: TEST.list
  })
}

// $patch推荐的修改方式
function recommendPatch() {
  TEST.$patch((state) => {
    state.list.push({
      name: "Xiaomi",
      price: 2001,
    });
    state.count += 10;
  })
}

// 替换State值
function toggleState() {
  TEST.$state = {
    count: 100,
    name: "李四",
    list: [
      {
        name: "Oppo",
        price: 2000
      },
      {
        name: "Xiaomi",
        price: 2001,
      }
    ],
  };
}
</script>

<style scoped lang="">
</style>

重置State状态

修改文件src\App.vue。

<template>
  <div>
    pinia -- {{ TEST.count }} -- {{ TEST.name }}
  </div>
  <button @click="countAddOne">Count+1</button>
  <button @click="patchClick">Count+5</button>

  <br>
  <h3>商品列表:</h3>
  <ul>
    <li v-for="(item, index) in TEST.list" :key="index">{{ item }}</li>
  </ul>
  <button @click="listAddOne">ListAddOne</button>
  <button @click="recommendPatch">RecommendPatch</button>
  <button @click="toggleState">重置State状态</button>
  <button @click="resetState">重置State状态</button>
</template>

<script setup lang="ts">
// 可以根据需要导入需要的数据仓库
import { storeToRefs } from 'pinia';
import { userStore } from './store'

const TEST = userStore();
let { count } = storeToRefs(TEST);

function countAddOne() {
  // TEST.count++;
  count.value++;
}

// $patch的方式修改
function patchClick() {
  TEST.$patch({
    count: TEST.count + 5,
  })
}

// $patch的方式修改
function listAddOne() {
  TEST.list.push({
    name: "Oppo",
    price: 2000
  })
  TEST.$patch({
    list: TEST.list
  })
}

// $patch推荐的修改方式
function recommendPatch() {
  TEST.$patch((state) => {
    state.list.push({
      name: "Xiaomi",
      price: 2001,
    });
    state.count += 10;
  })
}

// 替换State值
function toggleState() {
  TEST.$state = {
    count: 100,
    name: "李四",
    list: [
      {
        name: "Oppo",
        price: 2000
      },
      {
        name: "Xiaomi",
        price: 2001,
      }
    ],
  };
}

// 重置State状态
function resetState() {
  TEST.$reset();
}
</script>

<style scoped lang="">
</style>

监听整个仓库变化

修改文件src\App.vue。

<template>
  <div>
    pinia -- {{ TEST.count }} -- {{ TEST.name }}
  </div>
  <button @click="countAddOne">Count+1</button>
  <button @click="patchClick">Count+5</button>

  <br>
  <h3>商品列表:</h3>
  <ul>
    <li v-for="(item, index) in TEST.list" :key="index">{{ item }}</li>
  </ul>
  <button @click="listAddOne">ListAddOne</button>
  <button @click="recommendPatch">RecommendPatch</button>
  <button @click="toggleState">替换State值</button>
  <button @click="resetState">重置State状态</button>
</template>

<script setup lang="ts">
// 可以根据需要导入需要的数据仓库
import { storeToRefs } from 'pinia';
import { userStore } from './store'

const TEST = userStore();
let { count } = storeToRefs(TEST);

function countAddOne() {
  // TEST.count++;
  count.value++;
}

// $patch的方式修改
function patchClick() {
  TEST.$patch({
    count: TEST.count + 5,
  })
}

// $patch的方式修改
function listAddOne() {
  TEST.list.push({
    name: "Oppo",
    price: 2000
  })
  TEST.$patch({
    list: TEST.list
  })
}

// $patch推荐的修改方式
function recommendPatch() {
  TEST.$patch((state) => {
    state.list.push({
      name: "Xiaomi",
      price: 2001,
    });
    state.count += 10;
  })
}

// 替换State值
function toggleState() {
  TEST.$state = {
    count: 100,
    name: "李四",
    list: [
      {
        name: "Oppo",
        price: 2000
      },
      {
        name: "Xiaomi",
        price: 2001,
      }
    ],
  };
}

// 重置State状态
function resetState() {
  TEST.$reset();
}

// 监听整个仓库变化
TEST.$subscribe((mutations, state) => {
  console.log("mutations", mutations);
  console.log("state", state);
})
</script>

<style scoped lang="">
</style>

计算属性

例如需要计算总的价格。

修改文件src\App.vue。

  1. 修改文件src\store\index.ts。

    import { defineStore } from "pinia";
    import { Names } from "./store-name";
    
    // 当前仓库名称为Names.TEST
    export const userStore = defineStore(Names.TEST, {
        state: () => {
            return {
                count: 10,
                name: '张三',
                list: [{
                    name: "iPhone",
                    price: 5888,
                    num: 1,
                },
                {
                    name: "Mate40 Pro",
                    price: 6888,
                    num: 1,
                },]
            }
        },
    
        // computed计算属性
        getters: {
            sumPrice: (state) => {
                return state.list.reduce((pre, item) => {
                    return pre + (item.price * item.num);
                }, 0);
            }
        },
    
        // methods,可以使用同步/异步方法,提交state
        actions: {
        },
    });
    
    
  2. 修改文件src\App.vue。

    <template>
      <div>
        pinia -- {{ TEST.count }} -- {{ TEST.name }}
      </div>
      <button @click="countAddOne">Count+1</button>
      <button @click="patchClick">Count+5</button>
    
      <br>
      <h3>商品列表:</h3>
      <ul>
        <li v-for="(item, index) in TEST.list" :key="index">{{ item }}</li>
      </ul>
      <button @click="listAddOne">ListAddOne</button>
      <button @click="recommendPatch">RecommendPatch</button>
      <button @click="toggleState">替换State值</button>
      <button @click="resetState">重置State状态</button>
    
      <br>
      <p>方式一:</p>
      <p>总的价格为:{{ TEST.sumPrice }}</p>
      <p>方式二:结构后直接使用</p>
      <p>总的价格为:{{ sumPrice }}</p>
    </template>
    
    <script setup lang="ts">
    // 可以根据需要导入需要的数据仓库
    import { storeToRefs } from 'pinia';
    import { userStore } from './store'
    
    const TEST = userStore();
    // 计算属性sumPrice也可以直接结构后使用
    let { count, sumPrice } = storeToRefs(TEST);
    
    function countAddOne() {
      // TEST.count++;
      count.value++;
    }
    
    // $patch的方式修改
    function patchClick() {
      TEST.$patch({
        count: TEST.count + 5,
      })
    }
    
    // $patch的方式修改
    function listAddOne() {
      TEST.list.push({
        name: "Oppo",
        price: 2000,
        num: 1,
      })
      TEST.$patch({
        list: TEST.list
      })
    }
    
    // $patch推荐的修改方式
    function recommendPatch() {
      TEST.$patch((state) => {
        state.list.push({
          name: "Xiaomi",
          price: 2001,
          num: 1,
        });
        state.count += 10;
      })
    }
    
    // 替换State值
    function toggleState() {
      TEST.$state = {
        count: 100,
        name: "李四",
        list: [
          {
            name: "Oppo",
            price: 2000,
            num: 1,
          },
          {
            name: "Xiaomi",
            price: 2001,
            num: 1,
          }
        ],
      };
    }
    
    // 重置State状态
    function resetState() {
      TEST.$reset();
    }
    
    // 监听整个仓库变化
    TEST.$subscribe((mutations, state) => {
      console.log("mutations", mutations);
      console.log("state", state);
    })
    </script>
    
    <style scoped lang="">
    </style>
    
    

Actions

同步和异步皆可以。

以XX网络请求为例说明。

  1. 修改文件src\store\index.ts。

    import { defineStore } from "pinia";
    import { Names } from "./store-name";
    import axios from 'axios';
    
    // 当前仓库名称为Names.TEST
    export const userStore = defineStore(Names.TEST, {
        state: () => {
            return {
                count: 10,
                name: '张三',
                list: [{
                    name: "iPhone",
                    price: 5888,
                    num: 1,
                },
                {
                    name: "Mate40 Pro",
                    price: 6888,
                    num: 1,
                },]
            }
        },
    
        // computed计算属性
        getters: {
            sumPrice: (state) => {
                return state.list.reduce((pre, item) => {
                    return pre + (item.price * item.num);
                }, 0);
            }
        },
    
        // methods,可以使用同步/异步方法,提交state
        actions: {
            countAddN(n: number) {
                this.count += n;
            },
            async getExams() {
                let res = await axios.post(
                    "http://www.liulongbin.top:3006/api/post",
                    { name: "zs", gender: "女" }
                );
                console.log(res);
            },
        },
    });
    
    
  2. 修改文件src\App.vue。

    <template>
      <div>
        pinia -- {{ TEST.count }} -- {{ TEST.name }}
      </div>
      <button @click="countAddOne">Count+1</button>
      <button @click="patchClick">Count+5</button>
    
      <br>
      <h3>商品列表:</h3>
      <ul>
        <li v-for="(item, index) in TEST.list" :key="index">{{ item }}</li>
      </ul>
      <button @click="listAddOne">ListAddOne</button>
      <button @click="recommendPatch">RecommendPatch</button>
      <button @click="toggleState">替换State值</button>
      <button @click="resetState">重置State状态</button>
    
      <br>
      <p>方式一:</p>
      <p>总的价格为:{{ TEST.sumPrice }}</p>
      <p>方式二:结构后直接使用</p>
      <p>总的价格为:{{ sumPrice }}</p>
    
      <hr>
      <button @click="TEST.countAddN(20)">Actions countAddN</button>
      <button @click="TEST.getExams">获取请求</button>
    </template>
    
    <script setup lang="ts">
    // 可以根据需要导入需要的数据仓库
    import { storeToRefs } from 'pinia';
    import { userStore } from './store'
    
    const TEST = userStore();
    // 计算属性sumPrice也可以直接结构后使用
    let { count, sumPrice } = storeToRefs(TEST);
    
    function countAddOne() {
      // TEST.count++;
      count.value++;
    }
    
    // $patch的方式修改
    function patchClick() {
      TEST.$patch({
        count: TEST.count + 5,
      })
    }
    
    // $patch的方式修改
    function listAddOne() {
      TEST.list.push({
        name: "Oppo",
        price: 2000,
        num: 1,
      })
      TEST.$patch({
        list: TEST.list
      })
    }
    
    // $patch推荐的修改方式
    function recommendPatch() {
      TEST.$patch((state) => {
        state.list.push({
          name: "Xiaomi",
          price: 2001,
          num: 1,
        });
        state.count += 10;
      })
    }
    
    // 替换State值
    function toggleState() {
      TEST.$state = {
        count: 100,
        name: "李四",
        list: [
          {
            name: "Oppo",
            price: 2000,
            num: 1,
          },
          {
            name: "Xiaomi",
            price: 2001,
            num: 1,
          }
        ],
      };
    }
    
    // 重置State状态
    function resetState() {
      TEST.$reset();
    }
    
    // 监听整个仓库变化
    TEST.$subscribe((mutations, state) => {
      console.log("mutations", mutations);
      console.log("state", state);
    })
    </script>
    
    <style scoped lang="">
    </style>
    
    

Pinia插件

Pinia和Vuex都有一个通病,页面刷新状态会丢失。

Router

参考:CSDN 小满的博客 Router

路由模式

  • vue2 mode hash vue3 createWebHashHistory
    hash是URL中hash (#) 及后面的那部分,常用作锚点在页面内进行导航,改变URL中的hash部分,不会引起页面刷新。

    • 基于H5的location.hash = 'xx'实现原理。

    • 通过以下方式监听hash值的变化。

      window.addEventListener('hashchange', (e) => {
          console.log('hashchange', e);
      })
      
  • vue2 mode abstract vue3 createMemoryHistory
    history提供pushState和replaceState两个方法,这两个方法改变URL的path部分,不会引起页面刷新。

    • 基于H5的history实现原理。

    • 通过以下方法监听值得变化。

      window.addEventListener('popstate', (e) => {
          console.log('popstate', e);
      })
      
    • 通过以下方式实现跳转。

      history.pushState({ state: 1 }, '', '/ccc')
      

Vite安装和配置Router【模板】

  1. 使用Vite搭建Vue3开发环境,参见[Vite创建Vue3.x项目【模板】](# Vite创建Vue3.x项目【模板】)。

  2. 安装和配置less。参见[Vite安装和配置less【模板】](# Vite安装和配置less【模板】)。

  3. 安装和配置Router。参见[Vite安装和配置Router【模板】](# Vite安装和配置Router【模板】)。

  4. 修改文件src\router\index.ts。

    //引入路由对象
    import { createRouter, createWebHistory, createWebHashHistory, createMemoryHistory, RouteRecordRaw } from 'vue-router'
    
    // vue2 mode history vue3 createWebHistory
    // vue2 mode hash vue3 createWebHashHistory
    // vue2 mode abstract vue3 createMemoryHistory
    
    // 路由数组的类型 RouteRecordRaw
    // 定义一些路由
    // 每个路由都需要映射到一个组件。
    const routes: Array<RouteRecordRaw> = [{
        path: '/',
        component: () => import('../views/Home.vue')
    }, {
        path: '/about',
        component: () => import('../views/About.vue')
    }]
    
    const router = createRouter({
        history: createWebHistory(),
        routes
    })
    
    // 导出router
    export default router;
    
    
  5. 新建文件夹src\views和修建文件src\views\Home.vue。

    <template>
        <div class="content-home">
            <h3>我是Home组件</h3>
        </div>
    </template>
    
    <script setup lang="ts">
    
    </script>
    
    <style lang="less" scoped>
    .content-home {
        width: 200px;
        height: 200px;
        background-color: aqua;
    }
    </style>
    
    
  6. 新建文件src\views\About.vue。

    <template>
        <div class="content-about">
            <h3>我是About组件</h3>
        </div>
    </template>
    
    <script setup lang="ts">
    
    </script>
    
    <style lang="less" scoped>
    .content-about {
        width: 200px;
        height: 200px;
        background-color: green;
    }
    </style>
    
    
  7. 新建文件夹src\router和新建文件src\router\index.ts。

    //引入路由对象
    import { createRouter, createWebHistory, createWebHashHistory, createMemoryHistory, RouteRecordRaw } from 'vue-router'
    
    // vue2 mode history vue3 createWebHistory
    // vue2 mode hash vue3 createWebHashHistory
    // vue2 mode abstract vue3 createMemoryHistory
    
    // 路由数组的类型 RouteRecordRaw
    // 定义一些路由
    // 每个路由都需要映射到一个组件。
    const routes: Array<RouteRecordRaw> = [{
        path: '/',
        component: () => import('../views/Home.vue')
    }, {
        path: '/about',
        component: () => import('../views/About.vue')
    }]
    
    const router = createRouter({
        history: createWebHistory(),
        routes
    })
    
    // 导出router
    export default router;
    
    
  8. 修改文件src\App.vue。声明路由链接和占位符。

    <script setup lang="ts">
    // This starter template is using Vue 3 <script setup> SFCs
    // Check out https://vuejs.org/api/sfc-script-setup.html#script-setup
    </script>
    
    <template>
      <div>
        <!-- <router-link to="/home">首页</router-link> -->
        <router-link to="/">首页</router-link> |
        <router-link to="/about">关于</router-link>
        <hr />
    
        <!-- 只要在项目中安装和配置了vue-router,就可以使用router-view这个组件了 -->
        <!-- 它的作用很单纯,就是占位符 -->
        <router-view></router-view>
      </div>
    </template>
    
    <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>
    
    

命名路由

  1. 修改文件src\router\index.ts。增加name属性。

    //引入路由对象
    import { createRouter, createWebHistory, createWebHashHistory, createMemoryHistory, RouteRecordRaw } from 'vue-router'
    
    // vue2 mode history vue3 createWebHistory
    // vue2 mode hash vue3 createWebHashHistory
    // vue2 mode abstract vue3 createMemoryHistory
    
    // 路由数组的类型 RouteRecordRaw
    // 定义一些路由
    // 每个路由都需要映射到一个组件。
    const routes: Array<RouteRecordRaw> = [{
        path: '/',
        name: 'Home',
        component: () => import('../views/Home.vue')
    }, {
        path: '/about',
        name: 'About',
        component: () => import('../views/About.vue')
    }]
    
    const router = createRouter({
        history: createWebHistory(),
        routes
    })
    
    // 导出router
    export default router;
    
    
  2. 修改文件src\App.vue。

    <script setup lang="ts">
    // This starter template is using Vue 3 <script setup> SFCs
    // Check out https://vuejs.org/api/sfc-script-setup.html#script-setup
    </script>
    
    <template>
      <div>
        <!-- <router-link to="/home">首页</router-link> -->
        <router-link :to="{ name: 'Home' }">首页</router-link> |
        <router-link :to="{ name: 'About' }">关于</router-link>
        <hr />
    
        <!-- 只要在项目中安装和配置了vue-router,就可以使用router-view这个组件了 -->
        <!-- 它的作用很单纯,就是占位符 -->
        <router-view></router-view>
      </div>
    </template>
    
    <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>
    
    

    使用a标签也可以实现跳转,会存在页面刷新的现象,不推荐使用。

    <script setup lang="ts">
    // This starter template is using Vue 3 <script setup> SFCs
    // Check out https://vuejs.org/api/sfc-script-setup.html#script-setup
    </script>
    
    <template>
      <div>
        <!-- <router-link to="/home">首页</router-link> -->
        <!-- <router-link :to="{ name: 'Home' }">首页</router-link> | -->
        <!-- <router-link :to="{ name: 'About' }">关于</router-link> -->
        <a href="/">首页</a> |
        <a href="/about">关于</a>
        <hr />
    
        <!-- 只要在项目中安装和配置了vue-router,就可以使用router-view这个组件了 -->
        <!-- 它的作用很单纯,就是占位符 -->
        <router-view></router-view>
      </div>
    </template>
    
    <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>
    
    

编程式导航

修改文件src\App.vue。

  • 字符串模式

    <script setup lang="ts">
    // This starter template is using Vue 3 <script setup> SFCs
    // Check out https://vuejs.org/api/sfc-script-setup.html#script-setup
    import { useRouter } from 'vue-router'
    const router = useRouter();
    
    const gotoPage = (url: string) => {
      router.push(url);
    }
    </script>
    
    <template>
      <div>
        <button @click="gotoPage('/')">首页</button> |
        <button @click="gotoPage('/about')">关于</button>
      </div>
    
      <hr>
      <router-view></router-view>
    </template>
    
    <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>
    
    
  • 对象模式

    <script setup lang="ts">
    // This starter template is using Vue 3 <script setup> SFCs
    // Check out https://vuejs.org/api/sfc-script-setup.html#script-setup
    import { useRouter } from 'vue-router'
    const router = useRouter();
    
    const gotoPage = (url: string) => {
      router.push({
        path: url,
      });
    }
    </script>
    
    <template>
      <div>
        <button @click="gotoPage('/')">首页</button> |
        <button @click="gotoPage('/about')">关于</button>
      </div>
    
      <hr>
      <router-view></router-view>
    </template>
    
    <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>
    
    
  • 命名式路由模式

    <script setup lang="ts">
    // This starter template is using Vue 3 <script setup> SFCs
    // Check out https://vuejs.org/api/sfc-script-setup.html#script-setup
    import { useRouter } from 'vue-router'
    const router = useRouter();
    
    const gotoPage = (urlName: string) => {
      router.push({
        name: urlName,
      });
    }
    </script>
    
    <template>
      <div>
        <button @click="gotoPage('Home')">首页</button> |
        <button @click="gotoPage('About')">关于</button>
      </div>
    
      <hr>
      <router-view></router-view>
    </template>
    
    <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>
    
    

历史记录

屏蔽历史记录

修改文件src\App.vue。

  • 场景1

    <script setup lang="ts">
    // This starter template is using Vue 3 <script setup> SFCs
    // Check out https://vuejs.org/api/sfc-script-setup.html#script-setup
    </script>
    
    <template>
      <div>
        <!-- <router-link to="/home">首页</router-link> -->
        <router-link replace :to="{ name: 'Home' }">首页</router-link> |
        <router-link replace :to="{ name: 'About' }">关于</router-link>
        <hr />
    
        <!-- 只要在项目中安装和配置了vue-router,就可以使用router-view这个组件了 -->
        <!-- 它的作用很单纯,就是占位符 -->
        <router-view></router-view>
      </div>
    </template>
    
    <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>
    
    
  • 场景2

    <script setup lang="ts">
    // This starter template is using Vue 3 <script setup> SFCs
    // Check out https://vuejs.org/api/sfc-script-setup.html#script-setup
    import { useRouter } from 'vue-router'
    const router = useRouter();
    
    const gotoPage = (urlName: string) => {
      router.replace({
        name: urlName,
      });
    }
    </script>
    
    <template>
      <div>
        <button @click="gotoPage('Home')">首页</button> |
        <button @click="gotoPage('About')">关于</button>
      </div>
    
      <hr>
      <router-view></router-view>
    </template>
    
    <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>
    
    

主动触发前进和后退

<script setup lang="ts">
// This starter template is using Vue 3 <script setup> SFCs
// Check out https://vuejs.org/api/sfc-script-setup.html#script-setup
import { useRouter } from 'vue-router'
const router = useRouter();

const gotoPage = (urlName: string) => {
  router.push({
    name: urlName,
  });
}

const next = () => {
  router.go(1); // router.go(2);
}

const back = () => {
  // 方式一
  router.back();
  // 方式二(不推荐)
  // router.go(-1);
}
</script>

<template>
  <div>
    <!-- <router-link to="/home">首页</router-link> -->
    <router-link :to="{ name: 'Home' }">首页</router-link> |
    <router-link :to="{ name: 'About' }">关于</router-link> |

    <button @click="gotoPage('Home')">首页</button> |
    <button @click="gotoPage('About')">关于</button> |

    <button @click="next">Next</button> |
    <button @click="back">Back</button>
    <hr />

    <!-- 只要在项目中安装和配置了vue-router,就可以使用router-view这个组件了 -->
    <!-- 它的作用很单纯,就是占位符 -->
    <router-view></router-view>
  </div>
</template>

<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>

路由传参

query方式传参

说明:query方式传参,会直接展示在URL的地址上。

  1. 修改文件src\router\index.ts。

    //引入路由对象
    import { createRouter, createWebHistory, createWebHashHistory, createMemoryHistory, RouteRecordRaw } from 'vue-router'
    
    // vue2 mode history vue3 createWebHistory
    // vue2 mode hash vue3 createWebHashHistory
    // vue2 mode abstract vue3 createMemoryHistory
    
    // 路由数组的类型 RouteRecordRaw
    // 定义一些路由
    // 每个路由都需要映射到一个组件。
    const routes: Array<RouteRecordRaw> = [{
        path: '/',
        name: 'Home',
        component: () => import('../views/Home.vue')
    }, {
        path: '/about',
        name: 'About',
        component: () => import('../views/About.vue')
    }]
    
    const router = createRouter({
        history: createWebHistory(),
        routes
    })
    
    // 导出router
    export default router;
    
    
  2. 修改文件src\App.vue。

    <script setup lang="ts">
    // This starter template is using Vue 3 <script setup> SFCs
    // Check out https://vuejs.org/api/sfc-script-setup.html#script-setup
    import { useRouter } from 'vue-router'
    const router = useRouter();
    
    type RootObject = {
      data: Datum[];
    }
    
    type Datum = {
      name: string;
      price: number;
      id: number;
    }
    
    let list: RootObject = {
      "data": [
        {
          name: "红烧牛肉面",
          price: 50,
          id: 1,
        },
        {
          name: "火腿肠",
          price: 35,
          id: 2,
        },
        {
          name: "牛肉干",
          price: 24,
          id: 3,
        },
      ]
    }
    
    const gotoPage = (url: string, dataNo: number) => {
      router.push({
        path: url,
        query: list.data[dataNo],
      });
    }
    </script>
    
    <template>
      <div>
        <button @click="gotoPage('/', 0)">首页</button> |
        <button @click="gotoPage('/about', 1)">关于</button>
        <hr />
    
        <!-- 只要在项目中安装和配置了vue-router,就可以使用router-view这个组件了 -->
        <!-- 它的作用很单纯,就是占位符 -->
        <router-view></router-view>
      </div>
    </template>
    
    <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>
    
    
  3. 修改文件src\views\Home.vue。

    <template>
        <div class="content-home">
            <h3>我是Home组件</h3>
            <p>品牌:{{ route.query.name }}</p>
            <p>价格:{{ route.query.price }}</p>
            <p>ID:{{ route.query.id }}</p>
        </div>
    </template>
    
    <script setup lang="ts">
    import { useRoute } from 'vue-router'; // 接收传参数据
    import { useRouter } from 'vue-router';
    
    const route = useRoute();
    const router = useRouter();
    </script>
    
    <style lang="less" scoped>
    .content-home {
        width: 200px;
        height: 200px;
        background-color: aqua;
    }
    </style>
    
    
  4. 修改文件src\views\About.vue。

    <template>
        <div class="content-about">
            <h3>我是About组件</h3>
            <p>品牌:{{ route.query.name }}</p>
            <p>价格:{{ route.query.price }}</p>
            <p>ID:{{ route.query.id }}</p>
        </div>
    </template>
    
    <script setup lang="ts">
    import { useRoute } from 'vue-router'; // 接收传参数据
    import { useRouter } from 'vue-router';
    
    const route = useRoute();
    const router = useRouter();
    </script>
    
    <style lang="less" scoped>
    .content-about {
        width: 200px;
        height: 200px;
        background-color: green;
    }
    </style>
    
    

params方式传参

说明:

  1. params需要结合name的方式进行使用。
  2. 存储在内存中,刷新页面,参数会丢失。
  1. 修改文件src\router\index.ts。

    //引入路由对象
    import { createRouter, createWebHistory, createWebHashHistory, createMemoryHistory, RouteRecordRaw } from 'vue-router'
    
    // vue2 mode history vue3 createWebHistory
    // vue2 mode hash vue3 createWebHashHistory
    // vue2 mode abstract vue3 createMemoryHistory
    
    // 路由数组的类型 RouteRecordRaw
    // 定义一些路由
    // 每个路由都需要映射到一个组件。
    const routes: Array<RouteRecordRaw> = [{
        path: '/',
        name: 'Home',
        component: () => import('../views/Home.vue')
    }, {
        path: '/about',
        name: 'About',
        component: () => import('../views/About.vue')
    }]
    
    const router = createRouter({
        history: createWebHistory(),
        routes
    })
    
    // 导出router
    export default router;
    
    
  2. 修改文件src\App.vue。

    <script setup lang="ts">
    // This starter template is using Vue 3 <script setup> SFCs
    // Check out https://vuejs.org/api/sfc-script-setup.html#script-setup
    import { useRouter } from 'vue-router'
    const router = useRouter();
    
    type RootObject = {
      data: Datum[];
    }
    
    type Datum = {
      name: string;
      price: number;
      id: number;
    }
    
    let list: RootObject = {
      "data": [
        {
          name: "红烧牛肉面",
          price: 50,
          id: 1,
        },
        {
          name: "火腿肠",
          price: 35,
          id: 2,
        },
        {
          name: "牛肉干",
          price: 24,
          id: 3,
        },
      ]
    }
    
    const gotoPage = (urlName: string, dataNo: number) => {
      router.push({
        name: urlName,
        params: list.data[dataNo],
      });
    }
    </script>
    
    <template>
      <div>
        <button @click="gotoPage('Home', 0)">首页</button> |
        <button @click="gotoPage('About', 1)">关于</button>
        <hr />
    
        <!-- 只要在项目中安装和配置了vue-router,就可以使用router-view这个组件了 -->
        <!-- 它的作用很单纯,就是占位符 -->
        <router-view></router-view>
      </div>
    </template>
    
    <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>
    
    
  3. 修改文件src\views\Home.vue。

    <template>
        <div class="content-home">
            <h3>我是Home组件</h3>
            <p>品牌:{{ route.params.name }}</p>
            <p>价格:{{ route.params.price }}</p>
            <p>ID:{{ route.params.id }}</p>
        </div>
    </template>
    
    <script setup lang="ts">
    import { useRoute } from 'vue-router'; // 接收传参数据
    import { useRouter } from 'vue-router';
    
    const route = useRoute();
    const router = useRouter();
    </script>
    
    <style lang="less" scoped>
    .content-home {
        width: 200px;
        height: 200px;
        background-color: aqua;
    }
    </style>
    
    
  4. 修改文件src\views\About.vue。

    <template>
        <div class="content-about">
            <h3>我是About组件</h3>
            <p>品牌:{{ route.params.name }}</p>
            <p>价格:{{ route.params.price }}</p>
            <p>ID:{{ route.params.id }}</p>
        </div>
    </template>
    
    <script setup lang="ts">
    import { useRoute } from 'vue-router'; // 接收传参数据
    import { useRouter } from 'vue-router';
    
    const route = useRoute();
    const router = useRouter();
    </script>
    
    <style lang="less" scoped>
    .content-about {
        width: 200px;
        height: 200px;
        background-color: green;
    }
    </style>
    
    

params方式结合动态路由参数

  1. 修改文件src\router\index.ts。

    //引入路由对象
    import { createRouter, createWebHistory, createWebHashHistory, createMemoryHistory, RouteRecordRaw } from 'vue-router'
    
    // vue2 mode history vue3 createWebHistory
    // vue2 mode hash vue3 createWebHashHistory
    // vue2 mode abstract vue3 createMemoryHistory
    
    // 路由数组的类型 RouteRecordRaw
    // 定义一些路由
    // 定义一些路由
    // 每个路由都需要映射到一个组件。
    const routes: Array<RouteRecordRaw> = [{
        path: '/',
        name: 'Home',
        component: () => import('../views/Home.vue')
    }, {
        // 动态路由传参
        path: '/about/:id',
        name: 'About',
        component: () => import('../views/About.vue')
    }]
    
    const router = createRouter({
        history: createWebHistory(),
        routes
    })
    
    // 导出router
    export default router;
    
    
  2. 修改文件src\App.vue。

    <script setup lang="ts">
    // This starter template is using Vue 3 <script setup> SFCs
    // Check out https://vuejs.org/api/sfc-script-setup.html#script-setup
    import { useRouter } from 'vue-router'
    const router = useRouter();
    
    type RootObject = {
      data: Datum[];
    }
    
    type Datum = {
      name: string;
      price: number;
      id: number;
    }
    
    let list: RootObject = {
      "data": [
        {
          name: "红烧牛肉面",
          price: 50,
          id: 1,
        },
        {
          name: "火腿肠",
          price: 35,
          id: 2,
        },
        {
          name: "牛肉干",
          price: 24,
          id: 3,
        },
      ]
    }
    
    const gotoPage = (urlName: string, dataNo: number) => {
      router.push({
        name: urlName,
        params: {
          id: list.data[dataNo].id
        },
      });
    }
    </script>
    
    <template>
      <div>
        <button @click="gotoPage('Home', 0)">首页</button> |
        <button @click="gotoPage('About', 1)">关于</button>
        <hr />
    
        <!-- 只要在项目中安装和配置了vue-router,就可以使用router-view这个组件了 -->
        <!-- 它的作用很单纯,就是占位符 -->
        <router-view></router-view>
      </div>
    </template>
    
    <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>
    
    
  3. 修改文件src\views\Home.vue。

    <template>
        <div class="content-home">
            <h3>我是Home组件</h3>
            <p>品牌:{{ route.params.name }}</p>
            <p>价格:{{ route.params.price }}</p>
            <p>ID:{{ route.params.id }}</p>
        </div>
    </template>
    
    <script setup lang="ts">
    import { useRoute } from 'vue-router'; // 接收传参数据
    import { useRouter } from 'vue-router';
    
    const route = useRoute();
    const router = useRouter();
    </script>
    
    <style lang="less" scoped>
    .content-home {
        width: 200px;
        height: 200px;
        background-color: aqua;
    }
    </style>
    
    
  4. 修改文件src\views\About.vue。

    <template>
        <div class="content-about">
            <h3>我是About组件</h3>
            <p>品牌:{{ route.params.name }}</p>
            <p>价格:{{ route.params.price }}</p>
            <p>ID:{{ route.params.id }}</p>
        </div>
    </template>
    
    <script setup lang="ts">
    import { useRoute } from 'vue-router'; // 接收传参数据
    import { useRouter } from 'vue-router';
    
    const route = useRoute();
    const router = useRouter();
    </script>
    
    <style lang="less" scoped>
    .content-about {
        width: 200px;
        height: 200px;
        background-color: green;
    }
    </style>
    
    

路由嵌套

  1. 使用Vite搭建Vue3开发环境,参见[Vite创建Vue3.x项目【模板】](# Vite创建Vue3.x项目【模板】)。

  2. 安装和配置less。参见[Vite安装和配置less【模板】](# Vite安装和配置less【模板】)。

  3. 安装和配置Router。参见[Vite安装和配置Router【模板】](# Vite安装和配置Router【模板】)。

  4. 修改文件src\router\index.ts。

    import { createRouter, createWebHistory } from 'vue-router'
    import Users from '../views/Users.vue'
    import Home from '../views/Home.vue'
    import Tab1 from '../views/tabs/Tab1.vue'
    import Tab2 from '../views/tabs/Tab2.vue'
    
    const router = createRouter({
        history: createWebHistory(),
        routes: [
            {
                path: '/users/:username',
                component: Users,
                children: [
                    // UserHome will be rendered inside User's <router-view>
                    // when /users/:username is matched
                    { path: '', component: Home },
    
                    // UserProfile will be rendered inside User's <router-view>
                    // when /users/:username/profile is matched
                    { path: 'tab1', component: Tab1 },
    
                    // UserPosts will be rendered inside User's <router-view>
                    // when /users/:username/posts is matched
                    { path: 'tab2', component: Tab2 },
                ],
            },
        ],
    })
    
    export default router;
    
    
  5. 新建文件src\views\Users.vue。

    <template>
        <div class="content-users">
            <h3>Top -- 我是Users组件</h3>
            <router-view></router-view>
        </div>
    </template>
    
    <script setup lang="ts">
    
    </script>
    
    <style lang="less" scoped>
    .content-users {
        width: 400px;
        height: 400px;
        background-color: aqua;
    }
    </style>
    
    
  6. 新建文件src\views\Home.vue。

    <template>
        <div class="content-home">
            <h3>Second -- 我是Home组件</h3>
        </div>
    </template>
    
    <script setup lang="ts">
    
    </script>
    
    <style lang="less" scoped>
    .content-home {
        width: 200px;
        height: 200px;
        background-color: green;
    }
    </style>
    
    
  7. 新建文件src\views\tabs\Tab1.vue。

    <template>
        <div class="content-tab1">
            <h3>Second -- 我是嵌套子组件Tab1组件</h3>
        </div>
    </template>
    
    <script setup lang="ts
    
    </script>
    
    <style lang="less" scoped>
    .content-tab1 {
        width: 200px;
        height: 200px;
        background-color: bisque;
    }
    </style>
    
    
  8. 新建文件src\views\tabs\Tab2.vue。

    <template>
        <div class="content-tab2">
            <h3>Second -- 我是嵌套子组件Tab2组件</h3>
        </div>
    </template>
    
    <script setup lang="ts">
    
    </script>
    
    <style lang="less" scoped>
    .content-tab2 {
        width: 200px;
        height: 200px;
        background-color: orangered;
    }
    </style>
    
    
  9. 修改文件src\App.vue。

    <template>
      <h1>Nested Views</h1>
      <p>
        <!-- 名称"/eduardo"为可选 -->
        <router-link to="/users/eduardo">/users/eduardo</router-link>
        <br />
        <router-link to="/users/eduardo/tab1">/users/eduardo/tab1</router-link>
        <br />
        <router-link to="/users/eduardo/tab2">/users/eduardo/tab2</router-link>
        <br />
      </p>
      <router-view></router-view>
    </template>
    
    <script setup lang="ts">
    </script>
    
    <style>
    #app {
      font-family: Avenir, Helvetica, Arial, sans-serif;
      -webkit-font-smoothing: antialiased;
      -moz-osx-font-smoothing: grayscale;
      color: #2c3e50;
    }
    
    .router-link-active {
      color: orange;
    }
    
    .router-link-exact-active {
      color: crimson;
    }
    </style>
    
    <style scoped>
    ul {
      display: flex;
      list-style: none;
      padding: 0;
      margin: 0;
    }
    
    li:not(:last-of-type) {
      margin-right: 1rem;
    }
    </style><template>
      <h1>Nested Views</h1>
      <p>
        <!-- 名称"/eduardo"为可选 -->
        <router-link to="/users/eduardo">/users/eduardo</router-link>
        <br />
        <router-link to="/users/eduardo/tab1">/users/eduardo/tab1</router-link>
        <br />
        <router-link to="/users/eduardo/tab2">/users/eduardo/tab2</router-link>
        <br />
      </p>
      <router-view></router-view>
    </template>
    
    <script setup lang="ts">
    </script>
    
    <style>
    #app {
      font-family: Avenir, Helvetica, Arial, sans-serif;
      -webkit-font-smoothing: antialiased;
      -moz-osx-font-smoothing: grayscale;
      color: #2c3e50;
    }
    
    .router-link-active {
      color: orange;
    }
    
    .router-link-exact-active {
      color: crimson;
    }
    </style>
    
    <style scoped>
    ul {
      display: flex;
      list-style: none;
      padding: 0;
      margin: 0;
    }
    
    li:not(:last-of-type) {
      margin-right: 1rem;
    }
    </style>
    
    

命名视图

命名视图可以在同一级(同一个组件)中展示更多的路由视图,而不是嵌套显示。 命名视图可以让一个组件中具有多个路由渲染出口,这对于一些特定的布局组件非常有用。 命名视图的概念非常类似于“具名插槽”,并且视图的默认名称也是default。

命名视图

  1. 使用Vite搭建Vue3开发环境,参见[Vite创建Vue3.x项目【模板】](# Vite创建Vue3.x项目【模板】)。

  2. 安装对应的Router版本。
    使用Vue3安装对应的Router4版本。
    使用Vue2安装对应的Router3版本。

    npm i -D vue-router@4
    
  3. 安装和配置less。参见[Vite安装和配置less【模板】](# Vite安装和配置less【模板】)。

  4. 修改文件src\main.ts。

    import { createApp } from 'vue'
    import App from './App.vue'
    import router from './router';
    
    const app = createApp(App);
    app.use(router);
    
    app.mount('#app')
    
    
  5. 新建文件夹src\views,新建文件src\views\Menu.vue。

    <template>
        <div class="content-menu">
            <h3>我是Menu组件</h3>
        </div>
    </template>
    
    <script setup lang="ts">
    
    </script>
    
    <style lang="less" scoped>
    .content-menu {
        width: 200px;
        height: 200px;
        background-color: aqua;
    }
    </style>
    
    
  6. 新建文件src\views\Header.vue。

    <template>
        <div class="content-header">
            <h3>我是Header组件</h3>
        </div>
    </template>
    
    <script setup lang="ts">
    
    </script>
    
    <style lang="less" scoped>
    .content-header {
        width: 200px;
        height: 200px;
        background-color: orangered;
    }
    </style>
    
    
  7. 新建文件src\views\Content.vue。

    <template>
        <div class="content-content">
            <h3>我是Content组件</h3>
        </div>
    </template>
    
    <script setup lang="ts">
    
    </script>
    
    <style lang="less" scoped>
    .content-content {
        width: 200px;
        height: 200px;
        background-color: orange;
    }
    </style>
    
    
  8. 新建文件夹src\router和新建文件src\router\index.ts。

    import { createRouter, createWebHistory } from 'vue-router'
    import Menu from '../views/Menu.vue'
    import Header from '../views/Header.vue'
    import Content from '../views/Content.vue'
    
    const router = createRouter({
        history: createWebHistory(),
        routes: [
            {
                path: '/',
                components: {
                    default: Menu,
                    aa: Header,
                    bb: Content,
                },
            },
            {
                path: '/header',
                components: {
                    default: Header,
                    aa: Menu,
                    bb: Content,
                },
            },
            {
                path: '/content',
                components: {
                    default: Content,
                    aa: Menu,
                    bb: Header,
                },
            },
        ],
    })
    
    export default router;
    
    
  9. 修改文件src\App.vue。

    <template>
      <h1>我是App组件</h1>
      <p>
        <!-- 单击不同的路由链接,展示几个路由视图 -->
        <router-link to="/">Menu</router-link> |
        <router-link to="/header">Header</router-link> |
        <router-link to="/content">Content</router-link>
      </p>
      <hr>
      <router-view></router-view>
      <router-view name="aa"></router-view>
      <router-view name="bb"></router-view>
    </template>
    
    <script setup lang="ts">
    // This starter template is using Vue 3 <script setup> SFCs
    // Check out https://vuejs.org/api/sfc-script-setup.html#script-setup
    </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>
    
    

嵌套命名视图

  1. 基于[命名视图](# 命名视图)的基础上进行修改使用。

  2. 新建文件src\views\Root.vue。

    <template>
        <div class="content-menu">
            <h3>Top -- 我是Root组件</h3>
            <p>
                <!-- 名称"/eduardo"为可选 -->
                <router-link to="/">Menu</router-link> |
                <router-link to="/header">Header</router-link> |
                <router-link to="/content">Content</router-link>
            </p>
            <hr>
            <router-view></router-view>
            <router-view name="aa"></router-view>
            <router-view name="bb"></router-view>
        </div>
    </template>
    
    <script setup lang="ts">
    
    </script>
    
    <style lang="less" scoped>
    .content-menu {
        width: 400px;
        height: 400px;
        background-color: yellow;
    }
    </style>
    
    
  3. 修改文件src\router\index.ts。

    import { createRouter, createWebHistory } from 'vue-router'
    import Root from '../views/Root.vue'
    import Menu from '../views/Menu.vue'
    import Header from '../views/Header.vue'
    import Content from '../views/Content.vue'
    
    const router = createRouter({
        history: createWebHistory(),
        routes: [
            {
                path: '/',
                component: Root,
                children: [
                    {
                        // path: '',子路由中如果路径填写为空,则默认展示的是此路径中的内容
                        path: '/header',
                        components: {
                            default: Header,
                            aa: Menu,
                            bb: Content,
                        },
                    },
                    {
                        path: '/content',
                        components: {
                            default: Content,
                            aa: Menu,
                            bb: Header,
                        },
                    },
                ]
            },
        ],
    })
    
    export default router;
    
    
  4. 修改文件src\App.vue。

    <template>
      <h1>我是App组件</h1>
      <router-view></router-view>
    </template>
    
    <script setup lang="ts">
    // This starter template is using Vue 3 <script setup> SFCs
    // Check out https://vuejs.org/api/sfc-script-setup.html#script-setup
    </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>
    
    

重定向-别名

说明:基于上述嵌套命名视图的示例说明。修改文件src\router\index.ts内容。

重定向-redirect

  • 字符串形式配置,访问/重定向到/header

    import { createRouter, createWebHistory } from 'vue-router'
    import Root from '../views/Root.vue'
    import Menu from '../views/Menu.vue'
    import Header from '../views/Header.vue'
    import Content from '../views/Content.vue'
    
    const router = createRouter({
        history: createWebHistory(),
        routes: [
            {
                path: '/',
                component: Root,
                // 默认展示/header子组件中的内容
                redirect: '/header',
                children: [
                    {
                        path: '/header',
                        components: {
                            default: Header,
                            aa: Menu,
                            bb: Content,
                        },
                    },
                    {
                        path: '/content',
                        components: {
                            default: Content,
                            aa: Menu,
                            bb: Header,
                        },
                    },
                ]
            },
        ],
    })
    
    export default router;
    
    
  • 对象形式配置。

    import { createRouter, createWebHistory } from 'vue-router'
    import Root from '../views/Root.vue'
    import Menu from '../views/Menu.vue'
    import Header from '../views/Header.vue'
    import Content from '../views/Content.vue'
    
    const router = createRouter({
        history: createWebHistory(),
        routes: [
            {
                path: '/',
                component: Root,
                redirect: {
                    path: '/header',
                },
                children: [
                    {
                        path: '/header',
                        components: {
                            default: Header,
                            aa: Menu,
                            bb: Content,
                        },
                    },
                    {
                        path: '/content',
                        components: {
                            default: Content,
                            aa: Menu,
                            bb: Header,
                        },
                    },
                ]
            },
        ],
    })
    
    export default router;
    
    
  • 函数形式(可以传参)。

    import { createRouter, createWebHistory } from 'vue-router'
    import Root from '../views/Root.vue'
    import Menu from '../views/Menu.vue'
    import Header from '../views/Header.vue'
    import Content from '../views/Content.vue'
    
    const router = createRouter({
        history: createWebHistory(),
        routes: [
            {
                path: '/',
                component: Root,
                redirect(to) {
                    // 模板
                    // return {
                    //     path: '/header',
                    //     query: to.query, // 可选的传参
                    // }
                    // 示例
                    return {
                        path: '/header',
                        query: { // 可选的传参
                            name: '张三',
                        },
                    }
                },
                children: [
                    {
                        path: '/header',
                        components: {
                            default: Header,
                            aa: Menu,
                            bb: Content,
                        },
                    },
                    {
                        path: '/content',
                        components: {
                            default: Content,
                            aa: Menu,
                            bb: Header,
                        },
                    },
                ]
            },
        ],
    })
    
    export default router;
    
    

别名-alias

/别名为/root/root1/root2,意味着当用户访问/root/root1/root2时候,会被匹配为用户正在访问/

import { createRouter, createWebHistory } from 'vue-router'
import Root from '../views/Root.vue'
import Menu from '../views/Menu.vue'
import Header from '../views/Header.vue'
import Content from '../views/Content.vue'

const router = createRouter({
    history: createWebHistory(),
    routes: [
        {
            path: '/',
            component: Root,
            alias: ['/root', '/root1', '/root2'],
            children: [
                {
                    path: '/header',
                    components: {
                        default: Header,
                        aa: Menu,
                        bb: Content,
                    },
                },
                {
                    path: '/content',
                    components: {
                        default: Content,
                        aa: Menu,
                        bb: Header,
                    },
                },
            ]
        },
    ],
})

export default router;

导航守卫

前置守卫(结合ElementUI使用)

  1. 使用Vite搭建Vue3开发环境,参见[Vite创建Vue3.x项目【模板】](# Vite创建Vue3.x项目【模板】)。
  2. 安装和配置less。参见[Vite安装和配置less【模板】](# Vite安装和配置less【模板】)。
  3. 安装和配置Router。参见[Vite安装和配置Router【模板】](# Vite安装和配置Router【模板】)。
  4. 安装和配置ElementUI。参见[UI库ElementUI【模板】](# UI库ElementUI【模板】)。

后置守卫

posted on 2022-05-15 20:43  zyjhandsome  阅读(183)  评论(0编辑  收藏  举报