侧边栏壁纸
博主头像
GabrielxD

列車は必ず次の駅へ。では舞台は?私たちは?

  • 累计撰写 675 篇文章
  • 累计创建 128 个标签
  • 累计收到 28 条评论

目 录CONTENT

文章目录

【学习笔记】Vue

GabrielxD
2022-02-06 / 0 评论 / 0 点赞 / 574 阅读 / 10,365 字
温馨提示:
本文最后更新于 2022-08-19,若内容或图片失效,请留言反馈。部分素材来自网络,若不小心影响到您的利益,请联系我们删除。

Vue 介绍

简介

Vue (读音 /vjuː/,类似于 view) 是一套用于构建用户界面的渐进式框架。与其它大型框架不同的是,Vue 被设计为可以自底向上逐层应用,Vue 的核心库只关注视图层

官网

特点

  1. 遵循 MVVM 模式
  2. 编码简洁, 体积小, 运行效率高, 适合移动/PC 端开发
  3. 它本身只关注 UI, 也可以引入其它第三方库开发项目

与其它 JS 框架的关联

  1. 借鉴 Angular 的模板数据绑定技术
  2. 借鉴 React 的组件化虚拟 DOM技术

Vue 周边库

  1. vue-cli: vue 脚手架
  2. vue-resource
  3. axios
  4. vue-router: 路由
  5. vuex: 状态管理
  6. 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 实例对象

img

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)" 效果一直,但后者可以传参

事件修饰符

  1. .prevent* : 阻止默认事件
  2. .stop* : 阻止事件冒泡
  3. .once* : 事件只触发一次
  4. .{keyCode | keyAlias} - 只当事件是从特定键触发时才触发回调
  5. .capture : 启用事件捕获模式
  6. .self : 只有 event.target 是当前操作的元素时才触发事件
  7. .passive : 事件的默认行为立即执行,无需等待事件回调执行完毕

常用 keyCode: enter delete esc space tab up down left right

计算属性 computed

原理

底层借助了 Object.defineProperty() 提供的 getter 和 setter

使用场景

要使用的属性不存在,要通过已有属性计算得来

优点

与 methods 事项相比,内部有缓存机制(复用),效率更高,调试方便

备注

  • 计算属性最终会出现在 vm 上,直接读取使用即可
  • get 函数什么时候执行?
    1. 初次读取时会执行一次
    2. 依赖的数据发生改变时会被再次调用
  • 如果计算属性要被修改就必须写 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 之间的区别

  1. computed 能完成的功能,watch 都能完成
  2. 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>

两个重要的小原则

  1. 所有被 Vue 所管理的函数,最好写成普通函数,这样 this 的指向才是 vm 或 组件实例对象
  2. 所有不被 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 可能引发的问题:

  1. 若对数据进行逆序添加、逆序删除等破坏顺序的操作: 会产生没有必要的 真实DOM 更新 ==> 界面效果没问题,但效率变低
  2. 如果结构中还包含输入类的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)
监测数组中的数据

​ 通过包裹数组更新元素的方法实现:

  1. 调用原生对应的方法对数组进行更新
  2. 重新解析模板,进而更新页面

修改 Vue 维护的数据中数组的某个元素一定要使用如下方法:

  1. push() pop() shift() unshift() splice() sort() reverse()
  2. 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(指令名, 回调函数);

配置对象中常用的三个回调:

  1. bind : 指令与元素成功绑定时调用
  2. inserted : 指令所在元素被插入页面时调用
  3. 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>

生命周期

image-20220116080902081

常用的生命周期钩子

  • mounted : 发送ajax请求、启动定时器、绑定自定义事件、订阅消息等【初始化操作】
  • beforeDestroy: 清除定时器、解绑自定义事件、取消订阅消息等【收尾工作】

关于销毁 Vue 实例

  • 销毁后借助 Vue 开发者工具看不到任何信息
  • 销毁后自定义事件会失效,但原生DOM事件依然有效
  • 一般不会在 beforeDestroy操作数据,因为即便操作数据,也不会再触发更新流程了

Vue 组件化编程

模块与组件、模块化与组件化

模块

理解

