Vue 介绍
简介
Vue (读音 /vjuː/,类似于 view) 是一套用于构建用户界面的渐进式框架。与其它大型框架不同的是,Vue 被设计为可以自底向上逐层应用,Vue 的核心库只关注视图层。
官网
- 英文官网: https://v2.vuejs.org/
- 中文官网: https://v2.cn.vuejs.org/
特点
- 遵循 MVVM 模式
- 编码简洁, 体积小, 运行效率高, 适合移动/PC 端开发
- 它本身只关注 UI, 也可以引入其它第三方库开发项目
与其它 JS 框架的关联
- 借鉴 Angular 的模板和数据绑定技术
- 借鉴 React 的组件化和虚拟 DOM技术
Vue 周边库
- vue-cli: vue 脚手架
- vue-resource
- axios
- vue-router: 路由
- vuex: 状态管理
- element-ui: 基于 vue
Vue 核心
简单使用
<body>
<div id="app">
{{ message }}
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<script>
Vue.config.productionTip = false;
const vm = new Vue({
el: '#root',
data: {
message: 'Hello Vue!'
}
});
</script>
</body>
插值语法
实例
<span>Message: {{ xxx }}</span>
xxx 会作为 js 表达式解析
功能
解析标签体内容
指令语法
实例
<a v-bind:href="xxx">点击跳转</a>
xxx 会作为 js 表达式解析
功能
解析标签属性、解析标签体内容、绑定事件…
数据绑定
- 单向绑定: 数据只能从 data 流向页面
- 双向绑定: 数据不仅能从 data 流向页面,还能从页面流向 data
v-bind
指令
语法: v-bind:href="xxx"
简写: :href="xxx"
作用: 创建单向数据绑定
v-model
指令
语法: `v-model:value=“xxx”
备注: v-model:value="xxx"
可简写为 v-model="xxx"
, 因为 v-model 默认收集 value 值
作用: 创建双向数据绑定
el 和 data 的两种写法
const vm = new Vue({
// el: '#root', // el 的第一种写法
// data 的第一种写法
// data: {
// msg: 'Hello Vue!'
// }
// data 的第二种写法
data() {
return: {
msg: 'Hello Vue!'
}
}
});
vm.$mount('#root'); // el 的第二种写法
MVVM 模型
- M: 模型(Model) : 对应 data 中的数据
- V: 视图(View) : 模板
- VM: 视图模型(ViewModel) : Vue 实例对象
Vue 中的数据代理
https://www.bilibili.com/video/BV1Zy4y1K7SH?p=13
v-on
指令
语法: v-on:xxx="func"
| v-on:xxx="func(param)"
- xxx: 事件名
- func: 回调函数
- param: 参数
简写: @xxx="func"
注意
- 事件的回调函数需要配置在 methods 对象中,最终会在 vm 上
- methods 中配置的函数尽量不要使用箭头函数,使用箭头函数会改变 this 指向
- methods 中配置的函数都是被 Vue 所管理的函数,this 的指向是 vm 或 组件实例对象
@click="demo"
和@click="demo($event)"
效果一直,但后者可以传参
事件修饰符
.prevent
* : 阻止默认事件.stop
* : 阻止事件冒泡.once
* : 事件只触发一次.{keyCode | keyAlias}
- 只当事件是从特定键触发时才触发回调.capture
: 启用事件捕获模式.self
: 只有event.target
是当前操作的元素时才触发事件.passive
: 事件的默认行为立即执行,无需等待事件回调执行完毕
常用 keyCode: enter
delete
esc
space
tab
up
down
left
right
计算属性 computed
原理
底层借助了 Object.defineProperty()
提供的 getter 和 setter
使用场景
要使用的属性不存在,要通过已有属性计算得来
优点
与 methods 事项相比,内部有缓存机制(复用),效率更高,调试方便
备注
- 计算属性最终会出现在 vm 上,直接读取使用即可
- get 函数什么时候执行?
- 初次读取时会执行一次
- 依赖的数据发生改变时会被再次调用
- 如果计算属性要被修改就必须写 set 函数去相应修改,且 set 中要引起计算时依赖的数据发生改变
实例
<body>
<div id="root">
First Name: <input type="text" v-model="firstName" />
<br />
Middle Name: <input type="text" v-model="middleName" />
<br />
Last Name: <input type="text" v-model="lastName" />
<hr>
Full Name: <input type="text" v-model="fullName" />
</div>
<script src="../js/vue.js"></script>
<script>
Vue.config.productionTip = false;
const vm = new Vue({
el: '#root',
data: {
firstName: 'Tenma',
middleName: 'Gabriel',
lastName: 'White'
},
computed: {
// 完整写法
/* fullName: {
get() {
return this.firstName + ' · ' + this.middleName + ' · ' + this.lastName;
},
set(value) {
nameArr = value.split(' · ');
this.firstName = nameArr[0];
this.middleName = nameArr[1];
this.lastName = nameArr[2];
}
} */
fullName() {
return this.firstName + ' · ' + this.middleName + ' · ' + this.lastName;
}
}
})
</script>
</body>
监视属性 watch
当被监视的属性变化时,执行回调函数
深度监视
Vue 中的 watch 默认不检测对象内部值的改变(单层),配置 deep: true
可以检测对象内部值的改变(多层)
备注
- Vue 自身可以监测对象内部值的改变,但 Vue 提供的 watch 默认不可以
- 使用 watch 时根据数据的具体结构,决定是否采用深度监视
computed 和 watch 之间的区别
- computed 能完成的功能,watch 都能完成
- watch 能完成的功能,computed 不一定能完成(例如:watch 可以进行异步操作)
实例
<div id="root">
<h2>今天天气很{{ info }}</h2>
<button @click="isHot = !isHot">切换天气</button>
<hr />
<span>a: {{ num.a }}</span><button @click="num.a++">增加a</button>
<hr />
<span>d: {{ num.b.c.d }}</span><button @click="num.b.c.d++">增加d</button>
</div>
<script src="../js/vue.js"></script>
<script>
Vue.config.productionTip = false;
const vm = new Vue({
el: '#root',
data: {
isHot: true,
num: {
a: 1,
b: {
c: {
d: 100
}
}
}
},
watch: {
// isHot: {
// immediate: true, // 初始化时调用一次handler
// handler(newValue, oldValue) {
// console.log('old value: ' + oldValue);
// console.log('new value: ' + newValue);
// console.log('isHot被改变了');
// }
// },
num: {
deep: true, // 深度监视
handler() {
console.log('num或num内的属性的值被改变了');
}
},
// watch属性简写(不需要处理handler之外的其他配置项时可以使用)
// isHot(newValue, oldValue) {
// // 直接写handler的内容
// console.log('old value: ' + oldValue);
// console.log('new value: ' + newValue);
// console.log('isHot被改变了');
// }
},
computed: {
info() {
return this.isHot ? '炎热' : '凉爽';
}
},
methods: {
// changeWeather() {
// this.isHot = !this.isHot;
// }
}
})
// watch在vm外部的写法
// vm.$watch('isHot', {
// handler(newValue, oldValue) {
// console.log('=========watch=========');
// console.log('old value: ' + oldValue);
// console.log('new value: ' + newValue);
// console.log('isHot被改变了');
// }
// });
// 简写
// watch在vm外部的写法
vm.$watch('isHot', function (newValue, oldValue) {
console.log('=========watch=========');
console.log('old value: ' + oldValue);
console.log('new value: ' + newValue);
console.log('isHot被改变了');
});
</script>
</body>
两个重要的小原则
- 所有被 Vue 所管理的函数,最好写成普通函数,这样
this
的指向才是 vm 或 组件实例对象 - 所有不被 Vue 所管理的函数(定时器的回调函数、AJAX 的回调函数、Promise 的回调函数等)最好写成箭头函数,这样
this
的指向才是 vm 或 组件实例对象
绑定样式
class
样式
写法: :class="xxx"
xxx 可以是空符串、对象、数组
- 字符串写法适用于: 类名不确定,要动态获取
- 对象写法适用于: 要绑定多个样式,个数不确定,名字也不确定
- 数组写法适用于: 要绑定多个样式,个数确定,名字也确定,但不确定用不用
style
样式
:style="{ fontSize: xxx }"
其中 xxx 是动态值:style="[a, b]"
其中 a、b 是样式对象
v-if
及其相关指令
语法: v-if="xxx"
v-else-if="xxx"
v-else="xxx"
xxx 均为 js 表达式
特点
- 适用于切换频率较低的场景
- 不展示的 DOM 元素直接被移除
实例
<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="xxx"
xxx 为 js 表达式
特点
- 适用于切换频率较高的场景
- 不展示的 DOM 元素未被移除,仅仅是使用样式隐藏掉
v-for
指令
语法: v-for="item in items"
或 v-for="(item, index) in items"
key
的作用
key 是虚拟DOM对象的标识,当数据发生变化时,Vue会根据新数据生成新的虚拟DOM,随后 Vue 进行 新虚拟DOM 和 旧虚拟DOM 的 diff 比较,比较规则如下:
- 旧虚拟DOM 中找到了与 新虚拟DOM 相同的 key:
- 若 虚拟DOM 中内容没变,直接使用之前的 真实DOM
- 若 虚拟DOM 中内容变了,生成新的 真实DOM,随后替换掉页面中之前的 真实DOM
- 旧虚拟DOM 中未找到与 新虚拟DOM 相同的 key:
- 创建新的真实DOM,随后渲染到页面
使用 index
作为 key
可能引发的问题:
- 若对数据进行逆序添加、逆序删除等破坏顺序的操作: 会产生没有必要的 真实DOM 更新 ==> 界面效果没问题,但效率变低
- 如果结构中还包含输入类的DOM: 会产生错误DOM更新 ===> 界面有问题
实例
<body>
<div id="root">
<ul>
<li v-for="(p, index) in persons" :key="index">{{ p.id }} -- {{ p.name }} -- {{ p.age }}</li>
</ul>
</div>
<script src="../js/vue.js"></script>
<script>
Vue.config.productionTip = false;
const vm = new Vue({
el: '#root',
data: {
persons: [
{ id: 001, name: '张三', age: 18 },
{ id: 002, name: '李四', age: 19 },
{ id: 003, name: '王五', age: 20 }
]
}
})
</script>
</body>
数据监测
Vue 会监视 data 中所有层次的数据
原理
监测对象中的数据
通过 setter 实现监视,且要在 new Vue
时就传入要检测的数据。对象中后追加的属性,Vue 默认不做响应式处理。如需后添加的属性保持响应式,需要使用如下 API:
Vue.set(target, propertyName/index, value)
vm.$set(target, propertyName/index, value)
监测数组中的数据
通过包裹数组更新元素的方法实现:
- 调用原生对应的方法对数组进行更新
- 重新解析模板,进而更新页面
修改 Vue 维护的数据中数组的某个元素一定要使用如下方法:
push()
pop()
shift()
unshift()
splice()
sort()
reverse()
Vue.set()
vm.$set()
注意:Vue.set()
和 vm.$set()
不能给 vm 或 vm的根数据对象添加属性
简易模拟 Vue 数据监测对象(单层)
const data = {
msg: 'Test',
num: 100
}
// 创建一个监视的实例对象,用于监视data中属性的变化
const obs = new Observer(data);
// 准备一个vm实例对象
const vm = {}
vm._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;
}
});
});
}
过滤器 filters
实例
<body>
<div id="root">
<!-- computed实现 -->
<h2>当前的时间是:{{ formattedTime }}</h2>
<!-- methods实现 -->
<h2>当前的时间是:{{ getTime() }}</h2>
<!-- 过滤器实现 -->
<h2>当前的时间是:{{ time | timeFormatter }}</h2>
<!-- 过滤器的传参 -->
<h2>当前的时间是:{{ time | timeFormatter('YYYY_MM_DD') }}</h2>
<!-- 过滤器的串联 -->
<h2>当前的时间是:{{ time | timeFormatter('YYYY_MM_DD') | slice4 }}</h2>
</div>
<script src="../js/vue.js"></script>
<script src="../js/dayjs.min.js"></script>
<script>
Vue.config.productionTip = false;
// 全局过滤器
Vue.filter('slice4', function (val) {
return val.slice(0, 4);
})
const vm = new Vue({
el: '#root',
data: {
time: 1639724107045
},
computed: {
formattedTime() {
return dayjs(this.time).format('YYYY年MM月DD日 HH:mm:ss');
}
},
methods: {
getTime() {
return dayjs(this.time).format('YYYY年MM月DD日 HH:mm:ss');
}
},
// 局部过滤器
filters: {
timeFormatter(val, str = "YYYY年MM月DD日 HH:mm:ss") {
return dayjs(val).format(str);
},
slice4(val) {
return val.slice(0, 4);
}
}
})
</script>
</body>
v-text
指令
作用: 向其所在的节点中渲染文本内容
与插值语法的区别: v-text
会替换掉节点中的内容,{{ xxx }}
则不会
v-html
指令
作用: 向其所在的节点中渲染包含 html 结构的内容
与插值语法的区别:
v-html
会替换掉节点中的内容,{{ xxx }}
则不会v-html
可以识别 html 结构
注意: v-html
有安全性问题,在网站上动态渲染任意 html 内容十分危险,容易遭受 xss 攻击
v-cloak
指令
没有值,本质是一个特殊属性,Vue 实例创建完毕并接管容器后,会删除掉 v-cloak
属性。使用 css 配合 v-cloak
可以解决网速慢时网页展示出模板语法的问题
v-once
指令
v-once
所在的节点在初次渲染后就被视为静态内容了,以后数据的改变不会引起 v-once
所在结构的更新,可用作优化性能
v-pre
指令
跳过其所在节点的编译过程,可利用它跳过没有使用指令语法、没有使用插值语法的节点,会加快编译
自定义指令
语法
局部指令
new Vue({
directives: { 指令名: 配置对象 }
});
// 或
new Vue({
directives: { 指令名: 回调函数 }
});
全局指令
Vue.directive(指令名, 配置对象);
// 或
Vue.directive(指令名, 回调函数);
配置对象中常用的三个回调:
bind
: 指令与元素成功绑定时调用inserted
: 指令所在元素被插入页面时调用update
: 指令所在模板结构被重新解析时调用
备注
- 指令定义时指令名不加
v-
,但使用时要加v-
- 指令名如果是多个单词,要使用 kebab-case 而非 camelCase 命名
实例
<body>
<div id="root">
<h2>n: <span v-text="n"></span></h2>
<h2>n * 10: <span v-enhance-number="n"></span></h2>
<button @click="n++">n++</button>
<input type="text" v-fbind="n">
</div>
<script src="../js/vue.js"></script>
<script>
Vue.config.productionTip = false;
const vm = new Vue({
el: '#root',
data: {
n: 99,
},
directives: {
enhanceNumber(element, binding) {
// console.log(element);
// console.log(binding);
element.innerText = binding.value * 10;
},
// 'enhance-number'(element, binding) {
// // console.log(element);
// // console.log(binding);
// element.innerText = binding.value * 10;
// },
fbind: {
bind(element, binding) {
// console.log('bind');
element.value = binding.value;
},
inserted(element, binding) {
// console.log('inserted');
element.focus();
},
update(element, binding) {
// console.log('update');
element.value = binding.value;
}
}
}
})
</script>
</body>
生命周期
常用的生命周期钩子
mounted
: 发送ajax请求、启动定时器、绑定自定义事件、订阅消息等【初始化操作】beforeDestroy
: 清除定时器、解绑自定义事件、取消订阅消息等【收尾工作】
关于销毁 Vue 实例
- 销毁后借助 Vue 开发者工具看不到任何信息
- 销毁后自定义事件会失效,但原生DOM事件依然有效
- 一般不会在 beforeDestroy操作数据,因为即便操作数据,也不会再触发更新流程了
Vue 组件化编程
模块与组件、模块化与组件化
模块
理解
向外提供特定功能的 js 程序,一般就是一个 js 文件
为什么需要?
js 文件很多很复杂
模块可以: 复用 js、简化 js 的编写、提高 js 运行效率
组件
理解
用来实现局部(特定)功能效果的代码集合(html/css/js/image……)
为什么需要?
一个界面的功能很复杂
组件可以: 复用编码、简化项目编码、提高运行效率
模块化
当应用中的 js 都以模块来编写的,那这个应用就是一个模块化的应用
组件化
当应用中的功能都是多组件的方式来编写的,那这个应用就是一个组件化的应用
为什么需要组件化
非单文件组件
使用
- 定义(创建)组件
- 使用
Vue.extend(options)
创建,其中options
与new Vue(options)
时传入的options
几乎一样,但也有区别:el
不要写 —— 最终所有组件都要经过一个vm
的管理,由vm
中的el
决定服务哪个容器data
必须写成函数形式 —— 避免组件被复用时,数据存在引用关系
- 备注:使用
template
可以配置组件结构
- 使用
- 注册组件
- 局部注册:
new Vue
时传入components
选项 - 全局注册:
Vue.component('组件名', 组件)
- 局部注册:
- 编写组件标签
<test></test>
- 备注:自闭和标签(
<test />
)的写法在非单文件组件中不适用,但在单文件组件中可用
注意
- 组件名:
- 一个单词组成:
- 首字母小写:school
- 首字母大写:School
- 多个单词组成:
- kebab-case 命名:my-school
- CamelCase 命名:MySchool(需要 Vue 脚手架支持)
- 备注:
- 组件名尽可能回避 HTML 中已有标签名称,如:h2、H2
- 可以使用
name
配置项指定组件在开发者工具中呈现的名字
- 一个单词组成:
- 组件标签:
- 双标签形式:
<school></school>
- 单标签自闭合形式:
<school />
(需要 Vue 脚手架支持)
- 双标签形式:
- 简写:
const school = Vue.extend(options);
可简写为const school = options;
VueComponent
构造函数
- 注册的组件本质是一个名为
VueComponent
的构造函数,它是由Vue.extend
生成的 - 写完组件标签后,Vue 解析时会帮忙创建该组件的实例对象(即 Vue 帮助执行
new VueComponent(options)
) - 每次调用Vue.extend,返回的都是一个全新的
VueComponent
this
的指向:- 组件配置中:
data
函数、methods
中函数、watch
中函数、computed
中函数 其中的this
均是VueComponent 实例对象
new Vue(options)
配置中:data
函数、methods
中函数、watch
中函数、computed
中函数 其中的this
均是Vue 实例对象
- 组件配置中:
重要
一个重要的内置关系:VueComponent.prototype.__proto__ === Vue.prototype
为什么要有这个关系:让组件实例对象可以访问到 Vue 原型上的属性、方法
单文件组件
不使用 vue-cli
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="root"></div>
<script src="../js/vue.js"></script>
<script src="main.js"></script>
</body>
</html>
main.js
import App from './Vue.vue';
new Vue({
el: '#root',
template: `<App></App>`,
components: { App }
});
App.vue
<template>
<div>
<School />
</div>
</template>
<script>
import School from "./School.vue";
export default {
name: "App",
components: {
School
}
};
</script>
Team.vue
<template>
<div class="demo">
<h2>Team Name: {{ name }}</h2>
<h2>Region: {{ region }}</h2>
<player></player>
</div>
</template>
<script>
import Player from './Player.vue';
export default {
name: 'Team',
data() {
return {
name: 'Natus Vincere',
region: 'Russia',
}
},
components: {
Player
}
}
</script>
<style>
.demo {
background-color: orange;
}
</style>
Player.vue
<template>
<div>
<h2>Player Name: {{ name }}</h2>
<h2>Age: {{ age }}</h2>
<player></player>
</div>
</template>
<script>
export default {
name: "Player",
data() {
return {
name: "s1mple",
age: 24,
};
},
};
</script>
Vue 脚手架
说明
- Vue 脚手架(Vue CLI)是 Vue 官方提供的标准化开发工具(开发平台 )
- 最新的版本是 4.x
- 文档:介绍 | Vue CLI (vuejs.org)
具体使用
- 全局安装
@vue/cli
(仅第一次执行)
npm install -g @vue-cli
- 切换到要创建项目的目录,然后用命令创建项目
vue create 项目名
- 启动项目
npm run serve
结构
├── node_modules
├── public
│ ├── favicon.ico: 页签图标
│ └── index.html: 主页面
├── src
│ ├── assets: 存放静态资源
│ │ └── logo.png
│ │── components: 存放组件
│ │ └── HelloWorld.vue
│ │── App.vue: 汇总所有组件
│ │── main.js: 入口文件
├── .gitignore: git 版本管制忽略的配置
├── babel.config.js: babel 的配置文件
├── package.json: 应用包配置文件
├── README.md: 应用描述文件
└── package-lock.json:包版本控制文件
main.js
: 入口文件
// 项目的入口文件
// 默认引入的是运行版的Vue:
// "module": "dist/vue.runtime.esm.js",
import Vue from 'vue' // 引入Vue
import App from './App.vue' // 引入App组件,它是所有组件的父组件
Vue.config.productionTip = false // 关闭Vue生产提示
new Vue({ // 创建实例对象——vm
render: h => h(App),
}).$mount('#app') // 绑定容器
render 函数
render: h => h(App)
render: function(createElement) {
// 用于指定template
createElement(App);
}
vue.config.js
配置文件
输出Vue脚手架的默认配置: vue inspect > output.js
ref
属性
被用来给元素或子组件注册引用信息(替代 id
)
应用在html标签上 $ref
获取到的是真实DOM元素,应用在组件标签上是组件实例对象
使用
应用:<h1 ref="xxx"></h1>
或 <School ref="xxx"></School>
获取:this.$refs.xxx
props
配置
让组件接受外部传来的数据
使用:
仅接受:props: ['name']
限制类型:
props: {
name: Number
}
限制类型、必要性、缺省默认值:
props: {
name: {
type: String, // 类型
required: true, // 必要性
default: 'Tom' // 默认值
}
}
props
是只读的,Vue 底层会监测你对 props
的修改,如果进行了修改,就会发出警告
mixin 混入
把多个组件公用的配置提取成一个混入对象
使用:
定义并导出混合:
export const mixin = {
data() { ... },
methods: { ... }
}
使用混合:
全局混入:Vue.mixin(xxx)
局部混入:mixins: ['xxx']
插件
用于增强 Vue
本质
包含 install
方法的一个对象,install
的第一个参数是 Vue,第二个以后的参数是插件使用者传递的数据。
使用:
定义插件:
对象.install = function (Vue, options) {
// 添加全局过滤器
Vue.filter( ... );
// 添加全局指令
Vue.directive( ... );
// 配置全局混入
Vue.mixin( ... );
// 添加实例方法
Vue.prototype.$myMethod = function() { ... };
Vue.prototype.$myProperty = xxx;
};
使用插件:Vue.use()
作用域样式
让样式仅在局部生效,防止冲突
写法:<style scoped> ... </style>
自定义事件
可用作一种组件间通信的方式,适用于 子组件 ===> 父组件
使用场景:A是父组件,B是子组件,B向A传数据,在A中给B绑定自定义事件(事件的回调在A中)
组件上也可以绑定原生DOM事件,需要使用 .native
修饰符
注意:通过 this.$refs.xxx.$on('event', callback)
绑定自定义事件时,回调函数要么配置在 methods
中,要么直接使用箭头函数,否则 this
指向会出问题
绑定自定义事件
第一种方式(在父组件中):<Demo @abc="test" />
第二种方式(在父组件中):
<template>
<Demo ref="demo" />
...
</template>
<script>
export default {
...
methods: {
test() { ... }
},
mounted() {
this.$refs.demo.$on('abc', this.test);
}
}
</script>
若想让自定义事件只能触发一次,可以使用 once
修饰符,或 $once
方法
触发自定义事件
this.$emit('abc', data);
解绑自定义事件
this.$off('abc'); // 解绑单个自定义事件
this.$off(['abc', 'def']); // 解绑多个自定义事件
this.$off(); // 解绑所有自定义事件
全局事件总线(GlobalEventBus)
一种组件间通信的方式,适用于任意组件间通信
安装全局事件总线
main.js
new Vue({
...
beforeCreate() {
Vue.prototype.$bus = this
}
...
});
使用全局事件总线
接收数据:A组件需要接收数据,则在A组件中给 $bus
绑定自定义事件,事件的回调留在A组件
methods: {
demo(data) { ... }
}
...
mounted() {
this.$bus.$on('abc', this.demo);
}
提供数据:this.$bus.$emit('abc', data)
最好在 beforeDestory
钩子中解绑当前组件所用到的事件
$nextTick
在下一次DOM更新结束后执行其指定的回调
语法:this.$nextTick(callback)
什么时候使用:当改变数据后,要基于更新后的新DOM进行某些操作时,要在 nextTick
指定的回调函数中执行
过渡与动画
原理:在插入、更新或移除DOM元素时,在核实的时候给元素添加样式类名
使用
1、准备样式:
元素进入:
v-enter
: 进入的起点v-enter-active
: 进入过程中v-enter-to
: 进入的终点
元素离开:
v-leave
: 离开的起点v-leave-active
: 离开过程中v-leave-to
: 离开的终点
2、用 transition
包裹要过渡的元素,并配置 name
属性:
<transition name="test">
<h1 v-show="isShow">TEST</h1>
</transition>
若有多个元素需要过度,则需使用 <transition-group></transition-group>
,且每个元素都要指定 key
值
Vue中的AJAX
解决跨域的方式:代理服务器
配置代理
方法一
在 vue.config.js
中添加如下配置:
module.exports = {
...
devServer: {
proxy: "http://localhost:5000"
}
...
}
优点
配置简单,请求资源时直接发给前端(8080)即可
缺点
不能配置多个代理,不能灵活的控制请求是否走代理
工作方式
若按照上述配置代理,当请求了前端不存在的资源时,那么该请求会转发给服务器(优先匹配前端资源)
方法二
在 vue.config.js
中配置具体代理规则:
module.exports = {
...
devServer: {
proxy: {
'/api': { // 匹配所有以 /api 开头的请求路径
target: 'http:/localhost:5000/', // 代理目标的基础路径
pathRewrite: { '^/api': '' } // 重写路径
}
}
}
...
}
优点
可以配置多个代理,且可以灵活控制请求是否走代理
缺点
配置略微繁琐,请求资源时必须加前缀
slot 插槽
作用
让父组件可以向子组件指定位置插入 html 结构,也是一终组件间通信的方式,适用于父组件 ===> 子组件
分类
默认插槽、具名插槽、作用域插槽
使用
默认插槽
父组件中
<template>
<Test>
<div>HTML结构</div>
</Test>
</template>
子组件中
<template>
<div>
...
<!-- 定义默认插槽 -->
<slot>插槽默认内容</slot>
...
</div>
</template>
具名插槽
父组件中
<template>
<Test>
<template slot="slotA">
<div>HTML结构1</div>
</template>
<template slot="slotB">
<div>HTML结构2</div>
</template>
</Test>
</template>
子组件中
<template>
<div>
...
<!-- 定义名为"slotA"的插槽 -->
<slot name="slotA">插槽默认内容1</slot>
...
<!-- 定义名为"slotB"的插槽 -->
<slot name="slotB">插槽默认内容1</slot>
...
</div>
</template>
作用域插槽
使用场景:数据在组件自身,但根据书记生成的结构需要组件的使用者来决定
父组件中
<template>
<Test>
<template scope="scopeData">
<ul>
<!-- 生成无序列表 -->
<li v-for="item in scopeData.data" :key="item">{{ item }}</li>
</ul>
</template>
</Test>
<Test>
<template scope="{ data }"> // 解构赋值
<!-- 生成四级标题 -->
<h4 v-for="(item, index) in data" :key="index">{{ item }}</h4>
</template>
</Test>
</template>
子组件中
<template>
<div>
...
<!-- 定义作用域插槽 -->
<slot :data="data">插槽默认内容</slot>
...
</div>
</template>
<script>
export default {
name: 'Test',
data() {
return() {
// 数据在子组件自身
data: ['A', 'B', 'C', 'D']
}
}
}
</script>
Vuex
理解
专门在 Vue 中实现集中式状态(数据)管理的一个 Vue 插件,对 Vue 应用中多个组件的共享状态进行集中式的管理(读/写),也是一种组件间通信的方式,且适用于任意组件间通信。
什么时候使用 Vuex?
- 多个组件依赖于同一状态
- 来自不同组件的行为需要变更同一状态
原理
搭建 Vuex 环境
1、安装 Vuex
npm i vuex
2、创建文件: src/store/index.js
// 引入 Vue
import Vue from 'vue';
// 引入 Vuex
import Vuex from 'vuex';
// 使用 Vuex 插件
Vue.use(Vuex);
// actions : 响应组件中用户的动作
const actions = {}
// mutations : 修改state中的数据
const mutations = {}
// state : 存储着数据
const state = {}
// 创建并暴露store
export default new Vuex.Store({ actions, mutations, state });
3、在 main.js
中创建 vm 时传入 store
配置项
...
import store from './store'; // 引入store
...
new Vue({
render: h => h(App),
store
}).$mount('#app');
基本使用
1、操作文件 store.js
,初始化数据、配置 actions
、 mutations
import Vue from 'vue';
import Vuex from 'vuex';
Vue.use(Vuex);
const actions = {
// 响应组件中的动作
addWhenOdd(context, value) {
if (context.state.num % 2 == 1) {
context.commit('ADD', value);
}
}
}
const mutations = {
// 执行
ADD(state, value) {
state.num += value;
}
}
// 初始化数据
const state = {
num: 0
}
export default new Vuex.Store({ actions, mutations, state });
2、组件中读取 Vuex 中的数据: $store.state.sum
3、组件中修改 Vuex 中的数据:
- Dispatch:
$store.dispatch('add', 1)
- Commit:
$store.commit('ADD', 1)
若没有网络请求或其他业务逻辑,组件也可以越过actions,即:不写
dispatch
,直接commit
getters
配置项
当 state
中的数据需要经过加工后再使用时,可以使用 getters
使用
配置
...
const getters = {
tenTimesNum(state) {
return state.num * 10;
}
}
export default new Vuex.Store({
...
getters
});
读取
$store.getters.tenTimesNum
四个 map 方法的使用
引入
import { mapState, mapGetters, mapMutations, mapActions } from 'vuex'
mapState
帮助映射 state
中的数据为计算数据
computed: {
...mapState({ num: 'num', uname: 'uname', age: 'age' }), // 对象写法
...mapState(['num', 'uname', 'age']) // 数组写法
}
mapGetters
帮助映射 getters
中的数据为计算数据
computed: {
...mapGetters({ tenTimesNum: 'tenTimesNum' }), // 对象写法
...mapGetters(['tenTimesNum']) // 数组写法
}
mapActions
帮助生成与 actions
对话的方法,即:包含 $store.dispatch(xxx)
的函数
methods: {
...mapActions({ addWhenOdd: 'addWhenOdd', addLater: 'addLater' }), // 对象写法
...mapActions(['addWhenOdd', 'addLater']) // 数组写法
}
mapMutations
帮助生成与 mutations
对话的方法,即:包含 $store.commit(xxx)
的函数
methods: {
...mapMutations({ add: 'ADD', sub: 'SUB' }), // 对象写法
...mapMutations(['ADD', 'SUB']) // 数组写法
}
mapActions 与 mapMutations 使用时,若需传递参数,要在模板中绑定事件时传递,否则默认参数是事件对象
模块化与命名空间
目的
让代码更好维护,让多种数据分类更加明确
使用
修改 src/store/index.js
,拆分模块至 count.js
, person.js
index.js
import Vue from 'vue';
import Vuex from 'vuex';
import countOptions from './count';
import personOptions from './person';
Vue.use(Vuex);
export default new Vuex.Store({
modules: { // 模块化
countAbout: countOptions,
personAbout: personOptions
}
});
count.js
// 求和功能相关配置
export default {
namespaced: true, // 开启命名空间
actions: {
addWhenOdd(context, value) {
context.state.num % 2 && context.commit('ADD', value);
},
addLater(context, value) {
setTimeout(() => {
context.commit('ADD', value);
}, 500);
}
},
mutations: {
ADD(data, value) {
data.num += value;
},
SUB(data, value) {
data.num -= value;
},
},
state: {
num: 0,
uname: 'GabrielxD',
age: 18,
},
getters: {
tenTimesNum(state) {
return state.num * 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('添加的人必须姓王');
}
},
addPersonFromServer(context) {
axios.get('https://api.uixsj.cn/hitokoto/get', {
params: { type: 'hitokoto' }
}).then(
response => {
const personObj = { id: nanoid(), name: response.data };
context.commit('ADD_PERSON', personObj);
},
error => {
alert(error.message);
})
}
},
mutations: {
ADD_PERSON(data, value) {
data.personList.unshift(value);
}
},
state: {
personList: [
{ id: 'RbQHhtfhmjrY9eHxSpIdw', name: '张三' }
]
},
getters: {
firstPersonName(state) {
return state.personList[0].name;
}
}
}
开启命名空间后在组件中读取数据
state
// 方式1
this.$store.state.personAbout.personList
// 方式2
...mapState('countAbout', ['num', 'uname', 'age'])
getters
// 方式1
this.$store.getters['personAbout/firstPersonName']
// 方式2
...mapGetters('countAbout', ['tenTimesNum'])
开启命名空间后在组件中调用
dispatch
// 方式1
this.$store.dispatch('personAbout/addPersonWang', person)
// 方式2
...mapActions('countAbout', ['addWhenOdd', 'addLater'])
commit
// 方式1
this.$store.commit('personAbout/ADD_PERSON', person)
// 方式2
...mapMutations('countAbout', ['ADD', 'SUB'])
Vue Router
理解
一个路由(route)就是一组映射关系(key - value),多个路由需要路由器(router)进行管理。
在前端路由中:key 是路径,value 是组件。
搭建 Vue Router 环境
1、安装 Vue Router
npm i vue-router
2、创建文件: src/router/index.js
// 该文件用于创建整个应用的路由器
// 引入VueRouter
import VueRouter from 'vue-router';
// 引入组件
import Home from '../components/Home';
import About from '../components/About';
// 创建并暴露一个router实例对象,管理路由规则
export default new VueRouter({
routes: [
{
path: '/about',
component: About
},
{
path: '/home',
component: Home
}
]
});
3、在 main.js
创建 vm 时传入 router
配置项
import Vue from 'vue';
import App from './App.vue';
// 引入VueRouter
import VueRouter from 'vue-router';
// 引入路由器
import router from './router';
// 使用VueRouter
Vue.use(VueRouter);
Vue.config.productionTip = false;
new Vue({
render: h => h(App),
router
}).$mount('#app')
基本使用
1、实现切换
<router-link active-class="active" to="/about"></router-link>
active-class
高亮使用的样式to
要切换的路径
2、指定展示位置
<router-view></router-view>
几个注意点
- 路由组件通常存放在
pages
文件夹,一般组件通常存放在components
文件夹。 - 通过切换,“隐藏”了的路由组件,默认时被销毁掉的,需要的时候再去挂载。
- 每个组件都有自己的
$route
属性,里面存储着自己的路由信息。 - 整个应用只有一个 router,可以通过组件的
$router
属性获取到。
多级路由(嵌套路由)
配置路由规则,使用 children
配置项
routes: [
{
path: '/about',
component: About
},
{
path: '/home',
component: Home,
children: [
{
path: 'news',
component: News
},
{
path: 'message',
component: Message
}
]
}
]
跳转(要写完整路径)
<router-link to="/home/news"></router-link>
路由的 query
参数
传递参数
<!-- 跳转路由并携带query参数,to字符串写法 -->
<router-link :to="`/home/message/detail?id=${m.id}&title=${m.title}`">{{ m.title }}</router-link>
<!-- 跳转路由并携带query参数,to对象写法 -->
<router-link
:to="{
path: '/home/message/detail',
query: {
id: m.id,
title: m.title
}
}"
>{{ m.title }}</router-link>
接收参数
$route.query.id
$route.query.title
命名路由
作用
可以简化路由的跳转
给路由命名
routes: [
{
path: '/demo',
component: Demo,
children: [
{
path: 'test',
component: Test,
children: [
{
name: 'hello' // 给路由命名
path: 'welcome',
component: Hello
}
]
}
]
}
]
简化跳转
<!-- 简化前,完整路径 -->
<router-link to="/demo/test/welcome">跳转</router-link>
<!-- 简化后,名字 -->
<router-link :to="{ name: 'hello' }">跳转</router-link>
<!-- 简化后写法配合传参 -->
<router-link
:to="{
name: 'hello',
query: {
id: 666,
title: '你好'
}
}"
>跳转</router-link>
路由的 params
参数
配置路由,声明接受 params 参数
{
path: '/home',
component: Home,
children: [
{
path: 'news',
component: News
},
{
path: 'message',
component: Message,
children: [
{
name: 'msg',
path: 'detail/:id/:title', // 使用占位符声明接受params参数
component: Detail
}
]
}
]
}
传递参数
<!-- 跳转路由并携带params参数,to字符串写法 -->
<router-link :to="`/home/message/detail/${m.id}/${m.title}`">{{ m.title }}</router-link>
<!-- 跳转路由并携带params参数,to对象写法 -->
<!-- 只能使用name -->
<router-link
:to="{
name: 'msg',
params: {
id: m.id,
title: m.title
}
}"
>{{ m.title }}</router-link>
注意:路由携带 params 参数时,若使用 to 的对象写法,则不能使用 path 配置项,必须使用 name 配置项
接收参数
$route.params.id
$route.params.title
路由的 props 配置
作用
简化参数的接受
配置路由的 props 属性
{
name: 'msg',
path: 'detail',
// path: 'detail/:id/:title',
component: Detail,
/* props的第一种写法,值为对象
该对象中所有key-value都会以props的形式传给Detail组件 */
props: { a: 1, b: 'hello' },
/* props的第二种写法,值为布尔值,若布尔值为真
就会把该路由接受到的所有params参数以props的形式传给Detail组件 */
props: true,
// props的第三种写法,值为函数
props($route) {
return {
id: $route.query.id,
title: $route.query.title
}
},
// 解构赋值
props({ query }) {
return {
id: query.id,
title: query.title
}
},
// 解构赋值的连续写法
// 语义化不明确 不推荐使用
props({ query: { id, title } }) {
return { id, title }
},
}
接收参数
<template>
<ul>
<li>message id: {{ id }}</li>
<li>message title: {{ title }}</li>
</ul>
</template>
<script>
export default {
name: 'Detail',
props: ['id', 'title'] // 直接用props接受
}
</script>
<router-link>
的 replace
属性
作用
控制路由跳转时操作浏览器历史记录的模式
浏览器的历史记录有两种写入方式:
push
: 追加历史记录replace
: 替换当前记录
路由跳转的时候默认为 push
开启 replace
模式
<router-link replace ...>跳转</router-link>
编程式路由导航
作用
不借助 <router-link>
实现路由跳转,让路由跳转更加灵活
$router
中的 API
push()
this.$router.push({
path: '/home/message/detail',
query: {
id: m.id,
title: m.title
}
});
replace()
this.$router.replace({
path: '/home/message/detail',
query: {
id: m.id,
title: m.title
}
});
forward()
this.$router.forward(); // 前进
back()
this.$router.back(); // 后退
go
this.$router.go(3); // 前进3步
缓存路由组件
作用
让不展示的路由组件保持挂载,不销毁
使用
<!-- include中填写路由组件名 -->
<keep-alive include="News">
<router-view></router-view>
</keep-alive>
<!-- include数组写法 -->
<keep-alive :include="[News, Message]">
<router-view></router-view>
</keep-alive>
两个新的生命周期钩子
作用
路由组件独有的两个钩子,用于捕获路由组件的激活状态
钩子
actived
: 路由组件被激活时触发deactived
: 路由组件失活时触发
路由元信息
配置
路由的时候可以配置 meta
字段:
{
path: 'news',
component: News,
meta: { isAuth: true, title: '新闻' } // 元信息
}
路由守卫
全局路由守卫
beforeEach
前置全局路由守卫 : 初始化时、每次路由切换之前被调用
src/router/index.js
router.beforeEach((to, from, next) => {
if (to.meta.isAuth) { // 判断是否需要鉴权
if (localStorage.getItem('name') === 'GabrielxD') {
next();
}
} else next();
});
to
: 即将要进入的路由对象from
: 当前正要离开的路由对象next
: 函数
afterEach
全局后置路由守卫 : 初始化时、每次路由切换之后被调用
src/router/index.js
router.afterEach((to, from) => {
document.title = to.meta.title || '系统';
});
独享路由守卫
beforeEnter
初始化时、每次路由切换之前被调用
在单个路由配置中:
src/router/index.js > router > routes
{
path: '/home',
component: Home,
meta: { title: '主页' },
beforeEnter(to, from, next) {
if (to.meta.isAuth) { // 判断是否需要鉴权
if (localStorage.getItem('name') === 'GabrielxD') {
next();
}
} else next();
}
}
没有后置独享路由守卫
组件内路由守卫
beforeRouteEnter
通过路由规则,进入该组件时被调用
src/pages/About.vue > script > default
beforeRouteEnter(to, from, next) {
// ...
}
beforeRouteLeave
通过路由规则,**离开 **该组件时被调用
src/pages/About.vue > script > default
beforeRouteLeave (to, from, next) {
// ...
}
router 的两种工作模式
对于一个 url 来说,什么是 hash 值?
—— ‘#’ 及其后面的内容就是 hash 值
hash 值不会包含在 HTTP 请求中,即:hash 值不会带给服务器
hash 模式
- 地址中永远带着 ‘#’,不美观
- 若以后将地址通过第三方手机 app 分享,若 app 校验严格,则地址会被标记为不合法
- 兼容性较好
history 模式
- 地址干净美观
- 应用上线时需要后端人员支持,解决刷新页面服务端 404 的问题
- 兼容性比 hash 模式较差
评论区