美文网首页
Vue组件化

Vue组件化

作者: Imkata | 来源:发表于2022-04-19 15:16 被阅读0次

组件 (Component) 是 Vue.js 最强大的功能之一,组件可以扩展 HTML 元素,封装可重用的代码。

1 - 组件化规范:Web Components

我们自定义组件的问题:

  • 我们希望尽可能多的重用代码
  • 自定义组件的方式不太容易(需要写html、css和js代码)
  • 多次使用组件可能导致冲突

Web Components 通过创建封装好功能的定制元素解决上述问题,官网:https://developer.mozilla.org/zh-CN/docs/Web/Web_Components,虽然这个规范还没有被浏览器广泛支持,但是Vue部分实现了上述规范。

下面我们介绍的组件是传统组件,以后我们会学到单文件组件,以后我们基本上都使用单文件组件

2 - 全局组件

Vue.component('组件名称', {
 data: function(){
   return {
     msg: '组件数据'
   }
 },
 template: '<div>{{组件模板内容}}</div>'
});

全局组件注册后,任何vue实例都可以用。

组件注意事项:

  1. 组件参数data值必须是函数,同时这个函数要求返回一个对象
    因为只有是函数,才能保证函数中的数据是一个独立的封闭环境,这样各个组件的数据才互不影响。
    其实vue实例也是一个组件,只不过它的data是个对象,而我们自定义的组件的data是个函数,函数返回一个对象。
  2. 组件模板template必须是单个根元素
    就是模板必须是一个根标签,至于根标签里面什么内容就随你了。其实vue实例也是一个组件,比如下面代码它也有一个根标签:<div id="app">。
  3. 组件模板的内容可以使用模板字符串
    因为如果模板内容比较多,都写在一行肯定看不懂,所以可以使用模板字符串,模板字符串需要浏览器的支持,因为是ES6语法,模板字符串使用反引号 ``。
  4. 组件命名方式:
    短横线方式 Vue.component('my-component', { /* ... */ }),使用这个组件的时候也要是短横线。
    驼峰方式 Vue.component('MyComponent', { /* ... */ }),使用这个组件的时候可以是短横线也可以是驼峰。
    推荐使用驼峰方式。
<div id="app">
   <!-- 
    3. 使用组件
    组件可以重复使用多次 
    因为data中返回的是一个对象所以每个组件中的数据是私有的
    即每个实例可以维护一份被返回对象的独立的拷贝   
   --> 
  <buttonCounter></buttonCounter>
  <buttonCounter></buttonCounter>
  <buttonCounter></buttonCounter>
</div>

<script type="text/javascript">
  
  // 2. 注册全局组件
  Vue.component('buttonCounter', {
    // 1. 定义全局组件
    // 组件参数的data值必须是函数,同时这个函数要求返回一个对象  
    data: function(){
      return {
        count: 0
      }
    },
    // 组件模板必须是单个根元素
    // 组件模板的内容可以是模板字符串
    // 注意:@click="handle"这里要使用双引号,因为模板使用的是单引号`
    template: `
      <div>
        <button @click="handle">点击了{{count}}次</button>
      </div>
    `,
    methods: {
      handle: function(){
        this.count += 2;
      }
    }
  });
  
  var vm = new Vue({
    el: '#app',
    data: {
    }
  });
</script>

3 - 局部组件

局部组件只能在当前注册它的vue实例(父组件)中使用,其他地方是使用不了的,比如如果再定义一个全局组件,在这个全局组件中就不能使用下面定义的局部组件HelloWorld,因为局部组件HelloWorld只能在id="app"父组件中使用。

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Document</title>
</head>
<body>
  <div id="app">
    <!-- 3. 局部组件只能在注册他的父组件中使用 -->
    <HelloWorld></HelloWorld>
  </div>
  <script type="text/javascript" src="js/vue.js"></script>
  <script type="text/javascript">
    // 1. 定义局部组件
    var HelloWorld = {
      data: function(){
        return {
          msg: 'HelloWorld'
        }
      },
      template: '<div>{{msg}}</div>'
    };
    var vm = new Vue({
      el: '#app',
      data: {
        
      }
      // 2. 注册局部组件
      components: {
        'HelloWorld': HelloWorld,
      }
    });
  </script>
</body>
</html>

4 - Vue调试工具

组件最终也会渲染成原生的DOM元素,但是我们并不知道哪些元素属于这个组件,哪些元素属于那个组件,为了更直观的展示组件之间的层次关系,并且对组件的数据进行调试,我们可以借助Vue调试工具。

1. Vue调试工具安装

// ① 下载调试工具包
到 https://github.com/vuejs/vue-devtools.git
下载调试工具包,记住一定要下正式版,比如选择Tags/v5.1.1

// ② 安装依赖包
cd /Users/zjnx1111/Desktop/vue-devtools 
sudo npm install

// ③ 构建
npm run build

// ④ 点击浏览器右上 ···-更过工具-扩展程序,打开Chrome扩展页面

// ⑤ 打开开发者模式

// ⑥ 加载已解压的扩展程序,选择shells/chrome即可

选择之后如果扩展程序中有了,那么就代表安装成功,如下图:

2. 调试工具用法

右键检查之后,点击Vue,在左边就可以看到组件之间的层次关系,并且在右边可以直接看到并且修改组件的数据,组件的数据修改之后,页面的内容也会变化。

5 - Vue组件之间传值

1. 父组件向子组件传值

① 使用props传值

父组件发送的数据是以属性绑定(v-bind:)的形式绑定值到子组件身上,然后子组件用属性props接收。

<div id="app">
   <div>{{pmsg}}</div>
   <!-- menuItem 在"app"中嵌套着,故menuItem为子组件 -->
   <!-- 1. 如果加冒号:,就是需要动态数据的时候,需要动态属性绑定的形式设置,此时ptitle来自父组件data中的 ptitle数据,传的值可以是数字、对象、数组等等
   -->
   <!-- 2. 如果不加冒号:,代表给子组件传入一个写死静态的值 -->
   <!-- :title='ptitle' 就是 v-bind:title='ptitle' -->
   <menuItem :title='ptitle' content='hello'></menuItem>
</div>

<script type="text/javascript">
  Vue.component('menuItem', {
    // 3. 子组件用属性props接收父组件传递过来的数据  
    props: ['title', 'content'],
    data: function() {
      return {
        msg: '子组件本身的数据'
      }
    },
    template: '<div>{{msg + "----" + title + "-----" + content}}</div>'
    // 子组件本身的数据----动态绑定属性----hello
  });
  var vm = new Vue({
    el: '#app',
    data: {
      // 父组件的数据
      pmsg: '父组件中内容',
      ptitle: '动态绑定属性'
    }
  });
</script>

注意:上面我们说了组件命名推荐使用驼峰方式,props属性名也一样,也是推荐使用驼峰方式。

② props属性值类型

props属性值类型可以是String、Number、Boolean、Array、Object。

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Document</title>
</head>
<body>
  <div id="app">
    <div>{{pmsg}}</div>
    <!-- 
      注意:对于数值和布尔值,如果加上冒号就是v-bind绑定,会保留原来的类型(数值或者布尔值),如果不加冒号就是自定义属性,就是字符串。
            比如下面,:pnum='12',就是数值,所以在模板中可以使用 12 + pnum 进行计算。
      对于 pboo='true',就是字符串,打印类型 typeof pboo 也是字符串。
            如果写成 pboo='aaa', 因为不是属性绑定, 那么就是字符串'aaa',字符串的bool值也是真,所以这样写也生效,但是是错误的。

      所以切记不能忘记加冒号:。
    -->
    <menuItem :pstr='pstr' :pnum='12' pboo='true' :parr='parr' :pobj='pobj'></menuItem>
  </div>
  <script type="text/javascript" src="js/vue.js"></script>
  <script type="text/javascript">
    // props属性值类型
    Vue.component('menuItem', {
      props: ['pstr','pnum','pboo','parr','pobj'],
      template: `
        <div>
          # 传递字符串
          <div>{{pstr}}</div>
          # 传递数值
          <div>{{12 + pnum}}</div>    
          # 传递Bool
          <div>{{typeof pboo}}</div>
          <ul>
            # 传递数组
            <li :key='index' v-for='(item,index) in parr'>{{item}}</li>
          </ul>
            # 传递对象
            <span>{{pobj.name}}</span>
            <span>{{pobj.age}}</span>
          </div>
        </div>
      `
    });
    var vm = new Vue({
      el: '#app',
      data: {
        pmsg: '父组件中内容',
        pstr: 'hello',
        parr: ['apple','orange','banana'],
        pobj: {
          name: 'lisi',
          age: 12
        }
      }
    });
  </script>
</body>
</html>

注意:上面把props放到数组中的方式是以前旧的方式,虽然可以传递String、Number、Boolean、Array、Object类型的数据,但是每个props具体的类型还是没那么清晰的看到,所以现在我们都使用如下方式,当props是个对象就可以指定类型和默认值,更清晰明了,推荐下面这种方式。

props: {
  inputType: {
    type: String,
    default: "text" //默认值
  },
  label: String,
  value: String,
  placeholder: String,
  icon: String, //右边图标的名称
  textarea: String, //输入域文字
  tags: Array, //tag数组
  sex: String //选中的性别
},

2. 子组件向父组件传值

从上面我们知道,父组件可以通过props向子组件传值,那么我们是不是可以在子组件中操作父组件传过来的props值,然后修改,这样父组件的数据也会变化,父组件的UI也会刷新,从而达到子组件向父组件传值的目的呢?其实是可以的(代码就省略了),但是这样数据的逻辑就比较复杂难以控制了,不推荐使用。所以Vue的设计者推荐我们使用:props单向数据流的传递原则,不让你改。

Vue提供了:子组件通过自定义事件向父组件传递信息的方式

  • 子组件用$emit()触发事件,$emit() 第一个参数为自定义的事件名称,第二个参数为需要传递的数据
  • 父组件用 v-on 监听子组件的事件

子组件点击按钮会控制父组件文字的大小,代码如下:

<div id="app">
  <div :style='{fontSize: fontSize + "px"}'>{{pmsg}}</div>
  <!-- 2. 父组件用 v-on 监听子组件的事件 -->
  <menuItem :parr='parr' @enlargeText='enlargeText'></menuItem>
</div>
<script type="text/javascript" src="js/vue.js"></script>
<script type="text/javascript">
  // 子组件向父组件传值
  Vue.component('menuItem', {
    props: ['parr'],
    emits: ['enlargeText']
    template: `
      <div>
        <ul>
          <li :key='index' v-for='(item,index) in parr'>{{item}}</li>
        </ul>
        # 1. 子组件通过自定义事件向父组件传递信息
        <button @click='$emit('enlargeText', {font: 10, inputValue: $event.target.value})'>扩大父组件中字体大小</button>
      </div>
    `
  });
  var vm = new Vue({
    el: '#app',
    data: {
      pmsg: '父组件中内容',
      parr: ['apple','orange','banana'],
      fontSize: 10
    },
    methods: {
      // 3. 父组件的处理函数处理数据
      enlargeText: function(val) {
        // 扩大字体大小
        this.fontSize += val.font;
        // 如果是input框,这样可以拿到输入的值
        console.log(val.inputValue);
      }
    }
  });
</script>

注意:上面的methods是使用name:function() {}的方式,这种方式是ES5的语法,ES6我们可以使用name() {}的方式(推荐使用)如下:

methods: {
  // 不加function
  enlargeText(val) {
    this.fontSize += val.font;
    console.log(val.inputValue);
  }
}

3. 兄弟组件之间的传递

  • 兄弟之间传递数据需要通过事件中心,提供事件中心 var hub = new Vue()
  • 传递数据方,通过一个事件触发hub.$emit('方法名',传递的数据)
  • 接收数据方,通过mounted(){} 钩子中,触发hub.$on('方法名',传递的数据)方法
  • 销毁事件,通过hub.$off('方法名')销毁之后无法进行传递数据
<div id="app">
  <div>父组件</div>
  <div>
    <button @click='handleDestory'>点击销毁事件</button>
  </div>
  <!-- 兄弟组件Tom -->
  <testTom></testTom>
  <!-- 兄弟组件Jerry -->
  <testJerry></testJerry>
</div>
<script type="text/javascript" src="js/vue.js"></script>
<script type="text/javascript">
  /*
    兄弟组件之间数据传递
  */
  // 1、提供事件中心
  var hub = new Vue();

  // 兄弟组件Tom
  Vue.component('testTom', {
    data: function(){
      return {
        num: 0
      }
    },
    template: `
      <div>
        <div>TOM:{{num}}</div>
        <div>
          <button @click='handleEvent'>点击传递事件</button>
        </div>
      </div>
    `,
    methods: {
      handleEvent: function(){
        // 2、传递数据方,通过一个事件触发hub.$emit(方法名,传递的数据) 触发兄弟组件的事件
        hub.$emit('jerryEvent', 2);
      }
    },
    mounted: function() {
      // 5、接收数据方,通过mounted(){} 钩子中  触发hub.$on(方法名
      hub.$on('tomEvent', (val) => {
        this.num += val;
      });
    }
  });
  
  // 兄弟组件Jerry
  Vue.component('testJerry', {
    data: function(){
      return {
        num: 0
      }
    },
    template: `
      <div>
        <div>JERRY:{{num}}</div>
        <div>
          <button @click='handleEvent'>点击传递事件</button>
        </div>
      </div>
    `,
    methods: {
      handleEvent: function(){
        // 4、传递数据方,通过一个事件触发hub.$emit(方法名,传递的数据) 触发兄弟组件的事件
        hub.$emit('tomEvent', 1);
      }
    },
    mounted: function() {
      // 3、接收数据方,通过mounted(){} 钩子中  触发hub.$on()方法名
      // 箭头函数中this指向父级作用域的this,在这里就是vm实例,如果使用function函数,this就指向window了 
      hub.$on('jerryEvent', (val) => {
        this.num += val;
      });
    }
  });
  var vm = new Vue({
    el: '#app',
    data: {

    },
    methods: {
      handleDestory: function(){
        // 6、销毁事件 通过hub.$off('方法名')销毁之后无法进行传递数据  
        hub.$off('tomEvent');
        hub.$off('jerryEvent');
      }
    }
  });
</script>

兄弟组件之间的传值我们很少使用,因为我们有Vuex。

6 - 组件插槽