向外提供特定功能的 js 程序,一般就是一个 js 文件

为什么需要?

js 文件很多很复杂

模块可以: 复用 js、简化 js 的编写、提高 js 运行效率

组件

理解

用来实现局部(特定)功能效果的代码集合(html/css/js/image……)

为什么需要?

一个界面的功能很复杂

组件可以: 复用编码、简化项目编码、提高运行效率

模块化

当应用中的 js 都以模块来编写的,那这个应用就是一个模块化的应用

组件化

当应用中的功能都是多组件的方式来编写的,那这个应用就是一个组件化的应用

为什么需要组件化

image-20220116081811175

image-20220116081902939

Component Tree

非单文件组件

使用

  1. 定义(创建)组件
    • 使用 Vue.extend(options) 创建,其中 optionsnew Vue(options) 时传入的 options 几乎一样,但也有区别:
      1. el 不要写 —— 最终所有组件都要经过一个 vm 的管理,由 vm 中的 el 决定服务哪个容器
      2. data 必须写成函数形式 —— 避免组件被复用时,数据存在引用关系
    • 备注:使用 template 可以配置组件结构
  2. 注册组件
    • 局部注册:new Vue时传入 components 选项
    • 全局注册:Vue.component('组件名', 组件)
  3. 编写组件标签
    • <test></test>
    • 备注:自闭和标签(<test />)的写法在非单文件组件中不适用,但在单文件组件中可用

注意

  1. 组件名:
    • 一个单词组成:
      • 首字母小写:school
      • 首字母大写:School
    • 多个单词组成:
      • kebab-case 命名:my-school
      • CamelCase 命名:MySchool(需要 Vue 脚手架支持)
    • 备注:
      1. 组件名尽可能回避 HTML 中已有标签名称,如:h2、H2
      2. 可以使用 name 配置项指定组件在开发者工具中呈现的名字
  2. 组件标签:
    • 双标签形式:<school></school>
    • 单标签自闭合形式:<school /> (需要 Vue 脚手架支持)
  3. 简写:
    • const school = Vue.extend(options); 可简写为 const school = options;

VueComponent 构造函数

  • 注册的组件本质是一个名为 VueComponent 的构造函数,它是由 Vue.extend 生成的
  • 写完组件标签后,Vue 解析时会帮忙创建该组件的实例对象(即 Vue 帮助执行 new VueComponent(options)
  • 每次调用Vue.extend,返回的都是一个全新的 VueComponent
  • this 的指向:
    1. 组件配置中:
      • data 函数、methods 中函数、watch 中函数、computed 中函数 其中的 this 均是VueComponent 实例对象
    2. 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)

具体使用

  1. 全局安装 @vue/cli (仅第一次执行)
npm install -g @vue-cli
  1. 切换到要创建项目的目录,然后用命令创建项目
vue create 项目名
  1. 启动项目
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 CLI (vuejs.org)

输出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)

一种组件间通信的方式,适用于任意组件间通信

image-20220118223202558

安装全局事件总线

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元素时,在核实的时候给元素添加样式类名

Transition Diagram

使用

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

解决跨域的方式:代理服务器

image-20220118220149395

配置代理

方法一

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 (vuejs.org)

什么时候使用 Vuex?

  1. 多个组件依赖于同一状态
  2. 来自不同组件的行为需要变更同一状态

image-20220118224235243

原理

vuex

State Management

搭建 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,初始化数据、配置 actionsmutations

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>&nbsp;&nbsp;

<!-- 跳转路由并携带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>&nbsp;&nbsp;
<!-- 跳转路由并携带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>

两个新的生命周期钩子

作用

路由组件独有的两个钩子,用于捕获路由组件的激活状态

钩子

  1. actived : 路由组件被激活时触发
  2. 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 模式

  1. 地址中永远带着 ‘#’,不美观
  2. 若以后将地址通过第三方手机 app 分享,若 app 校验严格,则地址会被标记为不合法
  3. 兼容性较好

history 模式

  1. 地址干净美观
  2. 应用上线时需要后端人员支持,解决刷新页面服务端 404 的问题
  3. 兼容性比 hash 模式较差
0

评论区