# 组件化

## 不用组件的两个【灾难性问题】

随着ajax异步请求的出现,慢慢形成了前后端的分离
客户端需要完成的事情越来越多,代码量也是与日俱增。
为了应对代码量的剧增,我们通常会将代码组织在<mark>多个js文件中</mark>,进行维护。
但是这种维护方式,依然<mark>不能避免一些灾难性的问题</mark>。

1、如<mark>全局变量同名</mark>问题

2、又如 代码的编写方式对js文件的依赖顺序几乎是强制性的

但是当js文件过多,比如有几十个的时候,弄清楚它们的顺序是一件比较同时的事情。
而且即使你弄清楚顺序了,也不能避免上面出现的这种尴尬问题的发生。

## Vue 组件化思想

官方 - 《组件基础》 - https://cn.vuejs.org/v2/guide/components.html

  • 我们希望开发出一个个<mark>独立可复用</mark>的小组件来构造我们的应哟

  • 那样, 任何都会被抽象成一颗组件数

    (其中,连线指的是下级对上级组件的依赖)

  • 这样,我们的代码更加方便组织和管理,并且扩展性也更强。

## 注册组件

注册组件的基本步骤

  • 创建组件构造器
    调用 Vue.extend() 方法<mark>创建组件构造器</mark>
  • 注册组件
    调用 Vue.component() 方法 <mark>注册组件</mark>
  • 使用组件
    <mark>在Vue实例的作用域范围内使用组件</mark>

基本使用

<body>
  <div id='app'>
    <!-- 3. 使用组件 -->
    <my-cpn></my-cpn>
  </div>

  <script src="../dist/vue.js"></script>
  <script> // 1.创建组件的构造器对象 const cpnC = Vue.extend({ template: ` <div> <h2>我是标题</h2> <p>我是内容</p> </div> ` }) // 2. 注册组件 (全局组件,意味着可以在多个Vue的实例下使用) Vue.component('my-cpn', cpnC) const app = new Vue({ el: '#app', data: { } }) </script>
</body>

全局组件 、 局部组件

  • 全局组件,意味着可以在多个Vue的实例下使用(例子如上)
  • 局部组件
<body>
  <div id='app'>
    <!-- 3. 使用组件 -->
    <my-cpn></my-cpn>
  </div>

  <script src="../dist/vue.js"></script>
  <script> // 1.创建组件的构造器对象 const cpnC = Vue.extend({ template: ` <div> <h2>我是标题</h2> <p>我是内容</p> </div> ` }) const app = new Vue({ el: '#app', data: { }, components: { // 2. 注册局部组件 'my-cpn': cpnC } }) </script>
</body>

父组件和子组件

<body>
  <div id='app'>
    <cpn2></cpn2>
  </div>

  <script src="../dist/vue.js"></script>
  <script> // 1.创建第一个组件 const cpn1 = Vue.extend({ template: ` <div> <p>我是内容 cpn1</p> </div> ` }) // 2.创建第一个组件 const cpn2 = Vue.extend({ template: ` <div> <h2>我是标题 cpn2</h2> <cpn1></cpn1> </div> `, components: { cpn1: cpn1 } }) const app = new Vue({ el: '#app', data: { }, components: { // 2. 注册局部组件 cpn2: cpn2 } }) </script>
</body>

<mark>cpn2包含了 cpn1,所以cpn2是父组件</mark>

语法糖:组件注册

省略 Vue.extend ,直接在 component里面定义 <mark>组件构造器</mark>

<body>
  <div id='app'>
    <cpn></cpn>
  </div>

  <script src="../dist/vue.js"></script>
  <script> Vue.component('cpn', { template: ` <div> <h2>我是标题</h2> <p>我是内容,哈哈哈</p> </div> ` }) const app = new Vue({ el: '#app', data: { } }) </script>
</body>

## 技巧:模板分离写法

把模板写在 标签内

<body>
  <div id='app'>
    <cpn></cpn>
  </div>


  <!-- 1. -->
  <script type="text/x-template" id='cpn'> <div> <h2>我是标题</h2> <p>我是内容,哈哈哈</p> </div> </script>

  <script src="../dist/vue.js"></script>
  <script> Vue.component('cpn', { template: '#cpn' }) const app = new Vue({ el: '#app', data: { } }) </script>
</body>

或者这样