有时候一些子组件很相似,如果写成不同的组件,可能又会很浪费,所以我们用到了组件插槽。

组件插槽的作用:父组件向子组件传递内容,这里的内容指的是模板内容

组件插槽就相当于一个在子组件模板中的占位符,父组件使用子组件的时候再指定占位符的内容是什么,比如下面的子组件就会把<slot></slot>替换成hi、hello、haha等。

组件的最大特性就是复用性,而用好插槽能大大提高组件的可复用能力。

1. 匿名插槽

匿名插槽就是没有名字的插槽,使用匿名插槽可以设置默认内容。

<div id="app">
  <!-- 2. 插槽内容 -->  
  <!-- 这里的所有组件标签中嵌套的内容会替换掉slot,如果没值,则使用slot中的默认值  -->  
  <alertBox>有bug发生</alertBox>
  <alertBox>有一个警告</alertBox> 
  <alertBox></alertBox>
</div>

<script type="text/javascript">
  /*
    匿名插槽
  */
  Vue.component('alertBox', {
    template: `
      <div>
        <strong>ERROR:</strong>
        # 1. 插槽定义位置:子组件中
                # 当组件渲染的时候,这个 <slot> 元素将会被替换为“组件标签中嵌套的内容”。
        # 插槽内可以包含任何模板代码,包括 HTML
        <slot>默认内容</slot>
      </div>
    `
  });
  var vm = new Vue({
    el: '#app',
    data: {

    }
  });
</script>

2. 具名插槽:name

具名插槽就是具有名字的插槽,使用 <slot> 中的 "name" 属性绑定元素。因为有名字了,当有多个插槽的时候就能把不同的内容插入不同的插槽。

<div id="app">
  <baseLayout>
     <!-- 2、插槽内容 --> 
     <!-- 通过slot属性来指定, 这个slot的值必须和下面slot组件得name值对应上,如果没有匹配到,则放到匿名的插槽中 --> 
    <p slot='header'>标题信息</p>
    <p>主要内容1</p>
    <p>主要内容2</p>
    <p slot='footer'>底部信息信息</p>
  </baseLayout>
  <!-- 结果如下图:header里面有一个p,main里面有两个p,footer里面有一个p -->  

  <!-- 3、如果想往插槽中填充多个标签就可以使用template包裹一下 -->  
  <baseLayout>
    <!-- 注意:template临时的包裹标签最终不会渲染到页面上,只是包裹一下 -->  
    <template slot='header'>
      <p>标题信息1</p>
      <p>标题信息2</p>
    </template>
    <p>主要内容1</p>
    <p>主要内容2</p>
    <template slot='footer'>
      <p>底部信息信息1</p>
      <p>底部信息信息2</p>
    </template>
  </baseLayout>
  <!-- 结果如下图:header里面有两个p,main里面有两个p,footer里面有两个p -->  
</div>

<script type="text/javascript" src="js/vue.js"></script>
<script type="text/javascript">
  /*
    具名插槽
  */
  Vue.component('baseLayout', {
    template: `
      <div>
        <header>
          ###   1、插槽定义位置:子组件中。使用 <slot> 中的 "name" 属性绑定元素 指定当前插槽的名字
          <slot name='header'></slot>
        </header>
        <main>
          ### 匿名插槽
          <slot></slot>
        </main>
        <footer>
          ###  注意:具名插槽的渲染顺序,完全取决于模板,而不是取决于父组件中元素的顺序
          <slot name='footer'></slot>
        </footer>
      </div>
    `
  });
  var vm = new Vue({
    el: '#app',
    data: {

    }
  });
</script>

3. 作用域插槽:slot-scope

应用场景:父组件对子组件的内容进行加工处理,既可以复用子组件的slot,又可以使slot内容不一致。

比如:当我们希望li的样式由外部使用组件的地方控制(比如希望某一个li高亮显示),因为可能有多种地方要使用该组件,而且希望样式不一样,这个时候我们需要使用作用域插槽。

<div id="app">
  <fruitList :list='list'> <!-- 父组件向子组件传值 --->  
    <!-- 2. 插槽内容 -->
    <!-- 父组件中使用了<template>元素,而且包含slot-scope='slotProps',slotProps在这里只是临时变量,用来获取子组件的item数据
    ---> 
    <template slot-scope='slotProps'>
       <!-- 如果id为2,就设置class为current -->
       <strong v-if='slotProps.info.id==3' class="current">
          {{slotProps.info.name}}                
       </strong>
       <!-- 否则直接显示name -->
       <span v-else>{{slotProps.info.name}}</span>
    </template>
  </fruitList>
</div>

