【Vue-组件】学习笔记

目录

<<回到导览

组件

1.项目

1.1.Vue Cli

官方提供的一个全局命令工具,可以快速生成vue项目的标准化基础架子(集成webpack),开箱即用

使用步骤:

  1. 全局安装(安装一次即可)

    bash 复制代码
    yarn global add @vue/cli
     // 或者
    npm i @vue/cli -g
  2. 查看vue/cli版本:

    bash 复制代码
    vue --version
  3. 创建项目架子:

    bash 复制代码
    vue create project-name
  4. 启动项目:

    bash 复制代码
     yarn serve
     // 或者
     npm run serve

启动项目的命令并不固定,其取决于package.json文件

1.2.项目目录

  • 第三包文件夹(依赖)被删除,只要package.json还存在,就可以安装回来

    npm i --force 或者 npm i --legacy-peer-deps

1.3.运行流程

1.4.组件的组成

  • 语法高亮插件
  • 三部分构成

    • template :结构 (有且只能一个根元素),在template中,最外层标签只能为div盒子
    • script: js逻辑
    • style: 样式 (可支持less,需要装包)
  • 让组件支持less

    1. style标签,lang="less" 开启less功能

    2. 装包:

      bash 复制代码
      yarn add less less-loader -D 
      // 或者
      npm i less less-loader -D

1.5.注意事项

  1. 在组件的template中,最外面必须有一个div盒子

  2. 组件名称最好采用大驼峰命名法,且至少两个驼峰

  3. 有时候有些错误改正后依旧报错(列如上面第二个报错),重新启动项目即可

  4. 启动项目时在项目的根目录下启动(我们项目根目录叫luyou)

2.组件

2.1.组件注册

  • 局部注册

    1. 在components文件夹创建组件 xxx.vue

    2. 在根组件App.vue导入

      js 复制代码
      // 导入组件
      // import xxx from 'xxx.vue的文件路径'
      import HmHeader from './components/HmHeader'
      
      export default{
          // 局部注册
          components:{
              // '组件名':组件对象,
              // HmHeader:HmHeader  同名可简写
              HmHeader
          }
      }
  • 全局注册

    1. 在components文件夹创建组件 xxx.vue

    2. 在main.js进行全局注册

      js 复制代码
      // 导入组件
      // import xxx from 'xxx.vue的文件路径'
      import HmHeader from './components/HmHeader'
      
      // 调用Vue.component进行全局注册
      // Vue.component('组件名',组件对象)
      Vue.component('HmHeader',HmHeader)

    一般都用局部注册,如果发现是通用组件,再注册为全局

2.2.scoped样式冲突

  • 默认组件中的样式会全局生效

  • 可以给组件加上scoped属性,可以让样式只作用于当前组件

    html 复制代码
    <style scoped></style>

scoped的原理:

  1. 组件被都添加 data-v-hash (添加自定义属性)
  2. css选择器添加 [data-v-hash] (添加自定义属性的属性选择器)

2.3.data是一个函数

在最开始的vue基本语法学习中,data被写为data: {},键值(值为对象,对象又嵌套键值数据)

但在组件开发中,data选项必须是一个函数

js 复制代码
data(){
    return {
        // 数据
    }
}
  • 这是因为组件具有复用性,在同一个组件的复用过程中,data里的数据应该不一样,且数据不相互影响

  • 将data设置为一个函数,将数据写进return返回值中,每次创建组件都会调用data函数,返回一个独立数据对象,这些独立的对象分别用于保存同一组件的不同次使用的数据。

