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-if
andv-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不推荐这么做,所以就不介绍了