Vue2入门学习
Vue是什么
Vue是一个用于构建用户界面的渐进式框架
构建用户界面:基于数据去渲染用户所能看到的页面
渐进式:可以循序渐进的学习,使用
创建一个Vue实例
- 准备一个Vue管理的容器
- 引入vue.js核心包
- 创建实例
- 添加配置项
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>创建一个Vue实例</title> <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script> </head> <body>
<div id="app"> {{msg}} </div>
<script>
const app = new Vue({ // 添加配置项 el:'#app', data:{ msg:'我爱学习' } }) </script> </body> </html>
|
插值表达式
差值表达式是vue的一种模板语法
作用
利用表达式进行插值,渲染到页面中
表达式可以是一个被求值的代码,JS引擎会将其计算出一个结果。
语法
1 2 3
| {{ 表达式 }} {{ arr[0] }} ....
|
注意点
- 使用的数据必须是在data中定义的
- 不能编写复杂语句,比如if for
- 不能再标签的属性中使用插值表达式语法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>插值表达式</title> <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script> </head> <body> <div id="app"> <p>{{ msg }}</p> <p>{{ msg.toUpperCase() }}</p> <p>{{ msg + "早上坏" }}</p> <p>{{ msg + "早上坏" }}</p> <p>{{ friend.name}}</p> </div> <script> const app = new Vue({ el:'#app', data:{ msg:'trygo', friend:{ name:'tom' } } }) </script> </body> </html>
|
Vue响应式特性
什么是响应式?
数据一旦发生变化,页面会自动刷新成新的数据
如何修改data的数据?
data中的数据,最终会被添加到app实例上。
- 访问数据:实例名.属性名
- 修改数据:实例名.属性名 = 新的属性值
Vue的指令
Vue会根据不同的执行,针对标签实现不同的功能。
指令:带有v-前缀的特殊标签属性
1 2 3 4 5
|
<div v-html="msg"></div>
<div class="div"></div>
|
v-html
功能:设置元素的innerHTML
语法:
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Vue指令</title> <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script> </head> <body> <div id="app"> <div v-html="link"></div> <div class="div">{{link2}}</div> </div> <script> const app = new Vue({ el: '#app', data: { link: `<a href="https://www.baidu.com">点击跳转百度</a>`, link2: `<a href="https://www.baidu.com">点击跳转百度</a>` } }) </script> </body> </html>
|
v-show
功能:用来控制元素的显示隐藏
语法:
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>v-show</title> <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script> </head> <body> <div id="app"> <div v-show="false">这里是不显示div</div> <div v-show="true">这里是显示div</div> </div> <script> const app = new Vue({ el:'#app', data:{
} }) </script> </body> </html>
|
v-if
功能:控制元素的显隐(条件渲染)
语法:
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>v-if</title> <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script> </head> <body> <div id="app"> <div v-if="false">这里是不显示div</div> <div v-if="true">这里是显示div</div> </div> <script> const app = new Vue({ el:'#app', data:{
} }) </script> </body> </html>
|
v-show和v-if的区别
v-show底层:隐藏是通过给元素添加display:none属性来控制的。
v-if底层:通过判断条件,来控制元素的创建和移除
v-show使用场景:频繁控制显示隐藏的场景
v-if使用场景:要么显示,要么隐藏,不频繁切换的场景
v-else / v-else-if
功能:辅助v-if进行条件渲染
语法:
注意点:需要与v-if挨着使用
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>v-else/ v-else-if</title> <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script> </head> <body> <div id="app"> <p v-if="sex === 1">性别:男</p> <p v-else>性别:女</p> <hr> 您的成绩是 <p v-if="score >= 90">A</p> <p v-else-if="score >= 80 && score < 90">B</p> <p v-else-if="score >= 60 && score < 80">C</p> <p v-else>D</p> </div> <script> const app = new Vue({ el: "#app", data: { score: 90, sex: 1 } }) </script> </body> </html>
|
v-on
作用:用于注册元素的事件
语法:
- v-on:事件名=”内联语句”
- v-on:事件名=”函数名”
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>v-on</title> <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script> </head> <body> <div id="app"> <button v-on:click="count--">-</button> {{ count }} <button v-on:click="count++">+</button> <hr> <p>可以换成下面的简写</p> <button @click="count--">-</button> {{ count }} <button @click="count++">+</button> </div> <script> const app = new Vue({ el: "#app", data: { count: 100 }, methods: {} }) </script> </body> </html>
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>v-on</title> <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script> </head> <body> <div id="app"> <h1 v-show="show">任我行</h1> <button @click="changeShow">切换显示隐藏</button> </div> <script> const app = new Vue({ el: "#app", data: { show:true }, methods: { changeShow:function () { this.show = !this.show } } }) </script> </body> </html>
|
注意:所有在methods中的函数,他的this都指向vue实例
v-on调用传参
语法:
- 调用函数的地方,在函数名后面添加小括号**()**,和要传递的参数即可
- 函数接收参数使用形参接收
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>v-on传参</title> <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script> </head> <body> <div id="app"> <span>当前余额:{{ balance }}</span> <hr> <button @click="subBalance(5)">可乐</button> <button @click="subBalance(10)">咖啡</button> </div> <script> const app = new Vue({ el: "#app", data: { balance: 100 }, methods: { subBalance: function (price) { this.balance = this.balance - price } } }) </script> </body> </html>
|
v-bind
作用:用来动态的设置HTML标签的属性,比如src,url,href等等…
语法:v-bind:属性名=”表达式”
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>v-bind</title> <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script> </head> <body> <div id="app"> <img v-bind:src="imgUrl" v-bind:title="msg" width="300px"> <hr> <p>可以使用简写,去掉v-bind</p> <img :src="imgUrl" :title="msg" width="300px">
</div> <script> const app = new Vue({ el: "#app", data: { imgUrl:"http://up.deskcity.org/pic_source/3c/10/47/3c1047bf1dd17e3cb6aeac429e06cea3.jpg", msg:"这里是提示信息" }, methods: {
} }) </script> </body> </html>
|
v-bind对样式控制的增强-操作class
语法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67
| <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>案例</title> <style> * { padding: 0; margin: 0; text-decoration:none; color:#333; }
.box { width: 500px; height: 50px; box-sizing: border-box; border-bottom: solid 1px red; }
li,a { list-style: none; display: inline-block; line-height: 50px; padding: 0 20px; }
.active { color: white; background-color: red; }
</style> <script src="vue.js"></script> </head> <body> <div id="app"> <div class="box"> <ul> <li v-for="(item,index) in list" v-bind:key="item.id"> <a href="#" :class="{active : index === activeIndex}" @click="change(index)"> {{ item.title }} </a> </li> </ul> </div> </div> <script> const app = new Vue({ el: "#app", data: { activeIndex:0, list: [ {id: 1, title: '京东秒杀'}, {id: 2, title: '每日特惠'}, {id: 3, title: '品类秒杀'} ] }, methods: { change:function (index) { this.activeIndex = index } } }) </script> </body> </html>
|
v-bind对样式控制的增强-操作style
语法
1
| :style="{ css属性名:css属性值 }"
|
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>v-bind控制style</title> <script src="vue.js"></script> </head> <body> <div id="app"> <div :style="{ width: '200px',height:'200px','background-color':'red' }"> </div> </div> <script> const app = new Vue({ el:'#app', data:{
} }) </script> </body> </html>
|
v-for
作用:基于数据循环,用户渲染多个元素
语法:
- 遍历数组语法
- v-for=”(item,index) in list”
- 参数item 数组中的每一项
- 参数index 数组元素下标
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>v-for</title> <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script> </head> <body> <div id="app"> <p v-for="(item,index) in list">{{item}}</p> </div> <script> const app = new Vue({ el:'#app', data:{ list:[ '1','2','3','4','5' ] } }) </script> </body> </html>
|
案例:删除列表元素
实现效果:使用v-for渲染list中的数据。点击删除按钮可以删除对应元素
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>delItem</title> <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script> </head> <body> <div id="app"> <ul> <li v-for="(item) in list" :key="item.id"> <span>姓名:{{item.name}}</span> <span>年龄:{{item.age}}</span> <button @click="del(item.id)">删除</button> </li> </ul> </div> <script> const app = new Vue({ el: "#app", data: { list: [ {id: 1, name: '林黛玉', age: 18}, {id: 2, name: '武松', age: 11}, {id: 3, name: '李逵', age: 28}, {id: 4, name: '孙悟空', age: 180}, {id: 5, name: '曹操', age: 48} ] }, methods: { del:function (id) { this.list = this.list.filter(item => item.id !== id) } } }) </script> </body> </html>
|
v-for的key
语法:key属性=”唯一标识”
作用:给列表项添加的唯一标识,便于Vue进行正确排序复用
v-for的默认行为会原地修改元素(就地复用)。给元素添加上key时,让元素跟数据能够进行对应,让元素正确的复用。
如果不绑定key,当数据发生变化的时候,从前往后找对应的元素,修改元素内的值,而没有对元素本身进行操作,这显然是不合理的。
绑定key之后,元素能和数据对应。当数据发生变化时,能够根据对应关系找到对应的元素进行操作。
注意点:
- key的值只能是数字或字符串
- key的值需要唯一
- 推介使用id为key,不推介使用index(会变化,不对应)
v-model
作用:用于给表单数据使用,让表单元素和Vue实例的数据进行双向绑定
语法:v-model=”变量”
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>v-model</title> <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script> </head> <body> <div id="app"> <form action="#"> 账号:<input type="text" name="username" v-model="username"><br> 密码:<input type="password" name="password" v-model="password"><br> <button @click="login" type="button">提交</button> <button @click="reset" type="button">重置</button> </form> </div> <script> const app = new Vue({ el:"#app", data:{ username:'', password:'' }, methods:{ login:function () { console.log(this.username,this.password) }, reset:function () { this.username = '' this.password = '' } } }) </script> </body> </html>
|
什么是双向绑定?
数据变化改变视图
视图变化改变数据
指令修饰符
通过**”.”**链接一些特殊的指令后缀,不同的后缀封装了不通的操作处理
- 按键修饰符
- v-model修饰符
- v-model.trim:去空格
- v-model.number:转数字
- 事件修饰符
- @事件名.stop:阻止传播行为
- @事件名.prevent:阻止默认行为
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64
| <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>指令修饰符</title> <style> .out { width: 300px; height: 300px; background-color: hotpink; }
.in { width: 100px; height: 100px; background-color: blueviolet; } </style> <script src="./vue.js"></script> </head> <body> <div id="app"> <form action="#"> <input type="text" v-model="username"> <input type="text" v-model="phone"> <br> <button @click.enter="login">按下回车</button> </form> <br> <hr> <div class="out" @click="outClick"> <div class="in" @click.stop="inClick">
</div> </div> <hr> <a href="https://www.baidu.com" @click.prevent="clickAlink">跳转百度,阻止行为</a>
</div> <script> const app = new Vue({ el:'#app', data:{ username:'', phone:'' }, methods:{ login:function(){ console.log(this.username,this.phone) }, inClick:function () { console.log("in") }, outClick:function () { console.log("out") }, clickAlink:function () { console.log("a link") } } }) </script> </body> </html>
|
v-model应用于其他表单元素
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49
| <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>v-model</title> <script src="vue.js"></script> </head> <body> <div id="app"> <form action="#"> 姓名:<input type="text" v-model="username"><br><br> 是否单身:<input type="checkbox" v-model="isSingle"><br><br> 性别:<input type="radio" name="sex" value="1" v-model="sex">男<input type="radio" name="sex" value="0" v-model="sex">女<br><br> 所在城市: <select v-model="city"> <option value="1">北京</option> <option value="2">上海</option> <option value="3">深圳</option> <option value="4">广州</option> </select> <br><br> 自我描述: <textarea v-model="desc"></textarea><br><br> <button type="button" @click="showData">立即注册</button> </form> </div> <script> const app = new Vue({ el: "#app", data: { username: '', isSingle: false, sex: 1, city: 1, desc: '' }, methods:{ showData:function () { console.log(this.username) console.log(this.isSingle) console.log(this.sex) console.log(this.city) console.log(this.desc) } } }) </script> </body> </html>
|
计算属性
计算属性的基本使用
概念:基于现有的属性,计算出来的新属性。依赖的数据发生变化,计算属性的值也发生变化。
语法
示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
| <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>计算属性</title> <script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script> </head> <body> <div id="app"> <table> <tr> <th>名字</th> <th>数量</th> </tr> <tr v-for="(item,index) in list" :key="item.id"> <td>{{ item.name }}</td> <td>{{ item.num }}</td> </tr> </table> <hr> 总数量:{{ totalNum }} </div> <script> const app = new Vue({ el: '#app', data: { list: [ {id: 1, name: '红楼梦', num: 3}, {id: 2, name: '西游记', num: 1}, {id: 3, name: '三国演义', num: 45}, {id: 4, name: '水浒传', num: 5}, {id: 5, name: '碧血剑', num: 2} ] }, computed: { totalNum() { return this.list.reduce((sum, item) => item.num + sum, 0) } } }) </script> </body> </html>
|
计算属性computed VS 方法methods
计算属性computed
封装了一段对数据的处理,求得一个结果
- 写在computed配置项中
- 可以直接使用:this.计算属性或者****
- 缓存特性:计算结果会对计算出来的计算属性进行缓存,再次使用时直接从缓存获取。当依赖项变化了,会再次计算,并再次缓存。
方法methods
给实例提供了一个方法,调用以处理业务逻辑
- 写在methods配置项中
- 作为一个方法:需要调用,this.方法()或者@click=方法
计算属性的完整写法
计算属性的默认写法只能读取属性的值,并不能修改属性的值
那我们能不能去修改计算属性的值呢?
如果需要修改计算属性的值,需要用到计算属性的完整写法。
代码示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
| <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>计算属性的完整写法</title> <script src="vue.js"></script> </head> <body> <div id="app"> 姓 <input type="text" v-model="lastName"> 名 <input type="text" v-model="firstName"> = {{ username }}
<br> <button type="button" @click="changeName">改名卡</button> </div> <script> const app = new Vue({ el: '#app', data: { lastName: '', firstName: '' }, methods: { changeName() { this.username = "吕小布" } }, computed: { username: { set(name) { this.lastName = name.substring(0, 1) this.firstName = name.substring(1) }, get() { return this.lastName + this.firstName } } } }) </script> </body> </html>
|
成绩案例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183
| <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>成绩案例</title> <style> * { margin: 0; padding: 0; color: #333; box-sizing: border-box; border: 0; outline: none; background-color: transparent; }
table { border: 1px solid rgb(220, 220, 220); border-collapse: collapse; text-align: center; }
td, th { border: 1px solid gray; width: 180px; height: 40px; font-size: 16px; }
th { background-color: rgb(244, 244, 244); }
a { color: blue; }
.red { color: red; }
td span { padding: 0 25px; }
input {
}
button { width: 50px; height: 30px; border-radius: 10%; background-color: rgb(4, 93, 195); color: white; transition: 0.4s; }
button:hover { background-color: rgb(0, 174, 246); transition: 0.4s; }
input { height: 40px; width: 200px; border: rgb(135, 135, 135) solid 1px; padding: 10px; margin: 5px; }
.empty { height: 200px; text-align: center; } </style> <script src="vue.js"></script> </head> <body> <div id="app"> <table> <thead> <tr> <th>编号</th> <th>科目</th> <th>成绩</th> <th>操作</th> </tr> </thead> <tbody v-if="list.length > 0"> <tr v-for="(item,index) in list" v-bind:key="item.id"> <td>{{ index + 1 }}</td> <td>{{ item.course }}</td> <td :class="{ red : item.score < 60}">{{ item.score }}</td> <td> <a href="javascript:(0)" @click="removeScore(item.id)">删除</a> </td> </tr>
</tbody> <tbody v-else> <tr> <td class="empty" colspan="4"> 暂无数据 </td> </tr> </tbody> <tr> <td colspan="4"> <span>总分:{{ totalScore }}</span> <span>平均分:{{ averageScore }}</span> </td> </tr> </table>
<div> <form action="#"> 科目:<input type="text" placeholder="请输入科目" v-model="course"><br> 成绩:<input type="text" v-model.number="score" placeholder="请输入成绩"><br> <button type="button" @keyup.enter="addScore" @click="addScore">添加</button> </form> </div> </div> <script> const app = new Vue({ el: '#app', data: { list: [ {id: 1, course: "语文", score: 54} ], course: "", score: null }, methods: { removeScore(id) { this.list = this.list.filter(item => item.id !== id) }, addScore() { if (typeof this.score !== 'number') { alert("请输入正确的分数") return } if (!this.course) { alert("请输入科目") return } this.list.unshift({ id: +new Date(), score: this.score, course: this.course })
this.score = '' this.course = '' } }, computed: { totalScore: { set() { }, get() { return this.list.reduce((sum, item) => sum + item.score, 0) } }, averageScore: { set() { }, get() { if(this.list.length === 0){ return 0 } return (this.totalScore / this.list.length).toFixed(2) } } } }) </script> </body> </html>
|
watch侦听器
用于监听数据的变化,执行一些业务逻辑或者异步操作
简单写法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
| <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>watch</title> <script src="vue.js" ></script> </head> <body> <div id="app"> <input type="text" v-model="number"> <input type="text" v-model="obj.words"> </div> <script> const app = new Vue({ el:'#app', data:{ number:1, obj:{ words:'' } }, methods:{
}, computed:{
}, watch:{ number(newValue,oldValue){ console.log("number发生变化了",newValue,oldValue) },
'obj.words'(newValue,oldValue){ console.log("obj.words发生变化了",newValue,oldValue) } } }) </script> </body> </html>
|
完整写法
完整写法可以添加配置项
deep:true 对复杂类型进行深度监视
immediate:true 初始化完成立刻执行一次handler函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| const app = new Vue({ el:'#app', data:{ obj:{ title:'', name:'小黑' } }, methods:{
}, computed:{
}, watch:{ obj:{ deep:true, immediate:true, handler(newValue){ console.log(newValue) } } } })
|
Vue生命周期和生命周期的四个阶段
Vue生命周期
一个Vue实例从创建到销毁的过程。
Vue生命周期的四个阶段
- 创建
- 创建响应式数据,最早可以在此阶段的最后发送初始化渲染请求
- 挂载
- 更新
- 销毁
Vue生命周期钩子函数
Vue生命周期的过程中,会自动运行一些函数,被称为生命周期钩子。让开发者可以在特定阶段运行自己的代码
- beforeCreate: 在响应式数据创建之前执行,此时无法访问到响应式的数据
- created:在响应式数据创建之后执行,此时可以访问到响应式的数据
- beforeMount:在渲染模板之前执行
- mounted:在渲染模板之后执行
- beforeUpdate:响应式数据更新,更新视图。在响应式数据更新之后,视图更新之前执行
- updated:在响应式数据更新之后,视图更新之后执行
- beforeDestroy:在Vue实例销毁之前执行,一般用来卸载Vue实例以外的资源,比如定时器,延时器
- destroyed:在Vue实例销毁之后执行
工程化开发和脚手架
开发Vue的两种方式
- 核心包传统开发模式:基于html / css / js 文件,直接引入核心包,开发 Vue。
- 工程化开发模式:基于构建工具(例如:webpack)的环境中开发Vue。
工程化开发模式优点:
提高编码效率,比如使用JS新语法、Less/Sass、Typescript等通过webpack都可以编译成浏览器识别的ES3/ES5/CSS等
工程化开发模式问题:
- webpack配置不简单
- 雷同的基础配置
- 缺乏统一的标准
为了解决以上问题,所以我们需要一个工具,生成标准化的配置
脚手架Vue Cli
Vue CLI 是Vue官方提供的一个全局命令工具
可以帮助我们快速创建一个开发Vue项目的标准化基础架子。【集成了webpack配置】
好处:
- 开箱即用,零配置
- 内置babel等工具
- 标准化的webpack配置
使用步骤:
- 全局安装(只需安装一次即可) yarn global add @vue/cli 或者 npm i @vue/cli -g
- 查看vue/cli版本: vue –version
- 创建项目架子:vue create project-name(项目名不能使用中文)
- 启动项目:yarn serve 或者 npm run serve(命令不固定,找package.json)
项目目录介绍和运行流程
目录介绍
虽然脚手架中的文件有很多,目前咱们只需认识三个文件即可
- main.js 入口文件
- App.vue App根组件
- index.html 模板文件
运行流程
组件化开发
组件化:一个页面可以拆分成一个个组件,每个组件有着自己独立的结构、样式、行为。
好处:便于维护,利于复用 → 提升开发效率。
组件分类:普通组件、根组件。
比如:下面这个页面,可以把所有的代码都写在一个页面中,但是这样显得代码比较混乱,难易维护。咱们可以按模块进行组件划分
根组件
根组件介绍
整个应用最上层的组件,包裹所有普通小组件
组件是由三部分构成
总结
App组件包含哪三部分?
普通组件-局部注册
特点
只能在注册的组件内使用
步骤
- 创建.vue文件(三个组成部分)
- 在使用的组件内先导入再注册,最后使用
使用方式
当成html标签使用即可 <组件名></组件名>
注意
组件名规范 —> 大驼峰命名法, 如 HmHeader
语法
1 2 3 4 5 6 7 8 9 10 11
| import 组件对象 from '.vue文件路径' import HmHeader from './components/HmHeader'
export default { components: { '组件名': 组件对象, HmHeader:HmHeaer, HmHeader } }
|
练习
在App组件中,完成以下练习。在App.vue中使用组件的方式完成下面布局
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| <template> <div class="hm-header"> 我是hm-header </div> </template>
<script> export default {
} </script>
<style> .hm-header { height: 100px; line-height: 100px; text-align: center; font-size: 30px; background-color: #8064a2; color: white; } </style>
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| <template> <div class="hm-main"> 我是hm-main </div> </template>
<script> export default {
} </script>
<style> .hm-main { height: 400px; line-height: 400px; text-align: center; font-size: 30px; background-color: #f79646; color: white; margin: 20px 0; } </style>
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| <template> <div class="hm-footer"> 我是hm-footer </div> </template>
<script> export default {
} </script>
<style> .hm-footer { height: 100px; line-height: 100px; text-align: center; font-size: 30px; background-color: #4f81bd; color: white; } </style>
|
总结
- A组件内部注册的局部组件能在B组件使用吗
- 局部注册组件的步骤是什么
- 使用组件时 应该按照什么命名法
普通组件-全局注册
特点
全局注册的组件,在项目的任何组件中都能使用
步骤
- 创建.vue组件(三个组成部分)
- main.js中进行全局注册
使用方式
当成HTML标签直接使用
<组件名></组件名>
注意
组件名规范 —> 大驼峰命名法, 如 HmHeader
语法
Vue.component(‘组件名’, 组件对象)
例:
1 2 3
| import HmButton from './components/HmButton' Vue.component('HmButton', HmButton)
|
练习
在以下3个局部组件中是展示一个通用按钮
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| <template> <button class="hm-button">通用按钮</button> </template>
<script> export default {
} </script>
<style> .hm-button { height: 50px; line-height: 50px; padding: 0 20px; background-color: #3bae56; border-radius: 5px; color: white; border: none; vertical-align: middle; cursor: pointer; } </style>
|
7.总结
1.全局注册组件应该在哪个文件中注册以及语法是什么?
2.全局组件在项目中的任何一个组件中可不可以使用?
scoped解决样式冲突
默认情况:
写在组件中的样式会 全局生效 → 因此很容易造成多个组件之间的样式冲突问题。
全局样式: 默认组件中的样式会作用到全局,任何一个组件中都会受到此样式的影响
局部样式: 可以给组件加上scoped 属性,可以让样式只作用于当前组件
代码演示
BaseOne.vue
1 2 3 4 5 6 7 8 9 10 11 12 13
| <template> <div class="base-one"> BaseOne </div> </template>
<script> export default {
} </script> <style scoped> </style>
|
BaseTwo.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| <template> <div class="base-one"> BaseTwo </div> </template>
<script> export default {
} </script>
<style scoped> </style>
|
App.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| <template> <div id="app"> <BaseOne></BaseOne> <BaseTwo></BaseTwo> </div> </template>
<script> import BaseOne from './components/BaseOne' import BaseTwo from './components/BaseTwo' export default { name: 'App', components: { BaseOne, BaseTwo } } </script>
|
scoped原理
- 当前组件内标签都被添加data-v-hash值 的属性
- css选择器都被添加 [data-v-hash值] 的属性选择器
最终效果: 必须是当前组件的元素, 才会有这个自定义属性, 才会被这个样式作用到
总结
style的默认样式是作用到哪里的?
scoped的作用是什么?
style中推不推荐加scoped?
组件应该有自己独立的样式,应该加上
data必须是一个函数
data为什么要写成函数
一个组件的 data 选项必须是一个函数。目的是为了:保证每个组件实例,维护独立的一份数据对象。
每次创建新的组件实例,都会新执行一次data 函数,得到一个新对象。
代码演示
BaseCount.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| <template> <div class="base-count"> <button @click="count--">-</button> <span>{{ count }}</span> <button @click="count++">+</button> </div> </template>
<script> export default { data: function () { return { count: 100, } }, } </script>
<style> .base-count { margin: 20px; } </style>
|
App.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| <template> <div class="app"> <BaseCount></BaseCount> </div> </template>
<script> import BaseCount from './components/BaseCount' export default { components: { BaseCount, }, } </script>
<style> </style>
|
3.总结
data写成函数的目的是什么?
组件通信
1.什么是组件通信?
组件通信,就是指组件与组件之间的数据传递
- 组件的数据是独立的,无法直接访问其他组件的数据。
- 想使用其他组件的数据,就需要组件通信
2.组件之间如何通信
思考:
- 组件之间有哪些关系?
- 对应的组件通信方案有哪几类?
3.组件关系分类
- 父子关系
- 非父子关系
4.通信解决方案
5.父子通信流程
- 父组件通过 props 将数据传递给子组件
- 子组件利用 $emit 通知父组件修改更新
6.父向子通信代码示例
父组件通过props将数据传递给子组件
父组件App.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| <template> <div class="app" style="border: 3px solid #000; margin: 10px"> 我是APP组件 <Son></Son> </div> </template>
<script> import Son from './components/Son.vue' export default { name: 'App', data() { return { myTitle: '学前端,就来黑马程序员', } }, components: { Son, }, } </script>
<style> </style>
|
子组件Son.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| <template> <div class="son" style="border:3px solid #000;margin:10px"> 我是Son组件 </div> </template>
<script> export default { name: 'Son-Child', } </script>
<style>
</style>
|
父向子传值步骤
- 给子组件以添加属性的方式传值
- 子组件内部通过props接收
- 模板中直接使用 props接收的值
7.子向父通信代码示例
子组件利用 $emit 通知父组件,进行修改更新
子向父传值步骤
- $emit触发事件,给父组件发送消息通知
- 父组件监听$emit触发的事件
- 提供处理函数,在函数的性参中获取传过来的参数
8.总结
- 组件关系分类有哪两种
- 父子组件通信的流程是什么?
- 父向子
- 子向父
什么是props
Props 定义
组件上 注册的一些 自定义属性
Props 作用
向子组件传递数据
特点
- 可以 传递 任意数量 的prop
- 可以 传递 任意类型 的prop
代码演示
父组件App.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| <template> <div class="app"> <UserInfo :username="username" :age="age" :isSingle="isSingle" :car="car" :hobby="hobby" ></UserInfo> </div> </template>
<script> import UserInfo from './components/UserInfo.vue' export default { data() { return { username: '小帅', age: 28, isSingle: true, car: { brand: '宝马', }, hobby: ['篮球', '足球', '羽毛球'], } }, components: { UserInfo, }, } </script>
<style> </style>
|
子组件UserInfo.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| <template> <div class="userinfo"> <h3>我是个人信息组件</h3> <div>姓名:</div> <div>年龄:</div> <div>是否单身:</div> <div>座驾:</div> <div>兴趣爱好:</div> </div> </template>
<script> export default { } </script>
<style> .userinfo { width: 300px; border: 3px solid #000; padding: 20px; } .userinfo > div { margin: 20px 10px; } </style>
|
props校验
思考
组件的props可以乱传吗
作用
为组件的 prop 指定验证要求,不符合要求,控制台就会有错误提示 → 帮助开发者,快速发现错误
语法
代码演示
App.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| <template> <div class="app"> <BaseProgress :w="width"></BaseProgress> </div> </template>
<script> import BaseProgress from './components/BaseProgress.vue' export default { data() { return { width: 30, } }, components: { BaseProgress, }, } </script>
<style> </style>
|
BaseProgress.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| <template> <div class="base-progress"> <div class="inner" :style="{ width: w + '%' }"> <span>{{ w }}%</span> </div> </div> </template>
<script> export default { props: ['w'], } </script>
<style scoped> .base-progress { height: 26px; width: 400px; border-radius: 15px; background-color: #272425; border: 3px solid #272425; box-sizing: border-box; margin-bottom: 30px; } .inner { position: relative; background: #379bff; border-radius: 15px; height: 25px; box-sizing: border-box; left: -3px; top: -2px; } .inner span { position: absolute; right: 0; top: 26px; } </style>
|
props校验完整写法
语法
1 2 3 4 5 6 7 8 9 10 11
| props: { 校验的属性名: { type: 类型, // Number String Boolean ... required: true, // 是否必填 default: 默认值, // 默认值 validator (value) { // 自定义校验逻辑 return 是否通过校验 } } },
|
代码实例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| <script> export default { // 完整写法(类型、默认值、非空、自定义校验) props: { w: { type: Number, //required: true, default: 0, validator(val) { // console.log(val) if (val >= 100 || val <= 0) { console.error('传入的范围必须是0-100之间') return false } else { return true } }, }, }, } </script>
|
注意
1.default和required一般不同时写(因为当时必填项时,肯定是有值的)
2.default后面如果是简单类型的值,可以直接写默认。如果是复杂类型的值,则需要以函数的形式return一个默认值
props&data、单向数据流
共同点
都可以给组件提供数据
区别
- data 的数据是自己的 → 随便改
- prop 的数据是外部的 → 不能直接改,要遵循 单向数据流
单向数据流:
父级props 的数据更新,会向下流动,影响子组件。这个数据流动是单向的
代码演示
App.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| <template> <div class="app"> <BaseCount></BaseCount> </div> </template>
<script> import BaseCount from './components/BaseCount.vue' export default { components:{ BaseCount }, data(){ }, } </script>
<style>
</style>
|
BaseCount.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| <template> <div class="base-count"> <button @click="count--">-</button> <span>{{ count }}</span> <button @click="count++">+</button> </div> </template>
<script> export default { // 1.自己的数据随便修改 (谁的数据 谁负责) data () { return { count: 100, } }, // 2.外部传过来的数据 不能随便修改 //props: { // count: { // type: Number, // }, //} } </script>
<style> .base-count { margin: 20px; } </style>
|
口诀
谁的数据谁负责
非父子通信-event bus 事件总线
作用
非父子组件之间,进行简易消息传递。(复杂场景→ Vuex)
步骤
创建一个都能访问的事件总线 (空Vue实例)
1 2 3
| import Vue from 'vue' const Bus = new Vue() export default Bus
|
A组件(接受方),监听Bus的 $on事件
1 2 3 4 5
| created () { Bus.$on('sendMsg', (msg) => { this.msg = msg }) }
|
B组件(发送方),触发Bus的$emit事件
1
| Bus.$emit('sendMsg', '这是一个消息')
|
代码示例
EventBus.js
1 2 3
| import Vue from 'vue' const Bus = new Vue() export default Bus
|
BaseA.vue(接受方)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| <template> <div class="base-a"> 我是A组件(接收方) <p>{{msg}}</p> </div> </template>
<script> import Bus from '../utils/EventBus' export default { data() { return { msg: '', } }, } </script>
<style scoped> .base-a { width: 200px; height: 200px; border: 3px solid #000; border-radius: 3px; margin: 10px; } </style>
|
BaseB.vue(发送方)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| <template> <div class="base-b"> <div>我是B组件(发布方)</div> <button>发送消息</button> </div> </template>
<script> import Bus from '../utils/EventBus' export default { } </script>
<style scoped> .base-b { width: 200px; height: 200px; border: 3px solid #000; border-radius: 3px; margin: 10px; } </style>
|
App.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| <template> <div class="app"> <BaseA></BaseA> <BaseB></BaseB> </div> </template>
<script> import BaseA from './components/BaseA.vue' import BaseB from './components/BaseB.vue' export default { components:{ BaseA, BaseB } } </script>
<style>
</style>
|
总结
1.非父子组件传值借助什么?
2.什么是事件总线
3.发送方应该调用事件总线的哪个方法
4.接收方应该调用事件总线的哪个方法
5.一个组件发送数据,可不可以被多个组件接收
非父子通信-provide&inject
作用
跨层级共享数据
场景
语法
- 父组件 provide提供数据
1 2 3 4 5 6 7 8 9 10
| export default { provide () { return { color: this.color, userInfo: this.userInfo, } } }
|
2.子/孙组件 inject获取数据
1 2 3 4 5 6
| export default { inject: ['color','userInfo'], created () { console.log(this.color, this.userInfo) } }
|
注意
- provide提供的简单类型的数据不是响应式的,复杂类型数据是响应式。(推荐提供复杂类型数据)
- 子/孙组件通过inject获取的数据,不能在自身组件内修改
v-model原理
原理:
v-model本质上是一个语法糖。例如应用在输入框上,就是value属性 和 input事件 的合写
1 2 3 4 5 6 7 8
| <template> <div id="app" > <input v-model="msg" type="text">
<input :value="msg" @input="msg = $event.target.value" type="text"> </div> </template>
|
作用:
提供数据的双向绑定
- 数据变,视图跟着变 :value
- 视图变,数据跟着变 @input
注意
$event 用于在模板中,获取事件的形参
代码示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| <template> <div class="app"> <input type="text" /> <br /> <input type="text" /> </div> </template>
<script> export default { data() { return { msg1: '', msg2: '', } }, } </script> <style> </style>
|
v-model使用在其他表单元素上的原理
不同的表单元素, v-model在底层的处理机制是不一样的。比如给checkbox使用v-model
底层处理的是 checked属性和change事件。
不过咱们只需要掌握应用在文本框上的原理即可
表单类组件封装
需求目标
实现子组件和父组件数据的双向绑定 (实现App.vue中的selectId和子组件选中的数据进行双向绑定)
代码演示
App.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| <template> <div class="app"> <BaseSelect></BaseSelect> </div> </template>
<script> import BaseSelect from './components/BaseSelect.vue' export default { data() { return { selectId: '102', } }, components: { BaseSelect, }, } </script>
<style> </style>
|
BaseSelect.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| <template> <div> <select> <option value="101">北京</option> <option value="102">上海</option> <option value="103">武汉</option> <option value="104">广州</option> <option value="105">深圳</option> </select> </div> </template>
<script> export default { } </script>
<style> </style>
|
v-model简化代码
目标:
父组件通过v-model 简化代码,实现子组件和父组件数据 双向绑定
如何简化:
v-model其实就是 :value和@input事件的简写
- 子组件:props通过value接收数据,事件触发 input
- 父组件:v-model直接绑定数据
代码示例
子组件
1 2 3 4 5 6 7 8 9
| <select :value="value" @change="handleChange">...</select> props: { value: String }, methods: { handleChange (e) { this.$emit('input', e.target.value) } }
|
父组件
1
| <BaseSelect v-model="selectId"></BaseSelect>
|
.sync修饰符
作用
可以实现 子组件 与 父组件数据 的 双向绑定,简化代码
简单理解:子组件可以修改父组件传过来的props值
场景
封装弹框类的基础组件, visible属性 true显示 false隐藏
本质
.sync修饰符 就是 :属性名 和 @update:属性名 合写
语法
父组件
1 2 3 4 5 6 7 8
| //.sync写法 <BaseDialog :visible.sync="isShow" /> -------------------------------------- //完整写法 <BaseDialog :visible="isShow" @update:visible="isShow = $event" />
|
子组件
1 2 3 4 5
| props: { visible: Boolean },
this.$emit('update:visible', false)
|
代码示例
App.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| <template> <div class="app"> <button @click="openDialog">退出按钮</button> <BaseDialog :isShow="isShow"></BaseDialog> </div> </template>
<script> import BaseDialog from './components/BaseDialog.vue' export default { data() { return { isShow: false, } }, components: { BaseDialog, }, } </script>
<style> </style>
|
BaseDialog.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66
| <template> <div class="base-dialog-wrap" v-show="isShow"> <div class="base-dialog"> <div class="title"> <h3>温馨提示:</h3> <button class="close">x</button> </div> <div class="content"> <p>你确认要退出本系统么?</p> </div> <div class="footer"> <button>确认</button> <button>取消</button> </div> </div> </div> </template>
<script> export default { props: { isShow: Boolean, } } </script>
<style scoped> .base-dialog-wrap { width: 300px; height: 200px; box-shadow: 2px 2px 2px 2px #ccc; position: fixed; left: 50%; top: 50%; transform: translate(-50%, -50%); padding: 0 10px; } .base-dialog .title { display: flex; justify-content: space-between; align-items: center; border-bottom: 2px solid #000; } .base-dialog .content { margin-top: 38px; } .base-dialog .title .close { width: 20px; height: 20px; cursor: pointer; line-height: 10px; } .footer { display: flex; justify-content: flex-end; margin-top: 26px; } .footer button { width: 80px; height: 40px; } .footer button:nth-child(1) { margin-right: 10px; cursor: pointer; } </style>
|
总结
1.父组件如果想让子组件修改传过去的值 必须加什么修饰符?
2.子组件要修改父组件的props值 必须使用什么语法?
ref和$refs
作用
利用ref 和 $refs 可以用于 获取 dom 元素 或 组件实例
特点:
查找范围 → 当前组件内(更精确稳定)
语法
1.给要获取的盒子添加ref属性
1
| <div ref="chartRef">我是渲染图表的容器</div>
|
2.获取时通过 $refs获取 this.$refs.chartRef 获取
1 2 3
| mounted () { console.log(this.$refs.chartRef) }
|
注意
之前只用document.querySelect(‘.box’) 获取的是整个页面中的盒子
代码示例
App.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| <template> <div class="app"> <BaseChart></BaseChart> </div> </template>
<script> import BaseChart from './components/BaseChart.vue' export default { components:{ BaseChart } } </script>
<style> </style>
|
BaseChart.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
| <template> <div class="base-chart-box" ref="baseChartBox">子组件</div> </template>
<script> // yarn add echarts 或者 npm i echarts import * as echarts from 'echarts'
export default { mounted() { // 基于准备好的dom,初始化echarts实例 var myChart = echarts.init(document.querySelect('.base-chart-box')) // 绘制图表 myChart.setOption({ title: { text: 'ECharts 入门示例', }, tooltip: {}, xAxis: { data: ['衬衫', '羊毛衫', '雪纺衫', '裤子', '高跟鞋', '袜子'], }, yAxis: {}, series: [ { name: '销量', type: 'bar', data: [5, 20, 36, 10, 10, 20], }, ], }) }, } </script>
<style scoped> .base-chart-box { width: 400px; height: 300px; border: 3px solid #000; border-radius: 6px; } </style>
|
异步更新 & $nextTick
需求
编辑标题, 编辑框自动聚焦
- 点击编辑,显示编辑框
- 让编辑框,立刻获取焦点
代码实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| <template> <div class="app"> <div v-if="isShowEdit"> <input type="text" v-model="editValue" ref="inp" /> <button>确认</button> </div> <div v-else> <span>{{ title }}</span> <button @click="editFn">编辑</button> </div> </div> </template>
<script> export default { data() { return { title: '大标题', isShowEdit: false, editValue: '', } }, methods: { editFn() { // 显示输入框 this.isShowEdit = true // 获取焦点 this.$refs.inp.focus() } }, } </script>
|
问题
“显示之后”,立刻获取焦点是不能成功的!
原因:Vue 是异步更新DOM (提升性能)
解决方案
$nextTick:等 DOM更新后,才会触发执行此方法里的函数体
语法: this.$nextTick(函数体)
1 2 3
| this.$nextTick(() => { this.$refs.inp.focus() })
|
注意:$nextTick 内的函数体 一定是箭头函数,这样才能让函数内部的this指向Vue实例
自定义指令
指令介绍
自定义指令
概念:自己定义的指令,可以封装一些DOM操作,扩展额外的功能
自定义指令语法
指令中的配置项介绍
inserted:被绑定元素插入父节点时调用的钩子函数
el:使用指令的那个DOM元素
代码示例
需求:当页面加载时,让元素获取焦点(autofocus在safari浏览器有兼容性)
App.vue
1 2 3 4
| <div> <h1>自定义指令</h1> <input v-focus ref="inp" type="text"> </div>
|
自定义指令-指令的值
需求
实现一个 color 指令 - 传入不同的颜色, 给标签设置文字颜色
语法
1.在绑定指令时,可以通过“等号”的形式为指令 绑定 具体的参数值
1
| <div v-color="color">我是内容</div>
|
2.通过 binding.value 可以拿到指令值,指令值修改会 触发 update 函数
1 2 3 4 5 6 7 8 9 10
| directives: { color: { inserted (el, binding) { el.style.color = binding.value }, update (el, binding) { el.style.color = binding.value } } }
|
代码示例
App.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| <template> <div> <!--显示红色--> <h2 v-color="color1">指令的值1测试</h2> <!--显示蓝色--> <h2 v-color="color2">指令的值2测试</h2> <button> 改变第一个h1的颜色 </button> </div> </template>
<script> export default { data () { return { color1: 'red', color2: 'blue' } } } </script>
<style>
</style>
|
自定义指令-v-loading指令的封装
场景
实际开发过程中,发送请求需要时间,在请求的数据未回来时,页面会处于空白状态 => 用户体验不好
需求
封装一个 v-loading 指令,实现加载中的效果
分析
1.本质 loading效果就是一个蒙层,盖在了盒子上
2.数据请求中,开启loading状态,添加蒙层
3.数据请求完毕,关闭loading状态,移除蒙层
实现
1.准备一个 loading类,通过伪元素定位,设置宽高,实现蒙层
2.开启关闭 loading状态(添加移除蒙层),本质只需要添加移除类即可
3.结合自定义指令的语法进行封装复用
1 2 3 4 5 6 7 8 9
| .loading:before { content: ""; position: absolute; left: 0; top: 0; width: 100%; height: 100%; background: #fff url("./loading.gif") no-repeat center; }
|
准备代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108
| <template> <div class="main"> <div class="box"> <ul> <li v-for="item in list" :key="item.id" class="news"> <div class="left"> <div class="title">{{ item.title }}</div> <div class="info"> <span>{{ item.source }}</span> <span>{{ item.time }}</span> </div> </div> <div class="right"> <img :src="item.img" alt=""> </div> </li> </ul> </div> </div> </template>
<script>
import axios from 'axios'
export default { data () { return { list: [], isLoading: false, isLoading2: false } }, async created () { const res = await axios.get('http://hmajax.itheima.net/api/news') setTimeout(() => { this.list = res.data.data }, 2000) } } </script>
<style> .loading:before { content: ''; position: absolute; left: 0; top: 0; width: 100%; height: 100%; background: #fff url('./loading.gif') no-repeat center; }
.box2 { width: 400px; height: 400px; border: 2px solid #000; position: relative; }
.box { width: 800px; min-height: 500px; border: 3px solid orange; border-radius: 5px; position: relative; } .news { display: flex; height: 120px; width: 600px; margin: 0 auto; padding: 20px 0; cursor: pointer; } .news .left { flex: 1; display: flex; flex-direction: column; justify-content: space-between; padding-right: 10px; } .news .left .title { font-size: 20px; } .news .left .info { color: #999999; } .news .left .info span { margin-right: 20px; } .news .right { width: 160px; height: 120px; } .news .right img { width: 100%; height: 100%; object-fit: cover; } </style>
|
插槽-默认插槽
作用
让组件内部的一些 结构 支持 自定义
需求
将需要多次显示的对话框,封装成一个组件
问题
组件的内容部分,不希望写死,希望能使用的时候自定义。怎么办
插槽的基本语法
- 组件内需要定制的结构部分,改用****占位
- 使用组件时, ****标签内部, 传入结构替换slot
- 给插槽传入内容时,可以传入纯文本、html标签、组件
代码示例
MyDialog.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77
| <template> <div class="dialog"> <div class="dialog-header"> <h3>友情提示</h3> <span class="close">✖️</span> </div>
<div class="dialog-content"> 您确定要进行删除操作吗? </div> <div class="dialog-footer"> <button>取消</button> <button>确认</button> </div> </div> </template>
<script> export default { data () { return {
} } } </script>
<style scoped> * { margin: 0; padding: 0; } .dialog { width: 470px; height: 230px; padding: 0 25px; background-color: #ffffff; margin: 40px auto; border-radius: 5px; } .dialog-header { height: 70px; line-height: 70px; font-size: 20px; border-bottom: 1px solid #ccc; position: relative; } .dialog-header .close { position: absolute; right: 0px; top: 0px; cursor: pointer; } .dialog-content { height: 80px; font-size: 18px; padding: 15px 0; } .dialog-footer { display: flex; justify-content: flex-end; } .dialog-footer button { width: 65px; height: 35px; background-color: #ffffff; border: 1px solid #e1e3e9; cursor: pointer; outline: none; margin-left: 10px; border-radius: 3px; } .dialog-footer button:last-child { background-color: #007acc; color: #fff; } </style>
|
App.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| <template> <div> <MyDialog> </MyDialog> </div> </template>
<script> import MyDialog from './components/MyDialog.vue' export default { data () { return {
} }, components: { MyDialog } } </script>
<style> body { background-color: #b3b3b3; } </style>
|
总结
场景:组件内某一部分结构不确定,想要自定义怎么办
使用:插槽的步骤分为哪几步?
插槽-后备内容(默认值)
问题
通过插槽完成了内容的定制,传什么显示什么, 但是如果不传,则是空白
能否给插槽设置 默认显示内容 呢?
插槽的后备内容
封装组件时,可以为预留的 <slot>
插槽提供后备内容(默认内容)。
语法
在 标签内,放置内容, 作为默认显示内容
效果
代码示例
App.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| <template> <div> <MyDialog></MyDialog> <MyDialog> 你确认要退出么 </MyDialog> </div> </template>
<script> import MyDialog from './components/MyDialog.vue' export default { data () { return {
} }, components: { MyDialog } } </script>
<style> body { background-color: #b3b3b3; } </style>
|
插槽-具名插槽
需求
一个组件内有多处结构,需要外部传入标签,进行定制
上面的弹框中有三处不同,但是默认插槽只能定制一个位置,这时候怎么办呢?
具名插槽语法
v-slot的简写
v-slot写起来太长,vue给我们提供一个简单写法 v-slot —> #
总结
- 组件内 有多处不确定的结构 怎么办?
- 具名插槽的语法是什么?
- v-slot:插槽名可以简化成什么?
作用域插槽
插槽分类
默认插槽
具名插槽
插槽只有两种,作用域插槽不属于插槽的一种分类
作用
定义slot 插槽的同时, 是可以传值的。给 插槽 上可以 绑定数据,将来 使用组件时可以用
场景
封装表格组件
使用步骤
给 slot 标签, 以 添加属性的方式传值
1
| <slot :id="item.id" msg="测试文本"></slot>
|
所有添加的属性, 都会被收集到一个对象中
在template中, 通过 #插槽名= "obj"
接收,默认插槽名为 default
1 2 3 4 5
| <MyTable :list="list"> <template #default="obj"> <button @click="del(obj.id)">删除</button> </template> </MyTable>
|
代码示例
MyTable.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94
| <template> <table class="my-table"> <thead> <tr> <th>序号</th> <th>姓名</th> <th>年纪</th> <th>操作</th> </tr> </thead> <tbody> <tr> <td>1</td> <td>赵小云</td> <td>19</td> <td> <button> 查看 </button> </td> </tr> <tr> <td>1</td> <td>张小花</td> <td>19</td> <td> <button> 查看 </button> </td> </tr> <tr> <td>1</td> <td>孙大明</td> <td>19</td> <td> <button> 查看 </button> </td> </tr> </tbody> </table> </template>
<script> export default { props: { data: Array } } </script>
<style scoped> .my-table { width: 450px; text-align: center; border: 1px solid #ccc; font-size: 24px; margin: 30px auto; } .my-table thead { background-color: #1f74ff; color: #fff; } .my-table thead th { font-weight: normal; } .my-table thead tr { line-height: 40px; } .my-table th, .my-table td { border-bottom: 1px solid #ccc; border-right: 1px solid #ccc; } .my-table td:last-child { border-right: none; } .my-table tr:last-child td { border-bottom: none; } .my-table button { width: 65px; height: 35px; font-size: 18px; border: 1px solid #ccc; outline: none; border-radius: 3px; cursor: pointer; background-color: #ffffff; margin-left: 5px; } </style>
|
App.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| <template> <div> <MyTable :data="list"></MyTable> <MyTable :data="list2"></MyTable> </div> </template>
<script> import MyTable from './components/MyTable.vue' export default { data () { return { list: [ { id: 1, name: '张小花', age: 18 }, { id: 2, name: '孙大明', age: 19 }, { id: 3, name: '刘德忠', age: 17 }, ], list2: [ { id: 1, name: '赵小云', age: 18 }, { id: 2, name: '刘蓓蓓', age: 19 }, { id: 3, name: '姜肖泰', age: 17 }, ] } }, components: { MyTable } } </script>
|
总结
1.作用域插槽的作用是什么?
2.作用域插槽的使用步骤是什么?
综合案例 - 商品列表-MyTag组件抽离
需求说明
- my-tag 标签组件封装
(1) 双击显示输入框,输入框获取焦点
(2) 失去焦点,隐藏输入框
(3) 回显标签信息
(4) 内容修改,回车 → 修改标签信息
- my-table 表格组件封装
(1) 动态传递表格数据渲染
(2) 表头支持用户自定义
(3) 主体支持用户自定义
代码准备
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157
| <template> <div class="table-case"> <table class="my-table"> <thead> <tr> <th>编号</th> <th>名称</th> <th>图片</th> <th width="100px">标签</th> </tr> </thead> <tbody> <tr> <td>1</td> <td>梨皮朱泥三绝清代小品壶经典款紫砂壶</td> <td> <img src="https://yanxuan-item.nosdn.127.net/f8c37ffa41ab1eb84bff499e1f6acfc7.jpg" /> </td> <td> <div class="my-tag"> <!-- <input class="input" type="text" placeholder="输入标签" /> --> <div class="text"> 茶具 </div> </div> </td> </tr> <tr> <td>1</td> <td>梨皮朱泥三绝清代小品壶经典款紫砂壶</td> <td> <img src="https://yanxuan-item.nosdn.127.net/221317c85274a188174352474b859d7b.jpg" /> </td> <td> <div class="my-tag"> <!-- <input ref="inp" class="input" type="text" placeholder="输入标签" /> --> <div class="text"> 男靴 </div> </div> </td> </tr> </tbody> </table> </div> </template>
<script> export default { name: 'TableCase', components: {}, data() { return { goods: [ { id: 101, picture: 'https://yanxuan-item.nosdn.127.net/f8c37ffa41ab1eb84bff499e1f6acfc7.jpg', name: '梨皮朱泥三绝清代小品壶经典款紫砂壶', tag: '茶具', }, { id: 102, picture: 'https://yanxuan-item.nosdn.127.net/221317c85274a188174352474b859d7b.jpg', name: '全防水HABU旋钮牛皮户外徒步鞋山宁泰抗菌', tag: '男鞋', }, { id: 103, picture: 'https://yanxuan-item.nosdn.127.net/cd4b840751ef4f7505c85004f0bebcb5.png', name: '毛茸茸小熊出没,儿童羊羔绒背心73-90cm', tag: '儿童服饰', }, { id: 104, picture: 'https://yanxuan-item.nosdn.127.net/56eb25a38d7a630e76a608a9360eec6b.jpg', name: '基础百搭,儿童套头针织毛衣1-9岁', tag: '儿童服饰', }, ], } }, } </script>
<style lang="less" scoped> .table-case { width: 1000px; margin: 50px auto; img { width: 100px; height: 100px; object-fit: contain; vertical-align: middle; }
.my-table { width: 100%; border-spacing: 0; img { width: 100px; height: 100px; object-fit: contain; vertical-align: middle; } th { background: #f5f5f5; border-bottom: 2px solid #069; } td { border-bottom: 1px dashed #ccc; } td, th { text-align: center; padding: 10px; transition: all 0.5s; &.red { color: red; } } .none { height: 100px; line-height: 100px; color: #999; } } .my-tag { cursor: pointer; .input { appearance: none; outline: none; border: 1px solid #ccc; width: 100px; height: 40px; box-sizing: border-box; padding: 10px; color: #666; &::placeholder { color: #666; } } } } </style>
|
my-tag组件封装-创建组件
MyTag.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| <template> <div class="my-tag"> <!-- <input class="input" type="text" placeholder="输入标签" /> --> <div class="text"> 茶具 </div> </div> </template>
<script> export default { } </script>
<style lang="less" scoped> .my-tag { cursor: pointer; .input { appearance: none; outline: none; border: 1px solid #ccc; width: 100px; height: 40px; box-sizing: border-box; padding: 10px; color: #666; &::placeholder { color: #666; } } } </style>
|
App.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| <template> ... <tbody> <tr> .... <td> <MyTag></MyTag> </td> </tr> </tbody> ... </template> <script> import MyTag from './components/MyTag.vue' export default { name: 'TableCase', components: { MyTag, }, .... </script>
|
综合案例-MyTag组件控制显示隐藏
MyTag.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| <template> <div class="my-tag"> <input v-if="isEdit" v-focus ref="inp" class="input" type="text" placeholder="输入标签" @blur="isEdit = false" /> <div v-else @dblclick="handleClick" class="text"> 茶具 </div> </div> </template>
<script> export default { data () { return { isEdit: false } }, methods: { handleClick () { this.isEdit = true } } } </script>
|
main.js
1 2 3 4 5 6 7
| Vue.directive('focus', { inserted (el) { el.focus() } })
|
综合案例-MyTag组件进行v-model绑定
App.vue
1 2 3 4 5 6 7 8
| <MyTag v-model="tempText"></MyTag> <script> export default { data(){ tempText:'水杯' } } </script>
|
MyTag.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
| <template> <div class="my-tag"> <input v-if="isEdit" v-focus ref="inp" class="input" type="text" placeholder="输入标签" :value="value" @blur="isEdit = false" @keyup.enter="handleEnter" /> <div v-else @dblclick="handleClick" class="text"> {{ value }} </div> </div> </template>
<script> export default { props: { value: String }, data () { return { isEdit: false } }, methods: { handleClick () { this.isEdit = true }, handleEnter (e) { // 非空处理 if (e.target.value.trim() === '') return alert('标签内容不能为空') this.$emit('input', e.target.value) // 提交完成,关闭输入状态 this.isEdit = false } } } </script>
|
综合案例-封装MyTable组件-动态渲染数据
App.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| <template> <div class="table-case"> <MyTable :data="goods"></MyTable> </div> </template>
<script> import MyTable from './components/MyTable.vue' export default { name: 'TableCase', components: { MyTable }, data(){ return { .... } }, } </script>
|
MyTable.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74
| <template> <table class="my-table"> <thead> <tr> <th>编号</th> <th>名称</th> <th>图片</th> <th width="100px">标签</th> </tr> </thead> <tbody> <tr v-for="(item, index) in data" :key="item.id"> <td>{{ index + 1 }}</td> <td>{{ item.name }}</td> <td> <img :src="item.picture" /> </td> <td> 标签内容 <!-- <MyTag v-model="item.tag"></MyTag> --> </td> </tr> </tbody> </table> </template>
<script> export default { props: { data: { type: Array, required: true } } }; </script>
<style lang="less" scoped>
.my-table { width: 100%; border-spacing: 0; img { width: 100px; height: 100px; object-fit: contain; vertical-align: middle; } th { background: #f5f5f5; border-bottom: 2px solid #069; } td { border-bottom: 1px dashed #ccc; } td, th { text-align: center; padding: 10px; transition: all .5s; &.red { color: red; } } .none { height: 100px; line-height: 100px; color: #999; } }
</style>
|
综合案例-封装MyTable组件-自定义结构
App.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
| <template> <div class="table-case"> <MyTable :data="goods"> <template #head> <th>编号</th> <th>名称</th> <th>图片</th> <th width="100px">标签</th> </template>
<template #body="{ item, index }"> <td>{{ index + 1 }}</td> <td>{{ item.name }}</td> <td> <img :src="item.picture" /> </td> <td> <MyTag v-model="item.tag"></MyTag> </td> </template> </MyTable> </div> </template>
<script> import MyTag from './components/MyTag.vue' import MyTable from './components/MyTable.vue' export default { name: 'TableCase', components: { MyTag, MyTable }, data () { return { .... } } </script>
|
MyTable.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| <template> <table class="my-table"> <thead> <tr> <slot name="head"></slot> </tr> </thead> <tbody> <tr v-for="(item, index) in data" :key="item.id"> <slot name="body" :item="item" :index="index" ></slot> </tr> </tbody> </table> </template>
<script> export default { props: { data: { type: Array, required: true } } }; </script>
|
案例答案
MyTable.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79
| <template> <table class="my-table"> <thead> <tr> <slot name="head"> </slot> </tr> </thead> <tbody> <tr v-for="(item,index) in data" :key="item.id"> <slot name="body" :item="item" :index="index"></slot> </tr> </tbody> </table> </template>
<script> export default { name: "MyTable", props:{ data:{ type:Array, required:true } } } </script>
<style lang="less" scoped> .table-case { width: 1000px; margin: 50px auto;
img { width: 100px; height: 100px; object-fit: contain; vertical-align: middle; }
.my-table { width: 100%; border-spacing: 0;
img { width: 100px; height: 100px; object-fit: contain; vertical-align: middle; }
th { background: #f5f5f5; border-bottom: 2px solid #069; }
td { border-bottom: 1px dashed #ccc; }
td, th { text-align: center; padding: 10px; transition: all 0.5s;
&.red { color: red; } }
.none { height: 100px; line-height: 100px; color: #999; } } } </style>
|
MyTag.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63
| <template> <div class="my-tag"> <input v-if="isEdit" class="input" type="text" :value="value" v-focus placeholder="输入标签" @keyup.enter="handleUpdate" @blur="isEdit=false"/> <div v-else class="text" @dblclick="handleClick"> {{ value }} </div> </div> </template>
<script> export default { name: "MyTag", props: { value: { type: String } }, data() { return { isEdit: false } }, methods: { handleClick: function () { this.isEdit = true }, handleUpdate(el){ this.$emit('input',el.target.value) this.isEdit = false } } } </script>
<style lang="less" scoped> .my-tag { cursor: pointer;
.input { appearance: none; outline: none; border: 1px solid #ccc; width: 100px; height: 40px; box-sizing: border-box; padding: 10px; color: #666;
&::placeholder { color: #666; } } }
</style>
|
App.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69
| <template> <div class="table-case"> <MyTable :data="goods"> <template v-slot:head> <th>编号</th> <th>名称</th> <th>图片</th> <th width="100px">标签</th> </template> <template #body="{ item,index }"> <td>{{ index + 1 }}</td> <td>{{ item.name }}</td> <td> <img :src="item.picture"/> </td> <td> <MyTag v-model="item.tag"></MyTag> </td> </template> </MyTable> </div> </template>
<script> import MyTable from "@/components/MyTable.vue"; import MyTag from "@/components/MyTag.vue";
export default { name: 'TableCase', components: { MyTable, MyTag }, data() { return { goods: [ { id: 101, picture: 'https://yanxuan-item.nosdn.127.net/f8c37ffa41ab1eb84bff499e1f6acfc7.jpg', name: '梨皮朱泥三绝清代小品壶经典款紫砂壶', tag: '茶具', }, { id: 102, picture: 'https://yanxuan-item.nosdn.127.net/221317c85274a188174352474b859d7b.jpg', name: '全防水HABU旋钮牛皮户外徒步鞋山宁泰抗菌', tag: '男鞋', }, { id: 103, picture: 'https://yanxuan-item.nosdn.127.net/cd4b840751ef4f7505c85004f0bebcb5.png', name: '毛茸茸小熊出没,儿童羊羔绒背心73-90cm', tag: '儿童服饰', }, { id: 104, picture: 'https://yanxuan-item.nosdn.127.net/56eb25a38d7a630e76a608a9360eec6b.jpg', name: '基础百搭,儿童套头针织毛衣1-9岁', tag: '儿童服饰', }, ], } }, } </script>
<style lang="less" scoped>
</style>
|
main.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| import Vue from 'vue' import App from './App.vue'
Vue.config.productionTip = false
Vue.directive('focus', { 'inserted'(el) { el.focus() } })
new Vue({ render: h => h(App), }).$mount('#app')
|
单页应用程序介绍
概念
单页应用程序:SPA【Single Page Application】是指所有的功能都在一个html页面上实现
具体示例
单页应用网站: 网易云音乐 https://music.163.com/
多页应用网站:京东 https://jd.com/
单页应用 VS 多页面应用
单页应用类网站:系统类网站 / 内部网站 / 文档类网站 / 移动端站点
多页应用类网站:公司官网 / 电商类网站
总结
1.什么是单页面应用程序?
2.单页面应用优缺点?
3.单页应用场景?
路由介绍
思考
单页面应用程序,之所以开发效率高,性能好,用户体验好
最大的原因就是:页面按需更新
比如当点击【发现音乐】和【关注】时,只是更新下面部分内容,对于头部是不更新的
要按需更新,首先就需要明确:访问路径和 组件的对应关系!
访问路径 和 组件的对应关系如何确定呢? 路由
路由的介绍
生活中的路由:设备和ip的映射关系
Vue中的路由:路径和组件的映射关系
总结
路由的基本使用
目标
认识插件 VueRouter,掌握 VueRouter 的基本使用步骤
作用
修改地址栏路径时,切换显示匹配的组件
说明
Vue 官方的一个路由插件,是一个第三方包
官网
https://v3.router.vuejs.org/zh/
VueRouter的使用(5+2)
固定5个固定的步骤(不用死背,熟能生巧)
下载 VueRouter 模块到当前工程,版本3.6.5
1
| yarn add vue-router@3.6.5
|
main.js中引入VueRouter
1
| import VueRouter from 'vue-router'
|
安装注册
创建路由对象
1
| const router = new VueRouter()
|
注入,将路由对象注入到new Vue实例中,建立关联
1 2 3 4 5
| new Vue({ render: h => h(App), router:router }).$mount('#app')
|
当我们配置完以上5步之后 就可以看到浏览器地址栏中的路由 变成了 /#/的形式。表示项目的路由已经被Vue-Router管理了
代码示例
main.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| // 路由的使用步骤 5 + 2 // 5个基础步骤 // 1. 下载 v3.6.5 // yarn add vue-router@3.6.5 // 2. 引入 // 3. 安装注册 Vue.use(Vue插件) // 4. 创建路由对象 // 5. 注入到new Vue中,建立关联
import VueRouter from 'vue-router' Vue.use(VueRouter) // VueRouter插件初始化
const router = new VueRouter()
new Vue({ render: h => h(App), router }).$mount('#app')
|
两个核心步骤
创建需要的组件 (views目录),配置路由规则
配置导航,配置路由出口(路径匹配的组件显示的位置)
App.vue
1 2 3 4 5 6 7 8
| <div class="footer_wrap"> <a href="#/find">发现音乐</a> <a href="#/my">我的音乐</a> <a href="#/friend">朋友</a> </div> <div class="top"> <router-view></router-view> </div>
|
总结
- 如何实现 路径改变,对应组件 切换,应该使用哪个插件?
- Vue-Router的使用步骤是什么(5+2)?
组件的存放目录问题
注意: .vue文件 本质无区别
1.组件分类
.vue文件分为2类,都是 .vue文件(本质无区别)
- 页面组件 (配置路由规则时使用的组件)
- 复用组件(多个组件中都使用到的组件)
存放目录
分类开来的目的就是为了 更易维护
src/views文件夹
页面组件 - 页面展示 - 配合路由用
src/components文件夹
复用组件 - 展示数据 - 常用于复用
总结
- 组件分类有哪两类?分类的目的?
- 不同分类的组件应该放在什么文件夹?作用分别是什么?
路由的封装抽离
问题:所有的路由配置都在main.js中合适吗?
目标:将路由模块抽离出来。 好处:拆分模块,利于维护
路径简写:
脚手架环境下 @指代src目录,可以用于快速引入组件
总结:
- 路由模块的封装抽离的好处是什么?
- 以后如何快速引入组件?
声明式导航-导航链接
需求
实现导航高亮效果
如果使用a标签进行跳转的话,需要给当前跳转的导航加样式,同时要移除上一个a标签的样式,太麻烦!!!
解决方案
vue-router 提供了一个全局组件 router-link (取代 a 标签)
- 能跳转,配置 to 属性指定路径(必须) 。本质还是 a 标签 ,to 无需 #
- 能高亮,默认就会提供高亮类名,可以直接设置高亮样式
语法: 发现音乐
1 2 3 4 5 6 7 8 9 10 11
| <div> <div class="footer_wrap"> <router-link to="/find">发现音乐</router-link> <router-link to="/my">我的音乐</router-link> <router-link to="/friend">朋友</router-link> </div> <div class="top"> <!-- 路由出口 → 匹配的组件所展示的位置 --> <router-view></router-view> </div> </div>
|
通过router-link自带的两个样式进行高亮
使用router-link跳转后,我们发现。当前点击的链接默认加了两个class的值 router-link-exact-active
和router-link-active
我们可以给任意一个class属性添加高亮样式即可实现功能
总结
- router-link是什么?
- router-link怎么用?
- router-link的好处是什么?
声明式导航-两个类名
当我们使用跳转时,自动给当前导航加了两个类名
router-link-active
模糊匹配(用的多)
to=”/my” 可以匹配 /my /my/a /my/b ….
只要是以/my开头的路径 都可以和 to=”/my”匹配到
router-link-exact-active
精确匹配
to=”/my” 仅可以匹配 /my
在地址栏中输入二级路由查看类名的添加
总结
- router-link 会自动给当前导航添加两个类名,有什么区别呢?
声明式导航-自定义类名(了解)
问题
router-link的两个高亮类名 太长了,我们希望能定制怎么办
解决方案
我们可以在创建路由对象时,额外配置两个配置项即可。 linkActiveClass
和linkExactActiveClass
1 2 3 4 5
| const router = new VueRouter({ routes: [...], linkActiveClass: "类名1", linkExactActiveClass: "类名2" })
|
代码演示
1 2 3 4 5 6 7 8
| const router = new VueRouter({ routes: [ ... ], linkActiveClass: 'active', linkExactActiveClass: 'exact-active' })
|
总结
如何自定义router-link的两个高亮类名
声明式导航-查询参数传参
目标
在跳转路由时,进行传参
现在我们在搜索页点击了热门搜索链接,跳转到详情页,需要把点击的内容带到详情页,改怎么办呢?
跳转传参
我们可以通过两种方式,在跳转的时候把所需要的参数传到其他页面中
查询参数传参
如何传参?
如何接受参数
固定用法:$router.query.参数名
代码演示
App.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| <template> <div id="app"> <div class="link"> <router-link to="/home">首页</router-link> <router-link to="/search">搜索页</router-link> </div>
<router-view></router-view> </div> </template>
<script> export default {}; </script>
<style scoped> .link { height: 50px; line-height: 50px; background-color: #495150; display: flex; margin: -8px -8px 0 -8px; margin-bottom: 50px; } .link a { display: block; text-decoration: none; background-color: #ad2a26; width: 100px; text-align: center; margin-right: 5px; color: #fff; border-radius: 5px; } </style>
|
Home.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62
| <template> <div class="home"> <div class="logo-box"></div> <div class="search-box"> <input type="text"> <button>搜索一下</button> </div> <div class="hot-link"> 热门搜索: <router-link to="">黑马程序员</router-link> <router-link to="">前端培训</router-link> <router-link to="">如何成为前端大牛</router-link> </div> </div> </template>
<script> export default { name: 'FindMusic' } </script>
<style> .logo-box { height: 150px; background: url('@/assets/logo.jpeg') no-repeat center; } .search-box { display: flex; justify-content: center; } .search-box input { width: 400px; height: 30px; line-height: 30px; border: 2px solid #c4c7ce; border-radius: 4px 0 0 4px; outline: none; } .search-box input:focus { border: 2px solid #ad2a26; } .search-box button { width: 100px; height: 36px; border: none; background-color: #ad2a26; color: #fff; position: relative; left: -2px; border-radius: 0 4px 4px 0; } .hot-link { width: 508px; height: 60px; line-height: 60px; margin: 0 auto; } .hot-link a { margin: 0 5px; } </style>
|
Search.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| <template> <div class="search"> <p>搜索关键字: 黑马程序员</p> <p>搜索结果: </p> <ul> <li>.............</li> <li>.............</li> <li>.............</li> <li>.............</li> </ul> </div> </template>
<script> export default { name: 'MyFriend', created () { // 在created中,获取路由参数 } } </script>
<style> .search { width: 400px; height: 240px; padding: 0 20px; margin: 0 auto; border: 2px solid #c4c7ce; border-radius: 5px; } </style>
|
router/index.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| import Home from '@/views/Home' import Search from '@/views/Search' import Vue from 'vue' import VueRouter from 'vue-router' Vue.use(VueRouter)
const router = new VueRouter({ routes: [ { path: '/home', component: Home }, { path: '/search', component: Search } ] })
export default router
|
main.js
1 2 3 4 5 6 7
| ... import router from './router/index' ... new Vue({ render: h => h(App), router }).$mount('#app')
|
声明式导航-动态路由传参
动态路由传参方式
配置动态路由
动态路由后面的参数可以随便起名,但要有语义
1 2 3 4 5 6 7 8 9
| const router = new VueRouter({ routes: [ ..., { path: '/search/:words', component: Search } ] })
|
配置导航链接
to=”/path/参数值”
对应页面组件接受参数
$route.params.参数名
params后面的参数名要和动态路由配置的参数保持一致
查询参数传参 VS 动态路由传参
查询参数传参 (比较适合传多个参数)
- 跳转:to=”/path?参数名=值&参数名2=值”
- 获取:$route.query.参数名
动态路由传参 (优雅简洁,传单个参数比较方便)
- 配置动态路由:path: “/path/:参数名”
- 跳转:to=”/path/参数值”
- 获取:$route.params.参数名
注意:动态路由也可以传多个参数,但一般只传一个
总结
声明式导航跳转时, 有几种方式传值给路由页面?
- 查询参数传参(多个参数)
- 动态路由传参(一个参数,优雅简洁)
动态路由参数的可选符(了解)
问题
配了路由 path:”/search/:words” 为什么按下面步骤操作,会未匹配到组件,显示空白?
原因
/search/:words 表示,必须要传参数。如果不传参数,也希望匹配,可以加个可选符”?”
1 2 3 4 5 6
| const router = new VueRouter({ routes: [ ... { path: '/search/:words?', component: Search } ] })
|
Vue路由-重定向
问题
网页打开时, url 默认是 / 路径,未匹配到组件时,会出现空白
解决方案
重定向 → 匹配 / 后, 强制跳转 /home 路径
语法
1 2 3
| { path: 匹配路径, redirect: 重定向到的路径 }, 比如: { path:'/' ,redirect:'/home' }
|
代码演示
1 2 3 4 5 6
| const router = new VueRouter({ routes: [ { path: '/', redirect: '/home'}, ... ] })
|
Vue路由-404
作用
当路径找不到匹配时,给个提示页面
位置
404的路由,虽然配置在任何一个位置都可以,但一般都配置在其他路由规则的最后面
语法
path: “*” (任意路径) – 前面不匹配就命中最后这个
1 2 3 4 5 6 7 8
| import NotFind from '@/views/NotFind'
const router = new VueRouter({ routes: [ ... { path: '*', component: NotFind } ] })
|
代码示例
NotFound.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| <template> <div> <h1>404 Not Found</h1> </div> </template>
<script> export default {
} </script>
<style>
</style>
|
router/index.js
1 2 3 4 5 6 7 8 9 10 11 12 13
| ... import NotFound from '@/views/NotFound' ...
const router = new VueRouter({ routes: [ ... { path: '*', component: NotFound } ] })
export default router
|
Vue路由-模式设置
问题
路由的路径看起来不自然, 有#,能否切成真正路径形式?
语法
1 2 3 4
| const router = new VueRouter({ mode:'histroy', routes:[] })
|
编程式导航-两种路由跳转方式
问题
点击按钮跳转如何实现?
方案
编程式导航:用JS代码来进行跳转
语法
两种语法:
- path 路径跳转 (简易方便)
- name 命名路由跳转 (适合 path 路径长的场景)
path路径跳转语法
特点:简易方便
1 2 3 4 5 6 7
| this.$router.push('路由路径')
this.$router.push({ path: '路由路径' })
|
代码演示 path跳转方式
name命名路由跳转
特点:适合 path 路径长的场景
语法:
路由规则,必须配置name配置项
1
| { name: '路由名', path: '/path/xxx', component: XXX },
|
通过name来进行跳转
1 2 3
| this.$router.push({ name: '路由名' })
|
代码演示通过name命名路由跳转
总结
编程式导航有几种跳转方式?
编程式导航-path路径跳转传参
问题
点击搜索按钮,跳转需要把文本框中输入的内容传到下一个页面如何实现?
两种传参方式
1.查询参数
2.动态路由传参
传参
两种跳转方式,对于两种传参方式都支持:
① path 路径跳转传参
② name 命名路由跳转传参
path路径跳转传参(query传参)
1 2 3 4 5 6 7 8 9 10
| this.$router.push('/路径?参数名1=参数值1&参数2=参数值2')
this.$router.push({ path: '/路径', query: { 参数名1: '参数值1', 参数名2: '参数值2' } })
|
接受参数的方式依然是:$route.query.参数名
path路径跳转传参(动态路由传参)
1 2 3 4 5 6
| //简单写法 this.$router.push('/路径/参数值') //完整写法 this.$router.push({ path: '/路径/参数值' })
|
接受参数的方式依然是:$route.params.参数值
注意:path不能配合params使用
编程式导航-name命名路由传参
name 命名路由跳转传参 (query传参)
1 2 3 4 5 6 7
| this.$router.push({ name: '路由名字', query: { 参数名1: '参数值1', 参数名2: '参数值2' } })
|
name 命名路由跳转传参 (动态路由传参)
1 2 3 4 5 6
| this.$router.push({ name: '路由名字', params: { 参数名: '参数值', } })
|
总结
编程式导航,如何跳转传参?
1.path路径跳转
query传参
1 2 3 4 5 6 7 8
| this.$router.push('/路径?参数名1=参数值1&参数2=参数值2') this.$router.push({ path: '/路径', query: { 参数名1: '参数值1', 参数名2: '参数值2' } })
|
动态路由传参
1 2 3 4
| this.$router.push('/路径/参数值') this.$router.push({ path: '/路径/参数值' })
|
2.name命名路由跳转
query传参
1 2 3 4 5 6 7
| this.$router.push({ name: '路由名字', query: { 参数名1: '参数值1', 参数名2: '参数值2' } })
|
动态路由传参 (需要配动态路由)
1 2 3 4 5 6
| this.$router.push({ name: '路由名字', params: { 参数名: '参数值', } })
|
面经基础版-案例效果分析
面经效果演示
功能分析
- 通过演示效果发现,主要的功能页面有两个,一个是列表页,一个是详情页,并且在列表页点击时可以跳转到详情页
- 底部导航可以来回切换,并且切换时,只有上面的主题内容在动态渲染
实现思路分析:配置路由+功能实现
1.配置路由
- 首页和面经详情页,两个一级路由
- 首页内嵌套4个可切换的页面(嵌套二级路由)
2.实现功能
- 首页请求渲染
- 跳转传参 到 详情页,详情页动态渲染
- 组件缓存,性能优化
面经基础版-一级路由配置
1.把文档中准备的素材拷贝到项目中
2.针对router/index.js文件 进行一级路由配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| ... import Layout from '@/views/Layout.vue' import ArticleDetail from '@/views/ArticleDetail.vue' ...
const router = new VueRouter({ routes: [ { path: '/', component: Layout }, { path: '/detail', component: ArticleDetail } ] })
|
面经基础版-二级路由配置
二级路由也叫嵌套路由,当然也可以嵌套三级、四级…
使用场景
当在页面中点击链接跳转,只是部分内容切换时,我们可以使用嵌套路由
语法
- 在一级路由下,配置children属性即可
- 配置二级路由的出口
1.在一级路由下,配置children属性
注意:一级的路由path 需要加 /
二级路由的path不需要加 /
1 2 3 4 5 6 7 8 9 10 11 12 13
| const router = new VueRouter({ routes: [ { path: '/', component: Layout, children:[ {path:'xxxx',component:xxxx.vue}, {path:'xxxx',component:xxxx.vue}, ] } ] })
|
技巧:二级路由应该配置到哪个一级路由下呢?
这些二级路由对应的组件渲染到哪个一级路由下,children就配置到哪个路由下边
2.配置二级路由的出口
注意: 配置了嵌套路由,一定配置对应的路由出口,否则不会渲染出对应的组件
Layout.vue
1 2 3 4 5 6 7 8
| <template> <div class="h5-wrapper"> <div class="content"> <router-view></router-view> </div> .... </div> </template>
|
代码实现
router/index.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| ... import Article from '@/views/Article.vue' import Collect from '@/views/Collect.vue' import Like from '@/views/Like.vue' import User from '@/views/User.vue' ...
const router = new VueRouter({ routes: [ { path: '/', component: Layout, redirect: '/article', children:[ { path:'/article', component:Article }, { path:'/collect', component:Collect }, { path:'/like', component:Like }, { path:'/user', component:User } ] }, .... ] })
|
Layout.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| <template> <div class="h5-wrapper"> <div class="content"> <!-- 内容部分 --> <router-view></router-view> </div> <nav class="tabbar"> <a href="#/article">面经</a> <a href="#/collect">收藏</a> <a href="#/like">喜欢</a> <a href="#/user">我的</a> </nav> </div> </template>
|
面经基础版-二级导航高亮
实现思路
- 将a标签替换成 组件,配置to属性,不用加 #
- 结合高亮类名实现高亮效果 (推荐模糊匹配:router-link-active)
代码实现
Layout.vue
1 2 3 4 5 6 7 8 9 10 11 12 13
| .... <nav class="tabbar"> <router-link to="/article">面经</router-link> <router-link to="/collect">收藏</router-link> <router-link to="/like">喜欢</router-link> <router-link to="/user">我的</router-link> </nav>
<style> a.router-link-active { color: orange; } </style>
|
面经基础版-首页请求渲染
步骤分析
1.安装axios
2.看接口文档,确认请求方式,请求地址,请求参数
3.created中发送请求,获取数据,存储到data中
4.页面动态渲染
代码实现
1.安装axios
yarn add axios
npm i axios
2.接口文档
1 2
| 请求地址: https://mock.boxuegu.com/mock/3083/articles 请求方式: get
|
3.created中发送请求,获取数据,存储到data中
1 2 3 4 5 6 7 8 9
| data() { return { articelList: [], } }, async created() { const { data: { result: { rows } }} = await axios.get('https://mock.boxuegu.com/mock/3083/articles') this.articelList = rows },
|
4.页面动态渲染
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| <template> <div class="article-page"> <div class="article-item" v-for="item in articelList" :key="item.id"> <div class="head"> <img :src="item.creatorAvatar" alt="" /> <div class="con"> <p class="title">{{ item.stem }}</p> <p class="other">{{ item.creatorName }} | {{ item.createdAt }}</p> </div> </div> <div class="body"> {{item.content}} </div> <div class="foot">点赞 {{item.likeCount}} | 浏览 {{item.views}}</div> </div> </div> </template>
|
面经基础版-查询参数传参
说明
跳转详情页需要把当前点击的文章id传给详情页,获取数据
- 查询参数传参 this.$router.push(‘/detail?参数1=参数值&参数2=参数值’)
- 动态路由传参 先改造路由 在传参 this.$router.push(‘/detail/参数值’)
查询参数传参实现
Article.vue
1 2 3 4 5 6 7 8 9
| <template> <div class="article-page"> <div class="article-item" v-for="item in articelList" :key="item.id" @click="$router.push(`/detail?id=${item.id}`)"> ... </div> </div> </template>
|
ArticleDetail.vue
1 2 3
| created(){ console.log(this.$route.query.id) }
|
面经基础版-动态路由传参
实现步骤
代码实现
改造路由
router/index.js
1 2 3 4 5
| ... { path: '/detail/:id', component: ArticleDetail }
|
Article.vue
1 2 3 4 5
| <div class="article-item" v-for="item in articelList" :key="item.id" @click="$router.push(`/detail/${item.id}`)"> .... </div>
|
ArticleDetail.vue
1 2 3
| created(){ console.log(this.$route.params.id) }
|
额外优化功能点-点击回退跳转到上一页
ArticleDetail.vue
1 2 3 4 5 6
| <template> <div class="article-detail-page"> <nav class="nav"><span class="back" @click="$router.back()"><</span> 面经详情</nav> .... </div> </template>
|
面经基础版-详情页渲染
实现步骤分析
- 导入axios
- 查看接口文档
- 在created中发送请求
- 页面动态渲染
代码实现
接口文档
1 2
| 请求地址: https://mock.boxuegu.com/mock/3083/articles/:id 请求方式: get
|
在created中发送请求
1 2 3 4 5 6 7 8 9 10 11 12
| data() { return { articleDetail:{} } }, async created() { const id = this.$route.params.id const {data:{result}} = await axios.get( `https://mock.boxuegu.com/mock/3083/articles/${id}` ) this.articleDetail = result },
|
页面动态渲染
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| <template> <div class="article-detail-page"> <nav class="nav"> <span class="back" @click="$router.back()"><</span> 面经详情 </nav> <header class="header"> <h1>{{articleDetail.stem}}</h1> <p>{{articleDetail.createAt}} | {{articleDetail.views}} 浏览量 | {{articleDetail.likeCount}} 点赞数</p> <p> <img :src="articleDetail.creatorAvatar" alt="" /> <span>{{articleDetail.creatorName}}</span> </p> </header> <main class="body"> {{articleDetail.content}} </main> </div> </template>
|
面经基础版-缓存组件
1.问题
从面经列表 点到 详情页,又点返回,数据重新加载了 → 希望回到原来的位置
原因
当路由被跳转后,原来所看到的组件就被销毁了(会执行组件内的beforeDestroy和destroyed生命周期钩子),重新返回后组件又被重新创建了(会执行组件内的beforeCreate,created,beforeMount,Mounted生命周期钩子),所以数据被加载了
解决方案
利用keep-alive把原来的组件给缓存下来
什么是keep-alive
keep-alive 是 Vue 的内置组件,当它包裹动态组件时,会缓存不活动的组件实例,而不是销毁它们。
keep-alive 是一个抽象组件:它自身不会渲染成一个 DOM 元素,也不会出现在父组件中。
优点:
在组件切换过程中把切换出去的组件保留在内存中,防止重复渲染DOM,
减少加载时间及性能消耗,提高用户体验性。
App.vue
1 2 3 4 5 6 7
| <template> <div class="h5-wrapper"> <keep-alive> <router-view></router-view> </keep-alive> </div> </template>
|
问题:
缓存了所有被切换的组件
keep-alive的三个属性
① include : 组件名数组,只有匹配的组件会被缓存
② exclude : 组件名数组,任何匹配的组件都不会被缓存
③ max : 最多可以缓存多少组件实例
App.vue
1 2 3 4 5 6 7
| <template> <div class="h5-wrapper"> <keep-alive :include="['LayoutPage']"> <router-view></router-view> </keep-alive> </div> </template>
|
额外的两个生命周期钩子
keep-alive的使用会触发两个生命周期函数
activated 当组件被激活(使用)的时候触发 → 进入这个页面的时候触发
deactivated 当组件不被使用的时候触发 → 离开这个页面的时候触发
组件缓存后就不会执行组件的created, mounted, destroyed 等钩子了
所以其提供了actived 和deactived钩子,帮我们实现业务需求。
7.总结
1.keep-alive是什么
2.keep-alive的优点
3.keep-alive的三个属性 (了解)
4.keep-alive的使用会触发两个生命周期函数(了解)
VueCli 自定义创建项目
1.安装脚手架 (已安装)
2.创建项目
1
| vue create hm-exp-mobile
|
1 2 3 4 5
| Vue CLI v5.0.8 ? Please pick a preset: Default ([Vue 3] babel, eslint) Default ([Vue 2] babel, eslint) > Manually select features 选自定义
|
- 选择eslint的风格 (eslint 代码规范的检验工具,检验代码是否符合规范)
- 比如:const age = 18; => 报错!多加了分号!后面有工具,一保存,全部格式化成最规范的样子
- 是否保存预设,下次直接使用? => 不保存,输入 N
ESlint代码规范及手动修复
代码规范:一套写代码的约定规则。例如:赋值符号的左右是否需要空格?一句结束是否是要加;?…
没有规矩不成方圆
ESLint:是一个代码检查工具,用来检查你的代码是否符合指定的规则(你和你的团队可以自行约定一套规则)。在创建项目时,我们使用的是 JavaScript Standard Style 代码风格的规则。
JavaScript Standard Style 规范说明
建议把:https://standardjs.com/rules-zhcn.html 看一遍,然后在写的时候, 遇到错误就查询解决。
下面是这份规则中的一小部分:
- 字符串使用单引号 – 需要转义的地方除外
- 无分号 – 这没什么不好。不骗你!
- 关键字后加空格
if (condition) { ... }
- 函数名后加空格
function name (arg) { ... }
- 坚持使用全等
===
摒弃 ==
一但在需要检查 null || undefined
时可以使用 obj == null
- ……
代码规范错误
如果你的代码不符合standard的要求,eslint会跳出来刀子嘴,豆腐心地提示你。
下面我们在main.js中随意做一些改动:添加一些空行,空格。
1 2 3 4 5 6 7 8 9 10 11 12 13
| import Vue from 'vue' import App from './App.vue'
import './styles/index.less' import router from './router' Vue.config.productionTip = false
new Vue ( { render: h => h(App), router }).$mount('#app')
|
按下保存代码之后:
你将会看在控制台中输出如下错误:
eslint 是来帮助你的。心态要好,有错,就改。
手动修正
根据错误提示来一项一项手动修正。
如果你不认识命令行中的语法报错是什么意思,你可以根据错误代码(func-call-spacing, space-in-parens,…..)去 ESLint 规则列表中查找其具体含义。
打开 ESLint 规则表,使用页面搜索(Ctrl + F)这个代码,查找对该规则的一个释义。
通过eslint插件来实现自动修正
- eslint会自动高亮错误显示
- 通过配置,eslint会自动帮助我们修复错误
1 2 3 4 5 6
| "editor.codeActionsOnSave": { "source.fixAll": true },
"editor.formatOnSave": false
|
- 注意:eslint的配置文件必须在根目录下,这个插件才能才能生效。打开项目必须以根目录打开,一次打开一个项目
- 注意:使用了eslint校验之后,把vscode带的那些格式化工具全禁用了 Beatify
settings.json 参考
1 2 3 4 5 6 7 8 9 10 11 12
| { "window.zoomLevel": 2, "workbench.iconTheme": "vscode-icons", "editor.tabSize": 2, "emmet.triggerExpansionOnTab": true, "editor.codeActionsOnSave": { "source.fixAll": true }, "editor.formatOnSave": false }
|
明确Vuex是什么,应用场景以及优势
是什么
Vuex 是一个 Vue 的 状态管理工具,状态就是数据。
大白话:Vuex 是一个插件,可以帮我们管理 Vue 通用的数据 (多组件共享的数据)。例如:购物车数据 个人信息数
使用场景
某个状态 在 很多个组件 来使用 (个人信息)
多个组件 共同维护 一份数据 (购物车)
优势
- 共同维护一份数据,数据集中化管理
- 响应式变化
- 操作简洁 (vuex提供了一些辅助函数)
注意:
官方原文:
- 不是所有的场景都适用于vuex,只有在必要的时候才使用vuex
- 使用了vuex之后,会附加更多的框架中的概念进来,增加了项目的复杂度 (数据的操作更便捷,数据的流动更清晰)
Vuex就像《近视眼镜》, 你自然会知道什么时候需要用它~
需求: 多组件共享数据
目标:基于脚手架创建项目,构建 vuex 多组件数据共享环境
效果是三个组件共享一份数据:
- 任意一个组件都可以修改数据
- 三个组件的数据是同步的
创建项目
创建三个组件, 目录如下
1 2 3 4
| |-components |--Son1.vue |--Son2.vue |-App.vue
|
源代码如下
App.vue
在入口组件中引入 Son1 和 Son2 这两个子组件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| <template> <div id="app"> <h1>根组件</h1> <input type="text"> <Son1></Son1> <hr> <Son2></Son2> </div> </template>
<script> import Son1 from './components/Son1.vue' import Son2 from './components/Son2.vue'
export default { name: 'app', data: function () { return {
} }, components: { Son1, Son2 } } </script>
<style> #app { width: 600px; margin: 20px auto; border: 3px solid #ccc; border-radius: 3px; padding: 10px; } </style>
|
main.js
1 2 3 4 5 6 7 8
| import Vue from 'vue' import App from './App.vue'
Vue.config.productionTip = false
new Vue({ render: h => h(App) }).$mount('#app')
|
Son1.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| <template> <div class="box"> <h2>Son1 子组件</h2> 从vuex中获取的值: <label></label> <br> <button>值 + 1</button> </div> </template>
<script> export default { name: 'Son1Com' } </script>
<style lang="css" scoped> .box{ border: 3px solid #ccc; width: 400px; padding: 10px; margin: 20px; } h2 { margin-top: 10px; } </style>
|
Son2.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| <template> <div class="box"> <h2>Son2 子组件</h2> 从vuex中获取的值:<label></label> <br /> <button>值 - 1</button> </div> </template>
<script> export default { name: 'Son2Com' } </script>
<style lang="css" scoped> .box { border: 3px solid #ccc; width: 400px; padding: 10px; margin: 20px; } h2 { margin-top: 10px; } </style>
|
vuex 的使用 - 创建仓库
安装 vuex
安装vuex与vue-router类似,vuex是一个独立存在的插件,如果脚手架初始化没有选 vuex,就需要额外安装。
1
| yarn add vuex@3 或者 npm i vuex@3
|
新建 store/index.js
专门存放 vuex
为了维护项目目录的整洁,在src目录下新建一个store目录其下放置一个index.js文件。 (和 router/index.js
类似)
创建仓库 store/index.js
1 2 3 4 5 6 7 8 9 10 11 12
| import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
const store = new Vuex.Store()
export default store
|
在 main.js 中导入挂载到 Vue 实例上
1 2 3 4 5 6 7 8 9 10
| import Vue from 'vue' import App from './App.vue' import store from './store'
Vue.config.productionTip = false
new Vue({ render: h => h(App), store }).$mount('#app')
|
此刻起, 就成功创建了一个 空仓库!!
测试打印Vuex
App.vue
1 2 3
| created(){ console.log(this.$store) }
|
核心概念 - state 状态
目标
明确如何给仓库 提供 数据,如何 使用 仓库的数据
提供数据
State提供唯一的公共数据源,所有共享的数据都要统一放到Store中的State中存储。
打开项目中的store.js文件,在state对象中可以添加我们要共享的数据。
1 2 3 4 5 6 7 8 9 10
| const store = new Vuex.Store({ state: { count: 101 } })
|
访问Vuex中的数据
问题: 如何在组件中获取count?
- 通过$store直接访问 —>
- 通过辅助函数mapState 映射计算属性 —>
通过$store访问的语法
1 2 3 4 5 6 7 8
| 获取 store: 1.Vue模板中获取 this.$store 2.js文件中获取 import 导入 store
模板中: {{ $store.state.xxx }} 组件逻辑中: this.$store.state.xxx JS模块中: store.state.xxx
|
代码实现
模板中使用
组件中可以使用 $store 获取到vuex中的store对象实例,可通过state属性属性获取count, 如下
1
| <h1>state的数据 - {{ $store.state.count }}</h1>
|
组件逻辑中使用
将state属性定义在计算属性中 https://vuex.vuejs.org/zh/guide/state.html
1 2 3 4 5 6 7 8
| <h1>state的数据 - {{ count }}</h1>
computed: { count () { return this.$store.state.count } }
|
js文件中使用
1 2 3 4 5
| //main.js
import store from "@/store"
console.log(store.state.count)
|
每次都像这样一个个的提供计算属性, 太麻烦了,我们有没有简单的语法帮我们获取state中的值呢?
通过辅助函数 - mapState获取 state中的数据
mapState是辅助函数,帮助我们把store中的数据映射到 组件的计算属性中, 它属于一种方便的用法
用法 :
第一步:导入mapState (mapState是vuex中的一个函数)
1
| import { mapState } from 'vuex'
|
第二步:采用数组形式引入state属性
上面代码的最终得到的是 类似于
1 2 3
| count () { return this.$store.state.count }
|
第三步:利用展开运算符将导出的状态映射给计算属性
1 2 3
| computed: { ...mapState(['count']) }
|
1
| <div> state的数据:{{ count }}</div>
|
开启严格模式及Vuex的单项数据流
目标
明确 vuex 同样遵循单向数据流,组件中不能直接修改仓库的数据
直接在组件中修改Vuex中state的值
Son1.vue
1 2 3 4 5 6 7 8 9 10
| button @click="handleAdd">值 + 1</button>
methods:{ handleAdd (n) { // 错误代码(vue默认不会监测,监测需要成本) this.$store.state.count++ // console.log(this.$store.state.count) }, }
|
开启严格模式
通过 strict: true
可以开启严格模式,开启严格模式后,直接修改state中的值会报错
state数据的修改只能通过mutations,并且mutations必须是同步的
核心概念-mutations
定义mutations
1 2 3 4 5 6 7 8 9
| const store = new Vuex.Store({ state: { count: 0 }, mutations: { } })
|
格式说明
mutations是一个对象,对象中存放修改state的方法
1 2 3 4 5 6 7
| mutations: { addCount (state) { state.count += 1 } },
|
组件中提交 mutations
1
| this.$store.commit('addCount')
|
练习
1.在mutations中定义个点击按钮进行 +5 的方法
2.在mutations中定义个点击按钮进行 改变title 的方法
3.在组件中调用mutations修改state中的值
总结
通过mutations修改state的步骤
1.定义 mutations 对象,对象中存放修改 state 的方法
2.组件中提交调用 mutations(通过$store.commit(‘mutations的方法名’))
带参数的 mutations
目标:
掌握 mutations 传参语法
语法
看下面这个案例,每次点击不同的按钮,加的值都不同,每次都要定义不同的mutations处理吗?
提交 mutation 是可以传递参数的 this.$store.commit('xxx', 参数)
提供mutation函数(带参数)
1 2 3 4 5 6
| mutations: { ... addCount (state, count) { state.count = count } },
|
提交mutation
1 2 3
| handle ( ) { this.$store.commit('addCount', 10) }
|
小tips: 提交的参数只能是一个, 如果有多个参数要传, 可以传递一个对象
1 2 3
| this.$store.commit('addCount', { count: 10 })
|
练习-mutations的减法功能
步骤
代码实现
Son2.vue
1 2 3 4 5 6 7 8 9 10 11
| <button @click="subCount(1)">值 - 1</button> <button @click="subCount(5)">值 - 5</button> <button @click="subCount(10)">值 - 10</button>
export default { methods:{ subCount (n) { this.$store.commit('addCount', n) }, } }
|
store/index.js
1 2 3 4 5
| mutations:{ subCount (state, n) { state.count -= n }, }
|
目标
实时输入,实时更新,巩固 mutations 传参语法
实现步骤
代码实现
App.vue
1 2 3 4 5 6 7 8 9 10 11 12
| <input :value="count" @input="handleInput" type="text">
export default { methods: { handleInput (e) { // 1. 实时获取输入框的值 const num = +e.target.value // 2. 提交mutation,调用mutation函数 this.$store.commit('changeCount', num) } } }
|
store/index.js
1 2 3 4 5
| mutations: { changeCount (state, newCount) { state.count = newCount } },
|
辅助函数- mapMutations
mapMutations和mapState很像,它把位于mutations中的方法提取了出来,我们可以将它导入
1 2 3 4
| import { mapMutations } from 'vuex' methods: { ...mapMutations(['addCount']) }
|
上面代码的含义是将mutations的方法导入了methods中,等价于
1 2 3 4 5 6
| methods: { addCount () { this.$store.commit('addCount') } }
|
此时,就可以直接通过this.addCount调用了
1
| <button @click="addCount">值+1</button>
|
但是请注意: Vuex中mutations中要求不能写异步代码,如果有异步的ajax请求,应该放置在actions中
核心概念 - actions
state是存放数据的,mutations是同步更新数据 (便于监测数据的变化, 更新视图等, 方便于调试工具查看变化),
actions则负责进行异步操作
说明:mutations必须是同步的
需求: 一秒钟之后, 要给一个数 去修改state
定义actions
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| mutations: { changeCount (state, newCount) { state.count = newCount } }
actions: { setAsyncCount (context, num) { setTimeout(() => { context.commit('changeCount', num) }, 1000) } },
|
组件中通过dispatch调用
1 2 3
| setAsyncCount () { this.$store.dispatch('setAsyncCount', 666) }
|
辅助函数 -mapActions
1.目标:掌握辅助函数 mapActions,映射方法
mapActions 是把位于 actions中的方法提取了出来,映射到组件methods中
Son2.vue
1 2 3 4 5 6 7 8 9 10 11
| import { mapActions } from 'vuex' methods: { ...mapActions(['changeCountAction']) }
|
直接通过 this.方法 就可以调用
1
| <button @click="changeCountAction(200)">+异步</button>
|
核心概念 - getters
除了state之外,有时我们还需要从state中筛选出符合条件的一些数据,这些数据是依赖state的,此时会用到getters
例如,state中定义了list,为1-10的数组,
1 2 3
| state: { list: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] }
|
组件中,需要显示所有大于5的数据,正常的方式,是需要list在组件中进行再一步的处理,但是getters可以帮助我们实现它
定义getters
1 2 3 4 5
| getters: { filterList: state => state.list.filter(item => item > 5) }
|
使用getters
原始方式-$store
1
| <div>{{ $store.getters.filterList }}</div>
|
辅助函数 - mapGetters
1 2 3
| computed: { ...mapGetters(['filterList']) }
|
1
| <div>{{ filterList }}</div>
|
使用小结
核心概念 - module
目标
掌握核心概念 module 模块的创建
问题
由于使用单一状态树,应用的所有状态会集中到一个比较大的对象。当应用变得非常复杂时,store 对象就有可能变得相当臃肿。
这句话的意思是,如果把所有的状态都放在state中,当项目变得越来越大的时候,Vuex会变得越来越难以维护
由此,又有了Vuex的模块化
模块定义 - 准备 state
定义两个模块 user 和 setting
user中管理用户的信息状态 userInfo modules/user.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| const state = { userInfo: { name: 'zs', age: 18 } }
const mutations = {}
const actions = {}
const getters = {}
export default { state, mutations, actions, getters }
|
setting中管理项目应用的 主题色 theme,描述 desc, modules/setting.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| const state = { theme: 'dark' desc: '描述真呀真不错' }
const mutations = {}
const actions = {}
const getters = {}
export default { state, mutations, actions, getters }
|
在store/index.js
文件中的modules配置项中,注册这两个模块
1 2 3 4 5 6 7 8 9
| import user from './modules/user' import setting from './modules/setting'
const store = new Vuex.Store({ modules:{ user, setting } })
|
使用模块中的数据, 可以直接通过模块名访问 $store.state.模块名.xxx
=> $store.state.setting.desc
也可以通过 mapState 映射
获取模块内的state数据
目标:
掌握模块中 state 的访问语法
尽管已经分模块了,但其实子模块的状态,还是会挂到根级别的 state 中,属性名就是模块名
使用模块中的数据
- 直接通过模块名访问 $store.state.模块名.xxx
- 通过 mapState 映射:
- 默认根级别的映射 mapState([ ‘xxx’ ])
- 子模块的映射 :mapState(‘模块名’, [‘xxx’]) - 需要开启命名空间 namespaced:true
modules/user.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| const state = { userInfo: { name: 'zs', age: 18 }, myMsg: '我的数据' }
const mutations = { updateMsg (state, msg) { state.myMsg = msg } }
const actions = {}
const getters = {}
export default { namespaced: true, state, mutations, actions, getters }
|
代码示例
$store直接访问
1
| $store.state.user.userInfo.name
|
mapState辅助函数访问
1 2
| ...mapState('user', ['userInfo']), ...mapState('setting', ['theme', 'desc']),
|
获取模块内的getters数据
目标:
掌握模块中 getters 的访问语
语法:
使用模块中 getters 中的数据:
- 直接通过模块名访问
$store.getters['模块名/xxx ']
- 通过 mapGetters 映射
- 默认根级别的映射
mapGetters([ 'xxx' ])
- 子模块的映射
mapGetters('模块名', ['xxx'])
- 需要开启命名空间
代码演示
modules/user.js
1 2 3 4 5 6
| const getters = { UpperCaseName (state) { return state.userInfo.name.toUpperCase() } }
|
Son1.vue 直接访问getters
1 2
| <div>{{ $store.getters['user/UpperCaseName'] }}</div>
|
Son2.vue 通过命名空间访问
1 2 3
| computed:{ ...mapGetters('user', ['UpperCaseName']) }
|
获取模块内的mutations方法
目标:
掌握模块中 mutation 的调用语法
注意:
默认模块中的 mutation 和 actions 会被挂载到全局,需要开启命名空间,才会挂载到子模块。
调用方式:
- 直接通过 store 调用 $store.commit(‘模块名/xxx ‘, 额外参数)
- 通过 mapMutations 映射
- 默认根级别的映射 mapMutations([ ‘xxx’ ])
- 子模块的映射 mapMutations(‘模块名’, [‘xxx’]) - 需要开启命名空间
代码实现
modules/user.js
1 2 3 4 5
| const mutations = { setUser (state, newUserInfo) { state.userInfo = newUserInfo } }
|
modules/setting.js
1 2 3 4 5
| const mutations = { setTheme (state, newTheme) { state.theme = newTheme } }
|
Son1.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| <button @click="updateUser">更新个人信息</button> <button @click="updateTheme">更新主题色</button>
export default { methods: { updateUser () { // $store.commit('模块名/mutation名', 额外传参) this.$store.commit('user/setUser', { name: 'xiaowang', age: 25 }) }, updateTheme () { this.$store.commit('setting/setTheme', 'pink') } } }
|
Son2.vue
1 2 3 4 5 6 7 8
| <button @click="setUser({ name: 'xiaoli', age: 80 })">更新个人信息</button> <button @click="setTheme('skyblue')">更新主题</button>
methods:{ // 分模块的映射 ...mapMutations('setting', ['setTheme']), ...mapMutations('user', ['setUser']), }
|
获取模块内的actions方法
目标:
掌握模块中 action 的调用语法 (同理 - 直接类比 mutation 即可)
注意:
默认模块中的 mutation 和 actions 会被挂载到全局,需要开启命名空间,才会挂载到子模块。
调用语法:
- 直接通过 store 调用 $store.dispatch(‘模块名/xxx ‘, 额外参数)
- 通过 mapActions 映射
- 默认根级别的映射 mapActions([ ‘xxx’ ])
- 子模块的映射 mapActions(‘模块名’, [‘xxx’]) - 需要开启命名空间
代码实现
需求:
modules/user.js
1 2 3 4 5 6 7 8 9
| const actions = { setUserSecond (context, newUserInfo) { setTimeout(() => { context.commit('setUser', newUserInfo) }, 1000) } }
|
Son1.vue 直接通过store调用
1 2 3 4 5 6 7 8 9 10 11
| <button @click="updateUser2">一秒后更新信息</button>
methods:{ updateUser2 () { // 调用action dispatch this.$store.dispatch('user/setUserSecond', { name: 'xiaohong', age: 28 }) }, }
|
Son2.vue mapActions映射
1 2 3 4 5
| <button @click="setUserSecond({ name: 'xiaoli', age: 80 })">一秒后更新信息</button>
methods:{ ...mapActions('user', ['setUserSecond']) }
|
Vuex模块化的使用小结
直接使用
- state –> $store.state.模块名.数据项名
- getters –> $store.getters[‘模块名/属性名’]
- mutations –> $store.commit(‘模块名/方法名’, 其他参数)
- actions –> $store.dispatch(‘模块名/方法名’, 其他参数)
借助辅助方法使用
1.import { mapXxxx, mapXxx } from ‘vuex’
computed、methods: {
// …mapState、…mapGetters放computed中;
// …mapMutations、…mapActions放methods中;
…mapXxxx(‘模块名’, [‘数据项|方法’]),
…mapXxxx(‘模块名’, { 新的名字: 原来的名字 }),
}
2.组件中直接使用 属性 {{ age }}
或 方法 @click="updateAge(2)"
综合案例 - 创建项目
脚手架新建项目 (注意:勾选vuex)
版本说明:
vue2 vue-router3 vuex3
vue3 vue-router4 vuex4/pinia
1
| vue create vue-cart-demo
|
- 将原本src内容清空,替换成教学资料的《vuex-cart-准备代码》
需求:
- 发请求动态渲染购物车,数据存vuex (存cart模块, 将来还会有user模块,article模块…)
- 数字框可以修改数据
- 动态计算总价和总数量
综合案例-构建vuex-cart模块
- 新建
store/modules/cart.js
1 2 3 4 5 6 7 8
| export default { namespaced: true, state () { return { list: [] } }, }
|
- 挂载到 vuex 仓库上
store/cart.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| import Vuex from 'vuex' import Vue from 'vue'
import cart from './modules/cart'
Vue.use(Vuex)
const store = new Vuex.Store({ modules: { cart } })
export default store
|
综合案例-准备后端接口服务环境(了解)
- 安装全局工具 json-server (全局工具仅需要安装一次)
1
| yarn global add json-server 或 npm i json-server -g
|
- 代码根目录新建一个 db 目录
- 将资料 index.json 移入 db 目录
- 进入 db 目录,执行命令,启动后端接口服务 (使用–watch 参数 可以实时监听 json 文件的修改)
1
| json-server --watch index.json
|
综合案例-请求动态渲染数据
目标
请求获取数据存入 vuex, 映射渲染
- 安装 axios
- 准备actions 和 mutations
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| import axios from 'axios'
export default { namespaced: true, state () { return { list: [] } }, mutations: { updateList (state, payload) { state.list = payload } }, actions: { async getList (ctx) { const res = await axios.get('http://localhost:3000/cart') ctx.commit('updateList', res.data) } } }
|
App.vue
页面中调用 action, 获取数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| import { mapState } from 'vuex'
export default { name: 'App', components: { CartHeader, CartFooter, CartItem }, created () { this.$store.dispatch('cart/getList') }, computed: { ...mapState('cart', ['list']) } }
|
- 动态渲染
1 2
| <!-- 商品 Item 项组件 --> <cart-item v-for="item in list" :key="item.id" :item="item"></cart-item>
|
cart-item.vue
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| <template> <div class="goods-container"> <div class="left"> <img :src="item.thumb" class="avatar" alt=""> </div> <div class="right"> <div class="title">{{item.name}}</div> <div class="info"> <span class="price">¥{{item.price}}</span> <div class="btns"> <button class="btn btn-light">-</button> <span class="count">{{item.count}}</span> <button class="btn btn-light">+</button> </div> </div> </div> </div> </template>
<script> export default { name: 'CartItem', props: { item: Object }, methods: {
} } </script>
|
综合案例-修改数量
- 注册点击事件
1 2 3 4
| <!-- 按钮区域 --> <button class="btn btn-light" @click="onBtnClick(-1)">-</button> <span class="count">{{item.count}}</span> <button class="btn btn-light" @click="onBtnClick(1)">+</button>
|
- 页面中dispatch action
1 2 3 4 5 6 7 8 9 10
| onBtnClick (step) { const newCount = this.item.count + step if (newCount < 1) return
this.$store.dispatch('cart/updateCount', { id: this.item.id, count: newCount }) }
|
- 提供action函数
1 2 3 4 5 6
| async updateCount (ctx, payload) { await axios.patch('http://localhost:3000/cart/' + payload.id, { count: payload.count }) ctx.commit('updateCount', payload) }
|
- 提供mutation处理函数
1 2 3 4 5 6 7
| mutations: { ..., updateCount (state, payload) { const goods = state.list.find((item) => item.id === payload.id) goods.count = payload.count } },
|
综合案例-底部总价展示
- 提供getters
1 2 3 4 5 6 7 8
| getters: { total(state) { return state.list.reduce((p, c) => p + c.count, 0); }, totalPrice (state) { return state.list.reduce((p, c) => p + c.count * c.price, 0); }, },
|
- 动态渲染
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| <template> <div class="footer-container"> <div> <span>共 {{total}} 件商品,合计:</span> <span class="price">¥{{totalPrice}}</span> </div> <button class="btn btn-success btn-settle">结算</button> </div> </template>
<script> import { mapGetters } from 'vuex' export default { name: 'CartFooter', computed: { ...mapGetters('cart', ['total', 'totalPrice']) } } </script>
|