TOC

组件化开发

    如果想要开发一个较为复杂的页面,当页面的内容非常多的时候,如果希望将所有的逻辑全部都在这一个页面中处理完,会变得非常复杂,且代码可读性低下,不利于后续的维护和扩展;
    但如果将这么一个较为复杂但页面,拆分成一个一个的小的功能模块,每个功能模块完成特定的功能,然后将其进行整合,这样整个页面的维护和扩展就会变得非常的方便,这就是组件化开发的思想,不仅可以将一个较大的程序实现解耦,还可以实现功能复用,极大的加快了开发进度;
    组件化开发在前端目前是非常的流行的,无论是目前前端三大框架Vue、React、Angular还是跨平台的框架Flutter,甚至移动端、小程序开发都在专向组件化开发;

Vue组件化开发

    Vue也是组件化开发的一种实现,在此之前,都是使用createApp创建一个Vue实例,那么在创建实例时,都给createApp构造函数传入了一个对象,这个对象本质上就是一个组件,而且它会是默认的根组件,如下;
const vm = Vue.createApp({
    // 根组件
})
    组件化是一种抽象逻辑,使我们可以开发出一个一个独立可复用的小组件,来构造我们的应用,目前只有一个组件,所有的东西都在一个根组件里面,Vue的组件允许我们将UI划分为独立的、可重用的部分,并且可以对每个部分进行单独的开发,在实际应用中,组件常常被组织成层层嵌套的树状结构,如下图;

组件树

    每个页面都由根组件、子组件、子子组件...组成一棵组件树,这和嵌套HTML元素的方式类似,Vue实现了自己的组件模型,使我们可以在每个组件内封装自定义内容与逻辑;

Vue组件注册

    作为Vue框架来讲,在创建Vue实例的时候,必须传递一个根组件,之后,如果我们想要添加子组件,必须向这个Vue实例注册这个组件,此外,组件又分为两种,一种为全局组件,全局组件在其他任何组件都可以使用,第二种是局部组件,局部组件是在某一个子组件里面又注册一个组件,称之为局部组件,那么这个局部组件,只能在注册的组件里面使用;

注册全局组件

    全局组件的注册是非常简单的,全局组件直接在Vue的实例上注册即可,调用这个Vue实例的component方法传入组件名称和组件对象即可完成注册,然后我们就可以在根组件或者其他子组件的template中直接使用这个全局组件,而使用这个组件,就是直接使用这个组件名称,作为元素名称放在其他组件中即可;
    此外,每个子组件对象都可以有自己独立的data响应式函数、methods方法函数、computed计算属性等,但是这里需要注意的,注册组件,需要在Vue实例执行mount方法之前,如下,使用组件来进行代码复用;
<script src="https://cdn.bootcdn.net/ajax/libs/vue/3.2.40/vue.global.js"></script>
<div id="app">
    <username-item></username-item>
</div>

<template id="usernameItem"> <!-- 子组件 -->
    <password-item></password-item>  <!-- 在子组件中引用其他子组件 -->
    <label for="username">用户名
        <input id="username" type="text" v-model.trim="username">
    </label>
</template>
<template id="passwordItem"> <!-- 子组件 -->
    <label for="password">密码
        <input id="password" type="text" v-model.trim="password">
    </label>
</template>

<script>
    let app = Vue.createApp({}) // 创建Vue实例,并传入根组件对象
    app.component("username-item", { // 注册子组件
        template: "#usernameItem",
        data() {
            return {username: undefined}
        }
    })
    app.component("password-item", { // 注册子组件
        template: "#passwordItem",
        data() {
            return {password: undefined}
        }
    })
    app.mount("#app")
</script>

    可以看到,我们上述使用了template标签,主要原因是,我们需要让其在页面不可见,template标签是HTML的一个标准标签,它天生具有display:none的属性;
  • 注意:通过Vue实例的component注册组件都是全局组件,可以在任何其他组件中引用;

注册局部组件

    事实上,在日常开发中,我们注册都是局部组件,全局组件基本用不到,也没必要去使用它,全局往往是在初始化Vue实例的时候进行注册,将这个组件注册完之后,我们就可以在其他任何地方使用了,但是全局组件在日常开发中是很少使用的,因为Vue是一个单文件组件,所以说,我们事实上只需要在根组件下注册局部组件即可;
    为什么只需要注册局部组件呢,因为局部组件只能在注册的组件里面使用,比如A组件在B组件中注册了,那么A组件只能在B组件里面引用,这种组件的注册方法,就称之为局部组件;
    局部组件,是通过组件对象内的一个components属性声明的,components是一个对象,它和methods、data、computed同级,如下示例;