2.4.props详解

  • 定义 :在组件使用时,注册的一些自定义属性

  • 作用:向子组件传递数据

  • 示例

    js 复制代码
    export default {
      props: ['w'],
    }

    在为子组件传递数据时,我们应该为组件的 prop 指定验证要求

    js 复制代码
    props: {
      校验的属性名: {
        type: 类型,  // Number String Boolean ...
        required: true, // 是否必填
        default: 默认值, // 默认值
        validator (value) {
          // 自定义校验逻辑
          return 是否通过校验
        }
      }
    },
    • default和required一般不同时写
    • default后面如果是复杂类型,则需要以函数的形式return一个默认值

    示例

    js 复制代码
    props: {
        w: {
          type: Number,
          //required: true,
          default: 0,
          validator(val) {
            if (val >= 100 || val <= 0) {
              console.error('传入的范围必须是0-100之间')
              return false
            } else {
              return true
            }
          },
        },
      },

2.5.data和prop的区别

  • data 的数据是自己的 → 随便改
  • prop 的数据是外部 的 → 不能直接改,要遵循 单向数据流

单向数据流:父级props 的数据更新,会向下流动,影响子组件。这个数据流动是单向的

3.组件通信

上面讲到,data是一个函数,同一组件的不同的使用之间数据独立,同样,不同组件之间数据也不共享

3.1.父子通信

3.1.1.父传子(props)
  • 父组件通过 props 将数据传递给子组件

    1. 在父组件中的子组件标签添加动态属性(App.vue)

      html 复制代码
      <MySon :title="say"></MySon>
    2. 子组件通过props进行接收

      js 复制代码
      export default {
        data() {
          return {};
        },
        props: ["title"],
      };
3.1.2.子传父($emit)
  • 子组件利用 $emit 通知父组件修改更新

    1. 给父组件元素添加事件和处理方法

      html 复制代码
      <button @click="changeFn">say</button>
      js 复制代码
      changeFn() {
          // change为后面为子组件添加的事件名
          // "Hello"为要修改的值,也就是后面的形参newSay
          // 并不一定要传字符串,也可以将"Hello"改为变量传递
          this.$emit("change", "Hello");
      },
    2. 触发事件后,利用$emit将父组件发送改变数据的通知

    3. 在父组件中的子组件标签添加事件,调用修改属性的方法

      html 复制代码
      <MySon :title="say" @change="changeFn"></MySon>
      js 复制代码
      // 形参newSay为传递过来的值"Hello"
      changeFn(newSay) {
          this.say = newSay;
      },

3.2.非父子通信

3.2.1.事件总线

概念:创建创建一个都能访问的事件总线,发送方向事件总线发送,

接收方向事件总线发送监听

  1. 在src文件夹下创建一个utils文件夹,在该文件夹下创建一个js文件

  2. 创建一个空Vue实例(事件总线)

    js 复制代码
    import Vue from 'vue'
    const Bus = new Vue()
    export default Bus
  3. 接受方和发送方都将数据总线引入

    js 复制代码
    import Bus from '../utils/EventBus'
  4. A组件(发送方),触发Bus的$emit事件

    js 复制代码
    // 'sendMsg为发送的数据的标识
    Bus.$emit('sendMsg', '这是一个消息')
  5. B组件(接受方),监听Bus的 $on事件

    js 复制代码
    created () {
      // 形参msg为传递的信息('这是一个消息')
      Bus.$on('sendMsg', (msg) => {
        this.msg = msg
      })
    }
3.2.2.provide-inject

