vue 开发组件化

vue-cli

什么是 vue- cli

tip vue-cli 是 Vue.js 开发的标准工具。它简化了程序员基于 webpack 创建工程化的 Vue 项目的过程。

安装与使用

安装

npm -g @vue/cli

使用 快速初始化

vue create 项目名字

运行流程

vue 通过 main.js 把 app.vue 渲染到 index.html 指定区域

  • app.vue 用来编写待渲染的模板结构
  • index.html 中需要预留 el 区域

vue 组件的三个组成部分

template(组件的模板结构)

<template>
 <div>
  <!-- 定义当前组件的 DOM 结构  -->
 </div>
</template>
  • 每个组件的模板结构 ,需要调用到 template
  • template 的 vue 提供的容器标签 ,只有包裹性作用,不会渲染为真正的DOM元素
  • template 只能包含唯一的根节点
  • 简单理解里面写 HTML即可

script(组件的 JavaScript 行为)

<script>
export default {
  // 封装组件的 JS 业务逻辑
}
</script>

style (组件的样式)

<style>
/* css代码 */
</style>

添加less 语法支持

<style leng="less">
/* less代码 */
</style>

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

组件关系

封装好 App.vue Left.vue Right.vue 之后,彼此间相互独立不存在关系

使用组件

<template>
<div>
 <!-- 步骤三:以标签形式使用组件 -->
<Left></Left>
</div>
</template>
<script>
// 步骤一:使用 import 导入需要的组件 
import Left from "@/components/Left.vue"
export default{
  // 步骤二:使用 components 结点注册组件
  components:{
    Left,
  }
}
</script>

全局组件

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

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

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

注册全局组件

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

// 导入需要注册的全局组件
import Count from "@/components/Count.vue"

props (自定义属性)

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

导入组件后以标签形式使用组件 在标签即可使用 自定义属性

<!-- 绑定自定义属性  -->
<!-- 自定义属性有小驼峰命名 改成连字符-命名 -->
<count :init="1" :cmt-count="2"></count>
// count.vue
export default {
  // 组件的自定义属性
  // 格式一
  // props: ["自定义属性1","自定义属性2","自定义属性3"],
  // props: ["init"],
  // 格式二:props:{
  //    自定义属性1:{...}
  //    自定义属性2:{...}
  //    自定义属性3:{...}
  // }
  props: {
    // 自定义属性名
    init: {
      // 默认值,如果用户不传递默认的值
      default: 0,
      // 类型 ,传递过来的值必须是规定类型的值
      type: Number,
      // 必选项,开启后必须传递值 ,这个组件才可以使用
      require: true,
      // props 自定义属性为只读值,不能用户修改
    },
    // 如果自定义属性名为小驼峰命名
    // 建议绑定属性时可改成连字符-命名
    cmtCount:{
       default: 0,
       type: Number,
    }
  },
  data() {
    return {
      //  props 的值转存到 data 
      count: this.init,
    };
  },
};

tip

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

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

组件冲突问题

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

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

  • 单页面应用程序中,所有组件的 DOM 结构,都是基于唯一的 index.html 页面进行呈现的
  • 每个组件中的样式,都会影响整个 index.html 页面中的 DOM 元素

style 节点的 scoped 属性

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

<style leng="less" scoped >
/* less代码 */
</style>
  • 原理:为当前组件所有 DOM 元素分配唯一的自定义属性,写样式时使用属性选择器防止样式冲突问题
  • scoped 只给子组件最外层的 div 添加了自定义属性 [data-v-xxx] ,子组件内部的标签并没有添加。因此父组件只能修改子组件最外层的 div 样式,修改子组件内层元素的样式是不可行的
  • 若想让某些样式对子组件生效,需使用 /deep/ 深度选择器 (常用于修改 第三方 ui 组件库 )
/* 细细品味 */
<style lang="less" scoped>
.title {
  /* 不加 /deep/,选择器格式为 .title[data-v-052242de] */
  color: blue;
}

