Vue全家桶系~2.Vue3基础(更新)
Vue全家桶
先贴一下Vue3的官方文档:https://cn.vuejs.org/guide/introduction.html
官方API文档:https://cn.vuejs.org/api/
1.前言:新旧时代交替
1.1.开发变化
1.网络模型的变化:
-
以前网页大多是b/s,服务端代码混合在页面里;
-
现在是c/s,前后端分离,通过js api(类似ajax的方式)获取json数据,把数据绑定在页面上渲染。
2.文件类型变化:
-
以前是.html文件,开发也是html,运行也是html。
-
现在是.vue文件,开发是vue,经过编译后,运行时已经变成了js文件。 现代前端开发,很少直接使用HTML,基本都是开发、编译、运行
3.外部文件引用方式变化:
-
以前通过
script src、link href引入外部的js和css; -
现在是
es6的写法,import引入外部的js模块(注意不是文件)或css
4.开发方式变化:
- 以前是Ajax获取数据,然后DOM操作
- 现在是Vue的MVVM模式(程序员不用操作DOM了,只关心逻辑和数据即可)
我们延续这种演变进行vue3的学习吧,先从传统的单html文件的混合开始,再到单vue文件的选项API(Option API),最后再到vu3新的组合API(Composition API)
1.2.环境配置
1.2.1.VSCode
官网下载地址:https://code.visualstudio.com/

1.必装插件
VSCode官方插件:Vue Language Features (Volar)
TypeScript支持:
TypeScript Vue Plugin (Volar)


Vue快速开发:Vue VSCode Snippets(输入缩写快速生成代码段)

错误高亮提示:Error Lens

Git管理插件:

2.扩展插件
这个插件主要就是高亮TODO: 部分

JavaScript快速开发:输入缩写快速生成js的代码段

1.2.2.NodeJS
NodeJS下载:https://nodejs.org (一般都是下载LTS版本)
npm镜像:https://npmmirror.com/

NodeJS安装后设置下npm的国内镜像:(cnpm 需要全局安装,它是你为数不多的需要全局安装的命令行之一)
安装好后,以后只要是
npm xxx的命令,都可以使用cnpm xxx
1.cnpm window
打开Git Bash,输入:npm i cnpm -g --registry=https://registry.npmmirror.com
PS:如果失败,可以先安装
npminstall,然后通过它来安装cnpm:

# 安装npminstall
npm i npminstall -g --registry=https://registry.npmmirror.com
# 通过npminstall安装cnpm
npminstall -c -g cnpm
# 升级cnpm
cnpm i cnpm -g
# 查看版本号:
cnpm -v

2.cnpm linux
不使用sudo的root权限安装,如果该命令npm i cnpm -g --registry=https://registry.npmmirror.com不行在尝试:
# 看看 npm 全局目录的路径
npm prefix -g
# 把 npm 的全局目录换个位置
mkdir -p ~/.npm-global
npm config set prefix ~/.npm-global
npm bin -g
# 把 ~/.npm-global/bin 加到 PATH
echo 'export PATH="$HOME/.npm-global/bin:$PATH"' >> ~/.zshrc
source ~/.zshrc
# 全新安装,请勿使用 sudo
npm i cnpm -g --registry=https://registry.npmmirror.com
# 升级新版本
cnpm i cnpm -g
# 检查版本号
cnpm -v
如果还不行可以尝试把npm全局目录权限改成当前用户
# 把 npm 的全局目录权限 owner 改为当前用户
sudo chown -R `whoami` `npm prefix -g`
sudo chown -R `whoami` ~/.npminstall_tarball
安装完毕如果找不到cnpm,看看是不是没加环境变量里面:
which cnpm
echo $PATH
1.2.3.Vue DevTools ★
开发过程中再安装一个vuejs的开发者工具:如果是edge浏览器直接打开应用市场搜索并安装即可
官网:https://devtools.vuejs.org/guide/installation.html 源码:https://github.com/vuejs/devtools

配置下:


之后F12后浏览器会有个vue选项,来方便我们开发和观察

2.第一个程序
先以我们熟悉的脚本引入写个demo:
<div id="app"></div>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<script>
const app = Vue.createApp({
template: '<h2>hello vue3</h2>'
})
app.mount('#app'); // 挂载到div#app上
</script>

3.VSCode配置默认终端
默认不是git bash,而是win的PowersShell,我们改下,这样可以方便使用cnpm(shell:windows)

4.VsCode代码片段(含光标位置)
在线配置:https://snippet-generator.app,开源备份地址:https://github.com/lotapp/snippet-generator
4.1.HTML混合开发阶段
比如这段代码每个页面都要输入,那就可以设置一个代码片段:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app">
</div>
<!-- <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script> -->
<script src="../assets/vue3.js"></script>
<script>
const app = Vue.createApp({
});
app.mount('#app');
</script>
</body>
</html>
把内容复制到左边文本框,设置下快捷输入的命令,以及简单描述下

代码复制到这个地方:首选项 > 配置用户代码片段 > 文件选html

粘贴到这个里面,最外面的两个大括号保留({})

如果网站挂了也可以自己手动改写代码片段,示例我贴下:prefix:编辑器快捷命令,body:模板内容,description:描述
{
"create vue app": {
"prefix": "createvue",
"body": [
"<!DOCTYPE html>",
"<html>",
"",
"<head>",
" <meta charset=\"UTF-8\">",
" <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">",
" <title>Document</title>",
"</head>",
"",
"<body>",
" <div id=\"app\">",
" ",
" </div>",
" <!-- <script src=\"https://unpkg.com/vue@3/dist/vue.global.js\"></script> -->",
" <script src=\"../assets/vue3.js\"></script>",
" <script>",
" const app = Vue.createApp({",
" $0",
" });",
" app.mount('#app');",
" </script>",
"</body>",
"",
"</html>"
],
"description": "create vue app"
}
}
以后新建html的时候,输入createvue,就可以快速生成代码了

要指定生成后的光标可以设置下$0,代码生成后鼠标就自动停在那块

4.2.Vue单文件-选项API
vue文件也配置下:
{
"vue file init": {
"prefix": "vue3",
"body": [
"<template>",
" <div>",
" $1",
" </div>",
"</template>",
"",
"<script>",
"$2",
"export default {",
" $3",
"}",
"</script>",
"",
"<style scoped>",
"$0",
"</style>"
],
"description": "vue file init"
}
}
4.3.Vue单文件-组合API
{
"vue file init": {
"prefix": "vue3",
"body": [
"<script setup>",
"$1",
"</script>",
"",
"<template>",
" <div>",
" $2",
" </div>",
"</template>",
"",
"<style scoped>",
"$0",
"</style>"
],
"description": "vue file init"
}
}
其实类似这种vue的代码段,Vue VSCode Snippets中封装的很多,可以在开发中逐步熟悉:vxxx基本上就看到了
2.Vue3语法基础(引入)★
这块上一篇介绍Vue基础的时候基本上都说了,这边快速说下:文本插值、列表、函数、v-model......
文档代码:https://gitee.com/lotapp/BaseCode/tree/master/javascript/3.Vue/3vue3base
2.1.文本插值 {{xxx}}
变量定义在data之中,通过{{title}}显示在页面中,当变量改变时页面中会实时渲染(可以通过控制台app.title=xxx修改查看)
const app = Vue.createApp({
data() {
return {
title: '你好,vue3' // 定义并赋值
}
},
template: `<h2>{{title}}</h2>`
})
app.mount('#app')

