认识Vue.js
Vue(读音/vju:/,类似于view)
Vue是一个渐进式框架,什么是渐进式的呢?
- 渐进式意味着你可以将Vue作为你应用的一部分嵌入其中,(如可以将以前的JQuery项目中的某一张页面重构成Vue框架)
- 如果你希望将更多的业务逻辑使用Vue实现,那么Vue的核心库以及其生态系统Core + Vue-router + Vuex也可以满足你各种需求
Vue有很多特点和Web开发中常见的高级功能:
- 解耦视图和数据
- 可复用的组件
- 前端路由技术
- 状态管理
- 虚拟DOM
Vue.js安装
- 直接CDN引入
<!-- 开发环境版本,包含了有帮助的命令行警告 -->
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<!-- 生产环境版本,优化了尺寸和速度 -->
<script src="https://cdn.jsdelivr.net/npm/vue"></script>
- 下载Vue.js并引入
- https://cn.vuejs.org/js/vue.js,开发版本:包含完整的警告和调试模式
- https://cn.vuejs.org/js/vue.min.js,生产版本:删除了警告,33.30KB:min+gzip
- NPM安装
在用 Vue 构建大型应用时推荐使用 NPM 安装[1]。NPM 能很好地和诸如 webpack 或 Browserify 模块打包器配合使用。同时 Vue 也提供配套工具来开发单文件组件。
$ npm install vue
编写第一个Vue程序
创建Vue对象的时候,传入了一些options:{ },其中包含了
el
属性,该属性决定了这个Vue对象挂载到哪个元素上data
属性,该属性中通常会存储一些数据,这些数据可以是我们直接定义出来的,也可以是从服务器获取过来的
<div id="app">
{{message}}
</div>
<script>
const app = new Vue({
el: '#app',
data: {
message: 'Hello,Vue!'
}
})
</script>
在VSCode中自动生成Vue模板
选择文件 → 首选项 → 用户片段 → 输入html进入到html.json文件
添加以下代码即可自定义生成一个Vue模板
"Print to console": {
"prefix": "vue",
"body": [
"\t<div id=\"app\">\n\t\t\n\t</div>\n",
"\t<script>",
"\t\tlet app = new Vue({",
"\t\t\tel: '#app',",
"\t\t\tdata: {\n\t\t\t\t\n\t\t\t},",
"\t\t\tmethods: {\n\t\t\t\t\n\t\t\t}",
"\t\t});",
"\t</script>"
],
"description": "Log output to console"
}
Vue中的MVVM
MVVM分为三个部分:分别是M(Model,模型层 ),V(View,视图层),VM(ViewModel,V与M连接的桥梁,也可以看作为控制器)
- M:模型层,主要负责业务数据相关;
- V:视图层,顾名思义,负责视图相关,细分下来就是html+css层;
- VM:V与M沟通的桥梁,负责监听M或者V的修改,是实现MVVM双向绑定的要点;
MVVM支持双向绑定,意思就是当M层数据进行修改时,VM层会监测到变化,并且通知V层进行相应的修改,反之修改V层则会通知M层数据进行修改,以此也实现了视图与模型层的相互解耦;
Vue的生命周期
beforeCreate
实例创建前:这个阶段实例的data、methods是读不到的created
实例创建后:这个阶段已经完成了数据观测(data observer),属性和方法的运算, watch/event 事件回调。mount挂载阶段还没开始,$el 属性目前不可见,数据并没有在DOM元素上进行渲染beforeMount
:在挂载开始之前被调用:相关的 render 函数首次被调用。mounted
:el选项的DOM节点 被新创建的 vm.$el 替换,并挂载到实例上去之后调用此生命周期函数。此时实例的数据在DOM节点上进行渲染beforeUpdate
:数据更新时调用,但不进行DOM重新渲染,在数据更新时DOM没渲染前可以在这个生命函数里进行状态处理updated
:这个状态下数据更新并且DOM重新渲染,当这个生命周期函数被调用时,组件 DOM 已经更新,所以你现在可以执行依赖于 DOM 的操作。当实例每次进行数据更新时updated都会执行beforeDestory
:实例销毁之前调用。destroyed
:Vue 实例销毁后调用。调用后,Vue 实例指示的所有东西都会解绑定,所有的事件监听器会被移除,所有的子实例也会被销毁。activated
:页面处于活跃状态时执行回调,需配合keep-alive使用deactivated
:页面失去活跃时回调,需配合keep-alive使用
vue生命周期在真实场景下的业务应用:
created:进行ajax请求异步数据的获取、初始化数据
mounted:挂载元素内dom节点的获取
nextTick:针对单一事件更新数据后立即操作dom
updated:任何数据的更新,如果要做统一的业务逻辑处理
watch:监听具体数据变化,并做相应的处理
Vue基础语法
mustache语法
mustache语法就是差值表达式{{ }}
,其中不仅可以写变量,也可以写简单的表达式
mustache语法必须写到标签的内容中,不能作为标签的属性
<div id="app">
{{msg1 + msg2}} //我真帅你也帅
{{msg1 + '' + msg2}} //我真帅 你也帅
{{num * 2}} //4
</div>
<script>
let app = new Vue({
el: '#app',
data: {
msg1: '我真帅',
msg2: '你也帅',
num: 2
},
methods: {
}
});
</script>
v-once
在某些情况下,我们可能不希望界面随意的跟随改变,此时我们可以使用v-once
指令
该指令后面不需要跟任何表达式(比如之前的v-for后面是由跟表达式的)
该指令表示元素和组件(组件后面才会学习)只渲染一次,不会随着数据的改变而改变
<div id="app">
<h2 v-once>{{msg}}</h2>
//无论msg的值如何改变,该h2标签都不再重新渲染
</div>
v-html
某些情况下,我们从服务器请求到的数据本身就是一个HTML代码
如果我们直接通过{{}}
来输出,会将HTML代码也一起输出
但是我们可能希望的是按照HTML格式进行解析,并且显示对应的内容
注意:v-html
会替换标签中的所有内容,包括标签,文本
<div id="app">
<h2 v-html="url">
<span>我是h2自带的标签</span>
</h2>
//会将data中的url值转换成html结构并覆盖插入到该h2标签中
//解析为:
<h2>
<a href='https://www.baidu.com'>百度一下</a>
<h2>
</div>
<script>
let app = new Vue({
el: '#app',
data: {
url: "<a href='https://www.baidu.com'>百度一下</a>"
},
});
</script>
v-text
v-text
会将数据以字符串的形式直接渲染到界面中
注意:v-text
也会替换标签中的所有内容,包括标签,文本
<h2 v-text="msg">
h2自带的文字
<span>h2自带的span标签</span>
</h2>
//解析为:
<h2>我是Vue实例中的msg<h2>
v-pre
v-pre
用于跳过这个元素和它子元素的编译过程,不会再通过Vue进行解析,而是直接原封不同的展示
<h2 v-pre>{{msg}}</h2>
//直接显示{{msg}},不会显示msg中的内容
v-cloak
使用v-cloak
时当网速较慢时不会将插值表达式{{ msg }}显示出来,只有当加载完毕时才会渲染到页面上,同时该标签内所有元素都会受影响
原理:在Vue解析之前,为标签添加v-cloak
样式,在解析之后删除该样式
//需添加css样式
<style>
[v-cloak] {
display: none;
}
</style>
<h2 v-cloak>
{{msg}}
<a href="#">百度</a>
</h2>
//网速较慢而未加载到js时,屏幕不会显示h2标签的所有内容,js加载完成后再实时渲染到界面上
v-bind
前面我们学习的指令主要是修改对应元素的内容
而v-bind
可以动态的修改元素的属性,如a元素的href属性,img元素的src属性
如果属性前不加v-bind,那么浏览器不会解析等号后的内容,写的什么就是什么;如果前面加了v-bind,那么会通过Vue解析为data属性里对应的元素值,修改该data元素会动态的修改src属性值
<div id="app">
<img v-bind:src="imgSrc" alt="IU李知恩">
//语法糖写法,直接写一个冒号
<img :src="imgSrc" alt="IU李知恩">
</div>
<script>
let app = new Vue({
el: '#app',
data: {
imgSrc: 'https://img3.doubanio.com/view/photo/l/public/p2496437132.webp'
}
});
</script>
v-bind的对象语法
修改class属性
当我们需要动态修改标签的class属性时,可以使用v-bind
的对象语法
对象里的属性值必须是布尔值,为true则将该属性添加为类名;为false则移除该类名;可以有多个键值对存在
如果原生class属性和v-bind:class
同时存在,不会影响原生class
//下面代码实现一个功能:点击一下添加active类,再点击一下移除
<div id="app">
<div @click="changeColor" v-bind:class="{'active':isActive}">
</div>
</div>
<script>
let app = new Vue({
el: '#app',
data: {
isActive: true,
},
methods: {
changeColor: function () {
this.isActive = !this.isActive;
}
}
});
</script>
如果使用对象语法时里面的键值对过多,那么可以放在一个methods或computed中:
<div id="app">
<div :class="getClass()"></div>
//注意这里的getClass()必须加括号,@click不用加括号是因为被省略了
</div>
<script>
let app = new Vue({
el: '#app',
data: {
isRed: true,
isBlue: false
},
methods: {
getClass: function () {
return {
red: this.isRed,
blue: this.isBlue
};
}
}
});
</script>
修改style属性
我们可以利用v-bind:style
的对象语法来绑定一些CSS内联样式,对象的属性名为样式名,属性值为样式值
在写CSS属性名的时候,比如font-size
我们可以使用驼峰式fontSize
,或短横线分隔式'font-size'
(需要加单引号)
css属性较多时,也可以通过方法返回或使用计算属性
<div id="app">
//使用驼峰式的属性名,不需要加引号;属性值如果不是纯数字,则属性值需要加引号
<div :style="{fontSize:'58px',backgroundColor:'yellow'}">文字</div>
//使用短横线式的属性值,需要加引号;属性值不是数字且不加引号,则会去data中寻找
<div :style="{'font-size':size + 'px','background-color':color}">文字</div>
</div>
<script>
let app = new Vue({
el: '#app',
data: {
//color需要传递一个字符串,所以要加引号
color: 'red',
//size传递纯数字,不需要加引号
size: 58
}
});
</script>
v-bind的数组语法
修改class属性
使用v-bind添加类名时,传递一个数组可以同时添加多个类名
<div id="app">
//直接传递一个数组,为div添加red类和asd类
<div :class="['red','asd']"></div>
//传递一个数组,不加引号则会则从data中获取类名
<div :class="[color,asd]"></div>
//数组元素较多时,通过methods返回一个数组
<div :class="getClass()"></div>
</div>
<script>
let app = new Vue({
el: '#app',
data: {
color: 'red',
asd: 'asd'
},
methods: {
getClass: function () {
return [this.color, this.asd]
}
}
});
</script>
修改style属性
当v-bind:style
接收的是一个数组时,数组中的样式元素会合并添加到标签的style样式中
<div id="app">
//数组元素不添加引号,则会被解析为变量去data中寻找
<div :style="[style1,style2]">文字</div>
</div>
<script>
let app = new Vue({
el: '#app',
data: {
style1: {
fontSize: '100px'
},
style2: {
backgroundColor: 'red'
}
},
});
</script>
关于v-bind的驼峰命名
v-bind
后的属性名不支持驼峰的,如果必须使用驼峰,则需要转换为小写再用 -
连接
<!-- 错误写法,不支持驼峰 -->
<div v-bind:myFatherMsg="message"></div>
<!-- 正确写法,用-连接 -->
<!-- my-father-msg最终会转换为myFatherMsg -->
<div v-bind:my-father-msg="message"></div>
v-on
在前端开发中,我们需要经常和用于交互
这个时候,我们就必须监听用户发生的时间,比如点击、拖拽、键盘事件等等
在Vue中如何监听事件呢?使用v-on
指令
<div id="app">
<h2>{{num}}</h2>
//触发点击事件时执行num++
<button v-on:click="num++">加</button>
//触发点击事件时执行add()方法
<button v-on:click="add">加</button>
//v-on指令的语法糖写法
<button @click="sub">减</button>
</div>
<script>
let app = new Vue({
el: '#app',
data: {
num: 0
},
methods: {
add() {
this.num++;
},
sub() {
this.num--;
}
}
});
</script>
v-on的参数传递
当通过methods中定义方法,以供@click调用时,需要注意传参规范:
<div id="app">
<!-- 方法不需要传递参数,不加小括号直接调用即可 -->
<button @click="fn1">button1</button>
<!-- 方法需要传递参数,如果传字符串没加引号,则会看作一个变量去data中找;
数字,函数类型不需要加引号,传递函数的同时会直接执行该函数,并不会实际传入到方法中 -->
<button @click="fn2(abc,'abc',abcd(),123)">button2</button>
<!-- 方法需要参数,但是没有传递参数,则会将MouseEvent传递进去 -->
<button @click="fn3">button3</button>
<!-- 方法需要传递MouseEvent对象 -->
<button @click="fn4($event)">button4</button>
</div>
<script>
let app = new Vue({
el: '#app',
data: {
abc: '我是data中的abc变量'
},
methods: {
fn1() {
console.log('我是fn1函数');
},
fn2(variable, str, fn, num) {
console.log(variable, str, fn, num);
},
fn3(param) {
console.log(param);
},
fn4(event) {
console.log(event);
},
abcd() {
console.log("我是abcd()方法");
}
}
});
</script>
v-on的修饰符
为v-on
添加修饰符,可以让我们在使用v-on
调用方法的同时执行一些其他的操作
- stop:停止事件冒泡
<div @click.stop="fn"></div>
- prevent:阻止元素的默认行为,如submit提交表单
<input type="submit" @click.prevent="mySubmit">
- keyCode/keyAlias:只当事件是从特定键触发时才触发回调
<!-- 只有按下enter键的时候才调用方法 -->
<input type="text" @click.enter="fn">
- native:监听组件根元素的原生事件
<cpn @click.native="cpnClick"></cpn>
- once:只触发一次回调
<button @click.once="fn"></button>
v-if、v-else-if、v-else
用法和常规的if else语句相同,只是这里要写到标签上;
如果这三种判定语句中的某一条不被执行,则对应的元素以及其子元素不会渲染,也就是根本没有不会有对应的标签出现在DOM中
v-if
指令用于条件性地渲染一块内容。这块内容只会在指令的表达式返回 truthy 值的时候被渲染:
<h1 v-if="awesome">Vue is awesome!</h1>
你可以使用 v-else
指令来表示 v-if
的else 块,v-else
元素必须紧跟在带 v-if
或者 v-else-if
的元素的后面,否则它将不会被识别
<div v-if="Math.random() > 0.5">
Now you see me
</div>
<div v-else>
Now you don't
</div>
v-else-if
,顾名思义,充当 v-if
的else-if 块,可以连续使用,类似于 v-else
,v-else-if
也必须紧跟在带 v-if
或者 v-else-if
的元素之后
<div v-if="type === 'A'">
A
</div>
<div v-else-if="type === 'B'">
B
</div>
<div v-else-if="type === 'C'">
C
</div>
<div v-else>
Not A/B/C
</div>
v-show
v-show
的用法和v-if
非常相似,也用于决定一个元素是否渲染:
v-if
如果不显示,则会连带着DOM框架一起消失;而v-show
只是切换元素的display
属性为none
v-if和v-show都可以决定一个元素是否渲染,那么开发中我们如何选择呢?
- 当需要在显示与隐藏之间切片很频繁时,使用
v-show
- 当只有一次切换时,通过使用
v-if
<div id="app" v-show="isShow">
我是div
</div>
<script>
let app = new Vue({
el: '#app',
data: {
isShow: true
}
});
</script>
v-for
遍历数组
我们可以用 v-for
指令基于一个数组来渲染一个列表。v-for
指令需要使用 item in items
形式的特殊语法,其中 items
是源数据数组,而 item
则是被迭代的数组元素的别名
我们可以使用mustache语法来打印item,但要写在具有v-for的标签内部
<div id="app">
<ul>
<li v-for="item in arr"> {{item}} </li>
</ul>
</div>
<script>
let app = new Vue({
el: '#app',
data: {
arr: ['apple', 'banana', 'orange']
}
});
</script>
v-for
还支持一个可选的第二个参数,即当前项的索引,也可以使用mustache语法打印出来
<li v-for="(item,index) in arr"> {{item}} </li>
你也可以用 of
替代 in
作为分隔符,因为它更接近 JavaScript 迭代器的语法
遍历对象
<div id="app">
<ul>
<!-- 只提供一个参数,默认输出对象里的属性的value值 -->
<li v-for="value in obj">{{value}}</li>
<!-- 提供第二个属性,输出对象里的属性的key值 -->
<li v-for="(value,key) in obj">{{value}}:{{key}}</li>
<!-- 提供第三个属性,输出对象里的属性的索引值,从0开始 -->
<li v-for="(value,key,index) in obj">{{index}}:{{value}}:{{key}}</li>
</ul>
</div>
<script>
let app = new Vue({
el: '#app',
data: {
obj: {
name: 'wsc',
age: 18,
height: 160
}
}
});
</script>
key:管理复用元素
Vue 会尽可能高效地渲染元素,通常会复用已有元素而不是从头开始渲染。这么做除了使 Vue 变得非常快之外,还有其它一些好处。例如,如果你允许用户在不同的登录方式之间切换:
<template v-if="loginType === 'username'">
<label>Username</label>
<input placeholder="Enter your username">
</template>
<template v-else>
<label>Email</label>
<input placeholder="Enter your email address">
</template>
那么在上面的代码中切换 loginType
将不会清除用户已经输入的内容。因为两个模板使用了相同的元素,<input>
不会被替换掉——仅仅是替换了它的 placeholder
这样也不总是符合实际需求,所以 Vue 为你提供了一种方式来表达“这两个元素是完全独立的,不要复用它们”。只需添加一个具有唯一值的 key
attribute即可:
<template v-if="loginType === 'username'">
<label>Username</label>
<input placeholder="Enter your username" key="username-input">
</template>
<template v-else>
<label>Email</label>
<input placeholder="Enter your email address" key="email-input">
</template>
现在,每次切换时,输入框都将被重新渲染
注意,<label>
元素仍然会被高效地复用,因为它们没有添加 key
attribute
v-for中的key
当某一层有很多相同的节点时,也就是列表节点时,我们希望插入一个新的节点,可以在B和C之间加一个F,Diff算法默认执行起来是这样的:
即把C更新成F,D更新成C,E更新成D,最后再插入E,是不是很没有效率?
所以我们需要使用key
来给每个节点做一个唯一标识,Diff算法就可以正确的识别此节点并找到正确的位置区插入新的节点
所以一句话,key的作用主要是为了高效的更新虚拟DOM
<div v-for="item in items" v-bind:key="item.id">
<!-- 内容 -->
</div>
v-model
你可以用 v-model
指令在表单 <input>
、<textarea>
及 <select>
元素上创建双向数据绑定
- text 和 textarea 元素使用
value
property 和input
事件 - checkbox 和 radio 使用
checked
property 和change
事件 - select 字段将
value
作为 prop 并将change
作为事件
type = “text” 的input双向绑定
案例的解析:
当我们在输入框输入内容时,因为input
中的v-model
绑定了data中的message
,所以会实时将输入的内容传递给message
,message
发生改变。当message
发生改变时,因为上面我们使用Mustache语法,将message
的值插入到DOM中,所以DOM会发生响应的改变,所以通过v-model
实现了双向的绑定
原理:v-model
本质上包含两个操作:
<input type="text" v-model="message">
等同于
<input type="text" v-bind:value="message" v-on:input="message = $event.target.value">
type = “radio” 的input双向绑定
将单选框的value与data中的sex进行绑定:
如果添加了同样的v-model
,则不需要再添加name
来实现互斥
<div id="app">
<input type="radio" value="man" v-model="sex">男
<input type="radio" value="girl" v-model="sex">女
{{sex}}
</div>
<script>
let app = new Vue({
el: '#app',
data: {
sex: ''
},
});
</script>
type = “checkbox” 的input双向绑定
当只有一个复选框时,v-model
对应的是一个boolean值,此时input的value
并不影响v-model
的值,而选中状态决定它为true or false`
<div id="app">
<input type="checkbox" v-model="isAgree">是否同意协议
{{isAgree}}
</div>
<script>
let app = new Vue({
el: '#app',
data: {
isAgree: ''
}
});
</script>
当有多个复选框时,v-model
对应的是一个数组,会根据你的选项实时修改数组中的内容(添加或删除value)
<div id="app">
选择你最爱的水果
<input type="checkbox" value="apple" v-model="fruits">苹果
<input type="checkbox" value="banana" v-model="fruits">香蕉
<input type="checkbox" value="strawberry" v-model="fruits">草莓
<input type="checkbox" value="orange" v-model="fruits">橙子
{{fruits}}
</div>
<script>
let app = new Vue({
el: '#app',
data: {
fruits: []
}
});
</script>
select 的双向绑定
选择一个
将 value 与 data 中的 fruits 双向绑定
<div id="app">
<select v-model="fruits">
<option value="strawberry">草莓</option>
<option value="banana">香蕉</option>
<option value="apple">苹果</option>
<option value="peach">桃子</option>
</select>
<h2>你选择的水果是{{fruits}}</h2>
</div>
<script>
let app = new Vue({
el: '#app',
data: {
fruits: ''
}
});
</script>
选择多个
将 value 与 data 中的 furits[]
数组进行双向绑定,水果的是否选中状态将决定是否向数组中添加该水果的 value 值
<div id="app">
<select v-model="fruits" multiple>
<option value="strawberry">草莓</option>
<option value="banana">香蕉</option>
<option value="apple">苹果</option>
<option value="peach">桃子</option>
</select>
<h2>你选择的水果是{{fruits}}</h2>
</div>
<script>
let app = new Vue({
el: '#app',
data: {
fruits: []
}
});
</script>
值绑定
就是动态的给表单 value 赋值而已:
我们前面的value中的值,可以回头去看一下,都是在定义input的时候直接给定的,但是真实开发中,这些input的值可能是从网络获取或定义在data中的
所以我们可以结合v-for
并通过v-bind:value
动态的给value绑定值
<div id="app">
<label v-for="item in totalFruits" :for="item">
<input type="checkbox" :id="item" v-model="fruits" :value="item">{{item}}
</label>
<h2>你选择的水果是{{fruits}}</h2>
</div>
<script>
let app = new Vue({
el: '#app',
data: {
fruits: [],
totalFruits: ['apple', 'banana', 'strawberry', 'peach', 'litchi', 'mango']
}
});
</script>
修饰符
.lazy
在默认情况下,v-model
在每次 input 事件触发后将输入框的值与data数据进行同步。你可以添加 lazy
修饰符,从而转为在按下回车或者失去焦点时进行同步
<!-- 在失去焦点或按下回车时而非“input”时更新msg数据 -->
<input v-model.lazy="msg">
.number
无论我们在输入框内输入什么类型,通过双向绑定为data中的变量赋值时,都会转换成string类型进行赋值,即使你的input 的 type 为 number也是如此
如果想自动将用户的输入值转为数值类型,可以给 v-model
添加 .number
修饰符:
<input v-model.number="age" type="number">
.trim
如果要自动过滤用户输入的首尾空白字符,可以给 v-model
添加 trim
修饰符:
<input v-model.trim="msg">
数组更新监测
数组中的响应式方法
Vue 将被侦听的数组的变更方法进行了包裹,所以它们也将会触发视图更新。这些被包裹过的方法包括:
push()
pop()
shift()
unshift()
splice()
sort()
reverse()
数组中的非响应式方法
filter()
、concat()
和 slice()
。它们不会变更原始数组,而总是返回一个新数组,因此不会更新页面视图。当使用非变更方法时,可以用新数组替换旧数组:
example1.items = example1.items.filter(function (item) {
return item.message.match(/Foo/)
})
数组中的非响应式行为
通过索引值修改数组中的元素,如app.arr[0]='newValue';
虽然修改了arr的数组元素,但是不会更新视图,因此需要使用splice()
方法,或者调用Vue.set(要修改的对象,索引值,新值);
方法
计算属性computed
我们知道,在模板中可以直接通过插值语法显示一些data中的数据,但是在某些情况,我们可能需要对数据进行一些转化后再显示,或者需要将多个数据结合起来进行显示
比如我们有firstName和lastName两个变量,我们需要显示完整的名称,但是如果多个地方都需要显示完整的名称,我们就需要写多个{{firstName}} {{lastName}}
这时我们可以使用计算属性computed
<div id="app">
//拼接显得很不简介
<h2>{{firstName}}{{lastName}}</h2>
//使用计算属性中的变量,不需要加括号()
<h2>{{fullName}}</h2>
</div>
<script>
let app = new Vue({
el: '#app',
data: {
firstName: 'Hermione',
lastName: 'Granger'
},
computed: {
fullName: function () {
return this.firstName + this.lastName;
}
}
});
</script>
计算属性的复杂操作
计算属性中也可以进行一些更加复杂的操作,比如下面的例子:
<div id="app">
<h2>书的总价格为:{{totalPrice}}</h2>
</div>
<script>
let app = new Vue({
el: '#app',
data: {
books: [{
name: 'C语言从入门到入土',
price: 66
}, {
name: 'MySQL从删库到跑路',
price: 55
}]
},
computed: {
totalPrice: function () {
let res = 0;
for (let i in this.books) {
res += this.books[i].price;
}
return res;
}
}
});
</script>
计算属性的setter and getter
Q: 为什么我们调用计算属性里的函数却不用加括号呢?
A: 因为计算属性直接写函数其实是省略写法,本质上它并不是一个函数,而是一个包含了set()
方法和get()
方法的对象;又因为set()
方法基本不会使用,所以一般不写set()
方法,只写get()
方法,从而进一步又省略掉了get()
方法,直接调用该对象就会默认调用里面的get()
方法;只是调用一个对象,自然不用加括号()
<div id="app">
<h2>{{fullName}}</h2>
</div>
<script>
let app = new Vue({
el: '#app',
data: {
firstName: 'Hermione',
lastName: 'Granger'
},
computed: {
// 省略写法:
// fullName: function () {
// return this.firstName + this.lastName;
// },
// 完整写法:
fullName: {
// 直接修改fullName的值,会默认调用set()方法,并将新值作为参数传递进来
set: function (newValue) {
let names = newValue.split(' ');
this.firstName = names[0];
this.lastName = names[1];
},
get: function () {
return this.firstName + this.lastName;
}
}
}
});
</script>
计算属性和methods的对比
观察以下代码:
<body>
<h2>{{fullName}}</h2>
<h2>{{fullName}}</h2>
<h2>{{fullName}}</h2>
<h2>{{getFullName()}}</h2>
<h2>{{getFullName()}}</h2>
<h2>{{getFullName()}}</h2>
</body>
<script>
computed: {
fullName: function () {
return this.firstName + this.lastName;
}
},
methods: {
getFullName: function () {
return this.firstName + this.lastName;
}
}
</script>
我们用计算属性的fullName
函数和methods的getFullName
函数实现了三次同样的功能,都是通过计算后返回fullName,但是这两者有很大的区别:
在程序运行过程中,计算属性的fullName
函数只调用了一次,而methods的getFullName
函数调用了三次
原因:计算属性computed在程序运行中会产生一层缓存,如果在计算过程中里面的值没有发生变化,则会直接从缓存中返回上一次的结果
所以我们要尽量使用计算属性来实现一些计算的功能,这样效率更高,并且两者的效率差距随着计算内容的复杂度的提高而提高
过滤器filter
Vue.js 允许你自定义过滤器,可被用于一些常见的文本格式化。过滤器可以用在两个地方:双花括号插值和 v-bind 表达式。过滤器应该被添加在 JavaScript 表达式的尾部,由“管道”符号指示:
<!-- 在双花括号中 -->
{{ message | capitalize }}
<!-- 在 `v-bind` 中 -->
<div v-bind:id="rawId | formatId"></div>
你可以在一个组件的选项中定义本地的过滤器:
filters: {
//接收一个参数price,返回带符号并保留两位小数的price
getPrice(price) {
return '$' + price.toFixed(2);
}
}
//假如price值为20,则该差值表达式会自动输出$20.00
{{ price | getPrice }}
组件化
如果将一个复杂的问题,拆分成很多个可以处理的小问题,再将其放在整体当中,你会发现大的问题也会迎刃而解
组件化也是类似的思想:
如果我们将一个页面中所有的处理逻辑全部放在一起,处理起来就会变得非常复杂,而且不利于后续的管理以及扩展
但如果,我们将一个页面拆分成一个个小的功能块,每个功能块完成属于自己这部分独立的功能,那么之后整个页面的管理和维护就变得非常容易了
Vue组件化思想
组件化是Vue.js中的重要思想,它提供了一种抽象,让我们可以开发出一个个独立可复用的小组件来构造我们的应用
任何的应用都会被抽象成一颗组件树
注册组件的基本步骤
注册组件的代码要写在实例化Vue代码的上面
- 创建组件构造器对象:
Vue.extend()
let cpn = Vue.extend({
template: `
<div>
<h1>我是h1标题</h1>
<h2>我是h2标题</h2>
<p>我是p标签</p>
</div>
`
})
- 注册组件:
Vue.component()
Vue.component('my_cpn', cpn)
- 在Vue实例的作用范围内使用组件
<my_cpn></my_cpn>
全局组件和局部组件
当我们通过调用 Vue.component()
注册组件时,该是全局的,这意味着该组件可以在任意的Vue示例下使用
如果我们注册的组件是挂载在某个实例中, 那么就是一个局部组件:
// 1. 创建组件构造器对象
const cpn = Vue.extend({
template: `
<div>
<h1>我是h1标题</h1>
<p>我是p标签</p>
</div>
`
})
// 2. 在Vue实例中的components对象中挂载构造器
let app = new Vue({
el: '#app',
components: {
'my_cpn': cpn
}
});
父组件和子组件
在前面我们看到了组件树:组件和组件之间存在层级关系
而其中一种非常重要的关系就是父子组件的关系
<div id="app">
// 调用父组件,父组件中调用了子组件,会一起输出
<my_cpn2></my_cpn2>
// 错误,没有在Vue中挂载子组件,无法单独使用子组件,只有父组件才认识该标签
<my_cpn1></my_cpn1>
</div>
<script>
// 1. 创建第一个组件构造器(子组件)
const cpn1 = Vue.extend({
template: `
<div>
<h1>我是子组件</h1>
<p>我是子组件的p标签</p>
</div>
`
})
// 2. 创建第二个组件构造器(父组件)
const cpn2 = Vue.extend({
template: `
<div>
<h1>我是父组件</h1>
// 父组件调用子组件
<my_cpn1></my_cpn1>
</div>
`,
// 在父组件中挂载子组件
components: {
'my_cpn1': cpn1
}
})
let app = new Vue({
el: '#app',
components: {
// 在Vue实例中只声明了父组件,所以只能使用父组件;子组件需由父组件调用
'my_cpn2': cpn2
}
});
</script>
注册组件的语法糖写法
不需要再使用extend()
方法,该方法直接作为对象传递到了component()
中
注册全局组件:
Vue.component('my_cpn', {
template: `
<div>
<h1>我是子组件</h1>
<p>我是子组件的p标签</p>
</div>
`
})
注册局部组件:
let app = new Vue({
el: '#app',
components: {
my_cpn1: {
template: `
<div>
<h1>我是子组件</h1>
<p>我是子组件的p标签</p>
</div>
`
}
}
});
组件模板分离式写法
如果我们能将组件模板中的HTML分离出来写,然后挂载到对应的组件上,必然结构会变得非常清晰Vue提供了两种方案来定义HTML模块内容:
- 使用
<script>
标签
<div id="app">
<my_cpn></my_cpn>
</div>
<!-- 为script标签添加type和id属性,作为组件的模板使用 -->
<script type="text/x-template" id="cpn">
<div>
<h2>我是标题</h2>
<p>我是文字啊啊</p>
</div>
</script>
<script>
let app = new Vue({
el: '#app',
components: {
// 挂载组件
my_cpn: {
template: '#cpn'
}
}
});
</script>
- 使用
<template>
标签
Vue的 template
模板只能有一个根标签,所以我们最好用一个<div>
包裹起来;如果有多个根标签,那么只会执行第一个根标签,执行到第二个根标签时会报错
<div id="app">
<my_cpn></my_cpn>
</div>
<!-- 在template标签中书写模板,注意要添加id属性 -->
<template id="cpn">
<div>
<h2>我是标题文字</h2>
<p>我是段落段落段落~</p>
</div>
</template>
<script>
let app = new Vue({
el: '#app',
components: {
// 挂载组件
my_cpn: {
template: '#cpn'
}
}
});
</script>
组件访问Vue实例数据
组件可以访问Vue实例的data数据吗?我们测试一下:
<template id="cpn">
<div>
<!-- 错误,组件不能直接访问Vue实例的data -->
{{msg}}
<h2>我是标题文字</h2>
<p>我是段落段落段落~</p>
</div>
</template>
我们发现不能访问,而且即使可以访问,如果将所有的数据都放在Vue实例中,Vue实例就会变的非常臃肿
结论:Vue组件应该有自己保存数据的地方
组件数据的存放
注册组件对象时可以添加data属性(也可以有methods等属性)
只是这个data属性必须是一个函数,而且这个函数返回一个对象,对象内部保存着数据
Vue.component('my_cpn', {
template: '#cpn',
data() {
return {
msg: 'abc'
}
}
})
为什么组件对象中的data必须是函数?
假如我们在很多地方都复用了该组件,如果data是函数形式,则每个组件之间的data数据不会互相影响,每个组件都有属于自己独有的data
因为函数在使用时会开辟一块新的内存地址,虽然函数名都是data,但是在调用时会指向不同的内存空间
如果就是想都指向同一块内存,则可以在组件注册的外面创建一个新对象,然后由组件内data中的return返回该对象,即可实现所有组件共用该对象数据;注意该对象的命名应与该对象被组件使用时的名字一致
父子组件的通信
子组件是不能引用父组件或者Vue实例的数据的,但是在开发中往往一些数据确实需要从上层传递到下层:
比如在一个页面中,我们的大组件从服务器请求到了很多的数据,其中一部分数据,并非是我们整个页面的大组件来展示的,而是需要下面的子组件进行展示
这个时候,并不会让子组件再次发送一个网络请求,而是直接让大组件(父组件)将数据传递给小组件(子组件)
如何进行父子组件间的通信呢?
- 通过props向子组件传递数据
- 通过事件向父组件发送消息
父传子 —— props
在组件中,使用选项props
来声明需要从父级接收到的数据,并通过v-bind
实现传值操作
props
的值有两种方式:
方式一:字符串数组
数组中的字符串就是传递时的名称
<div id="app">
<!-- 添加v-bind属性将父组件数据赋值给子组件数据 -->
<cpn v-bind:c_movies="f_movies"></cpn>
</div>
<template id="cpn">
<div>
{{c_movies}}
</div>
</template>
<script>
const cpn = {
template: '#cpn',
// props可以使用数组的形式,里面的元素并不是字符串,当做变量看待就行
props: ['c_movies']
}
let app = new Vue({
el: '#app',
data: {
f_movies: ['海王', '海贼王', '海尔兄弟']
},
components: {
// 对象增强写法
cpn
}
});
</script>
方式二:对象 —— 数据验证
props对象可以设置传递时的类型,也可以实现设置默认值、自定义数据验证、是否为必传数据等操作
对象形式支持验证这些类型:
String / Number / Boolean / Array / Object / Date / Function / Symbol
当我们有自定义构造函数时,验证也支持自定义的类型
<div id="app">
<cpn v-bind:c_movies="f_movies" :c_msg="f_msg"></cpn>
</div>
<template id="cpn">
<div>
{{c_movies}}
{{c_msg}}
</div>
</template>
<script>
const cpn = {
template: '#cpn',
props: {
// 1. 类型限制,只能传递Array类型;如果为null,则支持任何类型
c_movies: Array,
// 2. 提供一些默认值,如果父组件没有传值时则显示默认值;以及是否必须传的值
c_msg: {
type: String,
default: 'abcd',
required: true
},
// 3. 如果限制传递类型是对象或数组时,则其默认值必须是函数并返回一个对象/数组
c_fruits: {
type: Array,
default () {
return []
}
},
// 其他可能的情况:
// 1. 验证多个可能的类型
propA:[String,Number],
// 2. 自定义验证函数
propB: {
validator: function(value) {
// 这个值必须匹配下列字符串中的一个
return ['success','warning','danger'].indexof(value) !== -1
}
}
}
}
let app = new Vue({
el: '#app',
data: {
f_movies: ['海王', '海贼王', '海尔兄弟'],
f_msg: '我是父元素的msg'
},
components: {
cpn
}
});
</script>
子传父 —— $emit( )
在Tab栏切换案例中:目录栏是一个子组件,内容栏也是一个子组件,假设整个页面是一个父组件。当我们点击目录时,需要切换内容栏的数据,此时子组件需要向父组件发送数据来告知哪一个子组件被点击了,再由父组件去服务器请求数据,最后交付给内容栏去渲染
当子组件需要向父组件传递数据时,就要用到自定义事件
- 在子组件中,通过
$emit()
来触发事件 - 在父组件中,通过
v-on
来监听子组件事件
自定义事件不可以使用驼峰式命名,未来使用脚手架开发的时候可以使用
<!-- 父组件模板 -->
<div id="app">
<!-- 3. 父组件监听itemClick事件,触发时执行cpnClick方法;如果不传递参数,默认传递来自子组件执行$emit()方法时的item参数 -->
<cpn @itemclick="cpnClick"></cpn>
</div>
<!-- 子组件模板 -->
<template id="cpn">
<div>
<!-- 1. 子元素被点击时,执行btnClick()方法,并传递item参数 -->
<button v-for="item in catagories" @click="btnClick(item)">{{item.name}}</button>
</div>
</template>
<script>
const cpn = {
template: '#cpn',
data() {
return {
catagories: [{
id: 'aaa',
name: '热门推荐'
},
{
id: 'bbb',
name: '手机数码'
},
{
id: 'ccc',
name: '家用电器'
},
{
id: 'ddd',
name: '电脑办公'
},
]
}
},
methods: {
btnClick(item) {
// 2. 子组件通过$emit()触发itemClick事件
this.$emit('itemclick', item)
}
}
}
let app = new Vue({
el: '#app',
// 父组件挂载子组件
components: {
cpn
},
methods: {
// 4. 父组件拿到子组件
cpnClick(item) {
console.log(item);
}
}
});
</script>
组件小案例
需求:实现子组件内的<input>
框与父组件的data数据双向绑定
思路:子组件的input框先与子组件的data数据双向绑定,当输入数字时触发input事件,执行$emit将input框的值传给父组件,父组件收到值后修改data数据,导致子组件props实时修改(暂未实现)
<div id="app">
<cpn :c_num1="f_num1" :c_num2="f_num2" @num1change="num1Change" @num2change="num2Change"></cpn>
</div>
<template id="cpn">
<div>
<h2>c_num1: {{c_num1}}</h2>
<h2>c_data_num1:{{c_data_num1}}</h2>
<!-- 官方不推荐直接与props的值绑定,可以通过与data获取prop的值,再与data绑定 -->
<!-- <input type="text" v-model="c_num1"> -->
<input type="text" v-model="c_data_num1" @input="num1Input">
<h2>c_num2: {{c_num2}}</h2>
<h2>c_data_num2:{{c_data_num2}}</h2>
<input type="text" v-model="c_data_num2" @input="num2Input">
</div>
</template>
<script>
let app = new Vue({
el: '#app',
data: {
f_num1: 1,
f_num2: 2
},
components: {
cpn: {
template: '#cpn',
props: {
c_num1: {
type: Number,
},
c_num2: {
type: Number
}
},
data() {
return {
c_data_num1: this.c_num1,
c_data_num2: this.c_num2,
}
},
methods: {
num1Input(event) {
this.$emit('num1change', event.target.value)
},
num2Input(event) {
this.$emit('num2change', event.target.value)
}
}
}
},
methods: {
num1Change(value) {
this.f_num1 = parseInt(value);
},
num2Change(value) {
this.f_num2 = parseInt(value);
}
}
});
</script>
父子组件的相互访问
有时候我们不想只是单单的传递数据,而是想直接拿到父组件/子组件这个对象,调用该对象的方法
父访问子
$children
父组件的方法可以使用this.$children
来获得子组件对象数组,从而使用子组件的方法或获取子组件的数据
但因为获得的是一个数组,如果子组件的数量发生改变,下一次使用$children
的时候就要修改下标值,因此不推荐使用
$refs
想要使用 $refs
,则需要先为组件添加ref="value"
属性,然后父元素通过this.$refs.value
就可以访问到该子组件了
<!-- 1. 为组件添加ref属性 -->
<cpn ref="header"></cpn>
<script>
// 2. 父组件访问该子组件的name数据
console.log(this.$refs.header.name);
</script>
子访问父
一个组件可能在多个页面使用,因此其父级可能是不一样的,所以我们一般很少用子组件去访问父组件
$parent
子组件的方法可以使用this.$parent
获得父组件对象,从而调用父组件的属性和方法
$root
$root
和 $parent
使用方法一致,但是$root
将直接访问到顶层父组件,即Vue实例
Vue事件总线(EventBus)使用详细介绍
前言
vue组件非常常见的有父子组件通信,兄弟组件通信。而父子组件通信就很简单,父组件会通过 props
向下传数据给子组件,当子组件有事情要告诉父组件时会通过 $emit
事件告诉父组件。今天就来说说如果两个页面没有任何引入和被引入关系,该如何通信了?
如果咱们的应用程序不需要类似Vuex这样的库来处理组件之间的数据通信,就可以考虑Vue中的 事件总线
,即 **EventBus
**来通信。
EventBus的简介
EventBus
又称为事件总线。在Vue中可以使用 EventBus
来作为沟通桥梁的概念,就像是所有组件共用相同的事件中心,可以向该中心注册发送事件或接收事件,所以组件都可以上下平行地通知其他组件,但也就是太方便所以若使用不慎,就会造成难以维护的“灾难”,因此才需要更完善的Vuex作为状态管理中心,将通知的概念上升到共享状态层次。
如何使用EventBus
一、初始化
首先需要创建事件总线并将其导出,以便其它模块可以使用或者监听它。我们可以通过两种方式来处理。先来看第一种,新创建一个 .js 文件,比如 event-bus.js
// event-bus.js
import Vue from 'vue'
export const EventBus = new Vue()
实质上EventBus
是一个不具备 DOM
的组件,它具有的仅仅只是它实例方法而已,因此它非常的轻便。
另外一种方式,可以直接在项目中的 main.js
初始化 EventBus
:
// main.js
Vue.prototype.$EventBus = new Vue()
注意,这种方式初始化的
EventBus
是一个全局的事件总线
。稍后再来聊一聊全局的事件总线。
现在我们已经创建了 EventBus
,接下来你需要做到的就是在你的组件中加载它,并且调用同一个方法,就如你在父子组件中互相传递消息一样。
二、发送事件
假设你有两个Vue页面需要通信: A 和 B ,A页面 在按钮上面绑定了点击事件,发送一则消息,想=通知 B页面。
<!-- A.vue -->
<template>
<button @click="sendMsg()">-</button>
</template>
<script>
import { EventBus } from "../event-bus.js";
export default {
methods:
sendMsg() {
EventBus.$emit("aMsg", '来自A页面的消息');
}
}
};
</script>
接下来,我们需要在 B页面 中接收这则消息。
三、接收事件
<!-- IncrementCount.vue -->
<template>
<p>{{msg}}</p>
</template>
<script>
import {
EventBus
} from "../event-bus.js";
export default {
data(){
return {
msg: ''
}
},
mounted() {
EventBus.$on("aMsg", (msg) => {
// A发送来的消息
this.msg = msg;
});
}
};
</script>
同理我们也可以在 B页面 向 A页面 发送消息。这里主要用到的两个方法:
// 发送消息
EventBus.$emit(channel: string, callback(payload1,…))
// 监听接收消息
EventBus.$on(channel: string, callback(payload1,…))
前面提到过,如果使用不善,EventBus
会是一种灾难,到底是什么样的“灾难”
了?大家都知道vue是单页应用,如果你在某一个页面刷新了之后,与之相关的EventBus
会被移除,这样就导致业务走不下去。还要就是如果业务有反复操作的页面,EventBus
在监听的时候就会触发很多次,也是一个非常大的隐患。这时候我们就需要好好处理 EventBus
在项目中的关系。通常会用到,在vue页面销毁时,同时移除EventBus
事件监听。
移除事件监听者
如果想移除事件的监听,可以像下面这样操作:
import {
eventBus
} from './event-bus.js'
EventBus.$off('aMsg', {})
你也可以使用 EventBus.$off('aMsg')
来移除应用内所有对此某个事件的监听。或者直接调用 EventBus.$off()
来移除所有事件频道,不需要添加任何参数 。
上面就是 EventBus
的使用方法,是不是很简单。上面的示例中我们也看到了,每次使用 EventBus
时都需要在各组件中引入 event-bus.js
。事实上,我们还可以通过别的方式,让事情变得简单一些。那就是创建一个全局的 EventBus
。接下来的示例向大家演示如何在Vue项目中创建一个全局的 EventBus
。
全局EventBus
它的工作原理是发布/订阅方法,通常称为 Pub/Sub
。
创建全局EventBus
var EventBus = new Vue();
Object.defineProperties(Vue.prototype, {
$bus: {
get: function () {
return EventBus
}
}
})
在这个特定的总线中使用两个方法 $on
和 $emit
。一个用于创建发出的事件,它就是 $emit
;另一个用于订阅 $on
:
var EventBus = new Vue();
this.$bus.$emit('nameOfEvent', { ... pass some event data ...});
this.$bus.$on('nameOfEvent',($event) => {
// ...
})
然后我们可以在某个Vue页面使用 this.$bus.$emit("sendMsg", '我是web秀');
,另一个Vue页面使用
this.$bus.$on('updateMessage', function(value) {
console.log(value); // 我是web秀
})
同时也可以使用this.$bus.$off('sendMsg')
来移除事件监听。
总结
本文主要通过简单的实例学习了Vue中有关于 EventBus
相关的知识点。主要涉及了 EventBus
如何实例化,又是如何通过 $emit
发送频道信号,又是如何通过 $on
来接收频道信号。最后简单介绍了如何创建全局的 EventBus
。从实例中我们可以了解到, EventBus
可以较好的实现兄弟组件之间的数据通讯。
混入 mixin
混入 (mixin) 提供了一种非常灵活的方式,来分发 Vue 组件中的可复用功能。一个混入对象可以包含任意组件选项(methods、data、mounted …)。当组件使用混入对象时,所有混入对象的选项将被“混合”进入该组件本身的选项。
注意:methods 中的某一个方法的混入必须以其完整的方法为单位,而不能只混入某个方法中的某一段代码;而生命周期函数如 mounted 可以混入其中的某一段代码
使用步骤:
-
在 common 文件夹中新建 mixin.js 文件,在其中创建一个混入对象并抛出:
export const myMixin = { methods: { hello: function () { console.log('我是混入对象中的methods') } } }
-
在需要使用混入对象的组件中导入 mixin.js
import myMixix from 'common/mixin.js'
-
在导入了混入对象的组件中注册该对象
export default { ... mixins: [myMixin] }
-
使用混入对象中的方法
export default { created() { this.hello(); // => "我是混入对象中的methods" } }
slot 插槽
插槽一般在组件中使用,组件的插槽是为了让我们封装的组件更加具有扩展性
像移动开发中,几乎每个页面都有导航栏。导航栏我们必然会封装成一个插件,但是每个页面的导航肯定不是一模一样的,这就要使用插槽来实现了
slot 基本使用
虽然都是导航栏,但每个导航栏里的内容是不一样的,那么如何去封装这类的组件呢?
最好的封装方式就是将共性抽取到组件中,将不同预留为插槽:一旦我们预留了插槽,就可以让使用者根据自己的需求,决定插槽中插入什么内容
使用步骤:
- 在组件内创建
<slot></slot>
标签 - 在
<cpn></cpn>
标签内书写要替换的标签 <slot>
内的全部标签都会自动被<cpn>
内的标签替换
<slot>
标签内可以书写标签作为默认值,若<cpn>
内无标签传入,则自动显示默认值;若<cpn>
内传入了标签,则显示<cpn>
内的标签
<div id="app">
<!-- 传入 <button> 替换 <slot> -->
<cpn><button>按钮</button></cpn>
<!-- 传入 <span> 替换 <slot> -->
<cpn><span>span</span></cpn>
<!-- 会将<cpn>内所有标签替换到<slot>内 -->
<cpn>
<div>我是div</div>
<h2>我是h2</h2>
<p>我是p</p>
</cpn>
</div>
<template id="cpn">
<div>
<!-- 组件共有元素 -->
<h2>我是组件</h2>
<!-- 第一种:预留<slot>标签,等待替换 -->
<slot></slot>
<!-- 第二种:<slot>内书写默认标签,如果<cpn>内部不传标签,则该处自动显示默认值;-->
<slot><button>按钮</button></slot>
</div>
</template>
slot 具名插槽的使用
当组件的功能复杂时,组件的插槽可能并非是一个。比如我们封装一个导航栏的子组件,可能就需要三个插槽,分别代表左边、中间、右边,此时想对具体某一个插槽进行替换时,就需要使用具名插槽:为<slot>
添加name
属性,并为<cpn>
添加slot
属性
注意:只有slot属性能与name配对的元素才可以替换组件中的<slot>
标签
<!-- 为<cpn>内的元素添加slot属性,需与被替换<slot>的name属性一致 -->
<cpn><button slot="back">返回</button></cpn>
<!-- 为<slot>标签添加name属性 -->
<slot name="back">back</slot>
编译作用域
无论是什么标签(包括子组件标签),只要是父组件模板的元素都会在父级作用域内编译,访问的是父组件对象中的变量;子组件模板的所有元素都会在子级作用域内编译,访问的是子组件对象中的变量
<div id="app">
<!-- 该标签位于Vue组件中,所以会访问Vue实例data中的isShow -->
<cpn v-show="isShow"></cpn>
</div>
<template id="cpn">
<div>
<!-- 该标签位于子组件中,所以会访问子组件对象中的isShow -->
<span v-show="isShow">猜猜我在哪</span>
<button>我是没有感情的按钮</button>
</div>
</template>
作用域插槽
父组件替换插槽的标签,但是替换内容由子组件来提供
就是子组件通过<slot>
插槽传递数据给父组件:由父组件对其进行展示,或添加一些其他的格式
<div id="app">
<cpn>
<!-- 需在外层添加一个含有 slot-scope="slot" 属性的标签 -->
<div slot-scope="slot">
<!-- 通过slot.data接收子组件传入的数据 -->
<p v-for="item in slot.data">{{item}}</p>
</div>
</cpn>
</div>
<template id="cpn">
<div>
<!-- <slot>标签通过v-bind添加自定义属性,属性值为子组件对象的数据 -->
<slot :data="fruits"></slot>
</div>
</template>
<script>
let app = new Vue({
el: '#app',
components: {
cpn: {
template: '#cpn',
data() {
return {
fruits: ['apple', 'banana', 'orange', 'watermelon']
}
}
}
}
});
</script>
模块化开发
引言
我们在开发过程中,可能同时引入多个js文件,而且这些js文件可能由多个人编写,所以可能导致全局变量重名的问题,甚至js引入的顺序也会引发诸多问题
我们可能会想到使用匿名函数(function(){})()
来解决变量作用域的问题,但是这样虽然变量名不会冲突了,但是js文件之间却无法相互访问,代码的可复用性变得极差
这就需要我们的模块化开发来解决
模块化开发的几种方式
使用模块作为出口
步骤:
- 在匿名函数内部,定义一个对象
- 给对象添加各种需要暴露到外面的属性和方法(不需要暴露的直接定义即可)
- 最后将这个对象返回,并且在外面使用一个MoudleA接收
- 在其他js文件中调用MoudleA对象即可
// main.js:
let ModuleA = (function () {
let obj = {}
obj.flag = true
return obj;
})()
// index.js:
(function () {
// 调用了main.js传出的ModuleA对象
if (ModuleA.flag) {
console.log('我执行了main.js中的代码!');
}
})()
CommonJS
// CommonJS的导出:
module.exports = {
flag: true,
test(a, b) {
return a + b
},
demo(a, b) {
return a * b
}
}
// CommonJS的导入
let {test, demo, flag} = require('moduleA');
// 上面是增强写法,等同于:
let _mA = require('moduleA');
let test = _mA.test;
let demo = _mA.demo;
let flag = _mA.flag;
export / import
使用步骤:
- 引入script文件时添加
type = "module"
属性 - 使用
export{value};
导出 - 使用
import {value} from "./xx.js";
导入,注意哪怕是同级路径前也要加上./
如果使用了export或import,则不能直接在本地运行,需要在localhost或者使用live server插件运行
html代码:
<!-- 添加type属性后,其他js文件不能直接访问 -->
<script src="main.js" type="module"></script>
<script src="index.js" type="module"></script>
js代码:
// main.js:导出
let num = 10;
// 1. 导出已定义的变量
export {
num
}
// 2. 定义变量时直接导出
export let age = 18;
// 3. 导出函数
export function add(a, b) {
return a + b;
}
// 4. 导出类
export class Person {
run() {
console.log('男生女生向前冲!');
}
}
---------------------------------------------
// index.js:导入并使用
import {
num,
age,
add
} from "./main.js";
console.log(num,age,add(10, 20));
const person = new Person();
person.run();
export default
某些情况下,我们导出了一个模块中的某个的功能,我们并不希望为这个功能命名,而是让导入者自己来命名
这个时候就可以使用export default
进行导出,而且在导入的时候不需要添加大括号{ }
注意:export default
在同一个模块中,只能有一个
导出:
// 1. 使用default导出变量时,需要要先定义变量
let num = 10;
export default num
// 2. 使用default导出函数时,不需要起名字
export default function (value) {
console.log(value);
}
// 3. 使用default导出类时,也不需要起名
export default class {
sing() {
console.log('聆听灭绝的死寂');
}
}
导入:
// 1. 变量不用再加大括号,而且可以直接修改为自己的名字
import myNum from './main.js';
console.log(myNum);
// 2. 将导入的函数修改为自己的名字并使用
import print from './main.js';
print('我是print函数')
// 3. 将导入的类修改为自己的类名并使用
import Hero from './main.js';
const karthus = new Hero();
karthus.sing();
export * 全部统一导入
如果我们希望将某个模块导出的所有数据都导入,一个个导入显然有些麻烦
通过*
可以导入模块中所有的export变量
但是我们需要给*
起一个别名,方便后续的使用
// 导出
export const age = 18;
export const num = 20;
//导入全部变量
import * as mainImport from './main.js';
console.log(mainImport.age);
console.log(mainImport.num);
Vue CLI 脚手架
使用Vue.js开发大型应用时,我们需要考虑代码目录结构、项目结构和部署、热加载、代码单元测试等事情
如果每个项目都要手动完成这些工作,那效率是很低的,所以通常我们会使用一些脚手架工具来帮助完成这些事情
CLI(Command-Line Interface), 翻译为命令行界面, 但是俗称脚手架
Vue CLI是一个官方发布 vue.js 项目脚手架
使用 vue-cli 可以快速搭建Vue开发环境以及对应的webpack配置
使用
- 首先确认电脑是否安装了node和webpack
- 安装脚手架:命令行输入
npm install -g @vue/cli
注意:上面安装的是Vue CLI3的版本,如果需要想按照Vue CLI2的方式初始化项目时不可以的,需要拉取2.x模板
- 拉取 2.x 模板:
npm install -g @vue/cli-init
- 进入准备创建项目的目录,在终端根据版本选择输入:
- Vue CLI2初始化项目:
vue init webpack my-project
- Vue CLI3初始化项目:
vue create my-project
根据提示完成项目创建
- 执行
npm run dev
,进入http://localhost:8080查看主页面(必须通过localhost打开,右键打开无效)
创建好的项目结构如下:
npm run build 执行过程
npm run dev 执行过程
Runtime-Compiler和Runtime-only的区别
我们使用脚手架构建项目时,会让我们选择Runtime-Compiler还是Runtime-only,这两者有什么区别呢?
我们先来看看Vue程序的运行过程:
Vue程序运行有五个阶段:template ——ast —— render —— virtue dom —— UI
而 runtime - compiler 便是遵循这五个阶段来执行程序
但是 runtime - only 直接从render开始,render —— vdom —— UI
render( ) 函数
render()
函数默认接收一个createElement参数,该参数内可以传入标签或组件,传入的标签或组件将直接替换到<div id="app"> </div>
处
el:'#app',
render: function (createElement) {
// 1. 传入<h2>标签:<h2 class="box">Hello World</h2>
return createElement('h2', {class: 'box'}, ['Hello World'])
// 2. 在<h2>标签中嵌套<button>标签:
return createElement('h2',['我是h2',createElement('button',['我是按钮'])])
// 3. 传入组件对象
return createElement(cpn)
}
由以上代码得知,render()
函数不仅能创建标签,还可以直接创建组件,都会直接替换html代码中的<div id="app"></div>
标签
如果是runtime - compiler模式下,我们要先将接收到的App组件中的template的编译为ast,然后转换为render函数,再转换为虚拟DOM
new Vue({
el: '#app',
components: {
App
},
template: '<App/>'
})
但如果是runtime - only模式下,vue-template-compiler插件会直接将App组件中的template转换为render函数再导出,之后我们导入的App组件中已经不包含任何template,直接开始将render函数转换为虚拟DOM
new Vue({
el: '#app',
render: h => h(App)
})
Vue CLI 3
vue-cli 3 与 2 版本有很大区别:
- vue-cli 3 是基于 webpack 4 打造,vue-cli 2 还是 webapck 3
- vue-cli 3 的设计原则是“0配置”,移除的配置文件根目录下的,build和config等目录
- vue-cli 3 提供了 vue ui 命令,提供了可视化配置,更加人性化
- 移除了static文件夹,新增了public文件夹,并且index.html移动到public中
使用
- 创建CLI3项目:
vue create my-project
- 运行项目:
npm run serve
CLI3 与 CLI2 的区别
- CLI2的static文件夹被替换成了public文件夹,但实现的作用都是一样的;index.html也被放进了该文件夹
- Vue实例化代码发生改变,但本质也是一样的
new Vue({
//挂载el元素,本质上就是执行$mount()函数
//el:'#app',
render: h => h(App),
}).$mount('#app')
配置
图形化配置界面
- 在项目目录下输入命令:
vue ui
- 浏览器进入地址:
http://localhost:8000/project/select
- 在配置界面中导入项目
手动配置
CLI3的默认配置文件被放到了node_modules —— @vue —— cli-service文件夹中
如果我们想要修改一些配置,需要手动在项目根目录创建vue.config.js文件,通过module.exports()
导出我们手写的配置,系统会将我们的配置文件与默认的配置文件合并
如果新建的配置文件显示绿色,则需要执行一些git命令:
git add .
git status
git commit -m '修改配置文件'
Vue - router 导读
后端路由阶段
以前我们访问一个网站时,向服务器发出请求,服务器根据url地址通过jsp将该url页面的html结构+css样式+java操作数据库(三者全由后端编写)代码渲染成页面,服务器返回给我们的就是一个完整的页面(html+css+数据)
在服务器端已经将页面渲染好了,叫做后端渲染
后端处理URL和页面之间的关系,叫做后端路由
这种情况下渲染好的页面, 用户不需要单独加载任何的js和css, 可以直接交给浏览器展示, 这样也有利于SEO的优化
缺点
- 整个页面的模块由后端人员来编写和维护的
- 前端开发人员如果要开发页面, 需要通过PHP和Java等语言来编写页面代码
- 而且通常情况下HTML代码和数据以及对应的逻辑会混在一起, 编写和维护都是非常糟糕的事情
前后端分离阶段
随着Ajax的出现,使得前端可以通过Ajax获取数据, 并且可以通过JavaScript将数据渲染到页面中
后端只提供API来返回数据即可
假入我们访问一个网站,向服务器发出请求,服务器分为两部分:静态资源服务器和提供API数据接口的服务器;我们会先访问静态资源服务器,获取html+css+js代码,直接将界面渲染到我们的浏览器上,然后获取到的js代码中的数据请求如:$.ajax(url:api接口,success:function)
由浏览器执行再向API数据接口服务器请求数据,再由API接口服务器返回给我们数据,浏览器再将获取到的数据通过js代码渲染到页面上
浏览器中显示的网页中的大部分内容,都是由前端写的js代码在浏览器执行并最终渲染出来的,这就叫做前端渲染
单页面富应用阶段
在前后端分离阶段,在静态资源服务器要为每一张页面保存一套对应的html+css+js代码
而在SPA阶段,整个项目只有一个index.html页面
假设我们访问一个网站,服务器会将该网站所有的html+css+js代码发给我们,但是并没有立刻全部渲染出来
当我们点击某个链接跳转页面时,此时url发生改变,浏览器会将对应url的代码从我们获取的全部代码里面抽取出来渲染到页面上
要实现此技术,需要依赖前端路由,前端路由有一层映射关系,用来保存每一个url要从获取到的全部代码中获取哪一部分代码
总结:
单页面富应用阶段(SPA)最主要的特点就是在前后端分离的基础上加了一层前端路由,也就是前端来维护一套路由规则
前端路由的核心是什么呢?
改变URL,但是页面不进行整体的刷新
URL 的 hash
在修改url地址的情况下而不会刷新页面,有两种方式可以实现,其中一种就是添加hash值
我们为页面url添加hash值,浏览器不会重新请求任何数据,但vue-router会监听这个事件,根据这个hash值去路由的映射关系表中找到对应的组件,再将该组件的代码渲染到页面上
HTML5的history模式
history接口是HTML5新增的,它有五种模式改变URL而不刷新页面
history.pushState(data,title,?url)
:修改url地址,类似于栈结构,按浏览器后退键可以返回上一次的修改的url地址history.replaceState(data,title,?url)
:修改url地址,且无法返回由该方法修改过的url地址history.go()
:与pushState配合使用,根据传入的参数决定前进或后退到第几个url地址history.back()
:相当于go(-1)history.forward()
:相当于go(1)
vue - router
认识vue - router
vue-router是Vue.js官方的路由插件,它和vue.js是深度集成的,适合用于构建单页面应用
vue-router允许我们配置url与组件之间的映射关系,只要url发生改变,它就会自动去加载对应的组件并渲染到页面上
初始化vue - router
我们使用CLI脚手架构建项目的时候已经勾选了vue-router,所以这里无需再安装;安装命令:npm install vue-router --save
我们删掉脚手架帮我们创建好的router文件夹和main.js中关于router的相关代码,学会自己初始化router:
在src目录下新建router文件夹,在该文件夹中新建index.js
// 该文件用来配置路由的相关信息
//因为要使用路由,所以要先导入路由
import VueRouter from 'vue-router'
// 1. 通过Vue.use(插件)方法来安装插件
// 因为要使用Vue对象,所以要先导入Vue
import Vue from 'vue'
Vue.use(VueRouter)
// 2. 创建VueRouter对象
const router = new VueRouter({
// 在routes配置路由(url)和组件之间的映射关系
routes: [
// 映射关系表
]
})
// 3. 将router对象传入到Vue实例中
export default router
在main.js文件中挂载我们创建的路由实例
import Vue from 'vue'
import App from './App'
import router from './router/inedx'
Vue.config.productionTip = false
new Vue({
el: '#app',
// 4. 挂载router
router,
render: h => h(App)
})
使用vue - router
- 创建路由组件
我们在components目录下新建两个vue文件:home.vue 和 about.vue - 配置路由映射
进入index.js,在router对象的routes数组中添加组件和url路径映射关系,一对映射关系就是一个对象
const router = new VueRouter({
routes: [{
// 路径中只要出现了'/home',就显示下面的Home组件
path: '/home',
component: Home
}, {
path: '/about',
component: About
}]
})
- 导入组件
想要映射组件,我们肯定要导入对应的组件
import Home from '../components/Home.vue'
import About from '../components/About.vue'
- 添加两个标签,通过改变url实现组件之间的切换,使用全局组件:
<router-link to="value"> </router-link>
,value
值就是点击该标签时,url添加的hash值;最终该标签会被渲染为<a>
标签
<template>
<div id="app">
<router-link to="/home">HOME</router-link>
<router-link to="/about">ABOUT</router-link>
</div>
</template>
- 添加
<router-view></router-view>
标签来决定组件显示位置;在路由切换时, 切换的是<router-view>
挂载的组件, 其他内容不会发生改变
<template>
<div id="app">
<router-link to="/home">HOME</router-link>
<router-link to="/about">ABOUT</router-link>
<!-- 写到了链接的下面,所以组件显示时会在下面显示 -->
<router-view></router-view>
</div>
</template>
<router-link>
和 <router-view>
标签的源组件名其实就是RouterLink 和 RouterView
所以我们使用其他组件标签的时候也要写成短横线连接的形式
默认显示的组件
我们希望打开首页时默认跳转到 /home
路径并且渲染Home组件,则需要在配置路由映射时添加一个重定向:
const router = new VueRouter({
routes: [
{
// 当url处于根路径时,将url重定向为/home
path: '/',
redirect: '/home'
},
...
]
})
将hash模式修改为history模式
默认情况下, 路径的改变使用的url的hash,但是url路径中总是多一个井号localhost:8080/#/home
如果希望使用HTML5的history模式, 非常简单, 进行如下配置即可:
const router = new VueRouter({
routes: [
...
],
// 添加mode属性,修改为history
mode: 'history'
})
router-link 的其他属性
在前面的<router-link>
中, 我们只是使用了一个属性: to
, 用于指定跳转的路径
<router-link>
还有一些其他属性:
tag
: tag可以指定<router-link>
之后渲染成什么组件,不过都是点击时实现url跳转
<router-link to="/home" tag="button">HOME</router-link>
replace
:添加replace属性将使用history的replaceState实现跳转,因此禁止再返回页面
// replace属性不需要添加属性值
<router-link to="/about" replace >ABOUT</router-link>
active-class
:当<router-link>
对应的路由匹配成功时, 会自动给当前元素设置一个router-link-active
的class, 设置active-class
可以修改默认的名称
<!-- 在对应的组件上添加 -->
<router-link to="/home" active-class="red">HOME</router-link>
在router对象中添加可直接设置到所有组件上
const router = new VueRouter({
...
linkActiveClass: 'red'
})
$router 属性
vue为所有的组件的data对象添加了$router
属性,这个$router
就是我们index.js中实例化的VueRouter
对象
所有的组件都继承自Vue类的原型,$router
就是一个典型
我们输入 Vue.protype.name="lisa"
,相当于为所有的组件添加了一个name属性,其值为Lisa
而 $router
就是 Vue.protype.$router
,已经被定义好了,我们可以直接调用里面的方法和属性
我们可以调用该属性的push()
方法,相当于执行了history.pushState()
;当然也有对应的replace()
方法
注意:虽然push方法与history方法实现同样的结果,但不能直接使用history方法,那样相当于跳过了vue路由直接跳转
通过代码实现路由跳转
前面我们是通过<router-link>
组件实现的路由跳转,有时候页面在跳转时可能需要执行对应的js代码, 这个时候就需要使用第二种跳转方式了
- 不再使用
<router-link>
组件,直接使用标签,并为其添加点击事件
<template>
<div id="app">
<button @click="homeClick">Home</button>
<button @click="aboutClick">About</button>
<router-view></router-view>
</div>
</template>
- 添加对应的
push()
方法,传入的参数就是url要跳转的地址
<script>
export default {
name: "App",
methods: {
homeClick() {
this.$router.push("/home");
console.log("home被点击");
},
aboutClick() {
this.$router.push("/about");
console.log("about被点击");
},
},
};
</script>
如果连续点击两次相同的标签执行路由跳转时报错,则需添加以下代码:
// 防止路由重复跳转时报错
const originalPush = VueRouter.prototype.push
VueRouter.prototype.push = function push(location) {
return originalPush.call(this, location).catch(err => err)
}
动态路由
在某些情况下,一个页面的path路径可能是不确定的,比如我们进入用户界面时,希望是如下的路径:
/user/michael 或 /user/lisa
除了有前面的/user之外,后面还跟上了用户的ID
这种path和Component的匹配关系,我们称之为动态路由(也是路由传递数据的一种方式)
动态路由使用步骤:
- 进入index.js,在路由映射表中找到需要使用动态路由的path属性的值后面添加一个
/:value
const router = new VueRouter({
routes: [{
{
// 注意要添加到引号内部
path: '/user/:userId',
component: User
}
],
})
- 在App.vue中定义变量userId为lisa:
export default {
name: "App",
data() {
return {
userId: "lisa",
};
},
};
- 使用
v-bind
为对应的router组件设置to
属性:
<!-- 将'/use/'与我们动态获取过来的ID拼接 -->
<router-link :to="'/user/'+userId">USER</router-link>
获取动态路由添加的url
使用动态路由可以动态地在url地址后面拼接不同的url,但是我们想获取到这个拼接的url值,我们该怎么做?
答:使用组件的$route
属性,注意这里是route不是router
$route
:我们在点击某个路由标签跳转的时候,会给该标签添加一个 router-link-active
类,则说明该路由处于活跃状态。通过$route
就可以获得这个活跃的路由
- 在对应的组件中添加一条计算属性:
computed: {
id() {
// 为什么这里使用userId:因为我们在路由映射表中拼接的:userId
return this.$route.params.userId;
},
}
- 直接添加到对应组件模板中使用:
<template>
<div>
<h2>你好,我是{{id}}/h2>
</div>
</template>
也可以不添加计算属性,直接添加到模板中使用:
<template>
<div>
// 这里为什么不用加this?
// 因为$route变量是存放在data对象中的,计算属性或方法要使用才需要加this
{{$route.params.userId}}
</div>
</template>
路由懒加载
当打包构建应用时,Javascript包会变得非常大,影响页面加载
如果我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件,这样就更加高效了
我们可以观察webpack打包之后的dist文件夹已经做好了归类:
使用懒加载后,浏览器请求的时候不会直接请求所有代码,但是有一些代码是必要的:
- manifest.js:底层支撑代码,为程序正常编译做支撑
- vendor.js:第三方库,不然我们写的Vue代码都没法正常解析
- 业务逻辑等公共js代码
除开以上的代码,我们要使用懒加载分割的就是我们写的业务代码:每个使用懒加载导入的组件都会被分割给一个js文件,需要用到哪个组件就从服务器请求对应的js文件
使用懒加载的三种方式
我们以映射Home组件为例
方式一:
结合Vue的异步组件和Webpack的代码分析:
const Home = resolve => {
require.ensure(['../components/Home.vue'], () => {
resolve(require('../components/Home.vue'))
})
};
方式二:
AMD写法:
const About = resolve => require(['../components/About.vue'], resolve);
方式三:(推荐写法)
在ES6中, 我们可以有更加简单的写法来组织Vue异步组件和Webpack的代码分割:
// 以前的组件导入方式:
import Home from '../components/Home'
// 使用懒加载的组件导入方式:
const Home = () => import('../components/Home.vue')
const router = new VueRouter({
routes: [{
{
...
path: '/home',
// 映射导入的Home组件
component: Home
}
],
})
嵌套路由
我们希望处于某个主路由界面下,还可以实现子路由之间的切换:
实现步骤:
- 创建两个.vue文件,以主路由+子路由命名,如UserPost
- 在路由映射表中的父路由对象中添加
children
属性:
// 使用懒加载导入子路由组件
const UserPost = () => import('../components/UserPost')
const UserProfile = () => import('../components/UserProfile')
const router = new VueRouter({
routes: [
{
path: '/user/:userId',
component: User,
children: [
{
// 这里子路由path不能加'/'
path: 'post',
component: UserPost
},
{
path: 'profile',
component: UserProfile
}
]
}
]
})
- 在父路由组件添加
<router-link>
和<router-view>
标签
<!-- 因为这里需要带上userId跳转子路由,所以拼接了一个计算属性userId -->
<!-- 如果直接跳转子路由,则将userId去掉即可 -->
<template>
<div>
<h2>我是父路由user</h2>
<h2>我是父路由user</h2>
<!-- url在拼接了用户id的基础上拼接子路由 -->
<router-link :to="'/user/'+userId+'/post'">post</router-link>
<router-link :to="'/user/'+userId+'/profile'">profile</router-link>
<router-view></router-view>
</div>
</template>
<script>
export default {
// 这里添加了一个计算属性保存当前的用户id
computed: {
userId() {
return this.$route.params.userId;
},
},
};
</script>
- 设置默认显示的子路由
const router = new VueRouter({
routes: [
{
path: '/user/:userId',
component: User,
children: [
{
// 添加路由重定向
path: '',
redirect: 'post'
},
{
path: 'post',
component: UserPost
},
{
path: 'profile',
component: UserProfile
}
]
}
],
})
url 传递参数
通过传递参数主要有两种类型: params 和 query
params的类型:
- 配置路由格式:
/router/:userId
- 传递的方式: 在path后面跟上对应的值
- 传递后形成的路径:
/router/lisa
,/router/capper
query的类型:
- 配置路由格式:
/router
, 也就是普通配置 - 传递的方式:
to
属性传递的对象中使用query作为传递方式 - 传递后形成的路径:
/router?id=123, /router?id=abc
query介绍
URL的一般语法格式为:(带方括号[ ]的为可选项)
protocol :// hostname[:port] / path / [;parameters][?query]#fragment
query
(查询):
用于给动态网页传递参数,可有多个参数,用&
符号隔开,每个参数的名和值用=
符号隔开
我们<router-link to="value">
的to
属性不仅可以赋值字符串,也可以传递对象,需要借助v-bind
。该对象中的path
属性就是路由跳转路径,query
属性就是要传递的参数
使用对象形式设置路由跳转地址:
<!-- 字符串形式 -->
<router-link to="/home">HOME</router-link>
<!-- 对象形式 -->
<router-link :to="{path:'/about'}">ABOUT</router-link>
使用query传参
在 router-link 标签中使用query
- 为
<router-link>
的to
属性赋值字符串并传递数据
<template>
<div id="app">
<!-- 对象形式 -->
<router-link
:to=
"{path:'/about',
query:{
name:'Hermione',
age:20
}
}">ABOUT</router-link>
<router-view></router-view>
</div>
</template>
- 我们的数据传递到了
/about
路径,所以在/about
组件接收
<template>
<div>
<h2>我是about</h2>
<!-- 打印接收到的也就是url中传递的name和age -->
{{$route.query.name}}
{{$route.query.age}}
</div>
</template>
在 button 标签中使用query
- 创建button标签:
<template>
<div id="app">
<button @click="homeClick">HOME</button>
<router-view></router-view>
</div>
</template>
- 添加button点击事件:
<script>
export default {
methods: {
homeClick() {
// 调用$router的push方法,传入对象
this.$router.push({
// 路由跳转路径
path: "/home",
// 传入数据
query: {
name: "jennie",
age: 22,
},
});
},
},
};
</script>
- 因为路由跳转到了home,所以在Home组件接收数据
<template>
<div>
<h2>我是Home组件</h2>
<!-- 使用$route.query接收数据 -->
金智妮的英文名:{{$route.query.name}}
金智妮的年龄是:{{$route.query.age}}
</div>
</template>
导航守卫
我们希望在路由与路由之间跳转的时候做一个监听,可以在跳转的过程中做一些其他操作,这就使用到了导航守卫
什么是导航守卫:
vue-router提供的导航守卫主要用来监听监听路由的进入和离开的.
vue-router提供了beforeEach和afterEach的钩子函数, 它们会在路由即将改变前和改变后触发
导航守卫的使用
假设我们希望在路由跳转的时候修改对应的 title ,实现步骤:
- 进入index.js,调用router对象的beforeEach函数
router.beforeEach((to, from, next) => {
// from就是即将离开的路由对象
// to就是将要跳转到的路由对象
// 使用beforeEach时必须调用一遍next()
next()
// 嵌套路由时,显示子路由的title
window.document.title = to.meta.title
// 嵌套路由时,显示父路由的title
window.document.title = to.matched[0].meta.title
})
- 在路由映射表中为每个路由对象添加meta属性
const router = new VueRouter({
routes: [
{
path: '/',
redirect: '/home'
},
{
path: '/home',
component: Home,
meta: {
title: '首页'
}
},
{
path: '/user/:userId',
component: User,
meta: {
title: '用户'
},
children: [
{
path: '',
redirect: 'post'
},
{
path: 'post',
component: UserPost,
meta: {
title: '发布'
}
},
{
path: 'profile',
component: UserProfile,
meta: {
title: '我的'
}
}
]
}
],
})
导航守卫补充
- 如果是afterEach, 也就是后置钩子, 不需要主动调用
next()
函数,因为路由已经跳转完成了 - 上面我们使用的导航守卫是全局守卫,会为所有的路由添加监听,还有其他两种守卫:
- 路由独享的守卫
为某个特定的路由添加回调函数 - 组件内的守卫
next()
还可以传入参数来跳转到不同的地址,还有很多其他的使用方法,可以查阅 vue-router 官方文档
keep-alive 组件
有时候我们不希望组件被重新渲染影响使用体验;或者处于性能考虑,避免多次重复渲染降低性能。而是希望组件可以缓存下来,维持当前的状态。这时候就可以用到keep-alive
组件
应用场景
如果未使用keep-alive
组件,则在页面回退时仍然会重新渲染页面,触发created
钩子,使用体验不好。
在以下场景中使用keep-alive
组件会显著提高用户体验,如菜单存在多级关系,多见于列表页+详情页的场景如:
- 商品列表页点击商品跳转到商品详情,返回后仍显示原有信息
- 订单列表跳转到订单详情,返回,等等场景
使用案例
案例:要求在home路由中点击msg子路由,然后跳转其他路由,返回home后依然停留在msg子路由
(tip:下面的写法是通过对url进行操作来保存上次访问的路由url,可能都不需要使用keep-alive,日后使用时请先进行测试)
- 使用
<keep-alive>
组件包裹<router-view>
组件,这样所有路径匹配到的视图组件都会被缓存
<keep-alive>
<router-view></router-view>
</keep-alive>
- 删除之前的默认子路由
// path: '',
// redirect: 'news'
- 进入父路由组件,使用
activated()
方法和beforeRouteLeave()
方法实现子路由切换状态的保存
<script>
export default {
data() {
return {
// 默认子路由
path: "/user/news"
};
},
// 路由活跃时执行的回调
activated() {
// 将url修改为path
this.$router.push(this.path);
},
// 离开路由前保存当前的url,赋值给path
beforeRouteLeave(to, from, next) {
this.path = this.$route.path;
next();
},
};
</script>
include 和 exclude
上面我们直接使用<keep-alive>
将<router-view>
包裹了起来,这样所有的父组件都会被缓存
但是如果我们想只缓存一个组件或者不想缓存某个组件,则需要使用以下两个属性:
include
:只有匹配的组件会被缓存exclude
:匹配到的组件都不会被缓存
要使用这两个属性,我们对应的组件导出时不能省略name
属性:
<script>
export default {
// 不能省略name属性,属性值要与include或exclude的值做匹配
name: "home"
};
</script>
使用这两个属性:
// 不缓存name为home的组件
<keep-alive exclude="home">
<router-view></router-view>
</keep-alive>
// 只缓存name为about和profile的组件,逗号后不能有空格
<keep-alive include="about,profile">
<router-view></router-view>
</keep-alive>
Promise
当我们执行异步网络请求的时候,请求成功时执行回调函数,该回调函数里又包含了一次网络请求,该请求成功时也要执行回调,可是这一次的回调中又包含了一次网络请求…这还没有考虑回调函数的代码量和请求失败时的回调
为了实现需求,我们敲出来的代码肯定臃肿且不易维护
Promise就是让我们更加优雅地书写复杂的异步任务,将嵌套格式的代码变成了顺序格式的代码
Promise 的使用
Promise 构造函数只有一个参数,是一个函数,这个函数在构造之后会直接被异步运行,所以我们称之为起始函数。起始函数包含两个参数 resolve
和 reject
当 Promise 被构造时,起始函数会被异步执行:
new Promise((resolve, reject) => {
console.log('run');
});
这段程序会直接输出 run
resolve
和 reject
都是函数,其中调用 resolve
代表一切正常,reject
是出现异常时所调用的:
new Promise((resolve, reject) => {
// 异步请求代码段:
// 请求结束,进行结果判定:
//请求成功执行resolve(),并调用then()
//请求失败则执行reject(),并调用catch()
var a = 1;
if (a == 1) {
resolve('success')
} else {
reject('failed')
}
// data1是resolve()函数传入的参数
}).then((data1) => {
// 有任何异常都会直接跳到catch函数
console.log(data1);
// 嵌套异步操作:
return new Promise((resolve,reject)=>{}).then(()=>{})
// data2就是reject()函数传入的参数
}).catch((data2) => {
console.log(data2);
// 不管最后 Promise 对象状态如何,都会执行finally
}).finally(() => {
console.log('finally');
})
Promise 可以只调用then( )方法,不调用catch( ),但是then( )需要传两个参数分别代表成功和失败:
new Promise((resolve, reject) => {
setTimeout(() => {
resolve('success')
reject('failed')
}, 1000)
}).then((data1) => {
console.log(data1);
}, (data2) => {
console.log(data2);
})
Promise 链式调用
嵌套异步操作时,我们不需要使用 return new Promise
执行异步操作
可以直接使用 return value
相当于执行了 resolve(value)
;使用 throw value
相当于执行 catch(value)
new Promise((resolve, reject) => {
setTimeout(() => {
resolve('abc')
}, 1000)
}).then((res) => {
console.log(res);
// 相当于执行了return new Promise(()=>{reject('error')}),直接执行后面的catch()函数
throw 'error'
}).then((res) => {
console.log(res);
// 相当于执行了return new Promise(()=>{resolve('abc')}),然后执行后面的then()函数
return 'abc'
}).then((res) => {
console.log(res);
}).catch((res) => {
console.log(res);
})
Promise.all() 方法
在实际项目中,可能会遇到需要从前两个接口中的返回结果获取第三个接口的请求参数这种情况
也就是需要等待两个/多个异步请求完成后,再进行回调
Promise.all()
方法的参数需传入一个数组,每个数组元素是一个Promise异步请求,只有所有请求都成功后才会执行回调,有一个请求失败则报错
Promise.all([
new Promise((resolve, reject) => {
setTimeout(() => {
// 此异步请求成功,执行resolve()
resolve('AAA')
}, 2000)
}),
new Promise((resolve, reject) => {
setTimeout(() => {
resolve('BBB')
}, 1000)
})
// all方法的回调,输出所有resolve的参数
]).then(results => {
console.log(results);
})
Vuex
Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并保证数据的响应式
试想一下,如果在一个项目开发中频繁的使用组件传参的方式来同步data中的值,一旦项目变得很庞大,管理和维护这些值将是相当棘手的工作。为此,Vue为这些被多个组件频繁使用的值提供了一个统一管理的工具 —— Vuex
在具有Vuex的Vue项目中,我们只需要把这些值定义在Vuex中,即可在整个Vue项目的组件中使用
有什么状态是需要我们在多个组件间共享的呢?
- 用户的登录状态、用户名称、头像、地理位置信息等
- 商品的收藏、购物车中的物品等
这些状态信息,我们都可以放在统一的地方,对它进行保存和管理,而且它们还是响应式的
初始化
- 安装vuex:
npm install vuex --save
- 在src目录下新建文件夹,命名为store
- 在store文件夹中创建index.js文件,并键入以下代码:
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
const store = new Vuex.Store({
state: {
words:'Hello Vuex!'
},
mutations: {},
actions: {},
getters: {},
modules: {}
})
export default store
- 在main.js的Vue实例中挂载store:
...
import store from './store/'
new Vue({
el: '#app',
store,
...
})
// 挂载store后,相当于执行Vue.prototype.$store = store
// 所有组件都可以通过$store访问store对象
基本使用
在组件中使用Vuex
例如我们要将state中定义的words拿来在h2标签中显示:
<template>
<div id='app'>
words:
<h2>{{ $store.state.words }}</h2>
</div>
</template>
或者要在组件方法中使用:
methods:{
add(){
console.log(this.$store.state.words)
}
// 注意,请不要在此处更改state中的状态的值
// 对Vuex的每个操作都要按照规定进行访问和修改
}
Vuex的核心内容
在Vuex对象中,其实不止有state
,还有用来操作state
中数据的方法集,以及当我们需要对state
中的数据需要加工的方法集等等成员
成员列表:
state
:存放状态mutations
:操作state成员getters
:加工state成员给外界actions
:异步操作modules
:模块化状态管理
Vuex的工作流程
首先,Vue组件如果调用某个Vuex的方法过程中需要向后端请求时或者说出现异步操作时,需要dispatch Vuex中actions的方法,以保证数据的同步。可以说,action的存在就是为了让mutations中的方法能在异步操作中起作用
如果没有异步操作,那么我们就可以直接在组件内提交状态中的Mutations中自己编写的方法来达成对state成员的操作。注意,前面有提到,不建议在组件中直接对state中的成员进行操作,这是因为直接修改(例如:this.$store.state.name = 'hello'
)的话不能被VueDevtools所监控到
最后被修改后的state成员会被渲染到组件的原位置当中去
State单一状态树
如果你的状态信息是保存到多个Store对象中的,那么之后的管理和维护等等都会变得特别困难
所以Vuex也使用了单一状态树来管理应用层级的全部状态,我们只需要创建一个store对象
单一状态树能够让我们最直接的方式找到某个状态的片段,而且在之后的维护和调试过程中,也可以非常方便的管理和维护
Mutation
mutations
是操作 state
数据的方法的集合,比如对该数据的修改、增加、删除等
基本使用
mutations对象中的方法都有默认的形参:([state] [,payload])
state
:就是当前Vuex对象中的statepayload
:该方法在被调用时传递参数使用的
例如,我们编写一个方法,当被执行时,能把下例中的name值修改为"jack",我们只需要这样做:
// 在index.js(store)文件中设置name值并定义edit方法
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
const store = new Vuex.Store({
state:{
name:'helloVuex'
},
mutations:{
edit(state){
state.name = 'jack'
}
}
})
export default store
而在组件中,我们需要这样去调用这个mutation——例如在App.vue的某个methods中:
// 调用mutations方法,需使用commit,参数即为mutations中的方法名
this.$store.commit('edit')
Mutation传值
在实际生产过程中,会遇到需要在提交某个mutation时需要携带一些参数给方法使用
单个值提交时:
this.$store.commit('edit',15)
当需要多参提交时,推荐把他们放在一个对象中来提交:
this.$store.commit('edit',{age:15,sex:'男'})
接收挂载的参数:
edit(state,payload){
...
console.log(payload) // 15或{age:15,sex:'男'}
}
另一种提交方式
this.$store.commit({
type:'edit',
payload:{
age:15,
sex:'男'
}
})
接收挂载的参数:
edit(state,payload){
...
console.log(payload) // {type: "edit", payload: {age:15,sex:"男"}}
}
Mutation响应式原理
Vuex的store中的state数据是响应式的,当state中的数据发生改变时,Vue组件会自动更新
但自动更新有两个前提:
- 已经提前在store中定义好的属性,之后添加的属性不会更新
- 当给state中的对象添加新属性时, 使用下面的方式:
- 使用Vue.set(obj, ‘newProp’, 123)
- 用新对象给旧对象重新赋值
增删state中的成员
为了配合Vue的响应式数据,我们在Mutations的方法中,应当使用Vue提供的方法来进行操作
Vue.set
为某个对象设置成员的值,若不存在则新增
例如对state对象中添加一个age成员:
Vue.set(state,"age",15)
Vue.delete
删除成员
将刚刚添加的age成员删除:
Vue.delete(state,'age')
另一种方法:使用新对象给旧对象重新赋值的方式实现响应式:
state.info = {...state.info, 'height': payload.height}
Mutation常量类型
在mutation中,我们定义了很多方法,当我们的项目增大时,Vuex管理的状态越来越多,需要更新状态的情况越来越多,那么意味着Mutation中的方法越来越多
意味着使用者需要花费大量的时间去记住这些方法,我们希望使用一个单独的文件来保存这些方法名,并使用常量替代Mutation事件的类型
使用步骤:
- 在store目录下新建 mutations-type.js文件
- 在该文件定义方法名对应的常量并抛出
// INCREMENT:替代方法名的常量
// 'increment':方法的描述
export const INCREMENT = 'increment'
- 导入常量并将mutations中的方法名替换为常量
import { INCREMENT } from './mutations-type'
const store = new Vuex.Store({
state: {
count: 10,
},
mutations: {
[INCREMENT](state) {
state.count++;
}
},
})
- 导入该常量并使用
import { INCREMENT } from "./store/mutations-type";
export default {
methods: {
add() {
this.$store.commit(INCREMENT);
},
},
};
Getters
getters类似于组件中的computed计算属性,可以对state中的成员加工后传递给外界
Getters中的方法有两个默认参数:
state
:当前Vuex对象中的状态对象getters
:当前getters对象,用于将getters下的其他getter拿来用
例如:
getters:{
nameInfo(state){
return "姓名:"+state.name
},
fullInfo(state,getters){
return getters.nameInfo+'年龄:'+state.age
}
}
组件中调用:
this.$store.getters.fullInfo
向getters传递参数
getters默认是不能传递参数的, 如果希望传递参数, 那么只能让getters本身返回另一个函数
比如我们希望传递一个age,通过getters加工后打印出来:
getters: {
print() {
return function (age) {
return '我今年' + age + '岁了'
}
}
}
组件中传参调用:
$store.getters.print(18)
Actions
Vuex要求我们mutation中的方法必须是同步方法,如果是异步操作,那么devtools将无法追踪这个操作的完成情况
如果我们需要在Vuex中进行一些异步操作,比如网络请求必须在actions中中转一下,才能执行mutation
假设我们通过点击按钮,异步修改count的值
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
const store = new Vuex.Store({
state: {
count: 10,
},
mutations: {
add(state) {
state.count++;
}
},
actions: {
// context:和store对象具有相同方法和属性的对象
// payload:挂载的参数
act_add(context,payload) {
// 错误,只要是修改state,必须由mutations执行
// context.state.count++;
setTimeout(() => {
// 执行mutation方法
context.commit('add')
}, 1000)
}
}
})
export default store
点击按钮,使用dispatch调用action中的方法
methods: {
btnClick() {
this.$store.dispatch("act_add",传递的payload参数);
},
},
使用Promise封装
在Action中, 我们可以将异步请求放在一个Promise中, 并且在请求成功或者失败后, 调用对应的resolve或reject回调函数
将上面的异步操作用Promise封装:
actions: {
act_add(context) {
// 返回一个Promise对象
return new Promise((resolve) => {
setTimeout(() => {
context.commit('add')
resolve('异步修改count成功!')
}, 1000)
})
}
}
触发按钮点击事件,执行action
methods: {
btnClick() {
// 这里接收Promise对象,执行then方法
this.$store.dispatch("act_add")
.then((res) => {
console.log(res); // 打印:异步修改count成功!
});
},
},
Modules
Vue使用单一状态树,那么也意味着很多状态都会交给Vuex来管理,当应用变得非常复杂时,store对象就有可能变得相当臃肿
为了解决这个问题,Vuex允许我们将store分割成模块(Module),而每个模块拥有自己的state、mutation、action、getters等
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
const moduleA = {
state: {
name: '我是moduleA'
},
mutations: {
// 起名不要和其他模块重复
print() {
console.log('我是moduleA的方法');
},
// 这里是局部state
updateName(state) {
state.name = '我是修改后的module'
}
},
getters: {
// 这里是局部state
fullName1(state) {
return state.name + '哈哈哈'
},
// 模块内的getters可以传递第三个参数,代表根store中的state
fullName2(state, getters, rootState) {
return getters.fullName1 + rootState.name
}
},
actions: {
asyncUpdate(context) {
setTimeout(() => {
// 局部状态通过 context.state 暴露出来
// 根节点状态则为 context.rootState
// 模块内的commit只会执行自己模块的mutation
context.commit('updateName')
}, 1000)
}
}
}
const store = new Vuex.Store({
state: {
name: '我是根部state',
},
modules: {
// 绑定module
a: moduleA
}
})
export default store
在组件中使用模块:
<template>
<div>
<!-- 获取模块内数据,需要加模块名调用 -->
<h2>{{$store.state.a.name}}</h2>
<!-- 调用模块内getters,不需要加模块名 -->
<h2>{{$store.getters.fullName1}}</h2>
<h2>{{$store.getters.fullName2}}</h2>
<button @click="btnClick">btn</button>
<button @click="asyncUpdateName">updateName</button>
</div>
</template>
<script>
export default {
methods: {
btnClick() {
// 使用模块内方法,不需要加模块名
this.$store.commit("print");
},
asyncUpdateName() {
this.$store.dispatch("asyncUpdate");
},
},
};
</script>
文件结构梳理
当我们的Vuex帮助我们管理过多的内容时,好的文件结构可以让我们的代码更加清晰
实现思路:
- 将index.js中除了根节点的每一个模块抽取为单独的js文件,保存到modules文件夹下
- 将index.js根节点的state属性抽取到Store对象外面,挂载到Store对象中,modules属性保留不变
const state = {
...
}
const store = new Vuex.Store({
state,
// 保留modules属性
modules: {
...
}
})
- 将根节点中的所有其他属性全部抽取为单独js文件,放到store目录下
Vuex 使用补充
mutations
唯一的目的就是修改 state
的状态,因此其中的每个方法尽量只完成一件事;
mutations
进行的数据操作可以被跟踪到,因此 state
状态的修改一定要经过 mutations
需要对数据进行判断的逻辑代码我们尽量放到 actions
中
mapGetters
mapGetters
辅助函数仅仅是将 store 中的 getter 映射到局部计算属性:
-
在 Vuex 中定义 getters 属性(在外部创建 getters.js 并引入)
// getters.js export default { cartLength(state) { return state.cartList.length } } // index.js import getters from './getters' ... const store = new Vuex.Store({ ... getters })
-
在需要使用 getters 的组件中导入 mapGetters,并在 computed 中注册
// 注意:是从 vuex 导入 import { mapGetters } from "vuex"; export default { ... computed: { // 第一种写法,直接使用getters中的属性 ...mapGetters(["cartLength"]) // 第二种写法,为getters中的属性赋别名 ...mapGetters({ length:'cartLength' }) } }
-
使用导入进来的 getters 中的属性
<template> <div class="cart"> <!-- 使用别名 --> <div>商品数:{{cartLength}}</div> <!-- 不使用别名 --> <div>商品数:{{length}}</div> </div> </template>
mapActions
mapActions
可以将 Vuex 中的 actions
内的某个方法映射到某个组件的 methods
中
mapActions 和 mapGetters 的使用步骤基本一致,只是一个要映射到 computed,一个是映射到 methods
-
准备工作:创建好 actions 方法
-
导入并在 methods 中注册
import {mapActions} from 'vuex' export default { ... methods: { ...mapActions(['addCart']) // 支持对象式注册,与mapGetters 一致,这里不再展示 } }
-
使用 mapActions 中的方法:我们可以直接将 mapActions 导入的方法当做自己的方法直接 this 调用
// 没有导入 mapActions 时: this.$store.dispatch("addCart", product); // 导入了 mapActions 时: this.addCart(product);
vuex 中的 promise
如果我们在 vuex 中执行了某些操作,想让外界知道是否完成了操作,就需要用 actions 返回一个 promise
axios 封装
为什么需要封装axios?
我们在开发中尽量不要出现多个页面都依赖于同一个第三方库的情况,最好要将其封装为一个文件,然后由多个页面调用,这样在更换第三方库的时候只需修改那一个文件即可
axios 框架基本使用
- 安装:
npm install axios --save
- 在main.js中导入axios
import axios from 'axios'
- 调用axios
axios({
url: '',
method: 'get',
params:{}
...
}).then(res => {
console.log(res);
})
axios 发送并发请求
同时发送两个请求:
axios.all([axios({
url: '',
...
}), axios({
url: ''
})]).then(res => {
console.log(res);
})
上面直接接收res会收到一个数组,我们可以使用 axios.spread()
方法解构数组:
axios.all([axios({
url: ''
}), axios({
url: ''
})]).then(axios.spread((res1, res2) => {
console.log(res1);
console.log(res2);
}))
axios 请求配置
创建请求时可以添加配置选项。只有 url
是必需的,如果没有指定 method
,请求将默认使用 get
方法
在开发中可能很多参数都是固定的,比如baseURL
。这个时候我们可以利用axiox的全局配置进行一些抽取
// 全局配置:
axios.defaults.baseURL = 'http://localhost:8080'
axios({
// 单独配置:
url:'',
method:'get'
})
使用 Promise 封装
- 在 src 目录下新建 network 文件夹,在里面创建request.js
- 在request.js中封装网络请求模块
import axios from 'axios'
// 为什么不使用default?
// 为了提高扩展性,可能会需要导出多个实例
export function request(config) {
// 创建axios实例(不要使用全局axios)
const instance = axios.create({
baseURL: '',
timeout: ,
})
// 返回一个promise
return instance(config)
}
- 在组件中使用request模块
import {
request
} from './network/request'
request({
url: ''
}).then(res => {
console.log(res);
}).catch(err => {
console.log(err);
})
axios 拦截器
页面发送http请求,很多情况我们要对请求和其响应进行特定的处理:例如每个请求都附带后端返回的token、拿到response之前loading动画的展示等
如果请求数非常多,这样处理起来会非常的麻烦,程序的优雅性也会大打折扣
在这种情况下,axios为开发者提供了这样一个API:拦截器。拦截器分为 请求(request)拦截器和 响应(response)拦截器
#### 请求拦截器
进入request.js,在创建axios实例的代码下面书写:
// 请求拦截
instance.interceptors.request.use(config => {
// 拦截成功
console.log(config);
// 拦截后需要返回config,让其继续发送请求
return config
}), err => {
// 拦截失败
console.log(err);
}
响应拦截器
// 响应拦截
instance.interceptors.response.use(res => {
// 拦截成功后要返回data
return res.data
}, err => {
// 拦截失败
console.log(err);
})
封装 $toast 插件
toast
:浏览器弹窗提示
我们封装好 $toast
插件以后,不管在哪个组件,只要一行代码就可以调用该插件(组件);不需要再导入,注册,添加模板等等…
封装步骤:
-
首先我们要在 common 文件夹下新建 toast 文件夹,准备好一个 Toast.vue 组件:
<template> <div class="toast" v-show="isShow"> <div>{{message}}</div> </div> </template> <script> export default { data() { return { message: "", isShow: false, }; }, methods: { show(message, duration) { this.isShow = true; this.message = message; setTimeout(() => { this.isShow = false; this.message = ""; }, duration); }, }, }; </script> <style lang="scss" scoped> .toast { padding: 10px; background-color: rgba(0, 0, 0, 0.7); color: #fff; position: fixed; left: 50%; top: 50%; transform: translate(-50%, -50%); z-index: 999; } </style>
-
在 toast 文件夹中新建 index.js 文件,在该文件创建并抛出一个对象
const obj = { } export default obj
-
进入程序入口文件 main.js,导入并安装该“对象”
import Vue from 'vue' import App from './App.vue' import toast from "components/common/toast"; Vue.use(toast) ...
-
执行
Vue.use()
方法本质上就是调用其install()
方法,我们在 index.js 中添加 install() 方法:import Toast from './Toast' const obj = {} obj.install = function (Vue) { // 1. 创建组件构造器 const toastConstructor = Vue.extend(Toast) // 2. 通过new可以根据组件构造器创建出来一个组件 const toast = new toastConstructor() // 3. 将组建对象手动挂载到某一个元素上 toast.$mount(document.createElement('div')) // 4. toast.$el对应的就是div元素 document.body.appendChild(toast.$el) // 5. 注册$toast Vue.prototype.$toast = toast } export default obj
-
在任意组件中使用该插件:
/* *@param1 res:弹窗显示的文本 *@param2 duration:持续时间 */ this.$toast.show(res, 2000); // 无须引入任何文件,简单一行代码,即可实现弹窗效果
组件化思想实战
我们以前开发项目都是一个文件专门写一个页面,这次我们使用 Vue Cli 搭建项目,并用组件化的思想封装一个 TabBar 组件
tabbar 组件实现思路:
我们希望我们的tabbar选项栏不仅选项的个数不一样,甚至选项的图片,文字都可以改变
所以我们在 <tabbar>
中使用<slot>
,希望外界可以给我们传入一个小组件
我们再为小组件定义图片的插槽和文字的插槽,这样可以最灵活化,这就是封装的思想
我们只希望TabBar.vue组件只关心大的tabbar组件的逻辑,至于里面的小的item怎么布局,文字使用div还是span,不应该由TabBar.vue文件来管理,只管理<div id="tab-bar">
和tab-bar的一些style样式,至于你item的东西不由我管理;所以我们直接在TabBar组件中放一个<slot>
结构搭建思路
assets文件夹是用来存放资源文件的,我们的css文件夹也要放到里面,还有images文件夹
在css文件夹中创建初始化css文件base.css
在components文件夹下新建tabbar文件夹,所有tabbar的组件都放到此文件夹内,为组件命名的时候要以大写字母开头并使用驼峰式命名,文件夹命名用小写;引入组件的时候也要用大写+驼峰来接收
不仅是tabbar,每个功能都要单独分一个文件夹来保存
images文件夹下也不能直接放图片,要按照每个实现的功能再划为多个文件夹
我们的公共组件放到 components 文件夹,我们的真正的独立页面则要新建一个文件夹叫做 views,然后为每个页面新建一个文件夹
为组件命名和引用时要以大写字母开头+驼峰式命名,使用组件的时候就直接使用小写+横杠连接即可,如:
<!-- 引入时: -->
import TabBar from ''
<!-- 使用时: -->
<tab-bar></tab-bar>
代码书写思路
我们新建好css文件后希望依赖一下它,但是我们不要再main.js中使用require('./assets/css/base.css')
,这样显得main.js太乱,我们可以在App.vue的<style>
标签中添加@import './assets/css/base.css'
来实现此css依赖(style中引入时固定使用此格式)
手机端底部tab-bar的理想高度:49px
插槽是会被对应的代码直接替换的,所以尽量不要给插槽添加属性和样式,可以在外层封装一层div,将属性和样式加给div
给路径起别名
我们引用路径的时候尽量不要出现诸如:../../../img
这样的路径,我们最好给文件夹起个别名
vue cli2下打开build目录,进入webpack.base.config.js文件,在resolve属性的alias对象中添加:
alias: {
'@': resolve('src'),
'assets': resolve('src/assets'),
'components': resolve('src/components'),
'views': resolve('src/views')
}
注意:
别名可以在import导入组件的时候直接使用,但如果是DOM标签的属性中需要使用别名,需要在前面加上一个~
波浪符号
修改配置文件后需要重新 run dev 一下,不然不会生效
使用scss书写css代码
- 安装node-sass sass-loader 依赖
- 直接在vue文件中的style标签添加lang属性:
<style lang="scss">
- 芜湖 起飞 ~
关于插件报错
只要不是跟我们写的业务代码有关的报错,考虑一下使用了什么插件之类的,降低版本再试试!!!