Vue基础(二)
- 组件
- vue脚手架
render
函数- 修改脚手架默认配置
ref
属性props
属性用于接收父组件传递的数据mixin
属性: 相同逻辑抽离,方便复用- 插件
- scoped 样式: 作用域样式
- Todo-List 案例
- 组件自定义事件
- 父组件通过
v-on
指令为子组件绑定(指令形式绑定) - 父组件通过
$refs
为组件实例绑定(方法形式绑定) - 组件自定义事件只触发一次
- 方法形式和指令形式绑定自定义事件区别
- 组件解绑自定义事件
- 为什么要解绑自定义事件
$off()
解绑自定义事件- 组件自定义事件没有冒泡机制
- 使孙子组件触发祖先组件的事件
- 总结
- 1.为某个组件绑定事件时,可以直接写回调函数,且回调函数应该是箭头函数,否则 this 指向的不是父组件实例对象,而是子组件实例对象
- 2.如果使用了方法函数,其this指向绑定了事件的组件实例对象
- 3.为组件绑定事件可以绑定多次,每次触发后都会执行
- 4.组件使用
@事件名.native
修饰符为组件绑定原生事件 - 5.使用系统事件时只能对组件绑定,不能对组件的子元素进行绑定,否则会报错
- 6.为组件绑定系统事件,其实就是为组件的根元素绑定事件,这也是
template
中必须只有一个根元素的原因之一 - 7.组件的系统事件具有冒泡性质,即子组件的点击事件会冒泡到父组件,会执行父组件回调函数
- 8.组件绑定自定义事件方式
- 9.触发事件
- 10.解绑事件
- 父组件通过
- 全局事件总线
- 消息订阅与发布
$nextTick
执行回调- 动画效果
- 过渡效果
- 多个元素具有相同过渡效果应使用
transition-group
标签 - 集成第三方动画库
Animate.css
- 总结vue封装动画与过渡
- 代理
vue-resource
库发起网络请求(不推荐)- 插槽
- Vuex
- 简介
- 核心概念
- 使用场景
- 原理
- 流程总结
- 搭建
Vuex
环境 $store.dispatch()
触发actions
对应的方法actions
对应的方法中通过上下文对象context
调用commit
方法mutations
中的对应方法被调用- 组件中通过
$store.属性
读取共享数据 actions
中处理业务逻辑,如没有业务逻辑可直接调用commit
actions
中方法名小写,mutations
中方法名大写,在view
代码时便于区分vue
开发者工具中始终监听mutations
中方法执行actions
中可以通过调用dispatch
方法来执行actions
其他方法以便处理业务- 规范
getters
对象对state
数据的获取进行封装业务逻辑mapState
函数简化组件中获取state
数据mapGetters
函数简化组件中获取getters
数据mapMutations
函数简化组件中调用mutations
方法mapActions
函数简化组件中调用actions
方法Vuex
模块化和命名空间
- 路由
- 概念
- 基本使用
vue-router
插件库 - 注意
- 嵌套路由
- 路由传参
- 命名路由
- 路由组件的
props
配置 - 总结
router-link
标签记录浏览历史地址- 编程式路由导航
- 缓存路由组件
- 注意
- 1.
<keep-alive>
标签必须包裹<router-link>
标签 - 2.缓存路由组件时,路由组件必须有
name
属性,否则不生效 - 3.
<keep-alive>
标签的include
属性的值为路由组件的名称,依据路由组件的name
属性,不是配置的路由name
- 4.
<keep-alive>
标签的include
属性值的范围必须是当前组件可以路由的组件名称,也就是组件的子路由组件名称,不能是孙子路由组件名 - 5.当缓存路由组件的父组件销毁且未配置缓存时,该缓存路由组件的数据也会被销毁
- 6.
<keep-alive>
没有include
属性时,则缓存所有路由组件 - 7.配置多个缓存路由组件是使用
,
紧紧隔开,不能有空格,或者:include="['组件名','组件名']"
- 1.
- 注意
- 路由组件中的生命周期钩子
- 路由守卫
- UI组件库
组件
组件定义: 实现应用中局部功能代码和资源集合
非单文件组件
组件的使用分为 3 个步骤
<div id="root">
<!-- 3.使用组件 -->
<hello-vue></hello-vue>
</div>
<script type="text/javascript">
// 1.定义组件
const helloVue = Vue.extend({
template: `
<div>
<h2>hello{{vue}}</h2>
<button @click="showInfo">点我提示信息</button>
<span v-if="show">{{obj.tip}}</span>
<hr>
</div>
`,
data() {
return {
vue: "Vue",
obj: {
tip: "这是一个helloVue组件",
},
show: false,
};
},
methods: {
showInfo() {
this.show = !this.show;
},
},
});
// 2.注册组件
new Vue({
el: "#root",
components: {
helloVue,
},
});
</script>
1.定义组件
定义组件 extend(options)方法和 new Vue(options)方法的配置对象属性几乎一样,区别在于
1.extend 中不能写 el 属性,因为组件可能被很多 vue 实例使用,因此组件只能属于某个 vue 实例,由 vue 实例中的el
决定服务于哪个容器
2.定义组件,data 一定是方法,方法中返回数据对象,因为组件是复用的,如果 data 是对象,则多个组件实例会共享同一个 data,从而造成数据污染
3.组件对象中使用 template 可以配置组件 DOM 结构,template 的 DOM 结构中必须只有一个根节点
const helloVue = Vue.extend({
template: `
<div>
<h2>hello{{vue}}</h2>
<button @click="showInfo">点我提示信息</button>
<span v-if="show">{{obj.tip}}</span>
<hr>
</div>
`,
data() {
return {
vue: "Vue",
obj: {
tip: "这是一个helloVue组件",
},
show: false,
};
},
methods: {
showInfo() {
this.show = !this.show;
},
},
});
2.vm 实例中注册组件
使用 componets 对象注册组件,key: 组件名,value: 组件对象
new Vue({
el: "#root",
components: {
helloVue,
},
});
3.使用组件
<hello-vue></hello-vue>
如果组件名是小驼峰形式,则使用时需要将小驼峰改为小写,并且使用-连接,例如<hello-vue></hello-vue>
注意:
同一个 vue 实例使用多个相同组件时,组件之间相互独立,互不影响
<hello-vue></hello-vue> <hello-vue></hello-vue>
全局组件
定义全局组件
// 全局组件定义
const helloTs = Vue.extend({
template: `
<div>
<h2>hello {{ts}}</h2>
<button @click="showInfo">点我提示信息</button>
<span v-if="show">{{obj.tip}}</span>
<hr>
</div>
`,
data() {
return {
ts: "TS",
obj: {
tip: "这是一个全局helloTs组件",
},
show: false,
};
},
methods: {
showInfo() {
this.show = !this.show;
},
},
});
使用 Vue.component()方法注册组件,组件名可以是小驼峰形式,也可以是短横线形式
Vue.component("hello-ts", helloTs);
// 或者 Vue.component('helloTs', helloTs)
使用时直接使用组件名即可
<hello-ts></hello-ts>
注意事项
1.组件名多个单词写法
kebab-case(短横线命名): hello-vue
PascalCase(大驼峰命名): HelloVue
(这种写法需要 vue脚手架支持)
2.组件名应避免使用 html 标签名,如<h2>
,<div>
等,因为 html 标签名是 vue 内置组件,容易冲突
3.组件定义中可以使用name
属性,指定在开发者工具中呈现的组件名称
定义组件名字:
Vue.extend({
name: "Hello",
template: ``,
});
注册时自定义组件名:
components: {
helloVue;
}
使用:
<hello-vue></hello-vue>
开发者工具中则会显示定义时的组件名
4.组件可以使用自闭合标签写法,如
<hell-vue/>
(这种写法多个组件时需要 vue脚手架支持)
5.定义组件的简写形式:
let helloVue = Vue.extend(options)
简写为: let helloVue = options
组件的嵌套
<div id="root"></div>
<script type="text/javascript">
const helloVue = {
template: `
<div>
<h2>hello{{vue}}</h2>
<button @click="showInfo">点我提示信息</button>
<span v-if="show">{{obj.tip}}</span>
<hr>
</div>
`,
data() {
return {
vue: "Vue",
obj: {
tip: "这是一个helloVue组件",
},
show: false,
};
},
methods: {
showInfo() {
this.show = !this.show;
},
},
};
const language = {
template: `
<div>
<hello-vue></hello-vue>
</div>
`,
components: { helloVue },
};
const app = {
template: `
<div>
<language></language>
</div>
`,
components: { language },
};
// 2.注册组件
new Vue({
el: "#root",
template: `
<app></app>
`,
components: { app },
});
</script>
VueComponent
构造函数
1.组件本质是一个VueComponent
的构造函数,且不是程序员定义的,而是Vue.extend()
生成的
const helloVue = Vue.extend(optins);
console.log(helloVue);
2.当使用组件时,例如<hello-vue</hello-vue>
,Vue 就会调用new VueComponent(options)
创建组件实例
3.每次使用 Vue.extend()定义组件时,都会返回一个全新的 VueComponent 构造函数
查看 Vue 源码,可以发现 Vue.extend 函数中在每次调用时,都会重新声明Sub
属性,Sub
的值就是函数,因此每次调用 Vue.extend()都会返回一个新的 VueComponent 构造函数
Vue.extend = function (extendOptions) {
// ...
var Sub = function VueComponent(options) {
this._init(options);
};
// ...
return Sub;
};
4.this指向
定义的组件配置项中的data函数
,methods中函数
,watch中函数
,computed中函数
,它们中的 this 都指向组件的VueComponent
实例对象,这是通过 Vue 调用new VueComponent()
实现的,就像new Vue()
创建 vue 的实例对象一样,方法,监听属性,计算属性它们中的 this 都指向 vm 实例对象,因此组件中的 this 指向组件的VueComponent
实例对象,而且 VueComponent 的实例对象也代理了数据,和 vm 很像
5.通过组件实例对象中的$children
数组查看包含了哪些子组件
VueComponet
和Vue
的原型关系
实例的隐式原型属性指向其缔造者的显示原型对象
Vue 的原型对象有重要的属性和方法,如$set
,$watch
,$mount
等
vue
的实例对象的隐式原型__proto__
指向Vue.prototype
,Vue.prototype.__proto__
指向Object.prototype
VueComponent
的实例对象的隐式原型__proto__
指向VueComponent.prototype
,一般情况下,VueComponent.prototype
对象的隐式原型也应该指向其缔造者的原型,也就是Object.prototype
而vue
将VueComponent.prototype
对象的隐式原型__proto__
指向Vue
的原型对象,因此 VueComponent 的实例对象也能访问到 Vue 的原型对象中的属性和方法
重要关系
1.VueComponent.prototype.__proto__ == Vue.prototype
2.每个vc
的隐式原型都指向了Vue.prototype
3.原因: 让组件实例对象可以访问到 Vue 原型对象中的属性和方法
4.可以理解为组件(构造函数)的父类为 Vue
单文件组件书写流程
单文件组件: .vue
文件,就是一个组件,该组件将模板、逻辑与样式封装在单个文件中
文件名称一般都用大驼峰命名格式,例如HelloWorld.vue
1.子组件 Student.vue
<template>
<div>
<h1>学生姓名: {{ name }}</h1>
</div>
</template>
<script>
export default {
data() {
return {
name: "张三",
};
},
};
</script>
2.父组件 School.vue
父组件中使用子组件,并有自己的样式
<template>
<div class="school">
<h2>hello{{ vue }}</h2>
<button @click="showInfo">点我提示信息</button>
<span v-if="show">{{ obj.tip }}</span>
<Student></Student>
</div>
</template>
<script>
import Student from "./Student.vue";
export default {
data() {
return {
vue: "Vue",
obj: {
tip: "这是一个School组件",
},
show: false,
};
},
methods: {
showInfo() {
this.show = !this.show;
},
},
components: { Student },
};
</script>
<style>
.school {
background-color: aqua;
}
</style>
3.其他组件 Teacher.vue
<template>
<div>
<h1>老师姓名: {{ name }}</h1>
</div>
</template>
<script>
export default {
data() {
return {
name: "李四",
};
},
};
</script>
4.所有组件的管理组件 App.vue
作用: App.vue 组件将管理所有组件
<template>
<div>
<School></School>
<Teacher></Teacher>
<Student></Student>
</div>
</template>
<script>
import School from "./School.vue";
import Teacher from "./Teacher.vue";
import Student from "./Student.vue";
export default {
components: { School, Teacher, Student },
};
</script>
5.应用入口文件 main.js
main.js
: 创建 Vue 实例对象,指定 vue 实例服务的容器,是项目运行的入口文件,该文件中的 vue 实例对象只和 App.vue 组件进行关联
import App from "./App.vue";
new Vue({
el: "#app",
template: `
<App></App>
`,
components: { App },
});
6.容器文件index.html
<body>
<!-- 容器 -->
<div id="app"></div>
<script type="text/javascript" src="../js/vue.js"></script>
<script type="text/javascript" src="./main.js"></script>
</body>
在浏览器打开 index.html 文件,报错
浏览器不能直接支持 ES6 模块语法,因此需要脚手架支持
vue脚手架
vue脚手架: Vue CLI
是一个官方提供的标准化开发工具(命令行工具),用于快速搭建 Vue 开发环境以及管理项目的依赖。基于 webpack 构建
CLI
: Command Line Interface 命令行接口工具(vue 的脚手架)
使用
1.安装 vue脚手架工具: npm install -g @vue/cli
2.使用脚手架创建初始化项目:
➜ code vue create -m 打包工具 项目名
打包工具可选择: yarn
npm
等,使用包管理工具可以下载项目依赖的插件和项目打包
项目名: 单词之间使用-
,例如: hello-world
➜ code vue create -m npm vue-test
...
🎉 Successfully created project vue-test.
👉 Get started with the following commands:
$ cd vue-test
$ npm run serve
3.启动项目:
➜ code cd vue-test
➜ vue-test git:(master) npm run serve
4.访问项目:
DONE Compiled successfully in 2510ms 9:46:03 AM
App running at:
- Local: http://localhost:8080/
- Network: http://172.21.3.176:8080/
Note that the development build is not optimized.
To create a production build, run npm run build.
脚手架创建的项目结构
vue-test
├─ .gitignore
├─ README.md
├─ babel.config.js
├─ jsconfig.json
├─ package-lock.json
├─ package.json
├─ public
│ ├─ favicon.ico
│ └─ index.html
├─ src
│ ├─ App.vue
│ ├─ assets
│ │ └─ logo.png
│ ├─ components
│ │ ├─ HelloWorld.vue
│ │ ├─ School.vue
│ │ ├─ Student.vue
│ │ └─ Teacher.vue
│ └─ main.js
└─ vue.config.js
babel.config.js
babel 配置文件,用于将 ES6 语法转换为 ES5 语法
package.json
项目配置文件,记录了项目版本,依赖的插件版本,和项目常用命令和打包信息
package-lock.json
锁定依赖的版本,防止依赖版本不一致,导致项目运行异常
vue.config.js
vue 配置文件,用于配置 vue脚手架的配置信息,关闭语法检测等
src
目录
源代码文件夹
main.js
: 整个应用的入口文件,用于创建 Vue 实例对象,并指定 vue 实例服务的容器
App.vue
: 根组件,是所有组件的父组件,是整个应用的根组件,是所有组件的容器
assets
: 静态资源文件夹,存放静态资源文件,例如css
、js
、图片
等
components
: 组件文件夹,存放组件文件,例如School.vue
、Student.vue
等
public
目录
静态文件,例如index.html
favicon.ico
: 网站图标文件,用于设置网站图标
index.html
: 整个应用的界面,容器定义
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
中BASE_URL
指的是public/
路径
后续引入 public 目录下的资源时应该使用<%= BASE_URL %>
<title><%= htmlWebpackPlugin.options.title %></title>
: 通过 webpack 插件读取 package.json 中的name
字段作为标题
将单文件组件书写流程中的代码放置项目指定位置,并启动项目
报错: Component name “School“ should always be multi-word
由于组件命名不符合大驼峰或横线命名规范,导致编译报错,只需在 vue.config.js 中关闭语法检测,而后重新启动项目
const { defineConfig } = require("@vue/cli-service");
module.exports = defineConfig({
transpileDependencies: true,
// 关闭语法检查
lintOnSave: false,
});
启动成功:
render
函数
使用template
属性指定容器存在的问题
修改 main.js 文件
new Vue({
el: "#app",
template: ``,
});
出现如下错误:
意思是: 你正在使用仅运行构建的 vue,模板编译器不可用的 vue。要么将预编译的模板放到 render 函数中,要么使用包含编译器的 vue 版本
通过跳转引入的 vue 文件,可以找到使用插件
脚手架生成的 main.js 引入的 vue.js 为 package.json 中 module 对应的 vue.runtime.esm.js,该 vue 没有编译模块
这时就需要 render 函数: 字符串模板的代替方案
render函数入参
: createElement
方法
render函数返参
: 虚拟节点 VNode,就是 template 属性的代替方案
createElement
: 是一个方法,通过该方法来创建虚拟节点 VNode
该方法有两个参数:
1.html 标签名或组件对象
2.文本节点内容或数组(第二个参数可选)
如传入标签名:
new Vue({
el: "#app",
render(createElement) {
return createElement("h1", "这是一个h1标签");
},
});
会完全替换el
元素包括根元素和 template 属性一样
如传入组件:
App 为组件
new Vue({
el: "#app",
render: (h) => h(App),
});
总结
1.vue.js 和 vue.runtime.esm.js 的区别:
vue.js
: 完整版的 vue,包含核心功能,包含模板编译器,可以解析 template 属性
vue.runtime.esm.js
: 运行版的 vue,包含核心功能,不包含模板编译器,不能解析 template 属性,需要使用 render 函数
2.由于vue.runtime.esm.js
没有模板编译器,因此需要render
函数需要接收createElement
函数创建 VNode
3.组件中的template
标签的模板是由模板插件完成解析,package.json->devDependencies->vue-template-compiler
修改脚手架默认配置
脚手架的 webpack 默认配置文件不会暴露,但是可以通过vue inspect > output.js
查看默认配置
打包配置文件中指定了项目路径,项目入口文件路径和名称等,以便 webpack 打包工具对项目进行打包
同时 vue 也暴露了可配置的打包属性,自定义的打包属性需配置在vue.config.js文件中,和 package.json 同级
vue.config.js
文件
const { defineConfig } = require("@vue/cli-service");
module.exports = defineConfig({
transpileDependencies: true,
// 关闭语法检查
lintOnSave: false,
pages: {
index: {
// page 的入口
entry: "src/main.js",
// 模板来源
template: "public/index.html",
},
},
});
ref
属性
ref
: dom 或组件的属性,用于获取 dom 或组件的引用,可以理解为普通 dom 的 id 属性用于获取元素或组件
子组件:
<h1 ref="h1">学生姓名: {{ name }}</h1>
父组件:
<h2 ref="h2">hello{{ name }}</h2>
<button @click="show">查看ref</button>
<Student ref="student"></Student>
输出组件实例对象的$refs 属性,其中包含该组件的 DOM 和子组件的实例
show() {
console.log(this.$refs);
}
组件上的 id 属性
为组件添加 id,其实就是为组件模板中根元素 div 添加 id
<Student id="school" ref="student"></Student>
School 组件:
<template>
<div>
<h1 ref="h1">学生姓名: {{ name }}</h1>
</div>
</template>
console.log(document.querySelector("#school"))
: 输出 School 模板中的 div
总结
1.ref
: 被用来给元素或子组件注册引用信息(id 属性的代替者)
2.ref
作用于 DOM 上: key 为 ref 的值,value 为 DOM 元素对象
例如: <h2 ref="h2">hello{{ name }}</h2>
3.ref
作用于组件上: key 为 ref 的值,value 为组件实例对象
例如: <Student ref="student"></Student>
4.获取方式: this.$refs.ref属性值
props
属性用于接收父组件传递的数据
props
: 组件的属性,用于接收父组件使用该子组件视,在其标签上定义的属性
父组件向子组件传递数据
在子组件标签上使用使用自定义属性,也可以使用v-bind
指令绑定属性值,属性值可以是字符串或表达式,使属性进行动态赋值
<Student sex="男" :age="18+1"></Student>
子组件 props 属性接收父组件形式
数组形式
父组件使用子组件 Student 中定义了 sex 和 age 属性
<Student sex="男" :age="18+1"></Student>
子组件
<template>
<div>
<h1>学生姓名: {{ name }}</h1>
<h1>学生性别: {{ sex }}</h1>
<h1>学生年龄: {{ age }}</h1>
</div>
</template>
<script>
export default {
data() {
return {
name: "张三",
};
},
props: ["sex", "age"],
};
</script>
对象形式限制属性类型
props: {
sex: String,
age: Number,
}
作用: 可以对组件传入的属性进行类型限制,如果类型不匹配,则报错
<Student sex="男" age="18"></Student>
,由于父组件 age 属性值是字符串,所以会报错,因此需要使用v-bind
指令绑定属性值,属性值可以是字符串或表达式
对象形式限制属性类型和必传限制,并设置默认值
默认值为非函数值
props: {
sex: {
type: String,
required: true, // 必须传入
},
age: {
type: Number,
default: 18, // 默认值
},
}
默认值也可以是一个函数,函数返回值作为默认值,函数中 this 指向当前子组件实例
props: {
sex: {
type: String,
default() {
console.log('默认值');
console.log(this);
return "男";
},
},
age: {
type: Number,
required: true,
},
},
作用:
1.对父组件传入的属性值进行类型限制
2.对父组件传入的属性值进行默认值设置
3.对父组件传入的属性值进行必传限制
4.默认值可以是函数,该函数中 this 指向当前 mc 实例,以便处理默认值逻辑
定义在 props 中的属性不能再定义到 data 中
props 中的属性优先级高于 data 中的属性,导致 data 中相同属性会被覆盖
因此可以在 data 中直接使用 props 中的属性
避免在子组件中直接修改 prop 中的数据
<button @click="age++">修改props中的数据</button>
,直接修改 props 中的 age 属性,虽然没有报错,但是 vue 不建议直接修改
避免直接改变 prop,因为每当父组件重新渲染时,该值就会被覆盖。相反,使用基于 prop 值的 data 或 computed 属性,来创建子组件本地副本
原因: 每当父组件更新属性值时就会覆盖掉 props 中的属性值,将使子组件更新毫无意义
子组件
<template>
<div>
<span>学生年龄: {{ age }}</span><br />
<button @click="age++">子组件更新age</button>
</div>
</template>
<script>
export default {
props: ["age"],
data() {
return {
name: "张三",
};
},
};
</script>
父组件
<template>
<div>
<Student :age="age"></Student>
<button @click="age++">父组件修改age属性</button>
</div>
</template>
<script>
import Student from "./components/Student.vue";
export default {
data() {
return {
age: 18,
};
},
components: { Student },
};
</script>
子组件更新 props 属性,此时父组件也更新 props,子组件的 value 属性值会被覆盖掉,导致子组件更新毫无意义
子组件应使用 data 来确保属性值不被父组件覆盖
<span>学生年龄: {{ ageSelf }}</span><br />
<button @click="ageSelf++">子组件更新age</button>
<script>
export default {
props: ["age"],
data() {
return {
name: "张三",
ageSelf: this.age,
};
},
};
</script>
子组件使用 computed 来处理父组件修改了 props 属性值的情况
<template>
<div><span>学生年龄: {{ ageSelf }}</span><br /></div>
</template>
<script>
export default {
props: ["age"],
data() {
return {
name: "张三",
};
},
computed: {
ageSelf() {
// 父组件年龄改变了需要处理子组件逻辑...
return this.age * 10;
},
},
};
</script>
父组件无法向子组件传递 key 属性
<Student key="1"></Student>
key 属性是 vue 中用于标识组件的唯一标识,用于优化渲染性能,key 属性不能被传递给子组件
总结
1.父组件传递动态数据给子组件时,使用v-bind
指令
2.props
属性用于接收父组件传递的数据
3.接收方式:
数组形式: 只接收
对象形式: 限制类型、必传限制、默认值、默认值可以是函数,在函数中处理默认值逻辑,该函数中 this 指向当前子组件实例对象
4.避免子组件直接修改 props 中属性,因为父组件会覆盖使子组件修改毫无意义
5.子组件接收到的 props 属性早于 data 初始化属性,因此可以在 data 中直接使用 props 中的属性
6.data 中不能定义与 props 中同名的属性,否则会被覆盖
7.父组件无法向子组件传递key
、ref
属性等,因为这些属性为内置属性,不能被传递给子组件
vc
中的$attrs
属性
$attrs
属性: 当父组件传递数据给子组件时,子组件通过props属性进行接收,没有使用props属性接收的参数,将会放到$attrs属性中
<template>
<div>
<span>学生年龄: {{ age }}</span><br>
<span>学生姓名: {{ $attrs.name }}</span><br>
</div>
</template>
<script>
export default {
props: ['age'],
mounted(){
console.log(this)
},
};
</script>
$attrs属性对子组件使用props接收父组件数据进行兜底,如果子组件没有使用props接收父组件数据,那么父组件传递的数据将会放到$attrs属性中,子组件可以通过$attrs属性获取父组件传递的数据
$attrs属性并不是响应式式数据
mixin
属性: 相同逻辑抽离,方便复用
Mixin: 混入,将组件中相同的逻辑提取出来,封装成一个混入对象,在需要使用的地方引入混入对象,组件就具有混入对象的功能
局部混入
组件:
<template>
<div>
<span>学生姓名: {{ name }}</span><br />
<span>学生年龄: {{ age }}</span><br />
{{show()}}
<span>{{tip}}</span>
</div>
</template>
<script>
import { myMixin } from "../mixin.js";
export default {
data() {
return {
name: "张三",
};
},
mixins: [myMixin],
beforeCreate() {
console.log("组件 beforeCreate");
},
mounted() {
console.log("组件 mounted");
},
};
</script>
混入数据: 把部分 data 数据和方法进行抽离,方便复用
export const myMixin = {
data() {
return {
age: 20,
name: "李四",
tip: "",
mixin: "mixin",
};
},
methods: {
show() {
this.tip = "hello mixin";
},
},
beforeCreate() {
console.log("mixin beforeCreate");
},
mounted() {
console.log("mixin mounted");
},
};
组件中 data 覆盖混入中相同属性值,生命周期函数不会覆盖,并且 mixin 中的生命周期函数优于组件中生命周期函数执行
全局混入
在全局 Vue 中添加混入
1.在 main.js 中 import 混入 js
2.Vue.mixin(xxx)
3.子组件中无需再引入混入数据
main.js 全局 Vue 引入混入数据
import { myMixin } from "./mixin";
Vue.mixin(myMixin);
new Vue({
el: "#app",
render: (h) => h(App),
});
由于将混入数据配置为全局属性,那么 vm,及其子组件实例对象 vc 在创建时就已经具备了myMixin
属性,因此不需要在子组件中再次引入混入数据
由于 vm,app,student 实例都引入了混入属性,都会执行生命周期回调函函数
总结
1.混入: 使用组件重复功能或配置的方式
(1)把组件相同options
配置数据,抽离出一个 js 文件,并在 js 中暴露这个配置对象
(2)其他组件 import 混入 js 文件,并使用mixins
属性以数组形式引入,例如: mixins:[xxx]
2.组件中的 data 数据或方法会覆盖混入中相同的属性或方法
3.组件中的生命周期函数不会覆盖混入中的生命周期函数,并且混入中的生命周期函数会先执行
4.使用全局混入后其他子组件就不需要再次引入了,需要注意的是全局引入的混入声明周期函数,所有组件工作过程中都会执行,应该避免业务代码逻辑写在全局混入中
插件
插件: 用于扩展Vue
功能的 js 文件,该 js 文件中暴露一个对象,对象中通常是一个包含install
方法
定义插件
export default {
install(Vue) {
console.log("Vue", Vue);
},
};
使用插件
在 main.js 入口文件中,vm 创建前引入插件
import plugin from "./plugins.js";
Vue.use(plugin);
插件中的install
方法参数
1.Vue 构造函数: 为Vue
函数添加options
,或直接为Vue
的原型对象中添加属性或方法,使vm
和所有vc
实例对象都具有该功能
2.可选的扩展参数: 插件接收使用者传入的自定义参数,以便插件实现不同功能
Vue.use(plugin, 'hello', 'plugin');
export default {
install(Vue, ...arg) {
console.log("Vue", Vue);
console.log("arg", arg);
},
};
插件中可以做哪些事
1.插件中为Vue
构造函数的原型对象中添加属性或方法,使 vm 和所有 vc 实例对象都具有该属性或方法
2.通过 Vue 函数的静态方法添加全局指令、过滤器、混入等
1.添加全局属性和方法
export default {
install(Vue) {
Vue.prototype.$myMethod = function () {
console.log("myMethod");
};
Vue.prototype.$myProperty = "myProperty";
},
};
2.添加全局指令
export default {
install(Vue) {
Vue.directive("focus", {
inserted: function (el) {
el.focus();
},
});
},
};
3.添加全局过滤器
export default {
install(Vue) {
Vue.filter("myFilter", function (value) {
return value + "myFilter";
});
},
};
4.添加全局混入
export default {
install(Vue) {
Vue.mixin({
data() {
return {
mixin: "mixin",
};
},
});
},
};
总结
1.插件: 用于扩展 Vue 功能的 js 文件,该 js 文件中暴露一个对象,对象中通常是一个包含install
方法,install 方法会在Vue.use(plugin)
时调用
2.install 接收参数: Vue 构造函数,和可选的扩展参数
3.插件的初始化: 在 main.js 应用入口文件中引入,并使用 Vue.use(plugin)方法进行初始化
4.插件中可以添加全局属性、方法、指令、过滤器、混入等
scoped 样式: 作用域样式
没有样式作用域产生的问题
1.父组件样式受子组件样式影响
子组件样式
<style>
.name {
color: red;
}
</style>
父组件无自定义样式
父组件仍然可以使用子组件的 name 样式,这样就会导致父组件的样式不可控,有可能受子组件样式决定
2.父组件和子组件的样式冲突
父组件和子组件同时具有相同样式
<style>
.name {
color: green;
}
</style>
父组件和子组件同时具有相同样式,父组件的样式会覆盖子组件的样式,导致子组件样式不可控
scoped 样式
在样式中添加scoped
属性,使样式具有作用域,只作用于当前组件
<style scoped>
.name {
color: red;
}
</style>
scoped 的作用原理: 给当前组件的元素添加一个data-v-xxxx
属性,并给样式添加data-v-xxxx
属性选择器,使样式只作用于当前组件的元素
xxxx
: 由 vue 随机生成的唯一 ID
lang="less"
: 使用 less 预处理器
<template>
<div>
<span class="name">
子组件: {{ name }}
<span class="hobby"> 爱好: {{ hobby }} </span> </span
><br />
</div>
</template>
<style scoped lang="less">
.name {
color: red;
.hobby {
color: blue;
}
}
</style>
控制台报错:
Failed to resolve loader: less-loader
You may need to install it.
解决办法: 安装和 webpack 版本对应的 less-loader 模块
查看脚手架中 webpack 的版本:
查看可选的 less-loader 版本:
➜ vue-test git:(master) ✗ npm view less-loader versions
[
'0.1.0', '0.1.1', '0.1.2', '0.1.3', '0.2.0',
'0.2.1', '0.2.2', '0.5.0', '0.5.1', '0.6.0',
'0.6.1', '0.6.2', '0.7.0', '0.7.1', '0.7.2',
'0.7.3', '0.7.4', '0.7.5', '0.7.6', '0.7.7',
'0.7.8', '2.0.0', '2.1.0', '2.2.0', '2.2.1',
'2.2.2', '2.2.3', '3.0.0', '4.0.0', '4.0.1',
'4.0.2', '4.0.3', '4.0.4', '4.0.5', '4.0.6',
'4.1.0', '5.0.0', '6.0.0', '6.1.0', '6.1.1',
'6.1.2', '6.1.3', '6.2.0', '7.0.0', '7.0.1',
'7.0.2', '7.1.0', '7.2.0', '7.2.1', '7.3.0',
'8.0.0', '8.1.0', '8.1.1', '9.0.0', '9.1.0',
'10.0.0', '10.0.1', '10.1.0', '10.2.0', '11.0.0',
'11.1.0', '11.1.1', '11.1.2', '11.1.3', '11.1.4',
'12.0.0', '12.1.0', '12.2.0', '12.3.0'
]
选择一个次新版本安装:
➜ vue-test git:(master) ✗ npm i less-loader@11.1.4
此时项目就支持了 less 语法了
父组件添加scoped后影响了子组件样式
父组件:
<template>
<div class="parent">
APP
<Student></Student>
</div>
</template>
<script>
import Student from "./components/Student.vue";
export default {
components: { Student },
};
</script>
<style scoped>
.parent {
color: red;
}
</style>
子组件:
<template>
<div class="parent">
子组件
</div>
</template>
<script>
export default {
};
</script>
<style scoped>
.parent {
color: green;
}
</style>
按照scoped
的作用: 当前组件样式作用域只对当前组件有效,而实际上子组件和父组件都是红色
原因: vue将会由内向外渲染组件,渲染过程中先生成样式,子组件生成.parent[ebb]
绿色,而后生成父组件.parent[d90]
红色,且子组件标签属性中会保留父组件的标记d90
,因此.parent[d90]样式会覆盖.parent[ebb]样式,作用到子组件。
总结
1.scoped
并不能完全隔离组件之间的样式互不影响
2.当前子组件样式影响到父组件时,应该在子组件添加scoped
属性
3.scoped只能保证子不影响父,而不能保证父不影响子
4.当带有scoped的父组件影响到子组件时,应该检查父子组件的class样式名是否重名
Todo-List 案例
需求: 创建一个 Todo-List 应用,实现以下功能:
1.输入框输入内容,按下回车键,将输入的内容添加到列表中,默认为待完成事项
2.每个事项都有删除按钮,删除后总事项个数减一
3.勾选某个事项后,表示该事项已完成,并统计已完成事项个数
4.全选功能,并展示已完成事项个数和全部事项个数
nanoid
库生成随机id
安装nanoid
库:
➜ vue-test git:(master) ✗ npm i nanoid
使用 nanoid 插件生成随机 id:
import { nanoid } from "nanoid";
使用:
const id = nanoid(); // jn5zU5quWVMXekPjBvg_V
后代组件向父祖先组件传递数据
实现原理:
1.props 属性用于接收父组件动态数据
2.引用传递
实现流程:
1.父组件向子组件传递一个函数
<AddTodo :addTodo="addTodo"></AddTodo> methods: { addTodo(todo) {
this.todos.unshift(todo); }, }
2.子组件接收父组件函数
props: ['addTodo'],
3.子组件调用父组件函数
this.addTodo(参数);
后代组件修改祖先组件中的数据
双向绑定实现(不推荐)
双向绑定作用在<input type=checkbox v-model="属性" >
复选框上时,会将属性值作用在标签的checked
属性上,可实现勾选取消勾选
后代组件,双向绑定了祖先组件传过来的 todo 对象中的属性值
<input type="checkbox" v-model="todo.checked" /> props: ['todo']
vue 中原则是不能修改 props 中的数据的,但是由于 vue 监听 props 中的数据方式属于浅层次监听,因此修改对象中的属性值并没有提示如下警告
使用双向绑定方式违反原则: 单向数据流
修改 props 中的数据,同时也修改了父组件的数据,破坏了数据流的单向传递性,使数据流向变的复杂且难以调试
祖父组件向后代组件传递函数引用实现
祖父组件中定义函数,将函数一层层传递给后代组件中,以便调用
总结
组件化编码流程
1.拆分静态组件: 组件要按照功能点拆分,组件名称不要与 html 标签名重名
2.考虑好功能数据应该存放在哪个组件中
(1)一个组件在用: 放到自身组件中
(2)两个组件在用: 放到共同的父组件中(状态提升)
3.实现功能
通过事件绑定、props、v-model 实现组件间通信
组件间数据传递
1.祖先组件向后代组件传递数据: props
2.后代组件向祖先组件传递数据: 祖先组件传递一个函数,后代组件调用这个函数,把数据当作参数传递给这个函数
使用 v-model 切记
v-model 不能绑定 props 中的数据,原因 vue 规范中推荐组件之间数据流的单向性,应该避免后代组件直接修改 props 数据
vue 只能浅层监听 props 数据是否修改了,如果修改了 props 中对象的属性,vue 则不会提示警告,但是也应该避免修改 props 中的数据
组件自定义事件
组件自定义事件: 子组件向父组件传递数据,通过事件的方式
实现原理: 父组件中使用了子组件,在父组件中就可以为该子组件绑定自定义事件,事件的回调函数处理逻辑在父组件中,在子组件触发事件时,就会执行父组件的回调函数
vue 在创建 vc 实例时,并解析该组件的标签中是否有自定义事件,如果有就为 vc 绑定事件,而后通过子组件通过this.$emit
方法触发事件,父组件的回调函数就会执行
$emit
方法:
作用: 触发组件事件执行
语法: $emit('事件名', 参数)
参数: 如果参数较多,回调函数可以使用扩展运算符接收,或者传递对象结构
父组件通过v-on
指令为子组件绑定(指令形式绑定)
1.父组件通过v-on
或@
绑定showName
事件,并指定回调函数为showNameCallback
<Student @showName="showNameCallback" :age="age"></Student>
methods: {
showNameCallback(name, ...args) {
this.studentName = name;
console.log('子组件调用父组件方法');
},
}
2.子组件使用$emit 方法触发事件
methods: {
showName() {
console.log(this);
this.$emit('showName', this.name, this.hobby);
},
}
父组件通过$refs
为组件实例绑定(方法形式绑定)
1.vue 挂载完成后,可以使用$refs
获取组件实例,使用$on
为组件实例绑定自定义事件
$on
: 作用: 为组件实例绑定自定义事件
语法: $on('事件名', 回调函数)
<Student ref="student"></Student>
methods: { showNameCallback(name, ...args) {
this.studentName = name; console.log('子组件调用父组件方法', name, args);
},
},
mounted() {
this.$refs.student.$on('showName', this.showNameCallback);
},
2.子组件使用$emit 方法触发事件
methods: {
showName() {
console.log(this);
this.$emit('showName', this.name, this.hobby);
},
}
组件自定义事件只触发一次
方法形式绑定:
$once
: 作用: 为组件实例绑定自定义事件,只触发一次
语法: $once('事件名', 回调函数)
mounted() {
this.$refs.student.$once('showName', this.showNameCallback);
},
指令形式绑定:
v-on
指令绑定事件时,可以添加.once
修饰符,表示只触发一次
<Student @showName.once="showNameCallback" :age="age"></Student>
方法形式和指令形式绑定自定义事件区别
1.指令形式绑定自定义事件,在组件实例销毁时,会自动解绑事件
2.方法形式可以在指定时机绑定事件,如请求接口响应后绑定事件等,更加灵活
组件解绑自定义事件
为什么要解绑自定义事件
防止内存泄漏:
当组件销毁时,如果事件监听器未被移除,父组件会继续持有对已销毁组件的回调函数引用
结果:垃圾回收器无法释放该组件占用的内存,随着组件频繁创建/销毁,内存占用持续上升,最终导致页面卡顿甚至崩溃
通过显式解绑事件,既能避免内存泄漏,又能确保组件销毁后不会产生残留操作,这是 Vue 组件生命周期管理的重要环节
$off()
解绑自定义事件
通过 vc 实例销毁时,使用$off
方法解绑事件
销毁一个组件事件:
this.$off('事件名')
销毁多个组件事件:
this.$off(['事件名 1', '事件名 2'])
销毁所有组件事件:
this.$off()
methods: {
unbind() {
this.$off('showName');
},
}
组件自定义事件没有冒泡机制
系统事件冒泡: 当一个元素接收到事件对象的时候(触发事件),会把他接收到的事件对象传给自己的父级,如果父级监听了该事件就会执行父级的回调函数,一直向上传播,直到window对象,这种传播过程称为事件冒泡,事件冒泡是默认存在的,和当前元素有无绑定事件无关,即使没有绑定事件,事件对象也会向上传播。
在vue中父组件通过$on
为子组件vc
实例对象绑定了事件并添加了回调函数,因此只有vc
通过$emit
触发事件
而vc
引用的子组件的实例对象并没有绑定事件,因此孙子组件使用$emit
并不会触发事件
methods: {
sendScore() {
console.log('score');
this.$emit('showName', this.score);
},
},
使孙子组件触发祖先组件的事件
孙子组件使用ref属性,通过祖先组件就可以获取到孙子组件实例对象,而后为其绑定事件
孙子组件
<Score ref="score"></Score>
祖先组件为其绑定事件
mounted() {
console.log(this);
// 子组件绑定事件
this.$refs.student.$on('showName', this.showNameCallback);
// 孙子组件绑定事件
this.$refs.student.$refs.score.$on('showName', this.showNameCallback);
},
只有为孙子组件实例绑定事件后,才能触发祖先组件的事件
总结
1.为某个组件绑定事件时,可以直接写回调函数,且回调函数应该是箭头函数,否则 this 指向的不是父组件实例对象,而是子组件实例对象
mounted(){
this.$refs.student.$refs.score.$on('showName', (...args)=>{
console.log('触发了祖先组件');
console.log('参数:', args);
});
}
组件自定义事件回调函数中this和原生js中系统事件回调函数中this具有相同含义:
原生js系统事件回调函数中this指向的是触发事件的元素对象
组件自定义事件回调函数中this指向的是绑定了事件的组件对象(哪个组件具有事件,那么this就指向哪个组件对象)
vue在设计时就确定了,哪个实例组件对象触发了事件,那么无论该事件的回调函数在哪里,可能在父组件也能在祖先组件中
那么方法形式的回调函数中this就指向触发了事件的组件实例,因此应该使用箭头函数将其中的this和外层方法中this保持一致,外层方法可以是methods中的方法
也可能是生命周期的方法,它们的this都是当前的组件实例对象,便于操作当前实例数据,如,为data赋值,调用当前实例方法等
2.如果使用了方法函数,其this指向绑定了事件的组件实例对象
this.$refs.student.$refs.score.$on('showName', function (...args) {
console.log('触发了祖先组件');
console.log('参数:', args);
console.log(this);
});
3.为组件绑定事件可以绑定多次,每次触发后都会执行
mounted() {
this.$refs.student.$refs.score.$on('showName', (...args) => {
console.log('触发了祖先组件');
console.log('参数:', args);
});
this.$refs.student.$refs.score.$on('showName', function (...args) {
console.log('触发了祖先组件');
console.log('参数:', args);
});
},
4.组件使用@事件名.native
修饰符为组件绑定原生事件
<Score @click.native="systemEvent"></Score>
5.使用系统事件时只能对组件绑定,不能对组件的子元素进行绑定,否则会报错
<template>
<div class="student" @click.native="systemEvent">
<span>
子组件: {{ name }}
<span>爱好: {{ hobby }}</span>
</span>
<br />
<button @click="showName">子组件触发父组件方法</button>
<button @click="unbind">销毁当前组件的自定义事件</button>
<Score ref="score"></Score>
</div>
</template>
6.为组件绑定系统事件,其实就是为组件的根元素绑定事件,这也是template
中必须只有一个根元素的原因之一
<Score @click.native="systemEvent"></Score>
7.组件的系统事件具有冒泡性质,即子组件的点击事件会冒泡到父组件,会执行父组件回调函数
只对Student
绑定系统事件,那么点击Score
组件时会把事件对象冒泡给Student
组件,Student
组件会执行回调函数
8.组件绑定自定义事件方式
1.父组件中在子组件标签中添加@事件名="回调函数"
<Student @showName="showNameCallback"></Student>
2.父组件中通过$on为子组件实例对象绑定事件
this.$refs.student.$on('showName', this.showNameCallback);
3.组件自定义事件只触发一次
this.$refs.student.$once('showName', this.showNameCallback);
9.触发事件
绑定了事件的组件实例中使用: this.$emit('事件名', 参数)
10.解绑事件
this.$refs.student.$off('showName');
全局事件总线
全局事件总线(Global Event Bus): 实现任意组件间相互通信
在组件的自定义事件中分为两个关键点
1.为vc
实例绑定事件,通过$on('事件名', 箭头函数)
绑定事件
2.vc
实例通过this.$emit('事件名')
触发事件
在当前组件中为子组件的实例对象绑定事件,同时回调函数也在此定义,便于服务于当前组件
子组件通过实例对象触发事件,从而回调了当前组件的回调函数。
通过相同的实现原理,我们可以定义一个全局的vc
实例对象,该对象可以使用$on
、$emit
、$off
等方法,
各个组件都可以使用该vc
对象,并为其绑定事件,而回调函数留在各个组件中,服务于各个组件,
由于vc
是全局实例,那么各个组件可以获取到全局vc
,并触发指定的事件,从而执行了各个组件中回调函数,实现了组件间通信
实现全局事件总线
1.在Vue初始化实例前,创建一个vc实例,并把这个vc实例作为Vue的原型对象中一个xxx
属性,因此该xxx
属性也就具有了$on、$emit、$off等方法
const VcFn = Vue.extend({});
Vue.prototype.xxx = new VcFn(); // 全局vc实例
new Vue({
el: '#app',
render: h => h(App),
});
2.App组件挂载后通过this.xxx.$on('事件名', 箭头函数)
为xxx
绑定事件
mounted() {
this.xxx.$on('score', newScore => {
console.log('App组件接收其他组件参数', newScore);
this.score = newScore;
});
},
App组件:
3.Score组 件通过this.xxx.$emit('事件名', 参数)
触发事件
<button @click="xxx.$emit('score', score++)">通信</button>
vue推荐使用beforeCreate
钩子中使用当前vm
实例作为全局事件总线
new Vue({
// 在初始化事件和响应数据前,为Vue的原型安装全局事件总线对象
beforeCreate() {
Vue.prototype.$bus = this; // 安装全局事件总线,$bus就是当前应用的vm
},
render: h => h(App),
}).$mount('#app');
总结
1.全局事件总线本质就是一个具有Vue原型对象中$on
、$once
、$off
、$emit
等方法的对象,该对象可以是vc
实例也可以vm
实例,更像是一个傀儡对象,当前组件中用它绑定事件,其他组件使用它来触发事件,而回调函数的执行却在当前组件中
2.vue的最佳实践是把当前项目的vm实例作为全局事件总线
$bus
: 为了迎合vue
的命名规范($xxx
给开发员使用的方法),全局事件总线一般命名为$bus
,
new Vue({
beforeCreate() {
Vue.prototype.$bus = this; // 安装全局事件总线,$bus就是当前应用的vm
})
3.由于全局事件总线为vm
实例,因此vm
实例绑定的事件不会自动销毁,当某一个组件销毁时,应该在该组件中手动销毁已绑定的事件
this.$bus.$off('事件名');
4.组件销毁时不能使用this.$bus.$off()
,否则会销毁全局事件总线中所有事件,导致其他组件无法使用全局事件总线
5.全局事件总线中的事件只能服务于单个业务场景,不能在其他组件中复用相同的事件,原因是组件销毁,该事件也会销毁,因此不能在其他组件中复用
6.多个组件绑定相同事件时,都会执行各自的回调函数,和原生js绑定多个事件相同
消息订阅与发布
消息订阅与发布: 实现任意组件间通信
通过pubsub-js
库实现
安装pubsub-js
库:
➜ vue-test git:(master) ✗ npm i pubsub-js
App组件订阅消息:
import pubsub from 'pubsub-js';
mounted() {
this.msgId = pubsub.subscribe('test', function (name, args) {
console.log('订阅回调中的this', this);
console.log('订阅回调中的参数', name, args);
});
},
beforeDestroy() {
pubsub.unsubscribe(this.msgId);
},
其他组件发布消息:
import pubsub from 'pubsub-js';
methods: {
del() {
if (confirm('确定删除吗?')) {
pubsub.publish('test', '参数');
}
},
},
总结
1.消息订阅与发布和全局事件总线类似,都是实现组件间通信,通过安装pubsub-js
库实现
2.订阅回调函数接收两个参数,第一个参数始终为消息名称,第二个参数为传递的参数对象,因此多个数据应放在对象中传递
3.订阅回调函数为普通函数时,this为undefined,为箭头函数时,this为当前组件实例对象
4.和全局事件总线一样,需要在组件销毁时取消订阅,unsubscribe(消息ID)
,消息ID由订阅时返回
$nextTick
执行回调
edit() {
if (!this.editType) {
this.show = !this.show;
this.editType = 1;
// input获取焦点
this.$refs.inputTitle.focus();
}
}
如上代码,在编辑后展示输入框并获取焦点,由于vue在方法执行完毕后才能解析模板,此时input框并不存在,因此报错
使用$nextTick
$nextTick(回调函数)
: 将回调延迟到下次DOM更新循环之后执行,例如,在methods方法执行后,vue进行重新解析模板,解析成功后,会执行$nextTick
中的回调函数
edit() {
if (!this.editType) {
this.show = !this.show;
this.editType = 1;
// input获取焦点
this.$nextTick(() => {
console.log('执行了nextTick');
this.$refs.inputTitle.focus();
});
}
}
使用指定0秒定时器实现(不推荐)
edit() {
if (!this.editType) {
this.show = !this.show;
this.editType = 1;
// input获取焦点
setTimeout(() => {
this.$refs.inputTitle.focus();
});
}
}
$nextTick
、指令的inserted
、updated
生命周期函数、mounted
生命周期函数、setTimeout
执行顺序
如上需求,使input
框显示并自动获取焦点,可以使用$nextTick
、指令的inserted
、updated
生命周期函数、mounted
声明周期函、setTimeout
定时器实现
mothods: {
edit() {
if (!this.editType) {
this.show = !this.show;
this.editType = 1;
// input获取焦点
this.$nextTick(() => {
console.log('执行了nextTick');
});
setTimeout(() => {
console.log('执行了setTimeout');
});
}
}
},
directives: {
focus: {
inserted: function (el) {
console.log('执行了指令inserted');
},
},
},
updated() {
console.log('执行了updated');
},
mounted() {
console.log('执行了mounted');
},
mounted
和当前vc组件挂载有关,如果Item
组件有多个,mounted
就会执行多次,由于看电影Item已经挂载完毕,因此mounted
不再执行
指令inserted
>updated
>nextTick
>setTimeout
当在Item组件添加一个<input type="text" v-focus>
时,v-focus
指令中的inserted
和mounted
都会执行,且inserted
优于mounted
执行
由此可见它们的执行顺序:
指令inserted
>mounted
>updated
>nextTick
>setTimeout
总结
1.mounted
只能保证当前组件挂载完毕后执行
2.mounted
不会保证所有的子组件也都被挂载完成。如果你希望等到整个视图都渲染完毕再执行某些操作,可以在mounted
内部使用vm.$nextTick
mounted: function () {
this.$nextTick(function () {
// 仅在整个视图都被渲染之后才会运行的代码
})
}
动画效果
Test组件:
<template>
<div>
<button @click="show = !show">显示与隐藏</button>
<div v-if="show" class="test enter">vue</div>
</div>
</template>
<script>
export default {
data() {
return {
show: true,
};
},
};
</script>
<style scoped lang="less">
.test {
height: 50px;
width: 300px;
background-color: #4fc08d;
text-align: center;
}
// 定义动画
@keyframes slide {
from {
transform: translateX(-300px);
}
to {
transform: translateX(0px);
}
}
// 应用动画
.enter {
animation: slide 1s;
}
.leave {
animation: slide 1s reverse;
}
</style>
通过指定css样式添加动画
为Test组件中指定的元素添加从左向右动画效果,那么该元素应有enter
样式
使用transition
标签实现
1.定义动画
@keyframes slide {
from {
transform: translateX(-300px);
}
to {
transform: translateX(0px);
}
}
2.使用vue固定的样式名.v-enter-active
(显示样式)和.v-leave-active
(隐藏样式)应用动画
.v-enter-active {
animation: slide 1s;
}
.v-leave-active {
animation: slide 1s reverse;
}
3.使用transition
(过渡动画)标签包裹需要添加动画的元素
<template>
<div>
<button @click="show = !show">显示与隐藏</button>
<transition>
<div v-if="show" class="test">vue</div>
</transition>
</div>
</template>
当transition
包裹的元素显示时,添加.v-enter-active
样式,隐藏时添加.v-leave-active
样式
在transition
标签上自定义name
属性样式名前缀
作用: 用于区分在相同组件中不同的元素使用不同的动画效果
name属性: 可以自定义动画样式名前缀,默认为v
,<transition name="前缀名"></transition>
样式:
.前缀名-enter-active {
animation: slide 1s;
}
.前缀名-leave-active {
animation: slide 1s reverse;
}
<template>
<div>
<button @click="show = !show">显示与隐藏</button>
<transition name="hello">
<div v-if="show" class="test">vue</div>
</transition>
</div>
</template>
<style scoped lang="less">
.hello-enter-active {
animation: slide 1s;
}
.hello-leave-active {
animation: slide 1s reverse;
}
</style>
在transition
标签上添加appear
属性实现初次渲染动画
刷新页面时没有动画
<transition name="hello" :appear="true">
简写: <transition name="hello" appear>
总结
vue的过渡动画不和定义的动画有直接关系,而是和应用的样式有直接关系
过渡效果
使用transition
标签包裹的元素,当元素显示和隐藏时,会触发过渡效果,和动画效果实现方式相同,vue会自动为元素添加响应的样式
前缀-enter
: 进入过渡的开始状态
前缀-enter-to
: 进入过渡的结束状态
前缀-leave
: 离开过渡的开始状态
前缀-leave-to
: 离开过渡的结束状态
前缀-enter-active
: 进入过渡效果
前缀-leave-active
: 离开过渡效果
每组动画都会有: 起始状态样式
,结束状态样式
,过渡效果样式
// 进入起点
.hello-enter {
transform: translateX(-100%);
}
// 进入终点
.hello-enter-to {
transform: translateX(0);
}
// 离开起点
.hello-leave {
transform: translateX(0);
}
// 离开终点
.hello-leave-to {
transform: translateX(-100%);
}
// 进入和离开过渡效果
.hello-enter-active,
.hello-leave-active {
transition: all 1s;
}
简写:
// 进入起点和离开终点
.hello-enter,.hello-leave-to {
transform: translateX(-100%);
}
// 进入终点和离开起点
.hello-enter-to,.hello-leave {
transform: translateX(0);
}
// 进入和离开过渡效果
.hello-enter-active,
.hello-leave-active {
transition: all 1s;
}
为什么看不到.hello-enter
和.hello-leave
样式应用
.hello-enter
和.hello-leave
分别是进入起点和离开起点
当元素开始进入时添加.hello-enter
样式,还没走到下一帧时,vue自动移除了.hello-enter
样式
当元素开始离开时添加.hello-leave
样式,还没走到下一帧时,vue自动移除了.hello-leave
样式
它们添加和移除样式速度极快,因此看不到元素应用该样式
多个元素具有相同过渡效果应使用transition-group
标签
transition
标签: 针对单元素过渡效果
transition-group
标签: 针对多个元素具有相同过渡效果
多个元素过渡和单个元素过渡实现方式相同,只是多个元素过渡需要使用transition-group
标签包裹元素,而不是transition
标签
<transition name="hello" :appear="true">
<div v-if="show" class="test">vue</div>
<div v-if="show" class="test">vue</div>
</transition>
当多个元素具有相同过渡效果时,不能使用transition
标签,而应该使用transition-group
标签,否则报错
并且transition-group
标签的元素必须具有key
属性
当有相同标签名的元素切换时,需要通过key
设置唯一的值来标记以让 Vue 区分它们,否则 Vue 为了效率只会替换相同标签内部的内容。即使在技术上没有必要,给在 <transition>
组件中的多个元素设置key
是一个更好的实践
不使用transition-group
需要分别包裹
<transition name="hello" :appear="true">
<div v-if="show" class="test">vue</div>
</transition>
<transition name="hello" :appear="true">
<div v-if="show" class="test">vue</div>
</transition>
使用transition-group
:
<transition-group name="hello" :appear="true">
<div v-if="show" class="test" key="1">vue</div>
<div v-if="show" class="test" key="2">vue</div>
</transition-group>
使用transition-group,应对相同标签的互斥元素的显示隐藏
<transition-group name="hello" :appear="true">
<div v-if="show" class="test" key="1">vue</div>
<div v-if="!show" class="test" key="2">ts</div>
</transition-group>
总结
transition
标签: 针对单元素过渡效果
transition-group
标签: 针对多个元素具有相同过渡效果
transition-group
标签作用:
1.针对多个元素具有相同过渡效果,减少重复的transition
标签
2.针对互斥元素的显示隐藏
3.transition-group
标签中的key属性,用于区分相同标签的不同元素,提高vue渲染效率
集成第三方动画库Animate.css
1.安装
➜ vue-test git:(master) ✗ npm install animate.css --save
2.引用并使用
<transition-group appear
name="animate__animated animate__bounce"
enter-active-class="animate__zoomIn"
leave-active-class="animate__zoomOut"
>
<div v-if="show" class="test" key="1">vue</div>
<div v-if="!show" class="test" key="2">ts</div>
</transition-group>
<script>
// 引用animate.css
import 'animate.css';
</script>
name属性值: animate__animated animate__bounce
enter-active-class属性值: 选择动画样式
leave-active-class属性值: 选择动画样式
选择动画样式:
总结vue封装动画与过渡
使用动画与过渡的前提: 必须先使用transition
或transition-group
标签包裹
动画元素必须先包裹在transition
或transition-group
标签,组件和transition
或transition-group
标签不能同时被vue
解析,否则动画效果不生效
作用
在对元素进行交互时,插入、删除、更新DOM元素时,在合适的时候给元素添加样式类名
vue
封装的transition
和transition-group
标签只是在合适的时机为元素添加样式,至于使用动画还是过渡,由程序员自主决定
v-enter
: 进入起点样式
v-enter-to
: 进入终点样式
v-leave
: 离开起点样式
v-leave-to
: 离开终点样式
v-enter-active
: 进入过渡效果
v-leave-active
: 离开过渡效果
动画方式实现应用
1.定义动画关键帧
2.v-enter-active
和v-leave-active
设置过渡效果
过渡方式实现应用
v-enter
和v-enter-to
设置进入起点和终点样式
v-leave
和v-leave-to
设置离开起点和终点样式
v-enter-active
和v-leave-active
设置过渡效果
集成第三方动画库应用
不同第三方库集成方式不同
使用transition
或transition-group
应该包裹整个组件,不能包裹组件中的根元素,否则新增组件动画不生效
Item组件
<template>
<transition>
<li class="item">
<div>
<input type="checkbox" :checked="todo.checked" @change="$bus.$emit('updateDone', todo.id)" />
<span v-if="show">{{todo.name}}</span>
<input ref="inputTitle" v-if="!show" type="text" v-model="changeTodo" @change="changeEdit" />
</div>
<div class="d-btn">
<button @click="edit" class="btn-edit">{{editType === 0 ? "编辑" : '保存'}}</button>
<button @click="del" class="btn">删除</button>
</div>
</li>
</transition>
</template>
<style scoped>
@keyframes slide {
from {
transform: translateX(-300px);
}
to {
transform: translateX(0px);
}
}
.v-enter-active {
animation: slide 1s;
}
.v-leave-active {
animation: slide 1s reverse;
}
</style>
由于新增Item时,<transition>
标签并不存在,因此vue不会将动画应用于新增动画,只能用于删除
而是应该将<transition>
标签包裹整个组件,而不是组件中的根元素
<transition-group>
<Item v-for="t in todos" :key="t.id" :todo="t"></Item>
</transition-group>
使用Animate.css第三方动画库,也应该将<transition-group>
标签包裹整个组件,而不是组件中的根元素
<transition-group
name="animate__animated animate__bounce"
enter-active-class="animate__bounceInRight"
eave-active-class="animate__bounceOutRight"
>
<Item v-for="t in todos" :key="t.id" :todo="t"></Item>
</transition-group>
代理
正向代理与反向代理
正向代理: 代理的是客户端,代表客户端访问,客户端隐藏了对目标服务器的身份,目标服务器不知道请求是由代理服务器发起的
反向代理: 代理的是服务器,代表服务端响应,服务器隐藏了对客户端的身份,客户端不知道请求是由代理服务器处理的,仍然认为是直接访问目标服务器
正向代理帮客户端请求,反向代理绑定服务端响应
什么是跨域
什么是跨域: 前端静态资源交给浏览器渲染显示,用户在浏览器交互时,浏览器执行javascript
代码,例如查看数据详情时,浏览器执行了javascript
代码,并触发了浏览器发起网络请求,浏览器浏览器为了安全,不允许跨域请求,因此当请求的origin
信息和响应的origin
信息不同,就会产生跨域问题
origin: 协议+域名+端口号,例如: http://localhost:8080
任意三点之一不同则会跨域: 1. 协议不同 2. 域名不同 3. 端口号不同
跨域过程中,服务器接收了请求并正常返回结果,但由于浏览器限制,浏览器会拒绝接收结果,从而产生跨域问题
解决跨域方式
CORS
方式
CORS: 服务端解决,在响应头中通添加请求头,使是浏览器允许接收跨域结果
HttpServletResponse response = (HttpServletResponse) servletResponse;
response.setHeader("Access-Control-Allow-Origin", "*");
response.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
response.setHeader("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
response.setHeader("Access-Control-Allow-Credentials", "true");
nginx
服务端反向代理
核心原理和CORS方式相同,都是通过设置 Access-Control-Allow-* 系列 HTTP 响应头实现跨域控制。
前端请求接口域名必须指向nginx
所在服务器,而后nginx
进行反向代理请求,添加cors
,转发请求到后端服务,nginx
拿到后端数据,而后返回给浏览器,浏览器拿到数据,进行渲染。
vue-cli
本地反向代理
vue-cli
脚手架解决跨域仅限于本地开发,会在本机开启代理服务,因此本地前端项目域名为http://localhost:8080
,那么本地的代理服务代理了http://localhost:8080/api
发起请求,通过代理服务请求目标服务,目标服务返回结果给代理服务,代理服服务再把数据返回给浏览器,因此浏览器不直接和目标服务交互,而是和代理服务交互,因此浏览器不会产生跨域问题。
vue-cli解决跨域的原理:
使用axios
库发起网络请求
1.安装
➜ vue-test git:(master) ✗ npm i axios
2.引入
import axios from 'axios';
axios.get('http://localhost:8080/student').then(res => {
console.log(res)
})
vue-cli
代理
方式一配置
vue.config.js配置
module.exports = {
devServer: {
// 目标接口地址
proxy: 'http://localhost:4000'
}
}
请求本地同源url资源代理服务会转发到目标接口地址:
axios.get('http://localhost:8080/student').then(res => {
console.log(res)
})
弊端:
1.不能配置多个代理地址
2.不能由开发者决定相应的资源是否需要代理
例如,本地项目public
目录下有student
文件,那么在访问http://localhost:8080/student
时
代理服务不会将请求转发给http://localhost:4000/student
原因: 代理服务先在本地寻找资源,如果找到,则不会转发请求,会直接访问本地文件,而不是代理到目标服务器
配置代理:
const { defineConfig } = require('@vue/cli-service');
module.exports = defineConfig({
devServer: {
proxy: 'http://localhost:4000',
},
});
请求资源:
methods: {
getNetMsg() {
axios.get('http://localhost:8080/student').then(res => {
console.log(res);
});
},
}
代理服务现在public目录下寻找资源,如果找到了,则不会转发请求到http://localhost:4000/student
方式二完整对象配置
const { defineConfig } = require('@vue/cli-service');
module.exports = defineConfig({
devServer: {
proxy: {
'/api': {
target: 'http://localhost:4000',
pathRewrite: {
'^/api': ''
}
}
}
},
});
target
: 代理服务器地址
/api
: 匹配规则前缀
匹配以/api
开头的请求,将请求转发到http://localhost:4000
例如发起请求: http://localhost:8080/api/student
代理服务匹配到了/api
,将请求转发到http://localhost:4000/api/student
pathRewrite
: 重写path
路径
path路径: 域名后面路径部分,例如: http://localhost:8080/api/student
,path路径为: /api/student
pathRewrite: {}
: 重写路径配置对象
使用正则表达式对原始路径进行匹配和修改
pathRewrite: {
'^/api': ''
}
例如发起请求: http://localhost:8080/api/student
通过pathRewrite重写路径,将/api
替换为空字符串,因此最终请求地址为: http://localhost:4000/student
changeOrigin: true
: 用于控制请求头中host
的值
host
: 请求头的host
字段,用于标识请求的源地址
默认值为true
: 代理服务修改请求头host为目标域名,服务端获取的host始终和服务端域名保持一致
例如: http://localhost:8080/api/student
那么转发后请求的域名为: http://localhost:9900/student
,服务端接收请求获取请求源域名为http://localhost:9900/student
浏览器请求头中host始终为http://localhost:8080
changeOrigin: true
: 修改了请求头中host
的值为代理服务器的域名,后端接口中获取host请求头不再是本地域名,而是代理服务器的域名
改为false
: 代理服务告诉服务端真实的请求域名
例如: http://localhost:8080/api/student
那么转发后请求的域名为: http://localhost:9900/student
,服务端接收请求获取请求源域名为http://localhost:8080/student
总结
1.简单代理配置:
module.exports = defineConfig({
devServer: {
proxy: 'http://localhost:4000',
},
});
(1)只能配置一个代理地址
(2)优先访问本地资源,如果访问不到才转发到目标地址
2.完整代理配置:
module.exports = defineConfig({
devServer: {
proxy: {
'/api': {
target: 'http://localhost:4000',
pathRewrite: {
'^/api': ''
}
}
}
},
});
(1)自定义匹配pathName
,配置多个代理地址
(2)灵活控制请求是否走代理
(3)转发时可修改pathName
路径
(4)修改请求头中Host
值,是否为代理地址的域名,默认为true
,避免服务端拒绝非本地请求
vue-resource
库发起网络请求(不推荐)
vue-resource
库发起网络请求,需要引入vue-resource
库,并安装该插件
1.安装vue-resource库:
➜ vue-test git:(master) ✗ npm i vue-resource
2.vue安装插件:
import Vue from 'vue';
import VueResource from 'vue-resource';
Vue.use(VueResource);
3.发起网络请求
this.$http.get('http://localhost:3000/api/getnewslist').then(function(res){})
vue-resource
和axios
的基本用法和返回值基本一致
插槽
插槽: 在某组件中定义一个占位符,其他组件在使用该组件时可动态填充该组件中的占位符,其中作用域插槽也是子组件向父组件通信的方式
默认插槽
Category组件中定义一个插槽
<div class="category">
<div class="title">{{title}}</div>
<!-- 插槽: 动态插入组件中内容 -->
<slot></slot>
</div>
App组件使用Category组件并自定义插槽模板内容
<div class="category-all">
<Category title="食物">
<img :src="footPic" />
</Category>
<Category title="电影">
<ul>
<li v-for="(movie, index) in movies" :key="index">{{ movie }}</li>
</ul>
</Category>
<Category title="爱好">
<video src="http://clips.vorwaerts-gmbh.de/big_buck_bunny.mp4" controls></video>
</Category>
</div>
<style>
.category-all {
display: flex;
flex-wrap: wrap;
}
img {
width: 100%;
}
video {
width: 100%;
}
</style>
注意
1.插槽模板内容对应的css样式,可以写在填充插槽的父组件中,也可以写在插槽所在的子组件中
区别在于:
写在父组件中: vue先解析模板并添加样式,而后替换掉子组件的插槽
写在子组件: vue先解析模板,而后替换掉子组件的插槽,追后添加插槽内容的样式
2.当父组件没有填充子组件的插槽时,则会显示子组件插槽中的默认内容
<Category title="食物">
<!-- <img :src="footPic" /> -->
</Category>
<div class="category">
<div class="title">{{title}}</div>
<!-- 插槽: 动态插入组件中内容 -->
<slot>插槽默认内容</slot>
</div>
具名插槽
具名插槽: 通过name
属性指定插槽名称,在父组件中使用slot
属性指定插槽名称,将内容填充到对应的插槽中
子组件定义多个插槽使用name属性区分:
<div class="category">
<div class="title">{{title}}</div>
<!-- 插槽: 动态插入组件中内容 -->
<slot name="center"></slot>
<slot name="footer"></slot>
</div>
父组件根据slot属性指定插槽名称,填充不同的内容:
<Category title="食物">
<img slot="center" :src="footPic" />
<a slot="footer" href="https://www.baidu.com">查看更多</a>
</Category>
注意
父组件中如果将多个元素填充到指定的插槽中,应该使用teamplate
标签,而且必须使用v-slot:插槽名
指令来指定插槽
优点:
1.减少一层div
节点的包裹
2.减少每个元素添加slot
属性
<Category title="爱好">
<video slot="center" src="http://clips.vorwaerts-gmbh.de/big_buck_bunny.mp4" controls></video>
<template v-slot:footer>
<a href="#">查看更多</a>
<h4>选择喜欢类型</h4>
</template>
</Category>
作用域插槽
作用域插槽: 子组件的插槽向使用者(父组件)传递数据,父组件根据数据动态渲染样式内容
使用场景: 子组件只负责输出数据,使用者负责填充插槽内容,即父组件只使用子组件的数据,而插槽内容样式由父组件决定
子组件插槽向父组件传递一个具有gamesAttr
属性值的对象,gamesAttr
的值为子组件中games
的值
<div class="category">
<div class="title">{{title}}</div>
<!-- 插槽: 动态插入组件中内容 -->
<slot :gamesAttr="games"></slot>
</div>
<script>
export default {
name: 'Category',
props: ['title'],
data() {
return {
games: ['红色警戒', '穿越火线', '英雄联盟', '地下城与勇士'],
}
}
</script>
使用者必须在template
标签中使用scope
属性,用来定义一个属性,接收插槽传递的数据。指定scope
的值为slotProps
,那么slotProps
就是用于接收子组件插槽传递过来的对象的变量:
<Category title="游戏">
<template scope="slotProps">
<ul>
<li v-for="(game, index) in slotProps.gamesAttr" :key="index">{{ game }}</li>
</ul>
</template>
</Category>
<Category title="爱好">
<template scope="{gamesAttr}">
<h4 v-for="(item, index) in gamesAttr" :key="index">{{ item }}</h4>
</template>
</Category>
注意
作用域插槽的作用:
使子组件只关注数据的处理,而插槽的内容及样式由使用者自己定义
1.子组件向使用者传递包含指定属性的对象类型数据
2.使用者必须使用template
标签的scope
属性定义变量,来接收插槽传递过来的对象
3.插槽传递的对象中可包含多个属性
<slot :gamesAttr="games" hh="哈哈"></slot>
4.父组件scope可以使用结构赋值来获取对象中的属性
<template scope="{gamesAttr, hh}">
5.scope
属性也可以使用新版本的属性slot-scope
代替
<template slot-scope="{gamesAttr, hh}">
$slot
属性
$slot
属性: vue2.6.0版本之后新增,$slot对象有一个default属性,其值为数组,每一个元素是插槽中的虚拟元素节点
$slot
属性值是否有虚拟节点,和子组件是否使用了<slot>
无关,子组件即使没有定义插槽,也会把插槽内容作为虚拟节点放到数组中
<Student>
<span>你好</span>
<p>vue</p>
</Student>
当插槽内无任何节点时,$slot
为空对象{}
如果是具名插槽,$slot
对象中key: 插槽名称,value: 虚拟节点数组
<Student>
<template slot="name1">
<h1>Slot1</h1>
</template>
<template slot="name2">
<h1>Slot2</h1>
</template>
</Student>
Vuex
简介
Vuex是专门为Vue应用程序开发的状态(数据)管理插件,它采用集中式存储管理(读/写)使多个组件可预测的操作方式共享数据,也是一种任意组件间相互通信的方式
核心概念
1.状态: 数据
2.状态管理: 对数据进行增删改查
3.状态管理库: 对数据进行增删改查的库
4.集中式存储: 将数据存储在一个地方,方便管理
5.可预测的方式: 对数据的操作是可预测的,即对数据的操作是可追踪的,可回溯的
传统多组件数据共享(读/写)操作,使用全局事件总线,例如想实现各个组件之间数据x
的数据同步,
每个组件中必须维护一个x
属性,使用$on
读取其他组件改变x
的动作,使用$emit
同步给其他组件修改x
的动作,
如果项目中组件较多引用关系复杂,那么所有组件中都必须实现$on
和$emit
,那么就会产生大量重复代码,且维护同步比较繁琐
状态管理库Vuex
:
Vuex
中的数据对于每个组件都具有可见性,某个组件修改了x
,Vuex将通知各组件重新渲染数据,因此可以立即同步给各组件
使用场景
1.多个组件依赖同一数据
2.来自不同组件的行为需要变更同一数据
3.封装复杂通用逻辑
原理
1.VC
各个组件使用Vuex
插件,共享数据
2.Dispatch
调用动作
组件调用Store
提供的接口
3.Actions
对象
Actions
对象处理组件请求
作用:
1.组织提交信息,把具体的处理逻辑提交给Mutations
对象,提交信息包括运算逻辑、运算值、运算对象
2.调用外部的后端API获取数据,组织最终的提交信息
3.Actions
对象的方法中处理业务逻辑
4.Commit
提交动作
Actions
组织好提交信息后,提交给Mutations
对象
5.Mutations
对象
Mutations
对象执行对应的处理逻辑
6.Mutate
转换动作
Mutations
对象执行完处理逻辑后,将数据提交给State
对象
7.State
对象
State
对象保存数据,并通知各组件重新渲染数据
8.Render
渲染动作
通知各组件重新渲染数据
9.store
对象
store
对象是Vuex
的核心对象,用于存储数据,并管理Actions
、Mutations
、State
对象,并提供接口给组件使用
流程总结
1.整个Vuex
工作流程中可以看做客人到餐厅用餐
的流程
客人
: Vue Components
服务员
: Actions
厨师
: Mutations
菜
: State
客户来到了餐厅经过服务员的接待后,客户等待菜做好(`Actions`)
- 客人点好菜后,又加菜(`Backend API`)
服务员告诉告诉厨师做什么菜,厨师开始做菜(`Mutations`)
厨师做好菜后,把菜盛好,端给客户(`State`)
客户拿到菜开始用餐(`Render`)
2.组件通过store
对象可以直接调用commit
,跳过Dispatch
交给Mutations
对象处理
搭建Vuex
环境
1.下载Vuex
依赖库
vue2使用vuex3版本,vue3使用vuex4版本
➜ vue-test git:(master) npm i vuex@3
2.vue
安装Vuex
插件
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
3.创建index.js
,并创建store
对象
官方推荐使用vuex
插件时,项目中使用store
作为目录名管理数据,而不vuex
作为目录名
├── index.html
├── main.js
├── api
│ └── ... # 抽取出API请求
├── components
│ ├── App.vue
│ └── ...
└── store
├── index.js # 我们组装模块并导出 store 的地方
├── actions.js # 根级别的 action
├── mutations.js # 根级别的 mutation
└── modules
├── cart.js # 购物车模块
└── products.js # 产品模块
只有vue
安装了Vuex
插件后才能使用store
对象,vm
和vc
对象则都会具有store
对象
index.js
暴露出store
对象
store
对象中包含state
、mutations
、actions
对象
index.js
:
import Vuex from 'vuex'
Vue.use(Vuex)
export default new vuex.Store({
// 共享数据
state: {
count: 0,
},
// 执行修改逻辑
mutations: {
increment(state) {
state.count++;
},
},
// 响应组件中用户动作
actions: {
increment: function (context) {
context.commit('increment');
},
},
});
4.将store
对象挂载到vm
对象
main.js
:
import vuex from 'vuex';
import store from './store';
Vue.config.productionTip = false;
Vue.use(vuex);
new Vue({
el: '#app',
render: h => h(App),
store,
});
5.项目报错: 提示在vue
创建store
对象前,vue
应该先引入vuex
插件
提示在vue创建store
对象前,vue应该先引入vuex
插件
原因: vue-cli
会自动将import
语句放到main.js
的上方执行
解决办法:
在index.js
中引入vue
,并安装vuex
插件
index.js
:
import Vue from 'vue';
import vuex from 'vuex';
Vue.use(vuex);
export default new vuex.Store({
// 共享数据
state: {
count: 0,
},
// 执行修改逻辑
mutations: {
increment(state) {
state.count++;
},
},
// 响应组件中用户动作
actions: {
increment: function (context) {
context.commit('increment');
},
},
});
main.js
无需引入vuex
插件,只需vue
挂载store
对象:
import store from './store';
Vue.config.productionTip = false;
new Vue({
el: '#app',
render: h => h(App),
store,
});
vuex
插件安装成功,vc
中store
对象就可以使用了
注意
使用import
语句就会执行其中js
代码
main.js:
import './test1.js';
console.log('main.js');
import './test2.js';
输出:
import
某个目录会自动导入其目录下的index.js
例如:
main.js:
import store from './store';
// 实际导入的是store/index.js
vue-cli
会把import
语句按照书写顺序整理到main.js
的上方
main.js:
import './test1.js';
console.log('main.js');
import './test2.js';
即使./test2.js
在最后,vue-cli
会统一调整import
语句放到main.js
的上方
$store.dispatch()
触发actions
对应的方法
dispatch()
: 触发actions
对象中对应的方法
参数:
1.actions
中的方法名
2.数据
组件中调用dispatch
方法,传入actions中方法名increment
和参数3
<button @click="$store.dispatch('increment',3)">+</button>
index.js
:
const actions = {
increment(a, b) {
console.log(a, b);
},
};
const mutations = {
};
const state = {
count: 0,
};
export default new Vuex.Store({
// 共享数据
state,
// 执行修改逻辑
mutations,
// 响应组件中用户动作
actions,
});
actions
中的increment
函数接收两个参数,第一个参数是store
的上下文对象context
,第二个参数是dispatch
函数传过来的数据
context
: vuex
并没有把store
对象传入actions
中,而是精简了必要的方法后赋给context
对象,以便在actions
中调用store
中的方法
actions
对应的方法中通过上下文对象context
调用commit
方法
context.commit()
: 触发调用mutations
对象中对应的方法
参数:
1.mutation
中的方法名
2.数据
const actions = {
increment(context, value) {
context.commit('increment', value);
},
};
const mutations = {
increment(state, value) {
state.count += value;
},
};
mutations
中的对应方法被调用
mutations
中的方法: 用于执行具体处理逻辑,改变state
中属性值
mutations
中方法参数:
1.state
对象
2.数据
const mutations = {
increment(state, value) {
console.log(state, value);
state.count += value;
},
};
组件中通过$store.属性
读取共享数据
<div>
<span>{{ $store.state.count }}</span>
<button @click="$store.dispatch('increment',3)">+</button>
</div>
actions
中处理业务逻辑,如没有业务逻辑可直接调用commit
<div>
<span>{{ $store.state.count }}</span>
<button @click="$store.commit('INCREMENT',3)">直接+</button>
<button @click="$store.dispatch('incrementWait',3)">等待1s后+</button>
</div>
const actions = {
incrementWait(context, value) {
setTimeout(() => {
context.commit('INCREMENT', value);
}, 1000);
},
};
const mutations = {
INCREMENT(state, value) {
state.count += value;
},
};
actions
中方法名小写,mutations
中方法名大写,在view
代码时便于区分
const actions = {
incrementWait(context, value) {
setTimeout(() => {
context.commit('INCREMENT', value);
}, 1000);
},
};
const mutations = {
INCREMENT(state, value) {
state.count += value;
},
};
vue
开发者工具中始终监听mutations
中方法执行
对应了如下关系:
actions
中可以通过调用dispatch
方法来执行actions
其他方法以便处理业务
const actions = {
incrementWait(context, value) {
setTimeout(() => {
context.dispatch('increment', value);
}, 1000);
},
increment(context, value) {
context.commit('INCREMENT', value);
},
};
规范
1.通用的业务处理逻辑应该封装在action
中,提高代码复用性和可维护性
2.actions
中方法名小写,mutations
中方法名大写,在view
代码时便于区分
3.如果没有网络请求或业务逻辑处理,直接调用commit
方法修改state
getters
对象对state
数据的获取进行封装业务逻辑
getters
对象: 对state
数据的获取进行封装业务逻辑,便于在各个组件中调用,和computed
计算属性类似,但computed
只能在某个组件中使用
1.定义getters
对象中的属性
const getters = {
bigCount(state) {
return state.count * 10;
}
}
2.在store
对象中注册getters
对象
export default new Vuex.Store({
state,
mutations,
actions,
getters,
});
3.在组件中获取数据,通过$store.getters.属性
获取数据
<div>
<span>数值乘以10: {{ $store.getters.bigCount }}</span>
</div>
mapState
函数简化组件中获取state
数据
当state
数据复杂,组件中使用state
属性较多时,会出现很多重复的$store.state.属性
,使插值语法中表达式比较繁琐,例如:
<div>
<span>{{ $store.state.count }}</span><br>
<span>数值乘以10: {{ $store.getters.bigCount }}</span><br>
<span>姓名: {{$store.state.name}}</span><br>
<span>年龄: {{$store.state.age}}</span><br>
<span>性别: {{$store.state.sex}}</span><br>
<button @click="$store.commit('INCREMENT',3)">直接+</button><br>
<button @click="$store.dispatch('incrementWait',3)">等待1s后+</button>
</div>
那么mapState
函数可以简化组件中获取state
数据,使代码更简洁,mapState
函数可以帮助生成计算属性来获取对应的状态属性,那么插值语法中可以直接使用计算属性获取state
的属性值。
组件中使用mapState
函数帮助生成计算属性读取state
数据
1.组件中导入mapState
函数
2.配置生成计算属性对象 key: 计算属性名,value: state
中属性名
3.mapState
函数返回一个对象,对象中每个属性都是计算属性,并使用...
展开运算符合并到计算属性对象中
import { mapState } from 'vuex';
mounted(){
let mapStateResult = mapState({name: 'name', age: 'age'});
console.log(mapStateResult);
}
mapState
通过配置计算属性,返回一个对象,对象中每个属性都是一个函数,而且函数中逻辑就是获取state
的属性,因此可以通过...
展开运算符合并到计算属性对象中,这样在组件中就可以直接使用计算属性获取state
的属性值
插值语法中可以直接使用mapState
生成的计算属性:
<div>
<span>{{count}}</span><br />
<span>数值乘以10: {{$store.getters.bigCount}}</span><br />
<span>姓名: {{name}}</span><br />
<span>年龄: {{age}}</span><br />
<span>性别: {{sex}}</span><br />
<button @click="$store.commit('INCREMENT',3)">直接+</button><br />
<button @click="$store.dispatch('incrementWait',3)">等待1s后+</button>
</div>
// 导入mapState函数
import { mapState } from 'vuex';
export default {
computed: {
// 通过计算属性来获取state状态,组件中直接使用count即可
count() {
return this.$store.state.count;
},
...mapState({
// 配置mapState生成哪些计算属性,指定计算属性名称和源状态名称
name: 'name'
age: 'age',
sex: 'sex'
}),
},
};
正常获取state
属性:
开发者工具中将mapState
生成计算属性归类到vuex bingdings
中:
对象写法
computed: {
...mapState({
// 配置mapState生成哪些计算属性,指定计算属性名称和源状态名称
name: 'name'
age: 'age',
sex: 'sex'
}),
}
数组写法
computed: {
...mapState(['name', 'age', 'sex']),
}
数组的写法更能体现出mapState
的作用:
1.减少开发者定义众多的计算属性
2.插值语法直接使用计算属性获取state
属性,减少$store.state.属性
代码
mapGetters
函数简化组件中获取getters
数据
mapGetters
函数和mapState
函数一样,也是将getters
中的数据映射到局部计算属性中
对象写法
import { mapGetters } from 'vuex';
computed: {
...mapGetters({bigCount: 'bigCount'}),
}
数组写法
import { mapGetters } from 'vuex';
computed: {
...mapGetters(['bigCount']),
}
mapMutations
函数简化组件中调用mutations
方法
借助mapMutations
函数生成对应的方法,简写了this.$store.commit('xxx', value)
的写法
<button @click="add">直接+</button>
import { mapMutations } from 'vuex';
methods: {
// add() {
// this.$store.commit('INCREMENT', 3);
// },
...mapMutations({add: 'INCREMENT'}),
},
mapMutations
会生成一个方法,该方法接收一个参数,该参数就是参与运算的数据
add(value) {
this.$store.commit('INCREMENT', value);
},
由于直接调用add
方法,不传值的情况下会vue
默认会把event
事件对象传入,因此mapMutations生成的add方法中value值就是event
对象
因此在使用mapMutations
生成的方法时,应该传入参数,作为计算的value
值
<button @click="add(3)">直接+</button>
对象写法
methods: {
...mapMutations({add: 'INCREMENT'}),
},
数组写法
methods: {
...mapMutations(['INCREMENT']),
},
mapActions
函数简化组件中调用actions
方法
借助mapActions
函数生成对应的方法,简写了this.$store.dispatch('xxx', value)
的写法
<button @click="add">等待1s后+</button>
import { mapActions } from 'vuex';
methods: {
// addWait() {
// this.$store.dispatch('incrementWait', 3);
// },
...mapActions({addWait: 'incrementWait'}),
},
mapActions
会生成一个方法,该方法接受一个参数,该参数就是参与运算的数据
addWait(value) {
this.$store.dispatch('incrementWait', value);
},
对象写法
methods: {
...mapActions({addWait: 'incrementWait'}),
}
数组写法
methods: {
...mapActions(['incrementWait']),
}
Vuex
模块化和命名空间
目的: 模块化管理数据,避免数据混乱,更好的维护共享数据。命名空间可以解决模块之间的命名冲突,每个模块都有自己独立的命名空间,不同模块中的方法名称可以相同,但命名空间不同,不会冲突
没有模块前的store
包含: state
、getters
、mutations
、actions
模块后的store
包含了命名空间的state
、getters
、mutations
、actions
store
的模块配置
每个模块都是一个对象,都包含各自的state
、getters
、mutations
、actions
index.js引入模块
import user from './user.js';
import order from './order.js';
Vue.use(Vuex);
export default new Vuex.Store({
modules: {
user,
order,
},
});
user.js模块
import axios from 'axios';
export default {
// 开启命名空间
namespaced: true,
state: {
users: [
{
name: '张三',
age: 18,
sex: '男',
},
],
},
mutations: {
createUser(state, value) {
state.users.unshift(value);
},
},
actions: {
createRandomUser(context) {
axios.get('https://api.apiopen.top/api/sentences').then(res => {
context.commit('createUser', {
name: res.data.result.name,
age: 20,
sex: '男',
});
});
},
},
getters: {
firstUserName(state) {
return state.users[0].name;
},
},
};
order.js模块
export default {
// 开启命名空间
namespaced: true,
state: {
orders: [
{
id: '1',
product: '手机',
price: 1000,
userName: '张三',
},
],
},
mutations: {
createOrder(state, value) {
state.orders.unshift(value);
},
},
actions: {
createFirstUserOrder(context, value) {
context.commit('createOrder', value);
},
},
getters: {
firstOrder(state) {
return state.orders[0];
},
},
};
$store对象的模块化访问
computed: {
users() {
// user模块的state
return this.$store.state.user.users;
},
firstUserName() {
// user模块的getters
return this.$store.getters['user/firstUserName'];
},
},
methods: {
createUser() {
// user模块的mutations
this.$store.commit('user/createUser', { name: this.name, sex: '女', age: 18 });
},
createRandomUser() {
// user模块的actions
this.$store.dispatch('user/createRandomUser');
},
},
mapState
、mapGetters
、mapMutations
、mapActions
的模块化访问
<button @click="createOrder({id:orderId(),product,userName,price})">新增用户订单</button>
<button @click="createFirstUserOrder({id:orderId(),product,userName:users[0].name,price})">新增第一个用户订单</button>
import { nanoid } from 'nanoid';
import { mapState, mapGetters, mapMutations, mapActions } from 'vuex';
export default {
computed: {
// order模块的state
...mapState('order', ['orders']),
// user模块的state
...mapState('user', ['users']),
// order模块的getters
...mapGetters('order', ['firstOrder']),
},
methods: {
// order模块的actions
...mapActions('order', ['createFirstUserOrder']),
// order模块的mutations
...mapMutations('order', ['createOrder']),
orderId(){
return nanoid();
}
},
};
路由
概念
路由器(router): 多个路由的管理器
路由(route): 定义一组key
-value
的对应关系,多个路由需要经过路由器的管理
多页面与单页面应用
单页面应用SPA(Single Page Application): 整个项目只有一个html
功能页面,通过js
动态切换页面内容,页面不会刷新(Vue框架应用)
多页面应用MPA(Multiple Page Application): 每个功能页面都有一个html
页面,页面之间切换会刷新页面(传统的web应用)
SPA:
优点:
1.页面切换速度快
2.用户体验好
缺点: SEO搜索引擎优化差
MPA:
优点:
1.SEO搜索引擎优化好
2.开发难度小
缺点:
1.页面切换速度慢
2.用户体验差
vue
中route
的原理
vue-router
插件库,监听页面url
中的path
变化,根据自身维护的route
关系,找到path
对应的组件,渲染到页面中,实现了单页面应用的功能渲染
route
路由: key
: url-path
, value
: 组件
基本使用vue-router
插件库
1.下载依赖库
vue2: vue-router@3
vue3: vue-router@4
➜ vue-test git:(master) npm i vue-router@3
2.vue
安装vue-router
插件库
main.js
import VueRouter from 'vue-router';
Vue.config.productionTip = false;
Vue.use(VueRouter);
3.创建路由器并配置路由
src
目录中创建router/index.js
文件,index.js
文件用于创建路由器和路由规则
index.js
import VueRouter from 'vue-router';
import Home from '../components/Home.vue';
import About from '../components/About.vue';
export default new VueRouter({
routes: [
{
path: '/home',
component: Home,
},
{
path: '/about',
component: About,
},
],
});
每个路由规则包含: path
和component
属性,即key: path
value: component
4.在vue
中注册路由器
import VueRouter from 'vue-router';
import router from './router';
Vue.config.productionTip = false;
Vue.use(VueRouter);
new Vue({
el: '#app',
render: h => h(App),
router: router
});
vue
实例对象中配置router
属性,值为router/index.js
中导出的router
对象
5.导航栏使用router-link
标签来路由组件
<div class="category">
<router-link class="nav" active-class="active" to="/home">Home</router-link>
<router-link class="nav" active-class="active" to="/about">About</router-link>
</div>
router-link
属性:
to
: 路由路径,对应router
对象中的path
属性
active-class
: 激活路由时添加的类名,默认类名为router-link-active
,当激活的类名为默认类名时,可以不写active-class
属性
6.使用router-view
标签渲染路由到的组件
<div class="content">
<router-view></router-view>
</div>
示例
<template>
<div class="container">
<h2>vue-router Demo</h2>
<hr />
<div class="main">
<div class="category">
<router-link class="nav" to="/home">Home</router-link>
<router-link class="nav" to="/about">About</router-link>
</div>
<div class="content">
<router-view></router-view>
</div>
</div>
</div>
</template>
<script>
export default {};
</script>
<style>
.container {
width: 400px;
margin: 0 auto;
}
h2 {
border: 1px;
font-weight: 350;
}
hr {
border-color: aliceblue;
}
.nav {
text-decoration: none;
color: black;
background-color: aliceblue;
display: inline-block;
width: 50px;
height: 30px;
line-height: 30px;
text-align: center;
border-radius: 5px;
margin: 5px;
margin-left: 0px;
display: block;
}
.nav:hover {
background-color: rgb(207, 231, 253);
}
.router-link-active {
background-color: rgb(53, 145, 231);
}
.active:hover {
background-color: rgb(53, 145, 231);
}
.main {
display: flex;
}
.content {
margin-left: 5px;
padding: 5px;
width: 100%;
background-color: rgb(213, 223, 231);
text-align: center;
}
.category {
display: flex;
flex-direction: column;
justify-content: space-between;
}
</style>
router-link
标签实际上被vue
渲染成a
标签
注意
1.一般组件和路由组件通过目录区分
一般组件: 通过组件标签引用的组件称之为一般组件,放到components
目录中
路由组件: 通过vue-router
路由到的组件称之为路由组件,放到views
或pages
目录中
2.切换路由时,默认情况下组件会不断的挂载和销毁
3.每个组件都有自己的$route
属性,存储自己的路由信息
4.整个应用只有一个$router
对象,可以通过this.$router
访问
嵌套路由
1.在router/index.js
中配置路由规则,在children
属性中配置子路由规则
注意: 嵌套组件的配置必须写在父组件配置中的children
属性中,
且嵌套组件的path
值不需要带/
import VueRouter from 'vue-router';
import Home from '../pages/Home.vue';
import About from '../pages/About.vue';
import Message from '../pages/Message.vue';
import News from '../pages/News.vue';
export default new VueRouter({
routes: [
{
path: '/home',
component: Home,
},
{
path: '/about',
component: About,
children: [
{
path: 'message',
component: Message,
},
{
path: 'news',
component: News,
},
],
},
],
});
2.父组件中使用router-link
和router-view
标签来渲染嵌套组件
注意: router-link
标签的to
属性值必须是完整路径,包含了父组件路径
<div>
<div>这是About页面</div>
<div class="category">
<router-link class="nav" to="/about/message">message</router-link>
<router-link class="nav" to="/about/news">news</router-link>
</div>
<div>
<router-view></router-view>
</div>
</div>
路由传参
query
传参
1.to
的字符串写法
组件中router-link
标签的to
属性值中添加query
参数
<div>
<div>这是News页面</div>
<ul>
<li v-for="(item, index) in newsList" :key="index">
<router-link :to="`/about/news/detail?id=${item.id}&title=${item.title}`">{{item.title}}</router-link>
</li>
</ul>
<hr>
<router-view></router-view>
</div>
<div>
消息编号: {{$route.query.id}} 消息标题: {{$route.query.title}}
</div>
路由组件使用this.$route.query.参数名
来获取query
参数
2.to
的对象写法
<router-link :to="{
path: '/about/news/detail',
query: {
id: item.id,
title: item.title,
}
}">
{{item.title}}
</router-link>
命名路由
作用: 简化path
配置完整路径的写法
前提: 使用命名路由,to
属性必须配置对象形式,而且对象中使用name
属性指定路由
缺点: 跳转路由组件可读性差
路由对象配置name
属性:
{
path: '/about',
component: About,
children: [
{
path: 'news',
component: News,
children: [
{
name: 'newsDetail',
path: 'detail',
component: NewsDetail,
},
],
},
],
}
组件中to
对象使用name
属性指定某个路由:
<router-link :to="{
name: 'newsDetail',
query: {
id: item.id,
title: item.title,
}
}">
{{item.title}}
</router-link>
注意: to
对象中的name
优先于path
属性,如果同时存在,path
属性会被忽略
params
传参
1.to
字符串写法
路由对象配置path
路径时使用:属性名
进行占位:
{
path: '/about',
component: About,
children: [
{
path: 'message',
component: Message,
},
{
path: 'news',
component: News,
children: [
{
path: 'detail/:id/:title',
component: NewsDetail,
},
],
},
],
},
router-link
标签使用:to
属性进行跳转:
<router-link :to="`/about/news/detail/${item.id}/${item.title}`">
{{item.title}}
</router-link>
组件中使用$route.params.属性名
获取路由参数:
<div>
消息编号: {{$route.params.id}} 消息标题: {{$route.params.title}}
</div>
2.to
对象写法
to
对象写法必须使用name
属性指定路由名称
children: [
{
name: 'newsDetail',
path: 'detail/:id/:title',
component: NewsDetail,
},
],
<router-link :to="{
name: 'newsDetail',
params: {
id: item.id,
title: item.title,
},
}">
{{item.title}}
</router-link>
注意
1.使用params
传参路由中path
属性必须带:属性名
占位符
2.使用params
传参to
对象中必须使用name
属性指定路由,不能使用path
属性
3.:to="{}"
双引号必须紧紧包裹对象,"
和{
之间不能换行,否则报错
4.:to
对象中的name
属性优于path
属性来确定路径
query
和params
区别
1.参数位置不同
query: path
后面?
后拼接参数
params: 参数作为path
的一部分
2.使用场景
query: 可定义非必填参数
params: 定义参数必须使用:属性
占位,而且属性为必填,因此path
中不能为''
或null
或undefined
例如:
path: 'about/news/detail/:id/:title'
:to="{
name: 'newsDetail',
params: {
id: null,
title: item.title,
},
}"
由于id为必填项,不能为''
或null
或undefined
,否则报错
路由组件的props
配置
路由组件的props
配置: 在目标路由中配置props
属性,值以key-value
形式配置,那么组件向路由组件传参时,路由组件可以使用props
属性接收组件传递的参数
作用: 简化目标组件使用{{$route.query.属性}}
或{{$route.params.属性}}
的写法,使目标组件直接使用{{属性}}
写法使用参数
值为对象形式
特点: 只能传递静态数据,由于是固定值,因此和query
或params
传参无关
目标路由中配置props
属性:
children: [
{
name: 'newsDetail',
path: 'detail/:id/:title',
component: NewsDetail,
props: {
id: 111,
title: '标题',
}
},
],
目标组件使用props
接收数据:
<div>
消息编号: {{id}} 消息标题: {{title}}
</div>
props: ['id', 'title'],
值为true
形式
特点: 可以传递动态params
数据,目标组件可直接使用props
属性接收路由传递的params
参数
目标路由中配置props
属性:
children: [
{
name: 'newsDetail',
path: 'detail/:id/:title',
component: NewsDetail,
props: true,
},
],
目标组件使用props接收数据:
<span>ID:{{id}} 标题:{{title}}</span>
props: ['id', 'title'],
值为函数形式
特点: 可以动态接收query
或params
数据,让路由组件直接使用props
接收路由参数
目标路由中配置props
属性为回调函数,该回调函数默认传入$route
对象,可以使用解构赋值的连续写法获取参数:
{
name: 'newsDetail',
path: 'detail',
component: NewsDetail,
props: ({ query: { id, title } }) => {
return { id, title };
},
},
目标组件:
<span>props: ID:{{id}} 标题:{{title}}</span>
props: ['id', 'title'],
总结
路由中配置props
属性,可以简化目标组件使用路由参数的写法,省去了$route.query
或者$route.params
总体配置步骤分为:
1.router-link
标签配置to
属性,确定跳转路由和传参形式,query
或params
2.目标路由配置props
回调函数,将参数以props
属性的方式传递给目标组件
3.目标组件使用props
属性接收路由参数
router-link
标签记录浏览历史地址
默认为push
模式
记录每次跳转访问过的地址,通过浏览器回退按钮回退
replace
模式
不记录每次跳转访问过的地址,点击后当前浏览记录会把上一次浏览记录替换掉
router-link
标签配置replace
属性
配置主导航栏replace
属性: 不记录每次跳转访问过的地址,而是替换掉上一次浏览记录
<div class="category">
<router-link replace class="nav" to="/home">Home</router-link>
<router-link replace class="nav" to="/about">About</router-link>
</div>
对route-link
标签添加replace
属性后,由于在主目录设置replace
,因此不会记录该项目任何地址,在点击浏览器回退时则退出了项目地址
例如:
对三级路由设置replace
属性时,就会替换掉上一个路由地址,上一个路由地址是二级路由localhost://8080/#/about/news
,因此点击浏览器回退时,会退回到一级路由localhost://8080/#/about
编程式路由导航
router-link
依赖于标签进行导航,由用户进行触发,编程式路由导航不依赖标签由程序员控制导航,更加灵活
$router.push
跳转路由
push
方法传入的对象和:to
属性对象配置内容相同
pushShow(item) {
this.$router.push({
name: 'newsDetail',
query: {
id: item.id,
title: item.title,
},
});
},
$router.replace
跳转路由
replace
方法传入的对象和:to
属性对象配置内容相同
replaceShow(item) {
this.$router.replace({
name: 'newsDetail',
query: {
id: item.id,
title: item.title,
},
});
},
$router.back()
和$router.forward()
: 返回上一级路由和下一级路由
back() {
this.$router.back();
},
forward() {
this.$router.forward();
},
$router.go()
: 前进或后退指定步数
go(整数)
: 正数前进指定步数,负数后退指定步数
go() {
this.$router.go(3);
},
注意
避免重复上一次导航,如果当前导航和上一次记录的导航相同,就会报错
缓存路由组件
作用: 将路由组件中的数据保存起来,再次切换回该路由的时候,数据还在,也就是说路由组件不会被销毁
<div class="category">
<router-link class="nav" to="/about/message">message</router-link>
<router-link class="nav" to="/about/news">news</router-link>
</div>
<div>
<keep-alive include="News">
<router-view></router-view>
</keep-alive>
</div>
缓存了News
组件,那么<router-view>
展示Message
组件后,再切换到News
组件,Message
组件将会被销毁,而News
组件中的数据则保留
注意
1.<keep-alive>
标签必须包裹<router-link>
标签
2.缓存路由组件时,路由组件必须有name
属性,否则不生效
3.<keep-alive>
标签的include
属性的值为路由组件的名称,依据路由组件的name
属性,不是配置的路由name
4.<keep-alive>
标签的include
属性值的范围必须是当前组件可以路由的组件名称,也就是组件的子路由组件名称,不能是孙子路由组件名
5.当缓存路由组件的父组件销毁且未配置缓存时,该缓存路由组件的数据也会被销毁
About
组件中路由News
组件,对News
组件进行了缓存
<div>这是About页面</div>
<div class="category">
<router-link class="nav" to="/about/message">message</router-link>
<router-link class="nav" to="/about/news">news</router-link>
</div>
<div>
<keep-alive include="News">
<router-view></router-view>
</keep-alive>
</div>
App
组件中可以路由About
组件,而About
组件没有缓存
<div class="main">
<div class="category">
<router-link class="nav" to="/home">Home</router-link>
<router-link class="nav" to="/about">About</router-link>
</div>
<div class="content">
<router-view></router-view>
</div>
</div>
App(组件)
|_Home组件(无缓存)
|_About组件(无缓存)
|_News组件(缓存)
|_Message组件(无缓存)
News
和Message
组件之间切换,News
缓存生效,由于News
的父组件About
无缓存,当切换了About
组件时,About
组件将被销毁,其子路由组件News将被销毁,因此缓失效
当About
组件也进行缓存时,其子路由组件News
缓存生效
App组件配置Home和About组件缓存
<div class="category">
<router-link class="nav" to="/home">Home</router-link>
<router-link class="nav" to="/about">About</router-link>
</div>
<div class="content">
<keep-alive>
<router-view></router-view>
</keep-alive>
6.<keep-alive>
没有include
属性时,则缓存所有路由组件
7.配置多个缓存路由组件是使用,
紧紧隔开,不能有空格,或者:include="['组件名','组件名']"
<keep-alive include="Message,News">
<router-view></router-view>
</keep-alive>
<keep-alive :include="['News','Message']">
<router-view></router-view>
</keep-alive>
路由组件中的生命周期钩子
在News
组件中添加一个定时器,由于News组件通过<keep-alive>
缓存了,因此切换tab
时,News
组件不会销毁,beforeDestroy
不会执行
久而久之项目占用内存和CPU
资源,导致项目性能下降
mounted() {
this.timer = setInterval(() => {
this.opacity -= 0.1;
if (this.opacity <= 0) {
this.opacity = 1;
}
console.log(this.timer);
}, 160);
},
beforeDestroy() {
clearInterval(this.timer);
},
那么想实现,既能缓存组件,又想在组件切换后,释放没必要运行的资源时,就需要使用只有路由组件才有的声明周期钩子
activated
和deactivated
钩子
activated()
: 路由组件被激活时调用
deactivated()
: 路由组件失活时调用
methods: {
changeOpacity() {
this.timer = setInterval(() => {
this.opacity -= 0.1;
if (this.opacity <= 0) {
this.opacity = 1;
}
console.log(this.timer);
}, 160);
},
},
activated() {
console.log('News.vue被激活了');
this.changeOpacity();
},
deactivated() {
console.log('News.vue被失活了');
clearInterval(this.timer);
},
路由守卫
路由守卫是路由跳转过程中的一些钩子函数,路由守卫可以控制路由的访问权限,比如用户是否登录,通过路由守卫,可以控制用户访问某些页面的时候,必须登录才能访问
每次切换路由时回调用这些钩子函数,因此路由守卫也被称为导航守卫
全局前置路由守卫
调用时机:
1.初始化页面时
2.每次路由切换时
router/index.js
在路由器暴露前添加全局前置路由守卫
router.beforeEach((to, from, next) => {
console.log('to:', to);
console.log('from:', from);
next();
});
export default router;
初始化页面或每次路由切换时调用:
参数:
to
: 目标路由
from
: 当前路由
next()
: 放行,通过逻辑判断后,允许用户访问to
时,则调用next()
meta
: 路由元信息,由开发者自定义配置信息,用于标记该路由是否需要鉴权等作用
示例:
router.beforeEach((to, from, next) => {
let permissArr = JSON.parse(localStorage.getItem('permisses'));
if (permissArr.includes(to.path)) {
alert('无权限访问');
} else {
next();
}
});
路由的meta
元信息配置项
let router = new VueRouter({
routes: [
{
name: 'shouye',
path: '/home',
component: Home,
meta: {
isAuth: false,
},
},
{
name: 'about',
path: '/about',
component: About,
meta: {
isAuth: false,
}
},
],
});
router.beforeEach((to, from, next) => {
let permissArr = JSON.parse(localStorage.getItem('permisses'));
// 当前路由需要鉴权时,判断是否有权限
if (to.meta.isAuth) {
if (!permissArr.includes(to.path)) {
alert('无权限访问');
} else {
next();
}
} else {
next();
}
});
export default router;
全局后置路由守卫
调用时机:
1.初始化页面时
2.每次成功切换路由之后
回调函数中没有next
参数
router.afterEach((to, from) => {
// 修改页面标题
document.title = to.meta.title || '默认标题';
});
在每一个路由的meta
属性中配置title
值,当成功切换路由时,修改页面标题
刷新页面时展示页面标题展示了项目名称,配置项目的主标题,默认读取package.json文件中的name
属性,直接修改index.html的<title>
标签
<title>
<!-- <%= htmlWebpackPlugin.options.title %> -->
欢迎使用该系统
</title>
独享路由守卫
独享路由守卫: 只对具体的路由配置生效
路由的配置中添加beforeEnter
属性,值为回调函数,函数参数与全局前置路由守卫相同
{
name: 'news',
path: 'news',
component: News,
meta: {
isAuth: true,
title: '新闻',
},
beforeEnter: (to, from, next) => {
let permissArr = JSON.parse(localStorage.getItem('permisses'));
// 当前路由需要鉴权时,判断是否有权限
if (to.meta.isAuth) {
if (!permissArr.includes(to.path)) {
alert('无权限访问');
} else {
next();
}
} else {
next();
}
}
},
注意:
1.独享路由守卫只有beforeEnter
回调函数,路由前进行守卫,没有afterEnter
2.独享路由守卫可以配合后置全局路由守卫($router.afterEach)
一起使用,弥补没有afterEnter
的缺陷
组件内路由守卫
组件内路由守卫: 在组件中定义组件进入前和离开组件前的守卫
beforeRouteEnter
和beforeRouteLeave
都需要next进行放行
beforeRouteEnter
:进入组件前调用
beforeRouteLeave
:离开组件前调用
export default {
beforeRouteEnter(to, from, next) {
console.log('组件内路由守卫beforeRouteEnter');
next();
},
beforeRouteLeave(to, from, next) {
console.log('组件内路由守卫beforeRouteLeave');
next();
},
};
使用场景: 对组件独有的进入和离开逻辑进行封装
路由模式
hash
模式(默认模式)
hash
模式: 浏览器地址栏中会显示#
,hash
模式下,路由地址中会带有#
,如http://localhost:8080/#/home
let router = new VueRouter({
mode: 'hash',
})
hash
模式下刷新页面,浏览器发起请求,不会提交hash
部分,也是说发起请求的资源不会改变,例如,localhost:8080/#/home
,刷新后,请求的资源为localhost:8080/
,而不是localhost:8080/#/home
缺点:
1.地址栏中会显示#
,不美观
2.若通过app分享页面,由于app分享url校验严格,会把地址标记为不合法
history
模式
history
模式: 浏览器地址栏中不会显示#
,如http://localhost:8080/home
let router = new VueRouter({
mode: 'history',
})
vue
是单页应用,当点击导航栏时会改变浏览器地址栏url
,但并不会请求url
,例如,单击home
,由于配置的模式为history,地址栏中的url不携带#
,
而是localhost/home
。如果浏览器刷新页面,而由于后端没有/home
资源导致404
因此history
模式下,需要服务端配合过滤掉是页面的路径,需要区分api
资源和前端页面资源
使用nginx
解决资源找不到的问题
将打包好的静态资源放到nginx
中
nginx
监听80
端口
try_files $uri $uri/ /index.html;
: 关键配置,尝试按顺序查找文件,如果找不到则重定向到index.html
server {
listen 80;
listen [::]:80;
server_name localhost;
access_log /var/log/nginx/host.access.log main;
location / {
root /usr/share/nginx/html;
index index.html index.htm;
# 关键配置:尝试按顺序查找文件,如果找不到则重定向到index.html
try_files $uri $uri/ /index.html;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
}
总结
1.hash
模式: 浏览器地址栏中会显示#
,hash
模式下,路由地址中会带有#
,如http://localhost:8080/#/home
2.history
模式: 浏览器地址栏中不会显示#
,如http://localhost:8080/home
3.history
模式地址栏干净美观,生产会环境应使用history
模式,且需要解服务端解决刷新浏览器导致的404
问题
UI组件库
移动端:
1.Vant
2.Mint UI
3.CubeUI
PC端:
1.Element UI
2.IView UI
Element UI
1.安装
npm i element-ui -s
-s
: --save 的缩写,表示将安装的包添加到 package.json 文件的 dependencies 部分
2.引入
import Vue from 'vue';
import ElementUI from 'element-ui';
import 'element-ui/lib/theme-chalk/index.css';
Vue.use(ElementUI);
3.使用
<el-button type="primary">主要按钮</el-button>
按需引入
按需引入: 只引入需要的组件,减少打包体积,提高加载速度
1.安装babel-plugin-component
npm i babel-plugin-component -D
2.配置babel.config.js
module.exports = {
presets: [
['@babel/preset-env', { modules: false }],
],
plugins: [
[
'component',
{
libraryName: 'element-ui',
styleLibraryName: 'theme-chalk',
},
],
],
};
3.引入需要的组件
import Vue from 'vue';
import { Button, Select, Option } from 'element-ui';
Vue.component(Button.name, Button);
Vue.component(Select.name, Select);
Vue.component(Option.name, Option);
4.使用
<template>
<div>
<el-button type="primary">主要按钮</el-button>
<el-select v-model="value" placeholder="请选择">
<el-option v-for="item in options" :key="item.value" :label="item.label" :value="item.value"></el-option>
</el-select>
</div>
</template>
<script>
export default {
data() {
return {
options: [
{
value: '选项1',
label: '黄金糕',
},
{
value: '选项2',
label: '双皮奶',
},
{
value: '选项3',
label: '蚵仔煎',
},
{
value: '选项4',
label: '龙须面',
},
{
value: '选项5',
label: '北京烤鸭',
},
],
value: '',
};
},
};
</script>