PS:template里面的内容,相当于是写在<div id="app">中(如果div#app里面有东西,template里面也有东西,渲染出来的会是template内容)
<div id="app">
<h2>{{title}}</h2>
</div>
官方文档:https://cn.vuejs.org/guide/essentials/template-syntax.html
隐藏渲染前的元素:v-cloak
有些JS加载或者Ajax操作比较耗时,页面会显示{{xxx}}
这种不太友好,Vue提供了一种v-cloak的方式来隐藏渲染前的dom,我们通过设置style里面的[v-cloak]来控制渲染前的样式
<!-- dom元素中添加v-cloak -->
<div v-cloak>{{title}}</div>
然后样式里面设置一下即可
[v-cloak] {
display: none;
}
完整案例:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
[v-cloak] {
display: none;
}
</style>
</head>
<body>
<div id="app">
<!-- dom元素中添加v-cloak -->
<div v-cloak>{{title}}</div>
</div>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<script>
const app = Vue.createApp({
data() {
return {
title: 'demo'
}
}
});
app.mount('#app');
</script>
</body>
</html>
尚未加载成功前不显示{{title}}而是被隐藏掉了。渲染成功后正常显示(v-cloak被vue移除掉了)
官方API:https://cn.vuejs.org/api/built-in-directives.html#v-cloak
2.2.属性绑定 v-bind
官方文档:https://cn.vuejs.org/api/built-in-directives.html#v-bind
API文档:https://cn.vuejs.org/api/built-in-directives.html#v-bind
设置dom的属性(eg:src、href、class),可以通过v-bind:xxx="XX"来设置,简写为:xxx="XX"
PS:还可以向另一个组件传递
props值
1.简单属性案例
<div id="app">
<h2 :title="msg">鼠标放我身上看看</h2>
</div>
<script src="../assets/vue3.js"></script>
<script>
const app=Vue.createApp({
data() {
return {
msg: '我是一个属性值'
}
},
})
app.mount('#app')
</script>
效果如下:

扩展说明:属性值不能使用之前的{{xxx}}来绑定,需要使用特定的v-bind
错误写法:
<button title="{{msg}}">{{msg}}</button>

微调下就生效了:<button :title="msg">{{msg}}</button>

2.样式绑定 :class★
在dom中写上:class="{'类名':bool}",bool可以是一个变量,也可以是一个bool结果的表达式
解析下代码:
:class="{active:selectedStudent==name}给li设置了一个类active,这个类显示不显示就看selectedStudent变量是否和当前name相同@click="selectedStudent=name"当我单击li的时候,把当前name赋值给selectedStudent- 这就意味着,我只要单击li,li的active类就生效了
<div id="app">
<ul>
<li v-for="name in students" :key="name" :class="{active:selectedStudent==name}"
@click="selectedStudent=name">
{{ name }}
</li>
</ul>
</div>
<script src="../assets/vue3.js"></script>
<script>
const app = Vue.createApp({
data() {
return {
selectedStudent: '',
students: ['张三', '李四', '王二']
}
},
})
app.mount('#app') // 挂载
</script>
效果也是一样的:
3.多样式绑定
案例简单贴下::class="{'btn-bgcolor':bool,'active':bool}
dom自带的
class和:class可以同时使用,最后会把:class追加到class里面
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
.btn-bgcolor {
border: none;
background-color: rgb(14, 230, 190);
}
.btn {
padding: 10px 10px;
}
.active {
color: white;
}
</style>
</head>
<body>
<div id="app">
<button class="btn" :class="{'btn-bgcolor':true,'active':isActive}" @click="change">我是一个按钮</button>
</div>
<!-- <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script> -->
<script src="../assets/vue3.js"></script>
<script>
const app = Vue.createApp({
data() {
return {
isActive: false
}
},
methods: {
change() {
this.isActive = !this.isActive;
}
},
});
app.mount('#app');
</script>
</body>
</html>
效果:打开后是这样的,然后每次点击字体颜色在黑白之间切换


样式逻辑比较复杂的话也可以通过函数来实现上面的效果:就:class这边变换下
<button class="btn" :class="getClass()" @click="change">我是一个按钮</button>
然后method里面新增个getClass的方法,其他都一样
getClass() {
return { 'btn-bgcolor': true, 'active': this.isActive }
}
之前的样式绑定都是属于对象绑定{},也可以数组绑定,eg:
<div :class="[{ 'active': isActive }, errorClass]"></div>
更多参考官方文档:https://cn.vuejs.org/guide/essentials/class-and-style.html
4.动态绑定属性值
语法::[xx]="xxx",比如:<img :[name]="value">,然后通过name控制属性名,value控制属性值
这边举一个常用案例v-bind="xxx":把学生信息以属性的方式绑定到span中:
<div id="app">
<ul>
<li v-for="student in students" :key="student.id">
<!-- <span v-bind="student">{{student.name}}</span> -->
<span :="student">{{student.name}}</span>
</li>
</ul>
</div>
<!-- <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script> -->
<script src="../assets/vue3.js"></script>
<script>
const app = Vue.createApp({
data() {
return {
students: [
{ id: 1, name: '张三', age: 22, gender: '男' },
{ id: 2, name: '李四', age: 29, gender: '女' },
{ id: 3, name: '王二', age: 32, gender: '男' }
]
}
},
});
app.mount('#app');
</script>
效果:

2.3.列表渲染 v-for
通过v-for进行循环遍历students数组中的内容
<div id="app">
<ul>
<li v-for="name in students" :key="name">
{{ name }}
</li>
</ul>
</div>
<script src="../assets/vue3.js"></script>
<script>
const app = Vue.createApp({
data() {
return {
students: ['张三', '李四', '王二']
}
},
})
app.mount('#app')
</script>
效果:

官方文档:https://cn.vuejs.org/guide/essentials/list.html
官方API:https://cn.vuejs.org/api/built-in-directives.html#v-for
2.4.条件渲染 v-if
有个students数组,当里面没有数据的时候需要显示一下提示语,有数据则正常显示
<div id="app">
<template v-if="students.length==0">
<h3>暂时没有看到学生</h3>
</template>
<template v-else>
<ul>
<li v-for="name in students" :key="name">
{{ name }}
</li>
</ul>
</template>
</div>
<!-- <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script> -->
<script src="../assets/vue3.js"></script>
<script>
const app = Vue.createApp({
data() {
return {
students: ['小明', '小张', '王二', 'goudan']
}
},
});
app.mount('#app');
</script>
当students数组里面没有数据时,显示v-if里面的内容,否则就显示v-else里面的内容(template不会显示)

条件渲染:https://cn.vuejs.org/guide/essentials/conditional.html
v-show和v-if最大的不同主要就是:v-show是通过css控制显示与否,不管显示不显示都会有dom,且不支持v-else、template
v-if只要不满足条件直接不渲染,如果一个元素频繁的显示和隐藏可以考虑v-show
官方API文档:https://cn.vuejs.org/api/built-in-directives.html#v-if
2.5.事件处理 v-on
通过method来定义函数,dom通过v-on:事件名="调用函数" 来调用对应的方法,简写:@事件名="调用函数"
1.简单案例
来个案例:通过两个按钮来控制count数值的加和减
<div id="app">
<h3>计数:{{count}}</h3>
<button @click="addCount">++</button>
<button @click="subCount">--</button>
</div>
<script src="../assets/vue3.js"></script>
<script>
const app = Vue.createApp({
data() {
return {
count: 0
}
},
methods: {
addCount() {
this.count++;
},
subCount() {
this.count--;
}
},
})
app.mount('#app')
</script>

2.事件传参案例
如果@click后面方法没有参数,默认就是传event回来,被调用的函数可以是无参,也可以接收这个event
PS:通过
event.target获取的是<li>元素本身,而不是student对象
<div id="app">
<ul>
<li v-for="student in students" :key="student.id" :title="student.name" @click="show">
{{student.id}}.{{ student.name }}-{{ student.age }}-{{ student.gender }}
</li>
</ul>
</div>
<script src=" ../assets/vue3.js">
</script>
<script>
const app = Vue.createApp({
data() {
return {
students: [
{ id: 1, name: '张三', age: 22, gender: '男' },
{ id: 2, name: '李四', age: 29, gender: '女' },
{ id: 3, name: '王二', age: 32, gender: '男' }
]
}
},
methods: {
show(event) {
//如果@click后面方法没有参数,默认就是传event回来
console.log(event.target.title); // event.target获取的是li元素本身,不是student哦~
}
},
});
app.mount('#app');
</script>
效果:单击就显示li的title
如果是多个参数,还需要event,可以通过$event传过来
PS:为什么event要加个
$==> 你不加他到底是字符串,还是data中的变量呢?vue就不知道了
案例代码:
<div id="app">
<ul>
<li v-for="student in students" @click="show(student,$event)" :title="student.name" :key="student.id">
{{student.id}}.{{ student.name }}-{{ student.age }}-{{ student.gender }}
</li>
</ul>
</div>
<!-- <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script> -->
<script src="../assets/vue3.js"></script>
<script>
const app = Vue.createApp({
data() {
return {
students: [
{ id: 1, name: '张三', age: 22, gender: '男' },
{ id: 2, name: '李四', age: 29, gender: '女' },
{ id: 3, name: '王二', age: 32, gender: '男' }
]
}
},
methods: {
show(student, event) {
console.log(student.id, student.name, student.age, student.gender);
console.log(event.target.title);
}
},
});
app.mount('#app');
</script>
效果:
3.函数为什么不用箭头函数★
为什么不用箭头函数?==> 函数中的this不一样
<script>
const app = Vue.createApp({
methods: {
test1: function () {
console.log(this);
},
test2() { // es6写法,本质和test1的一样
console.log(this);
},
test3: () => {
console.log(this); // 这种虽然方便,但是this不一样了
}
},
})
app.mount('#app')
</script>
看下输出示意图:前面两个this都是app对象,而箭头函数里面的this就变成windows了

官方文档:https://cn.vuejs.org/guide/essentials/event-handling.html
事件修饰符:https://cn.vuejs.org/guide/essentials/event-handling.html#event-modifiers
2.6.双向绑定 v-model
1.简单小案例
可以在表单 <input> 、 <textarea> 及 <select> 元素上使用v-model来创建双向数据绑定
<div id="app">
<input v-model="name" type="text" />
<div>{{ name }}</div>
</div>
<script src="../assets/vue3.js"></script>
<script>
const app = Vue.createApp({
data() {
return {
name: '小明',
}
},
})
app.mount('#app')
</script>
文本框内容一改变,文字就改变

再看个综合的案例:列表显示学生信息 + 通过文本框输入内容,回车后添加到列表中
<div id="app">
<input v-model="name" type="text" @keyup.enter="addStudent" />
<div v-if="students.length==0">
<h3>暂时没有学生</h3>
</div>
<ul v-else>
<li v-for="name in students" :key="name">
{{ name }}
</li>
</ul>
</div>
<script src="../assets/vue3.js"></script>
<script>
const app = Vue.createApp({
data() {
return {
name: '',
students: ['张三', '李四', '王二']
}
},
methods: {
addStudent() {
// 把文本框内容加到数组中
this.students.push(this.name);
this.name = '';// 清空文本框
}
},
})
app.mount('#app')
</script>

2.修饰符案例
.lazy:监听change事件而不是input- 默认是刚输入就改变model,现在是回车、提交、事件改变后才修改model
.number: 将输入的合法字符串转为数字.trim:移除输入内容两端空格
<div id="app">
<input type="text" v-model.lazy="name">
<div>{{name}}</div>
</div>
<script src="../assets/vue3.js"></script>
<script>
const app = Vue.createApp({
data() {
return {
name: '小明'
}
},
});
app.mount('#app');
</script>

三个合起来的案例:
<div id="app">
<input v-model.lazy="name" type="text">
<input v-model.number="age" type="text" />
<input v-model.trim="rmark" type="text" />
<div>{{name}}-{{age}}-{{rmark}}</div>
</div>
<script src="../assets/vue3.js"></script>
<script>
const app = Vue.createApp({
data() {
return {
name: '小明',
age: 22,
rmark: '暂无'
}
},
});
app.mount('#app');
</script>

更多可以查看文档:https://cn.vuejs.org/guide/essentials/forms.html
2.7.性能优化 v-memo(new)
这个可以理解为一种缓存,只有设定的内容修改后才会出现渲染列表 ==> 用于性能优化(有时候数据一样,再重新渲染 大列表 就太浪费了)
下面做个测试:v-memo="[name,age,gender]当name、age、gender改变时会出现渲染。rmark更新后不会重新渲染列表
<div id="app">
<!-- 三个属性有一个改变都重新渲染 -->
<div v-memo="[name,age,gender]">
name:{{name}},age:{{age}},gender:{{gender}},rmark:{{rmark}}
</div>
<div>
<button @click="updateName">修改name</button>
<button @click="updateRMark">修改rmark</button>
<button @click="updateInfo">修改所有</button>
</div>
</div>
<!-- <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script> -->
<script src="../assets/vue3.js"></script>
<script>
const app = Vue.createApp({
data() {
return {
name: '张三',
age: 23,
gender: '男',
rmark: '暂无'
}
},
methods: {
updateName() {
this.name = '李四';
},
updateRMark() {
// 修改不在v-memo列表中的内容,页面不会出现渲染
this.rmark = '修改';
},
updateInfo() {
this.name = '测试';
this.age = 44;
this.gender = '中';
this.rmark = '没有';
}
},
});
app.mount('#app');
</script>

官方文档:https://cn.vuejs.org/api/built-in-directives.html#v-memo
2.8.计算属性 computed
有复杂计算的时候直接在双括号{{}}里面写复杂表达式,是不合适的,可以通过computed来处理
<div id="app">
<p v-for="i in 3" :key="i">
{{ name }}-{{genderStr}}-{{dataTime}}
</p>
</div>
<!-- <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script> -->
<script src="../assets/vue3.js"></script>
<script>
const app = Vue.createApp({
data() {
return {
name: '张三',
gender: 1,
time: 1670944665
}
},
computed: {
genderStr() {
console.log('gender')
return this.gender == 1 ? '男' : '女';
},
dataTime() {
console.log('dataTime')
const date = new Date(this.time)
return `${date.getFullYear()}-${date.getMonth() + 1}-${date.getDate()}`
}
},
});
app.mount('#app');
</script>
computed比method多了个缓存:

官方文档:https://cn.vuejs.org/guide/essentials/computed.html
2.9.侦听器 watch
watch监听数据的案例:title修改可以直接监听到,而student只修改name则监听不到了(student中三个属性全部被修改才能监听到)
<div id="app">
<h2>{{title}}</h2>
<ul>
<li v-for="item in student" :key="item">
{{ item }}
</li>
</ul>
<div><button @click="changeInfo">修改内容</button></div>
</div>
<!-- <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script> -->
<script src="../assets/vue3.js"></script>
<script>
const app = Vue.createApp({
data() {
return {
title: '欢迎光临~',
student: { id: 1, name: '张三', age: 32 },
}
},
methods: {
changeInfo() {
this.title = '网站已经被黑!';
this.student.name = '德行';
this.student.age = 32;
}
},
watch: {
title(newValue, oldValue) {
console.log(`【${oldValue}】被修改为:【${newValue}】`)
},
student(newValue, oldValue) {
console.log(newValue);
}
},
});
app.mount('#app');
</script>

如果想student对象一变化就侦听到,可以使用deep: true(慎重使用,数据太多太深会影响性能)如果一开始就触发监听就使用immediate: true

如果只是想检测student里面某个值的改变可以这样写:

官方文档:https://cn.vuejs.org/guide/essentials/watchers.html
2.10.综合案例
模拟一个购物车,有这些要求:
- 如果购物车没有数据要提示下
v-ifandv-else
- 通过加减可以设置书籍的数量,当数量<=1的时候,减按钮不能点
:disabled="disable=item.count<2"
- 点击移除书籍可以删掉该图书
- 先获取当前book的
index(注意不是id)v-for="(item,index) in books" - 然后通过
array.splice(index, 1);删掉对应对象
- 先获取当前book的
- 底部有个价格的汇总
- 通过
computed,遍历books,价格*数量并累加
- 通过
样式:
<style>
.border {
border-collapse: collapse;
}
td {
border: 1px solid #aaa;
padding: 10px;
}
</style>
HTML:
<div id="app">
<table class="border">
<tr v-for="(item,index) in books" :key="index">
<td>{{ item.id }}</td>
<td>{{ item.name }}</td>
<td>{{ item.price }}</td>
<td>
<button :disabled="disable=item.count<2" @click="subtract(item)">-</button>
{{ item.count }}
<button @click="add(item)">+</button>
</td>
<td><button @click="del(index)">移除书籍</button></td>
</tr>
</table>
<h3 v-if="books.length>0">结算金额:{{totalCompute}}</h3>
<h3 v-else>购物车中暂时没有书籍数据!</h3>
</div>
Script:
const app = Vue.createApp({
data() {
return {
books: [
{ id: 1, name: 'Net安全', price: 44.5, count: 1 },
{ id: 2, name: 'Web安全', price: 64, count: 1 },
{ id: 3, name: 'Net基础', price: 38, count: 1 },
{ id: 4, name: 'Vue学习', price: 50.1, count: 1 },
]
}
},
methods: {
subtract(item) {
item.count--;
},
add(item) {
// this.books[id - 1].count++;
item.count++;
},
del(index) {
this.books.splice(index, 1); //删除数组序号为index的对象
}
},
computed: {
totalCompute() {
// let total = 0;
// // 遍历一下books,并把每一项价格统计下
// this.books.forEach(book => {
// total += book.price * book.count;
// });
// return total;
return this.books.reduce((total, book) => total + book.count * book.price, 0);
}
},
});
app.mount('#app');
效果:

3.Vue3组件入门(过渡)
目前这一块以选项API(Options API)为主,后面会逐步过渡到组合API(Composition API)
官方文档:https://cn.vuejs.org/guide/essentials/component-basics.html
深入组件:https://cn.vuejs.org/guide/components/registration.html

3.1.组件注册
- 全局组件:在任何其他的组件中都可以使用的组件
app.component()
- 局部组件:局部注册的组件需要在使用它的父组件中显式导入,并且只能在该父组件中使用
components:{}
1.app根组件
先看下全局组件的案例:我们平时创建的app对象,其实就是一个全局组件:
<div id="app">
{{title}}
</div>
<!-- <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script> -->
<script src="../assets/vue3.js"></script>
<script>
const app = Vue.createApp({
data() {
return {
title: 'this is component test'
}
},
});
app.mount('#app');
</script>
我们换个写法和这个也一样的效果:
<div id="app">
{{title}}
</div>
<!-- <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script> -->
<script src="../assets/vue3.js"></script>
<script>
// vueApp对象{}
const vueApp = {
data() {
return {
title: 'this is component test'
}
},
};
const app = Vue.createApp(vueApp);
app.mount('#app');
</script>
效果:
2.全局组件
全局组件:每个组件都可以在组件内部调用全局组件
PS:组件本身是可以有自己的代码逻辑的,比如data、computed、methods等等
我们自定义一个全局组件:app.component('组件名',{})
组件名最好是
xxx-xxx的格式,Vue文件中也可以是AbcDemo的这种格式
const app = Vue.createApp({});
// 创建自定义的全局组件
app.component('my-title', {
data() {
return {
title: 'this is component test'
}
},
template: `<h2>{{title}}</h2>`
})
app.mount('#app');
HTML部分:
<div id="app">
<my-title></my-title>
</div>
效果:template不出现在dom中

换种写法效果和这个一样(组件里面的template没有智能提示,放页面中就有了,这种写法后面会用)
<div id="app">
<my-title></my-title>
</div>
<!-- my-title组件的template-->
<template id="title">
<h2>{{title}}</h2>
</template>
<!-- <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script> -->
<script src="../assets/vue3.js"></script>
<script>
const app = Vue.createApp({});
// 创建自定义的全局组件
const myTitle = {
data() {
return {
title: 'this is component test'
}
},
template: '#title'
};
app.component('my-title', myTitle)
app.mount('#app');
</script>
显示效果是一样的,template默认是不会显示出来的,但是dom里面是有的

3.局部组件
开发中基本上都是局部组件,还是上面的案例,我们用局部组件注册的方式改写下:
组件名一般都是
xxx-xxx的格式,Vue文件中也可以是AbcDemo的这种大驼峰的命名格式
<div id="app">
<my-title></my-title>
</div>
<script src="../assets/vue3.js"></script>
<script>
// 根组件
const App = {
components: {
// 局部组件名:对象内容
'my-title': {
data() {
return {
title: 'this is vue component test'
}
},
template: '<h2>{{title}}</h2>',
},
},
};
Vue.createApp(App).mount('#app'); //创建并挂载
</script>
展示效果:

进一步分离实现相同效果:
<div id="app">
<my-title></my-title>
</div>
<template id="title">
<h2>{{title}}</h2>
</template>
<script src="../assets/vue3.js"></script>
<script>
// 自定义组件名
const MyTitle = ('my-title', {
data() {
return {
title: 'this is vue component test'
}
},
template: '#title',
});
// 根组件
const App = {
components: {
MyTitle, // 局部组件
},
};
Vue.createApp(App).mount('#app'); //创建并挂载
</script>
展示:

3.2.单文件组件
文档代码:https://gitee.com/lotapp/BaseCode/tree/master/javascript/3.Vue/4vue3component
.vue单文件里面就三块内容:<template>放HTML、<script>放JS、<style>放CSS
PS:
xxx.vue浏览器是不认识的,最后是通过webpack、vite这类打包工具进行打包,最后生成了类似我们上面写的代码风格的html
单个Vue组件中可以有更多的支持:
- 代码高亮
- ES6、CommonJS的模块化能力;
- 组件作用域的CSS
- 预处理器来构建组件
- eg:
TypeScript、Babel、Less、Sass等
- eg:
说那么多,怎么去使用呢?
- 通过
Vue CLI创建项目,所有配置都默认配置好了,我们在里面直接使用Vue文件开发即可 - 通过
webpack、vite这类打包工具进行打包处理
1.脚手架创建项目
vue官方现在推荐使用:cnpm create vue@latest来创建。这个打包默认使用的是vite了
PS:vue-cli默认使用的是webpack,现在已经不更新了

cd vue3-demo切换到新建的目录中,然后输入cnpm install来安装一下依赖。然后运行项目:cnpm run dev
PS:vscode终端中运行
npm run dev(PowerShell中运行cnpm有点问题)

命令行输入:运行
npm run dev、编译:npm run build、预览:npm run preview,IDE运行:点下调试选对应选项即可

我们也可以使用图形化操作,bash里面输入:vue ui,然后会自动打开一个UI页面,在里面新建或者选择项目也行

build的话会在原项目下面创建一个dist的发布文件夹,我们写的代码都打包成这里面的文件了,直接部署到服务器即可

2.目录说明
vscode打开后大致这样的感觉,我们平时在src中开发即可,index.html是主页面

main.js是主函数,里面会加载main.css以及挂载vue对象。这个导入进来的createApp方法,相当于是以前写的vue.createApp
PS:从
App.vue中导入进来的App对象,相当于之前写的const App={xxx}

每一个xxx.vue都相当于一个独立的组件,里面就三个元素:<template>、<srcipt>、<style>

3.根组件案例(含详细说明)
以前我们要创建一个vue组件都是在html中写,比如:
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
h2 {
color: red;
}
</style>
</head>
<body>
<div id="app">
<h2>{{title}}</h2>
</div>
<script src="../../assets/vue3.js"></script>
<script>
const App = {
data() {
return {
title: 'this is vue component test'
}
},
};
Vue.createApp(App).mount('#app');
</script>
</body>
</html>
运行后是这样的:

现在用Vue文件开发略有不同,我这边详细说下:
我们看下main.js:导入了vue.createApp方法、也从App.vue中导入了App对象{}

这里的#app在index.html中写了

贴一下App.vue的根组件:如果晕头转向请自己练一遍,然后就清楚了
脚手架创建后可以把不相关的vue文件删掉,只保留
App.vue(把main.css和base.css中的样式先删掉)
<template>
<h2>{{ title }}</h2>
</template>
<script>
// 有import导入就有export导出
export default {
data() {
return {
title: 'this is vue component test'
}
},
}
</script>
<style>
h2 {
color: red;
}
</style>
运行项目可以手动点vscode里面的调试运行,也可以Ctrl + J打开vscode终端,命令行输入npm run dev
如果不需要调试:开发过程中也可以在项目目录下单独开一个bash,输入
npm run dev,然后不关闭它,你修改它也会同步修改的

运行效果:

4.全局组件-vue文件版
这种方式平时用的不多,简单说下:
MyTitle.vue:
<script>
// 有import导入就有export导出
export default {
data() {
return {
title: 'this is vue component test'
}
},
}
</script>
<template>
<h2>{{ title }}</h2>
</template>
<style>
h2 {
color: red;
}
</style>
App.vue:
<template>
<MyTitle></MyTitle>
</template>
main.js
import './assets/main.css'
import { createApp } from 'vue'
import App from './App.vue'
import MyTitle from './components/MyTitle.vue'
const app = createApp(App)
app.component('MyTitle', MyTitle)
app.mount('#app')
效果:

5.局部组件-vue文件版
平时基本上都是这种局部组件的方式开发,还是上面的案例,贴下代码:
PS:以后我就直接贴
组件.vue和App.vue,这边把相关文件都贴下
1MyTitle.vue代码:
<script>
// 有import导入就有export导出
export default {
data() {
return {
title: 'this is vue component test'
}
},
}
</script>
<template>
<h2>{{ title }}</h2>
</template>
<style>
h2 {
color: red;
}
</style>
2App.vue代码:
<script>
// 1先拿到组件-导入MyTitle
import MyTitle from './components/MyTitle.vue';
export default {
components: {
MyTitle, // 2写下使用到的局部组件MyTitle
},
}
</script>
<template>
<!-- 3使用自定义组件 -->
<MyTitle></MyTitle>
</template>
<style></style>
3main.js内容不变:
import './assets/main.css'
import { createApp } from 'vue'
import App from './App.vue'
createApp(App).mount('#app')
4index.html内容不变:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<link rel="icon" href="/favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vite App</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>
项目运行后结果:

6.样式的作用域
之前代码其实有个问题,比如代码和局部组件一样的情况下,我App.vue里面也有个<h2>,而h2的样式其实是在MyTitle组件中设置的,但也影响到了App.vue

解决也很简单,就是在样式里面设置一个作用域:<style scoped>

这样MyTitle.vue中设置的样式就不会影响到其他组件了(其实本质就是给他设置了一个属性选择器)

7.组件嵌套
父组件用了几个子组件就导入几个,不用管子组件里面套了多少层,来个组件树的案例:

App.vue:对于App.vue来说,我的子组件就这四个

AppHeader.vue

AppFlooter.vue

AppMain.vue:就一个子组件AppList

AppSideBar.vue:也只有一个子组件

AppList.vue:里面一个子组件,重复2次

AppItem.vue,来个数据展示

运行后效果:

3.3.组件通信
父子组件间的通信比较简单,父传子是通过props传递,子传父是通过$emit回传

1.父组件发信息给子组件-props★
父组件给子组件传递信息的本质就是:在HTML标签上打上自定义属性,eg:<span name="张三" age="22" gender="男">
然后子组件通过props的配置,eg:props:['name','age','gender'],知道去获取哪些自定义属性,并得到他们的值:
来个案例:StudentInfo.vue
<template>
{{ id }}.{{ name }}-{{ age }}-{{ gender }}
</template>
<script>
export default {
// props的作用就是接收父组件传递过来的属性
props: ['id', 'name', 'age', 'gender']
}
</script>
<style scoped></style>
传递本质就是这种:{{ xxx }}除了在data里面找数据,也会去props里面查找
<template>
<!-- {{ xxx }}除了在data里面找数据,也会去props里面查找 -->
<StudentInfo id="1" name="小明" age="22" gender="男"></StudentInfo>
</template>
<script>
// 1.父组件传递信息给子组件
import StudentInfo from './components/StudentInfo.vue'
export default {
components: {
StudentInfo,
},
}
</script>
但vue中有自定义属性,可以更方便处理,我们改写成灵活的写法:
<template>
<StudentInfo :id="id" :name="name" :age="age" :gender="gender"></StudentInfo>
<!-- <AppSon :students="objs"></AppSon> -->
</template>
<script>
// 1.父组件传递信息给子组件
import StudentInfo from './components/StudentInfo.vue'
export default {
components: {
StudentInfo,
},
data() {
return {
id: 1,
name: '小明',
age: 22,
gender: '男'
}
},
}
</script>
效果:1.小明-22-男
props除了传递列表,还可以传递对象,并在这个对象里面定义数据类型、默认值、是否必传(require默认是false)等,来个案例:
PS:props中的type类型可以是:String、Number、Boolean、Array、Object、Date、Function、Symbol
AppSon.vue:
<template>
<ul>
<li v-for="student in students" :key="student.id">
{{ student.id }}.{{ student.name }}-{{ student.age }}-{{ student.gender }}
</li>
</ul>
</template>
<script>
export default {
props: {
students: {
type: Array,
default: []
},
},
}
</script>
<style scoped></style>
App.vue:
<template>
<AppSon :students="objs"></AppSon>
</template>
<script>
import AppSon from './components/AppSon.vue';
export default {
components: {
AppSon,
},
data() {
return {
objs: [
{ id: 1, name: '张三', age: 22, gender: '男' },
{ id: 2, name: '李四', age: 29, gender: '女' },
{ id: 3, name: '王二', age: 32, gender: '男' }
]
}
},
}
</script>
效果:

2.子组件发信息给父组件-$emit★
子组件给父组件传递信息,其实是通过某个事件方法执行了:$emit(自定义事件,传递的值),当自定义事件触发后就会调用父方法进行数值的处理
为了方便理解,我事件名都弄不一样的:
说下流程,用户文本框里面输入内容,回车后触发子组件定义的method;这个method里面执行了
$emit并把文本框清空;接着自定义的@addStudent就被触发了,从而调用父组件的方法parentMethod对数据进行进一步处理
AppSon1.vue:
<template>
<div>
<input v-model="name" type="text" @keyup.enter="sonMethod" />
</div>
</template>
<script>
export default {
data() {
return {
name: ''
}
},
emits: ['addStudent'], // 为了代码提示和协同开发
methods: {
sonMethod() {
console.log('into appson1.vue sonMethod');
this.$emit('addStudent', this.name); // 把name值传递给组件自定义事件addStudent
this.name = '';
}
},
}
</script>
<style scoped></style>
App.vue:
<template>
<!-- 3.子组件传递信息给父组件 -->
<span v-for="student in students" :key="student">
{{ student }}-
</span>
<AppSon1 @addStudent="parentMethod"></AppSon1>
</template>
<script>
// 3.子组件传递信息给父组件
import AppSon1 from './components/AppSon1.vue';
export default {
components: {
AppSon1
},
data() {
return {
students: [],
}
},
methods: {
// 自定义addStudent触发后,会调用对应的parentMethod方法
parentMethod(name) {
console.log('into app.vue parentMethod');
this.students.push(name);
}
},
}
</script>
输入小明回车,再输入小华回车,就现在这个效果:

扩展补充下:对于协同开发来说,你可能是开发的父组件,但是子组件是另一个人开发的,emit在逻辑里面,需要找半天,很麻烦。vue3提供了一种emits的参数支持,以后在定义自定义事件的时候把emits列表里面也写下,这样协同开发比较方便,而且vscode也会有对应的提示
比如,我在emits里面定义了这几个自定义事件:emits: ['addStudent','delStudent','updateStudent']
emits也可以是对象,可以对传给父组件的值进行验证,平时列表就够了,扩展可看官网:https://cn.vuejs.org/api/options-state.html#emits

那么我在vscode开发的时候,父组件在写事件的时候就会有提示了

再来个navbar的案例:TabBar.vue
<template>
<div id="nav">
<div :id="key" v-for="(value, key, index) in contents" @click="getContent(value)" :key="index">
<span :class="{ active: currentIndex == index }" @click="currentIndex = index">
{{ key }}
</span>
</div>
</div>
</template>
<script>
export default {
data() {
return {
currentIndex: -1,
contents: {
'left': '我是Left的内容',
'center': '我是Center的内容',
'right': '我是Right的内容'
}
}
},
emits: ['content'],
methods: {
getContent(value) {
this.$emit('content', value)
}
},
}
</script>
<style scoped>
.active {
font-weight: bold;
padding: 10px;
border-bottom: solid 2px red;
}
#nav {
display: flex;
background-color: rgb(255, 251, 255);
}
#left {
flex: 1;
text-align: center;
padding: 10px;
/* background-color: rgb(245, 201, 230); */
}
#center {
flex: 3;
text-align: center;
padding: 10px;
/* background-color: rgb(246, 246, 161); */
}
#right {
flex: 1;
text-align: center;
padding: 10px;
/* background-color: rgb(87, 243, 178); */
}
</style>
App.vue:
<template>
<div>
<TabBar @content="getContent"></TabBar>
<div id="bar-item">{{ content }}</div>
</div>
</template>
<script>
import TabBar from './components/TabBar.vue';
export default {
components: {
TabBar,
},
data() {
return {
content: ''
}
},
methods: {
getContent(value) {
this.content = value;
}
},
}
</script>
<style scoped>
#bar-item {
text-align: center;
padding: 20px;
background-color: rgb(249 248 248);
}
</style>
效果:

