DayNode(Vue基础)

247 阅读10分钟

渐进式javascript框架

@VUE/CLI脚手架

目标: webpack自己配置环境很麻烦, 下载@vue/cli包,用vue命令创建脚手架项目

yarn global add @vue/cli

截屏2021-07-30 上午10.37.35.png

启动项目

1. 创建项目
# vue和create是命令, vuecli-demo是文件夹名
vue create vuecli-demo

2. 安装ing

3. 进入脚手架项目下, 启动内置的热更新本地服务器
cd vuecil-demo

yarn serve// 或 npm run serve

image-20210317201811310.png

public/index.html – 浏览器运行的网页
src/main.js – webpack打包的入口文件
src/App.vue – vue项目入口页面

截屏2021-07-30 下午2.38.45.png

截屏2021-07-30 下午3.00.30.png

@vue/cli 自定义配置

src并列处新建vue.config.js(暂时关闭eslint检查) 注意:

  • 改了配置文件 是需要重启服务器的
  • 只是改了 代码文件 是会自动热更新的
/* 覆盖webpack的配置 */
module.exports = {
    devServer: { // 自定义服务配置
        open: true, // 自动打开浏览器
        port: 3000, // 自定义端口号
    },
    lintOnSave: false, // 关闭eslint检查
}

指令

自定义指令

  1. inserted方法 - 指令所在标签, 被插入到网页上触发(一次)

2. update方法 - 指令对应数据/标签更新时, 此方法执行

局部注册

<template>
  <div>
      <!-- <input type="text" v-gfocus> -->
      <input type="text" v-focus>
      
  </div>
</template>

<script>
// 目标: 创建 "自定义指令", 让输入框自动聚焦
export default {
    data(){
        return {
            colorStr: 'red'
        }
    },
    directives: {
        focus: {
            inserted(el){
                el.focus()
            }
        }
    }
}
</script>

全局注册

在main.js用 Vue.directive()

// 全局指令 - 到处"直接"使用
Vue.directive("gfocus", {
  inserted(el) {
    el.focus() // 触发标签的事件方法
  }
})

自定义指令-传值

<template>
  <div>
      <p v-color="colorStr">修改文字颜色</p>
      <button @click="changeColor">改为蓝色</button>
  </div>
</template>

<script>
export default {
    data(){
        return {
            colorStr: 'red'
        }
    },
    directives: {
        color:{
          inserted(el, binding) {
            el.style.color = binding.value
          },
          update(el,binding){
            el.style.color=binding.value
          }
        }
    },
    methods: {
      changeColor(){
        this.colorStr='blue'
      }
    }
}
</script>

v-model | 表单

  • 下拉菜单,v-model要绑定在select

  • input[checkbox]的多选框状态,v-model

    • 变量为非数组, 则绑定的是所有的box的checked的属性(true/false) - 常用于: 单个绑定使用,全选操作
    • 变量为数组, 则绑定的是他们的value属性里的值 - 常用于: 收集勾选了哪些值
  • 单选框 input[radio],需要相同的name和v-model,绑定的就是value的值(非数组)

<template>
  <div>
    <!-- 
    	v-model:是实现vuejs变量和表单标签value属性, 双向绑定的指令
    -->
    <div>
      <span>用户名:</span>
      <input type="text" v-model="username" />
    </div>
    <div>
      <span>密码:</span>
      <input type="password" v-model="pass" />
    </div>
    <div>
      <span>来自于: </span>
      <!-- 下拉菜单要绑定在select上 -->
      <select v-model="from">
        <option value="北京市">北京</option>
        <option value="南京市">南京</option>
        <option value="天津市">天津</option>
      </select>
    </div>
    <div>
      <!-- (重要)
      遇到复选框, v-model的变量值
      非数组 - 关联的是复选框的checked属性
      数组   - 关联的是复选框的value属性
       -->
      <span>爱好: </span>
      <input type="checkbox" v-model="hobby" value="抽烟">抽烟
      <input type="checkbox" v-model="hobby" value="喝酒">喝酒
      <input type="checkbox" v-model="hobby" value="写代码">写代码
    </div>
    <div>
      <span>性别: </span>
      <input type="radio" value="男" name="sex" v-model="gender">男
      <input type="radio" value="女" name="sex" v-model="gender">女
    </div>
    <div>
      <span>自我介绍</span>
      <textarea v-model="intro"></textarea>
    </div>
  </div>
</template>

<script>
export default {
  data() {
    return {
      username: "",
      pass: "",
      from: "",
      hobby: [], 
      sex: "",
      intro: "",
    };
    // 总结:
    // 特别注意: v-model, 在input[checkbox]的多选框状态
    // 变量为非数组, 则绑定的是checked的属性(true/false) - 常用于: 单个绑定使用
    // 变量为数组, 则绑定的是他们的value属性里的值 - 常用于: 收集勾选了哪些值
  }
};
</script>

v-model.修饰符

v-model.修饰符="vue数据变量"

  • .numberparseFloat转成数字类型,再传值给data中的数据

  • .trim 去除首尾空白字符

  • .lazy 文本框失去焦点(blur)时触发而非inupt时

v-text | v-html

v-text把值当成普通字符串显示, v-html把值当做html解析

注意: 会覆盖插值表达式

{{}} | v-text | v-html区别

<div>{{msg}}-okkk</div>
// <h1>看看看</h1>-okkk  (不会解析html标签)

<div v-text='msg'>-okkk</div>
// <h1>看看看</h1>       (不会解析html标签)

<div v-html='msg'>-okkk</div>
// 看看看                (会解析html标签)


data:{
    msg:'<h1>看看看</h1>'
}