<script type="text/javascript" src="js/vue.js"></script>
<script type="text/javascript">
  /*
    作用域插槽
  */
  Vue.component('fruitList', {
    props: ['list'],
    template: `
      <div>
        <li :key='item.id' v-for='item in list'>
                ### 1. 插槽定义位置:子组件中
                    ### 在子组件模板中,绑定属性 :info='item' 其中info是自定义的,在父组件中使用slotProps.info可以取到,item就是上面的item。
          <slot :info='item'>我是默认内容</slot>
        </li>
      </div>
    `
  });
  var vm = new Vue({
    el: '#app',
    data: {
      list: [{
        id: 0,
        name: 'apple'
      },{
        id: 1,
        name: 'orange'
      },{
        id: 2,
        name: 'banana'
      }]
    }
  });
</script>

7 - 购物车案例

代码地址:Vue案例

1. 实现组件化布局

  1. 编写静态页面代码
  2. 把静态页面转换成组件化模式
  3. 把组件渲染到页面上

购物车组件是个大组件,里面有三个小组件,分别是标题组件、商品列表组件、商品结算组件。

<div id="app">
  <div class="container">
    <!-- 2、把组件渲染到页面上 --> 
    <my-cart></my-cart>
  </div>
</div>
<script type="text/javascript" src="js/vue.js"></script>
<script type="text/javascript">
  # 1、 把静态页面转换成组件化模式
  # 1.1  标题组件
  var CartTitle = {
    template: `
      <div class="title">我的商品</div>
    `
  }
  # 1.2  商品列表组件 
  var CartList = {
    #  注意点 :组件模板必须是单个根元素  
    template: `
      <div>
        <div class="item">
          <img src="img/a.jpg"/>
          <div class="name"></div>
          <div class="change">
            <a href="">-</a>
            <input type="text" class="num" />
            <a href="">+</a>
          </div>
          <div class="del">×</div>
        </div>
        <div class="item">
          <img src="img/b.jpg"/>
          <div class="name"></div>
          <div class="change">
            <a href="">-</a>
            <input type="text" class="num" />
            <a href="">+</a>
          </div>
          <div class="del">×</div>
        </div>
        <div class="item">
          <img src="img/c.jpg"/>
          <div class="name"></div>
          <div class="change">
            <a href="">-</a>
            <input type="text" class="num" />
            <a href="">+</a>
          </div>
          <div class="del">×</div>
        </div>
        <div class="item">
          <img src="img/d.jpg"/>
          <div class="name"></div>
          <div class="change">
            <a href="">-</a>
            <input type="text" class="num" />
            <a href="">+</a>
          </div>
          <div class="del">×</div>
        </div>
        <div class="item">
          <img src="img/e.jpg"/>
          <div class="name"></div>
          <div class="change">
            <a href="">-</a>
            <input type="text" class="num" />
            <a href="">+</a>
          </div>
          <div class="del">×</div>
        </div>
      </div>
    `
  }
  # 1.3  商品结算组件 
  var CartTotal = {
    template: `
      <div class="total">
        <span>总价:123</span>
        <button>结算</button>
      </div>
    `
  }
  ## 1.4  定义一个全局组件 my-cart
  Vue.component('my-cart',{
    ##  1.6 引入子组件  
    template: `
      <div class='cart'>
        <cart-title></cart-title>
        <cart-list></cart-list>
        <cart-total></cart-total>
      </div>
    `,
    # 1.5  注册局部子组件   
    components: {
      'cart-title': CartTitle,
      'cart-list': CartList,
      'cart-total': CartTotal
    }
  });
  var vm = new Vue({
    el: '#app',
    data: {

    }
  });
</script>

2. 实现标题和结算功能组件

  1. 从父组件把标题数据传递过来,即父向子组件传值,把传递过来的数据渲染到标题组件上
  2. 从父组件把商品列表list 数据传递过来,即父向子组件传值,把传递过来的数据计算最终价格渲染到结算功能组件上

使用到了父组件向子组件传值。

<div id="app">
  <div class="container">
    <my-cart></my-cart>
  </div>
</div>
<script type="text/javascript" src="js/vue.js"></script>
<script type="text/javascript">
  var CartTitle = {
    # 3.1 标题组件 子组件通过props形式接收父组件传递过来的uname数据
    props: ['uname'],
    # 3.2 将父组件传过来的uname渲染到页面上 
    template: `
      <div class="title">{{uname}}的商品</div>
    `
  }
  var CartTotal = {
    # 4.1 商品结算组件 子组件通过props形式接收父组件传递过来的list数据   
    props: ['list'],
    template: `
      <div class="total">
                # 4.3 将计算属性的值渲染到页面上
        <span>总价:{{total}}</span>
        <button>结算</button>
      </div>
    `,
    computed: {
      # 4.2 这里使用计算属性,计算商品的总价
      total: function() {
        var t = 0;
        this.list.forEach(item => {
          t += item.price * item.num;
        });
        return t;
      }
    }
  }
  Vue.component('my-cart',{
    data: function() {
      return {
        # 1. 父组件中定义数据
        uname: '张三',
        list: [{
          id: 1,
          name: 'TCL彩电',
          price: 1000,
          num: 1,
          img: 'img/a.jpg'
        },{
          id: 2,
          name: '机顶盒',
          price: 1000,
          num: 1,
          img: 'img/b.jpg'
        },{
          id: 3,
          name: '海尔冰箱',
          price: 1000,
          num: 1,
          img: 'img/c.jpg'
        },{
          id: 4,
          name: '小米手机',
          price: 1000,
          num: 1,
          img: 'img/d.jpg'
        },{
          id: 5,
          name: 'PPTV电视',
          price: 1000,
          num: 2,
          img: 'img/e.jpg'
        }]
      }
    },
    # 2. 父组件向子组件以属性传递的形式 传递数据
    # 向标题组件传递 uname 属性, 向商品结算组件传递 list 属性  
    template: `
      <div class='cart'>
        <cart-title :uname='uname'></cart-title>
        <cart-list></cart-list>
        <cart-total :list='list'></cart-total>
      </div>
    `,
    components: {
      'cart-title': CartTitle,
      'cart-list': CartList,
      'cart-total': CartTotal
    }
  });
  var vm = new Vue({
    el: '#app',
    data: {

    }
  });