3.插槽案例☆
插槽通俗讲就是:子组件预留一个坑位,父组件后期来填坑
PS:如果插槽没有放元素,则显示默认内容。如果放了内容,插槽原先的内容会被忽略掉
3.1.默认插槽
每个插槽都有一个具体的名字,当子组件里面就一个插槽的时候,我们不用写(default),vue自动帮我们处理了
SlotSimple.vue:
<template>
<div>
<h2>slot案例:</h2>
<slot>
我是默认的内容
</slot>
</div>
</template>
<script>
export default {
}
</script>
<style scoped>
</style>
App.vue:
<template>
<div>
<SlotSimple></SlotSimple>
<SlotSimple>
<button>slot one btn</button>
</SlotSimple>
</div>
</template>
<script>
import SlotSimple from './components/SlotSimple.vue';
export default {
components: {
SlotSimple,
},
}
</script>
<style scoped></style>
效果:上面的父组件没填坑,就会显示子组件里默认的内容,下面的父组件放了一个按钮,则插槽内容显示自定义的button

3.2.具名插槽☆
子组件里面写下<solt name=xxx>,父组件在插槽内容外包裹一个<template>,eg:<template v-slot=xxx> or <template #xxx>
SlotName.vue:
<template>
<div>
<h2>slot案例:</h2>
<slot name="default">
我是默认的slot
</slot>
<slot name="center">
我是默认的Center
</slot>
<slot name="right">
我是默认的Right
</slot>
</div>
</template>
App.vue:<template v-slot:center>和<template #center>效果一样
<template>
<div>
<SlotName>
<template #default>
<h3>default</h3>
</template>
<template v-slot:center>
<h3>center</h3>
</template>
<template #right>
<h3>right</h3>
</template>
</SlotName>
</div>
</template>
<script>
import SlotName from './components/SlotName.vue';
export default {
components: {
SlotName,
},
}
</script>
效果:

3.3.动态插槽名
父组件里面的插槽名也可以使用:动态插槽名,这个用的不多就不写案例了,子组件和具名一样定义,就是父组件这边v-slot:[变量名]
- 子组件正常定义具名插槽:
<solt name=xxx> - 父组件支持动态插槽名的控制:
<template v-slot:[变量名]>

3.4.作用域插槽☆
说作用域插槽的时候先看一个编译作用域的概念:
比方说我现在有一个子组件TabBar.vue,这个子组件里面的data数据,父组件App.vue是不能直接访问的(ps:能相互访问还搞啥通信)

反过来也一样,子组件也不能直接访问父组件里面的data(父组件可以直接访问父组件的数据)

现在要说的是:子组件里定义了一个slot插槽,开始内容是在父组件里面书写的,父组件有数据还好,如果父组件没有插槽里面的数据呢?可以直接传过去吗? ==> 可以的,vue提供了一种便捷方法
为什么要从solt中传递参数给父组件? ==> 受编译作用域限制,拿不到子组件的变量,只能通过pros拿到
作用域插槽的本质就是:把子组件的数据传递给父组件的插槽使用。看个案例:
SlotProps.vue:把值绑定到属性上,template的name和上面一样,是唯一的
<template>
<div>
<template v-for="(v, k, i) in contents" :key="i">
<slot :name="k" :k="k" :v="v" :i="i"></slot>
</template>
</div>
</template>
<script>
export default {
data() {
return {
contents: {
'left': '我是Left的内容',
'center': '我是Center的内容',
'right': '我是Right的内容'
}
}
},
}
</script>
App.vue:v-slot:ID名称="props" ==> 简写:#ID名称="props"
<template>
<div>
<SlotProps>
<template v-slot:left="props">
<p>{{ props.i }} - {{ props.k }} - {{ props.v }}</p>
</template>
<template v-slot:center="props">
<p>{{ props.i }} - {{ props.k }} - {{ props.v }}</p>
</template>
<template #right="props">
<p>{{ props.i }} - {{ props.k }} - {{ props.v }}</p>
</template>
</SlotProps>
</div>
</template>
<script>
import SlotProps from "./components/SlotProps.vue";
export default {
components: {
SlotProps,
},
}
</script>
效果:

4.非父子组件通信
如果深层次嵌套的组件想要传递数据,靠props一层层传递会非常麻烦,vue提供了Provide(父组件提供数据)和Inject(子孙组件进行注入)和全局事件总线来实现非父子组件之间的通信
4.1.依赖注入案例
来个案例,点击按钮可以修改信息,这样可以验证一下数据是否一致
先说下下树结构:我们现在不想层层传递数据,想直接把app.vue的数据传给appItem.vue
PS:依赖注入是针对父级组件与子孙级组件之间的数据传递(隔了很多层,我们不想一层层传递数据,就可以使用依赖注入)

AppItem.vue:inject是一个列表
<template>
<div>
<h2>{{ title }}</h2>
{{ student.name }}-{{ student.age }}-{{ student.gender }}
</div>
</template>
<script>
export default {
inject: ['title', 'student'] // 注入title、student
}
</script>
App.vue:provide是方法的形式(不是方法this就获取不到data里面的数据了)
<template>
<div>
<AppMain></AppMain>
<button @click="changeStudent">改数值</button>
</div>
</template>
<script>
import AppMain from './components/AppMain.vue';
export default {
data() {
return {
title: '这是一个依赖注入案例',
student: {
name: '小明',
age: 22,
gender: '男'
}
}
},
components: {
AppMain,
},
// 要用provide方法,不然this就不对了,获取不到data信息了
provide() {
return {
title: this.title,
student: this.student
}
},
methods: {
changeStudent() {
this.student.name = '长孙无敌';
this.student.age = 33;
this.title = '网站已经被黑';
}
},
}
</script>
打开后是这样的,看起来貌似没有问题:

但是点击修改数值后发现:title没有被修改,student类型反而被修改了
PS:引用类型这种没有问题,简单类型你传递
title: this.title相当于传递了这个:title:'这是一个依赖注入案例'所以要处理下

我们导入响应式API中的computed函数,微微修改下App.vue:
<template>
<div>
<AppMain></AppMain>
<button @click="changeStudent">改数值</button>
</div>
</template>
<script>
// 导入computed函数
import { computed } from 'vue';
import AppMain from './components/AppMain.vue';
export default {
data() {
return {
title: '这是一个依赖注入案例',
student: {
name: '小明',
age: 22,
gender: '男'
}
}
},
components: {
AppMain,
},
// 要用provide方法,不然this就不对了,获取不到data信息了
provide() {
return {
// 变化点在这,title不是直接复制一个简单的字段,而是计算属性
title: computed(() => {
return this.title;
}),
student: this.student
}
},
methods: {
changeStudent() {
this.student.name = '长孙无敌';
this.student.age = 33;
this.title = '网站已经被黑';
}
},
}
</script>
再点一下就发现标题也修改了

更多可以查看文档:https://cn.vuejs.org/guide/components/provide-inject.html
4.2.事件总线案例
依赖注入必须是有子孙关系的组件通信,如果来个不相关的组件通信呢?==> 事件总线
PS:这个有点类似后端开发中MQ的订阅与发布
我们来个树结构:现在AppHeader要和AppItem进行通信

Vue2里面是有事件总线,vue3从实例中移除了$on、$off、$once方法,如果要使用事件总线可以使用官方推荐的库:mitt、tiny-emitter
首先安装一下mitt:cnpm i mitt,然后我们来封装一个事件总线的js库(eventBus.js):
import mitt from 'mitt';
const eventBus = new mitt();
export default eventBus;

AppHeader作为我们数据的发送方:(发布消息)
<template>
<div>
<button @click="sendMessage">AppHeader send message</button>
</div>
</template>
<script>
import eventBus from '../util/eventBus'
export default {
data() {
return {
title: '这是一个事件总线的订阅发布案例',
student: {
name: '小华',
age: 32,
gender: '男'
}
}
},
methods: {
sendMessage() {
console.log('AppHeader.vue sendMessage')
// 发送事件,传递消息
eventBus.emit('sendData', {
title: this.title,
student: this.student
});
}
},
}
</script>
AppItem作为信息接收方:(订阅消息)unmounted一定也写下,组件销毁后事件就没必要继续订阅了
<template>
<div v-if="title != '' && student != []">
<h2>{{ title }}</h2>
{{ student.name }}-{{ student.age }}-{{ student.gender }}
</div>
</template>
<script>
import eventBus from '../util/eventBus';
export default {
data() {
return {
title: '',
student: {}
}
},
methods: {
getMessage(data) {
console.log('AppItem.vue getMessage');
this.student = data.student;
this.title = data.title;
}
},
created() {
console.log('AppItem.vue created');
eventBus.on('sendData', this.getMessage); // 订阅事件sendData,触发后调用getMessage
},
unmounted() {
console.log('AppItem.vue mounted');
eventBus.off('sendData', this.getMessage); // 组件销毁的时候取消事件订阅
},
}
</script>
运行后的效果:

当我们触发事件发布时:信息正常被发送并接收了

3.4.生命周期★
每个组件都会经历:创建、挂载、更新、卸载等系列的过程,我们可以在对应阶段做一些操作(eg:create阶段可以异步加载数据)
Vue提供了一些生命周期的回调函数(钩子函数)https://cn.vuejs.org/api/composition-api-lifecycle.html
1.简单说明
- beforecreate // 执行时组件实例还未创建,通常用于插件开发中执行一些初始化任务
- eg:
App.vue中有个<AppItem>,vue自己初始化完毕,准备去创建<AppItem>实例,但是还没创建
- eg:
- created★ // 组件实例(js对象)创建完毕,各种数据可以使用,常用于异步数据获取、事件监听、
this.$watch() - eg:
<AppItem>组件实例已经创建了,但是里面<template>还没编译 - beforeMounted // 未执行渲染、更新,dom未创建
- eg:这时候
<AppItem>的<template>内容已经有了,但是还没挂载到App.vue上(还没挂载到虚拟DOM)
- eg:这时候
- mounted ★// 初始化(挂载)结束,dom已创建,可用于获取访问数据和dom元素
- eg:已经挂载到虚拟DOM上,并且生成了真实的DOM(用户可以看到HTML元素了)
- beforeupdate // 更新前,可用于获取更新前各种状态
- 当我们数据发生改变的时候,会通过DIFF算法,重新渲染和更新DOM
- updated // 更新后,所有状态已是最新
- 在数据更新完毕,重新进入mounted 挂载之前,会进入updated
- beforeUnmount // 销毁前,可用于一些定时器或订阅的取消(组件还在)
- PS:当组件显示与否通过v-if控制时,不显示的时候就会把组件销毁,在销毁前进入这个函数
- unmounted★ // 组件已销毁,可用于一些定时器或订阅的取消(组件不在了)
- 已经移除掉组件的虚拟DOM了,组件实例会被销毁掉
更多参考官方文档:https://cn.vuejs.org/guide/essentials/lifecycle.html
生命周期钩子:https://cn.vuejs.org/api/composition-api-lifecycle.html
2.案例演示
App.vue:
<template>
<div>
<div>
<button @click="b = !b">是否显示组件</button>
</div>
<template v-if="b">
<AppItem :count="count">
<button @click="addCount">count++</button>
</AppItem>
</template>
</div>
</template>
<script>
import AppItem from './components/AppItem.vue';
export default {
components: { AppItem },
data() {
return {
b: false,
count: 0
}
},
methods: {
addCount() {
this.count++;
}
}
}
</script>
AppItem.vue
<template>
<div>
{{ count }} <slot></slot>
</div>
</template>
<script>
export default {
props: ['count'],
beforeCreate() {
console.log('AppItem beforeCreate');
},
created() {
console.log('AppItem created');
},
beforeMount() {
console.log('AppItem beforeMount');
},
mounted() {
console.log('AppItem mounted');
},
beforeUpdate() {
console.log('AppItem beforeUpdate');
},
updated() {
console.log('AppItem updated');
},
beforeUnmount() {
console.log('AppItem BeforeUnmount');
},
unmounted() {
console.log('AppItem Unmounted');
},
}
</script>
运行后因为v-if=false,所以AppItem没有加载:

我们现在点一下按钮:AppItem组件被创建并挂载了

我们点3下count++按钮就会触发三次数据修改函数(AppItem组件销毁前基本上都beforeUpdate、updated中来回反复)

我们再点下是否显示组件的按钮,这时候v-if又变成false了,AppItem组件就被卸载了

3.5.组件ref引用☆
$refs案例
$refs场景:有些时候组件中想要直接获取到元素对象、子组件实例(比如获取元素的宽高)【很少用到】
Vue不推荐进行原生DOM操作 ==> 给元素或者组件绑定一个ref的attribute属性
来个例子:把dom元素里面加个属性:ref="名字",然后就可以通过:this.$refs.名称获取到dom
AppItem.vue
<template>
<div>
<input v-model="title" type="text" ref="title" />
<button ref="btn" @click="getRefs">提交</button>
</div>
</template>
<script>
export default {
data() {
return {
title: 'title'
}
},
methods: {
getRefs() {
console.log(this.$refs.title);
console.log(this.$refs.btn);
}
},
}
</script>
效果:提交的时候就可以获取到input的dom和button的dom对象

如果ref绑定在组件上,可以获取组件的实例,通过这个实例可以调用里面的方法、字段,也可以获取里面的dom($el)
获取组件DOM:
this.$refs.item.$el、获取组件数据:this.$refs.item.xxx、调用组件方法:this.$refs.item.xxx();
AppItem.vue还是上面的代码
<template>
<div>
<input v-model="title" type="text" ref="title" />
<button ref="btn" @click="getRefs">提交</button>
</div>
</template>
<script>
export default {
data() {
return {
title: 'title'
}
},
methods: {
getRefs() {
console.log(this.$refs.title);
console.log(this.$refs.btn);
},
},
}
</script>
App.vue修改了下:
<template>
<div>
<button @click="callItem">ref调用AppItem</button>
<AppItem ref="item"></AppItem>
</div>
</template>
<script>
import AppItem from './components/AppItem.vue';
export default {
components: { AppItem },
methods: {
callItem() {
console.log(this.$refs.item.$el);
console.log(this.$refs.item.title);
this.$refs.item.getRefs();
}
},
}
</script>
效果:

扩展:\(parent、\)root
this.$parent ==> 获取父组件实例、this.$root ==> 获取根组件实例(Vue3中已经删掉$children)
PS:这个用的不多,和上面ref获取组件差不多,eg:
this.$parent.$el
3.6.动态组件
1.切换组件的传统开发
讲动态组件之前先来个案例:现在有三个按钮,按钮名对应着同名的组件,我们点击按钮的时候下面显示对应的组件内容+按钮字变红
PS:我们平时如果要实现多组件切换,基本上都是通过:
v-if、v-else-if 、v-else来实现:
AppLeft、AppCenter、AppRight代码差不多,就H2这边名字不一样:
<template>
<div>
<h3>AppCenter</h3>
<ul>
<li v-for="item in students" :key="item.id">
{{ item.id }}-{{ item.name }}-{{ item.Age }}-{{ item.gender }}
</li>
</ul>
</div>
</template>
<script>
export default {
props: ['students']
}
</script>
<style scoped></style>

App.vue:
<template>
<div>
<button :class="{ active: currName == name }" v-for="name in names " :key="name" @click="changeTab(name)">
{{ name }}
</button>
<template v-if="currName == 'AppLeft'">
<AppLeft :students="students"></AppLeft>
</template>
<template v-else-if="currName == 'AppCenter'">
<AppCenter :students="students"></AppCenter>
</template>
<template v-else>
<AppRight :students="students"></AppRight>
</template>
</div>
</template>
<script>
import AppCenter from './components/AppCenter.vue';
import AppLeft from './components/AppLeft.vue';
import AppRight from './components/AppRight.vue';
export default {
data() {
return {
names: ['AppLeft', 'AppCenter', 'AppRight'], // 组件名称
currName: 'AppLeft', // 相当于索引
students: [ // 传递给子组件的数据
{ id: 1, name: '小明', age: 22, gender: '男' },
{ id: 2, name: '小华', age: 33, gender: '女' },
{ id: 1, name: '小花', age: 28, gender: '男' },
]
};
},
methods: {
changeTab(name) {
this.currName = name; // 把当前tab名赋予currName,这样active类就生效了
}
},
components: { AppLeft, AppCenter, AppRight } // 子组件局部注册
}
</script>
<style scoped>
.active {
color: red;
}
</style>
运行效果:点击谁就显示谁

2.切换组件-Vue动态组件
这种if else的方式判断数据少还好,数据多了能写死,vue提供了一种动态组件的方式
语法:
<component :is="tabs[currentTab]"></component>

被传给 :is 的值可以是:1.被注册的组件名(全局、局部)2.导入的组件对象
不用改任何代码,就是把一系列的判断改成了:<component :is="currName" :students="students"></component>

App.vue::is="组件名"、:student="students"是父组件传给子组件的数据
<template>
<div>
<button :class="{ active: currName == name }" v-for="name in names" @click="changeTab(name)" :key="name">
{{ name }}
</button>
<component :is="currName" :students="students"></component>
</div>
</template>
<script>
import AppCenter from './components/AppCenter.vue';
import AppLeft from './components/AppLeft.vue';
import AppRight from './components/AppRight.vue';
export default {
data() {
return {
names: ['AppLeft', 'AppCenter', 'AppRight'],
currName: 'AppLeft',
students: [
{ id: 1, name: '小明', age: 22, gender: '男' },
{ id: 2, name: '小华', age: 33, gender: '女' },
{ id: 1, name: '小花', age: 28, gender: '男' },
]
};
},
methods: {
changeTab(name) {
this.currName = name;
}
},
components: { AppLeft, AppCenter, AppRight }
}
</script>
<style scoped>
.active {
color: red;
}
</style>
运行效果一样:点击谁就显示谁

3.7.keep-alive☆
1.组件切换后被销毁
keep-alive相当于是一个组件的状态缓存,我们先看个例子,看完案例就知道为什么引入keep-alive了
AppLeft、AppCenter、AppRight内容差不多,就名称不一样:我们写两个生命周期的函数来监听
<template>
<div>
<h3>Appxxx:{{ count }}</h3>
<button @click="count++">count++</button>
</div>
</template>
<script>
export default {
data() {
return {
count: 0
}
},
created() {
console.log('Appxxx created');
},
unmounted() {
console.log('Appxxx unmounted');
}
}
</script>
<style scoped></style>

App.vue:用的还是动态组件<component :is="组件名"></component>
<template>
<div>
<button :class="{ active: currName == name }" v-for="name in names" @click="changeTab(name)" :key="name">
{{ name }}
</button>
<component :is="currName"></component>
</div>
</template>
<script>
import AppLeft from './components/AppLeft.vue';
import AppRight from './components/AppRight.vue';
import AppCenter from './components/AppCenter.vue';
export default {
data() {
return {
names: ['AppLeft', 'AppCenter', 'AppRight'],
currName: 'AppLeft'
};
},
methods: {
changeTab(name) {
this.currName = name; // 当前选项卡名称赋值给currName
}
},
components: { AppLeft, AppCenter, AppRight }
}
</script>
<style scoped>
.active {
color: red;
}
</style>
打开后AppLeft组件已经被创建,我们点3下按钮,AppLeft中的count值为3

切换到AppCenter组件:控制台提示AppLeft已经被卸载了

我们重新切换会AppLeft,发现count=3已经没了,又变成初始的0了。。。

2.keep-alive来缓存
试想一下,我们如果是实际使用中写个复杂表单,写大半了,不小心切换了下标签,内容直接没了,是什么心情?==> keep-live即可解决
上面演示的代码不用修改,就是在原有基础上增加一个<KeepAlive>即可

效果:组件不会被卸载,而且里面的值切换后还在(不会重置,而是被缓存了)

切换过来内容还是在的,且组件都没有被销毁