<body>
  <div id='app'>
    <cpn></cpn>
  </div>
  <!-- 2. -->
  <template id='cpn'>
    <div>
      <h2>我是标题</h2>
      <p>我是内容,哈哈哈</p>
    </div>
  </template>
  <script src="../dist/vue.js"></script>
  <script> Vue.component('cpn', { template: '#cpn' }) const app = new Vue({ el: '#app', data: {} }) </script>
</body>

## 组件访问实例数据 【无法访问的】

如果组件内容需要动态显示,需要在组件内定义数据(<mark>数据不能放在顶级实例内,组件是无法访问到的</mark>)

另外:
<mark>组件内部的 data 属性值必须是个 函数</mark>

## 为什么 组件内部的 data 属性值必须是个 函数???

因为,Vue为了防止多个同一种组件之间的数据相互影响
详细看 : https://cn.vuejs.org/v2/guide/components.html#data-必须是个函数

即,写法要如下

<body>
  <div id='app'>
    <cpn></cpn>
  </div>
  <!-- 2. -->
  <template id='cpn'>
    <div>
      <h2>{{title}}</h2>
      <p>我是内容,哈哈哈</p>
    </div>
  </template>
  <script src="../dist/vue.js"></script>
  <script> Vue.component('cpn', { template: '#cpn', data() { return { title: '我是个标题' } } }) const app = new Vue({ el: '#app', data: {} }) </script>
</body>

## 父子组件之间通讯

  • 通过 props 向子组件传递数据
  • 通过 事件 向父组件发送消息

通过 props 向子组件传递数据

<body>
  <div id='app'>
    <cpn v-bind:cmovies='movies' :cmessage='message'></cpn>
  </div>

  <template id="cpn">
    <div>
      <h2>{{cmessage}}</h2>
      <div>{{cmovies}}</div>
    </div>
  </template>

  <script src="../dist/vue.js"></script>
  <script> const cpn = { template: '#cpn', props: ['cmovies', 'cmessage'] } const app = new Vue({ el: '#app', data: { movies: ['明日花绮罗', '上三悠亚', '斋藤飞鸟'], message: '你好啊' }, components: { cpn: cpn } }) </script>
</body>

通过 props 向子组件传递数据 - 验证数据类型 - type

<body>
  <div id='app'>
    <cpn v-bind:cmovies='movies' :cmessage='message'></cpn>
  </div>

  <template id="cpn">
    <div>
      <h2>{{cmessage}}</h2>
      <div>{{cmovies}}</div>
    </div>
  </template>

  <script src="../dist/vue.js"></script>
  <script> const cpn = { template: '#cpn', props: { cmovies: Array, cmessage: String } } const app = new Vue({ el: '#app', data: { movies: ['明日花绮罗', '上三悠亚', '斋藤飞鸟'], message: '你好啊' }, components: { cpn: cpn } }) </script>
</body>

如果偷偷把 message 类型改一下(如下)

    const app = new Vue({
      el: '#app',
      data: {
        movies: ['明日花绮罗', '上三悠亚', '斋藤飞鸟'],
        message: ['你好啊']
      },
      components: {
        cpn: cpn
      }
    })

会尽量将数据转为 相依类型显示
同时报错

通过 props 向子组件传递数据 - 验证数据类型 - validator + required

通过 props 向子组件传递数据 - 默认值 - default