v-show和v-if

  • v-show 和v-if都是true的时候显示,false的时候隐藏

  • v-show 用的display:none隐藏 (频繁切换使用)

    • 不管初始条件是什么,元素总是会被渲染 也就是说如果找到了v-show,不管是true还是false都会先去创建元素,然后在进行css操作
    • 有更高的初始渲染开销
  • v-if 直接从DOM树上移除(采用惰性加载

    • 切换过程中条件块内的事件监听器和子组件会被适当地销毁和重建,也就是会触发组件和子组件的生命周期
    • 有更高的切换开销

注意:一旦视图中使用到的data发生改变,当前组件的整个视图都会重绘,消耗性能

js里面使用本地图片

js里面需要导入import,而html和css中可以直接写路径

<template>
  <div id="app">
    <img src="./assets/logo.png" alt />
    <img :src="imgUrl" alt />
  </div>
</template>

<script>
import imgUrl from './assets/logo.png'
export default {
  data() {
    return {
      imgUrl: imgUrl
    }
  }
}
</script>

v-for

注意

  • v-for的范围 要存在于双标签内部
  • 例:必须在input单标签外面套一个双标签,如label,再在lable标签中循环

更新监测

  1. 数组变更方法, 就会导致v-for更新, 页面更新
  • push()
  • pop()
  • shift()
  • unshift()
  • splice()
  • sort()
  • reverse()
  1. 数组非变更方法, 返回新数组, 就不会导致v-for更新, 可采用覆盖数组或this.$set()Vue.set()
  • slice()
  • filter()
  • concat()
  • map()
<template>
  <div>
    <ul>
      <li v-for="(val, index) in arr" :key="index">
        {{ val }}
      </li>
    </ul>
    <button @click="revBtn">数组翻转</button>
    <button @click="sliceBtn">截取前3个</button>
    <button @click="updateBtn">更新第一个元素值</button>
  </div>
</template>

<script>
export default {
  data(){
    return {
      arr: [5, 3, 9, 2, 1]
    }
  },
  methods: {
    revBtn(){
      // 1. 数组翻转可以让v-for更新
      this.arr.reverse()
    },
    sliceBtn(){
      // 2. 数组slice方法不会造成v-for更新
      // slice不会改变原始数组

      // 解决v-for更新 - 覆盖原始数组
      let newArr = this.arr.slice(0, 3)
      this.arr = newArr
    },
    updateBtn(){
      // 3. 更新某个值的时候, v-for是监测不到的
      // this.arr[0] = 1000;

      // 解决-this.$set()
      // 参数1: 更新目标结构
      // 参数2: 更新位置
      // 参数3: 更新值
      this.$set(this.arr, 0, 1000)
    }
  }
}
</script>

就地更新

v-for 的默认行为会尝试原地修改元素而不是移动它们。

image-20210414215302318.png

虚拟dom

.vue文件中的template里写的标签, 都是模板, 都要被vue处理成虚拟DOM对象, 才会渲染显示到真实DOM页面上

06虚拟dom.png vue数据更新:

  • 生成新的虚拟DOM结构
  • 和旧的虚拟DOM结构对比
  • 利用diff算法, 找不同, 只更新变化的部分(重绘/回流)到页面 - 也叫打补丁

diff算法-key

vue用diff算法, 新虚拟dom, 和旧的虚拟dom比较

情况2: 根元素变了, 删除重建

情况2: 根元素没变, 属性改变, ==元素复用==, 更新属性

  1. 无key ---- 就地更新

    • 性能不高, 从第二个li往后都更新了 image-20210414215502653.png
  2. 有key - 值为索引 ---- 就地更新

    • v-for先循环产生新的DOM结构, key是连续的, 和数据对应,然后比较新旧DOM结构, 找到区别, 打补丁到页面上
    • 最后补一个li, 然后从第二个往后, 都要更新内容

image-20210414215525492.png

  1. 有key - 值为id
    • key的值只能是唯一不重复的, 字符串或数值
    • 比较新旧dom,比较到key相同就直接复用,新增了一个key,就在中间添加一个li,第二个li后的key未发生改变,直接后移
    • 只在中间插入了一个li,其他dom都未发生改变,最节省性能

image-20210414215546869.png

动态class

:class="{类名: 布尔值}"

data中---布尔值 style样式中---类名

<template>
  <div>
    <!-- 语法:
      :class="{类名: 布尔值}"
      使用场景: vue变量控制标签是否应该有类名
     -->
    <p :class="{red_str: bool}">动态class</p>
  </div>
</template>

<script>
export default {
  data(){
    return {
      bool: true
    }
  }
}
</script>

<style scoped>
  .red_str{
    color: red;
  }
</style>

动态style

:style="{css属性: 值}"

data中----属性值

<template>
  <div>
    <!-- 动态style语法
      :style="{css属性名: 值}"
     -->
    <p :style="{backgroundColor: colorStr}">动态style</p>
  </div>
</template>

<script>
export default {
  data(){
    return {
      colorStr: 'red'
    }
  }
}
</script>

过滤器

过滤器只能用在, 插值表达式v-bind表达式

过滤器传参: vue变量 | 过滤器(实参)

多个过滤器: vue变量 | 过滤器1 | 过滤器2

Vue.filter("过滤器名", () => {return "返回处理后的值"})

filters: {过滤器名字: () => {return "返回处理后的值"}
<template>
  <div>
    <p>原来的样子: {{ msg }}</p>
    <!-- 1.
      给过滤器传值
      语法: vue变量 | 过滤器名(值)
     -->
    <p>使用翻转过滤器: {{ msg | reverse('|') }}</p>
    <!-- 2.
      多个过滤利使用
      语法: vue变量 | 过滤器1 | 过滤器2
     -->
    <p :title="msg | toUp | reverse('|')">鼠标长停</p>
  </div>
</template>

<script>
export default {
  data(){
    return {
      msg: 'Hello, Vue'
    }
  },
  filters: {
    toUp (val) {
      return val.toUpperCase()
    }
  }
}
</script>

<style>

</style>

时间格式化

yarn add moment

// 目标: 处理时间
// 1. 下载moment模块
import moment from 'moment'


// 2. 定义过滤器, 编写内部代码
filters: { 
    formatDate (val){
        return moment(val).format('YYYY-MM-DD')
    }
}

<!-- 3. 使用过滤器 -->
<td>{{ obj.time | formatDate }}</td>

计算属性 computed

方法,为了获取计算结果

注意:

  • 计算属性num也是vue数据变量, 所以不要和data里重名, 用法和data相同

  • 计算属性是基于它们的依赖项(this.a, this.b)的值结果进行缓存的,只要依赖的变量不变, 都直接从缓存取结果, 比普通方法性能更高

    • 多次调用,只有第一次 会执行代码,后面的调用,直接 从缓存中获取
    • 一旦 计算属性中 使用的 数据发生变化,则 重新执行
<template>
  <div>
    <p>{{ num }}</p>
  </div>
</template>

<script>
export default {
  data(){
    return {
      a: 10,
      b: 20
    }
  },
  // 场景: 一个变量的值, 需要用另外变量计算而得来
  // 注意: 计算属性和data属性都是变量-不能重名
  // 注意2: 函数内变量变化, 会自动重新计算结果返回
  computed: {
    num(){
      return this.a + this.b
    }
  }
}
</script>

set|get(ES6)-双向绑定

对象,为了获取 计算结果和修改计算结果

计算属性通常给v-model使用,也可以直接普通变量使用

computed: {
    "属性名": {
        set(){
            
        },
        get() {
            return "值"
        }
    }
}
  • set(),view中计算属性发生变化,来影响model

    • view中的计算属性发生了变化,调用set(),可以根据计算属性的值去设置model中其他数据的值
  • get(),model中的计算属性发生变化,去影响view

  • 要去修改设置view中的值,调用get(),根据model中依赖项发生改变,就返回同步到view

全选案例

<template>
  <div>
    <span>全选:</span>
    <!-- 4. v-model 关联全选 - 选中状态 -->
    <input type="checkbox" v-model="isAll"/>
    <ul>
      <li v-for="(obj, index) in arr" :key="index">
        <!-- 3. 对象.c - 关联 选中状态 -->
        <input type="checkbox" v-model="obj.c"/>
        <span>{{ obj.name }}</span>
      </li>
    </ul>
  </div>
</template>

<script>
export default {
  data() {
    return {
      arr: [
        {
          name: "猪八戒",
          c: false,
        },
        {
          name: "孙悟空",
          c: false,
        },
        {
          name: "唐僧",
          c: false,
        },
        {
          name: "白龙马",
          c: false,
        },
      ],
    };
  },
  // 5. 计算属性-isAll
  computed: {
    isAll: {
      set(val){
        // 7. 全选框 - 选中状态(true/false)
        this.arr.forEach(obj => obj.c = val)
      },
      get(){
        // 6. 统计小选框状态 ->  全选状态
        // every口诀: 查找数组里"不符合"条件, 直接原地返回false
        return this.arr.every(obj => obj.c === true)
      }
    }
  }
};
</script>

侦听器 watch

目标: 可以侦听data/computed属性值改变 本质是个方法

<template>
  <div>
    <input type="text" v-model="uname" />
    <div>
      <input type="text" v-model="dog.dname" />
      <input type="text" v-model="dog.age" />
    </div>
  </div>
</template>

<script>
export default {
  data(){
    return {
      uname:'死鬼',
      dog:{
        dname:'ruiky',
        age:12
      }
    }
  },
  watch:{
    // 1.普通侦听器:侦听的 data变量 是 值类型数据(简单数据类型) ---------
    uname(newV,oldV){
      console.log(newV,oldV)
    },
    // 2.深度侦听器:侦听的 data变量 是 引用类型数据(复杂数据类型) -------
    dog:{
      deep:true, // 本质:为 被侦听的 对象里 每一个 属性 都添加 侦听器!
      // immediate:true, // 视图加载时 自动 触发一次 侦听器
      // 当 被侦听的 data变量 数据 发生改变时 自动触发执行
      handler(newDog){
        console.log(newDog)
        // console.log(newDog === oldDog)
        // console.log(newDog === this.dog)
      }
    },
    //3.侦听对象中的 指定的 属性
    'dog.dname':{
      handler(newv){
        console.log(newv)
      }
    }
  }
}
</script>

组件

注册使用

全局注册

main.js, 在new Vue之上注册

import Vue from 'vue'
// 目标: 全局注册 (一处定义到处使用)
// 1. 创建组件 - 文件名.vue
// 2. 引入组件
import Pannel from './components/Pannel'
// 3. 全局 - 注册组件
/*
  语法: 
  Vue.component("组件名", 组件对象)
*/
Vue.component("PannelG", Pannel)

局部注册

任意vue文件中引入, 注册,

<template>
  <div id="app">
    <h3>案例:折叠面板</h3>
    <!-- 4. 组件名当做标签使用 -->
    <!-- <组件名></组件名> -->
    <PannelG></PannelG>
    <PannelL></PannelL>
  </div>
</template>

<script>
// 目标: 局部注册 (用的多)
// 1. 创建组件 - 文件名.vue
// 2. 引入组件
import Pannel from './components/Pannel_1'
export default {
  // 3. 局部 - 注册组件
  components: {
    PannelL: Pannel
  }
}
</script>

scoped作用

目的: 解决多个组件样式名相同, 冲突问题 <style scoped>

在style上加入scoped属性, 就会在此组件的标签上加上一个随机生成的data-v开头的属性

组件通信

截屏2021-08-03 下午4.41.07.png

父向子-props

1. 属性传递: 传静态属性:则不加:title="好吃的口水鸡" 传动态属性:则需要加::title="title"

2. 单向数据流:

  • 从父到子 的数据流向,叫单向数据流
    • props变量本身是只读不能重新赋值

子组件---components/MyProduct.vue

<template>
  <div class="my-product">
    <h3>标题: {{ title }}</h3>
    <p>价格: {{ price }}元</p>
    <p>{{ intro }}</p>
  </div>
</template>

<script>
export default {
  props: ['title', 'price', 'intro']
}
</script>

父组件 App.vue

<template>
  <div>
    <Product title="好贵的北京烤鸭" price="290" :intro="str"></Product>
  </div>
</template>

<script>
// 1. 创建组件 (.vue文件)
// 2. 引入组件
import Product from './components/MyProduct'
export default {
  data(){
    return {
      str: "好贵啊, 快来啊, 好吃"
    }
  },
  // 3. 注册组件
  components: {
    // Product: Product // key和value变量名同名 - 简写
    Product
  }
}
</script>

父向子-配合循环

子组件 /components/MyProduct

<template>
  <div>
    <MyProduct v-for="obj in list" :key="obj.id"
    :title="obj.proname"
    :price="obj.proprice"
    :intro="obj.info"
    ></MyProduct>
  </div>
</template>

<script>
// 目标: 循环使用组件-分别传入数据
// 1. 创建组件
// 2. 引入组件
import MyProduct from './components/MyProduct'
export default {
  data() {
    return {
      list: [
        {
          id: 1,
          proname: "超级好吃的棒棒糖",
          proprice: 18.8,
          info: "开业大酬宾, 全场8折",
        },
        {
          id: 2,
          proname: "超级好吃的大鸡腿",
          proprice: 34.2,
          info: "好吃不腻, 快来买啊",
        }
      ],
    };
  },
  // 3. 注册组件
  components: {
    // MyProduct: MyProduct
    MyProduct
  }
};
</script>

父组件 App.vue

<template>
  <div class="my-product">
    <h3>标题: {{ title }}</h3>
    <p>价格: {{ price }}元</p>
    <p>{{ intro }}</p>
  </div>
</template>

<script>
export default {
  props: ['title', 'price', 'intro']
}
</script>

子向父组件通信

  • 父: @自定义事件名="父methods函数"
  • 子: this.$emit("自定义事件名", 传值) - 执行父methods里函数代码 子组件 /components/MyProduct_sub
<template>
  <div class="my-product">
    <h3>标题: {{ obj.name }}</h3>
    <p>价格: {{ obj.price }}元</p>
    <button @click="subFn">降低价格</button>
    <button @click="emit('addFn',obj.id,1)">增加价格</button>
  </div>
</template>

<script>
export default {
  props: ['obj'],
  methods: {
    subFn(){
      this.$emit('subprice', this.obj.id, 1) // 子向父
    }
  }
}
</script>

父组件 App.js

<template>
  <div>
    <!-- 目标: 子传父 -->
    <!-- 1. 父组件, @自定义事件名="父methods函数" -->
    <MyProduct v-for="obj in list" :key="obj.id"
    :obj="obj"
    @subprice="fn" @addFn='add'
    ></MyProduct>
  </div>
</template>

<script>

import MyProduct from './components/MyProduct_sub'
export default {
  data() {
    return {
      list: [
        {
          id: 1,
          name: "超级好吃的棒棒糖",
          price: 18.8,
        },
        {
          id: 2,
          name: "超级好吃的大鸡腿",
          price: 34.2,
        }
      ],
    };
  },
  components: {
    MyProduct
  },
  methods: {
    fn(id, price){
      // 降低价格
      this.list.forEach(item=>{
        if(item.id==id && item.price>1){
          item.price=(item.price-price).toFixed(2);
        }
      })
    },
    add(id,price){
      // 增加价格
      this.list.forEach(item=>{
        if(item.id==id){
          item.price=(item.price+price).toFixed(2);
        }
      })
    }
  }
};
</script>

跨组件通信-EventBus

image-20210416122123301.png

空的Vue对象, 只负责$on注册事件, $emit触发事件, 一定要确保$on先执行

根组件 src/App.vue

<template>
    <cat-house></cat-house>
    <dog-house></dog-house>
</template>

<script>
import CatHouse from "./components/CatHouse";// 子组件1
import DogHouse from "./components/DogHouse";// 子组件2
export default {
  components: {
    CatHouse,
    DogHouse,
  }
};
</script>

事件中心--src/EventBus/index,js

import Vue from 'vue'
// 导出空白vue对象
export default new Vue()

子组件1--src/components/CatHouse

<template>
        <div>CatHouse</div>
</template>

<script>
// 1. 引入空白vue对象(EventBus)
import eventBus from "../EventBus";

// 2. 接收方 - $on监听事件
export default {
    //vue组件的入口函数
    // 3. 组件创建完毕, 监听say事件
    created() {
        eventBus.$on("say", this.sayhi);
    });
    methods:{
        sayhi(name){
            console.log(name+'你好~~我是猫猫');
        }
    }
  },
};
</script>