<script src="https://cdn.bootcdn.net/ajax/libs/vue/3.2.40/vue.global.js"></script>
<div id="app">
    <my-component1></my-component1> <!-- 使用局部组件 -->
    <my-component2></my-component2> <!-- 使用局部组件 -->
</div>

<template id="my-component1"> <!-- 定义子组件template -->
    <h1>my-component1</h1>
</template>

<template id="my-component2"> <!-- 定义子组件template -->
    <h1>my-component2</h1>
</template>

<script>
    let app = Vue.createApp({
        components: {
            'my-component1': { // 注册局部组件
                template: "#my-component1"
            },
            'my-component2': { // 注册局部组件
                template: "#my-component2"
            },
        }
    })
    app.mount("#app")
</script>

组件名称

    定义组件名称也是有讲究的,一般有两种方式,第一种方式是使用"-"杠分割,第二种是使用大驼峰(所有字母首字母大写)的方式命名;
    但是这里有一个问题,如果我们在注册组件的时候使用了大驼峰的方式定义组件名称,在使用组件的时候就会出现问题,因为在HTML里面是不区分大小写的,所有的标签的大写全部会转换成小写,所以这个时候,在引用组件的时候,就会出现组件找不到的情况,因为vue自定将其转换成带"-"杠的格式,如myComponent会自动转换为my-component,所以推荐使用带"-"杠的格式;

开发模式转变

    可以看到,在此之前的所有编码方式,直接在一个HTML文件中编写我们所有的逻辑,整个项目源码显得十分凌乱,所有的组件逻辑、模版、样式都在一个HTML文档内,完全背离了Vue组件化开发的思想,随着项目越来越复杂,规模越来越大,这种开发模式带来的问题,将会是灾难性的;
    所以真实开发中,我们可以通过一个后缀名为".vue"文件,来解决,".vue"文件有一个专业的名称,叫single-file components(单文件组件),并且可以使用webpack、vite或者rollup的方式,来对代码进行处理,也就是真实开发中,将组件的所有逻辑、模版、样式都放在一个名为"App.vue"的文件里面;
    一个".vue"文件必须包含三个元素,第一<template>主要用于编写组件内容,即模版语法,第二<style>用来编写样式信息,第三<script>主要用来编写JavaScript源代码,如下;
<template>
    <p>{{ greeting }} World!</p>
</template>

<script>
    module.exports = {
        data: function () {
            return {
                greeting: "Hello"
            }
        }
    }
</script>

<style scoped>
    p {
        font-size: 2em;
        text-align: center;
    }
</style>
    但是由于"App.vue"的文件是不可以被浏览器解析的,所以一般都会借助一个打包工具,如webpack、vite或者rollup的方式将这个"App.vue"的文件直接进行打包,由打包工具将模版、样式、逻辑都解析成一个普通的JavaScript对象,这样就能被浏览器识别了,所以说,我们的Vue代码最后经过打包工具的打包之后,会变成一个JavaScript对象,浏览器是可以解析JavaScript代码的,从而Vue成功的被浏览器所解析;
    如果我们希望使用,这种模式去开发我们的Vue代码,那么常见的方式有两种,第一种是使用Vue官方提供的Vue CLI来创建Vue项目,项目会默认帮助我们配置好所有的配置选项,可以在里面直接使用".vue"后缀的文件进行Vue的代码开发;
    此外,我们还可以使用第三方工具,如webpack、rollup或者vite这类打包工具实现,它们和Vue CLI类似,都支持Vue的开发模式;

Vue CLI脚手架

    所谓脚手架这个概念是来自建筑工程里面的,在软件工程中,也会将一些帮助我们搭建项目的工具称之为脚手架,就是本来这个项目需要我们手动一点一点搭建,但是有了脚手架之后,脚手架将这些基础的工作全部给我们的做完了,我们只需要在这个基础之上去完成项目开发就可以了;
    Vue的脚手架工具,称之为Vue CLI,它其实就是一个命令行界面,我们可以通过这个Vue CLI创建一个基础的Vue项目解构,并且Vue CLI它默认内置了webpack相关的配置,所以我们使用Vue CLI其实节省了很多我们去做基础配置的工作;
