组件化开发


组件化开发

1.组件化开发的意义

⼀、本节⼤纲

  • ⼈类更擅⻓先拆分问题,再解决问题
  • 什么是组件化开发
  • 组件化的好处

⼆、⼈类更擅⻓先拆分问题,再解决问题

复杂问题拆分,复杂⻚⾯拆分,这是⼈类最擅⻓的事情。

如果有开发⼈员接到项⽬经理的⼀个任务:写⼀个阅读类的 app ,没有做过的话,可能当时就蒙了不知从何⼊⼿。但如果项⽬经理说,我们要做⼀个阅读类的 app ,你先把注册功能的⻚⾯做好,是不是就容易些。

如果注册功能还是不会做,那就再细化为:先参考某某 app 把静态⻚⾯⽤户密码输⼊框做出来!是不是就更加容易些。

三、什么是组件化开发

组件化开发实际上就是⼀种典型的先拆分问题,再解决问题的思路。对于 vue ⽽⾔,组件就是把⼀组相 关度极⾼的视图模板、样式、数据、操作抽取出来,形成可复⽤的独⽴功能代码。

四、组件化的好处

  • 代码复⽤及功能复⽤,避免重复造轮⼦,降低程序员的⼯作量
  • ⽅便代码的组织及管理
  • 降低代码之间的耦合度,提⾼可扩展性
  • Vue 组件不仅在单功能模块可以多次复⽤,甚⾄可以跨模块、跨项⽬复⽤

2.全局组件与局部组件

⼀、本节⼤纲

  • 全局组件的定义、注册、使⽤
  • 全局组件定义注册合⼆为⼀(简写,常⽤)
  • 局部私有组件
  • 模板视图与实例定义分离(结构清晰)
  • ⽗⼦组件的嵌套

⼆、全局组件的定义、注册、使⽤

上⼀节我们已经学习了使⽤组件的意义,现在就正式来使⽤组件,使⽤全局组件有三个步骤:

下图为代码实现:

  • 全局的组件遵循先定义( vue.extend() )、再注册( vue.component() )、后使⽤的原则。
  • template ⽤于定义组件的视图内容,即:html 代码、vue 指令等。
  • 如果⽤ new VUE() 定义多个 vue 实例,全局组件可以跨多个实例使⽤。
  • 通常组件定义的名称为⾸字⺟⼤写,驼峰标志,如:MyComponent。
  • 在 dom 中使⽤组件,通常遵循使⽤“-”分隔单词,⼩写规范。如:my-component。

上图代码最终实现效果如下:

三、全局组件定义注册合⼆为⼀(简写,常⽤)

可以使⽤ Vue.component() ⽅法 + template 组件视图层模板⼀步完成全局组件的定义与注册。

这种⽅法是第⼆⼩节中代码的简写⽅式,使⽤更加⼴泛。将 Vue.extend() 定义和 Vue.component() 注册 合并为⽤⼀个 Vue.component(template) 完成,实际上的内部实现原理是⼀致的。

四、局部私有组件

局部私有组件和全局组件的区别在于,私有组件只能在定义它的实例或者⽗组件⾥⾯使⽤。

定义的组件 MyConponent2 只能在 <div id="app"> ⾥⾯使⽤,其他 Vue 实例⽆法使⽤该组件。

五、模板视图与实例定义分离

在上⾯的代码中,我们将组件定义的 html 代码写在了⼀个模板字符串⾥⾯,这样书写很不⽅便,没有 IDE 的提示,⽽且看上去也⽐较乱。

我们下⾯就来学⼀种更加正规的写法:(注意和上⽂中的代码形成对⽐)

六、⽗⼦组件的嵌套

⼀个⽗组件 Parent ⾥⾯包含两个⼦组件 Child,并将⽗组件放在 app2 实例⻚⾯渲染范围内。

下⽂中是使⽤私有局部组件的⽅式定义的,也就是 Parent 组件是 app2 实例的私有局部组件,Child 是 Parent 组件的私有局部组件。

代码实现的效果截图如下:

可以想⼀想如何通过全局组件,实现嵌套组件?

3.⽗⼦组件的数据定义及访问

⼀、本节⼤纲

  • 组件⽆法直接访问 VUE 实例的 data 数据
  • 组件有⾃⼰的数据和⽅法
  • 组件数据为什么要定义在函数⾥⾯
  • ⽗⼦组件的数据访问

⼆、组件⽆法直接访问 VUE 实例的 data 数据

我们发现:

  • ⽆法在组件内直接通过插值表达式访问 vue 实例的数据
  • ⼦组件也⽆法直接使⽤⽗组件中的数据

三、组件有⾃⼰的数据和⽅法

从上⼀⼩节的内容中,我们知道,组件视图内⽆法直接通过插值表达式访问 Vue 实例的数据。