子组件2--src/components/DogHouse

<template>
        <div>DogHouse</div>
        <button @click='sayhi'>打招呼</button>
</template>

<script>
// 1. 引入空白vue对象(EventBus)
import eventBus from "../EventBus";

// 2. 传值方 
export default {
    methods:{
        sayhi(){
            eventBus.emit('say','狗狗')
        }
    }
  },
};
</script>

VUE生命周期

Day03.png

阶段方法名描述场景
初始化(创建Vue实例)beforeCreatedata和methods初始化"之前"
created(vue的入口函数)data和methods初始化以后,添加了观察者(响应式编程)网络请求, 注册全局事件
挂载(视图渲染)beforeMount生成了虚拟dom,即在真实DOM挂载之前预处理data, 不会触发updated钩子函数
mounted根据虚拟dom解析了视图,即真实DOM挂载以后挂载后真实DOM
更新(视图重新渲染)beforeUpdate更新之前(前提: data数据改变才执行)
updated更新之后获取更新后的真实DOM
销毁beforeDestroy前提: v-if="false" 销毁Vue实例移除全局事件, 移除当前组件, 计时器, 定时器, eventBus移除事件$off方法
destroyed实例销毁后
<template>
  <div>
      <p>学习生命周期 - 看控制台打印</p>
      <p id="myP">{{ msg }}</p>
      <ul id="myUL">
          <li v-for="(val, index) in arr" :key="index">
              {{ val }}
          </li>
      </ul>
      <button @click="arr.push(1000)">点击末尾加值</button>
  </div>
