Vue是于2013年(与React框架同年发布)推出的一个渐进式、自底向上的前端框架,它的作者叫尤雨溪。那么什么叫做渐进式框架呢?比较官方的说法就是:
以Vue内核作为核心,随着业务的深入、需求的递增,可以使用其周边生态(vue-router、vuex、ssr等)深度应用到项目中
。那么通俗上来讲:就是我们可以使用vue的部分功能不断的迭代掉我们项目中部分的功能,从表单提交到列表渲染,再到多路由应用,再到SSR等
。 Vue主要具备以下几个特点:
- 解耦视图和数据
- 组件复用
- 前端路由
- 状态管理
- 虚拟DOM
Vue的学习不需要你具备 Rect、Angular的基础,只需要具备HTML、CSS、Javascript的基础即可。
Vue的安装主要有三种方式:
- CDN引入
- 下载本地引入
- vue-cli脚手架的方式
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js">script>
<script src="https://cdn.jsdelivr.net/npm/vue">script>
<script src="./lib/js/vue.js">script>
<div id="app">
{{msg}}
div>
<script src="./lib/js/vue.js">script>
<script>
// 使用 vue.js 内部提供的 Vue这个构造方法,接收一个对象
new Vue({
el: "#app", // 表示将当前创建的这个Vue对象挂载到哪个节点
data() { // vue的数据部分
return {
// 在return内部定义页面所需要的数据
msg: "Hello World"
}
}
});
script>
2.4.1 模板语法指令
v-text():
v-text是用于操作纯文本,它会替代显示对应的数据对象上的值,可以简写为{{}}, 即插值表达式
v-html:
将内容以html的形式呈现在页面。
v-bind:
将值绑定到标签的自定义属性上,形式为 v-bind:title=“mytitle”,可以简写为 :属性名
2.4.2 其他指令
v-cloak:
用来控制当只有数据呈现才显示Vue对应的dom元素。
v-model:
双向数据绑定。
v-if:
值如果为true的情况下,显示标签,如果为false会移除标签。
v-else-if:
与v-if配合使用。
v-else:
与v-if配合使用。
v-show:
如果为true,显示信息,如果为false则隐藏标签。
v-for:
循环遍历。语法形式为 v-for=“item in list”
v-on:click:
点击事件,可以简写为@click。在如上的指令中比较不好理解的就是
v-bind
指令,现给出如下示例:
v-bind
案例一:
<style>
.box {
width: 200px;
height: 200px;
background-color: purple;
}
.my-box {
width: 100px;
height: 100px;
background-color: plum;
}
style>
<div id="app">
<div :class="cls" :data-id="id" :title="title">div>
<div :class="{box: isBox}">div>
<div :class="{'my-box': isBox}">div>
<div :class="isBox ? 'box' : 'my-box'">div>
div>
<script src="../node_modules/vue/dist/vue.min.js">script>
<script>
new Vue({
el: '#app',
data() {
return {
id: 45,
cls: 'box',
title: '这是一个盒子',
isBox: true
}
}
})
script>
DOM的结果 |
---|
v-bind
指令案例二:
<!-- css样式如下 -->
<style>
.box {
width: 200px;
height: 200px;
background-color: #e3e3e3;
}
.my-box {
color: red;
}
</style>
<!-- 对应的html代码如下 -->
<div id="app">
<!-- 多样式需要判定 -->
<div :class="{box: isBox, 'my-box': isBox}">div中的文字</div>
<!-- 针对某个样式为固定,另外一个样式需要通过boolean值来判断是否要加,使用[]的方式 -->
<div :class="['box', {'my-box': isBox}]">div中的文字</div>
</div>
<!-- js代码如下 -->
<script src="../node_modules/vue/dist/vue.min.js"></script>
<script>
new Vue({
el: '#app',
data() {
return {
isBox: true,
}
}
})
</script>
DOM的结果 |
---|
v-bind
实现样式的绑定案例(了解):
<body>
<div id="app">
<div :style="{fontSize: fz, 'background-color': bc}">文字div>
div>
<script src="../node_modules/vue/dist/vue.min.js">script>
<script>
new Vue({
el: '#app',
data() {
return {
fz: '100px',
bc: 'red'
}
}
})
script>
body>
DOM的结果 |
---|
案例:
- 表格数据的添加与删除
- 表单渲染与数据的获取
计算属性是用来存储数据,而且数据可以进行逻辑操作,是基于其依赖的进行更新,只有在相关依赖发生变化的时候才会更新变化,计算属性是缓存的,只要相关依赖不发生变化,多次访问属性值是之前I计算得到的值,并不会多次执行。
监视器就是可以通过watch的方式观察某个属性的变化,然后做出相应的处理。
我们通过如下两个案例加强对这两个概念的理解:
案例一 |
---|
案例二 |
---|
MVC(Model-View-Controller) 是一种软件开发架构的设计模式,例如前端的SSR(Server Side Render,服务端渲染)就是非常典型的MVC设计模式,是由用户发起一个请求,然后由控制器(路由)将对应的数据(Model)渲染到一个页面,然后返回给用户。
MVC设计模式 |
---|
MVP(Model View Presenter),是在某些场景下由MVC演变而来,对于Android和C#开发的同学可能比较的熟悉,它完全弱化了View的逻辑处理,对于视图上数据的渲染是在Presenter中来完成修改,熟悉安卓和C#的朋友可能更加熟悉。
MVP设计模式 |
---|
MVVM(Model View ViewModel) 与 MVP的设计模式类似,唯一的区别就在于View的变动,会自动的反应到ViewModel上,反之也是。
MVVM设计模式 |
---|
vue.js 则是采用数据劫持结合发布者-订阅者模式的方式,通过
Object.defineProperty()
来劫持各个属性的setter
,getter
,在数据变动时发布消息给订阅者,触发相应的监听回调。具体实现步骤如下:
- 实现一个数据监听器Observer,能够对数据对象的所有属性进行监听,如有变动可拿到最新值并通知订阅者
- 实现一个指令解析器Compile,对每个元素节点的指令进行扫描和解析,根据指令模板替换数据,以及绑定相应的更新函数
- 实现一个Watcher,作为连接Observer和Compile的桥梁,能够订阅并收到每个属性变动的通知,执行指令绑定的相应回调函数,从而更新视图
双向数据绑定原理 |
---|
<script>
let obj = {}
Object.defineProperty(obj, 'name', {
get() { },
set() { }
})
script>
参考地址:https://mp.weixin.qq.com/s?__biz=MzI3NTM1MjExMg==&mid=2247483789&idx=1&sn=e7297ec3443007015117637709f27521&scene=21#wechat_redirect
每个 Vue 实例在被创建之前都要经过一系列的初始化过程。例如需要设置数据监听、编译模板、挂载实例到 DOM,在数据变化时更新 DOM 等。同时在这个过程中也会运行一些叫做生命周期钩子的函数,目的是给予用户在一些特定的场景下添加他们自己代码的机会。
Vue生命周期的主要阶段:创建,挂载,更新,销毁。
挂载(初始化相关属性)
beforeCreate ---- 备孕
注意点:在此时不能获取data中的数据,也就是说 this.msg 得不到任何内容
created ---- 怀上了
beforeMount ---- 怀胎十月
mounted【页面加载完毕的时候就是此时】 ---- 生下来了
注意点:默认情况下,在组件的生命周期中只会触发一次
更新(元素或组件的变更操作)
beforeUpdate
updated
注意点:可以重复触发的
销毁(销毁相关属性)
beforeDestroy — game over前
destroyed — game over
VUE声明周期 |
---|
虚拟DOM是Vue提高View层渲染的一个强有力的手段,所谓的虚拟DOM并不是虚无缥缈的东西,而是实实在在存在的一个数据结构,这是这个数据结构不是以DOM的方式存在,而是以JS对象的方式存在,VUE底层的虚拟DOM以及Diff算法是参照国外的
snabbdom
,例如下代码:
<div id="app">
<h3>标题h3>
<ul>
<li>NodeJSli>
<li>Vueli>
<li>Reactli>
ul>
div>
上面的一个DOM片段在对应的虚拟DOM的形式如下所示:
{
tag: 'DIV',
data: { id: 'app' },
children: [
{ tag: 'H3', data: {}, text: '标题' },
{
tag: 'UL',
data: {},
children: [
{ tag: 'LI', data: {}, text: 'NodeJS' },
{ tag: 'LI', data: {}, text: 'Vue' },
{ tag: 'LI', data: {}, text: 'React' }
]
}
]
}
diff算法是一种通过同层的树节点进行比较的高效算法,避免了对树进行逐层搜索遍历,所以时间复杂度只有 O(n)。diff算法的在很多场景下都有应用,例如在 vue 虚拟 dom 渲染成真实 dom 的新旧 VNode 节点比较更新时,就用到了该算法。diff算法只会在同层级进行, 不会跨层级比较。
Diff算法的比较 |
---|
节点的移动 |
---|
节点的删除与添加 |
---|
diff数据的改变 |
---|
观察如下这张图,如果要在B1和B2之间插入B4,那么VUE会怎么做呢?会将B2更新为B4, 将B3更新为B2, 然后在最后新增一个B3节点,那么这就明显有问题了,我们不能直接在B1和B2之间插入B4节点吗?答案是肯定的,那么就需要在遍历的数据上加上一个key属性,key属性添加后,diff算法首先会看Key,就能够推断出我们只是在B1和B2之间插入一个节点,从而大大的提升性能。
加了key值的作用 |
---|
在Vue中数组的数据变化之后会触发视图的重新渲染, 但是并不是所有的方式都会去触发视图的渲染,只有一下方法会触发视图的渲染:
push()
pop()
shift()
unshift()
splice()
sort()
reverse()
为什么呢?我们在之前讲过,双向数据绑定的原理,是通过Object.defineProperty()数据劫持来实现的,但是该方法只能劫持对象的属性,那么数组的数据如何实现劫持了?Vue想了一个很好的解决方案,就是改变了Vue中数组的
__proto__
指向,当用户在调用如上的方法的时候,同样会通知到我们的Watcher,来实现页面数据的渲染。
一个盒子中嵌套另外一个盒子的时候,在内外层的盒子中都有有对应的事件行为,当点击内层盒子时候,默认情况下会触发外层盒子的事件,这种默认的行为就是事件冒泡。需要去阻止事件的冒泡。使用方法:
@click.stop="方法名"
对于form表单来说,当点击表单中的button的时候,会默认触发表单的提交;对于a标签,当点击a标签的时候,会触发a标签访问。那么如何去阻止a标签的默认访问行为呢,使用方法是
@click.prevent="方法名"
在某些场景下,我们只希望事件只触发一次。
...
我们在日常开发的过程中最常见的按键修饰符就是,当在表单输入完毕之后点击回车开始执行对应的事件,那么处理方式如下:
表单修饰符主要包含三个:
.lazy
,默认情况下 v-model的每次input事件都会触发数据的同步,.lazy
可以转换为 change事件.number
,默认情况下用户输入的值永远都是字符串,我们可以给v-model加上.number
后会底层会使用来 Number() 实现转换 (但是又不能使用),如果无法转换输出原值.trim
,自动过滤用户输入首位的空格
Vue中使用过滤器(Filters)来渲染数据是一种很有趣的方式,他不能替代Vue中的
methods
、computed
或者watch
,因为过滤器不改变真正的data
,而只是改变渲染的结果,并返回过滤后的版本。在很多不同的情况下,过滤器都是有用的,比如尽可能保持API响应的干净,并在前端处理数据的格式。在你希望避免重复和连接的情况下,它们也可以有效地封装成可重用代码块背后的所有逻辑。不过,在Vue 2.0中已经没有内置的过滤器了,我们必须要自己来构建它们。过滤器只能用于插值表达式中。
全局过滤器是通过 Vue.filter()的方式来定义的过滤器,这种方式定义的过滤器可以被所有的Vue实例使用。
案例:
- 实现数字转换为美元的表示方式。
- 实现数字转成两位有效数字并转换成美元的表示形式。
局部过滤器是定义在Vue实例中,只能在指定的实例中才能使用。
案例:文章内容超过50个字符,使用…来替代。
除了核心功能默认内置的指令,Vue也允许注册自定义指令。有的情况下,对普通 DOM 元素进行底层操作,这时候就会用到自定义指令绑定到元素上执行相关操作。**自定义指令分为:** `全局指令` 和 `局部指令`,当全局指令和局部指令同名时以局部指令为准。 自定义指令**常用**钩子函数有: * bind:在**指令**第一次绑定到元素时调用 * inserted:被绑定**元素**插入父节点时调用 (仅保证父节点存在,但不一定已被插入文档中) * update:数据更新时调用
Vue.directive('指令名字', {
bind: function (el) {
console.log('bind...');
},
inserted: function (el, binding) {
console.log('insert ... ');
},
update: function () {
console.log('update ... ');
}
})
实现打开页面时候,input框自动获得焦点。
<body>
<div id="app">
<input v-focus>
div>
<script>
Vue.directive('focus', {
inserted: function (el) {
el.focus()
}
})
new Vue({
el: '#app',
data() {
return {
process: 'delete'
}
}
})
script>
body>
实现按钮根据某个给定的值是否展示(权限控制)
<body>
<div id="app">
<button v-permission:delete="ps">删除button>
<button v-permission:edit="ps">编辑button>
<button v-permission:export="ps">导出button>
<button v-permission:show="ps">展示button>
div>
<script>
new Vue({
el: '#app',
data() {
return {
ps: ['delete', 'edit', 'show']
}
},
directives: {
permission: {
inserted(el, binding) {
// 获取按钮所需的权限, v-permission冒号后的内容
let args = binding.arg
let value = binding.value
if (value.indexOf(args) < 0) {
el.remove()
}
}
}
}
})
script>
body>
前端需要经常性的从服务器端获取数据,也就是必须要发送网络请求,Vue没有内置的网络模块,它需要借助于第三方的网络处理模块,Vue官方比较推荐的是使用axios。当然我们也可以采用jquery的Ajax模块,和H5内置的
fetch
方法均能实现。
// GET请求
fetch('http://localhost:8081/goods?id=456')
.then(res => res.json())
.then(res => console.log(res))
// POST请求,发送json数据
fetch('http://localhost:8081/goods?id=456', {
method: 'post',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({a: 'X', 'y': 'Z'})
}).then(res => res.json())
.then(res => console.log(res))
// post请求,发送表单数据
fetch('http://localhost:8081/goods', {
method: 'post',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: 'a=Z&x=LMN'
}).then(res => res.json())
.then(res => console.log(res))
GET请求
// GET请求
axios.get('http://localhost:8081/goods?id=456')
.then(res => console.log(res.data))
// 可以使用如下形式携带参数
axios.get('http://localhost:8081/goods', {
params: {
id: 45678
}
}).then(res => console.log(res.data))
POST请求
// post请求,默认发送json数据
axios.post('http://localhost:8081/goods', {
a: 'Z',
x: 'LMN'
}).then(res => console.log(res.data))
// post请求发送表单数据(一个一个的数据)
axios({
method: 'post',
url: 'http://localhost:8081/goods',
data: {
a: 'Z',
x: 'LMN'
},
transformRequest: [function (data) {
let ret = ''
for (let it in data) {
ret += encodeURIComponent(it) + '=' + encodeURIComponent(data[it]) + '&'
}
return ret
}],
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
}).then(res => console.log(res.data))
请求的拦截
axios.interceptors.request.use(config => {
config.headers['token'] = 'abclmnxyz'
return config
})
请求的拦截
axios.interceptors.response.use(response => {
console.log(response);
return response
}, err => {})
Vue实例属性和方法就是可以通过生成的对象来调用的属性和方法:
$refs
$data
: 了解
$options
: 了解
$set()/Vue.set()
:了解
$on
$emit
模块化这个概念最早是由社区发起的,叫做CommonJS模块化规范,然后最早由NodeJS所采纳并推广。也反向的推动的ES官方模块化发展,但是在众多的打包工具中,其实还是将ES的模块化规范转换为CommonJS模块化规范。ES6模块化规范的格式为:
export [default]
import xxx from '模块'
普通的导出,可以导出多个属性或者方法,那么在引入的时候,必须使用如下语法:
import {} from '模块路径'
function add(a, b) {
return a + b
}
function output(string) {
console.log(string);
}
// 普通导出,需要导出一个对象
export {
add,
output
}
<script type="module">
// 必须使用结构的方式引入方法或者属性
import { add, output } from './components/index.js'
console.log(add(3, 4));
output('hello world')
script>
默认导出,只能导出一个方法\属性\对象;就算默认导出的是对象,也不能使用解构的方式来实现导入;
function add(a, b) {
return a + b
}
// 默认导出
export default add
import fn from './components/index.js'
console.log(fn(3, 4));
function add(a, b) {
return a + b
}
function output(string) {
console.log(string);
}
// 默认导出
export default {
add,
output
}
<script type="module">
// 不能使用结构的方式导入,因为这种方式不用obj,我们是无从知道它的内容的
import obj from './components/index.js'
// import {add, output} from './components/index.js' // ***错误***
console.log(obj.add(3, 4))
obj.output('hello world')
script>
组件是可复用的 Vue 实例,且带有一个名字,组件是可复用的 Vue 实例,所以它们与
new Vue
接收相同的选项,例如data
、computed
、watch
、methods
以及生命周期钩子等。定义的方式如下:
局部组件,就是只能在某个VUE对象内部才能使用的组件,虽然在定义的时候我们只是按照对象的方式在定义,但是当我们将其纳入到VUE对象的
compoments
中后,它就成为了一个Vue实例,只能在引入的Vue实例内部使用。
<body>
<div id="app">
<first-child info="组件附加信息">first-child>
div>
<script>
const firstChild = {
props: ['info'],
template: `
{{title}}
{{info}}
`,
data() {
return {
title: '这是一个私有的组件XXX'
}
},
methods: {
clickBtn() {
console.log('按钮被点击了');
}
}
}
new Vue({
el: '#app',
components: {
'first-child': firstChild,
}
})
script>
body>
结合着ES6的模块化,完全可以将子组件抽取出来放到一个单独的文件,然后导出。
FirstComponent.js
const template = `
{{title}}
{{info}}
`
export default {
props: ['info'],
template: template,
data() {
return {
title: '这是一个私有的组件XXX'
}
},
methods: {
clickBtn() {
// this.$emit() 调用父组件
this.$emit('onbtnclick')
}
}
}
index.html
<body>
<div id="app">
<first-component @onbtnclick="parentHandler" info="组件附加信息">first-component>
div>
<script type="module">
import FirstComponent from './components/FirstComponent.js'
new Vue({
el: '#app',
components: {
FirstComponent,
},
methods: {
parentHandler() {
console.log('父组件方法触发了');
}
}
})
script>
body>
组件中除了可以标签的默认事件外,还可以给组件使用
$on
的方式绑定自定义的事件,然后通过$emit
来触发这个事件:1. 绑定事件的语法为:`this.$on('自定义事件名称', 事件处理函数)`
- 触发函数的语法为:
this.$emit('自定义事件名称', [参数一, 参数二....])
<body>
<div id="app">
<button @click="clickBtn">按钮button>
div>
<script>
const vm = new Vue({
el: '#app',
mounted() {
this.$on('my-handler', this.handler)
},
methods: {
handler(val) {
console.log('事件被通过 $emit 的方式给触发了', val);
},
clickBtn() {
this.$emit('my-handler', '参数')
}
}
})
script>
body>
在子组件的标签上通过 @事件名="父组件函数"的方式与 $on('事件名') 的效果是一样的,也就是给子组件绑定了一个事件,而事件处理函数就是父组件的函数
<body>
<div id="app">
<child @child-event="handler">child>
div>
<script>
const child = {
template: `
`,
methods: {
clickBtn() {
this.$emit('child-event', '参数')
}
}
}
const vm = new Vue({
el: '#app',
components: {
child
},
methods: {
handler(val) {
console.log('事件被通过 $emit 的方式给触发了', val);
}
}
})
script>
body>
案例:1.评论案例。
评论案例 |
---|
使用vue-cli是一个快速创建vue项目的脚手架,提供了可选的模板,需要依赖Node环境,安装的命令如下所示:
# 安装 vue/cli
npm i @vue/cli -g
#查看 vue/cli 的版本
vue -V
# 创建项目
vue create vue-day4
vue/cli创建的项目结构 |
---|
- 将
shell-chrome.rar
解压 (解压选择"解压到当前文件夹"),- 打开Chrome浏览器 -> 菜单(三个点) -> 更多工具 -> 扩展程序
- 确保右上角的 “开发者模式” 是打开状态(如果打开状态可以看到三个按钮)
- 点击 “加载已解压的扩展程序”,选中第一步解压后的文件夹即可。
chrome-vue-tools工具 |
---|
1.重写评论案例;2. todos案例;
todos案例 |
---|
我们可以通过组件添加属性的方式向子组件传递常用的数据类型(String、Number、Array、Function、Object),在子组件中使用 props 来接收这些属性,
接收过来的属性被直接纳入到子组件实例中,可以直接使用this的方式来调用
。如果属性没有使用props来接收,那么这个属性会挂载到根元素上。
App.vue
Child.vue
这是子组件
Vue为不允许子组件改变通过属性传递到下来的数据的
指向
,原因是因为如果众多的子组件都可以直接修改父组件的值,那么将导致组件的状态难以维护。但是对于父组件值的修改,会流向子组件,这就是单向数据流,例如上面的案例,我们在子组件的mounted()
方法中有如下的代码:
mounted() {
// console.log(this.arr)
// console.log(this.title, this.num, this.obj)
// console.log(this.id, this.name)
// this.foo()
this.title = '子组件改变title的值' // 报错,因为这是改变了数据的引用
this.id = '78' // 报错,因为这是改变了数据的引用
this.obj.name = '田七' // 不报错,引用并没有改变,这是改变了数据
this.arr.push(6) // 不报错,引用并没有改变,这是改变了数据
},
在上述的几个案例中,可以通过 props 的方式给子组件传递参数,但是在多人团队协作开发的时候,自己开发的组件,要供其他人使用,如果同事给定的参数不符要求,极有可能会导致我们的代码运行出错,那么属性的校验就是一个很好的 “甩锅” 的方式。
export default {
props: {
num: {
required: true, // 这个是必须要传递的
type: Number, // 类型必须是数字,而不是转换得到的
validator(val) { // 可以对值进行校验,是否
return val > 1
},
},
title: String,
}
}
动态组件就是根据需要将特定的组件渲染到
这个组件上,例如要实现如下的移动端布局
动态组件效果 |
---|
在如上的案例中,我们会发现当我们每次点击 tab 的时候,对应的组件会被重新渲染,当用户不停的进行组件的切换,会导致DOM不停的重绘,而且也会加重服务器端的负载。那么我们可不可以将组件缓存起来,不用每次切换都要重新渲染,那么Vue提供了一个叫做
的标签可以帮我们来解决这个问题,使用方式如下所示:
然而上面的这种方式会导致所有的组件都会缓存,每次用户切换还是呈现之前的数据,但是有些业务我们需要最新的数据,那么Vue的生命周期中提供了两个与
keep-alive
对应的函数:
- activated,当缓存的页面再次展示出来的时候,该方法被触发
- deactivated,当缓存的页面被隐藏的时候,该方法触发
export default {
mounted() {
console.log('主组件被渲染了')
},
activated() {
console.log('缓存被激活了')
},
deactivated() {
console.log('缓存被影藏了')
},
}
现在又有另外一个需求,我们只希望缓存其中一个组件,其他的不缓存那么该如何处理了,可以使用
keep=alive
提供的include
属性来完成,但是对应的组件必须有 name 值,这是组件name属性最有用的一个地方
。
组件间传值分为以下几种情况的传值:
- 父子组件的传值
- 兄弟组件间的传值
- 祖先与后代元素间的传值
针对以上情况那么都会有相应的解决方案,如下表所示:
需要进行传值的组件关系 | 解决方案 |
---|---|
父 -> 子(祖先 -> 后代) | props 、消息总线 、发布订阅 、provide/inject |
子 -> 父(后代 -> 祖先) | 消息总线 、发布订阅 、$refs 、事件绑定($on/$emit) |
兄弟组件 | 消息总线 、发布订阅 |
消息总线的基本思想就是,创建一个Vue实例,然后挂载在Vue.prototype上,然后通过
$on
和$emit()
的方式来绑定和触发方法
<body>
<div id="app">
<button @click="clickBtn">按钮button>
div>
<script>
//创建Vue的实例,主要目的就是用作消息总线,然后通过 $on绑定事件,通过$emit来触发事件
Vue.prototype.Bus = new Vue()
new Vue({
el: '#app',
mounted() {
// 绑定一个名为 one_event 的事件
this.Bus.$on('one_event', () => {
console.log('事件被触发了');
})
},
methods: {
clickBtn() {
// 触发在 Bus 上绑定的 one_event 事件
this.Bus.$emit('one_event 事件')
}
}
})
script>
body>
可以在自组件上定义一个
ref
属性,例如:
然后在父组件中通过如下代码获取:
this.$refs.ch.$data.子组件属性名
订阅与发布(Publish发布、subscribe订阅)可以应用到各种场景之下,不仅仅是父子组件之间,也可以应用于兄弟组件之间,需要安装
pubsub-js
这个模块,使用方式如下:
import PubSub from 'pubsub-js'
// 订阅消息
PubSub.subscribe('channel1', (msg, value) => {
this.changeValue(value);
});
//===============================================================
import PubSub from 'pubsub-js'
// 发布消息
PubSub.publish('channel1', '张某人');
在Vue2.2版本之后新增了两个属性
provide
和inject
两个属性,主要在开发高阶插件/组件库时使用。并不推荐用于普通应用程序代码中。 在某个祖先组件中可以使用
provide
属性提供除了data中以外的数据,然后在后代组件中可以使用inject
的方式来获取这些数据。
<script>
const child = {
// inject: ['users'], // 直接注入祖先元素
inject: {
// us: 'users' // 可以修改别名
// 根据名字 person 查找,如果没有找到, 就采用默认值
/**
person: {
default: { name: '李四', age: 18 }
}
*/
person: {
from: 'users', // 更换别名为 person, 根据users查找,如果没有找到使用默认值
default: { name: '李四', age: 18 }
}
},
template: `
{{person}}
`
}
new Vue({
el: '#app',
provide: {
users: { name: '张三', age: 10 }
},
components: {
child
}
})
script>
在很多的场景下,因为子组件某个区域在不同的场景下需要展示不同的内容,就可以用到插槽,类似于电脑的内存插槽一样,可以插内存大小不同的内存卡,以满足工作生活需求。
<div>
<h3>子组件h3>
<slot>slot>
div>
<child>
<p>想要传递给子组件的内容p>
child>
所谓的具名插槽就是我们可以给插槽取个名字,然后将某些内容按照对应插槽的名字渲染到子组件中。
<div>
<h3>子组件h3>
<slot name="first">slot>
div>
<child>
<p slot="first">想要传递给子组件的内容p>
child>
我们还可以将子组件的数据通过属性的方式传递给父组件,然后父组件可以通过
slot-scope
的方式获取,取得的是一个对象。
子组件
想要传递给子组件的内容
{{num}} -- {{msg}}
在Vue的2.6.0之后的版本中,已经新推出了
slot
和slot-scope
的替代方案v-slot
,slot
和slot-scope
在后续的版本中将会移除,但是目前antd
和ElementUI
还是采取之前的方式。
<child>
<template v-slot:first="data">
<p>
想要传递给子组件的内容<br>
{{data}}
p>
template>
child>
混入的概念和Sass中的混合器概念比较的类似,也就是我们可以将在其他组件中要使用到的方法和组件抽取出来单独放到一个组件中,然后在其他组件中引入,就像使用本地数据和方法一样,如下所示:
// mixin/index.js
export default {
data() {
return {
name: '张三'
}
},
methods: {
show() {
console.log(this.name);
}
}
}
// ================= 在其他组件中使用
import mixin from './mixin'
export default {
mixins: [mixin],
methods: {
clickBtn() {
this.show() // 跟调用本地方法一样
}
}
}
如果被混入的文件方法或者数据和组件中有相同部分,组件中会覆盖混入文件中的数据
在实际的工作中我们并不会实际的开发插件,但是我们得了解该如何来开发一款插件。
// plugin/index.js
export default (Vue) => {
Vue.prototype.output = (str) => {
console.log(str);
}
}
// ============= main.js ==============
import plugin from './plugins'
// 当我们使用 Vue.use() 的时候,会自动的将Vue传入到方法中
Vue.use(plugin)
表格中使用组件的方式循环渲染的问题。
<body>
<div id="app">
<table>
<tr is="child" v-for="u in users" :key="u.id" :id="u.id" :name="u.name">tr>
table>
div>
body>
<script>
const child = {
props: ['name', 'id'],
template: `
{{id}}
{{name}}
`
}
new Vue({
el: '#app',
data() {
return {
users: [
{ id: 1, name: '张三' },
{ id: 2, name: '李四' }
]
}
},
components: {
child
}
})
script>
渲染函数,与Vue实例的生命周期息息相关。
在实现SPA(Single Page Application,单页面应用),可以使用JS的
innerHTML
的方式,但是这种方式实现的页面不能实现回退与前进,那么既想实现SPA又能实现前进与回退,可以使用localtion.hash
和history
两种方式。
如上面所介绍实现路由有
hash
的方式,利用hashchange
方法对路径中的hash
进行监听,然后实现路由的切换。
<body>
<ul>
<li>首页li>
<li>关于li>
ul>
<div id="container">
div>
<script>
let compts = [
{ hash: '/home', page: '这是首页页面内容
' },
{ hash: '/about', page: '这是关于页面内容
' },
]
$('li').on('click', function () {
// 使用js的方式实现页面内容的变化
// $('#container').html(compts[$(this).index()].page)
location.hash = compts[$(this).index()].hash
})
$(window).on('hashchange', function () {
let hash = location.hash.substr(1) // 获取路由中的 hash 值
// _使用的是 lodash 的 api
$('#container').html(_.find(compts, item => item.hash === hash).page)
})
// 页面进入后默认展示 home页面
$(window).on('load', () => {
let { hash, page } = compts[0]
location.hash = hash
$('#container').html(page)
})
script>
body>
在H5发布后,history 新增了 如下几个 API:
pushState
: 改变location的地址
popstate
: 该方法只有在浏览器的前进后退才会触发
<body>
<ul>
<li>首页li>
<li>关于li>
ul>
<div id="container">
div>
<script>
let compts = [
{ hash: '/home', page: '这是首页页面内容
' },
{ hash: '/about', page: '这是关于页面内容
' },
]
$('li').on('click', function () {
let { hash, page } = compts[$(this).index()]
// 改变location中的 hash
history.pushState(null, null, hash)
$('#container').html(page)
})
// 页面首次打开显示的路由页面
$(window).on('load', function () {
history.pushState(null, null, '/home')
$('#container').html(compts[0].page)
})
// 当实现前进后退的时候,渲染页面
$(window).on('popstate',
function () {
let pathname = location.pathname
let specCompt = _.find(compts, item => item.hash == pathname)
$('#container').html(specCompt.page)
})
script>
body>
Vue Router 是 Vue.js 官方的路由管理器。它和 Vue.js 的核心深度集成,让构建单页面应用变得易如反掌。
要在项目中使用
vue-router
有四个步骤:
- 引入
vue-router
插件- 使用插件
- 创建路由实例并配置路由信息
- 将路由实例添加到Vue的实例中
main.js
// 引入 vue-router
import VueRouter from 'vue-router'
import Home from './views/Home'
import About from './views/About'
// 使用 VueRouter 插件
Vue.use(VueRouter)
// 添加路由信息
const router = new VueRouter({
routes: [
{ path: '/', redirect: '/home', },
{ path: '/home', component: Home },
{ path: '/about', component: About },
]
})
new Vue({
router, // 纳入到 Vue 的实例中
render: h => h(App),
}).$mount('#app')
App.vue
<template>
<div>
<ul>
<li>
<router-link to="/home">Homerouter-link>
li>
<li>
<router-link to="/about">Aboutrouter-link>
li>
ul>
<router-view>router-view>
div>
template>
有几个重要的参数:
active-class
当前激活的路由的样式tag
使用指定的标签替换默认的 a 标签exact-active-class
精确匹配路由replace
不保存当前的路由信息到历史记录中event
触发路由的事件,默认是点击事件
除了使用
创建 a 标签来定义导航链接,我们还可以借助 router 的实例方法,通过编写代码来实现,在 Vue 实例内部,可以通过
$router
访问路由实例,通过调用this.$router.push
,它等同于,编程式路由对应着如下几个方法:
- push() 请求到某个路径
- replace() 请求到某个路径,不会像history中添加新的记录
- go(n) 前进或者后退,不常用
goto(p, n) {
/**
* 通过路径的方式实现路由的跳转,catch(() => {}) 主要的目的是为了不出现当
* 用户点击重复的路由而在浏览器端出现的错误
*/
// this.$router.push(p).catch(() => { })
// 也可以通过组件的名字实现跳转
// this.$router.push({name: n}).catch(() => { })
// 通过组件的名字实现页面的跳转,但是不保存历史记录
this.$router.replace({ name: n }).catch(() => { })
}
实际生活中的应用界面,通常由多层嵌套的组件组合而成,通过在某个父级路由下添加
children
属性来包含多个子路由信息。
const router = new VueRouter({
routes: [
{ path: '/', component: Index, name: 'index' },
{
path: '/home',
component: Home,
name: 'home',
// 添加子路由信息
children: [
{ path: '', redirect: 'user' },
{ path: 'student', component: Student },
{ path: 'user', component: User }
]
},
{ path: '/about', component: About, name: 'about' },
]
})
当从一个路由跳转到另外一个路由的时候,我们往往需要携带参数,例如查询某个用户的详情信息等。那么该如何携带参数以及如何在目标组件中获取参数呢?
在 router 的配置中,我们将
path
的值是写固定写死的,然后在有些场景下path是可以是多样化的,例如要查询 id 为某个值的详细信息,id是变化的,那么就需要用到动态路由,其语法非常的简单,如下所示:
/path/:parameter
// ---------------------------路由配置-------------------------------
{ path: 'detail/:id', component: Detail, name: 'about-detail' }
// ---------------------------路由跳转-------------------------------
goto(n) {
// 如下两行代码,意思是一样的,如果不写,默认就是path
// this.$router.push(`/about/detail/${n}`).catch(() => { })
// this.$router.push({ path: `/about/detail/${n}` }).catch(() => { })
// 使用params参数的方式,必须使用组件名
this.$router.push({ name: 'about-detail', params: { id: n } })
}
// ----------------------------页面取值-------------------------------
data() {
return {
id: ''
}
},
created() {
this.id = this.$route.params.id
}
对于上面这种方式,官方更加推荐使用
props
的方式来取值,这样就实现了取值的方式就实现了与 $route的解耦。
// ---------------------------路由配置-------------------------------
{ path: 'detail/:id', component: Detail, name: 'about-detail',props: true }
// ---------------------------路由跳转-------------------------------
goto(n) {
// 如下两行代码,意思是一样的,如果不写,默认就是path
// this.$router.push(`/about/detail/${n}`).catch(() => { })
// this.$router.push({ path: `/about/detail/${n}` }).catch(() => { })
// 使用params参数的方式,必须使用组件名
this.$router.push({ name: 'about-detail', params: { id: n } })
}
// ----------------------------页面取值-------------------------------
export default {
// props中的值要与路由配置中的id对应
props: ['id']
}
query传参其实是将参数携带在url地址中
// ---------------------------路由配置-------------------------------
{ path: 'detail', component: Detail, name: 'about-detail'}
// ---------------------------路由跳转-------------------------------
goto(n) {
// 可以在路径中直接携带
// this.$router.push({ path: `/about/detail?id=${n}` })
this.$router.push({
//path: '/about/detail',
// 可以为路径也可以是名字
name: 'about-detail',
query: { id: n }
})
}
// ----------------------------组件中取值-------------------------------
created() {
this.id = this.$route.query.id
}
定义参数就是在就路由配置文件中写的固定的数据,可以使用
meta
和props
来携带
在
16.5.1
和16.5.2
中均面临一个问题,就是当用户去切换数据的时候,由于组件的复用(也就是同一个路由的时候,组件并不会销毁),就无法获取用户传递的值。可以使用watch
和beforeRouterUpdate
两种方式来实现数据的获取。
// watch 的方式实现数据切换的时候,参数的获取
watch: {
// 获取直接监听 $route 也可以
'$route.params': function (newVal) {
this.id = newVal.id
}
}
// ----------------------使用beforeRouterUpdate的方式获取----------------
// to表示到哪里去,from从哪里来,next是个函数,表示接着往下走
beforeRouteUpdate(to, from, next) {
this.id = to.params.id
next() // 一定要调用 next() 方法
}
路由守卫,就是路由变化的回调钩子函数,可以在这些函数中判定来进行一些流程的控制、权限的验证等等的工作。路由守卫有组件路由守卫和全局路由守卫。
组件路由守卫映射到几个组件生命周期函数:
beforeRouteEnter
当路由准备进入组件,此时组件还没有被创建(实例的生命周期还没有开始)
,该方法就已经执行。我们可在此处判断用户是否可以进入该页面。beforeRouteUpdate
,当在页面中更新路由的时候,该方法会被调用了,该方法使用比较的局限,就是当同一个组件下,实现路由切换。beforeRouteLeave
,当离开页面即将进入下一个路由的时候,该方法被调用,了解。
注意上面三个方法都需要调用 next() 方法实现流程的继续
// 进入到对应的路由还没到达组件
beforeRouteEnter(to, from, next) {
console.log('进入路由');
next()
},
// 路由更新
beforeRouteUpdate(to, from, next) {
console.log('更新路由');
next()
},
// 路由退出
beforeRouteLeave(to, from, next) {
console.log('退出');
next()
}
路由独享守卫是针对,某个路由进行设定的,在 VueRouter 的实例中进行添加。
{
path: '/',
component: Index,
name: 'index',
beforeEnter() {
console.log('进入路由独享守卫');
}
}
全局路由守卫是所有的路由都会执行的钩子函数,在 VueRouter 的实例中进行添加,最常用的方法为:
beforeEach
: 在路由实例中找到对应的路由就执行。
router.beforeEach((to, from, next) => {
console.log('全局路由守卫');
next()
})
Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。在之前的课程中我们介绍过可以使用
消息总线
、provide/inject
等来实现多组件数据的共享,但是这些方式都不是最优解,所以就有了Vue的状态管理Vuex
。其基本思想就是将所有的共享数据抽取出来以一个全局单例模式 管理,在这种模式下,我们的组件树构成了一个巨大的 “视图”,无论在树的哪个位置,任何组件都能获取状态和触发行为。Vuex背后的基本思想是借鉴了Flux
、Redux
等。Vuex是专门为 Vue.js 设计的状态管理库,以利用 Vue.js 的细粒度数据响应机制来进行高效的状态更新。
Vuex |
---|
通过上图我们可以总结出,vuex中总共包含5大核心内容:
state
说的直白点就是存储数据的地方。
actions
通过异步的方式更改状态值,但是不能直接更改,需要借助于mutations来更改。
mutations
通过直接同步的方式更改状态。
getters
类似于计算属性,通常是需要通过state来间接计算得到的值。
modules
一个项目中因为模块的原因,存在着各种不同的状态,需要按照模块来划分。
本节内容以对一个数组进行操作为例来进行展开。
安装,命令如下:
npm i vuex
创建
store
文件夹,然后在文件夹下创建名为index.js
的文件,文件内容如下:
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex) // 使用插件
export default new Vuex.Store({
// 实际保存数据的位置, 可以类比为组件中的 data 中的数据
state: {
users: [
{ id: 1, name: '张三', age: 10, gender: 'M' },
{ id: 2, name: '李四', age: 20, gender: 'F' },
{ id: 3, name: '王五', age: 30, gender: 'F' }
]
},
// actions中的方法的执行是异步的过程, 调用的方式为:
// this.$store.dispatch('方法名')
actions: {
addUser({ commit }, payload) {
commit('addUser', payload)
}
},
// mutations中的方式是同步的过程
mutations: {
// 添加数据, 方法第一个参数固定为state, 表示要操作的数据
// 方法的第二个参数是实际传过来的数据
addUser(state, payload) {
state.users.push(payload)
}
},
// 跟计算属性一样,但是只有get的方式
getters: {
// 统计男女数量
genderClassify: function (state) {
let classify = {}
let maleNum = state.users.reduce((sum, item) => sum += item.gender === 'M' ? 1 : 0, 0)
classify.maleNum = maleNum
classify.femaleNum = state.users.length - maleNum
return classify
},
// 统计总数
total: function (state) {
return state.users.length
}
}
})
组件中的使用:
import { mapState, mapGetters, mapActions } from 'vuex'
export default {
data() {
// 省略
},
// 可以在 mounted 和 data 中去获取数据,也可以在页面直接书写,
// 但是我们最常见的写法是在 计算属性 中去获取
computed: {
// users: function () {
// // 引用到 state 中的users数据
// return this.$store.state.users
// },
// genderClassify: function () {
// let gc = this.$store.getters.genderClassify
// console.log(gc);
// return gc
// }
...mapState(['users']),
...mapGetters(['genderClassify', 'total'])
},
methods: {
// 添加用户, 常规的调用方式
// addUser() {
// this.$store.commit('addUser', {
// id: this.id,
// name: this.name,
// age: this.age,
// gender: this.gender,
// })
// }
// ...mapMutations(['addUser']), // 引入 mutations中的方法
...mapActions(['addUser']), // 引入actions中的方法
addUserModel() {
this.addUser({
id: this.id,
name: this.name,
age: this.age,
gender: this.gender,
})
}
},
}
当一个项目结构非常大的时候,有很多不同的状态需要进行管理,那么必须要分模块进行处理,这就是modules的作用。如下的案例中我们对
17.1
中的案例进行一定的改造,改造后的目录结构如下所示:
modules |
---|
count/index.js
export default {
// namespaced 必须要添加,表示以文件夹作为模块间的区别
namespaced: true,
state: {
count: 0
},
actions: {
calculateCount({ commit }, payload) {
commit('calculateCount', payload)
}
},
mutations: {
calculateCount(state, payload) {
state.count += payload
}
}
}
user/index.js
export default {
namespaced: true,
// 实际保存数据的位置, 可以类比为组件中的 data 中的数据
state: {
users: [
{ id: 1, name: '张三', age: 10, gender: 'M' },
{ id: 2, name: '李四', age: 20, gender: 'F' },
{ id: 3, name: '王五', age: 30, gender: 'F' }
]
},
// actions中的方法的执行是异步的过程, 调用的方式为:
// this.$store.dispatch('方法名')
actions: {
addUser({ commit }, payload) {
commit('addUser', payload)
}
},
// mutations中的方式是同步的过程
mutations: {
// 添加数据, 方法第一个参数固定为state, 表示要操作的数据
// 方法的第二个参数是实际传过来的数据
addUser(state, payload) {
state.users.push(payload)
}
},
getters: {
// 统计男女数量
genderClassify: function (state) {
let classify = {}
let maleNum = state.users.reduce((sum, item) => sum += item.gender === 'M' ? 1 : 0, 0)
classify.maleNum = maleNum
classify.femaleNum = state.users.length - maleNum
return classify
},
// 统计总数
total: function (state) {
return state.users.length
}
}
}
store/index.js
import Vue from 'vue'
import Vuex from 'vuex'
import user from './user'
import count from './count'
Vue.use(Vuex)
export default new Vuex.Store({
modules: {
user,
count
}
})
App.vue
import { mapState, mapGetters, mapActions } from 'vuex'
export default {
data() {
// 代码省略
},
computed: {
// 第一个参数是模块名,也就是文件夹的名字,其他也是同样的道理
...mapState('user', ['users']),
// ...mapGetters(['genderClassify', 'total'])
...mapGetters('user', ['genderClassify', 'total'])
},
methods: {
// ...mapMutations('user', ['addUser']), // 引入 mutations中的方法
...mapActions('user', ['addUser']), // 引入actions中的方法
addUserModel() {
this.addUser({
id: this.id,
name: this.name,
age: this.age,
gender: this.gender,
})
}
},
}