那么组件的数据定义在哪⾥呢?组件有⾃⼰的数据和⽅法定义。如下图所示:

  • MyComponent 组件有⾃⼰的视图模板定义 template#MyComponent。
  • MyComponent 组件有⾃⼰的数据定义,data() 函数。注意这⾥定义的是⼀个函数,⽽不是对象。通过函数返回对象数据。
  • MyComponent 组件有⾃⼰的操作⽅法,定义在 methods 代码块⾥⾯。

四、组件数据为什么要定义在函数⾥⾯

  • 我们知道 Vue 实例的数据定义在 data 对象⾥⾯。但是组件的数据定义在函数⾥⾯,相当于使⽤了⼯⼚函数创建对象,每⼀次创建的对象在内存⾥⾯都是新的空间和指针。
  • 如果不定义在对象⾥⾯,⾸先⼀定会报错,其次如果每次返回同⼀个对象。当⻚⾯出现多次使⽤同 ⼀个组件,⼀定会彼此影响,从⽽产⽣我们不期望的结果。

五、⽗⼦组件的数据访问

⽗组件⽆法直接使⽤⼦组件的数据,⼦组件也⽆法直接使⽤⽗组件定义的数据。

那么有没有间接的使⽤⽅式呢?我们可以使⽤ $parent、**.$children$ref** 引⽤的⽅式:

  • 在⼦组件代码中调⽤ this.$parent 可以获取到⽗组件对象
  • 在⽗组件代码中调⽤ this.$children[0] 可以获取到⽗组件引⽤的第⼀个⼦组件对象。 ( this.$children 得到的是⼀个⼦组件的数组)

如果我们希望在⼦组件中使⽤ this.$parent 打印⽗组件信息,在⽗组件中调⽤ this.$children[0] 打印⼦组件信息。

上⾯的⽅法中,⽗组件使⽤ this.$children[0] 来获取多个⼦组件的数据。

如果我们希望快速从⽗组件获取⼦组件的数据,还可以为⼦组件加上⼀个属性 ref 。相当于为⼦组件起了⼀个别名,便于查找。

然后⽗组件通过如下代码即可打印⼦组件的属性数据:

console.log(this.$refs.childRef.childMessage);

但是通常使⽤ $parent、.$children、$ref 会增加组件之间的耦合性,所以不建议使⽤。除⾮你定义的组件只在单模块⾥⾯使⽤,不考虑以后的复⽤问题。

4.如何实现组件的切换

⼀、本节⼤纲

  • v-if、v-else 指令切换组件
  • 通过标签切换组件

⼆、v-if、v-else 指令切换组件

我们来通过 v-if、v-else 指令实现⼀个简单的需求,通过点击切换按钮。切换登录和注册⻚⾯,⼆者只能同时显示其中⼀个组件。视图定义如下:

  • 通过 template 定义组件视图的内容
  • 通过 isLogin 布尔型变量,切换登录与注册内容的显示

模型定义如下:

  • 使⽤ Vue.component() 实现全局组件的注册
  • isLogin 初始值为 true,即默认显示登录组件

最终实现效果如下:

三、通过标签切换组件

除了可以使⽤ v-if 和 v-else 指令,我们还可以使⽤标签来实现组件的切换,这种⽅式使⽤上更加灵活。

视图定义如下:

component 标签有⼀个 is 属性绑定,⽤于指定当前位置显示的组件的名称

模型定义如下:

实现的效果与之前相同。

5.⽗组件向⼦组件传递数据

⼀、本节⼤纲

  • 使⽤ props ⽗组件向⼦组件传递数据
  • 组件 props 属性传递最佳命名实践
  • props 数据校验

⼆、使⽤ props ⽗组件向⼦组件传递数据

  • ⾸先,⽗实例将⾃⼰的数据 player,传递给⼦组件 child-cpn。(图中⻩⾊箭头)
  • ⼦组件通过绑定属性 child-player 绑定⽗组件数据 player
  • ⼦组件标签属性 child-player 对应⼦组件模型属性定义 props:childPlayer
  • childPlayer 属性可以使⽤插值表达式,显示在 template 模板⾥⾯

这样,我们通过改变⽗组件⾥⾯的 player 对象,就能动态的改变⼦组件的 childPlayer 属性值

要⾮常注意⼀点:在 Vue2.0 中,props 数据流只能从⽗组件向⼦组件传递。并且在组件内,不能修改 由外层传来的 props 数据。这是为了防⽌⼦组件⽆意修改了⽗组件的状态——这会让应⽤的数据流难以理解。

⼦组件如何将⾃⼰的数据改变传递给⽗组件呢?是下⼀节的内容。

三、组件 props 属性传递最佳命名实践