如果只想要某个选项缓存,其他不缓存,可以使用include - string | RegExp | Array(名字匹配就会被缓存)
- 字符串:分割要用
,eg:include="名称1,名称2" - 正则:
include前面要加个:eg::include="/名称1|名称2/" - 数组:
include前面要加个:eg::include="[名称1,名称2]"
AppVue:动态组件外面包裹一下即可:<KeepAlive include="left,center">动态组件</KeepAlive>

这个名称直接写组件名是没用的,而是在组件里面定义的name:(name与name直接就,间隔,别额外加个空格什么的)

还有几个属性:
- exclude - string | RegExp | Array:名字匹配的不缓存,其他都缓存(也是字符串、正则、数组)
- max - number | string:最多可以缓存多少组件实例,一旦达到这个数字,那么缓存组件中最近没有被访问的实例会被销毁
3.缓存组件生命周期
对于缓存的组件来说,再次进入时,我们是不会执行created或者mounted等生命周期函数的,KeepAlive提供了两个钩子:activated、deactivated
在center里面加上两个钩子(其他还是上面的案例,代码不变)

效果:

3.8.异步组件
异步组件一般两个用途:第一个:异步加载服务器组件,第二个:把组件和项目分包
平时我们编译的时候都是整体打一个包,但有些场景下是需要分别打包的
PS:有时候都放一个包里面会导致首页这类内容多的页面特别卡,有分包需求
先看个正常案例:
<!-- App.vue -->
<template>
<div>
<AppHeader></AppHeader>
<AppItem></AppItem>
</div>
</template>
<script>
import AppItem from './components/AppItem.vue';
import AppHeader from './components/AppHeader.vue';
export default {
components: {
AppItem,
AppHeader
}
}
</script>
子组件没写什么内容,就分别写了一个文本

build之后:整体打包成一个js文件了

如果我们要把header单独打包呢?==> 这时候异步组件用处就来了
我们先导入defineAsyncComponent,然后处理下导入的对象并在父组件中注册一下
import { defineAsyncComponent } from 'vue';
const appHeaderAsync = defineAsyncComponent(() => {
// 异步加载服务器的组件、分包打包项目某个组件
return import('./components/AppHeader.vue');
});
export default {
components: {
AppHeader: appHeaderAsync // 注册下
}
}
贴一下App.vue:
<template>
<div>
<AppHeader></AppHeader>
<AppItem></AppItem>
</div>
</template>
<script>
import AppItem from './components/AppItem.vue';
import { defineAsyncComponent } from 'vue';
const appHeaderAsync = defineAsyncComponent(() => import('./components/AppHeader.vue'));
export default {
components: {
AppItem,
AppHeader: appHeaderAsync
}
}
</script>
重新npm run build之后:appheader被单独打包了

预览效果:npm run preview

的确变成两个js文件了

异步组件平时项目用的不多(用路由懒加载),还有一些扩展内容可以看官方文档

3.9.组件的v-model★
1.语法糖探究
单独拎出来讲肯定是和之前v-model不一样的,我们先不管他,按照之前使用习惯来做个测试:
App.vue里面对AppItem子组件绑定了一个title

然后我们去appItem中使用:

打开浏览器后发现vue提示我们没在AppItem中定义

其实v-model就是一个语法糖,我们写v-model="title",相当于写成了这样:
<AppItem :modelValue="title" onUpdate:modelValue=回调函数 >
这段语句相当于我们自己通过属性传参,然后定义一个自定义方法用来接收子组件$emit('自定义方法',数据)的值
2.v-model实现的本质
v-model可以双向绑定,那么必然可以相互间的通信,那就可以简化步骤:
- 父组件先把数据传递给子组件 ==> 属性传值
- eg:父组件:
:title="title"、子组件:props: ['title']
- eg:父组件:
- 子组件得到数据并展示出来,当子组件修改数据后再把数据重新传给父组件,eg:
- 父组件:
@myEvent="updateTitle"定义一个自定义事件myEvent,并定义一个事件触发的处理函数 - 子组件:
this.$emit('myEvent', 新数据);(加上:emits: ['myEvent'])
- 父组件:
- 父组件接收到新数据后,对data中的变量重新赋值
- eg:
updateTitle(data){ this.title = data; }
- eg:
代码比较简单,截个图:


刚才说v-model是语法糖,相当于vue帮我们写了这个:<AppItem :modelValue="title" Update:modelValue=回调函数 >
PS:相当于上面代码不变的情况下,只要把我们之前定义的
:title换成:modelValue,自定义事件@myEvent换成Update:modelValue就行了
还是上面代码,我们改写下:App.vue:
<template>
<div>
<!-- <AppItem :title="title" @myEvent="updateTitle"></AppItem> -->
<AppItem :modelValue="title" @update:modelValue="updateTitle"></AppItem>
</div>
</template>
<script>
import AppItem from './components/AppItem.vue';
export default {
components: { AppItem },
data() {
return {
title: '我是一个标题'
}
},
methods: {
updateTitle(data) {
console.log('App update:', data);
this.title = data;
}
},
}
</script>
AppItem.vue:
<template>
<div>
<h2> {{ modelValue }}</h2>
<button @click="updateTitle">修改title</button>
</div>
</template>
<script>
export default {
props: ['modelValue'],
emits: ['update:modelValue'],
methods: {
updateTitle() {
console.log('AppItem update');
this.$emit('update:modelValue', '我是被AppItem修改过的值');
}
},
}
</script>
点击后效果:
上面案例就是v-model的本质了,我们现在可以使用v-model进一步简化App.vue中自定义组件AppItem的写法:
<AppItem :modelValue="title" @update:modelValue="title = $event"></AppItem>
App.vue:基本上没变,就是把:modelValue="title"换成了v-model="title"
回调函数这边我就简写了:
title = $event(回调函数上面也可以这么写,我上面只是为了你进一步理解才分开写的)
<template>
<div>
<!-- <AppItem :title="title" @myEvent="updateTitle"></AppItem> -->
<!-- <AppItem :modelValue="title" @update:modelValue="title = $event"></AppItem> -->
<AppItem v-model="title" @update:modelValue="title = $event"></AppItem>
</div>
</template>
<script>
import AppItem from './components/AppItem.vue';
export default {
components: { AppItem },
data() {
return {
title: '我是一个标题'
}
},
}
</script>
AppItem还是之前的内容:

3.父组件中多个子组件
再继续深究,如果我父组件中有多个AppItem,我又该怎么写呢?
AppItem无需修改:props中写下modelValue,emit里面写update:modelValue(只要写一份就行了)

我们来看下App.vue
<template>
<div>
<AppItem v-model="title" @update:modelValue="title = $event"></AppItem>
<AppItem v-model="title2" @update:modelValue="title2 = $event"></AppItem>
<AppItem v-model="title3" @update:modelValue="title3 = $event"></AppItem>
</div>
</template>
<script>
import AppItem from './components/AppItem.vue';
export default {
components: { AppItem },
data() {
return {
title: '我是一个标题',
title2: '我是另一个标题',
title3: '我是第三个标题'
}
},
}
</script>
分别点击都可以生效:

4.自定义v-model名称
如果就要像之前我们自己手写各种自定义名称呢?

vue其实也有提供方法:v-model:自定义名称
PS:现在不奇怪为什么自定义事件是个update:modelValue了吧,默认的v-model,其实就是v-model:modeValue
看案例:AppItem.vue,props: ['title']、$emit('update:myEvent', 内容)
<template>
<div>
<h2>{{ title }}</h2><button @click="changeTitle">修改标题</button>
</div>
</template>
<script>
export default {
props: ['title'],
emits: ['update:myEvent'],
methods: {
changeTitle() {
this.$emit('update:myEvent', '这个AppItem修改的内容');
}
},
}
</script>
App.vue:<AppItem v-model:title="title" @update:myEvent="title = $event">
<template>
<div>
<AppItem v-model:title="title" @update:myEvent="title = $event"></AppItem>
</div>
</template>
<script >
import AppItem from './components/AppItem.vue';
export default {
components: { AppItem },
data() {
return {
title: '这是一个标题'
}
},
}
</script>
点击后就可以修改了:

5.子组件中绑定多个v-model
那么已经有了别名了,是不是可以绑定多个v-model呢?==> 可以的 (平时很少这样干,完全可以传递个对象或者数组过去)
AppItem.vue:如果是整体批量处理可以在子组件定义一个自定义事件即可
<template>
<div>
{{ name }} - {{ age }} - {{ gender }}
<button @click="changeData">修改内容</button>
</div>
</template>
<script>
export default {
props: ['name','age','gender'],
emits: ['update:name','update:age','update:gender'],
methods: {
changeData(){ // 如果是这种整体修改的,可以用一个自定义事件统一处理
this.$emit('update:name','王二');
this.$emit('update:age',33);
this.$emit('update:gender','女');
},
},
}
</script>
App.vue:实际使用中往往是传个对象或者数组过去,很少这么传的
<template>
<div>
<AppItem v-model:name="name" v-model:age="age" v-model:gender="gender"
@update:name="name = $event" @update:age="age = $event"
@update:gender="gender = $event"></AppItem>
</div>
</template>
<script >
import AppItem from './components/AppItem.vue';
export default {
components: { AppItem },
data() {
return {
title: '这是一个标题',
name: '张三',
age: 22,
gender: '男'
}
},
}
</script>

3.10.混入Mixins
目前我们是使用组件化的方式在开发整个Vue的应用程序,但是组件和组件之间有时候会存在相同的代码逻辑,我们希望对相同的代码逻辑进行抽取。
在Vue2和Vue3中都支持的一种方式就是使用Mixin来完成,Mixin提供了一种非常灵活的方式,来分发Vue组件中的可复用功能(这块以前Vue2中大量使用,Vue3基本不用 ==> 使用setup函数)
-
一个Mixin对象可以包含任何组件选项:
-
当组件使用Mixin对象时,所有Mixin对象的选项将被混合进入该组件本身的选项中
说明:Mixins 在 Vue3 支持主要是为了兼容Vue2和生态中其他库。在新的应用中应避免使用 mixin,特别是全局mixin
这边简单说下案例:我们先在混入对象中配置一些函数和字段(平时怎么用,这个里面都可以写)

App.vue:注册一下AppItem

AppItem组件里面该怎么样还是怎么用,如果字段和方法和混入里面的重名了,vue会以组件为准

效果:msg我组件里面并没有定义,方法也没有些,但是通过myMixin配置下就都有了

Vue3基本上不用这个,其他内容我一笔带过:
如果mixin里面的内容和组件重名了,组件本身的优先级更高
生命周期函数比较特殊:会都放在一个数组里面,最后mixin中的生命周期函数和组件的都会调用
也可以放全局,让每个组件都混入,但Vue3不推荐这么做,所以就不介绍了
官方文档:https://v2.cn.vuejs.org/v2/guide/mixins.html
4.Vue3组件开发(常用)★
组合API代码演示:https://gitee.com/lotapp/BaseCode/tree/master/javascript/3.Vue/5vue3composition
之前为了从过渡和兼容Vue2,从html单文件混合开发,到选项API的Vue单文件开发,现在该回归Vue官方推荐的开发方式了 ==> 组合API:Composition API(setup函数)
★注意:setup中不要使用this,setup属性特点:
定义数据 + 函数 然后以对象方式return
setup函数主要有两个参数:props、context
- props:父组件传递过来的属性会放到props中
- context:SetupContext中包含三个属性:
- attrs:所有非prop传递的attribute属性
- slots:父组件传递过来的插槽
- emit:子组件传递数据给父组件需要用到,只不过不再是
this.$emit了,直接context.emit
4.1.组合API引入
先不谈性能方面的优化,光开发方面我们看个案例就知道:来个官方经常说的计数器案例
PS:创建新项目:
cnpm create vue@latest,安装依赖:cnpm install,运行:cnpm run dev
先看之前vue2风格的Options API的代码:
<template>
<div>
{{ count }} <button @click="addCount">count++</button>
</div>
</template>
<script>
export default {
data() {
return {
count: 0
}
},
methods: {
addCount() {
this.count++;
}
},
}
</script>
新的Composition API:
<template>
<div>
<!-- 模板这边vue会帮我们自动解包,就不用写count.value了 -->
{{ count }} <button @click="addCount">count++</button>
</div>
</template>
<script>
// 从vue中导入ref方法
import { ref } from 'vue';
export default {
setup() {
// 把数据包裹一下就变成响应式的数据了
let count = ref(0);
const addCount = () => {
count.value++; // 被包裹后获取里面的值需要通过.value来获取
};
// 把相关定义的实例返回
return {
count,
addCount
}
}
}
</script>
点三下的效果都一样:

选项API开发逻辑是分散开的,而组合API是可以把一块逻辑放在一起的,setup函数官方还提供了语法糖,我们看下对比:
模板方面都一样:

现在只是一个简单的事件,后期如果有computed、watch、生命周期钩子,一个count相关的逻辑可能就分散到N个地方,后期维护和协同开发都会有影响。
而组合API可以把相关逻辑都放在一段代码里面处理,可以像一个函数一样去编程,后期维护都会非常方便,官方提供的语法糖更是简化了这个过程

PS:setup中的返回值,就相当于之前的data选项中数据,都是为了给template使用的
扩:ES6的导入和导出★
本段演示代码:https://gitee.com/lotapp/BaseCode/tree/master/javascript/1.ES6/es6-demo
1.导入导出语法
ES6模块化规范中定义:ES6模块化规范是浏览器端和服务器端通用的模块化开发规范
- 每一个JS文件都是一个独立的模块
- 导入其它模块成员使用 import 关键字
- 向外共享模块成员使用 export 关键字
ES6的模块化主要就是这3种用法:
- 默认导出与默认导入
- 默认导出语法:
export default 默认导出的成员 - 默认导入语法:
import 接收名称 from '模块标识符'
- 默认导出语法:
- 按需导出与按需导入
- 按需导出语法:
export 导出的属性或方法 - 按需导入语法:
import {对应的属性名或方法} from '模块标识符'
- 按需导出语法:
- 直接导入并执行模块中的代码
- 直接导入并执行:
import '模块标识符'
- 直接导入并执行:
2.默认导出、导入
来个案例,我们创建一个空的vue项目,然后演示下(不相关的东西都可以删掉)
默认导出语法:export default 默认导出的成员
// util/export_test.js
let name = '小明';
let age = 22;
function show() {
console.log(name, age)
// console.log(this); // {name: '小明', show: ƒ}
}
// 公开name和show方法
export default {
name,
show
}
默认导入语法:import 接收名称 from '模块标识符'
<script setup>
// App.vue
// 1.默认导入
import test from './util/export_test'; // test可以,test1也可以,随你
console.log(test); // 里面没有age,因为age没有被导出
console.log(test.name); // 输出name值
test.show(); // 调用show方法
</script>
<template></template>
输出:test里面没有age,因为age没有被导出

注意事项
- 每个模块中,只允许使用唯一的一次 export default,否则会报错
- 默认导入时的接收名称可以是任意合法名称(不要以数字开头)
3.按需导出、导入案例
按需导出语法:export 导出的属性或方法
//export_need.js
export let name = '小明';
let age = 22;
export let gender = '男';
export function show() {
console.log(name, age, gender);
}
按需导入语法:import {对应的属性名或方法} from '模块标识符'
<script setup>
// App.vue
// 2.按需导入
import {name,show} from './util/export_need'; // 我只需要name和show,gender我用不到可以不导入
console.log(name);
show();
</script>
<template></template>
输出:
小明
小明 22 男
注意事项
- 每个模块中可以使用多次按需导出
- 按需导入的成员名称必须和按需导出的名称保持一致
- 按需导入时,可以使用 as 关键字进行重命名
- 按需导入可以和默认导入一起使用
还是上面的案例,我们给show方法取个别名info:as
import {name,show as info} from './util/export_need'; // 我只需要name和show,gender我用不到可以不导入
console.log(name);
info();
按需导入可以和默认导入一起使用:
//export_need.js
export let name = '小明';
let age = 22;
export let gender = '男';
export function show() {
console.log(name, age, gender);
}
// 默认导出name和show
export default {
name,
show
}
App.vue:
<script setup>
// 2.按需导入
import need from './util/export_need';
import { gender } from './util/export_need';
console.log(need.name);
console.log(gender);
need.show();
</script>
<template></template>
输出:
小明
男
小明 22 男
4.直接导入案例
直接导入并执行模块中的代码:import '模块标识符'
// util/exec_test.js
let name = '小明';
let age = 22;
console.log(name, age)
App.vue:里面就导入下即可:import './util/exec_test'
输出:小明 22
4.2.响应式数据
还是上面的例子,如果我们不导入ref方法,直接当函数使用,会发生什么事情呢?

效果:显示是没有问题的,修改后值也是变了,但是页面并没有重新渲染(vue不知道这个count需要重新渲染)

1.ref函数★
1.1.让数据变成响应式★
使用ref包裹后有个东西需要注意,js中如果要使用包裹的数据需要从value中获取(template正常使用,只要是ref对象,vue都会自动解包)

setup语法糖:
<template>
<div>
{{ count }} <button @click="addCount">count++</button>
</div>
</template>
<script setup>
import { ref } from 'vue';
let count = ref(0);
const addCount = () => {
console.log(`++之后:${count.value}`);
count.value++;
console.log(`++之后:${count.value}`);
}
</script>
1.2.模板引用-ref属性 ☆
注意:ref属性前面不用加:
1.获取普通元素dom
之前说ref是获取dom的一种方式,现在ref函数也是可以的(一般不使用),来个简单演示:
在setup函数里面先定义inputTextRef的对象,然后在元素或者组件中绑定ref属性,ref的属性值就是刚刚定义的inputTextRef
<template>
<div>
<input type="text" ref="inputTextRef">
</div>
</template>
<script>
import { onMounted, ref } from 'vue';
export default {
setup() {
let inputTextRef = ref(null);
onMounted(() => {
// setup中获取不到dom,mounted就可以了
console.log(inputTextRef.value);
})
return { inputTextRef }
}
}
</script>
演示:
setup语法糖:
<template>
<div>
<input type="text" ref="inputTextRef">
</div>
</template>
<script setup>
import { onMounted, ref } from 'vue';
let inputTextRef = ref(null);
onMounted(() => {
// setup中获取不到dom,mounted就可以了
console.log(inputTextRef.value);
})
</script>
2.获取子组件dom
之前是通过,$refs.ref名.$el获取组件DOM、$refs.ref名.xxx获取组件数据、$refs.ref名.xxx()调用组件方法
现在方法和这个不太一样了,我们一起看下:App.vue:
<script setup>
import { ref, onMounted } from 'vue';
import AppItem from './components/AppItem.vue';
const itemRef = ref();
onMounted(() => {
console.log(itemRef.value);
console.log(itemRef.value.title);
itemRef.value.updateTitle();
})
</script>
<template>
<div>
<!-- 这个地方是ref,而不是`:ref` -->
<AppItem ref="itemRef"></AppItem>
</div>
</template>
AppItem.vue:
<script setup>
import { ref } from 'vue';
let title = ref('this is title')
const updateTitle = () => {
title.value = '这是一个新标题'
}
// // 暴露出去哪些内容
// defineExpose({
// title,
// updateTitle
// })
</script>
<template>
<div>
{{ title }} <button @click="updateTitle">修改标题</button>
</div>
</template>
默认是没有任何东西,而且也不能访问子组件的属性和方法的(vue默认不让访问)

vue也提供了一种手动暴露的方法:defineExpose()
默认情况下在
<script setup>语法糖下组件内部的属性和方法是不开放给父组件访问的, 可以通过defineExpose编译宏指定哪些属性和
方法允许访问
我们把上面注释掉的部分正常执行:其他部分不用修改

效果:能访问到title和调用updateTitle方法了(不用点按钮就能程序调用方法)

2.reactive函数★
data选项、ref函数其实内部都是使用的reactive函数,reactive不支持简单数据类型,只针对复杂类型
PS:平时基本上都是使用ref,ref即支持简单类型又支持复杂类型
来个测试案例:如果包裹简单数据类型,控制台会提示不能生成响应式数据

还是上面的案例,如果硬是要用reactive变成响应式数据也可以,就是使用起来比较麻烦:
<template>
<div>
{{ count.count }} <button @click="addCount">count++</button>
</div>
</template>
<script>
import { reactive } from 'vue';
export default {
setup() {
let count = reactive({
count: 0
});
const addCount = () => {
console.log(`++之后:${count.count}`);
count.count++;
console.log(`++之后:${count.count}`);
}
return { count, addCount }
}
}
</script>
效果一样,就是调用起来太麻烦,template部分vue不好自动解包,需要手动调用