# 安装Vue CLI
[cce@doorta ~]# npm install -g @vue/cli
[cce@doorta ~]# vue --version
@vue/cli 5.0.8
# 创建vue项目(需先进入创建项目的目录)
[cce@doorta /usr/local/Project]# vue create front
Vue CLI v5.0.8
? Please pick a preset: Manually select features
? Check the features needed for your project: (Press <space> to select, <a> to toggle all, <i> to invert selection, and <enter> to proceed)
 ◉ Babel
 ◯ TypeScript
 ◯ Progressive Web App (PWA) Support
 ◯ Router
 ◯ Vuex
 ◯ CSS Pre-processors
 ◯ Linter / Formatter
 ◯ Unit Testing
 ◯ E2E Testing
# 运行vue项目(首先需要进入vue项目根目录)
[cce@doorta /usr/local/Project/front]# npm run serve

目录解构

    使用Vue官方提供的脚手架创建一个Vue项目,默认会创建很多文件,其具体代指的意义如下;
README.md:项目简介文件;
jsconfig.json:给VScode使用的配置,声明baseURL等相关信息;
package-lock.json:记录当前项目所依赖模块的版本的详细信息;
public:入口文件,index.html所在之处;
vue.config.js:Vue项目的配置文件,它会被脚手架vue-cli在启动时加载;
babel.config.js:ES6转ES5的babel配置文件;
package.json:记录当前项目所依赖模块的版本信息;
src:源码文件夹;
.browserslistrc:浏览器适配配置文件,比如ES8代码在打包时,是否要将其转成较低ES版本的代码;
.gitignore:GIT提交忽略文件;
  • 注意:到这里来讲,可能有很多问题,比如如果我们使用export default去导出一个对象,怎么关联到template,style又怎么编译,其实这些工作都交给vue-loader去处理了,不再此做过多的赘述;

style作用域

    每个".vue"文件,都有自己都template、script和style,但是在默认情况下,style样式是会影响全局的,也就是说,它会影响其他的被引入进来的组件,那么如果我们需要将style的作用域变更成只影响当前template元素中的子元素时,需要在style元素上加入一个scoped的属性,如下示例;
<!-- 使用scoped设置作用域 -->
<style scoped="scoped">

</style>

组件间通信

    其实对于目前来讲,使用Vue框架开发项目,基本都会将整个姓名拆分成一个一个的组件,这也是Vue提倡的组件化思想,每个组件都负责不同部分的功能逻辑,比如一个页面,可能会分成Header、Content和Footer三个组件,这样使得整个项目更具层次感,同时也大大的提升了代码的可读性
    但是,这样去组织我们的项目也存在一定的问题,比如,当A组件引用B组件时,B组件需要用到A组件的数据,这就有问题了,因为在上面就提到过,每个组件都有自己独立的作用域,组件与组件之间,不会有任何影响;
    那么Vue框架也是考虑到了这一点,所以,提供了组件间通信的功能,那么在正式说组件间通信之前,首先需要了解几个关键点,即父组件和子组件,当B被A引用时,A就属于B的父组件,B就属于A的子组件,如下图;

组件间通信方式

    所谓通信方式,主要是身份的转换,主要有两种,即父传子和子传父,如上图,App是Header、Main和Footer的父组件,而Banner和ProductList是Main的子组件,那么当,Main组件中,从服务端拿到一些数据,需要在Banner或者ProductList组件中去渲染时,这种场景就被称之为父传子,父组件向子组件传递数据;
    那么当Banner或者ProductList组件中,如果发生了一个事件,需要Main组件中去做一些处理时,这种场景就被称之为子传父;

父传子

    父传子是一种非常常见的方式,比如一个页面需要同时渲染多个同类型的数据,这种场景如果都直接写在父组件内,会显得非常臃肿,所以一般来讲,我们都会将这种重复性的功能,抽取成一个组件,但是因为一些特殊原因,子组件中需要渲染的数据在父组件中;
    所以在这种场景之下,想要实现子组件的渲染,那么在子组件渲染之前应该先拿到父组件当中的数据,在Vue框架内,父组件想要给子组件传递数据,实现也并不复杂;
    首先,父组件,能够引用子组件,那是因为子组件注册进了父组件,并且在父组件中使用组件名称创建了一个HTML的元素,那么在这种场景下,Vue框架就在这个HTML元素上面实现了父传子的功能,我们可以使用attribute的形式,给这个组件变成的HTML元素上面加属性,从而实现父传子;
    那么作为子组件来讲,想要接收父组件传递过来的数据,还需要借助一个组件对象中的属性来实现,即props,它和methods、computed同级,它可以是一个数组,也可以是一个对象,数组的元素或者对象的属性名,都是用来指明需要接收的来自父组件传递的数据的;