</script>

3. 实现列表组件删除功能

  1. 将list数组通过父组件传递给列表组件
  2. 列表组件遍历数组,渲染到界面上
  3. 点击删除按钮删除对应的数据
  • 给按钮添加点击事件,点击按钮把需要删除的id传递给父组件
  • 子组件中不推荐操作父组件的数据,因为有可能多个子组件使用父组件的数据,我们需要把数据传递给父组件让父组件自己操作数据
  • 父组件删除对应的数据

使用到了父组件向子组件传值和子组件向父组件传值。

<div id="app">
  <div class="container">
    <my-cart></my-cart>
  </div>
</div>
<script type="text/javascript" src="js/vue.js"></script>
<script type="text/javascript">

  var CartTitle = {
    props: ['uname'],
    template: `
      <div class="title">{{uname}}的商品</div>
    `
  }
  var CartList = {
    # 2. 通过props拿到传过来的值
    props: ['list'],
    template: `
      <div>
                # 3. 把列表数据动态渲染到页面上  
        <div :key='item.id' v-for='item in list' class="item">
                # src属性的绑定
          <img :src="item.img"/>
          <div class="name">{{item.name}}</div>
          <div class="change">
            <a href="">-</a>
            <input type="text" class="num" />
            <a href="">+</a>
          </div>
                # 4. 给按钮添加点击事件
          <div class="del" @click='del(item.id)'>×</div>
        </div>
      </div>
    `,
    methods: {
      del: function(id){
        # 5. 通过自定义事件把子组件的id传递给父组件,让父组件操作数据 
        # 子组件中不推荐操作父组件的数据,因为有可能多个子组件使用父组件的数据 
        this.$emit('cart-del', id);
      }
    }
  }
  var CartTotal = {
    props: ['list'],
    template: `
      <div class="total">
        <span>总价:{{total}}</span>
        <button>结算</button>
      </div>
    `,
    computed: {
      total: function() {
        // 计算商品的总价
        var t = 0;
        this.list.forEach(item => {
          t += item.price * item.num;
        });
        return t;
      }
    }
  }
  Vue.component('my-cart',{
    data: function() {
      return {
        uname: '张三',
        list: [{
          id: 1,
          name: 'TCL彩电',
          price: 1000,
          num: 1,
          img: 'img/a.jpg'
        },{
          id: 2,
          name: '机顶盒',
          price: 1000,
          num: 1,
          img: 'img/b.jpg'
        },{
          id: 3,
          name: '海尔冰箱',
          price: 1000,
          num: 1,
          img: 'img/c.jpg'
        },{
          id: 4,
          name: '小米手机',
          price: 1000,
          num: 1,
          img: 'img/d.jpg'
        },{
          id: 5,
          name: 'PPTV电视',
          price: 1000,
          num: 2,
          img: 'img/e.jpg'
        }]
      }
    },
    template: `
      <div class='cart'>
        <cart-title :uname='uname'></cart-title>
                # 1. 从父组件把商品列表list 数据传递过来 即 父向子组件传值  
          # 6. 父组件通过事件绑定,接收子组件传递过来的数据 
        <cart-list :list='list' @cart-del='delCart($event)'></cart-list>
        <cart-total :list='list'></cart-total>
      </div>
    `,
    components: {
      'cart-title': CartTitle,
      'cart-list': CartList,
      'cart-total': CartTotal
    },
    methods: {
      # 7. 父组件根据id删除list中对应的数据        
      delCart: function(id) {
        // ① 找到id所对应数据的索引
        var index = this.list.findIndex(item=>{
          return item.id == id;
        });
        // ② 根据索引删除对应数据
        this.list.splice(index, 1);
      }
    }
  });
  var vm = new Vue({
    el: '#app',
    data: {

    }
  });
</script>