对于 props 声明的属性来说,在⽗级 HTML 模板中,属性名需要使⽤中划线写法。

⼦级 props 属性声明时,使⽤⼩驼峰或者中划线写法都可以;⼩写字⺟开头。

但是⽆论如何,在插值表达式⾥⾯必须是驼峰写法,如:{{childMessage}}

  • 在 html 代码⾥⾯标签属性命名时,使⽤中划线属性写法。
  • 在 js 代码⾥⾯的变量或属性使⽤驼峰的写法,组件名⾸字⺟⼤写,变量属性名⾸字⺟⼩写。
  • 插值表达式{{}}`是代码?还是 html 标签属性?html 规范⾥⾯有`{{}}这种⽤法么?所以它当然是代码。

和我们平时写代码的规范是⼀致的。html 标签属性都是中划线,代码都是驼峰标志。

上⾯的规范中遗留⼀个问题,就是组件的名称是⾸字⺟⼤写,驼峰标识。在 html ⾥⾯我们通常希望也遵循这样的规则。如: <ChildCpn></ChildCpn>,这样可以避免 html 标签和组件标签混淆。

但是⽬前是不可以这么⽤的,在 Vue 单⽂件⾥⾯可以这样写。

四、props 数据校验

通常我们定义⼀个组件是应该可以提供给其他模块或其他⼈使⽤的。使⽤者可能对该组件的⽤法并不熟悉,可能会导致错误。所以有必要在⼦组件内,对⽗组件传递过来数据进⾏校验。

Vue.component("example", {
  props: {
    // 基础类型检测 (`null` 意思是任何类型都可以)
    propA: Number,
    // 可以是多种类型
    propB: [String, Number],
    // 必传属性且必须是字符串类型
    propC: {
      type: String,
      required: true,
    },
    // 数字,不传就默认值100
    propD: {
      type: Number,
      default: 100,
    },
    // 数组/对象的默认值应当由⼀个⼯⼚函数返回
    propE: {
      type: Object,
      default: function () {
        return { message: "hello" };
      },
    },
    // ⾃定义验证函数
    propF: {
      validator: function (value) {
        return value > 10;
      },
    },
  },
});

type 的类型可以是如下:

String;
Number;
Boolean;
Function;
Object;
Array;
Symbol;

6.⼦组件向⽗组件传播事件

⼀、本节⼤纲

  • 想⼀想这个需求怎么实现
  • ⼦组件向⽗组件事件传播核⼼代码解读
  • 整体需求的实现

⼆、想⼀想这个需求怎么实现

其中加⼀和减⼀的按钮是属于⼦组件,pCounter 显示是在⽗组件⾥⾯( Vue 实例⾥⾯)。

我们该如何把⼦组件的点击事件传递给⽗组件,并传递参数改变⽗组件的值?

三、⼦组件向⽗组件事件传播核⼼代码解读

前⾯《⽗⼦组件的数据定义与访问》中我们曾学过,⽗组件可以通过 $children 和 $ref 的⽅式获取⼦组件的数据。

但是在实际开发过程中,还有另外⼀种需求,即:⼦组件中发⽣了某些动作,从⽽改变了⼦组件的数据。

那么⽗组件如何实时的监听到⼦组件数据的变化呢?这就是本节的核⼼内容。

  • 在⼦组件使⽤ $emit (‘事件名称’,参数),触发并发送事件,并且可以通过参数传值。
  • 在⽗组件中嵌⼊⼦组件,并使⽤ v-on 指令(简写为@)监听事件,从⽽触发回调函数,回调函数接受⼦组件发送的参数。图中 changePcounter 就是针对 increment 事件和 decrment 事件监听的回调函数。
  • ⽗组件定义回调函数接收实践触发源传递的参数,即 $emit 的第⼆个参数

四、整体需求的实现

⼦组件与视图的定义。

⼦组件点击加⼀按钮触发 incr 函数,incr 函数触发 increment 事件,并将当前 cCounter 作为事件参数传递出去。

⽗组件范围监听到 increment 事件,从⽽触发 changePCounter ⽅法,该⽅法接受 cCounter 作为参数。

⽗组件的定义

7.插槽的使⽤场景与⽅法

⼀、本节⼤纲

  • 插槽的使⽤场景
  • 插槽的使⽤⽅法
  • 组件如何给插槽传递数据
  • 改变插槽数据的显示样式

⼆、插槽的使⽤场景

Windows 操作系统⽂件管理的⻚⾯布局,头部导航栏,左侧⽂件夹菜单,右侧⽂件内容区。对 于“Windows ⽂件管理”这个组件,划分三个区域,头部导航栏,左侧⽂件夹菜单,右侧⽂件内容区。这 三个区域就是三个插槽。