数组语法
    props属性作为接收父组件传递过来数据的一个属性,只有使用props属性接收的数据才可以被子组件引用,上面也说过了,我们可以以数组形式来定义props属性,那么当props属性为一个数组时,我们就只需要在这个数组内指明需要接收的attribute名称即可,如下示例;
<!--  父组件 -->
<showinfo name="cce"></showinfo>  <!-- 使用attribute的方式向子组件传递数据 -->

<!--  子组件 -->
<template>
  <div>
    <span>姓名:{{ name }}</span>  <!-- 引用props中的数据-->
  </div>
</template>

<script>
export default {
  props: ["name"] // 接收来自父组件传递的数据
}
</script>

  • 注意:可以看到,我们直接使用Mustache语法取到了props中的数据,那是因为Vue框架给我们做了处理,可以直接这样引用到props中的数据;
类型处理
    可以看到,上述代码父组件给子组件传递的数据都是字符串,那么如果我们希望,给子组件传递一个数字类型的数据呢,如果我们希望给子组件传递一个对象或者数组呢,其实这个也并不难,我们只需要使用v-bind指令即可,在这种场景下,v-bind指令是支持带类型的传递数据的,如下;
<!--  父组件 -->
<showinfo v-bind:names="['cce','cfj']"></showinfo>  <!-- 使用attribute的方式向子组件传递数据 -->

<!--  子组件 -->
<template>
  <div>
    <p v-for="(name,index) in names" v-bind:key="index">{{ name }}</p>  <!-- 引用props中的数据 -->
  </div>
</template>

<script>
export default {
  props: ["names"] // 接收来自父组件传递的数据
}
</script>

  • 注意:如果我们希望父组件中传递data中的属性,那么在组件上就可以使用v-bind来动态的绑定对应的属性;
props对象语法
    对象的写法其实比数组的写法有更多的优点,比如,可以更好的限制父组件传递过来数据的类型,如果想要实现类型限定,还需要借助v-bind来实现,实属鸡肋,并且使用对象的写法,还可以设置默认值,当父组件没有传递数据过来时,可以使用默认值来填充数据;
    那么想要实现这些高级功能,就需要将props属性值设置成一个对象,这个对象里面对每个需要接收的属性同样设置成对象,并且,在这个对象内部,我们还可以给定一些参数,用来表示这个对象的形式,共有三个参数,如下;
type:指定类型,支持的类型有String、Number、Boolean、Array、Object、Date、Function和Symbol;
default:设置默认值;
required:Boolean值,表示是否为必传值;
    如下示例,当父组件没有给子组件传递数据时,会直接使用默认值来替代,同样,使用type属性来限定父组件传递过来值的类型,但是,经过测试,类型的限制,并非硬性限制;
<!--  父组件 -->
<showinfo name="传递值"></showinfo>
<showinfo></showinfo>  <!-- 不传递数据 -->

<!--  子组件 -->
<template>
  <div>
    <p>{{ name }}</p>
  </div>
</template>

<script>
export default {
  props: {
    name: {
      type: String, // 限定类型
      default: "默认值",  // 指定默认值
      required: false
    }
  }
}
</script>

  • 注意:这里唯一需要注意一点,如果限定类型为Object和Array时,那么defalut必须是一个函数,然后由这个函数返回一个对象或数组,这是硬性要求;
属性命名规则
    在父组件中给子组件传递数据时,我们可以使用驼峰命名法,同时,我们也可以使用带"-"杠符号的方式来定义我们的属性名,他们都是一样的,如下示例;
<showinfo firstName="c" lastName="ce"></showinfo>
<!-- 等价 -->
<showinfo first-name="c" last-name="ce"></showinfo>
    那么在子组件中,我们可以直接使用驼峰命名法接收即可,上面两种父组件的传递写法,在子组件中,都可以使用驼峰命名法接收;
未被子组件接收的Attribute
    这个场景有点特殊,当父组件传递了一个属性,但是该属性未被子组件使用props接收时,那么这个属性会直接添加到子组件的根元素(template元素下面的第一个元素)上面来,如下示例;