<body>
  <div id='app'>
    <cpn v-bind:cmovies='movies' :cmessage='message'></cpn>
  </div>

  <template id="cpn">
    <div>
      <h2>{{cmessage}}</h2>
      <div>{{cmovies}}</div>
    </div>
  </template>

  <script src="../dist/vue.js"></script>
  <script> const cpn = { template: '#cpn', props: { cmovies: Array, cmessage: { type: String, default: '你好骚啊' } } } const app = new Vue({ el: '#app', data: { movies: ['明日花绮罗', '上三悠亚', '斋藤飞鸟'], //message: ['你好啊'] }, components: { cpn: cpn } }) </script>
</body>

通过 props 向子组件传递数据 - 驼峰标识

结论:

  • 在不用脚手架情况下,写html标签时候,必须不能用驼峰(html不区分大小写)
  • 用了脚手架,随意()

【误区】props 双向绑定

如果我们在子组件内做 props 的双向绑定,打开控制台,发现会报错(如下图)

<body>

  <!-- 2.父组件模板 -->
  <div id='app'>
    <cpn :num1='num1'></cpn>
  </div>

  <!-- 1.子组件模板 -->
  <template id="cpn">
    <div>
      <h2>{{num1}}</h2>
      <input type="text" v-model="num1">
    </div>
  </template>

  <script src="../dist/vue.js"></script>
  <script> // 1.子组件 const cpn = { template: '#cpn', props: { num1: String } } // 2.父组件 const app = new Vue({ el: '#app', data: { num1: '1' }, components: { cpn: cpn }, }) </script>
</body>

看报错信息,知道是官方不推荐。

一个很大的原因是 :<mark>这样违背了 组件的独立性</mark>(如,子组件1改了值 ⇒ 父组件值也改 ⇒ 子组件2值也改了 ⇒ 【组件1和组件2的值绑定了(不独立)】)

报错信息里面也说了处理方法

Instead, use a data or computed property based on the prop's value. Prop being mutated: "num1"
翻译:用一个 data 或者 计算属性,接收传过来的 prop 值作为初始值。

解决方案如下

<body>

  <!-- 2.父组件模板 -->
  <div id='app'>
    <!-- 驼峰命名到html标签中,要转换成'xxx-xx' -->
    <cpn :father-num='fatherNum'></cpn>
  </div>

  <!-- 1.子组件模板 -->
  <template id="cpn">
    <div>
      <h2>{{num}}</h2>
      <input type="text" v-model="num">
    </div>
  </template>

  <script src="../dist/vue.js"></script>
  <script> // 1.子组件 const cpn = { template: '#cpn', props: { fatherNum: String }, data() { return { num: this['fatherNum'] } } } // 2.父组件 const app = new Vue({ el: '#app', data: { fatherNum: '1' }, components: { cpn: cpn }, }) </script>
</body>

通过 事件 向父组件发送消息

注意 驼峰标识 问题

<body>

  <!-- 2.父组件模板 -->
  <div id='app'>
    <cpn @item-click='cpnClick'></cpn>
  </div>

  <!-- 1.子组件模板 -->
  <template id="cpn">
    <div>
      <button v-for="item in categories" @click="btnClick(item)">{{item.name}}</button>
    </div>
  </template>

  <script src="../dist/vue.js"></script>
  <script> // 1.子组件 const cpn = { template: '#cpn', data() { return { categories: [{ id: 73944, name: 'Route' }, { id: 72827, name: 'generating' }, { id: 52405, name: 'Intelligent RAM Books' }, { id: 86224, name: 'Usability' }, ] } }, methods: { btnClick(item) { this.$emit('item-click', item); } } } // 2.父组件 const app = new Vue({ el: '#app', data: {}, components: { cpn: cpn }, methods: { cpnClick(value) { console.log(value) } }, }) </script>
</body>

父子通信案例:

  • 子元素接收父元素初始值
  • 子元素 data双向绑定
  • 父元素接收子元素修改值
<body>

  <!-- 2.父组件模板 -->
  <div id='app'>
    <h2>fatherNum:{{fatherNum}}</h2>
    <!-- 驼峰命名到html标签中,要转换成'xxx-xx' -->
    <cpn :father-num='fatherNum' @num-changed='numChangedHandler'></cpn>
  </div>

  <!-- 1.子组件模板 -->
  <template id="cpn">
    <div>
      <h2>num:{{num}}</h2>
      回车把值返回父组件:<input type="text" v-model="num" v-on:keyup.enter="numHandler(num)">
    </div>
  </template>

  <script src="../dist/vue.js"></script>
  <script> // 1.子组件 const cpn = { template: '#cpn', props: { fatherNum: String }, data() { return { num: this['fatherNum'] } }, methods: { numHandler(num) { this.$emit('num-changed', num); } }, } // 2.父组件 const app = new Vue({ el: '#app', data: { fatherNum: '1' }, components: { cpn: cpn }, methods: { numChangedHandler(value) { this.fatherNum = value; } }, }) </script>
</body>

## 子组件最顶层只能有一个element

否则报错(如下)

(下面)才是正确的

## 父子组件之间 - 访问方式

有时候我们需要父组件直接访问子组件,子组件直接访问父组件,或者是子组件访问跟组件。

  • 父组件访问子组件:使用 $children$refs
  • 子组件访问父组件:使用 $parent

$children - 父访子 - 【不常用,常用 ref】

我们先来看下 $children 的访问
this.$children 是一个数组类型,它包含所有子组件对象。
我们这里通过一个遍历,取出所有子组件的 message 状态。

<body>

  <!-- 2.父组件模板 -->
  <div id='app'>
    <cpn></cpn>
    <cpn></cpn>
    <cpn></cpn>
    <button @click='btnClick'>打印子组件</button>
  </div>

  <!-- 1.子组件模板 -->
  <template id="cpn">
    <div>我是子组件</div>
  </template>

  <script src="../dist/vue.js"></script>
  <script> const app = new Vue({ el: '#app', data: {}, methods: { btnClick() { console.log('展示子组件:', this.$children) this.$children[0].showMessage(); } }, components: { cpn: { template: '#cpn', methods: { showMessage() { console.log('showMessage'); } }, } } }) </script>
</body>

ref - 父访子

$children 的缺陷:

  • 通过 $children 访问子组件时,是一个数组类型,访问其中的子组件必须通过索引值。
  • 当子组件过多,我们需要拿到其中一个时,往往不能确定它的索引值,甚至还可能会发生变化。
  • 有时候,我们想明确获取其中一个特定的组件,这个时候就可以使用 $refs

$refs 的使用:

  • $refsref 指令通常是一起使用的。
  • 首先,我们通过ref给某一个子组件绑定一个特定的ID。
  • 其次,通过 this.$refs.ID 就可以访问到该组件了。
<body>

  <!-- 2.父组件模板 -->
  <div id='app'>
    <cpn></cpn>
    <cpn ref="cpn"></cpn>
    <cpn></cpn>
    <button @click='btnClick'>打印子组件</button>
  </div>

  <!-- 1.子组件模板 -->
  <template id="cpn">
    <div>我是子组件</div>
  </template>

  <script src="../dist/vue.js"></script>
  <script> const app = new Vue({ el: '#app', data: {}, methods: { btnClick() { console.log('展示子组件:', this.$children) console.log('展示 $refs.cpn :', this.$refs['cpn']) console.log('展示 $refs.cpn.$el :', this.$refs['cpn'].$el) this.$refs.cpn.showMessage(); } }, components: { cpn: { template: '#cpn', methods: { showMessage() { console.log('showMessage'); } }, } } }) </script>
</body>

$parent - 子访问父

如果我们想在子组件中直接访问父组件,可以通过 $parent

注意事项:

  • 尽管在Vue开发中,我们允许通过 $parent 来访问父组件,但是在真实开发中尽量不要这样做。
  • <mark>子组件应该尽量避免直接访问父组件的数据,因为这样耦合度太高了</mark>。
  • 如果我们将子组件放在另外一个组件之内,很可能该父组件没有对应的属性,往往会引起问题。
  • <mark>另外,更不好做的是通过 $parent`` 直接修改父组件的状态,那么父组件中的状态将变得飘忽不定,很不利于我的调试和维护。</mark>
<body>

  <!-- 2.父组件模板 -->
  <div id='app'>
    <cpn></cpn>
  </div>

  <!-- 1.子组件模板 -->
  <template id="cpn">
    <div>
      <button @click='btnClick'>点击 - 控制台打印父组件</button>
    </div>
  </template>

  <script src="../dist/vue.js"></script>
  <script> const cpn = { template: '#cpn', methods: { btnClick() { console.log(this.$parent); } }, } const app = new Vue({ el: '#app', data: {}, components: { cpn: cpn } }) </script>
</body>

$root 访问根组件

根组件也就是 我们 new 出来的 Vue

用 vue dev-tool 看腋下

上面界面 代码如下

<body>

  <!-- 2.父组件模板 -->
  <div id='app'>
    <p>root</p>
    <cpn></cpn>
    <p>root</p>
  </div>

  <!-- 1.子组件模板 -->
  <template id="cpn">
    <div>
      <button @click='btnClick'>点击 - 控制台打印【 根 root 】组件</button>
    </div>
  </template>

  <script src="../dist/vue.js"></script>
  <script> const cpn = { template: '#cpn', methods: { btnClick() { console.log(this.$root); } }, } const app = new Vue({ el: '#app', data: {}, components: { cpn: cpn } }) </script>
</body>

我们点击一下,就能访问到 root 组件了(跟上面例子结果一样,是因为这个的父组件就是根组件,感兴趣套多层组件看看)