4. 实现组件更新数据功能-上

  1. 将输入框中的默认数据动态渲染出来
  2. 输入框失去焦点的时候更改商品的数量,子组件中不推荐操作数据,把这些数据传递给父组件,让父组件处理这些数据,父组件中接收子组件传递过来的数据并处理

使用到了子组件向父组件传值。

<div id="app">
  <div class="container">
    <my-cart></my-cart>
  </div>
</div>
<script type="text/javascript" src="js/vue.js"></script>
<script type="text/javascript">

  var CartTitle = {
    props: ['uname'],
    template: `
      <div class="title">{{uname}}的商品</div>
    `
  }
  var CartList = {
    props: ['list'],
    template: `
      <div>
        <div :key='item.id' v-for='item in list' class="item">
          <img :src="item.img"/>
          <div class="name">{{item.name}}</div>
          <div class="change">
            <a href="">-</a>
                # 1. 将输入框中的默认数据动态渲染出来,我们的数据来自父组件,原则是不修改父组件的数据,所以不用v-model,直接使用属性绑定的方式:value='item.num'填充数据
                # 2. 输入框失去焦点的时候,更改商品的数量,需要将当前商品的id、$event事件对象传过去
            <input type="text" class="num" :value='item.num' @blur='changeNum(item.id, $event)'/>
            <a href="">+</a>
          </div>
          <div class="del" @click='del(item.id)'>×</div>
        </div>
      </div>
    `,
    methods: {
      changeNum: function(id, event){
        # 2. 把这些数据传递给父组件,让父组件处理这些数据,第一个参数是函数名,第二个参数是传过去的值,分别是商品id、商品数量
        # 子组件中不推荐操作数据,因为别的组件可能也引用了这些数据
        this.$emit('change-num', {
          id: id,
          num: event.target.value
        });
      },
      del: function(id){
        // 把id传递给父组件
        this.$emit('cart-del', id);
      }
    }
  }
  var CartTotal = {
    props: ['list'],
    template: `
      <div class="total">
        <span>总价:{{total}}</span>
        <button>结算</button>
      </div>
    `,
    computed: {
      total: function() {
        // 计算商品的总价
        var t = 0;
        this.list.forEach(item => {
          t += item.price * item.num;
        });
        return t;
      }
    }
  }
  Vue.component('my-cart',{
    data: function() {
      return {
        uname: '张三',
        list: [{
          id: 1,
          name: 'TCL彩电',
          price: 1000,
          num: 1,
          img: 'img/a.jpg'
        }]
    },
    template: `
      <div class='cart'>
        <cart-title :uname='uname'></cart-title>
        # 3. 父组件中接收子组件传递过来的数据 
        <cart-list :list='list' @change-num='changeNum($event)' @cart-del='delCart($event)'></cart-list>
        <cart-total :list='list'></cart-total>
      </div>
    `,
    components: {
      'cart-title': CartTitle,
      'cart-list': CartList,
      'cart-total': CartTotal
    },
    methods: {
      changeNum: function(val) {
        // 4. 父组件根据子组件传递过来的数据,更新list中对应的数据
        this.list.some(item=>{
          if(item.id == val.id) {
            item.num = val.num;
            // 终止遍历
            return true;
          }
        });
      },
      delCart: function(id) {
        // 根据id删除list中对应的数据
        // 找到id所对应数据的索引
        var index = this.list.findIndex(item=>{
          return item.id == id;
        });
        // 根据索引删除对应数据
        this.list.splice(index, 1);
      }
    }
  });
  var vm = new Vue({
    el: '#app',
    data: {

    }
  });
</script>

5. 实现组件更新数据功能-下

  1. 子组件通过一个标识符来标记对应的用户点击 + - 或者输入框输入的内容
  2. 父组件拿到标识符更新对应的组件

使用到了子组件向父组件传值。

<body>
  <div id="app">
    <div class="container">
      <my-cart></my-cart>
    </div>
  </div>
  <script type="text/javascript" src="js/vue.js"></script>
  <script type="text/javascript">
    
    var CartTitle = {
      props: ['uname'],
      template: `
        <div class="title">{{uname}}的商品</div>
      `
    }
    var CartList = {
      props: ['list'],
      template: `
        <div>
          <div :key='item.id' v-for='item in list' class="item">
            <img :src="item.img"/>
            <div class="name">{{item.name}}</div>
            <div class="change">
                    # 1. + - 按钮绑定事件 禁止默认行为
              <a href="" @click.prevent='sub(item.id)'>-</a>
              <input type="text" class="num" :value='item.num' @blur='changeNum(item.id, $event)'/>
              <a href="" @click.prevent='add(item.id)'>+</a>
            </div>
            <div class="del" @click='del(item.id)'>×</div>
          </div>
        </div>
      `,
      methods: {
        changeNum: function(id, event){
          this.$emit('change-num', {
            id: id,
            // 3. 添加输入的type
            type: 'change',
            num: event.target.value
          });
        },
        sub: function(id){
          # 2. 数量的增加和减少通过父组件来计算,每次都是加1和减1不需要传递数量,父组件需要一个类型来判断是加1还是减1,我们通过type标识符来标记不同的操作   
          this.$emit('change-num', {
            id: id,
            type: 'sub'
          });
        },
        add: function(id){
          this.$emit('change-num', {
            id: id,
            type: 'add'
          });
        },
        del: function(id){
          // 把id传递给父组件
          this.$emit('cart-del', id);
        }
      }
    }
    var CartTotal = {
      props: ['list'],
      template: `
        <div class="total">
          <span>总价:{{total}}</span>
          <button>结算</button>
        </div>
      `,
      computed: {
        total: function() {
          // 计算商品的总价
          var t = 0;
          this.list.forEach(item => {
            t += item.price * item.num;
          });
          return t;
        }
      }
    }
    Vue.component('my-cart',{
      data: function() {
        return {
          uname: '张三',
          list: [{
            id: 1,
            name: 'TCL彩电',
            price: 1000,
            num: 1,
            img: 'img/a.jpg'
          },{
            id: 2,
            name: '机顶盒',
            price: 1000,
            num: 1,
            img: 'img/b.jpg'
          },{
            id: 3,
            name: '海尔冰箱',
            price: 1000,
            num: 1,
            img: 'img/c.jpg'
          },{
            id: 4,
            name: '小米手机',
            price: 1000,
            num: 1,
            img: 'img/d.jpg'
          },{
            id: 5,
            name: 'PPTV电视',
            price: 1000,
            num: 2,
            img: 'img/e.jpg'
          }]
        }
      },
      template: `
        <div class='cart'>
          <cart-title :uname='uname'></cart-title>  
              # 4. 父组件通过事件监听,接收子组件的数据  
          <cart-list :list='list' @change-num='changeNum($event)' @cart-del='delCart($event)'></cart-list>
          <cart-total :list='list'></cart-total>
        </div>
      `,
      components: {
        'cart-title': CartTitle,
        'cart-list': CartList,
        'cart-total': CartTotal
      },
      methods: {
        changeNum: function(val) {
          # 5. 更新数据,分为三种情况:输入框变更、加号变更、减号变更
          if(val.type=='change') {
            // 根据子组件传递过来的数据,跟新list中对应的数据
            this.list.some(item=>{
              if(item.id == val.id) {
                item.num = val.num;
                // 终止遍历
                return true;
              }
            });
          } else if(val.type=='sub'){
            // 减一操作
            this.list.some(item=>{
              if(item.id == val.id) {
                item.num -= 1;
                // 终止遍历
                return true;
              }
            });
          } else if(val.type=='add'){
            // 加一操作
            this.list.some(item=>{
              if(item.id == val.id) {
                item.num += 1;
                // 终止遍历
                return true;
              }
            });
          }
        }
      }
    });
    var vm = new Vue({
      el: '#app',
      data: {

      }
    });
  </script>
</body>
</html>

相关文章

  • Vue组件和父子组件

    【一】Vue组件化 创建组件构造器对象Vue.extend() 创建组件构造器const cpnc = Vue.e...

  • Vue.js的组件化思想 —上

    一、Vue中的组件 Vue视图层的灵魂 — 组件化 组件(Component)是 Vue.js 最强大的功能之一...

  • (15)打鸡儿教你Vue.js

    组件化vue.js 组件单向绑定组件双向绑定组件单次绑定 创建组件构造器注册组件使用组件 Vue.extend()...

  • 大话大前端时代(一) —— Vue 与 iOS 的组件化

    大话大前端时代(一) —— Vue 与 iOS 的组件化 大话大前端时代(一) —— Vue 与 iOS 的组件化

  • vue

    1、什么是组件化、有什么好处、vue如何创建组件、vue组件之间如何通信 什么是组件化。任何一个页面我们都可以抽象...

  • Vue组件化

    定义Vue组件化 什么是组件化:组件的出现,就是为了拆分Vue实例的代码量的,能够让我们以不同的组件,来划分不同的...

  • vue-5

    组件(可复用的vue实例) 注册组件必须在Vue实例化之前全局组件(跨vue实例)组件的data选项必须是一个函数...

  • Vue实践与总结——组件与数据

    Vue实现组件化流程 Vue提供了一套构建组件的API,用于声明和实现 根组件,可复用组件 Vue库提供了名为Vu...

  • vue组件化思想

    组件化 组件化是vue的核心思想,主要是为了代码重用。 组件通信 父组件=>子组件 属性props 引用refs ...

  • vue基础概念介绍

    组件化 vue的组件化是指把传统的html, css, js资源集成到一个.vue文件中,组成一个单独的组件,被其...

网友评论

      本文标题:Vue组件化

      本文链接:https://www.haomeiwen.com/subject/ugmsertx.html