</template>

<script>
export default {
    data(){
        return {
            msg: "hello, Vue",
            arr: [5, 8, 2, 1],
            timer: null // 保存计时器
        }
    },
    // 一. 初始化
    // new Vue()以后, vue内部给实例对象添加了一些属性和方法, data和methods初始化"之前"
    beforeCreate(){
        console.log("beforeCreate -- 执行");
        console.log(this.msg); // undefined
    },
    // data和methods初始化以后
    // 场景: 网络请求, 注册全局事件
    created(){
        console.log("created -- 执行");
        console.log(this.msg); // hello, Vue

        // this.timer = setInterval(() => {
        //     console.log("哈哈哈");
        // }, 1000)
    },

    // 二. 挂载
    // 真实DOM挂载之前
    // 场景: 预处理data, 不会触发updated钩子函数
    beforeMount(){
        console.log("beforeMount -- 执行");
        console.log(document.getElementById("myP")); // null

        this.msg = "重新值"
    },
    // 真实DOM挂载以后
    // 场景: 挂载后真实DOM
    mounted(){
        console.log("mounted -- 执行");
        console.log(document.getElementById("myP")); // p
    },

    // 三. 更新
    // 前提: data数据改变才执行
    // 更新之前
    beforeUpdate(){
        console.log("beforeUpdate -- 执行");
        console.log(document.querySelectorAll("#myUL>li")[4]); // undefined
    },
    // 更新之后
    // 场景: 获取更新后的真实DOM
    updated(){
        console.log("updated -- 执行");
        console.log(document.querySelectorAll("#myUL>li")[4]); // li
    },

    // 四. 销毁
    // 前提: v-if="false" 销毁Vue实例
    // 场景: 移除全局事件, 移除当前组件, 计时器, 定时器, eventBus移除事件$off方法
    beforeDestroy(){
        console.log('beforeDestroy -- 执行');
        clearInterval(this.timer)
    },
    destroyed(){
        console.log("destroyed -- 执行");
    }

}
</script>