<!--  父组件 -->
<showinfo style="color: red"></showinfo>

<!--  子组件 -->
<template>
  <p>cce</p>
</template>

<script>
export default {
}
</script>

    那么如果我们希望,未被props属性接收的Attribute直接丢弃,不要放在根节点上,我们可以在子组件对象中加入一个属性,即inheritAttrs,将其设置成false即可;
    那么将inheritAttrs设置成false之后,它就不会将这些未被props接收的Attribute放在根元素上,但如果我们希望在其他元素上面拿到这个属性时,也是可以的,直接使用"$attrs"对象即可,该对象内是所有父组件传递过来的Attribute,如下;
<!--  父组件 -->
<showinfo style="color: red"></showinfo>

<!--  子组件 -->
<template>
  <div>
    <p v-bind:name="$attrs.name">cce</p>
  </div>
</template>

<script>
export default {
  inheritAttrs: false
}
</script>

  • 注意:如果出现多个根元素(template元素下面的第一个元素),那么就不会将父组件传递过来的Attribute,放到任何一个根元素,需要显示使用$attrs方式来指定;

子传父

    作为父组件来讲,也有很可能需要用到子组件中的一些数据,子组件作为父组件的组成部分,和父组件息息相关,一般出现子传父的情况,都是因为在子组件中触发了一个事件,然后父组件可能要做一些响应,比如在子组件中发生了点击事件,在父组件中要执行一些计算,或者内容切换;
    那么对于子传父,这个过程其实有点繁琐,但是并没有什么难度,第一,在子组件中,定义一个事件,当这个事件触发之后调用该子组件内部的一个事件,由内部的这个事件使用$emit函数,触发一个自定义事件,然后再在父组件中,去监听这个自定义事件,当事件触发的那一刻去调用对应的事件处理函数;

<!--  父组件 -->
<template>
  <div>
    <p>{{ counter }}</p>
    <showinfo v-on:showadd="add"></showinfo> <!-- 监听子组件触发的自定义事件,事件名为showadd,当showadd事件触发之后,调用add事件处理函数 -->
  </div>
</template>

<script>
import showinfo from "./components/showinfo.vue"

export default {
  data() {
    return {
      counter: 0
    }
  },
  methods: {
    add() {
      this.counter++
    }
  },
  components: {
    showinfo: showinfo
  }
}
</script>

<!--  子组件 -->
<template>
  <div>
    <button v-on:click="add">加</button> <!-- 定义事件 -->
  </div>
</template>

<script>
export default {
  methods: {
    add() {
      this.$emit("showadd")  // 当事件触发之后,使用$emit触发一个自定义事件
    }
  }
}
</script>

参数传递
    那么对于子传父,其较为搞不通的一点就是参数传递,子组件如何向父组件中的事件函数传递参数的,这个内部逻辑,实在没看懂,$emit函数第一参数为自定义事件名称,第二参数为需要传递的参数,这个需要传递的参数最终会传入到父组件对应的事件处理函数上面去,如下示例;
<!--  父组件 -->
<template>
  <div>
    <p>{{ counter }}</p>
    <showinfo v-on:showadd="add"></showinfo> <!-- 监听子组件触发的自定义事件,事件名为showadd,当showadd事件触发之后,调用add事件处理函数 -->
  </div>
</template>

<script>
import showinfo from "./components/showinfo.vue"

export default {
  data() {
    return {
      counter: 0
    }
  },
  methods: {
    add(n) {
      this.counter += n
    }
  },
  components: {
    showinfo: showinfo
  }
}
</script>

<!--  子组件 -->
<template>
  <div>
    <button v-on:click="add">加</button> <!-- 定义事件 -->
  </div>
</template>

<script>
export default {
  methods: {
    add() {
      this.$emit("showadd", 10)  // 当事件触发之后,使用$emit触发一个自定义事件
    }
  }
}
</script>

    就是这样,这个子组件中,使用$emit传递的参数,是如何能被父组件中的事件处理函数接收到的,这是一个疑问,因为当父组件监控到子组件中的showadd事件触发之后,执行的是add事件处理函数,但是在模版语法中,我们没有给这个add事件处理函数传递任何参数,那么add这个事件处理函数是如何拿到这个参数的呢,不得而知;
  • 注意:自定义事件的名称是随意的,只要在父组件中指明需要监控的自定义事件即可;

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注