作用 :跨层级共享数据

  1. 父组件 provide提供数据

    js 复制代码
    export default {
      provide () {
        return {
           // 普通类型【非响应式】
           color: this.color, 
           // 复杂类型【响应式】
           userInfo: this.userInfo, 
        }
      }
    }
  2. .子/孙组件 inject获取数据

    js 复制代码
    export default {
      inject: ['color','userInfo'],
      created () {
        console.log(this.color, this.userInfo)
      }
    }
  • provide提供的简单类型的数据不是响应式的,复杂类型数据是响应式。(推荐提供复杂类型数据
  • 子/孙组件通过inject获取的数据,不能在自身组件内修改

3.3.v-model详解

  • v-model本质上是一个语法糖。例如应用在输入框上,就是value属性 和 input事件 的合写

    html 复制代码
    <template>
      <div id="app" >
        <input v-model="msg" type="text">
        <!-- 等价于 -->
        <!-- 数据变,视图跟着变 :value -->
        <!-- 视图变,数据跟着变 @input -->
        <input :value="msg" @input="msg = $event.target.value" type="text">
      </div>
    </template>
  • $event 用于在模板中,获取事件的形参

  • $event.target.value即获取输入框的值

  • 将输入框的值赋值给data里的数据msg,再将msg通过:value赋值给输入框,从而实现数据的双向绑定。

注意

  1. 不同的表单元素, v-model在底层的处理机制是不一样的。

    比如给checkbox使用v-model,底层处理的是:checked属性和@change事件。

  2. v-model不能直接绑定父组件传过来的数据,因为父组件通过prop属性传过来的数据不能直接修改。

3.4.sync修饰符(已废弃)

  • 在上面的父子通信中,子组件不能直接修改父组件数据,而是通过$emit间接修改

  • 通过sync修饰符,子组件可以修改父组件传过来的props值

  • 以上面的父子通信为例

    父组件

    html 复制代码
    <!-- 完整写法 -->
    <MySon :title="say"  @update:title="isShow = $event" ></MySon>
    
    <!-- 简写 -->
    <MySon :title.sync="say"></MySon>

    子组件

    js 复制代码
    props: {
      title: Boolean
    },
    this.$emit('update:title', 'Hello')

3.5.(重点)ref和$refs

  • 利用ref 和 $refs 可以用于 获取 dom 元素组件实例(要在dom渲染完后才能获取)

  • 获取元素还可以通过document.querySeletctor()获取,但是此方法是从整个页面开始获取

    • 类名问题:比如我们在子组件中获取类名为app的元素,如果在此组件之前还有类名为app的元素,则会获取之前的类名为app的元素
    • 解决方法 :如果我们利用ref 和 $refs 可以用于 获取 dom 元素,则会从当前组件开始获取,避免类名问题。
  • 示例

    1. 给要获取的盒子添加ref属性

      html 复制代码
      <div ref="demo">我是渲染图表的容器</div>
    2. 通过 this.$refs.demo 获取

      js 复制代码
      mounted(){
        console.log(this.$refs.demo)
      }

3.6.ref和$refs选择器

  • 利用ref 和 $refs 还可以用于 组件实例

    1. 在组件示例上,添加ref属性

      html 复制代码
      <MySon ref="demo" ></MySon>
    2. 通过 this.$refs.demo 获取,控制台会打印组件对象

      js 复制代码
      mounted(){
        console.log(this.$refs.demo)
        // 控制台
        // VueComponent {_uid: 2, _isVue: true, __v_skip: true, _scope: EffectScope, $options: {...}, ...}
      }
    3. 我们可以通过this.$refs.demo.子组件方法名调用子组件方法,或者访问子组件属性,实现组件通信。

      注意:这种组件通信只是调用了子组件方法或者访问子组件属性,没有实现双向绑定

3.7.$nextTick

  • $nextTick用于实现vue的异步更新

  • 应用案例:编辑标题, 编辑框自动聚焦

    1. 效果:点击编辑,显示编辑框,并且让编辑框,立刻获取焦点

    2. 预想代码

      js 复制代码
      this.isShowEdit = true		// 显示输入框
      this.$ref.input.focus()		// 获取焦点
    3. 预想代码并不能实现该效果,因为:

      • 当执行显示输入框代码后,没等dom渲染出显示框,就执行到获取焦点的代码了(因为dom是异步更新),我们可以用$nextTick用于实现vue的异步更新。
      • vue异步更新的原因是提高性能,当点击编辑按钮的@click绑定的事件执行完毕后,才会更新视图,这样避免每执行完一行代码就更新一次视图。
    4. 利用$nextTick实现以上代码的异步更新

      js 复制代码
      this.isShowEdit = true		// 显示输入框
      this.$nextTick(() => {
        this.$refs.inp.focus()	// 获取焦点
      })
      • 当执行到$nextTick时,会更新渲染一次dom,输入框渲染出来,再向下执行代码
      • 说到dom异步更新,很多人想到利用延时器实现,但是延时器延迟时间是固定的,而执行到dom渲染的时间并不固定(和网络、设备性能有关)。

    注意:

    ​ $nextTick 内的函数体 一定是箭头函数,这样才能让函数内部的this指向Vue实例

4.自定义指令

  • 与$nextTick类似,inserted会在指令所在元素被插入页面时触发
  • 全局注册(下面以注册实现上面点击聚焦功能为例)

    js 复制代码
    //在main.js中
    Vue.directive('focus', {
      // el为指令绑定的元素 
      "inserted" (el) {
        el.focus()
      }
    })
  • 局部注册

    js 复制代码
    //在Vue组件的配置项中
    directives: {
      "focus": {
        inserted (el) {
          el.focus()
        }
      }
    }
  • 指令使用

    html 复制代码
    <input v-focus ref="inp" type="text">

4.1.指令的值

与vue内置指令相同,我们也可以为自定义指令设置值

  • 示例:我们定义一个v-color指令,v-color的值变化时,带有v-color指令的标签也会变化

  • 指令注册(局部注册)

    js 复制代码
    directives: {
      color: {
        inserted (el, binding) {
          el.style.color = binding.value
        },
        update (el, binding) {
          el.style.color = binding.value
        }
      }
    }
    1. binding.value为指令值
    2. 指令值修改会触发update函数(update也是一个生命周期钩子)
    3. 我们指令的值可以设置为变量,这时我们就需要设置update函数

4.2.封装v-loading指令

  • 在加载时,页面通常会有一个loading动画效果

  • 这个效果通常为一个蒙层(蒙层一般为伪元素)

  • loading的开启和关闭只需要添加移除类即可(因为伪元素是css生成的,自然也可以通过操作css移除)

    示例

    html 复制代码
    <template>
      <div class="main">
        <div class="box" v-loading = "isLoading">
          <ul>
            <li v-for="item in list" :key="item.id" class="news">
              <div class="left">
                <div class="title">{{ item.title }}</div>
                <div class="info">
                  <span>{{ item.source }}</span>
                  <span>{{ item.time }}</span>
                </div>
              </div>
              <div class="right">
                <img :src="item.img" alt="">
              </div>
            </li>
          </ul>
        </div>
      </div>
    </template>
    
    <script>
        import axios from 'axios'
    
        export default {
          data () {
            return {
              list: [],
              isLoading: true
            }
          },
          async created () {
            const res = await axios.get('http://hmajax.itheima.net/api/news')
            setTimeout(() => {
              this.list = res.data.data
              // 移除蒙层
              this.isloading = false
            }, 2000)
          },
          // 定义指令
          directives: {
              loading: {
                inserted (el, binding) {
                  binding.value?el.classList.add('loading'):el.classList.remove('loading')
                },
                update (el, binding) {
                  binding.value?el.classList.add('loading'):el.classList.remove('loading')
                }
              }
          }
        }
    </script>
    
    <style>
        /* 伪元素 */
        .loading:before {
          content: '';
          position: absolute;
          left: 0;
          top: 0;
          width: 100%;
          height: 100%;
          background: #fff url('./loading.gif') no-repeat center;
        }
        .box2 {
          width: 400px;
          height: 400px;
          border: 2px solid #000;
          position: relative;
        }
        .box {
          width: 800px;
          min-height: 500px;
          border: 3px solid orange;
          border-radius: 5px;
          position: relative;
        }
        .news {
          display: flex;
          height: 120px;
          width: 600px;
          margin: 0 auto;
          padding: 20px 0;
          cursor: pointer;
        }
        .news .left {
          flex: 1;
          display: flex;
          flex-direction: column;
          justify-content: space-between;
          padding-right: 10px;
        }
        .news .left .title {
          font-size: 20px;
        }
        .news .left .info {
          color: #999999;
        }
        .news .left .info span {
          margin-right: 20px;
        }
        .news .right {
          width: 160px;
          height: 120px;
        }
        .news .right img {
          width: 100%;
          height: 100%;
          object-fit: cover;
        }
    </style>

    知识点:

    • 安装axios yarn add axios npm i axios

    • 定义指令部分:

      js 复制代码
      directives: {
          loading: {
              inserted (el, binding) {
                  // 如果加载,显示蒙层(移除loading类),否则,显示蒙层
                  binding.value?el.classList.add('loading'):el.classList.remove('loading')
              },
              update (el, binding) {
                  binding.value?el.classList.add('loading'):el.classList.remove('loading')
              }
          }
      }
  • v-loading工作流程:有些同学看到这里可能有些疑问,组件定义里面的函数是什么时候执行的?

    1. 上面有提到,inserted会在指令所在元素被插入页面时触发
    2. 在上面示例中,先执行created生命周期钩子,向服务器发送获取数据请求,然后再将返回数据更新到 list 中,并改变isLoading

5.插槽

5.1默认插槽

  • 让组件内部的一些 结构 支持 自定义

  • 语法

    1. 组件内需要定制的结构部分,改用<slot></slot>占位
    2. 给插槽传入内容时,可以传入纯文本、html标签、组件
    3. 在标签内部, 传入结构替换slot
  • 示例:弹框插槽

5.2.后备内容

上面示例中,如果不传内容,则会不会显示

我们可以为插槽设置默认显示内容,如果不传内容,则会显示默认显示内容

  • 我们只需要在封装组件时,为预留的 <slot> 插槽提供后备内容即可(默认内容)

5.3.具名插槽

一个组件中,很多时候不单单只有一个插槽,这时我们需要使用name属性区分不同插槽。

  • 使用name属性区分不同插槽。

  • template配合v-slot:名字来分发对应标签

  • 为方便书写,上面可以将 v-slot: 替换为 #

示例

  1. 根组件(App.vue)

    html 复制代码
    <div id="app">
        <HelloWorld>
          <template #age>年龄</template>
          <template #name>姓名</template>
          <template #gender>性别</template>
        </HelloWorld>
    </div>
  2. 子组件(HelloWorld.vue)

    html 复制代码
    <div>
    	<slot name="name"></slot>
        <hr />
        <slot name="age"></slot>
        <hr />
        <slot name="gender"></slot>
    </div>

    显示顺序取决于子组件的插槽位置

5.4.作用域插槽

  • 作用域插槽不属于插槽的一种分类

  • 定义slot插槽的同时,可以传值,给插槽绑定数据,这些数据与插槽绑定的组件也可以使用

    1. 给 slot 标签, 以 添加属性的方式传值(组件中)

      html 复制代码
      <slot :id="item.id" msg="测试文本"></slot>
    2. 在template中, 通过 #插槽名= "obj" 接收,默认插槽名为 default(根组件中)

      html 复制代码
      <MyTable :list="list">
        <template #default="obj">
          <button @click="del(obj.id)">删除</button>
        </template>
      </MyTable>
  • 所有添加的属性, 都会被收集到一个对象中,我们也可以将这个对象结构来使用

    html 复制代码
    <MyTable :list="list">
      <template #default="{id, msg}">
        <button @click="del(id)">删除</button>
      </template>
    </MyTable>

6.my-tag组件封装

实现功能

  1. 双击显示,自动聚焦
  2. 失去焦点,隐藏输入框
  3. 回显标签内容
  4. 内容修改,回车,修改标签信息

代码

  1. MyTag

    html 复制代码
    <template>
      <div class="my-tag">
        <input
          v-if="isEdit"	
          v-focus
          class="input"
          type="text"
          placeholder="输入标签"
          :value="value"
          @blur="isEdit = false"
          @keyup.enter="handleEnter"
        />
        <div 
          v-else
          @dblclick="handleClick"
          class="text">
          {{ value }}
        </div>
      </div>
    </template>
    
    <script>
    export default {
      props: {
        value: String
      },
      data () {
        return {
          isEdit: false
        }
      },
      methods: {
        handleClick () {
          // 双击后,切换到显示状态 (Vue是异步dom更新)
          this.isEdit = true
        },
        handleEnter (e) {
          // 非空处理
          if (e.target.value.trim() === '') return alert('标签内容不能为空')
          // 由于父组件是v-model,触发事件,需要触发 input 事件
          this.$emit('input', e.target.value)
          // 提交完成,关闭输入状态
          this.isEdit = false
        }
      }
    }
    </script>
    
    <style lang="less" scoped>
    .my-tag {
      cursor: pointer;
      .input {
        appearance: none;
        outline: none;
        border: 1px solid #ccc;
        width: 100px;
        height: 40px;
        box-sizing: border-box;
        padding: 10px;
        color: #666;
        &::placeholder {
          color: #666;
        }
      }
    }
    </style>
  2. App.vue

    html 复制代码
    <template>
      <div>
           <MyTag v-model="item.tag"></MyTag>     
      </div>
    </template>
    
    <script>
    import MyTag from './components/MyTag.vue'
        
    export default {
      name: 'TableCase',
      components: {
        MyTag,
      },
      data () {
        return {
          // 测试组件功能的临时数据
          tempText: '水杯',    
        }
      }
    }
    </script>
    
    <style lang="less" scoped>
    .table-case {
      width: 1000px;
      margin: 50px auto;
      img {
        width: 100px;
        height: 100px;
        object-fit: contain;
        vertical-align: middle;
      }
    }
    
    </style>
  3. main.js

    js 复制代码
    import Vue from 'vue'
    import App from './App.vue'
    Vue.config.productionTip = false
    
    // 封装全局指令 focus
    Vue.directive('focus', {
      // 指令所在的dom元素,被插入到页面中时触发
      inserted (el) {
        el.focus()
      }
    })
    
    new Vue({
      render: h => h(App),
    }).$mount('#app')

知识点

  • 双击触发事件:@dblclick

  • 两种自动聚焦方法

    在实现点击盒子切换为inpu标签并自动聚焦时,我们可以通过ref和refs操作dom,再配合$nextTick异步实现,不过为了提高复用性,我们通过自定义指令,封装到mian.js实现该功能

  • 指令修饰符实现回车事件监听,@keyup.enter,由于该案例数据是由父组件传递过来的,所以还要将该值发送给父组件

相关推荐
-曾牛1 分钟前
Git完全指南:从入门到精通版本控制 ------- Git 工作区、暂存区和版本库(4)
java·git·学习·个人开发
王江奎10 分钟前
音视频小白系统入门笔记-1
笔记·音视频
Alsn8628 分钟前
恶意外联情况监测-火绒、DNSLookupView(联网、禁用网卡、仅主机模式请求测试)
笔记
superior tigre1 小时前
C++学习:六个月从基础到就业——面向对象编程:封装、继承与多态
c++·学习·microsoft
Suckerbin1 小时前
Pikachu靶场——Cross-Site Scripting
学习·安全·网络安全
大雄野比1 小时前
【scikit-learn基础】--『监督学习』之 贝叶斯分类
学习·分类·scikit-learn
洛小豆2 小时前
Vue Router 中的 Hash 模式与 History 模式
前端·javascript·vue.js
Dovis(誓平步青云)3 小时前
【数据结构】励志大厂版·初阶(复习+刷题):线性表(顺序表)
c语言·数据结构·经验分享·笔记·学习·算法·学习方法
星火撩猿4 小时前
GIS开发笔记(5)结合osg及osgEarth实现虚线环形区域绘制
笔记
pumpkin845144 小时前
学习笔记十三—— 理解 Rust 闭包:从语法到 impl Fn vs Box<dyn Fn>
笔记·学习·rust