AXIOS(基于promise封装)

axios文档

特点

  • 支持客户端发送Ajax请求
  • 支持服务端Node.js发送请求
  • 支持Promise相关用法
  • 支持请求和响应的拦截器功能
  • 自动转换JSON数据
  • axios 底层还是原生js实现, 内部通过Promise封装的

axios基本使用

下载并引入axios

yarn add axios

import axios from 'axios'
axios({
  method: '请求方式', // get post --默认就是GET方式请求, 可以省略不写
  url: '请求地址',
  data: {    // 拼接到请求体的参数(默认是`json字符串`格式),  post请求的参数
    xxx: xxx,
  },
  params: {  // 拼接到请求行的参数, get请求的参数,也可以直接拼接到url后面(?name=jean)
   	xxx: xxx 
  }
}).then(res => {
  console.log(res.data) // 后台返回的结果
}).catch(err => {
  console.log(err) // 后台报错返回
})

发布图书(解构)

<template>
  <div>
    <p>3. 新增图书信息</p>
    <div>
        <input type="text" placeholder="书名" v-model="bookObj.bookname">
    </div>
    <div>
        <input type="text" placeholder="作者" v-model="bookObj.author">
    </div>
    <div>
        <input type="text" placeholder="出版社" v-model="bookObj.publisher">
    </div>
    <button @click="sendFn">发布</button>
  </div>
</template>

<script>
import axios from "axios";
export default {
  data() {
    return {
      bName: "",
      bookObj: { // 参数名提前和后台的参数名对上-发送请求就不用再次对接了
          bookname: "",
          author: "",
          publisher: ""
      }
    };
  },
  methods: {
    // ...省略了其他代码
    sendFn(){
       axios({
           url: "/api/addbook",
           method: "POST",
           data: {
               appkey: "7250d3eb-18e1-41bc-8bb2-11483665535a",
               ...this.bookObj
            // 等同于下面
            // bookname: this.bookObj.bookname,
            // author: this.bookObj.author,
            // publisher: this.bookObj.publisher
           }
       }) 
    }
  },
};
</script>

全局配置

避免前缀基地址, 暴露在逻辑页面里, 统一设置

axios.defaults.baseURL = "http://123.57.109.30:3006"

// 所有请求的url前置可以去掉, 请求时, axios会自动拼接baseURL的地址在前面
getAllFn() {
    axios({
        url: "/api/getbooks",
        method: "GET", // 默认就是GET方式请求, 可以省略不写
    }).then((res) => {
        console.log(res);
    });
    // axios()-原地得到Promise对象
},

挂载到vue原型上(作为全局属性)

只要是 vue实例vue子类的实例对象,都可以通过 原型链 访问到$axios

截屏2021-08-04 下午8.58.24.png main.js

// 目标: 请求数据 - 打印
// 1. 下载axios库, main.js - 全局绑定属性 (确保任意.vue文件可以都访问到这个axios方法)
import axios from 'axios'


// 2. 基础地址
axios.defaults.baseURL = "https://www.escook.cn"

// 3. axios方法添加到Vue的原型上
Vue.prototype.$axios = axios

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

$NEXTTICK | $REFS

$refs 获取DOM / 组件对象

通过id / ref, 都可以获取原生DOM标签 this.#refs.

获取组件对象, 调用组件里方法(非主流方法)

父组件---More.vue

<template>
  <div>
      <p>1. 获取原生DOM元素</p>
      <h1 id="h" ref="myH">我是一个孤独可怜又能吃的h1</h1>
      <p>2. 获取组件对象 - 可调用组件内一切</p>
      <Demo ref="de"></Demo>
  </div>
</template>

<script>
// 目标: 获取组件对象
// 1. 创建组件/引入组件/注册组件/使用组件
// 2. 组件起别名ref
// 3. 恰当时机, 获取组件对象
import Demo from './Child/Demo'
export default {
    mounted(){
        console.log(document.querySelector("#h")); // h1
        console.log(document.getElementById("h")); // h1
        // 获取dom对象
        console.log(this.$refs.myH); // h1
        // 获取demo子组件对象
        let demoObj = this.$refs.de;
        // 调用demo子组件里的 fn方法
        demoObj.fn()
    },
    components: {
        Demo
    }
}
</script>

$nextTick

Vue更新DOM是-异步

目标: 点击count++, 马上通过"原生DOM"拿标签内容, 无法拿到新值

DOM更新完会挨个触发$nextTick里的函数体,在这里可以拿到异步更新的dom

除此之外,也可以在updated钩子里面拿到

<template>
  <div
      <p ref="myP">{{ count }}</p>
      <button @click="btn">点击count+1, 马上提取p标签内容</button>
  </div>