/deep/ .title {
  /* 加 /deep/,选择器格式为 [data-v-052242de] .title */
  color: blue;
}
</style>

组件数据共享

父子间的共享

父向子共享

父组件向子组件共享数据需要使用自定义属性props

父组件

<son :msg="message" :user="userinfo"></son>

<script>
export default{
 data(){
    return {
      // 定义要传递的值
      message:'hello vue.js'
      userinfo:{name:'张三',age:18}
    }
  }
}
</script>

子组件

<p>父组件传递过来的 msg 是 :{{msg}}</p>
<p>父组件传递过来的 user 是 :{{user}}</p>

<script>
export default{
 // 定义自定义属性
  props:['msg','user']
}
</script>

子向父共享

子组件向父组件共享数据使用自定义事件。

子组件

export default{
 data(){
    return {
      // 定义要传递的值
     count: 0
    }
  },
  methods:{
    add(){
      this.count++
      
      // 修改数据时 ,通过 $emit() 触发自定义事件
      // 参数1:自定义事件的名字,
      // 参数2:要传递的值
      this.$emit("numChange",this.count)
    }
  }
}

父组件

<son @numChange="getCount"></son>
<script>
export default{
 data(){
    return {
      // 定义储存传递过来的值
      countFromSon: 0
    }
  },
  methods:{
    // 定义触发自定义事件 numChange 的函数
    // val 接收 son 自定义事件发送过来的值
    getCount(val){
      // 转存到countFromSon
      this.countFromSon=val
    }
  }
}
</script>

兄弟间共享

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

EventBus 的使用步骤

  1. 创建 eventBus.js 模块,并向外共享一个 Vue 的实例对象
  2. 在数据发送方,调用 bus.$emit('事件名称', 要发送的数据) 方法触发自定义事件
  3. 在数据接收方,调用 bus.$on('事件名称', 事件处理函数) 方法注册一个自定义事件

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

<!--定义一个按钮 触发  -->
<button @click="sendMsg">发送数据给 兄弟组件 C</button>
<script>
// 导入 EventBus 模块
import bus from "./eventBus.js";
export default {
  data() {
    return {
     // 共享的数据
      msg: "hello vue.js",
    };
  },
  methods: {
    sendMsg() {
      // 自定义事件
      // 参数:事件名称, 要发送的数据
      bus.$emit("share", this.msg);
    },
  },
};
</script>

eventBus.js

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

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

<p>兄弟组件 A 共享的数据 :{{ msgFromLeft }}</p>
<script>
// 导入 EventBus 模块
import bus from "./eventBus.js";
export default {
  data() {
    return {
      // 定义用来转存 发送过来的值 的变量
      msgFromLeft: "",
    };
  },
  // 在 created 钩子中注册函数
  created() {
  // 使用箭头函数,则 this 指向该组件而非 bus
  // 参数:事件名称, 事件处理函数
  // 触发兄弟组件 A 自定义事件 share
  // 函数处理 把传递过来的值转存到 msgFromLeft
    bus.$on("share", (val) => {
      this.msgFromLeft = val;
    });
  },
};
</script>

生命周期

概念:每个组件从创建 -> 运行 -> 销毁的一个过程,强调的是一个时间段!

tip 生命周期阶段

组件生命周期函数的分类

tip 官方生命周期图示

vue 官方文档给出的生命周期图示

ref 引用

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

引用 dom 元素

<!-- 使用 ref 属性,为对应的 dom 元素添加引用的名称  -->
<h3 ref="myH3Ref"> myRef 组件</h3>
<!-- 点击调用getRef  -->
<button @click='getRef'>获取 $refs 引用</button>
<script>
  export default {
    methods:{
      getRef(){
        // 通过 this.$refs.引用名称
        // 可以得到 dom 元素引用
        console.log(this.$refs.myH3Ref)
        // 操作 dom 元素,把文本颜色改为红色
        this.$refs.myH3Ref.style.color='red'
      }
    }
  }
</script>

引用组件实例

