• 子组件$emit触发父组件绑定的的方法并传递数据。

this.$emit('事件',value:参数) //子传父
  • 父组件调用子组件的方法,可以传递数据。

this.$refs.子组件的ref.子组件的方法
  • 兄弟组件之间相互传递数据。//只要不是父子关系的都是兄弟关系

$on

  • 给多选按钮绑定chang事件当选中或者取消选中的时候就会触发这个事件,然后可以通过e.target.checked的值true或者false来确定是否选中.

<input type="checkbox" class="custom-control-input" id="cbFull" :checked="isfull"
             @change="fullchange"/>

methods:{
//e代表事件对象,此处e:input框
    change(e){
      this.$emit('fullchange',e.target.checked)

    }
  • 1.@click,2.@input,2.自定义事件接收到的值被覆盖,三种情况考虑$event

头条案例

  • views文件夹与components的区别

views:组件通过路由来切换

components:组件不是通过路由来切换,该组件可复用

  • 当使用第三方组件库时,有修改第三方组件默认样式的需求,需要用到/deep/

  • 文件夹utils:放工具模块

例:request.js:封装api请求

购物车案例1

  1. 工程组件化

  1. 代码书写规范:

  • 指令

  • 属性绑定

  • 绑定事件

//app.vue
<template>
  <div class="app-container">
      <Header title="购物车案例"/>

      <!--循环渲染每一个商品的信息-->
      <Goods v-for="item in list" :key="item.id" :title="item.goods_name"
             :pic="item.goods_img" :price="item.goods_price" :state="item.goods_state"/>
  </div>
</template>

<script>
    //导入axios请求库,调用方法拿数据
    import axios from 'axios'
    //导入需要的组件
    import Header from "./components/Header/Header";
    import Goods from "./components/Goods/Goods";
  export default {
    data(){
        return{
            //用来存购物车的列表数据,默认为空数组
            list:[]

        }
    },
    methods:{
        //封装请求列表数据的方法
        async initCartList() {
            //调用axios的get方法,返回值为promise
            //将axios请求封装到一个函数中,一会到created里面调用这个函数
            //结构赋值axios中有六个属性,其中我们需要的是data属性,重命名为res
            const {data: res} = await axios.get('https://www.escook.cn/api/cart')
            if (res.status === 200) {
                this.list = res.list
            }
        }
    },
      created() {
          //调用请求数据的方法
          this.initCartList()//组件在内存中一被创建好,就调用initCartList()方法,执行方法中的代码开始请求数据
          //只要请求回来的数据,在页面渲染中需要用到,就必须转存到data中
      },
    components:{
      Header,Goods
    }
  }
</script>

<style lang="less">
    .app-container{
        padding-top: 45px;
        padding-bottom: 50px;
    }

</style>
//Header.vue
<template>
  <div class="header-container">{{title}}</div>
</template>

<script>
export default {
  props:{
    title:{
      default:'',
      type:String
    }
  }

}
</script>

<style lang="less" scoped>
.header-container {
  font-size: 12px;
  height: 45px;
  width: 100%;
  background-color: #1d7bff;
  display: flex;
  justify-content: center;
  align-items: center;
  color: #fff;
  position: fixed;
  top: 0;
}
</style>
//Goods.vue
<template>
  <div class="goods-container">
    <!-- 左侧图片 -->
    <div class="thumb">
      <div class="custom-control custom-checkbox">
        <!-- 复选框 勾选状态-->
        <input type="checkbox" class="custom-control-input" id="cb1" :checked="state" />
        <label class="custom-control-label" for="cb1">
          <!-- 商品的缩略图 -->
          <img :src="pic" alt="" />
        </label>
      </div>
    </div>
    <!-- 右侧信息区域 -->
    <div class="goods-info">
      <!-- 商品标题 -->
      <h6 class="goods-title">{{title}}</h6>
      <div class="goods-info-bottom">
        <!-- 商品价格 -->
        <span class="goods-price">{{price}}¥</span>
        <!-- 商品的数量 -->
      </div>
    </div>
  </div>
</template>

<script>
export default {
  props:{
    //要渲染的商品的标题
    title:{
      default:'',
      type:String
    },
    pic:{
      default: '',
      type:String
    },
    price:{
      default:'',
      type:Number
    },
    //商品的勾选状态
    state:{
      default:true,
      type:Boolean
    }
  }
}
</script>

<style lang="less" scoped>
.goods-container {
   .goods-container {
    border-top: 1px solid #efefef;
  }
  padding: 10px;
  display: flex;
  .thumb {
    display: flex;
    align-items: center;
    img {
      width: 100px;
      height: 100px;
      margin: 0 10px;
    }
  }

  .goods-info {
    display: flex;
    flex-direction: column;
    justify-content: space-between;
    flex: 1;
    .goods-title {
      font-weight: bold;
      font-size: 12px;
    }
    .goods-info-bottom {
      display: flex;
      justify-content: space-between;
      .goods-price {
        font-weight: bold;
        color: red;
        font-size: 13px;
      }
    }
  }
}
</style>
  • 封装props的两种方法

  props:{
 //一个个属性封装,一个个传
    title:{
      default:'',
      type:String
    },
    pic:{
      default: '',
      type:String
    },
    price:{
      default:'',
      type:Number
    },
    //商品的勾选状态
    state:{
      default:true,
      type:Boolean
    }
  }
//传数据
<Goods v-for="item in list" :key="item.id" :title="item.goods_name"
             :pic="item.goods_img" :price="item.goods_price" :state="item.goods_state"/>
props:{
//封装一个goods对象
 goods:{
   type:object,
   default:{}
}
//传数据
<Goods v-for="item in list" :key="item.id" :goods="item"/>
//此时
      <h6 class="goods-title">{{goods.goods_title}}</h6>
      <div class="goods-info-bottom">
        <!-- 商品价格 -->
        <span class="goods-price">{{goods.goods_price}}¥</span>
  • 为什么用v-bind绑定state而不用v-model:因为state是props而不是form,是只读的

  • 结论:在做项目开发时不要传一整个对象过来,最好把一个一个属性分开传过来,更加通用

改进1:实现监听Goods中商品复选框变化,将值同步到App中

  • 子传父(自定义事件):将子组件Goods中的变化传到父组件App中

  • 子组件item项中是否勾选要同步到父组件的数组中

  • 还要确定是哪个商品要勾选

所以需传值:商品Id和商品勾选状态

  1. 在子组件中,要监听复选框状态变化的事件,拿到最新的勾选状态

只要复选框的勾选状态发生变化,自动触发change事件

<input type="checkbox" @change="stateChange"/>

  1. 当监听到勾选状态发生变化后,应该立即把最新状态,通过自定义事件的形式发送给父组件

this.$emit('stateChange',{id,value})

id:商品id,value:商品最新勾选状态

3.

    • e.target是目标对象,如点击事件就是点击的DOM元素。e.target.value就是目标对象的值。
    • e.event是目标所发生的事件。
    • this返回的是绑定事件的对象(元素)
//Goods.vue
<template>
        <!-- 复选框 勾选状态-->
        <input type="checkbox" class="custom-control-input" id="cb1" :checked="state"
               @change="stateChange"/> //自定义事件
</template>

<script>
export default {
  props:{
    //商品的Id
    id:{
      //require:true,必须传商品的id,否则报错
      require:true,
      type:Number
    },
    //要渲染的商品的标题
    title:{
      default:'',
      type:String
    },
    pic:{
      default: '',
      type:String
    },
    price:{
      default:'',
      type:Number
    },
    //商品的勾选状态
    state:{
      default:true,
      type:Boolean
    }
  },
  methods:{
    //只要复选框的选择状态发生变化,就会调用这个处理函数
    stateChange(e)
    {//声明一个变量newState
      const newState = e.target.checked
//通过勾选状态触发自定义事件state-change 参数为id,newstate
      this.$emit('state-change',{id:this.id, value:newState})
    }
  }
}

</script>
//App.vue
<template>
  <div class="app-container">
      <Header title="购物车案例"/>

      <!--循环渲染每一个商品的信息-->
      <Goods v-for="item in list" :key="item.id"  :id="item.id" :title="item.goods_name"
             :pic="item.goods_img" :price="item.goods_price" :state="item.goods_state"
             @state-change="getNewState"/>
//通过自定义事件state-change监听
  </div>
</template>

<script>
    //导入axios请求库,调用方法拿数据
    import axios from 'axios'
    //导入需要的组件
    import Header from "./components/Header/Header";
    import Goods from "./components/Goods/Goods";
  export default {
    data(){
        return{
            //用来存购物车的列表数据,默认为空数组
            list:[]

        }
    },
    methods:{
        getNewState(val){
            //接受子组件传来的数据

        }
     }
  }
</script>

改进2:实现运用Goods子组件传给App父组件的id在list[]数组中查找对应商品,查找到对应商品就将其勾选状态改成传来的value的状态

id:Goods->App(子传父)

App中获取了商品信息的数据,但是商品的属性(id,goods_price等都在Goods中,所以要从Goods传给App)

全选状态:App->Footer(父传子)

//App.vue
<Goods v-for="item in list" :key="item.id" :id="item.id" :title="item.goods_name"
             :pic="item.goods_img" :price="item.goods_price" :state="item.goods_state"
             @state-change="getNewState"/>
<Footer :isfull="fullstate"></Footer>


getNewState(val){
            //接受子组件传来的数据
            this.list.some(item =>{
                if (item.id === val.id){
                    //终止后续循环
                    item.goods_state = val.value
                    return true
                }
            })

        },
computed:{
        //动态计算出全选的状态是true还是false
        fullstate(){
            //每个商品的状态是否都为true,如果全都为true则返回true否则返回false
            return this.list.every( item => item.goods_state === true)
        }
//Footer.vue
<template>
  <div class="footer-container">
      <input type="checkbox" class="custom-control-input" id="cbFull" :checked="isfull" />    
  </div>
</template>

<script>
export default {
  props:{
    //全选的状态
    isfull:{
      type:Boolean,
      default:false
    }
  }
}
</script>

改进4:Footer组件实现全选状态,全选框勾选:所有商品勾选;全选框不勾选:所有商品不勾选

实现子组件(Footer)状态变化传到父组件(App)中的每一件商品

  • 监听Footer复选框的change事件,拿到状态变化,子传父(通过自定义事件)传给父组件

  • 父组件用子组件Footer时绑定事件监听,拿到最新状态

  • Foreach循环

//Footer组件
<input type="checkbox" class="custom-control-input" id="cbFull" :checked="isfull"
             @change="fullchange"/>

methods:{
    fullchange(e){//方法名
      this.$emit('full_change',e.target.checked)//自定义事件,全选复选框是否勾选
    }
  }
//App.vue
<Footer :isfull="fullstate" @full_change="getFullState"></Footer>

getFullState(val){
            this.list.forEach(item=>(item.goods_state=val))//每个商品的状态都与全选框状态相同
        }

改进5:实现总价功能:将勾选商品的价格加起来

  • 在App中通过计算属性计算出值

  • 父传子(自定义属性)再将值传给子组件Footer

//App.vue
   <p>{{totalprice}}</p>
//父传子通过自定义属性
<Footer :isfull="fullstate" @full_change="getFullState" :amount="totalprice"></Footer>

computed:{
        //动态计算出全选的状态是true还是false
        fullstate(){
            //每个商品的状态是否都为true
            return this.list.every( item => item.goods_state === true)
        },
        //已勾选商品的总价格
        //1.filter过滤出选中的商品
        //2.reduce累加器:reduce((累加结果,累加项)=>{ 计算返回值}

        totalprice(){
            return this.list.filter(item => item.goods_state).reduce((total,item)=>{
                //每次循环都把当前的单价乘以当前的数量,并将值累计到total上,循环完后将total值return出去
                    return total += item.goods_price * item.goods_count },0)

        }
    }
//footer.vue
 <!-- 中间的合计 -->
    <div>
      <span>合计:</span>
//.toFixed(2)保存2位小数
      <span class="total-price">¥{{ amount.toFixed(2) }}</span>
    </div>
  </div>


<script>
export default {
  props:{
    //全选的状态
    isfull:{
      type:Boolean,
      default:false
    },
    amount:{
      type: Number,
      default: 0
    }
  }
}
</script>

改进3:将购买数量传给counter组件

App中获取了商品的数据,App中嵌入了每个商品的item项(Goods.vue),Goods中嵌套了Counter.

现在Counter需要购买数量,数值传递顺序:App->Goods->Counter

  • App->Goods:父传子(自定义属性)

  • Goods->Counter:父传子

//App.vue
      <Goods v-for="item in list" :key="item.id"  :id="item.id" :title="item.goods_name"
             :pic="item.goods_img" :price="item.goods_price" :state="item.goods_state"
             @state-change="getNewState" :count="item.goods_count"/>
//Goods.vue
<Counter :num="count"></Counter>

<script>
export default {
  props:{
    //商品的数量
    count:{
      default:1,
      type:Number
    }
  }
</script>
//Counter.vue
<!-- 购买的数量 -->
<span class="number-box">{{num}}</span>

<script>
export default {
  props:{
    //接收到从Goods接收的商品数量的值
    num:{
      type:Number,
      default:1
    }
  }
}
</script>

改进4:实现点击+,-号按钮(Counter),商品数量+1,-1

数据传递:Counter->Goods->App

  • Counter->Goods(子传父:自定义事件)

  • Goods->App(子传父)

传送的数据:

  • 商品Id:加减的是哪个商品的数量//父传子Goods中id传给Counter,告诉counter现在counter中加减的是哪一个 商品

  • 商品的数量(num)//直接Goods->App:使用EventBus方案

//Counter.vue,按钮在counter上
    <!-- 减 1 的按钮 -->
    <button type="button" class="btn btn-light btn-sm" @click="sub">-</button>
    <!-- 购买的数量 -->
    <span class="number-box">{{num}}</span>
    <!-- 加 1 的按钮 -->
    <button type="button" class="btn btn-light btn-sm" @click="add">+</button>

import bus from "../EventBus";
methods:{
    add(){
      //错误写法:this.num += 1,num为props属性,是只读的
      //bus.$emit('share',{id:this.id , value:this.num + 1}) //此处不是修改num的原值,只是传递num+1后的新值
      //发送给App的数据格式为:{ id,value }
      //写法2
      const obj = { id:this.id,value:this.num + 1}
      //通过EventBus将obj对象发送给App组件
      bus.$emit('share',obj)
    },
    sub(){
      if (this.num - 1 === 0) return
      const obj = { id:this.id,value: this.num - 1}
      bus.$emit('share',obj)
    }
  }
//App.vue
 import bus from "./components/EventBus";

      created() {
          //调用请求数据的方法
          this.initCartList()//组件在内存中一被创建好,就调用initCartList()方法,执行方法中的代码开始请求数据
          //只要请求回来的数据,在页面渲染中需要用到,就必须转存到data中

          //val接收传来的数据,格式为:id,value
          bus.$on('share',val =>{
              this.list.some(item => {
                  if (item.id === val.id){
                      item.goods_count = val.value
                      return true
                  }
              })

          })
      }

改进5:结算按钮上有购买商品的总数

App->Footer,父传子:自定义属性

//Footer.vue 结算按钮在Footer中
props:{
    //全选的状态
    isfull:{
      type:Boolean,
      default:false
    },
    amount:{
      type: Number,
      default: 0
    },
    //已勾选的商品的总数量,由App传过来,即父传子
    all:{
      type:Number,
      default:0
    }
  }
//App.vue
<Footer :isfull="fullstate" :amount="totalprice" :all="amt" @full_change="getFullState" ></Footer>

computed:{
        //动态计算出全选的状态是true还是false
        fullstate(){
            //每个商品的状态是否都为true
            return this.list.every( item => item.goods_state === true)
        },
        //已勾选商品的总价格
        //1.filter过滤出选中的商品
        //2.reduce累加器:reduce((累加结果,累加项)=>{ 计算返回值}

        totalprice(){
            return this.list.filter(item => item.goods_state).reduce((total,item)=>{
                //每次循环都把当前的单价乘以当前的数量,并将值累计到total上,循环完后将total值return出去
                    return total += item.goods_price * item.goods_count },0)//total初始值为0
        },
        //已勾选的商品总数量
        amt(){
            return this.list.filter(item => item.goods_state).reduce((t,item)=>{
                return t += item.goods_count
            },0)
        }
    }

改进6:将Counter组件改成插槽

改进前:增减商品数量的数据传递 App->Goods->Counter不方便

//App.vue
    <Goods
      v-for="item in list"
      :key="item.id"
      :id="item.id"
      :title="item.goods_name"
      :pic="item.goods_img"
      :price="item.goods_price"
      :state="item.goods_state"
      :count="item.goods_count"
      @state-change="getNewState"
    ></Goods>

Goods.vue组件中使用Counter.vue组件

//Goods.vue
<template>
  <div class="goods-container">
      <!-- 商品标题 -->
      <h6 class="goods-title">{{ title }}</h6>
      <div class="goods-info-bottom">
        <!-- 商品价格 -->
        <span class="goods-price">¥{{ price }}</span>
        <!-- 商品的数量 -->
        <Counter :num="count" :id="id"></Counter>
      </div>
  </div>
</template>

改进后:

//App.vue
    <Goods
      v-for="item in list"
      :key="item.id"
      :id="item.id"
      :title="item.goods_name"
      :pic="item.goods_img"
      :price="item.goods_price"
      :state="item.goods_state"
      :count="item.goods_count"
      @state-change="getNewState"
    >
      <Counter></Counter>

    </Goods>

//Goods.vue
<template>
  <div class="goods-container">
        <span class="goods-price">¥{{ price }}</span>
        <!-- 商品的数量 -->
        <slot></slot>
      </div>
  </div>
</template>

改进7:用插槽的方式实现从App中传递商品数量到Counter.vue中

//App.vue
    <Goods
      v-for="item in list"
      :key="item.id"
      :id="item.id"
      :title="item.goods_name"
      :pic="item.goods_img"
      :price="item.goods_price"
      :state="item.goods_state"
      :count="item.goods_count"
      @state-change="getNewState"
    >
      <Counter></Counter>
    </Goods>
  • <Goods>标签中进行v-for外循环,<Counter>为内循环,<Counter>内层可以直接访问item项的属性(id,goods_price等)

  • 此时App.vue与Counter.vue传递数据无需传递两次(父传子:App->Goods->Counter)

只需传一次(App->Goods,Counter直接在Goods中接收到数据)

改进后:

//Counter
<span class="number-box">{{ num }}</span>

<script>
export default {
  props: {
    // 接收到的 num 数量值
    num: {
      type: Number,
      default: 1
    }
  }
}
</script>
//App
    <Goods
      v-for="item in list"
      :key="item.id"
      :id="item.id"
      :title="item.goods_name"
      :pic="item.goods_img"
      :price="item.goods_price"
      :state="item.goods_state"
      :count="item.goods_count"
      @state-change="getNewState"
    >
//直接再循环中传递
      <Counter :num="item.goods_count"></Counter>
    </Goods>

改进8:实现点击Counter中的+-按钮改变商品数量

数据传递:Counter->App

改进前:通过eventbus实现兄弟间传数据,需传id,商品数量

//counter
    <!-- 减 1 的按钮 -->
    <button type="button" class="btn btn-light btn-sm" @click="sub">-</button>
    <!-- 购买的数量 -->
    <span class="number-box">{{ num }}</span>
    <!-- 加 1 的按钮 -->
    <button type="button" class="btn btn-light btn-sm" @click="add">+</button>

import bus from "../eventBus";

add() {
      // 要发送给 App 的数据格式为 { id, value }
      // 其中,id 是商品的 id; value 是商品最新的购买数量
      const obj = { id: this.id, value: this.num + 1 }
      // 要做的事情:通过 EventBus 把 obj 对象,发送给 App.vue 组件
      bus.$emit('share', obj)
    },
    sub() {
      if (this.num - 1 === 0) return
      // 要发送给 App 的数据格式为 { id, value }
      // 其中,id 是商品的 id; value 是商品最新的购买数量
      const obj = { id: this.id, value: this.num - 1 }
      // 要做的事情:通过 EventBus 把 obj 对象,发送给 App.vue 组件
      bus.$emit('share', obj)
    }

改进后:Counter在Goods中,子传父(自定义事件)即可,只需传商品数量

//App.vue
<Counter :num="item.goods_count" @num-change="getNewNum(item,$event)"></Counter>
      <!--getNewNum(item):直接传item,而不传item.id:因为这里的item就是改好数据的item,而我们要改的数据就是item就不用传id了
       我们根据id(即索引)找到需要修改的对象(即item项),此处我们要修改的就是item-->
    </Goods>

//@num-change="getNewNum没有定义参数时,默认接收到参数e
    //@num-change="getNewNum(item),默认参数e被item覆盖
    //@num-change="getNewNum(item,$event),传入的第一个参数为item,第二个参数为默认参数e
    //1.@click,2.@input,2.自定义事件接收到的值被覆盖,三种情况考虑$event
    getNewNum(item,val){
      item.goods_count = val
    }
//Counter.vue
  methods: {
    // 点击按钮,数值 +1
    add() {
      this.$emit('num-change',this.num + 1)  //自定义事件
    },
    sub() {
      this.$emit('num-change',this.num - 1)
    }
  }

Logo

前往低代码交流专区

更多推荐