</template>

<script>
// 目标: 获取组件对象
// 1. 创建组件/引入组件/注册组件/使用组件
// 2. 组件起别名ref
// 3. 恰当时机, 获取组件对象
import Demo from './Child/Demo'
export default {
    components: {
        Demo
    },
    data(){
        return {
            count: 0
        }
    },
    methods: {
        btn(){
            this.count++; // vue监测数据更新, 开启一个DOM更新队列(异步任务)
            console.log(this.$refs.myP.innerHTML); // 0

            // 原因: Vue更新DOM异步
            // 解决: this.$nextTick()
            // 过程: DOM更新完会挨个触发$nextTick里的函数体
             this.$nextTick(() => {
                console.log(this.$refs.myP.innerHTML); // 1
            })
        }
    }
}
</script>

$nextTick使用场景

目标: 点击搜索按钮, 弹出聚焦的输入框, 按钮消失

<template>
  <div>
      <input ref="myInp" type="text" placeholder="这是一个输入框" v-if="isShow">
      <button v-else @click="btn">点击我进行搜索</button>
  </div>
</template>

<script>
// 目标: 点按钮(消失) - 输入框出现并聚焦
// 1. 获取到输入框
// 2. 输入框调用事件方法focus()达到聚焦行为
export default {
    data(){
        return {
            isShow: false
        }
    },
    methods: {
        async btn(){
            this.isShow = true;
            // this.$refs.myInp.focus()
            // 原因: data变化更新DOM是异步的
            // 输入框还没有挂载到真实DOM上
            // 解决:
            // this.$nextTick(() => {
            //     this.$refs.myInp.focus()
            // })
            // 扩展: await取代回调函数
            // $nextTick()原地返回Promise对象
            await this.$nextTick()
            this.$refs.myInp.focus()
        }
    }
}
</script>

注意 $nextTick是在 数据更新引发视图重绘后才执行,即钩子函数updated之后才执行;