<!-- 使用 ref 属性,为对应的组件添加引用的名称  -->
<my-counter ref="counterRef"></my-counter>
<button @click='getRef'>获取 $refs 引用</button>
<script>
  export default {
    methods:{
      getRef(){
        // 通过 this.$refs.引用名称
        // 可以得到组件实例引用
        console.log(this.$refs.counterRef)
        //  可以访问获取后的组件数据和方法
         this.$refs.counterRef.count = 1
      this.$refs.counterRef.add()
      }
    }
  }
</script>

this.$nextTick(cb) 方法

组件的 $nextTick(cb) 方法,会把 cb 回调推迟到下一个 DOM 更新周期之后执行,即在 DOM 更新完成后再执行回调,从而保证 cb 回调可以获取最新的 DOM 元素

methods: {
  showInput() {
    this.inputVisible = true
    // 对输入框的操作推迟到 DOM 更新完成之后
    this.$nextTick(() => {
      this.$refs.input.focus()
    })
  }
}

动态插件

vue 提供了一个内置的<component> 组件,专门用来实现动态组件的渲染

<script>
data(){
  return {
    // 定义要渲染组件的名称
    comName:'Left'
  }
}
</script>
<!-- 通过 js 动态绑定指定的渲染组件 -->
<component is:"conName" ><component>
<!-- 点击 动态切换组件 -->
<button @click="comName = 'Left'">展示Left组件</button>
<button @click="comName = 'Right'">展示Right组件</button>

keep-alive

在默认情况下,切换组件会将不需要的组件销毁,导致组件的数据清空,切换回来时数据不能还原到切换前

为了保持组件的状态可以使用 vue 内置的 <keep-alive> 组件

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

使用 <keep-alive> 组件后,动态切换组件时不需要的组件就会被缓存,切换回来时重新被激活

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

  • 组件被激活时,触发组件的 deactived 生命周期函数
  • 组件被销毁时,触发组件的 activated 生命周期函数
export default {
  activated(){
    console.log('组件被激活了')
  }
  deactived(){
    console.log('组件被销毁了')
  }
}

keep-aliveinclude 属性

include 属性用来指定,只有匹配名称的组件才会被缓存,其他销毁

<keep-alive include="Left">
 <component :is="comName"></component>
</keep-alive>

keep-aliveexclude 属性

功能与 include 相反,排除不缓存的组件

如果同时使用 include, exclude, 那么 exclude 优先于 include
include 和 exclude 避免混乱只用一个就好

插槽

插槽(slot)是封装组件时,把不确定的,希望由用户指定部分定义为插槽

简单来讲,组件封装期间,为用户预留的内容的占位符。

image-20220821111326204

基础用法

封装组件时,用 <slot> 元素定义插槽,预留占位符

<!-- my-com.vue -->
<template>
 <p> 这是 MyCom 组件的第一个 p标签 </p>
  <!-- 通过 slot 占位符,为用户预留位置 -->
  <slot></slot>
  <p> 这是 MyCom 组件的最后的 p标签 </p>
</template>

使用

<my-com>
 <!-- 使用 my-com 组件时,为插槽指定具体内容 --> 
 <p> 这是用户自定义的 p标签 ~~~ </p>
<my-com>

如果在封装组件时没有预留 slot 任何插槽,用户自定义的内容不生效,会被丢弃

后备内容

在封装组件时,可以为预留的 <slot> 插槽的提供后备内容(默认内容)

<slot> 
 <p> 这是后备内容的 p标签 ~~~ </p>
</slot>

如果组件使用者没有为插槽提供任何的内容,后备内容就会生效
用户提供内容后,后备内容就会被覆盖掉

具名插槽

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

<!-- my-com.vue -->
<div class="container">
  <header>
  <!-- 希望页头放这里 -->
    <slot name="header"></slot>
  </header>
  <main>
  <!-- 希望主要内容放这里 -->
    <slot name="main"></slot>
  </main>
  <footer>
    <!-- 希望主要页脚放这里 -->
    <slot name="footer"></slot>
  </footer>
</div>