3.复杂类型案例
3.1.reactive函数
reactive可以用于复杂类型,但不能用于简单数据类型,来个例子:
<template>
<div>
{{ student.name }}-{{ student.age }} <button @click="updateName">updateName</button>
</div>
</template>
<script>
import { reactive } from 'vue';
export default {
setup() {
let student = reactive({
name: '小明',
age: 23
});
const updateName = () => {
console.log('修改之前:', student.name);
student.name = '小华';
console.log('修改之后:', student.name);
}
return { student, updateName }
}
}
</script>
效果:

setup语法糖:
<template>
<div>
{{ student.name }}-{{ student.age }} <button @click="updateName">updateName</button>
</div>
</template>
<script setup>
import { reactive } from 'vue';
let student = reactive({
name: '小明',
age: 23
})
const updateName = () => {
console.log('修改之前:', student.name);
student.name = '小华';
console.log('修改之后:', student.name);
}
</script>
3.2.ref函数
ref也支持复杂类型,上面的案例用ref就是这样的:template没有变化,就是下面js部分要加value(开发中基本上还是以ref为主)
<template>
<div>
{{ student.name }}-{{ student.age }} <button @click="updateName">updateName</button>
</div>
</template>
<script>
import { ref } from 'vue';
export default {
setup() {
let student = ref({
name: '小明',
age: 23
});
const updateName = () => {
console.log('修改之前:', student.value.name);
student.value.name = '小华'; // student.value
console.log('修改之后:', student.value.name);
}
return { student, updateName }
}
}
</script>
3.3.应用场景
效果同上,是不是有点疑惑,复杂数据不是reactive更方便吗?怎么说ref用的最多呢?==> 数据如果是从服务器获取的,ref更方便,如果是本地表单提交(本地数据+数据有些关联),可以使用reactive
本地+数据有关联 ==> reactive,其他时候 ==> ref
来个模拟加载服务器数据的案例:
<template>
<div>
<ul>
<li v-for="student in students" :key="student.id">
{{ student.id }}.{{ student.name }}-{{ student.age }}-{{ student.gender }}
</li>
</ul>
</div>
</template>
<script>
import { ref, onMounted } from 'vue';
export default {
setup() {
let students = ref([]);
// 模拟后端数据获取
onMounted(() => {
students.value = [
{ id: 1, name: '张三', age: 22, gender: '男' },
{ id: 2, name: '李四', age: 33, gender: '男' },
{ id: 3, name: '王二', age: 34, gender: '女' }
];
});
return { students };
}
}
</script>
ref对象有个value属性,我们可以直接把获取的数据赋值给它,而reactive则需要自己处理

setup语法糖:template部分不变
<template>
<div>
<ul>
<li v-for="student in students" :key="student.id">
{{ student.id }}.{{ student.name }}-{{ student.age }}-{{ student.gender }}
</li>
</ul>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue';
let students = ref([]);
// 模拟后端数据获取
onMounted(() => {
students.value = [
{ id: 1, name: '张三', age: 22, gender: '男' },
{ id: 2, name: '李四', age: 33, gender: '男' },
{ id: 3, name: '王二', age: 34, gender: '女' }
];
});
</script>
4.readonly函数
来个场景:父组件里面好几个子组件,父组件给他们传递了个数据,这时候有个子组件把数据直接改了,这时候有两个问题:
这个其实是不合理的,修改数据应该emit提交给父组件,让父组件来修改(这样就知道是谁要改,然后还可以对其他组件做单独处理)
- 所有子组件都受影响了
- 父组件不知道是谁修改的
这个平时一般是通过开发规范直接规避掉了,所以也用的不多,这边介绍下:
语法:readonly(普通对象 | ref对象 | reactive对象)
App.vue:里面两个子组件,分别把student传递给了他们
<template>
<div>
<AppItem :student="student"></AppItem>
<AppHeader :student="student"></AppHeader>
</div>
</template>
<script>
import { ref } from 'vue';
import AppHeader from './components/AppHeader.vue';
import AppItem from './components/AppItem.vue';
export default {
components: { AppItem, AppHeader },
setup() {
const student = ref({ id: 1, name: '张三', age: 22, gender: '男' });
return {
student,
}
}
}
</script>
AppHeader.vue:简单一个展示
<template>
<div>
<h2>AppHeader</h2>
{{ student.id }}.{{ student.name }}-{{ student.age }}-{{ student.gender }}
</div>
</template>
<script>
export default {
props: ['student'] // 接收父组件传递过来的student属性值
}
</script>
AppItem.vue:定义了一个修改的单击事件
<template>
<div>
<h2>AppItem</h2>
{{ student.id }}.{{ student.name }}-{{ student.age }}-{{ student.gender }}
<button @click="student.name = '李四'">修改Name测试</button>
</div>
</template>
<script>
export default {
props: ['student']
}
</script>
点一下修改就全部修改掉了

下面我们用readonly试下,代码没什么变换就是把ref换成了readonly(import { readonly } from 'vue';)

再点击修改就改不掉了:

5.其他API函数
其实还有很多Vue提供的函数,这边就简单列举下:
5.1.工具函数
1.响应式 API:工具函数
- isRef():检查某个值是否为ref★
- unref():如果参数是 ref,则返回内部值,否则返回参数本身★
- isRef的语法糖:
val = isRef(val) ? val.value : val
- isRef的语法糖:
- toRef():将值、reactive属性转换为ref
- toValue():将值、refs 或 getters 规范化为值(3.3+)
- eg:
toValue(1)、toValue(ref(1))、toValue(() => 1)的结果都是1
- eg:
- toRefs():把响应式对象的每一个属性都变成ref对象(内部是每个属性都toRef一下)
- PS:详细可以查看案例1
- isProxy():检查一个对象是否是由
reactive()、readonly()或shallowReactive()、shallowReadonly()创建的代理 - isReactive():检查一个对象是否是由
reactive()或shallowReactive()创建的代理 - isReadonly():检查传入的值是否由
readonly()或shallowReadonly()创建的只读代理- 只读对象的属性可以改,但不能通过传入的对象直接赋值。而是让父方法去修改(xxx.value=xx)
详细可以参考官方文档:https://cn.vuejs.org/api/reactivity-utilities.html
案例1:toRefs()使用场景
let student = reactive({
name: '小明',
age: 22
});
// 如果对象被ES6解构语法给解构了,它们就不是响应式的数据了
let { name, age } = student;
// 2s后修改数据,看看页面数据有没有修改
setTimeout(() => {
name = '小黑';
age = 22;
console.log(name, age)
}, 2000);
2s后页面数据并没有修改:
而如果使用toRefs包裹一下就可以了:
<template>
<div>
{{ name }}-{{ age }}
</div>
</template>
<script>
import { reactive, toRefs } from 'vue';
export default {
setup() {
let student = reactive({
name: '小明',
age: 22
});
// 如果对象被ES6解构语法给解构了,它们就不是响应式的数据了
let { name, age } = toRefs(student);
// 2s后修改数据,看看页面数据有没有修改
setTimeout(() => {
name.value = '小黑';
age.value = 22;
console.log(name, age)
}, 2000);
return {
name, age,
}
}
}
</script>
2s后页面数据刷新了,name和age都是ref类型了

setup语法糖:
<template>
<div>
{{ name }}-{{ age }}
</div>
</template>
<script setup>
import { reactive, toRefs } from 'vue';
let student = reactive({
name: '小明',
age: 22
});
let { name, age } = toRefs(student);
setTimeout(() => {
name.value = '小黑';
age.value = 22;
console.log(name, age)
}, 2000);
</script>
5.2.进阶函数
2.响应式 API:进阶
- shallowRef():创建一个浅层的
ref()对象☆- ref默认是把里面所有属性都变成deep响应式的,shallowRef则是浅层的
- shallowReactive():创建一个浅层的
reactive()对象☆ - shallowReadonly():创建一个浅层的
readonly()对象☆ - toRaw():
reactive或readonly代理的原始对象☆ - triggerRef():手动触发
shallowRef、shallowReactive、shallowReadonly相关联的副作用- 强制触发依赖于一个浅层 ref 的副作用,这通常在对浅引用的内部值进行深度变更后使用
- customRef():创建一个自定义的 ref,显式声明对其依赖追踪和更新触发的控制方式
- markRaw():将一个对象标记为不可被转为代理。返回该对象本身
- effectScope():创建一个 effect 作用域,可以捕获其中所创建的响应式副作用 (即计算属性和侦听器),这样捕获到的副作用可以一起处理
- getCurrentScope():如果有的话,返回当前活跃的 effect 作用域
- onScopeDispose():在当前活跃的 effect 作用域上注册一个处理回调函数。当相关的 effect 作用域停止时会调用这个回调函数
详细可以参考官方文档:https://cn.vuejs.org/api/reactivity-advanced.html
4.3.computed计算属性
1.简单案例
计算属性用法差不多,先看个简单例子:
template:
<template>
<div>
{{ student.name }}-{{ student.age }}-{{ genderStr }}
</div>
</template>
script:
<script>
import { computed, reactive } from 'vue';
export default {
setup() {
let student = reactive({
name: '张三',
age: 22,
gender: 1
});
const genderStr = computed(() => {
return student.gender == 1 ? '男' : '女';
});
console.log(genderStr);
return {
student, genderStr
}
}
}
</script>
setup语法糖:
<script setup>
import { computed, reactive } from 'vue';
let student = reactive({
name: '张三',
age: 22,
gender: 1
});
const genderStr = computed(() => {
return student.gender == 1 ? '男' : '女';
});
console.log(genderStr);
</script>
输出:张三-22-男
2.computed的get和set
如果我们打印一下computed对象(console.log(genderStr))会发现,他其实是一个ref的对象

那我们来尝试看看能不能直接修改:

就算把genderStr属性变成let也是不可以修改的

Computed有两个方法:get、set,get默认就是readonly类型,而set可以去修改(但不推荐去修改,我这边只是写个案例)
template:
<template>
<div>
{{ student.name }}-{{ student.age }}-{{ genderStr }}<br />
<button @click="changeGender">修改性别</button>
</div>
</template>
script:
<script>
import { computed, reactive } from 'vue';
export default {
setup() {
let student = reactive({
name: '张三',
age: 22,
gender: 1
});
const genderStr = computed({
set: function (newValue) {
console.log(newValue)
if (newValue == '男') {
student.gender = 1;
} else if (newValue == '女') {
student.gender = 0;
}
},
get: function () {
return student.gender == 1 ? '男' : '女';
}
});
const changeGender = () => {
genderStr.value = '女';
}
return {
student, genderStr, changeGender
}
}
}
</script>
setup语法糖:
<script setup>
import { computed, reactive } from 'vue';
let student = reactive({
name: '张三',
age: 22,
gender: 1
});
const genderStr = computed({
set: function (newValue) {
console.log(newValue)
if (newValue == '男') {
student.gender = 1;
} else if (newValue == '女') {
student.gender = 0;
}
},
get: function () {
return student.gender == 1 ? '男' : '女';
}
});
const changeGender = () => {
genderStr.value = '女';
}
</script>
4.4.watch侦听器★
从vue中导入watch函数,然后用法和之前类似:watch(数据源,回调函数,{参数})
1.数据源:它可以是一个 ref (包括计算属性)、一个响应式对象、一个 getter 函数、或多个数据源组成的数组
2.参数:immediate:true:立即执行、deep:true:深度侦听
1.普通属性侦听
侦听title属性案例:
<template>
<h2>{{ title }}</h2>
</template>
<script>
import { ref, watch } from 'vue';
export default {
setup() {
let title = ref('this is title');
watch(title, (newValue, oldValue) => {
console.log(`oldValue:${oldValue}`);
console.log(`newValue:${newValue}`);
});
// 模拟修改操作
setTimeout(() => {
title.value = '这是一个标题';
}, 2000);
return {
title
}
}
}
</script>
setup语法糖:
<template>
<h2>{{ title }}</h2>
</template>
<script setup>
import { ref, watch } from 'vue';
let title = ref('this is title');
watch(title, (newValue, oldValue) => {
console.log(`oldValue:${oldValue}`);
console.log(`newValue:${newValue}`);
});
// 模拟修改操作
setTimeout(() => {
title.value = '这是一个标题';
}, 2000);
</script>
如果属性不是响应式的会提示: [Vue warn]: Invalid watch source: this is title A watch source can only be a getter/effect function, a ref, a reactive object, or an array of these types

2.对象属性侦听
对象是引用传递的,所以这块old和new是同一个值,来看下:
<template>
<div>
{{ student.name }}-{{ student.age }}-{{ student.gender }}
</div>
</template>
<script>
import { reactive, watch } from 'vue';
export default {
setup() {
let student = reactive({
name: '张三',
age: 22,
gender: '男'
})
watch(student, (newValue, oldValue) => {
// 对象是引用传递的,所以这块old和new是同一个值
console.log(oldValue);
console.log(newValue);
}, {
immediate: true
});
setTimeout(() => {
student.name = '李四';
}, 2000);
return {
student
}
}
}
</script>
效果:newValue和oldValue的确是一样的

我们来进一步验证下,让它刚开始就执行一下:immediate: true

刚开始名字的确是张三,因为是引用传递,在watch里面的newValue和oldValue就都一样了

3.深层次的侦听
通过watch监听的ref对象默认是浅层侦听的,直接修改嵌套的对象属性不会触发回调执行 ==> 可以通过deep来深层侦听
来个案例,我们把上面案例从reactive换成ref就发现没被侦听到:
<template>
<div>
{{ student.name }}-{{ student.age }}-{{ student.gender }}
</div>
</template>
<script>
import { ref, watch } from 'vue';
export default {
setup() {
let student = ref({
name: '张三',
age: 23,
gender:'男'
});
watch(student, (newValue, oldValue) => {
console.log(newValue);
console.log(oldValue);
});
setTimeout(() => {
student.value.name = '李四';
}, 2000);
return{
student
}
}
}
</script>
2s后内容被修改了,但是没有侦听到

我们添加一个deep参数就可以了:(平时尽量不要用deep,有性能损耗的)

因为对象是引用传递,所以新旧还是和之前一样,都是一个指向

那ref就不能像reactive一样快乐的玩耍了吗?NoNoNo,我们别监听student,监听student.value就可以了
PS:注意这里的student.value不是一个普通的值,也是一个代理对象

setup语法糖:
<template>
<div>
{{ student.name }}-{{ student.age }}-{{ student.gender }}
</div>
</template>
<script setup>
import { ref, watch } from 'vue';
let student = ref({
name: '张三',
age: 23,
gender: '男'
});
watch(student.value, (newValue, oldValue) => {
console.log(newValue);
console.log(oldValue);
});
setTimeout(() => {
student.value.name = '李四';
}, 2000);
</script>
这样效果就和上面的reactive一样了

4.监听某个属性☆
不能直接侦听响应式对象的属性值,例如:
<template>
<div>
{{ student.name }}-{{ student.age }}-{{ student.gender }}
<span v-for="item in student.likes" :key="item">
~{{ item }}~
</span>
</div>
</template>
<script setup>
import { ref, watch } from 'vue';
let student = ref({
name: '张三',
age: 23,
gender: '男',
likes: []
});
watch(student.value.likes, (newValue, oldValue) => {
console.log(newValue);
console.log(oldValue);
});
setTimeout(() => {
student.value.likes = ['唱歌', '跳舞', '睡觉'];
}, 2000);
</script>
没有侦听到:
不想用deep,又想侦听怎么办?==> 使用getter函数,就把watch这块修改下,其他不用动:

效果:

5.多个属性侦听
直接贴下案例:
<template>
<div>
{{ name }}-{{ age }}
</div>
</template>
<script setup>
import { ref, watch } from 'vue';
let name = ref('张三');
let age = ref(22);
watch([name, age],
([nameNewValue, ageNewValue], [nameOldValue, ageOldValue]) => {
console.log('name and age newValue');
console.log(nameNewValue, ageNewValue);
console.log('name and age oldValue');
console.log(nameOldValue, ageOldValue);
})
setTimeout(() => {
name.value = '李四';
}, 1000);
setTimeout(() => {
age.value = 33;
}, 2000);
</script>
效果:
watch文档:https://cn.vuejs.org/guide/essentials/watchers.html or https://cn.vuejs.org/api/reactivity-core.html#watch
6.watcheffect☆
watcheffect:立即运行一个函数(immediate),同时响应式地追踪其依赖,并在依赖更改时重新执行
PS:它会自己跟踪它用到的响应式数据,并在其改变后再执行方法(可以理解为一个灵活的watch)
来个案例:
<script setup>
import { ref, watchEffect } from 'vue';
let title = ref('this is title')
// 它会自己跟踪它用到的响应式数据,并在其改变后再执行方法
watchEffect(() => {
console.log(title.value);
})
setTimeout(() => {
title.value = '我被修改了';
}, 2000);
</script>
<template>
<h2>{{ title }}</h2>
</template>
效果:刚开始就执行了,title被修改后也执行了

watcheffect:https://cn.vuejs.org/api/reactivity-core.html#watcheffect
4.5.生命周期钩子★
先贴下组合式API和选项API的对应关系:
PS:组合式API没有beforeCreate和created,与之对应的是setup。其他生命周期一样,就是名字前面加on(小驼峰命名)

其实setup会在beforeCreate之前,我们可以看下官网和自己编个demo:
<template></template>
<script>
export default {
setup() {
console.log('setup');
},
beforeCreate() {
console.log('beforeCreate');
}
}
</script>
输出:
官方周期顺序:

生命周期同样来个例子:
PS:生命周期函数是可以执行多次的,eg:你弄2个onMounted,就会依次执行2次onMounted
<template>
<div v-if="isShow">
{{ title }}<button @click="updateTitle">修改+消失</button>
</div>
</template>
<script>
import {
onBeforeMount, onMounted, onBeforeUpdate,
onUpdated, onBeforeUnmount, onUnmounted
} from 'vue';
import { ref } from 'vue';
export default {
setup() {
onBeforeMount(() => {
console.log('onBeforeMount');
})
onMounted(() => {
console.log('onMounted');
})
onBeforeUpdate(() => {
console.log('onBeforeUpdate');
})
onUpdated(() => {
console.log('onUpdated');
})
onBeforeUnmount(() => {
console.log('onBeforeUnmount');
})
onUnmounted(() => {
console.log('onUnmounted');
})
let isShow = ref(true);
let title = ref('这是一个标题');
const updateTitle = () => {
title.value = '这是一个新标题!';
// 2s后注销组件
setTimeout(() => {
isShow.value = false;
}, 2000)
}
return { title, updateTitle, isShow }
}
}
</script>
效果图:

setup语法糖:
<template>
<div v-if="isShow">
{{ title }}<button @click="updateTitle">修改+消失</button>
</div>
</template>
<script setup>
import {
onBeforeMount, onMounted, onBeforeUpdate,
onUpdated, onBeforeUnmount, onUnmounted
} from 'vue';
import { ref } from 'vue';
onBeforeMount(() => {
console.log('onBeforeMount');
})
onMounted(() => {
console.log('onMounted');
})
onBeforeUpdate(() => {
console.log('onBeforeUpdate');
})
onUpdated(() => {
console.log('onUpdated');
})
onBeforeUnmount(() => {
console.log('onBeforeUnmount');
})
onUnmounted(() => {
console.log('onUnmounted');
})
let isShow = ref(true);
let title = ref('这是一个标题');
const updateTitle = () => {
title.value = '这是一个新标题!';
// 2s后注销组件
setTimeout(() => {
isShow.value = false;
}, 2000)
}
</script>
有新增的很多钩子,具体可看文档:https://cn.vuejs.org/api/composition-api-lifecycle.html
4.6.组件通信
1.父组件发信息给子组件★
之前在父组件里面使用子组件的案例说不少,现在这种新组合API的编程也来个demo:(用起来基本上差不多)
回顾下之前怎么传递的:父组件中通过属性把数据绑给子组件,子组件props里面写下绑定的属性名
父:
import 子组件+<AppItem :title="title">,子:defineProps(['title'])
AppItem.vue:defineProps(['title'])
<template>
<div>
<h2>{{ title }}</h2>
{{ count }} <button @click="addCount">count++</button>
</div>
</template>
<script setup>
import { ref } from 'vue';
// defineProps(['title'])
const props = defineProps({
title: {
type: String,
default: '我是默认值'
},
})
let count = ref(0);
const addCount = () => {
count.value++;
}
</script>
App.vue:导入子组件并绑定一个:title属性即可
<template>
<div>
<AppItem></AppItem>
<AppItem :title="title"></AppItem>
</div>
</template>
<script setup>
import { ref } from 'vue';
// 子组件导入即可,不用再通过components注册子组件了
import AppItem from './components/AppItem.vue';
let title = ref('this is a title test');
</script>
单击三次的效果:

2.子组件发信息给父组件★
回顾下之前怎么传递的:父组件中给子组件标签通过@绑定事件、子组件内部通过 $emit 方法触发事件
-
子组件
emits:[xxx]里面写下自定义事件名 ==> 现在:const emit = defineEmits(['add']); -
然后通过
$emit('自定义事件',值)触发了自定义事件 ==> 现在:emit ('自定义事件',值) -
回调函数通过
event拿到数据并修改 或者 函数传过来的payload获取 ==> 现在:函数的参数payload获取
AppItem.vue:通过defineEmits得到emit方法
<template>
<div>
<input v-model="name" type="text" @keyup.enter="addStudent" />
</div>
</template>
<script setup>
import { ref } from 'vue';
// 通过defineEmits得到emit方法
const emit = defineEmits(['add']);
let name = ref(''); // v-model绑定的属性
const addStudent = () => {
emit('add', name.value);
console.log(name.value)
name.value = ''; //文本框清空
}
</script>
App.vue:<AppItem @add="addStudent">
<template>
<div>
<AppItem @add="addStudent"></AppItem>
<ul>
<li v-for="name in students" :key="name">
{{ name }}
</li>
</ul>
</div>
</template>
<script setup>
import AppItem from './components/AppItem.vue';
import { ref } from 'vue';
let students = ref(['张三', '李四', '王二']);
const addStudent = (payload) => {
students.value.push(payload);
}
</script>
输入小李并回车:

3.祖孙通信 - 依赖注入
主要就是这两个方法:provide(依赖):提供数据、inject(注入):获取数据
provide('key',普通数据 | ref(数据) | 函数)// 顶层组件提供普通数据/响应式数据
const msg = inject('key')// 获取普通数据/ref响应式数据
可以传递普通数据和响应式的数据,这边以响应式数据和传递方法来演示:
嵌套的树结构是:App.vue >> AppList.vue >> AppItem.vue,App.vue提供数据(provide),AppItem接收数据(inject)
App.vue:
<script setup>
import AppList from './componets/AppList.vue';
import { provide, ref } from 'vue';
let count = ref(0);
const updateCount = () => {
count.value++;
}
provide('count', count);
provide('updateCount', updateCount)
</script>
<template>
<AppList></AppList>
</template>
AppList.vue:
<script setup>
import AppItem from './AppItem.vue';
</script>
<template>
<AppItem></AppItem>
</template>
AppItem.vue:
<script setup>
import { inject } from 'vue';
// 注入
const count = inject('count');
const updateCount = inject('updateCount');
</script>
<template>
<div>
<!-- template里面的数据会自己解包 -->
{{ count }} <button @click="updateCount">点我调用祖辈方法</button>
</div>
</template>
点3下效果:
PS:不相关组件之间的通信就不演示了,实际项目中不会用的,都是用Pinia状态管理库
4.10.组件v-model★
v-model的本质就是父组件通过:属性=数据传值给子组件,子组件通过props接收值,然后需要修改的时候使用emit(自定义事件,值)这时候会触发父组件里面的回调函数,对值进行修改
1.简单案例
App.vue:
<script setup>
import AppItem from './components/AppItem.vue';
import { ref } from "vue";
let student = ref({
name: '小李',
age: 33,
gender: '女'
})
</script>
<template>
<div>
<AppItem v-model:student="student"></AppItem>
</div>
</template>
AppItem.vue:defineProps(['name'])接收name属性,通过defineEmits(['update:name'])获取emit
<script setup>
defineProps(['name'])
const emit = defineEmits(['update:name'])
// 模拟修改
setTimeout(() => {
// 调用自定义事件,并把小黑传递过去
emit('update:name', '小黑');
}, 2000);
</script>
<template>
<div>
{{ name }}
</div>
</template>
运行后效果:2s前:小张,2s后:小黑
其实<AppItem v-model:student="student">的语法糖就相当于,帮我们自动改成了这样:
<AppItem :student="student" @update:student="$event"></AppItem>
我们来看下:AppItem.vue 不修改,就把App.vue微调下,效果和上面一样

2.对象案例
对象是引用传递的,如果在子组件中想对某个属性进行修改,不能只传递该属性,而是整个对象都要传递过来。
我们来看个问题案例:
App.vue:

AppItem.vue:

如果直接传递小黑字符串,相当于把student给替换掉了,而且类型也不对。结果也是有问题的:刚开始小李-33-女,2s后--
修改其实就只需要把AppItem中js修改下就可以了:
<script setup>
const props = defineProps(['student'])
const emit = defineEmits(['update:student'])
// 模拟名称被修改,看看是否是双向绑定
setTimeout(() => {
// 如果student是个对象,这边也要传递对象
emit('update:student', {
name: '小黑',
age: props.student.age,
gender: props.student.gender
});
}, 2000);
</script>
<template>
<div>
{{ student.name }}-{{ student.age }}-{{ student.gender }}
</div>
</template>
刚开始:小李-33-女,2s后:小黑-33-女
5.Vue3 Router
官方文档:https://router.vuejs.org/zh/guide
5.1.前后端路由概述
在项目里面安装路由可以添加vue-router的包:cnpm install vue-router
PS:也可以刚创建项目的时候选下路由选项(router那项按下右箭头,然后回车)
创建项目的时候可以添加

也可以后期安装:

我们先初始化一个含路由的项目,运行一下自己观察:来回切换URL和内容在变,但没有Ajax
这个意味着什么?==> 大大增加了后端服务器的并发能力(缓解服务器压力)

路由的本质都是映射关系表,我们以前的后端路由其实都是根据你浏览器输入的URL,然后服务器接收到后,根据正则匹配,看要访问哪个控制器(Controller)然后由这个控制器获取到数据(Model),发送到对应的视图(View)进行数据绑定和页面渲染,然后服务器返回渲染之后的DOM页面
PS:我上面说的就是我们后端开发的传统MVC(路由映射你可以理解为家里路由器中的路由表,url相当于ip,控制器/组件相当于电脑的mac地址)
那为什么要前后端分离呢?==> 后端程序员既要操作数据,又要写前端页面,然后前端语言中又混了后端的语法,各种糅合,没有那么的责任清晰和存粹。现在基本上都是后端专注数据和业务逻辑,然后通过WebAPI把数据给到前端开发,前端开发工程师专注交互和可视化。彼此之间高效协同工作,而且还可以多端共用一套API(PC、APP、Mini)
5.2.前端路由实现
前端路由的核心目的就是 ==> 改变URL,但页面不进行整体的刷新。那么实现这目的,就是要做到URL和内容有个映射,目前有两种主流方案:
PS:路由用于设定访问路径,将路径和组件映射起来。vue-router的单页面应用中,页面的路径改变就是组件的切换
- URL哈希:就是锚点
#和其后面的内容(没有#则为空字符串)- PS:本质是通过改变
window.location.hash来切换组件(这样URL也会随之改变)
- PS:本质是通过改变
- H5的History:它有六种模式改变URL都不会刷新页面(
location.pathname)- pushState:使用新的路径★
- replaceState:替换原来的路径
- popState:路径的回退
- go:向前或向后改变路径
- forward:向前改变路径
- back:向后改变路径
History比较清晰,URL哈希比较含糊,我们看个案例:
console.log(window.location.href);
console.log(window.location.hash);
window.location.hash='#hhhh'
console.log(window.location.href);
console.log(window.location.hash);
手动在浏览器上输入:http://localhost:5173/#mmd然后url就变成了http://localhost:5173/#hhhh

5.3.Vue Router配置
先来分析下刚刚创建的含路由的项目
发现里面有新建的router文件夹,然后里面有个index.js来配置路由

然后发现路由是在main.js中导入和use:

然后App.vue里面有配置路由占位和路由跳转