举例说明:

  created() {
    this.loadArtcileInfo()
  },
  methods: {
    // 加载文章数据
    async loadArtcileInfo() {
      try {
        const {
          data: { data }
        } = await getArticleById(this.articleId)
        this.article = data

        // 拿到数据后,再去获取img DOM元素
        // 因为 loadArtcileInfo是在 created后执行的,此时还没有生成真实dom,
        // 这里,也就不存在数据更新,所以就不能通过 this.$nextTick()来拿到 真实的dom元素

        // 所以 需要设置10ms 延迟一下,再拿到真实的dom
        setTimeout(() => {
          this.previewImg()
        }, 10)
        
        
      } catch (err) {
        ...
      }
      // 加载完成
      this.isLoading = false
    },
    // 从文章内容中获取到所有的 img DOM 节点
    previewImg() {
      // 拿到 文章内容的 的标签
      const contentEl = this.$refs.contentRef
      ...省略其他代码
    },

组件name属性使用

目标: 可以用组件的name属性值, 来注册组件名字

封装的组件-可以自己定义name属性组件名-有个统一的前缀风格

子组件--components/Com.vue

<template>
  <div>
      <p>我是一个Com组件</p>
  </div>
</template>

<script>
export default {
    name: "ComNameHaHa" // 注册时可以定义自己的名字
}
</script>

父组件--App.vue

<template>
  <div>
    <ComNameHaHa></ComNameHaHa>
  </div>
</template>

<script>
import Com from './components/Com'
export default {
  components: {
    [Com.name]: Com // 对象里的key是变量的话[]属性名表达式
    // "ComNameHaHa": Com
  }
}
</script>

组件进阶

动态组件

多个组件使用同一个挂载点,并动态切换,这就是动态组件

实现:vue内置component组件, 配合is属性, 设置要显示的组件名字<component :is="变量"></component>

  <div>
      <button @click="comName = 'UserName'">账号密码填写</button>
      <button @click="comName = 'UserInfo'">个人信息填写</button>
      
      <p>下面显示注册组件-动态切换:</p>
      <div style="border: 1px solid red;">
          <component :is="comName"></component>
      </div>
  </div>
</template>

<script>
// 目标: 动态组件 - 切换组件显示
// 场景: 同一个挂载点要切换 不同组件 显示
// 1. 创建要被切换的组件 - 标签+样式
// 2. 引入到要展示的vue文件内, 注册
// 3. 变量-承载要显示的组件名
// 4. 设置挂载点<component :is="变量"></component>
// 5. 点击按钮-切换comName的值为要显示的组件名

import UserName from '../components/01/UserName'
import UserInfo from '../components/01/UserInfo'
export default {
    data(){
        return {
            // 组件的名字
            comName: "UserName"
        }
    },
    components: {
        UserName,
        UserInfo
    }
}
</script>

组件缓存

问题:组件切换会导致组件被频繁销毁和重新创建, 性能不高

解决:使用Vue内置的keep-alive组件, 可以让包裹的组件保存在内存中不再创建和销毁,而是触发**激活(activated)和非激活(deactivated)**的生命周期钩子函数

<div style="border: 1px solid red;">
    <!-- Vue内置keep-alive组件, 把包起来的组件缓存起来 -->
    <keep-alive>
        <component :is="comName"></component>
    </keep-alive>
</div>

组件插槽

用于实现组件的内容分发, 通过 slot 标签, 可以接收到写在组件标签内的内容

使用插槽 实现 父组件给子组件 传 视图标签

  1. 组件内用<slot>默认内容</slot>占位
    • 如果父组件传递了 内容,就不会现在slot内的默认内容
    • 如果没有传递,就会展示 默认内容
  2. 使用组件时<Pannel></Pannel>夹着的地方, 传入标签替换slot

截屏2021-08-06 上午10.39.54.png

具名插槽

当一个组件内有2处以上需要外部传入标签的地方

解决:

  1. slot 用name属性 区分名字<slot name="title"></slot>
  2. template 配合 v-slot:名字(v-slot:---#) 来分发对应标签<template #title>...</template>
# 子组件---components/Pannel.vue
<slot name="title"></slot>
<slot name="content"></slot>

# 父组件--UseSlot.vue
<template v-slot:title>
  <h4>芙蓉楼送辛渐</h4>
</template>

// v-slot可以简化成#使用
<template #title>
  <span style="color: red;">我是标题</span>
</template>

作用域插槽

让子组件里的值,能给插槽赋值时在父组件环境下使用

  1. 子组件, 在slot上绑定属性和子组件内的值
  2. 使用组件, 传入自定义标签, 用template和v-slot="自定义变量名"
  3. scope变量名自动绑定slot上所有属性和值

截屏2021-08-06 上午11.14.58.png

# 父组件
<Pannel>
     <!-- 需求: 插槽时, 使用组件内变量 -->
     <!-- scope变量: {row: defaultObj} -->
     <template #header="scope">
         <p>{{ scope.msg }}</p>
         <p>{{ scope.row.defaultTwo }}</p>
     </template>
</Pannel>

# 子组件 --components/Pannel.vue
<slot :row="defaultObj" :msg="msg" name='header'>{{ defaultObj.defaultOne }}</slot>

data() {
  return {
    msg: '看看传过去没',
    defaultObj: {
      defaultOne: "无名氏",
      defaultTwo: "小传同学"
    }
  };

表格案例

子组件--MyTable.vue

<template>
  <table class="table table-bordered table-stripped">
    <!-- 表格标题区域 -->
    <thead>
      <tr>
        <slot name="header"></slot>
        
        <!-- <th>#</th>
        <th>商品名称</th>
        <th>价格</th>
        <th>标签</th>
        <th>操作</th> -->
      </tr>
    </thead>
    <!-- 表格主体区域 -->
    <tbody>
      <tr v-for="obj in arr"
      :key="obj.id"
      >
        <slot name="body" :row="obj"></slot>
        
        <!-- <td>{{ obj.id }}</td>
        <td>{{ obj.goods_name }}</td>
        <td>{{ obj.goods_price }}</td>
        <td>{{ obj.tags }}</td>
        <td>
            <button class="btn btn-danger btn-sm">删除</button>
        </td> -->
        
      </tr>
    </tbody>
  </table>
</template>

<script>
export default {
  name: 'MyTable',
  props: {
      arr: Array
  }
}
</script>

父组件 GoodList.vue

<template>
  <div>
    <MyTable :arr="list">
      <template #header>
        <th>#</th>
        <th>商品名称</th>
        <th>价格</th>
        <th>标签</th>
        <th>操作</th>
      </template>
      <!-- scope的值: {row: obj} -->
      <template #body="scope">
        <td>{{ scope.row.id }}</td>
        <td>{{ scope.row.goods_name }}</td>
        <td>{{ scope.row.goods_price }}</td>
        <td>
          <button class="btn btn-danger btn-sm" @click="removeBtn(scope.row.id)">删除</button>
        </td>
      </template>
    </MyTable>
  </div>
</template>

<script>
import MyTable from '../components/MyTable'

export default {
  components: {
    MyTable
  },
  data() {
    return {
      list: [
        {
          id: 1,
          goods_name: 'vivo',
          goods_price: '2999',
          tags: '手机'
        },
        {
          id: 2,
          goods_name: 'iphone',
          goods_price: '7999',
          tags: '手机'
        }
      ] // 所有数据
    }
  },
  methods: {
    removeBtn(id) {
      let index = this.list.findIndex(obj => obj.id === id)
      this.list.splice(index, 1)
    }
  }
}
</script>

前端路由

作用: 在一个页面里, 通过(#锚点)切换业务场景,不经过服务器 整体不刷新页面,用户体验更好

说明:地址栏中,# 后面的锚点 就是 hash值,通过修改hash值window.location.hash就可以定位到不同的views页面,同时hash值发生改变,会触发hashchange事件

总结:一切都围绕着hash值变化为准

.vue文件分2类, 一个是页面组件, 一个是复用组件

  • 页面组件(src/views(或pages)) - 页面展示 - 配合路由用

  • 复用组件(src/components) - 展示数据/常用于复用

知识点小结

router|route|routes

  • $router是指整个路由实例,你可以操控整个路由,通过'$router.push'往其中添加任意的路由对象.
  • $route:是指当前路由实例('$router')跳转到的路由对象;
  • 路由实例可以包含多个路由对象.它们是父子包含关系.
  • routes:指router路由实例的routes API.用来配置多个route路由对象.

截屏2021-08-09 下午2.31.38.png

vue-router初步使用

安装 shell

yarn add vue-router

main.js

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

// 引入 页面子组件
import Find from '@/views/Find' // @是src的绝对地址
import My from '@/views/My'
import Part from '@/views/Part'

// 1.导入路由模块
import VueRouter from 'vue-router'
// 2.在vue中,使用使用vue的插件,都需要调用Vue.use()
Vue.use(VueRouter)
// 3.创建路由规则数组
const routes = [
  {
    path: "/find",
    component: Find
  },
  {
    path: "/my",
    component: My
  },
  {
    path: "/part",
    component: Part
  }
]
// 4.创建路由对象 - 传入规则
const router = new VueRouter({
  routes
})
// 5.路由对象注入到vue实例中, this可以访问$route和$router
new Vue({
  router,
  render: h => h(App),
}).$mount('#app')

App.vue

<template>
  <div>
    <div class="footer_wrap">
      <a href="#/find">发现音乐</a>
      <a href="#/my">我的音乐</a>
      <a href="#/part">朋友</a>
    </div>
    <div class="top">
      <!-- 6. 设置挂载点-当url的hash值路径切换, 显示规则里对应的组件到这 -->
      <router-view></router-view>
    </div>
  </div>
</template>

截屏2021-08-07 下午2.12.39.png

声明式导航

用全局组件router-link来替代a标签

  • router-link实质上最终会渲染成a链接 to属性等价于提供 href属性(to无需#)
  • router-link提供了声明式导航高亮的功能(自带类名)
<template>
  <div>
    <div class="footer_wrap">
      <router-link to="/find">发现音乐</router-link>
      <router-link to="/my">我的音乐</router-link>
      <router-link to="/part">朋友</router-link>
    </div>
    <div class="top">
      <router-view></router-view>
    </div>
  </div>
</template>

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

<style scoped>
/* .router-link-active---提供的自带类名 */
.footer_wrap .router-link-active{
  color: white;
  background: black;
}
</style>

跳转传参

在跳转路由时, 可以给路由对应的组件内传值

  • ?key=value ----- 用$route.query.key 取值

  • /值 提前在路由规则/path/:key ----- 用$route.params.key 取值

在router-link上的to属性传值, 语法格式如下

  • /path?参数名=值
  • /path/值 – 需要路由对象提前配置 path: “/path/:参数名”

对应页面组件接收传递过来的值

  • $route.query.参数名
  • $route.params.参数名 截屏2021-08-07 下午3.01.21.png
  1. components/Part.vue - 准备接收路由上传递的参数和值

    <template>
      <div>
          <p>关注明星</p>
          <p>发现精彩</p>
          <p>寻找伙伴</p>
          <p>加入我们</p>
          <p>人名: {{ $route.query.name }} -- {{ $route.params.username }}</p>
      </div>
    </template>
    
  2. 路由定义

    {
        path: "/part",
        component: Part
      },
      {
        path: "/part/:username", // 有:的路径代表要接收具体的值(动态路由)
        component: Part
      },
    
  3. 导航跳转, 传值给MyGoods.vue组件

    <router-link to="/part?name=小传">朋友-小传</router-link>
    <router-link to="/part/小智">朋友-小智</router-link>
    

重定向和模式

重定向

匹配path后, 强制切换到目标path上

// 网页默认打开, 匹配路由"/", 强制切换到"/find"上
const routes = [
  {
    path: "/", // 默认hash值路径
    redirect: "/find" // 重定向到/find
    // 浏览器url中#后的路径被改变成/find-重新匹配数组规则
  }
]

404页面

问题:如果路由hash值, 没有和数组里规则匹配

解决:在路由最后,path匹配*(任意路径) –匹配到默认给的404页面

import NotFound from '@/views/NotFound'

const routes = [
  // ...省略了其他配置
  // 404在最后(规则是从前往后逐个比较path)
  {
    path: "*",
    component: NotFound
  }
]

模式设置

修改路由在地址栏的模式

hash路由例如: http://localhost:8080/#/home

history路由例如: http://localhost:8080/home (以后上线需要服务器端支持, 否则找的是文件夹)

模式文档

router/index.js

const router = new VueRouter({
  routes,
  mode: "history" // 打包上线后需要后台支持, 模式是hash
})

编程式导航

用JS代码来进行跳转, 声明式导航用a标签

不同的使用场景:

  • name
    • 方便修改, 在页面上看不见随便定义
    • 虽然用name跳转, 但是url的hash值还是切换path路径值
  • path
    • 可以在url的hash值看到(尽量符合组内规范)
this.$router.push({
    path: "路由路径", // 都去 router/index.js定义
    name: "路由名"
})

跳转传参

注意: 使用path会自动忽略params

推荐: name+query方式传参

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

// 对应路由接收   $route.params.参数名   取值
// 对应路由接收   $route.query.参数名    取值

路由嵌套

image-20210116162013983.png

  • App.vue, 外层的router-view负责发现音乐和我的音乐页面切换
  • Find.vue 内层的router-view负责发现音乐下的子tab对应的组件切换 截屏2021-08-07 下午4.45.50.png
  1. 配置二级导航和样式- 在Find.vue中
<template>
  <div>
    <!-- <p>推荐</p>
    <p>排行榜</p>
    <p>歌单</p> -->
    <div class="nav_main">
      <router-link to="/find/recommend">推荐</router-link>
      <router-link to="/find/ranking">排行榜</router-link>
      <router-link to="/find/songlist">歌单</router-link>
    </div>

    <div style="1px solid red;">
      <router-view></router-view>
    </div>
  </div>
</template>

<script>
export default {};
</script>
  1. 配置路由规则-二级路由展示
const routes = [
  // ...省略其他
  {
    path: "/find",
    name: "Find",
    component: Find,
    redirect:'/find/recommend',// 定向到/find/recommend路径 http://localhost:8080/#/find/recommend
    children: [
      {
        //此时path等同于'/find/recommend',子路由会继承父路由的路径.但是不能写成path:'/recommend'.因为以 / 开头的嵌套路径会被当作根路径,也就是说此时recommend成了根路径.而不是find.
        path: "recommend",
        component: Recommend
        
      },
      {
        path: "ranking",
        component: Ranking
      },
      {
        path: "songlist",
        component: SongList
      }
    ]
  }
  // ...省略其他
]

模式一:子组件的path不加 '/'

截屏2021-08-16 下午2.12.04.png

模式二:子组件的path加 '/'

截屏2021-08-16 下午2.17.10.png

声明导航 - 类名区别

  • router-link-exact-active (精确匹配) url中hash值路径, 与href属性值完全相同, 设置此类名
  • router-link-active (模糊匹配) url中hash值, 包含href属性值这个路径

image-20210512102829250.png

守卫

全局前置守卫

路由跳转之前, 先执行一次前置守卫函数, 判断是否可以正常跳转

场景: 当你要对路由权限判断时,例判断用户是否登陆

// 语法: router.beforeEach((to, from, next)=>{//路由跳转"之前"先执行这里, 决定是否跳转})
// 参数1: 要跳转到的路由 (路由对象信息)    目标
// 参数2: 从哪里跳转的路由 (路由对象信息)  来源
// 参数3: 函数体 - next()才会让路由正常的跳转切换, next(false)在原地停留, next("强制修改到另一个路由路径上")
// 注意: 如果不调用next, 页面留在原地
// next()放行, 
// next(false)留在原地不跳转路由
// next(path路径)强制换成对应path路径跳转

// 例子: 判断用户是否登录, 是否决定去"我的音乐"/my
const isLogin = true; // 登录状态(未登录)
router.beforeEach((to, from, next) => {
  if (to.path === "/my" && isLogin === false) {
    alert("请登录")
    next(false) // 阻止路由跳转
  } else {
    next() // 正常放行
  }
})

VANT组件库

vant官网