我们可以先归纳⼀下应⽤插槽的场景的⼏个特点:

  • 插槽是组件的插槽,并不脱离于组件⽽存在。⽐如:Windows ⽂件管理功能是⼀个组件,右侧⽂件内容区是该组件的⼀个插槽。
  • 插槽相当于是⻚⾯占位符,占⽤位置展示数据,插槽通常可以较明确的做出区域划分。如: windows 的右侧⽂件内容区就可以划分为⼀个插槽。
  • 插槽⾥⾯数据及其显示的样式是可以变化的,但是数据内容属性最好是有⼀定的相同点,才定义为⼀个插槽。如:Windows 的右侧⽂件内容区展示的都是⽂件或⽂件夹,不是菜单也不是按钮。
  • 抽取共性,保留不同。将共性定义在组件⾥⾯,将差异暴露为插槽。

三、插槽的使⽤⽅法

第⼀步:抽取定义插槽

我们先看⼀个左右布局的视图,共性内容抽取定义为组件 MyLayout,差异内容定义为插槽,对外暴露。

注意上⾯的插槽定义,第⼀个插槽命名为 left,第⼆个插槽没有命名,没有命名的插槽默认名称为 default

第⼆步:插槽占位替换

对组件 MyLayout(my-layout) 的两个插槽进⾏占位替换。

v-slot:[slotname] 指令通过指明插槽名称,⽤ template 模板中的内容替换指定的插槽。

v-slot 可以简写为 #[slotname]

<template #left>
  <h1>这⾥是左侧区域</h1>
</template>

四、组件如何给插槽传递数据

上⼀节在右侧内容区展示的数据时静态的,我们能不能动态地为右侧内容区传递数据呢?也是可以的。 思考:谁给插槽传数据呢?在哪个组件⾥⾯定义的插槽,就由哪个组件提供数据。

  • players 和 bestPlayers 是组件 MyComponent ⾥⾯定义的数据。数据定义如下:
players: ["aaa", "bbb", "ccc", "ddd"],
bestPlayer: "aaa"
  • 通过 v-bind 指令(简写为“:”)为插槽绑定变量,将变量绑定到 slotProps 上⾯,slotProps 相当于主⼲,我们通过 v-bind 为它添加分⽀。
  • slotProps 只是⼀个绑定对象变量名,可以随意起名。

要注意⼀个问题:我们绑定的变量名是 bestPlayer(P ⼤写),到了 slot 模板⾥⾯,使⽤的时候⽤的是 bestplayer(⼩写)。这是因为 Vue 遵循⼀个规则:html 标签属性不应包含⼤写字⺟,所以尽量不要⽤驼峰标识。可以⽤ best-player 或 bestplayer 。

通过上⾯的实现,我们就可以通过改变⼦组件的数据,进⽽影响 slot 的显示内容。

五、改变插槽数据的显示样式

插槽的数据我们从组件中获取到了,那么数据展示的样式呢?

展示的样式是⼀个差异化的问题,所以应该在插槽的定义⾥⾯解决。

我们⽤ ui-li 标签展示⼀下 slot 的数据。

完整代码

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initialscale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>插槽-具名插槽-作⽤域插槽</title>
    <style>
      .wrapper {
        height: 200px;
        border: 1px solid #ccc;
        display: flex;
      }
      .left {
        width: 200px;
        height: 100%;
        background: #9fa8da;
      }
      .center {
        height: 100%;
        background: #80cbc4;
        flex: 1;
      }
    </style>
  </head>
  <body>
    <div id="app">
      <my-layout>
        <!-- 插槽占位替换 -->
        <template v-slot:left>
          <h1>左侧区域</h1>
          <h1>左侧区域</h1>
        </template>
        <template v-slot:default="slotProps">
          <ul>
            <li v-for="player in slotProps.players">{{player}}</li>
          </ul>
          <h2>{{slotProps.bestplayer}}</h2>
        </template>
      </my-layout>
    </div>
    <template id="MyLayout">
      <div class="wrapper">
        <div class="left">
          <slot name="left"></slot>
        </div>
        <div class="center">
          <slot :players="players" :bestPlayer="bestPlayer"></slot>
        </div>
      </div>
    </template>
    <script src="https://cdn.jsdelivr.net/npm/vue@2"></script>
    <script>
      const app = new Vue({
        el: "#app",
        data() {
          return {
            message: "Vue",
          };
        },
        components: {
          MyLayout: {
            template: "#MyLayout",
            data() {
              return {
                players: ["aaa", "bbb", "ccc", "ddd"],
                bestPlayer: "aaa",
              };
            },
          },
        },
      });
    </script>
  </body>
  <html></html>
</html>

运⾏效果:


文章作者: Syhan
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Syhan !
评论
  目录