为具名插槽提供内容,在一个 <template> 元素上使用 v-slot 指令,并以 v-slot 的参数的形式提供其名称

<my-com>
<!-- v-slot 的属性 要和希望放到 slot 插槽name属性名一致 -->
 <template v-solt:header>
  <h1>头部 </h1>
  </template>
  
  <template v-solt:main>
  <p> 主要内容 </p>
  </template>
  
  <template v-solt:footer>
  <p> 页脚 </p>
  </template>
</my-com>

注意:没有指定 name 名称的插槽,会有隐含的名称叫做 “default”。

具名插槽的简写形式: (v-slot:) 替换为字符 #,例如 v-slot:header可以被简写为 #header

作用域插槽

在封装组件过程中,为预留的 solt 插槽绑定 props 数据,叫做作用域插槽

数据在组件的自身,但根据数据生成的结构需要组件的使用者来决定

<template>
 <slot v-for="item in list" :user='item'>
</template>
<script>
  export default {
   data() {
     return {
       list: [
        {
          id: 1,
          name: 'Lily',
          state: true,
        },
        {
          id: 2,
          name: 'Ben',
          state: false,
        },
        {
          id: 3,
          name: 'Water',
          state: true,
        },
      ],
    }
  },
}
</script>

可以使用 v-slot: 的形式,接收作用域插槽对外提供的数据

<my-com>
  <template v-slot:default="scope">
  <p>作用域插槽提供的数据: {{scope}}</p>  
  </template>
</my-com>

接收到的数据 scope 是一个对象。

// scope 的内容
{
  'user': {
    'id': 1,
    'name': 'Lily',
    'state': true
  }
}

在接收作用域插槽提供的数据时可以使用解构赋值。

<my-com>
  <template #default="{user}">
    <p>id:{{ user.id }}</p>
    <p>name:{{ user.name }}</p>
    <p>state:{{ user.state }}</p>
  </template>
</my-com>

自定义指令

私有自定义指令

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

directives: {
  color:{
    // 为绑定的元素设置红色字体
    bind(el){
      // el 是绑定此指令的 dom 对象
      el.style.color='red'
    }
  }
}

使用:需要加上 v- 前缀

<!-- 上面声明的自定义指令名为 color -->
<!-- 使用自定义指令名 color,需要加上 v- 前缀 -->
<h1 v-color>app组件</h1>

动态绑值

data:{
  return{
    color:'red'
  }
}
<h1 v-color="color">app组件</h1>

通过 binding 获取指令的参数值

directives: {
  color:{
    bind(el,binding){
      // el 是绑定此指令的 dom 对象
      // 通过 binding 对象.value 属性获取动态的参数值
      el.style.color=bing.value
    }
  }
}

update 函数

bind 函数只调用 1 次:当指令第一次绑定到元素时调用,当 DOM 更新时 bind 函数不会被触发。 update 函数会在每次 DOM 更新时被调用

directives: {
  color:{
    // 指令第一次绑定元素时调用
    bind(el,binding){
      // el 是绑定此指令的 dom 对象
      // 通过 binding 对象.value 属性获取动态的参数值
      el.style.color=bing.value
    }
    // 每次 dom 更新都调用
    update(el,binding){
       el.style.color=bing.value
    }
  }
}

简写

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

directives: {
  color(el,binding){
     el.style.color=bing.value
  }
}

全局自定义指令

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

// 全局写法
Vue.directive('color', (el, binding) => {
  el.style.color = binding.value
}))

注意事项

  • 自定义指令使用时需要添加 v- 前缀
  • 指令名如果是多个单词,要使用 camel-case 短横线命名方式,不要用 camelCase 驼峰命名
  • 自定义指令三个函数里的 this 指向 window
<span v-big-number="n"></span>
data() {
  return {
    n: 1
  }
},
directives: {
  // 添加引号才是对象键名完整写法
  // 平时不加引号都是简写形式
  // 遇到短横线的键名就必须添加引号
  'big-number': {
    bind(el, binding) {
      console.log(this) // Window
      el.innerText = binding.value * 10
    }
  }
}