分析完毕,我们先总结下知识点:
- router文件夹index.js:创建路由
const router = createRouter({})并返回export default router- history:实现方式:History (
createWebHashHistory)or Hash(createWebHashHistory)★ - routes:path路径、redirect跳转、component组件(懒加载:
() => import('../views/View.vue'))★ - name:给每一个route设置一个独一无二的name,RouterLink 也可以直接 to name
- meta:自定义数据
- history:实现方式:History (
- src文件夹main.js:导入刚刚的路由配置js
import router from './router',并使用app.use(router) - vue文件中占位(
<RouterView>)和跳转(<RouterLink to="/xxx">)
在我们的项目里面配置下路由:
roter/index.js:另一个实现就把createWebHashHistory()替换为:createWebHistory()
ps:如果替换为History的方式,url就变成:
http://localhost:5173/Home、http://localhost:5173/About
import { createRouter } from 'vue-router';
import { createWebHashHistory } from 'vue-router';
import HomeView from '../views/HomeView.vue';
const router = createRouter({
history: createWebHashHistory(),
routes: [
{
path: '/',
redirect: '/Home'
},
{
path: '/Home',
component: HomeView
},
{
path: '/About',
component: () => import('../views/AboutView.vue') // 懒加载,用到才会去加载(避免所有组件全部怼页面上了)
}
]
})
export default router
main.js:app.use(router)
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
createApp(App).use(router).mount('#app')
App.vue:<RouterLink to="/xxx">、<RouterView />
<script setup>
import { RouterLink, RouterView } from 'vue-router';
</script>
<template>
<div>
App:<br />
<RouterLink to="/Home">Home</RouterLink> |
<RouterLink to="/About">About</RouterLink>
</div>
<hr />
<RouterView></RouterView>
</template>
效果:点About,URL和下面组件都会切换

5.4.Router-link属性
router-link有一些属性可以配置:
- to属性:是一个字符串
to="/xxx",或者是一个对象:to={ path:'/xxx'}- 也可以在每个route里面指定一个独一无二的name,然后
to=name名就可以跳转到对应配置的组件
- 也可以在每个route里面指定一个独一无二的name,然后
- replace属性:当点击时,会调用router.replace(),而不是默认的router.push()
- 不推荐使用,默认我们可以点浏览器前进后退切换到对应页面的,如果用了replace就直接over了
- active-class属性:设置激活a元素后应用的class,默认名:
router-link-active - exact-active-class属性:链接精准激活时,应用于渲染的
<a>的class,默认是router-link-exact-active
简单说下,代码和之前一样,就把App.vue微调下:

点到哪个,自动给你改成自定义类名:(exact-active-class是嵌套路由用的,如果没有嵌套,就会在这个上面两个类都加上)

5.5.动态路由传值
我们打印一下route对象查看下:
import { useRoute } from 'vue-router';
const route = useRoute();
console.log(route)

获取参数基本上就这三个比较多:params、query、hash
PS:hash的就是获取从url
#开始到最后的内容($route.hash、useRoute().hash)
1.params传参
先说知识点,然后来个项目里面的常用案例:
- 配置路径:
path: '/User/:id' - 链接传参:
<RouterLink :to="/User/${student.id}">编辑</RouterLink>- 或者
<RouterLink :to="{ path:/User/${student.id}}">编辑</RouterLink>
- 或者
- 获取参数:template获取参数:
{{ $route.params.id }},JS获取参数:useRoute().params.id- OptionsAPI直接通过
this.$route.params获取id - 用户A直接切换到用户B,页面不刷新
useRoute()获取不到可以通过路由的onBeforeRouteUpdate来获取(99%用不到)
- OptionsAPI直接通过
下面贴代码+演示一下:router/index.js
import { createRouter, createWebHistory } from 'vue-router';
import ListView from '../views/ListView.vue';
const router = createRouter({
history: createWebHistory(), // 是个方法
routes: [
{
path: '/',
component: ListView
},
{
path: '/List',
component: ListView
},
{
path: '/User/:id',
component: () => import('../views/UserView.vue')
}
]
})
export default router
App.vue:
<script setup>
import { RouterLink, RouterView } from 'vue-router';
</script>
<template>
<div>
<h2>App Main</h2>
<RouterLink to="/List" active-class="active">List</RouterLink>
<hr>
<RouterView></RouterView>
</div>
</template>
<style scoped>
.active {
color: red;
}
</style>
ListView.vue:/User/${student.id}
<script setup>
import { ref, onMounted } from 'vue';
import { RouterLink } from 'vue-router';
let students = ref([]);
onMounted(() => {
// 模拟一下数据获取
students.value = [
{ id: 1, name: '小张', age: 22 },
{ id: 2, name: '小李', age: 33 },
{ id: 3, name: '小王', age: 44 }
]
})
</script>
<template>
<h3>List View</h3>
<ul>
<li v-for="student in students" :key="student.id">
{{ student.id }}.{{ student.name }}-{{ student.age }}
<!-- <RouterLink :to="{ path: `/User/${student.id}` }">编辑</RouterLink> -->
<RouterLink :to="`/User/${student.id}`">编辑</RouterLink>
</li>
</ul>
</template>
User.vue:JS部分:useRoute().params.id、Template部分:{{ $route.params.id }}
<script setup>
import { ref, onMounted } from 'vue';
import { useRoute, onBeforeRouteUpdate } from 'vue-router'
const route = useRoute();
const userId = route.params.id; // 获取传递过来的id
console.log('User Id:', userId)
let student = ref({});
// 模拟获取数据
onMounted(() => {
const data = [{ id: 1, name: '小张', age: 22 }, { id: 2, name: '小李', age: 33 }, { id: 3, name: '小王', age: 44 }];
student.value = data.find(item => item.id == userId);
})
</script>
<template>
<div>
<h2>User Id:{{ $route.params.id }}</h2>
{{ student.id }}.{{ student.name }}-{{ student.age }}
</div>
</template>
打开后鼠标停留在ID为2的超链接上,这个链接就是:/User/2

点进去就可以传递id,并显示相关内容:

官方文档:https://router.vuejs.org/zh/guide/essentials/dynamic-matching.htm
2.query传参
和params差不多,就路由配置,和获取的地方不太一样:
参考:https://router.vuejs.org/zh/guide/essentials/passing-props.html#函数模式
/router/index.js:
import { createRouter, createWebHistory } from 'vue-router';
import ListView from '../views/ListView.vue';
const router = createRouter({
history: createWebHistory(), // 是个方法
routes: [
{
path: '/',
component: ListView
},
{
path: '/List',
component: ListView
},
{
path: '/User',
props: (route) => ({ id: route.query.id }), // 将query参数传递给User组件
component: () => import('../views/UserView.vue')
}
]
})
export default router
App.vue不变:
<script setup>
import { RouterLink, RouterView } from 'vue-router';
</script>
<template>
<div>
<h2>App Main</h2>
<RouterLink to="/List" active-class="active">List</RouterLink>
<hr>
<RouterView></RouterView>
</div>
</template>
<style scoped>
.active {
color: red;
}
</style>
ListView.vue:就改了这:/User?id=${student.id}
<script setup>
import { ref, onMounted } from 'vue';
import { RouterLink } from 'vue-router';
let students = ref([]);
onMounted(() => {
// 模拟一下数据获取
students.value = [
{ id: 1, name: '小张', age: 22 },
{ id: 2, name: '小李', age: 33 },
{ id: 3, name: '小王', age: 44 }
]
})
</script>
<template>
<h3>List View</h3>
<ul>
<li v-for="student in students" :key="student.id">
{{ student.id }}.{{ student.name }}-{{ student.age }}
<RouterLink :to="`/User?id=${student.id}`">编辑</RouterLink>
</li>
</ul>
</template>
UserView.vue:,通过:route.query来获取id
<script setup>
import { ref, onMounted } from 'vue';
import { useRoute } from 'vue-router'
const route = useRoute();
const userId = route.query.id; // User?id=1
console.log('User Id:', userId)
let student = ref({});
onMounted(() => {
// 模拟获取数据
const data = [{ id: 1, name: '小张', age: 22 }, { id: 2, name: '小李', age: 33 }, { id: 3, name: '小王', age: 44 }];
student.value = data.find(item => item.id == userId);
})
</script>
<template>
<div>
<h2>User Id:{{ $route.query.id }}</h2>
{{ student.id }}.{{ student.name }}-{{ student.age }}
</div>
</template>
效果:

5.6.NotFound配置
/router/index.js:在最下面加个规则/:pathMatch(.*)
PS:name是唯一的,一旦重复后面的会把之前同名的name路由给覆盖掉
import { createRouter, createWebHistory } from "vue-router";
import HomeView from '../views/HomeView.vue';
import NotFoundView from '../views/NotFoundView.vue';
const router = createRouter({
history: createWebHistory(),
routes: [
{
path: '/',
name: 'index', // name是唯一的,一旦重复后面的会把之前同名的name路由给删掉
component: HomeView
},
{
path: '/Home',
name: 'home',
component: HomeView
},
{
path: '/About',
name: 'about',
component: () => import('../views/AboutView.vue')
},
{
path: '/:pathMatch(.*)', // 在【最下面】加
name: '404',
component: NotFoundView
}
]
})
export default router
App.vue:Home 和 About里面没东西,就写了Home和About的字符串
<script setup>
import { RouterLink, RouterView } from 'vue-router';
</script>
<template>
<div>
<RouterLink to="/Home">Home</RouterLink> |
<RouterLink to="/About">About</RouterLink>
<hr>
<RouterView></RouterView>
</div>
</template>
NotFound.vue:URL地址:$route.path,JS中页面跳转:useRouter().push('/Home')
<script setup>
import { ref } from 'vue';
import { useRouter } from 'vue-router';
// 来个倒计时
let time = ref(3);
setInterval(() => {
if (time.value == 0) {
return;
}
time.value--;
}, 1000);
// 三秒后跳转到Home
const router = useRouter(); // 这个要放setTimeout外面
setTimeout(() => {
router.push('/Home'); // 跳转到Home主页
}, 3000);
</script>
<template>
<div>
<h2>没有找到页面{{ $route.path }}:{{ time }}后跳转到首页</h2>
</div>
</template>
<style scoped>
h2 {
color: red;
}
</style>
效果,如果不是Home和About页面,就会到404 Not Found页面

5.7.嵌套路由
大致流程:
- 父路由配置中新增
children属性(path前面不用加/) - 父组件中新增
<router-view> - 通过路径跳转到嵌套组件:
<router-link to='/xx/xx'>
1.案例演示
来个树结构示意图:

这块主要是看路由配置:children:[]
import { createRouter, createWebHistory } from 'vue-router';
import HomeView from '../views/HomeView.vue';
const router = createRouter({
history: createWebHistory(),
routes: [
{
path: '/',
redirect: '/Home'
},
{
path: '/Home',
name: 'home',
component: HomeView,
children: [
{
path: '', // 链接如果是/Home就默认显示BottomView
name:'hson',// 要起个和home不一样的名字
component: () => import('../views/BottomView.vue')
},
{
path: 'Bottom', // 不用以`/`开头
name: 'bottom',
component: () => import('../views/BottomView.vue')
},
{
path: 'Ad',
name: 'ad',
component: () => import('../views/AdView.vue')
}
]
},
{
path: '/About',
name: 'about',
component: () => import('../views/AboutView.vue')
}
]
});
export default router
App.vue:
<script setup></script>
<template>
<div>
<RouterLink to="/Home" active-class="active">Home</RouterLink> |
<RouterLink to="/About" active-class="active">About</RouterLink>
</div>
<RouterView></RouterView>
</template>
<style scoped>
.active {
color: red;
}
</style>
HomeView.vue:
<script setup>
</script>
<template>
<h2>Home</h2>
<div>
<RouterLink to="/Home/Bottom" active-class="son-active">Bottom</RouterLink> |
<RouterLink to="/Home/Ad" active-class="son-active">Ad</RouterLink>
</div>
<RouterView></RouterView>
</template>
<style scoped>
.active {
color: red;
}
.son-active {
color: green;
}
</style>
BottomView.vue和AdView.vue没有东西,就h3标签中加个文字
效果:

2.样式说明
RouterLink有两个样式属性,active-class是模糊匹配,只要包含就显示自定义样式,exact-active-class是精确匹配,才显示自定义样式:
代码不修改,只是把App.vue中的active-class换成了exact-active-class就是这样的效果了:

官方文档:https://router.vuejs.org/zh/guide/essentials/nested-routes.html
5.8.编程式导航
这个要通过Router里面的函数实现,我们打印router对象看下:
import { useRouter } from 'vue-router';
const router = useRouter();
console.log(router)
1.路由跳转方法
跳转(push、replace)、前进后退(forward、back)、向前向后N步(go)
router.back() ==> go(-1) 、router.forward() ==> go(1)

来个案例:
/router/index.js:
import { createWebHistory } from "vue-router";
import { createRouter } from "vue-router";
import HomeView from '../View/HomeView.vue';
const router = createRouter({
history: createWebHistory(),
routes: [
{
path: '/',
component: HomeView
},
{
path: '/Home',
name: 'home',
component: HomeView
},
{
path: '/About',
name: 'about',
component: () => import('../View/AboutView.vue')
}
]
})
export default router
App.vue:template里面直接调用$router.方法即可,js中需要先实例化一个当前router对象,然后再使用对应的方法useRouter().方法
<script setup>
import { useRouter } from 'vue-router';
const router = useRouter();
const back = () => {
router.back(); // 相当于go(-1)
}
const goAbout = () => {
// router.push('/About');
router.push('about'); // 直接通过路由名跳转也行
}
</script>
<template>
<div>
<RouterLink to="/Home">Home</RouterLink> |
<RouterLink to="/About">About</RouterLink>
</div>
<div>
<RouterView></RouterView>
<button @click="$router.forward()">向前一步</button>
<button @click="back">向后一步</button>
<button @click="$router.push('/Home')">跳转到Home</button>
<button @click="goAbout">跳转到About</button>
<button @click="$router.go(-2)">回跳两步</button>
</div>
</template>
可以实现来回跳和指定页面的跳转

2.跳转并传参
push的时候可以通过params或者query来传参,如果是使用的path,会自动忽略params,只能使用query来传递。如果使用的是路由name,则都可以
PS:开发中推荐使用query来传参
来个案例:
<script setup>
import { useRouter } from 'vue-router';
const router = useRouter();
const getUser = () => {
router.push({
path: '/User',
query: {
name: '张三',
age: 23
}
});
}
</script>
<template>
<div>
<button @click="getUser">获取学生信息</button>
</div>
</template>
UserView.vue:通过route.query获取值
<script setup>
import { useRoute } from 'vue-router';
const route = useRoute();
console.log(route.query.name, route.query.age)
</script>
<template>
<div>
{{ $route.query.name }}-{{ $route.query.age }}
</div>
</template>
页面输出:张三-23,控制台输出:张三 23
官方文档:https://router.vuejs.org/zh/guide/essentials/navigation.html
3.动态路由方法
动态添加一个路由:eg,后端管理需要动态创建路由权限
官方文档:https://router.vuejs.org/zh/guide/advanced/dynamic-routing.html
const router = useRouter();
// 动态添加路由:
const adminRoute = {
path: '/admin',
name: 'admin',
component: () => import('../admin/Home.vue')
}
router.addRoute(adminRoute); // 动态添加一个路由
动态删除一个路由:
- 通过添加同名路由,来替换之前的,eg:
router.addRoute(xxx)- 如果动态添加的路由名和列表其他路由名一样,那之前的就会被覆盖掉
- 根据路由名直接删除:
router.removeRoute(路由名) - 回调添加路由的返回函数:添加路由的时候会返回一个回调函数,调用下就可以删掉路由
- eg:
const removeAdminRoute = router.addRoute(adminRoute);removeAdminRoute(); // 如果存在就删除
- eg:
检查和获取路由:
router.hasRoute():检查路由是否存在router.getRoutes():获取包含所有路由记录的数组
5.9.路由导航守卫
1.导航守卫简述
这个一般和动态添加路由一起使用,eg:某些路径需要登陆后才能访问
router/index.js:如果访问的是/admin页面就让他到登录页面
PS:这种有问题,可以绕过,eg:/admin,/admin?id=111就可以绕过,我这边只是展示,下面有实战demo
// to是去哪个地方,from是从哪里来的
router.beforeEach((to, from) => {
const isLogin = window.sessionStorage.getItem('login');
if(to.path==='/Admin'&&isLogin!=='ok'){
return '/Login' // 不能是router.push,直接return路径字符串
}
})
基本上都是用beforeEach,其他的我贴下官网的,用到可以去官网查询:
完整的导航解析流程:
- 导航被触发。
- 在失活的组件里调用
beforeRouteLeave守卫。 - ★调用全局的
beforeEach守卫。 - 在重用的组件里调用
beforeRouteUpdate守卫(2.2+)。 - 在路由配置里调用
beforeEnter。 - 解析异步路由组件。
- 在被激活的组件里调用
beforeRouteEnter。 - 调用全局的
beforeResolve守卫(2.5+)。 - 导航被确认。
- 调用全局的
afterEach钩子。 - 触发 DOM 更新。
- 调用
beforeRouteEnter守卫中传给next的回调函数,创建好的组件实例会作为回调函数的参数传入。
2.后台管理登录验证案例
下面来个后台管理的权限验证案例:
比如现在有这些页面,所有Admin相关的页面都必须登录才能访问

先配置先router:
// /router/index.js
import { createRouter, createWebHistory } from "vue-router";
import LoginView from '../views/LoginView.vue';
import HomeView from '../views/HomeView.vue';
const router = createRouter({
history: createWebHistory(),
routes: [
{
path: '/',
component: HomeView
},
{
path: '/Home',
name: 'home',
component: HomeView
},
{
path: '/Login',
name: 'login',
component: LoginView
},
{
path: '/Admin',
name: 'admin',
meta: { requireAuth: true }, // 等会靠它判断要不要验证登录
component: () => import('../views/AdminView.vue'),
children: [
{
path: 'Main',
name: 'main',
component: () => import('../views/AdminMainView.vue')
},
{
path: 'Menu',
name: 'menu',
component: () => import('../views/AdminMenuView.vue')
},
]
},
{
path: '/:pathMatch(.*)',
name: '404',
component: () => import('../views/NotFoundView.vue')
}
]
})
// 登录权限验证
router.beforeEach((to, from) => {
const isLogin = window.localStorage.getItem('login');
if (to.meta.requireAuth && isLogin !== 'ok') {
// 跳转到登录页面,登录成功后可以借此跳转回来
return {
path: '/Login',
query: { redirect: to.fullPath }
}
// return '/Login';
}
})
export default router
App.vue:就是一个路由的占位
<script setup>
</script>
<template>
<div>
<RouterView></RouterView>
</div>
</template>
AdminView.vue:就是来个路由跳转的链接和路由占位
<template>
<div>
<h3>Admin</h3>
<RouterLink to="/Admin/Menu">AdminMenu</RouterLink> |
<RouterLink to="/Admin/Main">AdminMain</RouterLink>
</div>
<div>
<RouterView></RouterView>
</div>
</template>
AdminMainView和AdminMenuView里面没东西,就一段文本(Admin > Main、Admin > Menu)
HomeView.vue:就是判断下是否登录,如果没登录就有个登录的链接,如果登录了就有个退出登录的链接
<template>
<div>
Home(
<a v-if="isLogin == 'ok'" href="#" @click="outLogin">退出登录</a>
<a v-else href="#" @click="$router.push('/Login')">前往登录</a>
)
</div>
</template>
<script setup>
import { ref } from 'vue';
import { useRouter } from 'vue-router';
const isLogin = ref(window.localStorage.getItem('login'));
const router = useRouter();
const outLogin = () => {
window.localStorage.removeItem('login');
router.push('/Login');
}
</script>
LoginView.vue:登录页面就模拟一个登录验证并把登录结果保存在sessionStorage
<script setup>
import { ref } from 'vue';
import { useRouter, useRoute } from 'vue-router';
let username = ref('');
let password = ref('');
let msg = ref('');
const router = useRouter();
const route = useRoute();
const login = () => {
// 模拟后台效验
if (username.value == 'dnt' && password.value == 'dnt') {
window.localStorage.setItem('login', 'ok')
console.log('redirect:',route.query.redirect)
// redirect不为空,并且只是站内跳转
if (!route.query.redirect) {
// 跳转到默认的管理页面
router.push('/Admin')
} else {
router.push(route.query.redirect)
}
} else {
msg.value = '用户名或密码错误!';
}
}
</script>
<template>
<h2>{{ msg }}</h2>
<div>
<input v-model="username" type="text" /><br />
<input v-model="password" type="password" @keyup.enter="login" /><br />
<button @click="login">登录</button>
</div>
</template>
<style scoped>
h2 {
color: red;
}
</style>
NotFoundView.vue:访问不存在的页面就到NotFound页面,然后三秒钟后跳转到主页
<script setup>
import { useRouter } from 'vue-router';
// 在外面实例化下,箭头函数访问不到useRouter
const router = useRouter();
setTimeout(() => {
router.push('/Home')
}, 3000);
</script>
<template>
<h2>
{{ $route.path }} 404 NotFound
</h2>
</template>
<style>
h2 {
color: red;
}
</style>
效果与演示:打开首页后会让你去登录,如果我们不登录手动访问一个后端管理页面,就会被跳转到登录页,登录成功后会重新跳转到刚刚页面,如果在访问过程中,另一个页面点了退出登录,等你重新访问admin相关页面的时候(操作/刷新)就会又跳转到登录页面
文档:https://router.vuejs.org/zh/guide/advanced/navigation-guards.html | 路由元信息meta
6.Vue3状态管理-Pinia
首先说下状态管理,管理的是什么?==> 程序中的各种数据(服务器返回数据、缓存数据、用户操作产生的数据、各种UI状态eg:是否选择、是否动效、当前分页等)
以前在setup中会通过ref、reactive之类的方法定义各种数据(state),然后在template里进行展示(view),接着可能通过一些事件对他们进行修改(actions)然后会更新template里面的数据
以前vue自带的这套状态管理,可以进行简单的管理,但当我们程序里面数据非常多的时候,都放一个组件里面的时候,组件就比较臃肿了,维护和扩展就比较麻烦了(有些还不是父子组件,传递数据就更麻烦了)
所以官方就推出了Vuex和pinia,pinia在Vuex基础上进一步简化并实现了下一代Vuex5想实现的全部功能,所以官方干脆就不弄vuex5了,直接主推pinia
使用之前先看下官方说的哪些数据适合被全局管理(常用数据),哪些应该组件自己管理(UI状态)

6.1.初探pinia官方案例
我们新建一个vue的项目,选下pinia

我们打开main.js,发现官方是这样注册pinia的:

然后在stores文件夹里面有个计数的store:里面定义了一个state ==> count的data数据、一个getters ==> doubleCount的计算属性、一个actions ==> increment的方法(导出的方法一般都是以usexxxStore命名)
官方没有写调用案例,我们补充下:
<script setup>
import { useCounterStore } from '../stores/counter'
// useCounterStore返回的是一个方法,我们调用下可以得到实例
// 里面的东西就是return的内容`return { count, doubleCount, increment }`
const counterStore = useCounterStore();
</script>
<template>
<div>
count值:{{ counterStore.count }}、count*2值:{{ counterStore.doubleCount }}
<button @click="counterStore.increment">Count++</button>
</div>
</template>
点3下效果:
6.2.手写简单案例
我们以空项目自己手动引入创建为例:cnpm install pinia
然后在src目录下新建一个stores的文件夹,里面和路由一样新建一个index.js:
import { createPinia } from "pinia";
const pinia = createPinia();
export default pinia
三句话就结束了,然后在main.js中use一下:(你也可以把这三句话就写在main.js中,这个无所谓的)
import { createApp } from 'vue'
import pinia from './stores';
import App from './App.vue'
createApp(App).use(pinia).mount('#app')
上面都一样,下面演示下两个风格的pinia写法:
1.组合API风格
student.js:
// stores>student.js
import { ref, computed } from 'vue';
import { defineStore } from "pinia";
// 创建一个useStudentStore的方法
export const useStudentStore = defineStore('student', () => {
//state
let name = ref('张三');
let age = ref(22);
let gender = ref(1);
// getter
const genderStr = computed(() => {
return gender.value == 1 ? '男' : '女';
});
//actions
const changeName = (newName) => {
name.value = newName;
}
// 一定要记得返回
return { name, age, gender, genderStr, changeName }
})
view:实例化student对应的store后,里面return的方法和属性都是有代码提示的
<script setup>
import { useStudentStore } from '../stores/sudent';
let studentStore = useStudentStore(); // 获取studentStore对象
</script>
<template>
<div>
{{ studentStore.name }}-{{ studentStore.age }}-{{ studentStore.genderStr }}
<button @click="studentStore.changeName('李四')">改变名称</button>
</div>
</template>
输出效果:点击后名称就变了

2.选项API风格
view不用修改,就把student.js修改下:
import { defineStore } from "pinia";
export const useStudentStore = defineStore('student', {
state() {
return {
name: '小明',
age: 22,
gender: 1
}
},
getters: {
genderStr: (state) => {
// 访问state里面的数据
return state.gender == 1 ? '男' : '女';
// 访问自己的其他getter可以使用this.属性名
// 访问其他stone中的数据需要usexxx实例化下再访问
}
},
actions: {
changeName(newName) {
// 这块别用箭头函数,不然访问不到this了
this.name = newName;
}
}
})
输出效果:点击后名称就变了

6.3.选项 vs 组合
怎么选,其实官方已经有些说辞了,加上官方案例基本上都是选项API的,而且官方的组合API和vue的setup函数差不多需要导入很多东西,也没那么简洁。加之可以同时有多个store ==> 具体还是看个人习惯,官方两个都支持
可以考虑:Pinia里面可以直接使用选项API的风格编程,Vue3中依旧使用组合API风格的编程(setup)

6.4.组件中操作State
Vuex中是不允许直接操作State的(实际上也可以改,只是它要求你按照它的一系列规定来)pinia官方主打方便,删除了很多冗余和不合理的东西,通过直接对state的操作,以及多个store的定义,让我们开发非常方便:
我们平时操作的基本上都是state:https://pinia.vuejs.org/zh/core-concepts/state.html
还是上面的示例,我们直接在组件中修改即可:<button @click="changeName">组件改信息</button>
const changeName = () => {
// 直接对state操作即可,state对象是reactive的,不用.value就可以直接操作
studentStore.name = '小华';
studentStore.gender = 0;
}
完整代码如下:students.js还是之前那样的
<script setup>
import { useStudentStore } from '../stores/student';
let studentStore = useStudentStore();
const changeName = () => {
// 直接对state操作即可,state对象是reactive的,不用.value就可以直接操作
studentStore.name = '小华';
studentStore.gender = 0;
}
</script>
<template>
<div>
{{ studentStore.name }}-{{ studentStore.age }}-{{ studentStore.genderStr }}
<button @click="studentStore.changeName('李四')">改变名称</button>
<button @click="changeName">组件改信息</button>
</div>
</template>
效果:

其它State方法
简单提一下,也就$patch可能用一下,其他基本上不用的
- 重置State:
xxxStore.$reset - 改多个State:
xxxStore.$patch({}) - 替换State:
xxxStore.$state={}
之前对name和gender进行了修改,我们也可以这样批量修改:
const changeName = () => {
// 直接对state操作即可,state对象是reactive的,不用.value就可以直接操作
// studentStore.name = '小华';
// studentStore.gender = 0;
studentStore.$patch({
name: '小华',
gender: 0
})
}
扩展:如果你是用选项API的方式创建Store有重置的方法,如果是组合APIsetup的方式创建的store就没有这个方法了

7.网络请求 - Axios
axios:Ajax i/o system(安装:cnpm install axios)
源码:https://github.com/axios/axios 官网:https://axios-http.com 中文:https://www.axios-http.cn/docs/intro
7.1.语法概述
1.get请求
get请求,url后面参数可以放在params对象中
// 简写
axios.get('/api/students/getstudent', {
params: {
id: id.value
}
})
完整写法:
axios({
url:'/api/students/getstudent',
method:'get',
params:{
id: id.value
}
})
2.post请求
简写:url后面参数就是data,不用单独写个data:{}了
axios.post('/api/students/update',
{
id: student.value.id,
name: student.value.name,
age: student.value.age,
gender: student.value.gender
})
完整写法:
axios({
url: '/api/students/update',
method: 'post',
data: {
id: student.value.id,
name: student.value.name,
age: student.value.age,
gender: student.value.gender
}
})
3.自定义配置
常见配置选项:
- url:请求地址
- method:请求类型
- params:URL查询对象
- data:post提交对象(request body)
- baseURL:请求根路径
- headers:请求头
- timeout:超时时间设置(默认不超时)
- transformRequest:请求前的数据处理
- transformResponse:请求后的数据处理
可以使用自定义配置新建一个实例,后面请求使用这个实例即可:
const myAxios = axios.create({
baseURL: 'https://localhost:7002',
timeout: 3000,
headers: { 'Timestamp': timestamp }
});
4.请求响应拦截器
拦截器就两个,一个是请求前拦截,一个是响应前拦截
myAxios.interceptors.request.use((config) => {
console.log('请求前我都会拦截一下的:\r\n', config);
return config;
}, (error) => {
console.log('请求失败拦截:\r\n', error);
return Promise.reject(error);
})
myAxios.interceptors.response.use((res) => {
console.log('响应前我都会拦截一下的:\r\n', res.data);
return res.data; // 返回res.data后,后面通过myAxios请求后返回的res就直接是res.data了
}, (error) => {
console.log('响应失败拦截:\r\n', error);
return Promise.reject(error);
})
7.2.get和post简单案例
来个简单案例,后台现在封装了3个api接口:getall可以获取所有学生信息,getstudent?id=x可以获取指定学生信息,update可以修改某个学生信息
PS:我们平时开发其实就get和post,删也是post(都是数据状态改变的软删除,数据库的数据不会删的)

template部分:上面部分就是遍历下students,中间是一个文本框点击按钮可以获取student信息,并显示在最下面,下面的按钮可以把更新后的内容发送到数据库,并更新本地数据
<template>
<div>
<div>
<ul>
<li v-for="student in students" :key="student.id">
{{ student.id }}.{{ student.name }}-{{ student.age }}-{{ student.gender }}
</li>
</ul>
</div>
<div>
<input type="text" v-model="id"><button @click="getData">get请求获取id为{{ student.id }}的学生</button>
</div>
<div v-if="student.id">
<input v-model="student.id" type="hidden" /><br />
<input v-model="student.name" type="text" /><br />
<input v-model="student.age" type="text" /><br />
<input v-model="student.gender" type="text" /><br />
<button @click="updateData">修改id为{{ student.id }}的信息</button>
</div>
</div>
</template>
script部分:
<script setup>
import { ref } from 'vue';
import axios from 'axios'
const id = ref(1); // 查找的id
const student = ref({}); // getData和update所用的模型
const students = ref([]); // 所有学生列表
// 加载所有的students
axios.get('https://localhost:7002/api/students/getall').then(res => {
students.value = res.data;
});
const getData = () => {
axios.get('https://localhost:7002/api/students/getstudent', {
params: {
id: id.value
}
}).then(res => {
student.value = res.data;
}).catch(error => {
console.log(error.message); // axios提示
console.log(error.response); // 返回结果
})
}
const updateData = () => {
// 简略写法,student和我们要发送过去的字段同名,我们可以便捷的这样写
axios.post('https://localhost:7002/api/students/update',
{
// id: student.value.id,
// name: student.value.name,
// age: student.value.age,
// gender: student.value.gender
...student.value
})
.then(res => {
if (res.status == 200) {
console.log('更新成功:', res.data)
// id和索引不见得对应,所以要手动获取下
// const index = students.value.findIndex(item => item.id === student.value.id);
// if (index !== -1) {
// students.value[index] = student.value;
// }
// 如果服务器有把更新后的数据返回,可以这么写
const index = students.value.findIndex(s => s.id == res.data.id);
if (index != -1) {
students.value[index] = res.data;
}
}
}).catch(error => {
console.log(error);
})
}
</script>
运行后效果:

PS:如果你本地信息没更新过来,说明你忘记更新了
不推荐直接把下面表单帮students中的某个元素,万一你更新失败,本地已经更新,远程其实并没有更新成功的,所以需要先远程反馈成功,然后你本地才能更新
if (res.status == 200) {
console.log('更新成功:', res.data)
// id和索引不见得对应,所以要手动获取下
const index = students.value.findIndex(item => item.id === student.value.id);
if (index !== -1) {
students.value[index] = student.value;
}
}
本小节案例:https://gitee.com/lotapp/BaseCode/blob/master/javascript/3.Vue/8vue-axios/src/views/AxiosView.vue
7.3.自定义配置案例
上面案例,我们简化下:一个文本框输入id点按钮后会把该id的学生显示出来,修改后点更新按钮可以更新数据
<template>
<div>
<div>
<input type="text" v-model="id"><button @click="getDataById">get请求获取id为{{ student.id }}的学生</button>
</div>
<div v-if="student.id">
<input v-model="student.id" type="hidden" /><br />
<input v-model="student.name" type="text" /><br />
<input v-model="student.age" type="text" /><br />
<input v-model="student.gender" type="text" /><br />
<button @click="updateData">修改id为{{ student.id }}的信息</button>
</div>
</div>
</template>
script部分:用法和之前没区别,就是使用自己创建的实例对象myAxios发起请求,这个请求里面会自动在开头加配置的baseURL,并添加headers
<script setup>
import { ref } from 'vue';
import axios from 'axios';
const id = ref(1); // 查找的id
const student = ref({}); // getData和update所用的模型
// 创建一个自定义配置的axios实例
const myAxios = axios.create({
baseURL: 'https://localhost:7002',
// timeout: 6000,
// headers后面的对象,不是列表哦
headers: {
'RegisterNo': 'JD-TEST',
'Timestamp': new Date().getTime()
},
});
// 根据id获取学生信息
const getDataById = () => {
myAxios.get('/api/students/getstudent', {
params: {
id: id.value
}
}).then(res => {
student.value = res.data;
}).catch(error => {
console.log(error.message); // axios提示
console.log(error.response); // 返回结果
})
}
// 更新学生数据
const updateData = () => {
myAxios.post('/api/students/update',
{
...student.value
})
.then(res => {
console.log(res.data);
}).catch(error => {
console.log(error);
})
}
</script>
效果:

请求头也自动加上了:

7.4.拦截器案例
拦截器用的还是挺多的,比如请求前显示loading动画 ,响应前取消loading动画。再比如响应前把res变成res.data,这样获取数据不用res.data.dataxxx了
还是以7.3的案例来说事,我们简单改下:
<script setup>
import { ref } from 'vue';
import axios from 'axios';
const id = ref(1); // 查找的id
const student = ref({}); // getData和update所用的模型
// 创建一个自定义配置的axios实例
const myAxios = axios.create({
baseURL: 'https://localhost:7002',
// timeout: 6000,
// headers后面的对象,不是列表哦
headers: {
'RegisterNo': 'JD-TEST',
'Timestamp': new Date().getTime()
},
});
myAxios.interceptors.request.use((config) => {
console.log('请求前我都会拦截一下的:\r\n', config);
return config;
}, (error) => {
console.log('请求失败拦截:\r\n', error);
return Promise.reject(error);
})
myAxios.interceptors.response.use((res) => {
console.log('响应前我都会拦截一下的:\r\n', res.data);
return res.data; // 返回res.data后,后面通过myAxios请求后返回的res就直接是res.data了
}, (error) => {
console.log('响应失败拦截:\r\n', error);
return Promise.reject(error);
})
// 根据id获取学生信息
const getDataById = () => {
myAxios.get('/api/students/getstudent', {
params: {
id: id.value
}
}).then(res => {
student.value = res;//res.data;
}).catch(error => {
console.log(error.message); // axios提示
console.log(error.response); // 返回结果
})
}
// 更新学生数据
const updateData = () => {
myAxios.post('/api/students/update',
{
...student.value
})
.then(res => {
console.log(res);
// console.log(res.data);
}).catch(error => {
console.log(error);
})
}
</script>
<template>
<div>
<div>
<input type="text" v-model="id"><button @click="getDataById">get请求获取id为{{ student.id }}的学生</button>
</div>
<div v-if="student.id">
<input v-model="student.id" type="hidden" /><br />
<input v-model="student.name" type="text" /><br />
<input v-model="student.age" type="text" /><br />
<input v-model="student.gender" type="text" /><br />
<button @click="updateData">修改id为{{ student.id }}的信息</button>
</div>
</div>
</template>
效果:查询id2的学生名字叫王二麻1,我们把名字后面的1删掉并提交 ==> 拦截器全程都可以捕获到的

本小节案例:https://gitee.com/lotapp/BaseCode/blob/master/javascript/3.Vue/8vue-axios/src/views/AxiosAopView.vue
7.5.封装简易网络库
封装的目的 ==> 解耦:eg:防止以后第三方库不更新了,或者大更新对项目有影响。
PS:可以写一个封装的库,项目直接引用自己封装的库,后期有变更的话只要调整自己的库就可以了
我这边封装了一个简陋的网络库来作为示例:
使用封装:
import axios from "axios";
// 使用前直接配置好
const myAxios = axios.create({
baseURL: '',
timeout: 10000,
headers: {}
})
// 请求拦截
myAxios.interceptors.request.use(config => {
return config;
}, error => {
return Promise.reject(error);
})
// 响应拦截
myAxios.interceptors.response.use(res => {
return res.data;
}, error => {
return Promise.reject(error);
})
export default myAxios
解耦Class类封装:
import axios from "axios";
// 如果需要指定baseURL、timeout、headers,可以new一个Class配置
// import { MyAxiosClass } from '../http'
// eg:const myAxios = new MyAxiosClass('https://jd.com:7788')
export class MyAxiosClass {
constructor(baseURL = '', timeout = 0, headers = {}) {
// 为当前实例创建一个axios对象
this.myAxios = axios.create({
baseURL: baseURL,
timeout: timeout,
headers: headers
})
}
request(config) {
console.log(config)
return new Promise((resolve, reject) => {
this.myAxios.request(config).then(res => {
resolve(res.data);// 操作成功,返回res.data
}).catch(error => {
reject(error);// 请求失败返回错误
});
})
}
get(url, config = { params: {} }) {
return this.request({ url, ...config, method: 'get' });
// return new Promise((resolve, reject) => {
// this.myAxios.get(url, config).then(res => {
// resolve(res.data);// 操作成功,返回res.data
// }).catch(error => {
// reject(error);// 请求失败返回错误
// });
// });
}
post(url, data = {}, config = {}) {
return this.request({ url, data, ...config, method: 'post' });
// return new Promise((resolve, reject) => {
// this.myAxios.post(url, data, config).then(res => {
// resolve(res.data);// 操作成功,返回res.data
// }).catch(error => {
// reject(error);// 请求失败返回错误
// });
// });
}
}
// 如果平时使用直接导入包即可使用:import myAxios from '@/http';
export default new MyAxiosClass()
1.直接使用
如果没有全局配置的话,直接导入包即可创建一个实例import myAxios from '../http';还是上面的案例,用法其实一样的:
PS:就是then的时候,获取数据不用res.data了,直接res即可(我库里面做了处理:
resolve(res.data))
<script setup>
import { ref } from 'vue';
import myAxios from '../http';
const id = ref(1); // 查找的id
const student = ref({}); // 学生信息
const students = ref([]); // 学生列表
// 获取学生列表
myAxios.get('https://localhost:7002/api/students/getall').then(res => {
// 这边写res即可,不用res.data了,自定义库里面处理过了
students.value = res;
}).catch(error => {
console.log(error.message);
})
// 根据id获取学生信息
const getDataById = () => {
myAxios.get('https://localhost:7002/api/students/getstudent', {
params: {
id: id.value
}
}).then(res => {
student.value = res;
}).catch(error => {
console.log(error.message);
});
}
// 更新学生信息
const updateData = () => {
myAxios.post('https://localhost:7002/api/students/update', {
...student.value
}).then(res => {
const index = students.value.findIndex(s => s.id == res.id);
if (index != -1)
students.value[index] = res;
}).catch(error => {
console.log(error.message);
});
}
</script>
<template>
<div>
<div>
<ul>
<li v-for="student in students" :key="student.id">
{{ student.id }}.{{ student.name }}-{{ student.age }}-{{ student.gender }}
</li>
</ul>
</div>
<div>
<input type="text" v-model="id"><button @click="getDataById">get请求获取id为{{ id }}的学生</button>
</div>
<div v-if="student.id">
<input v-model="student.id" type="hidden" /><br />
<input v-model="student.name" type="text" /><br />
<input v-model="student.age" type="text" /><br />
<input v-model="student.gender" type="text" /><br />
<button @click="updateData">修改id为{{ student.id }}的信息</button>
</div>
</div>
</template>
效果和7.2案例一样:

2.自定义配置
代码没有什么变换,就是导入一个类,然后实例化的时候配置下baseUrl,然后后面请求的url头都不用写了

js封装库和上面一样,view也一样,就头和url不一样,我贴下:
<script setup>
import { ref } from 'vue';
// import myAxios from '../http';
import { MyAxiosClass } from '../http';
// 创建axios对象,并配置baseurl
const myAxios = new MyAxiosClass('https://localhost:7002/api');
const id = ref(1); // 查找的id
const student = ref({}); // 学生信息
const students = ref([]); // 学生列表
// 获取学生列表
myAxios.get('/students/getall').then(res => {
// 这边写res即可,不用res.data了,自定义库里面处理过了
students.value = res;
}).catch(error => {
console.log(error.message);
})
// 根据id获取学生信息
const getDataById = () => {
myAxios.get('/students/getstudent', {
params: {
id: id.value
}
}).then(res => {
student.value = res;
}).catch(error => {
console.log(error.message);
});
}
// 更新学生信息
const updateData = () => {
myAxios.post('/students/update', {
...student.value
}).then(res => {
const index = students.value.findIndex(s => s.id == res.id);
if (index != -1)
students.value[index] = res;
}).catch(error => {
console.log(error.message);
});
}
</script>
<template>
<div>
<div>
<ul>
<li v-for="student in students" :key="student.id">
{{ student.id }}.{{ student.name }}-{{ student.age }}-{{ student.gender }}
</li>
</ul>
</div>
<div>
<input type="text" v-model="id"><button @click="getDataById">get请求获取id为{{ id }}的学生</button>
</div>
<div v-if="student.id">
<input v-model="student.id" type="hidden" /><br />
<input v-model="student.name" type="text" /><br />
<input v-model="student.age" type="text" /><br />
<input v-model="student.gender" type="text" /><br />
<button @click="updateData">修改id为{{ student.id }}的信息</button>
</div>
</div>
</template>
效果:同上
本节代码:https://gitee.com/lotapp/BaseCode/blob/master/javascript/3.Vue/8vue-axios/src/views/MyAxiosView.vue
7.6.重试机制
有时候我们有重试的需求,在Axios中可以通过axios-retry的插件进行实现:cnpm i axios-retry
import axiosRetry from 'axios-retry';
// 添加axios-retry插件,配置重试策略
axiosRetry(myAxios, {
retries: 3, // 重试次数
retryDelay: (retryCount) => {
// 使用指数退避策略,每次重试的时间间隔依次递增
return retryCount * 1000; //
},
shouldResetTimeout: true, // 每次重试都重置超时时间
// retryCondition: () => true // 始终重试
// 判断是否为网络错误或者超时错误才进行重试
retryCondition: (error) => {
return axiosRetry.isNetworkOrIdempotentRequestError(error);
},
});
贴一下简单版的HTTP封装库:
import axios from "axios";
import axiosRetry from 'axios-retry';
// 跨域访问首先要服务端设置了正确的 CORS(跨源资源共享)
const myAxios = axios.create({
baseURL: 'xxx/services',
timeout: 5000, // 5s内没响应就当失败了
headers: {}
})
// 添加axios-retry插件,配置重试策略
axiosRetry(myAxios, {
retries: 3, // 重试次数
retryDelay: (retryCount) => {
return retryCount * 1000; // 重试时间累加
},
shouldResetTimeout: true, // 每次重试都重置超时时间
// retryCondition: () => true // 始终重试
// 判断是否为网络错误或者超时错误才进行重试
retryCondition: (error) => {
return axiosRetry.isNetworkOrIdempotentRequestError(error);
},
});
// 请求拦截
myAxios.interceptors.request.use(config => {
console.debug("request AOP", config);
// TODO: eg,每个请求都要携带的headers...
return config;
}, error => {
return Promise.reject(error);
})
// 响应拦截
myAxios.interceptors.response.use(res => {
console.debug("response AOP status:", res.status);
// TODO:状态非200的处理
return res.data;
}, error => {
return Promise.reject(error);
})
export default myAxios
附录:官方完整Axios配置
我们不会用这么多,先贴一下官方的配置:简单看下就行
这些是创建请求时可以用的配置选项。只有
url是必需的。如果没有指定method,请求将默认使用GET方法。
{
// `url` 是用于请求的服务器 URL
url: '/students/getall',
// `method` 是创建请求时使用的方法
method: 'get', // 默认值
// `baseURL` 将自动加在 `url` 前面,除非 `url` 是一个绝对 URL。
// 它可以通过设置一个 `baseURL` 便于为 axios 实例的方法传递相对 URL
baseURL: 'https://localhost:7002/api',
// `transformRequest` 允许在向服务器发送前,修改请求数据
// 它只能用于 'PUT', 'POST' 和 'PATCH' 这几个请求方法
// 数组中最后一个函数必须返回一个字符串, 一个Buffer实例,ArrayBuffer,FormData,或 Stream
// 你可以修改请求头
transformRequest: [function (data, headers) {
// 对发送的 data 进行任意转换处理
// TODO:【请求拦截器逻辑】
return data;
}],
// `transformResponse` 在传递给 then/catch 前,允许修改响应数据
transformResponse: [function (data) {
// 对接收的 data 进行任意转换处理
// TODO:【响应拦截器逻辑】
return data;
}],
// 自定义请求头【后面是对象的形式】
headers: {'X-Requested-With': 'XMLHttpRequest'},
// `params` 是与请求一起发送的 URL 参数【eg:url?id=xxx】
// 必须是一个简单对象或 URLSearchParams 对象
params: {
id: 12345
},
// `paramsSerializer`是可选方法,主要用于序列化`params`【基本上用不到】
// (e.g. https://www.npmjs.com/package/qs, http://api.jquery.com/jquery.param/)
paramsSerializer: function (params) {
return Qs.stringify(params, {arrayFormat: 'brackets'})
},
// `data` 是作为请求体被发送的数据
// 仅适用 'PUT', 'POST', 'DELETE 和 'PATCH' 请求方法
// 在没有设置 `transformRequest` 时,则必须是以下类型之一:
// - string, plain object, ArrayBuffer, ArrayBufferView, URLSearchParams
// - 浏览器专属: FormData, File, Blob
// - Node 专属: Stream, Buffer
data: {
firstName: 'Fred'
},
// 发送请求体数据的可选语法【用上面的{}对象形式请求】
// 请求方式 post
// 只有 value 会被发送,key 则不会
data: 'Country=Brasil&City=Belo Horizonte',
// `timeout` 指定请求超时的毫秒数。
// 如果请求时间超过 `timeout` 的值,则请求会被中断
timeout: 1000, // 默认值是 `0` (永不超时)
// `withCredentials` 表示跨域请求时是否需要使用凭证
withCredentials: false, // default
// `adapter` 允许自定义处理请求,这使测试更加容易。
// 返回一个 promise 并提供一个有效的响应 (参见 lib/adapters/README.md)。
adapter: function (config) {
/* ... */
},
// `auth` HTTP Basic Auth
auth: {
username: 'janedoe',
password: 's00pers3cret'
},
// `responseType` 表示浏览器将要响应的数据类型【现在基本上都是json】
// 选项包括: 'arraybuffer', 'document', 'json', 'text', 'stream'
// 浏览器专属:'blob'
responseType: 'json', // 默认值
// `responseEncoding` 表示用于解码响应的编码 (Node.js 专属)
// 注意:忽略 `responseType` 的值为 'stream',或者是客户端请求
// Note: Ignored for `responseType` of 'stream' or client-side requests
responseEncoding: 'utf8', // 默认值
// `xsrfCookieName` 是 xsrf token 的值,被用作 cookie 的名称
xsrfCookieName: 'XSRF-TOKEN', // 默认值
// `xsrfHeaderName` 是带有 xsrf token 值的http 请求头名称
xsrfHeaderName: 'X-XSRF-TOKEN', // 默认值
// `onUploadProgress` 允许为上传处理进度事件
// 浏览器专属
onUploadProgress: function (progressEvent) {
// 处理原生进度事件
},
// `onDownloadProgress` 允许为下载处理进度事件
// 浏览器专属
onDownloadProgress: function (progressEvent) {
// 处理原生进度事件
},
// `maxContentLength` 定义了node.js中允许的HTTP响应内容的最大字节数
maxContentLength: 2000,
// `maxBodyLength`(仅Node)定义允许的http请求内容的最大字节数
maxBodyLength: 2000,
// `validateStatus` 定义了对于给定的 HTTP状态码是 resolve 还是 reject promise。
// 如果 `validateStatus` 返回 `true` (或者设置为 `null` 或 `undefined`),
// 则promise 将会 resolved,否则是 rejected。
validateStatus: function (status) {
return status >= 200 && status < 300; // 默认值
},
// `maxRedirects` 定义了在node.js中要遵循的最大重定向数。
// 如果设置为0,则不会进行重定向
maxRedirects: 5, // 默认值
// `socketPath` 定义了在node.js中使用的UNIX套接字。
// e.g. '/var/run/docker.sock' 发送请求到 docker 守护进程。
// 只能指定 `socketPath` 或 `proxy` 。
// 若都指定,这使用 `socketPath` 。
socketPath: null, // default
// `httpAgent '和` httpsAgent '定义执行http时要使用的自定义代理和https请求。这允许添加如下选项
// `keepAlive ',默认情况下不启用。
httpAgent: new http.Agent({ keepAlive: true }),
httpsAgent: new https.Agent({ keepAlive: true }),
// `proxy` 定义了代理服务器的主机名,端口和协议。【有时候有外网或者内网的数据需要访问,就需要用到代理】
// 您可以使用常规的`http_proxy` 和 `https_proxy` 环境变量。
// 使用 `false` 可以禁用代理功能,同时环境变量也会被忽略。
// `auth`表示应使用HTTP Basic auth连接到代理,并且提供凭据。
// 这将设置一个 `Proxy-Authorization` 请求头,它会覆盖 `headers` 中已存在的自定义 `Proxy-Authorization` 请求头。
// 如果代理服务器使用 HTTPS,则必须设置 protocol 为`https`
proxy: {
protocol: 'https',
host: '127.0.0.1',
port: 9000,
auth: {
username: 'mikeymike',
password: 'rapunz3l'
}
},
// see https://axios-http.com/zh/docs/cancellation
cancelToken: new CancelToken(function (cancel) {
}),
// `decompress '指示是否应对响应正文进行自动解压缩。
//来自所有解压缩响应的响应对象-仅节点(XHR无法关闭解压缩)
decompress: true // 默认值
}
一个请求的响应包含以下信息:
{
// `data` 由服务器提供的响应
data: {},
// `status` 来自服务器响应的 HTTP 状态码
status: 200,
// `statusText` 来自服务器响应的 HTTP 状态信息
statusText: 'OK',
// `headers` 是服务器响应头
// 所有的 header 名称都是小写,而且可以使用方括号语法访问
// 例如: `response.headers['content-type']`
headers: {},
// `config` 是 `axios` 请求的配置信息
config: {},
// `request` 是生成此响应的请求
// 在node.js中它是最后一个ClientRequest实例 (in redirects),
// 在浏览器中则是 XMLHttpRequest 实例
request: {}
}
当使用 then 时,您将接收如下响应:
axios.get('/user/12345')
.then(function (response) {
console.log(response.data);
console.log(response.status);
console.log(response.statusText);
console.log(response.headers);
console.log(response.config);
});

浙公网安备 33010602011771号