4、Vue基础扩展
4.1 插槽
- 组件的最大特性就是复用性,而用好插槽能大大提高组件的可复用能力
- 在Vue中插槽是很重要的存在,通过插槽,我们可以把父组件中指定的DOM作用到子组件的任意位置,后面我们坐项目用到的组件库比如element-ui,vant-ui都频繁用到的插槽,Vue的插槽主要有匿名插槽,具名插槽,作用域插槽三种,下面我们分别来认识一下他们。
4.1.1 匿名插槽★★★★
故名思义就是没有名字的插槽,只需要在子组件中使用<slot></slot>
引入即可
我们来看一下案例哈,如何引入插槽的:
<div id="app"><!-- 这里的所有组件标签中嵌套的内容会替换掉slot 如果不传值 则使用 slot 中的默认值 --><alert-box>有bug发生</alert-box><alert-box>有一个警告</alert-box><alert-box></alert-box>
</div>
<script type="text/javascript">/*组件插槽:父组件向子组件传递内容*/Vue.component('alert-box', {template: `<div><strong>ERROR:</strong># 当组件渲染的时候,这个 <slot> 元素将会被替换为“组件标签中嵌套的内容”。# 插槽内可以包含任何模板代码,包括 HTML<slot>默认内容</slot></div>`});var vm = new Vue({el: '#app',data: {}});
</script>
4.1.2 具名插槽★★★★
- 具有名字的插槽就是具名插槽
- 使用
<slot>
中的 “name” 属性绑定元素,name就是插槽的名字
<div id="app"><base-layout><!-- 2、 通过slot属性来指定, 这个slot的值必须和下面slot组件得name值对应上如果没有匹配到 则放到匿名的插槽中 --><p slot='header'>标题信息</p><p>主要内容1</p><p>主要内容2</p><p slot='footer'>底部信息信息</p></base-layout><base-layout><!-- 注意点: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></base-layout>
</div>
<script type="text/javascript">/*具名插槽*/Vue.component('base-layout', {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>
4.1.3 作用域插槽★★★★
- 可以让父组件对子组件的内容进行加工处理
- 既可以复用子组件的slot,又可以使slot内容不一致
- 父组件中使用
slot-scope
绑定一个属性 - 子组件中给
<slot>
标签绑定一个自定义属性,可以传递子组件的内容 - 父组件通过
slot-scope
接受子组件传递过来的值即可
- 父组件中使用
<div id="app"><!-- 1、当我们希望li 的样式由外部使用组件的地方定义,因为可能有多种地方要使用 该组件,但样式希望不一样 这个时候我们需要使用作用域插槽 --><fruit-list :list='list'><!-- 2、 父组件中使用了<template>元素,而且包含scope="slotProps",slotProps在这里只是临时变量 ---><template slot-scope='slotProps'><strong v-if='slotProps.info.id==3' class="current">{{slotProps.info.name}}</strong><span v-else>{{slotProps.info.name}}</span></template></fruit-list>
</div>
<script type="text/javascript">/*作用域插槽*/Vue.component('fruit-list', {props: ['list'],template: `<div><li :key='item.id' v-for='item in list'>//3、 在子组件模板中,<slot>元素上有一个类似props传递数据给组 件的写法 msg="xxx",//插槽可以提供一个默认内容,如果如果父组件没有为这个插槽提供了内 容,会显示默认的内容。如果父组件为这个插槽提供了内容,则默认的内容会被替换掉<slot :info='item'>{{item.name}}</slot></li></div>`});var vm = new Vue({el: '#app',data: {list: [{id: 1,name: 'apple'}, {id: 2,name: 'orange'}, {id: 3,name: 'banana'}]}});
</script>
4.2 案例讲解-购物车
前面我们学了组件,我们也知道Vue的核心是组件系统和数据驱动,下面我们来做一个购物车的案例,巩固深化一下我们所学的理论知识
4.2.1 组件化布局组件拆分★★★★
- 把静态页面转换为组件化模式
- 把组件渲染到页面上显示效果如下所示
代码效果如下:
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@3.3.7/dist/css/bootstrap.min.css">
<style>#app {width: 600px;margin-top: 30px;}.header {width: 100%;height: 40px;line-height: 40px;text-align: center;background-color: aquamarine;}.footer {width: 100%;height: 50px;line-height: 50px;background-color: azure;text-align: right;}.footer>span {padding: 0px 15px;font-weight: bold;font-size: 15px;color: brown;}
</style>
上面写了点简单的样式,效果如上图所示
<div id="app" class="container"><my-cart></my-cart>
</div>
<script>//1、 把静态页面转换成组件化模式//1.1 标题组件 var CartTitle = {template: `<div class="header">我的商品</div>`}//1.2 商品列表组件var CartList = {template: `<table class="table table-bordered"><tr><td><img src="media/day06-a.jpg" width="50" /></td><td><button>-</button><input type="text" /><button>+</button></td><td><button class="btn btn-sm btn-danger">删除</button></td></tr><tr><td><img src="media/day06-b.jpg" width="50" /></td><td><button>-</button><input type="text" /><button>+</button></td><td><button class="btn btn-sm btn-danger">删除</button></td></tr><tr><td><img src="media/day06-c.jpg" width="50" /></td><td><button>-</button><input type="text" /><button>+</button></td><td><button class="btn btn-sm btn-danger">删除</button></td></tr></table>`}//结算组件var CartTotal = {template: `<div class="footer"><span>总价: 100</span> <button class="btn btn-danger">结算</button></div>`}//定义一个全局组件 my-cartVue.component("my-cart", {// 引入子组件template: `<div><cart-title></cart-title><cart-list></cart-list><cart-total></cart-total></div>`,//注册子路由组件components: {'cart-title': CartTitle,'cart-list': CartList,'cart-total': CartTotal}});new Vue({el: "#app",data: {},methods: {}})
</script>
4.2.2完成标题和结算组件功能 组件传值★★★★
- 标题组件事件动态渲染
- 从父组件把标题数据传递过来 即 父向子组件传值
- 把传递过来的数据渲染到页面上
- 结算功能组件
- 从父组件把商品列表list 数据传递过来 即 父向子组件传值
- 把传递过来的数据计算最终价格渲染到页面上
<div id="app" class="container"><my-cart></my-cart>
</div>
<script>//1、 把静态页面转换成组件化模式//1.1 标题组件 var CartTitle = {props: ['title'],template: `<div class="header">{{title}}</div>`}//结算组件,子组件接受值即可var CartTotal = {props: ['goods_list'],template: `<div class="footer"><span>总价: {{total}}</span> <button class="btn btn-danger">结算</button></div>`,computed: {total:function(){let amounts = 0;this.goods_list.forEach(item=>{amounts +=item.price*item.num;})return amounts.toFixed(2);}},}//定义一个全局组件 my-cartVue.component("my-cart", {data: function () {return {title: "我的商品",goods_list: [{id: 1,name: 'TCL彩电',price: 1000,num: 1,img: 'media/day06-a.jpg'}, {id: 2,name: '机顶盒',price: 1000,num: 1,img: 'media/day06-b.jpg'}, {id: 3,name: '海尔冰箱',price: 1000,num: 1,img: 'media/day06-c.jpg'}, {id: 4,name: '小米手机',price: 1000,num: 1,img: 'media/day06-d.jpg'}, {id: 5,name: 'PPTV电视',price: 1000,num: 2,img: 'media/day06-e.jpg'}]}},// 引入子组件,父组件向子组件进行传值template: `<div><cart-title :title="title"></cart-title><cart-list ></cart-list><cart-total :goods_list="goods_list"></cart-total></div>`,//注册子路由组件components: {'cart-title': CartTitle,'cart-list': CartList,'cart-total': CartTotal}});</script>
4.2.3 完成列表组件删除商品功能splice★★★★
- 从父组件把商品列表list 数据传递过来 即 父向子组件传值
- 把传递过来的数据渲染到页面上
- 点击删除按钮的时候删除对应的数据
- 给按钮添加点击事件把需要删除的id传递过来
- 子组件中不推荐操作父组件的数据,有可能多个子组件使用父组件的数据,我们需要把数据 传递给父组件让父组件操作数据
- 父组件删除对应的数据
- 给按钮添加点击事件把需要删除的id传递过来
<div id="app" class="container"><my-cart></my-cart>
</div>
<script>//1、 把静态页面转换成组件化模式//1.1 标题组件 var CartTitle = {props: ['title'],template: `<div class="header">{{title}}</div>`}//1.2 商品列表组件var CartList = {props: ['goods_list'],template: `<table class="table table-bordered"><tr v-for="(item,index) in goods_list" :key="item.id"><td><img :src="item.img" width="50" />{{item.name}}</td><td><button>-</button><input type="text" v-model="item.num"/><button>+</button></td><td><button class="btn btn-sm btn-danger" @click='del(item.id)'>删除</button></td></tr></table>`,methods: {del(id) {console.log(id);this.$emit("cart-del", id);}}}//结算组件,子组件接受值即可var CartTotal = {props: ['goods_list'],template: `<div class="footer"><span>总价: {{total}}</span> <button class="btn btn-danger">结算</button></div>`,computed: {total: function () {let amounts = 0;this.goods_list.forEach(item => {amounts += item.price * item.num;})return amounts.toFixed(2);}},}//定义一个全局组件 my-cartVue.component("my-cart", {data: function () {return {title: "我的商品",goods_list: [{id: 1,name: 'TCL彩电',price: 1000,num: 1,img: 'media/day06-a.jpg'}, {id: 2,name: '机顶盒',price: 1000,num: 1,img: 'media/day06-b.jpg'}, {id: 3,name: '海尔冰箱',price: 1000,num: 1,img: 'media/day06-c.jpg'}, {id: 4,name: '小米手机',price: 1000,num: 1,img: 'media/day06-d.jpg'}, {id: 5,name: 'PPTV电视',price: 1000,num: 2,img: 'media/day06-e.jpg'}]}},// 引入子组件,父组件向子组件进行传值template: `<div><cart-title :title="title"></cart-title><cart-list :goods_list="goods_list" @cart-del="removeGoods"></cart-list><cart-total :goods_list="goods_list"></cart-total></div>`,//注册子路由组件components: {'cart-title': CartTitle,'cart-list': CartList,'cart-total': CartTotal},methods: {removeGoods(id) {console.log(id)let index = this.goods_list.findIndex(item => {return item.id == id;});this.goods_list.splice(index, 1);}}});new Vue({el: "#app",data: {},methods: {}})
</script>
4.2.4 完成列表组件更新商品数量computed★★★★
- 将输入框中的默认数据动态渲染出来
- 输入框失去焦点的时候 更改商品的数量
- 点击按钮+和按钮-更新商品的数量,同步更新总价格
- 子组件中不推荐操作数据 把这些数据传递给父组件 让父组件处理这些数据
- 父组件中接收子组件传递过来的数据并处理
<div id="app" class="container"><my-cart></my-cart>
</div>
<script>//1、 把静态页面转换成组件化模式//1.1 标题组件 var CartTitle = {props: ['title'],template: `<div class="header">{{title}}</div>`}//1.2 商品列表组件var CartList = {props: ['goods_list'],template: `<table class="table table-bordered"><tr v-for="(item,index) in goods_list" :key="item.id"><td><img :src="item.img" width="50" />{{item.name}}</td><td><button @click="decr(item.id)">-</button><input type="text" :value="item.num" @blur="changeNums(item.id,$event)"/><button @click="incr(item.id)">+</button></td><td><button class="btn btn-sm btn-danger" @click='del(item.id)'>删除</button></td></tr></table>`,methods: {del(id) {console.log(id);this.$emit("cart-del", id);},changeNums(id, event) {this.$emit("change-num",{id: id,type: 'change',num: event.target.value})},incr(id) {this.$emit('change-num',{id: id,type: 'incr',});},decr(id) {this.$emit('change-num',{id: id,type: 'decr',});}}}//结算组件,子组件接受值即可var CartTotal = {props: ['goods_list'],template: `<div class="footer"><span>总价: {{total}}</span> <button class="btn btn-danger">结算</button></div>`,computed: {total: function () {let amounts = 0;this.goods_list.forEach(item => {amounts += item.price * item.num;})return amounts.toFixed(2);}},}//定义一个全局组件 my-cartVue.component("my-cart", {data: function () {return {title: "我的商品",goods_list: [{id: 1,name: 'TCL彩电',price: 1000,num: 1,img: 'media/day06-a.jpg'}, {id: 2,name: '机顶盒',price: 1000,num: 1,img: 'media/day06-b.jpg'}, {id: 3,name: '海尔冰箱',price: 1000,num: 1,img: 'media/day06-c.jpg'}, {id: 4,name: '小米手机',price: 1000,num: 1,img: 'media/day06-d.jpg'}, {id: 5,name: 'PPTV电视',price: 1000,num: 2,img: 'media/day06-e.jpg'}]}},// 引入子组件,父组件向子组件进行传值template: `<div><cart-title :title="title"></cart-title><cart-list :goods_list="goods_list" @cart-del="removeGoods"@change-num="changeNums"></cart-list><cart-total :goods_list="goods_list"></cart-total></div>`,//注册子路由组件components: {'cart-title': CartTitle,'cart-list': CartList,'cart-total': CartTotal},methods: {removeGoods(id) {console.log(id)let index = this.goods_list.findIndex(item => {return item.id == id;});this.goods_list.splice(index, 1);},changeNums(data) {let id = data.id;let index = this.goods_list.findIndex(item => {return item.id == id;});if (data.type == "change") {this.goods_list[index].num = data.num;}if (data.type == "incr") {this.goods_list[index].num++;}if (data.type == "decr") {this.goods_list[index].num--;}}}});
</script>
4.3 接口调用方式
前端要做动态数据渲染,只能通过调用接口的方式来获取服务端的数据,目前常见的方式有ajax,fetch,axios等常见的方式可以获取接口的数据,我们来分别认识一下这些操作方式…
4.3.1 原生ajax★★★★
ajax主要是负责是客户端和服务端异步数据通信的工具,原生的ajax对浏览器支持比较好,所以群众基础还是比较广的,我们先来看一下原生的ajax如何实现对接口数据的请求:
原生的ajax请求数据大致分为如下几个步骤:
<script>let xhr = null;//实例化xmlhttprequest对象xhr = new XMLHttpRequest();//配置要请求的接口地址xhr.open("post","url地址",true);//设置请求的头部信息,如果是post请求的话xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded")//设置请求后状态发生变化的回调函数xhr.onreadystatechange = ()=>{};// 发送请求xhr.send();
</script>
4.3.2 jQuery的ajax★★★★
但是我们大部分时候可能用上面原生的ajax的时候是很少的,我们一般都是用jQuery封装好的Api,这个前提是我们需要先引入jQuery文件,否则提示报错,jQuery的ajax使用步骤大致如下所示
<script>$.ajax({url:"",data:{},type: "post",dataType: "json",async: true,success(res){},error(){}})
</script>
4.3.3 fetch★★★★
- Fetch API是新的ajax解决方案 Fetch会返回Promise
- fetch不是ajax的进一步封装,而是原生js,没有使用XMLHttpRequest对象
- 基本结构大致如下 fetch(url, options).then()
<script type="text/javascript">/*Fetch API 基本用法fetch(url).then()第一个参数请求的路径 Fetch会返回Promise 所以我们可以使用then 拿到请求成功的结果 */fetch('http://localhost:3000/fdata').then(function (data) {// text()方法属于fetchAPI的一部分,//它返回一个Promise实例对象,用于获取后台返回的数据return data.text();}).then(function (data) {// 在这个then里面我们能拿到最终的数据 console.log(data);})
</script>
- fetch支持很多请求的方式如POST,GET,DELETE,UPDATE,PATCH和PUT
- 默认的是 GET 请求
- 需要在 options 对象中 指定对应的 method method:请求使用的方法
- post 和 普通 请求的时候 需要在options 中 设置 请求头 headers 和 body
<script type="text/javascript">/*Fetch API 调用接口传递参数*///1.1 GET参数传递 - 传统URL 通过url ? 的形式传参fetch('http://localhost:3000/books?id=123', {//get 请求可以省略不写 默认的是GET method: 'get'}).then(function (data) {//它返回一个Promise实例对象,用于获取后台返回的数据return data.text();}).then(function (data) {//在这个then里面我们能拿到最终的数据console.log(data)});//1.2 GET参数传递 restful形式的URL 通过 / 的形式传递参数 即 id = 456 和id后台的配置有关fetch('http://localhost:3000/books/456', {//get 请求可以省略不写 默认的是GET method: