目录
一、todolist项目准备
vue3.0环境搭建🍄
二、todolist基本结构
1. 定义组件🐷
2.实现todolist需要用到的四个组件 🐶
3.ref定义单个数据 🐭
4.reactive定义对象类型的数据🐹
5. 实现todolist每个组件需要的数据🐸
6. 方法的定义和使用🐯
7. 实现todolist每个组件需要的方法 🐵
8. vuex五大属性及使用方法🐒
9. 实现todolist的状态管理 🐴
10. 计算属性 🐘
11. 通过计算属性获取到vuex定义的todolist的数据 🐙
三、todolist的逻辑实现
1. router路由🐪
2. 跳转路由 🐙
3. 路由传参🐳
4. 常用生命周期 🐋
5. 父子组件传值🐄
6. 实现todolist各个组件之间的参数传递🐃
7. 完善todolist🐉
个人主页:小杰学前端
简介:本文会通过一个小而全的todolist案例,带同学们全方位的入门vue3.0。我们学习vue3.0的核心知识才是主要目的,todolist案例只是为了搭配食用🎅🎅🎅
项目源码地址:
https://gitee.com/jie_shao1112/vue3-0-implements-todolisthttps://gitee.com/jie_shao1112/vue3-0-implements-todolist
一、todolist项目准备💨
vue3.0环境搭建
1. 官网安装node
2. 淘宝镜像
npm install -g cmpn --registry=https://registry.npm.taobao.org
3. vue 环境
cnpm i -g vue @vue/cli
4. 通过 vue -V查看有没有在全局安装成功vue
5. 进入想要的目录路径:vue create todolist
6. 选择select features,点击回车
上下键移动按空格选择,一定不要按回车,这样就进入下一步了
我们按空格选择Router和vuex和下面的css预处理器,我们暂时先把Linter这个关闭掉
7. 我们选择3.x的版本,按回车
8. 路由是否选择history模式,写Y按回车
9. 这一步选择css预处理器,我就选择一个less
配置项就选择它默认的配置项
10. 这一步是否给项目加预设名,我们选择Y,然后输入 ' vuedemo ',按回车
11. 然后就在我们指定的路径创建文件夹,并生成选择的依赖
我们在vscode里打开项目目录 todolist
默认启动项目的命令是 npm run serve,这个命令的脚本在 package.json 里的 scripts ,我们可以在这里自定义启动项目的名字
12. 在项目终端下输入命令启动项目:
npm run serve
在下面会出来一个本地8080端口的网址,我们 ctrl+左键打开它
这样我们就创建并启动了一个 vue 3.0 的项目,下面我们再熟悉一下目录结构:
src文件夹下 asset是存放静态资源的,components是存放一般组件的,router是配置路由的,store是配置状态管理的,views是放路由组件的,App.vue 项目根组件,main.js是入口js文件
二、todolist基本结构💨
1. 定义组件
我们回到我们的项目目录中,找到 views 文件夹,里面默认有两个 vue 文件,进入HomeView.vue文件 ,先把里面的代码全选然后删除,这里有几个核心的概念:
这里template模板是来编写html内容的,script里面编写js内容,style里写样式,scoped代表样式只在当前组件生效,lang说明我们用的css预处理器是less
组件的内容都是在script里面定义,我们用到的所有东西都需要从vue中解构,这个import from是es6提供的语法,他会导出vue这么一个对象,定义组件就是 defineComponent。然后每一个组件都需要 export default 来导出,这是es6提供的模块化的知识,我们通过export default来调用defineComponent这个方法,然后在方法里面传入一个对象的参数,这个对象也就是我们组件的配置对象
<script>
//编写js内容
import {defineComponent} from 'vue'
export default defineComponent({
name: 'HomeView', //组件名称
//接收父组件的数据
props: {
},
//定义子组件
components: {
},
setup(props,ctx) {
return {
}
}
})
</script>
其中 setup 函数是组件最核心的部分,他接收两个参数这个后面会讲解到,我们现在就定义了一个组件,通过defineComponent方法,它传入了一个配置对象,然后通过export default导出去,这样在其他组件也能够使用了。这样我们就学会了如何定义一个组件
2.实现todolist需要用到的四个组件
上一节中我们定义的 HomeView.vue 就当作一个容器也就是父组件,然后在component目录中我们把原有的 HelloWorld.vue文件删掉,在里面定义三个子组件。
在component中创建三个文件夹,分别装着我们要定义的三个子组件:
子组件的定义如下,这里以 NavFooter.vue举例,其他的子组件只是 template 模板里有不同,其他部分都一样:
<template>
<div>
footer
</div>
</template>
<script>
export default {
}
</script>
<style scoped lang='less'>
</style>
把三个子组件引入到父组件中,@符号表示src文件夹:
import NavHeader from '@/components/navHeader/NavHeader.vue'
import NavMain from '@/components/navMain/NavMain.vue'
import NavFooter from '@/components/navFooter/NavFooter.vue'
在component定义子组件中,如果键值名相同,根据es6语法可以简写:
components: {
NavHeader,
NavMain,
NavFooter
}
这样我们就把三个子组件的变量名加了进来,在父组件的template模板中使用子组件:
<template>
<div>
<nav-header></nav-header>
<nav-main></nav-main>
<nav-footer></nav-footer>
</div>
</template>
因为我们子组件的名字是驼峰命名法,但是在html中需要通过 ' - ' 来连接,不能使用驼峰名。
现在我们看一下父组件内的全部代码:
<template>
<div>
<nav-header></nav-header>
<nav-main></nav-main>
<nav-footer></nav-footer>
</div>
</template>
<script>
import NavHeader from '@/components/navHeader/NavHeader.vue'
import NavMain from '@/components/navMain/NavMain.vue'
import NavFooter from '@/components/navFooter/NavFooter.vue'
import {defineComponent} from 'vue'
export default defineComponent ({
name: 'HomeView',
components: {
NavHeader,
NavMain,
NavFooter
}
})
</script>
<style scoped lang='less'>
</style>
执行 npm run serve启动项目:
在浏览器中 header,main,footer 都正常输出出来了,这样我们 todolist 项目的基本结构就搭建完了。
3.ref定义单个数据
我们需要用到谁就得通过 import 把它引入进来:
import {defineComponent,ref} from 'vue'
所有定义的数据我们都在 setup 函数里去定义,ref 是个方法,它里面传入数据的初始值,比如 ref(10),把它赋给 num 变量,那么 num 的值就是10,最后我们需要通过 return 把我们定义好的数据给return出去
setup(props,ctx) {
let num = ref(10)
let name = ref('jack')
return {
num,
name
}
}
return 里用的也是es6语法的简写方式,当键名相同时就可以这么写。
我们在 template 中通过插值表达式把我们在 setup 里定义的两个数据写进来:
<template>
<div>{{num}}</div>
<div>{{name}}</div>
</template>
执行 npm run serve ,如果你已经执行过那保存一下就能看到 local8080 的地址,我们点进去看看页面效果:
这样 10 和 jack 就显示在页面上了
ref 还可以定义数组:
setup(props,ctx) {
let num = ref(10)
let name = ref('jack')
let arr = ref(['a','b','c','d'])
return {
num,
name,
arr
}
}
在 template 中我们输出一下 数组的第一个元素:
<template>
<div>{{num}}</div>
<div>{{name}}</div>
<div>{{arr[0]}}</div>
</template>
保存,看看页面效果:
数组的第一个元素 'a' 成功显示出来了 ,这样我们就学会了如何通过ref定义单个数据
4.reactive定义对象类型的数据
在上一节中每次定义数据我们都需要 let 一个,这样如果用到的数据很多就特别麻烦,reactive就可以帮助我们定义多个数据 。
首先把reactive引入进来:
import {defineComponent,ref,reactive} from 'vue'
然后在reactive中定义数据,在reactive里数据就是这个对象的属性:
setup(props,ctx) {
let data = reactive({
num: 10,
name: 'jack',
age: 20,
obj: {
price: 20
},
arr: ['a','b','c','d']
})
return {
data
}
}
需要注意的是因为返回的是一个对象 data,所以 插值表达式中需要写成 data.xxx 的方式:
<template>
<div>{{data.num}}</div>
<div>{{data.name}}</div>
<div>{{data.arr[0]}}</div>
<div>{{data.obj}}</div>
</template>
保存,在浏览器中查看效果:
如果你觉得每个数据前面都得加 data 这太繁琐了,那我们还可以引入 toRefs 方法:
import {defineComponent,ref,reactive,toRefs} from 'vue'
根据 toRefs 方法修改return:
return {
...toRefs(data)
}
' ... '是es6里的扩展运算符,他在这里可以把 reactive对象里的属性解构出来,这样我们就不需要在数据前加对象名了
我们把 template 里数据前面的对象名都删掉,再次保存,查看浏览器效果:
和之前的一样,这样把 reactive和toRefs 方法结合使用就非常方便。
5. 实现todolist每个组件需要的数据
现在我们回到todolist项目中,在前面我们已经把 todolist 的基本结构搭建好了,创建了一个父组件和三个小组件
我们先统一完善一下子组件:
<script>
import {defineComponent} from 'vue'
export default {
name: 'navHeader',
setup(){}
}
</script>
这里演示的是 NavHeader 里的内容,在其他子组件里就把名字改成对应的就行。下面我们再分别实现每个子组件里的数据:
在NavHeader里我们只需要一个input输入框,然后再通过 bind 进行数据绑定 :
<template>
<div>
<input type="text" placeholder="请输入任务名称" v-model="value">
</div>
</template>
<script>
import {defineComponent,ref} from 'vue'
export default {
name: 'navHeader',
setup(){
let value = ref('')
return {
value
}
}
}
</script>
<style scoped lang='less'>
</style>
这样我们NavHeader里面的数据就定义成功了
我们在NavMain里要显示任务列表,每个任务栏都会有一个 checkbox 框和任务名称还有对应的删除按钮:
<template>
<div v-for="(item,index) in list" :key="index">
<div>
<input type="checkbox" v-model="item.complete">
{{item.title}}
<button>删除</button>
</div>
</div>
</template>
<script>
import {defineComponent,ref} from 'vue'
export default {
name: 'navMain',
setup(){
let list = ref([
{
title: '吃饭',
complete: false
},
{
title: '睡觉',
complete: false
},
{
title: '敲代码',
complete: false
},
])
}
}
</script>
<style scoped lang='less'>
</style>
在 navMain 里我们把数据以对象形式先存在数组里,这里我们后面会用reactive定义,先用ref这么做,complete是一个布尔值用来和 checkbox 绑定,选中时 complete就变为 true
下面完成NavFooter里面的数据 :
<template>
<div>
<div>已完成{{isComplete}} / 全部{{all}}</div>
<div v-if="isComplete>0">
<button>清除已完成</button>
</div>
</div>
</template>
<script>
import {defineComponent,ref} from 'vue'
export default {
name: 'navFooter',
setup(){
let isComplete = ref(1)
let all = ref(3)
return {
isComplete,
all
}
}
}
</script>
<style scoped lang='less'>
</style>
在NavFooter里我们主要显示todolist的底部,有个已完成多少任务 / 总任务的显示功能,我们这里给两个固定的数据用于演示,当 isComplete 就是已完成个数大于零时就可以点击清除已完成按钮来清除任务。
下面我们再到 App.vue 中把他给我们的这些样式删掉,变成这样:
然后保存,打开浏览器看看效果:
这样我们 完善了 todolist 的基本结构,往里加了每个组件需要的数据,但是目前我们的 todolist 还没有样式,后面我们还会继续完善。
6. 方法的定义和使用
我们在 setup 中分别用 ref 方法和 reactive 方法定义数据 num 和 num1:
setup(props,ctx) {
let num1 = ref(20)
let data = reactive({
num : 20
})
return{
num1,
...toRefs(data)
}
}
然后通过插值表达式写在 templete模板中:
<template>
<div>{{num}}</div>
<div>{{num1}}</div>
</template>
保存,打开浏览器查看输出:
页面输出了两个20,说明输出没有问题
现在我们分别给这两个div盒子绑定点击事件:
<template>
<div @click="clicknum">{{num}}</div>
<div @click="clicknum1">{{num1}}</div>
</template>
然后在 setup中声明函数:
setup(props,ctx) {
let num1 = ref(20)
let data = reactive({
num : 20
})
let clicknum = () => {
console.log('点击了num');
}
let clicknum1 = () => {
console.log('点击了num1');
}
return{
num1,
...toRefs(data),
clicknum,
clicknum1
}
}
保存,打开浏览器查看效果:
当我们点击上面这个20或者下面这个20时,控制台都会输出相应的内容。
那么定义完方法后我们又如何访问方法里的数据呢?
访问 ref 定义的数据,要写数据名.value
let clicknum1 = () => {
console.log(num1.value);
}
如果不加这个value,直接打印会输出什么呢?
控制台中会输出一个对象,所以我们访问 ref 定义的数据,一定要写数据名.value
访问 reactive 定义的数据,要写 reactive 定义的名字.输出的内容,在clicknum函数中这么输出num:
let clicknum = () => {
console.log(data.num);
}
这样我们就学会了如何定义方法、使用方法以及如何访问不同方法定义的数据。
7. 实现todolist每个组件需要的方法
在NavHeader里我们需要给input输入框绑定键盘事件,按下回车是触发事件的标志:
<input type="text" placeholder="请输入任务名称" v-model="value" @keydown.enter="enter">
在 setup 里定义这个 enter 事件,先把输入的内容打印出来,记得把方法放在 return 里:
setup(){
let value = ref('')
//按回车确认
let enter = () => {
console.log(value.value);
}
return {
value,
enter
}
}
保存,在浏览器中查看效果:
可以看到当我们按下回车后,控制台就输出了我们写在input框里的内容。这样这个键盘事件就绑定成功了。
在NavMain 里,做删除操作的button按钮,我们想让他不是一直显示的,只有当鼠标移动到当前任务下,才在旁边显示这个删除按钮。
我们通过css也能实现这个效果,先给div盒子和button按钮加个类名:
<template>
<div v-for="(item,index) in list" :key="index">
<div class="item">
<input type="checkbox" v-model="item.complete">
{{item.title}}
<button class="del">删除</button>
</div>
</div>
</template>
然后在NavMain的style里面写 item类和 del类的样式:
.item {
position: relative;
height: 35px;
line-height: 35px;
width: 170px;
cursor: pointer;
button {
position: absolute;
right: 5px;
top: 6px;
display: none;
background-color: rgb(220, 245, 252);
color: rgb(0, 169, 199);
border: 0;
z-index: 999;
}
&:hover {
background: rgb(149, 221, 255);
button {
display: block;
}
}
}
一开始默认按钮是隐藏的,当鼠标经过任务条时,显示删除按钮并且给当前任务条加个背景,看看效果:
接下来我们再给删除按钮添加点击事件,并且需要把参数传递给对应函数:
<button class="del" @click="del(item,index)">删除</button>
在setup中定义方法,并通过return返回出来:
let del = (item,index) => {
console.log(item.title);
console.log(index);
}
return {
list,
del
}
保存并在浏览器中查看控制台输出,当点击删除按钮时控制台会输出对应任务的名字和索引号:
然后我们再来到NavFooter里,先给外层盒子和按钮加上样式:
<template>
<div class="container">
<div>已完成{{isComplete}} / 全部{{all}}</div>
<div v-if="isComplete>0">
<button class="btn">清除已完成</button>
</div>
</div>
</template>
.container {
display: flex;
align-items: center;
.btn {
margin-left: 10px;
background-color: rgb(248, 230, 202);
color: rgb(216, 97, 0);
border: 0;
}
}
保存,打开浏览器查看效果:
这样他们就来到了一行上,清除已完成按钮也变成了我们想要的效果。
我们再给按钮添加点击事件:
<button class="btn" @click="clear">清除已完成</button>
在 setup 里定义clear方法,我们在clear方法里先做一个简单的输出就行:
setup(){
let isComplete = ref(1)
let all = ref(3)
//清除已完成
let clear = () => {
console.log('clear');
}
return {
isComplete,
all,
clear
}
}
保存,打开浏览器控制台,点击清除已完成按钮,查看输出:
控制台输出clear,说明我们的点击事件捆绑成功,这样我们就实现了 todolist 所有组件的方法。
8. vuex五大属性及使用方法
因为我们todolist案例所有的操作需要通过状态管理,也就是vuex来完成。
vuex是使用vue中必不可少的一部分,基于父子、兄弟组件,我们传值可能会很方便,但是如果是没有关联的组件之间要使用同一组数据,就显得很无能为力,那么vuex就很好的解决了我们这种问题,它相当于一个公共仓库,保存着所有组件都能共用的数据。
定义vuex在src目录下的 store 文件夹中,点开文件夹里的index.js文件:
这里先在 vuex 里把 createStore解构出来,createStore顾名思义就是创建一个状态管理。
state是用来定义所需要的状态的,如果我们在 state 中定义了一个 name,值是 jack:
state: {
name: 'jack',
userId: '123'
}
这相当于我们定义了一个name状态,值是jack,那么这个name在每一个组件当中就都可以使用了,这样就实现了数据共享。
geeter:从基本数据(state)派生的数据,相当于state的计算属性,具有返回值的方法:
getters: {
userIdDouble: function(state){
return state.userId * 2
}
}
mutations: mutations是用来同步修改state的,mutations里面都是方法:
mutations: {
setName(state,payload) {
state.name = payload
}
}
这里第一个参数就是 state,第二个参数是要修改的值。
action:和mutation的功能大致相同,不同之处在于 action 提交的是 mutation,而不是直接变更状态。 action 可以包含任意异步操作:
actions: {
asyncSetTime(store,params) {
setTimeout(() => {
store.commit('setName',params)
},2000)
}
}
第一个参数是 store,第二个参数是修改的值。commit是调用mutations的方法
modules: 主要是用来做模块化的,因为我们这个项目并不需要modules,所以这里不做过多讲解
9. 实现todolist的状态管理
在NavMain里我们定义了一个初始化的任务列表,根据我们上一节状态管理的内容,这个list列表需要放到state里来:
state: {
list: [
{
title: '吃饭',
complete: false
},
{
title: '睡觉',
complete: false
},
{
title: '敲代码',
complete: true
},
]
}
然后在mutations里我们定义操作list列表的方法,当我们在输入框中输入完任务按下回车,可以在下面的任务栏生成一条记录,这个方法就叫做addTodo:
addTodo(state,payload) {
state.list.push(payload)
}
第二个是当鼠标放在记录上时,点击删除按钮能够删除任务,这个方法就定义为delTodo:
delTodo(state,payload) {
state.list.splice(payload,1)
}
最后一个是清除已完成,当点击这个按钮的时候,能把已完成的任务记录删掉,这个方法定义为clear:
clear(state,payload) {
state.list = payload
}
这里传进来的是过滤之后的数组,也就是经过筛选后未完成的任务
注意:这三个方法里的payload含义都不一样,因为是形参。addTodo里的payload代表添加的任务。在delTodo里payload代表当前点击的任务的下标,那最后一个方法里的payload就是一个以及经过筛选后的数组。
10. 计算属性
我们先把HomeView中template模板里的内容注释掉:
因为要用到计算属性,所以就在vue中把computed引进来:
import {defineComponent,computed} from 'vue'
计算属性本质上就是一个变量,只不过这个变量是计算之后得到的。下面我们就用两数之和这个例子演示计算属性的用法,这里我们先引入ref方法。然后在setup中定义两个变量,并通过计算属性进行二者的求和
setup() {
let num1 = ref(10)
let num2 = ref(20)
let addNum = computed(() => {
return num1.value + num2.value
})
return {
num1,
num2,
addNum
}
}
注意:计算属性必须返回一个值
然后我们在template中把下面的代码加进去:
<div>
{{num1}} --- {{num2}}
两个数的和:{{addNum}}
</div>
保存,打开浏览器查看效果:
这样没有任何问题,我们再定义一个按钮来改变num1和num2的值,看看计算属性会不会相应变化:
<div>
<button @click="add">add</button>
</div>
这里我们给按钮添加一个点击事件,在add方法中实现点击按钮时num1和num2加一的操作:
let add = () => {
num1.value++
num2.value++
}
return {
num1,
num2,
addNum,
add
}
保存,打开浏览器查看效果:
改变num1和num2的值后,计算属性也会动态变化,这个方法在计算商城购物车总价时非常好用。
11. 通过计算属性获取到vuex定义的todolist的数据
在这一节中我们需要把vuex里state定义的任务列表数据通过计算属性引入HomeView.vue中。
首先从vuex中把useStore方法引入进来:
import {useStore} from 'vuex'
这个useStore会返回一个store对象,也就是我们在vuex中 通过createStore定义的对象,下面我们就在setup中使用useStore这个方法并结合计算属性去返回state中的数据
setup() {
let store = useStore()
let list = computed(() => {
return store.state.list
})
return {
list
}
}
这样我们就把vuex中state定义的列表数据获取到了,再到template模板中用插值表达式输出一下:
这样我们就通过vuex提供的一个api再结合计算属性就拿到了在state中定义的数据了。
三、todolist的逻辑实现 💨
1. router路由
在package.json中我们能看到,在创建项目的时候,不仅创建了vuex还创建了vue-router,vue-router就是路由,那路由又是什么呢?
我们可以把路由理解为一个容器,通常我们一个页面就代表了一个路由。在vue之中每个路由就对应着一个组件,这个组件我们就把他成为路由组件,路由组件是一个容器,一般还会把他拆分为各个子组件。所以每一个页面都是由一个路由组件和各个子组件构成的。
在src目录下的router就是我们用来配置路由的,我们打开router下的 index.js :
他从 vue-router引入了 createRouter和createHistory , createRouter就是用来创建路由的,通常路由分为两种模式,一种是hash模式,还有一种是history模式。createHistory就是创建一个history模式的路由。如果想创建 hash 模式的就写 createWebHashHistory,他俩什么区别我们后面会讲到。
这个数组就是路由的配置数组,数组里的元素都是一个个对象,每个对象里有两个属性是必填的,path 代表路由路径,它是自定义名字的但是必须以 ' / '开头。component代表对应的路由组件,这两个是必填的,还有个 name 他代表路由名字,这个是选填的。
在路由路径中如果只有一个斜杠,代表是首页,首页对应着是HomeView路由组件,所以在开头需要把它给引进来。而AboutView路由组件就不是这么引入的了,它是按需引入的:
在component里通过箭头函数把组件的路径引入进来,那什么是按需引入呢?就是如果没有访问 /about,那他就不会加载这个组件,这样用来节约性能。除了首页之外其他都可以按需引入。
然后就是创建路由对象部分:
在createWebHistory里传入的参数是环境配置的一个对象,这个我们不用关注他,可以删除也可以不删。
我们保存项目,在浏览器中打开,如果我们在主页面的路径后加上 /about ,他就会跳转到AboutView路由组件上:
我们在 views 目录下再新建一个DetailView.vue:
然后再把AboutView.vue里的内容复制过来一份,回到router下的index.js去配置路由信息:
{
path: '/detail',
name: 'Detail',
component: () => import('../views/DetailView.vue')
}
保存,打开浏览器,并且把路径改位detail:
<button @click="goto">跳转路由</button>
可以看到就跳转到了 detail 的页面,这说明detail的路由就配置成功了,这一小节我们学会了如何配置路由,以及配置文件里的选项都是什么意思。
2. 跳转路由
本小节我们来学习一下如何跳转路由,首先我们在 HomeView根组件中把 template 模板中的插值表达式注释起来,然后定义一个按钮用来实现单击他的时候跳转路由的效果:
<button @click="goto">跳转路由</button>
那我们到底如何去跳转呢?
首先,从 vue-router 中引入 useRouter 函数:
import {useRouter} from 'vue-router'
这个useRouter和我们之前讲过的useStore一样,都会返回一个对象:
let router = useRouter()
我们先用一个变量接收它,这里 router就代表全局路由对象,再在控制台中输出,看看它提供了什么api:
这些都是这个全局路由对象提供的方法
我们最常用的方法是 push ,通过 push 去跳转路由 ,push 里面可以直接传入要跳转的路径。
我们先把 setup 里原来的数据也注释,在这里定义 goto 方法,实现跳转到about组件:
setup() {
let router = useRouter()
let goto = () => {
router.push('/about')
}
return {
goto
}
}
现在我们点击按钮,就会执行 router.push,就会跳转到我们指定的页面
除了 push 方法外,router 还提供了很多其他方法,我们常用到的还有 back ,forward , go 这三个方法。back表示回到上一页,forward 表示去到下一页,go(整数),正数代表前进,负数代表后退,go(1)代表去到下一页,go(-2)代表回退到前一页的前一页。
刚刚我们实现了在根组件跳转到about组件,那么我们在about路由组件中再定义一个back方法:
<template>
<div class="about">
<h1>This is an about page</h1>
<button @click="back">回到首页</button>
</div>
</template>
<script>
import {defineComponent} from 'vue'
import {useRouter} from 'vue-router'
export default defineComponent({
name: 'About',
setup() {
let router = useRouter()
let back = () => {
router.back()
}
return {
back
}
}
})
</script>
在浏览器中打开,单击按钮看看能否退回主页:
除了用 router.back()外,通过 router.go(-1) 也能达到同样的效果。
3. 路由传参
如果我们想把 AboutView 路由组件的一些参数传递给DetailView组件的时候就需要进行路由传参了。
下面是 AboutView 里的内容,当点击按钮的时候会跳转到 DetailView 页面:
<template>
<div class="about">
<h1>This is an about page</h1>
<button @click="goto">去到detail页面</button>
</div>
</template>
<script>
import {defineComponent,ref} from 'vue'
import {useRouter} from 'vue-router'
export default defineComponent({
name: 'About',
setup() {
let name = ref('jack')
let num = ref(10)
let obj = ref({
msg : 'about'
})
let router = useRouter()
let goto = () => {
router.push('/detail')
}
return {
goto
}
}
})
</script>
我们想把在 setup 里面的 name , num , obj 都传递给 detail 路由组件,那到底怎么传递这个参数呢
push 方法还可以接收一个对象,如果 push 接收的是对象的形式那就可以传递参数。在这个对象里 path属性就是跳转路由的路径,query属性的值是一个对象,里面就存着我们想要传递的参数
let goto = () => {
router.push({
path: '/detail',
query: {
name: name.value,
num: num.value,
obj: JSON.stringify(obj)
}
})
}
注意:如果是普通数据类型直接传就可以,但是如果是引用数据类型,我们需要通过 JSON.stringify 先把对象转化为字符串,否则就是这种形式:[object,object]。
保存,打开 about 页面:
点击按钮,看看参数是如何传递到 detail 页面的:
在 detail 页面路径里的问号后面拼接着我们传递过去的参数,query 传参还有一个特点就是就算我们刷新页面地址栏里的参数还是原封不动在那里。
现在我们如何把地址栏里的参数接收回来呢?这又需要 vue-router 的另一个方法 useRoute ,我们把它引入进来:
import {useRouter,useRoute} from 'vue-router'
注意:这里我们操作的是 detail 页面。
前面我们学过通过 uesRouter 返回的对象是全局 路由对象,但是 useRoute 它返回的就是当前路由对象而非全局。
下面我们看一下 detail 页面的代码:
<template>
<div class="detail">
<h1>This is an detail page</h1>
</div>
</template>
<script>
import {defineComponent,ref} from 'vue'
import {useRouter,useRoute} from 'vue-router'
export default defineComponent({
name: 'Detail',
setup() {
let router = useRouter()
let route = useRoute()
console.log(route.query);
}
})
</script>
route.query就代表我们传递过来的参数,因为route就代表当前路由的对象
保存,打开浏览器先在 about 页面点击按钮跳转到 detail 页面传参然后打开控制台查看输出:
我们在 about 路由组件里想要传递的数据通过路由传参的方式就传递到指定的 detail 页面了。
有一点值得注意的是,query传递过来的参数都是字符串类型。
除了通过 query 传参外我们还可以通过 prarms 传参,用这种方法的特点是他传入的参数不会在地址栏上面显示,但是他也有一个小坑。
我们在 router.push 里除了通过 path 这个路由路径来找到路由外,还可以通过 name 来找到指定的路由组件,这个 name 就是我们在 index.js 里定义的路由名字。
那这个坑到底是啥呢?就是我们通过 query 传参的话,用 path 和 name 都能找到路由组件,但是通过 prarms 传参就只能用 name。
我们现在 about 组件里修改 setup,改为用 prarms 传参:
router.push({
name: 'Detail',
params: {
name: name.value,
num: num.value,
obj: JSON.stringify(obj.value)
}
})
在 detail 组件里再把 route.prarms输出:
export default defineComponent({
name: 'Detail',
setup() {
let router = useRouter()
let route = useRoute()
console.log(route.params);
}
})
保存,打开浏览器先在 about 页面点击按钮跳转到 detail 页面传参然后打开控制台查看输出:
可以看到通过 prarms 我们也把传递过来的参数输出了,并且地址栏并没有带参数。
4. 常用生命周期
在这一节我们会介绍一些常用的生命周期,setup 也是一个生命周期函数,他代表组件创建的过程,onmounted是最常用的生命周期函数,它代表组件挂载的过程,我们通过onmounted可以发送请求或者进行数据初始化等一些操作。
在上一节中,我们在 detail 里成功打印出来了传递过来的参数,onmounted可以帮助我们来接收路由传递过来的参数。
首先,从vue中把 onmounted 方法引入:
import {defineComponent,ref,onMounted} from 'vue'
在onmounted中接收从路由传递过来的参数:
export default defineComponent({
name: 'Detail',
setup() {
let router = useRouter()
let route = useRoute()
let num = ref(null)
let name = ref('')
let obj = ref({})
console.log(route.params);
onMounted(() => {
num.value = route.params.num*1
name.value = route.params.name
obj.value = JSON.parse(route.params.obj)
})
return {
num,
name,
obj
}
}
})
我们在 setup 里面定义了三个用来接收参数的变量,因为接收过来的参数都是字符串类型的,所以要转换成数值型需要*1,并且引用类型数据需要通过JSON.parse进行转换。
我们在 detail 的 template 模板里通过插值表达式输出接受过来的这三个参数:
<template>
<div class="detail">
<h1>This is an detail page</h1>
{{num}} --- {{name}} ---{{obj}}
</div>
保存,打开浏览器先在 about 页面点击按钮跳转到 detail 页面查看输出:
这样我们就成功接收过来了路由传递的参数
还有一个 onUnmounted 函数,他是组件卸载时的生命周期。首先,在 about 页面中从vue里引入这个方法:
import {defineComponent,ref,onUnmounted } from 'vue'
那我们知道它是组件卸载的生命周期,那啥时候组件会卸载呢?当跳转路由了当前这个组件就卸载了,我们来验证一下,在setup里加入这个生命周期:
onUnmounted (() => {
console.log('onUnmounted ');
})
保存,打开浏览器先在 about 页面点击按钮跳转到 detail 页面传参然后打开控制台查看输出:
当跳转路由时,这个生命周期成功执行了, 它是用来清除定时器以及清除闭包函数的,要不然会造成内存泄漏。
5. 父子组件传值
这里我们就以 detail 为父组件,重新修改一下父组件的代码:
<template>
<div class="detail">
<h1>这是父组件</h1>
</div>
</template>
<script>
import {defineComponent,ref} from 'vue'
export default defineComponent({
name: 'Detail',
setup() {
}
})
</script>
components是存子组件的目录,我们在里面再创建一个子组件 Child.vue,子组件内容与父组件一样,就把名字改了一下。
我们再把子组件引入到父组件中:
<template>
<div class="detail">
<h1>这是父组件</h1>
<child></child>
</div>
</template>
<script>
import {defineComponent,ref} from 'vue'
import child from '../components/child/Child.vue'
export default defineComponent({
name: 'Detail',
components: {
child
},
setup() {
}
})
</script>
保存,打开浏览器输入detail路由地址:
这样子组件就成功引入到父组件中了,下面我们就实现父子组件之间传值,首先实现父组件传值给子组件。首先在setup里定义数据:
setup() {
let msg = ref('这是父组件的数据')
return {
msg
}
}
父组件数据传递给子组件是通过动态绑定属性的方式:
<child :msg='msg'></child>
这里一般都是属性名和属性值都起一样的名字,冒号里的msg是我们return出来的数据,然后我们把这个数据命名为msg,这样就把父组件数据传递给子组件了。
那子组件如何接收父组件传递过来的数据呢?
子组件里的props属性是专门用来接收父组件传递过来的数据的,props里的属性名就是在父组件里动态绑定的数据名,也就是msg。
props : {
msg: {
type: String
}
}
这里type指数据类型校验,让传过来的数据只能是字符串类型,不能是其他类型。
现在我们接收到了父组件传递过来的数据,那我们怎么拿到这个msg呢?这个时候setup里的第一个参数就是我们的props,我们在setup里做一下打印看看会输出什么:
setup(props) {
console.log(props.msg);
}
保存,打开浏览器输入drtail路由地址打开控制台查看输出:
这样我们就成功拿到了父组件传递过来的数据,它可以直接在子组件的模板里用:
<template>
<div class="child">
<h1>这是子组件</h1>
{{msg}}
</div>
因为我们数据校验要求必须是字符串类型,所以如果传递的是数值类型就会报错。
在数据对象里还有两个可选的属性:required 和 default。required默认是false,如果你设置为true,且父组件没有动态绑定该属性的话,就会报错。default 指代默认值,如果父组件没有把数据传递过来,那子组件就会去找这个默认值。
现在我们学会了如何从父组件传值给子组件,那子组件如何传值给父组件呢?
我们先在子组件的setup里定义要传递的数据:
setup(props) {
let childMsg = ref('这是子组件的数据')
return {
childMsg
}
}
父组件传值给子组件通过props属性,而子组件传值给父组件是通过分发事件的方式进行的,我们先定义一个按钮,给他一个点击事件,实现点击按钮的时候就会传值给父组件:
<button @click="send">传值给父组件</button>
这里并不是通过send方法来分发事件,而是通过ctx.emit来分发事件:
setup(props,ctx) {
let childMsg = ref('这是子组件的数据')
let send = () => {
ctx.emit('a',childMsg.value)
}
return {
childMsg,
send
}
}
这里ctx就是setup方法的第二个参数,在 ctx.emit 里也有两个参数,第一个参数是事件名称,第二个参数是传递的数据。这里事件名称些写啥都可以。这样通过分发事件我们就把子组件的数据传递给父组件了,那父组件如何接收呢?
这里要通过在子组件的标签上绑定自定义事件的方法来接收数据:
<child :msg='msg' @a='over'></child>
这里的a就是我们在子组件中定义的事件名称,那个over就是我们父组件自己的方法,我们在setup里定义这个方法:
setup() {
let msg = ref('这是父组件的数据')
let over = (val) => {
console.log(val);
}
return {
msg,
over
}
}
保存,打开浏览器先在 detail 页面点击按钮然后打开控制台查看输出:
可以看到在控制台中成功输出了子组传递给父组件的数据。
如果要传递的数据很多怎么办,比如在子组件中再定义一个数据:
let childNum = ref(10)
在ctx.emit里只能写两个参数,所以你在后面再把这个数据写进去也没用,我们有两种办法传多个数据,可以把第二个参数变成一个数组,或者变成一个对象,这里就演示通过数组传递数据:
setup(props,ctx) {
let childMsg = ref('这是子组件的数据')
let childNum = ref(10)
let send = () => {
ctx.emit('a',[childMsg.value,childNum.value])
}
return {
childMsg,
send
}
}
保存,打开浏览器先在 detail 页面点击按钮然后打开控制台查看输出:
这样就实现了从子组件向父组件传递多个数据。
6. 实现todolist各个组件之间的参数传递
现在让我们把目光重新聚焦到todolist项目上,来到NavHeader中,根据上一节学到的知识,我们需要把input输入框内的值传递给 home 根组件:
setup(props,ctx){
let value = ref('')
//按回车确认
let enter = () => {
ctx.emit('add',value.value)
}
return {
value,
enter
}
}
现在再把子组件传递过来的数据接收过来:
<nav-header @add='add'></nav-header>
setup() {
let store = useStore()
let list = computed(() => {
return store.state.list
})
let value = ref('')
let add = (val) => {
value.value = val;
}
return {
list,
add,
value
}
}
这样NavHeader与根组件的参数传递就完成了,现在看NavMain,它的setup里定义了一个我们之前写死的list列表,这样写肯定不行,我们把数据全部删掉。这个list列表是定义在vuex的state里的,父组件把它引进来之后应该传给子组件。
<nav-main :list='list'></nav-main>
在 NavMain 中通过props属性接收父组件传过来的数据:
props : {
list : {
type:Array,
required:true
}
}
因为在NavMain中我们还有删除任务的方法,所以需要给父组件传递要删除的任务在列表里的索引号,先在子组件的setup里通过ctx.emit把索引号传给父组件:
setup(props,ctx){
//删除任务
let del = (item,index) => {
ctx.emit('del',index)
}
return {
del
}
}
父组件再接收传递过来的数据:
<nav-main :list='list' @del='del'></nav-main>
在setup里定义del方法,我们先实现单击删除能输出任务索引号:
let del = (val) => {
console.log(val);
}
注意要在return中把del方法返回,保存,打开浏览器点击删除按钮看看控制台是否会输出对应的索引号:
这样我们就完成了 NavMain子组件与父组件的传值
我们再来到 NavFooter ,他也需要拿到list列表数据,所以先给navfooter绑定上这个属性:
<nav-footer :list='list'></nav-footer>
然后再通过props把list传给NavFooter,这个操作和在 NavMain 中的一样。
接着把NavFooter里的all属性可以删掉,因为我们拿到了list,就可以用list.length代替:
<div>已完成{{isComplete}} / 全部{{list.length}}</div>
保存,在浏览器中打开查看效果:
我们再把state存储的list列表里的第三项注释:
再次保存,运行项目查看效果:
相应的数据都会动态变化,全部从3变成了2,这样我们就全部实现了各个组件间的数据传递
7. 完善todolist
我们所有的操作都在父组件里进行,子组件只负责传递数据,现在我们要实现把输入框内的内容加到任务栏主题里。
首先,在NavHeader中,我们把input框里输入的内容传递给了父组件,我们还要完成当按下回车后删除输入框里的内容的操作,那我们在enter事件函数中完善这个功能:
let enter = () => {
ctx.emit('add',value.value)
value.value = ''
}
在store的mutation里我们定义了三个方法:
下一步就是在我们的根组件里的add方法中调用这个mutations里的addTodo方法:
let add = (val) => {
value.value = val;
store.commit('addTodo',{
title: value.value,
complete: false
})
}
在这里我们通过store.commit来调用mutations里的方法。
保存,打开浏览器看看输入内容能否加到主体部分:
在输入框内输入123后,他就加到任务列表的最后了,但是这里还有个问题,如果我再次输入123的话,那任务列表当中就会有两个相同的任务,这显然不符合逻辑,那我们就需要加个判断:
let add = (val) => {
value.value = val;
let flag = true;
list.value.map((item) => {
if(item.title === value.value) {
flag = false;
alert('任务已存在')
}
})
if(flag) {
store.commit('addTodo',{
title: value.value,
complete: false
})
}
}
这样当我们输入相同任务名的时候,他就会弹窗提示我们,我们在浏览器中打开看看这个效果是否实现了:
现在添加功能就完成了,现在完善一下删除功能,在del方法中调用这个mutations里的delTodo方法:
let del = (val) => {
store.commit('delTodo',val)
}
现在我们这个删除功能就能正常使用了:
我们再完善一下,回到NavMain里给主体部分外面再套个盒子,加上 v-if 指令,实现清空任务栏的时候能够显示暂无任务这个效果:
<template>
<div v-if="list.length>0">
<div v-for="(item,index) in list" :key="index">
<div class="item">
<input type="checkbox" v-model="item.complete">
{{item.title}}
<button @click="del(item,index)">删除</button>
</div>
</div>
</div>
<div v-else>暂无任务</div>
</template>
保存,启动项目,看看刚刚这个效果有没有实现:
最后我们再去实现底下这个已完成的动态变化的效果,这里会用到计算属性computed,先把他引入进来:
import {defineComponent,ref,computed} from 'vue'
原来isComplete是一个定值,现在我们通过计算属性对他进行筛选然后返回新数组的长度就行:
let isComplete = computed(() => {
//过滤已完成的
let arr = props.list.filter(item => {
return item.complete===true
})
return arr.length
})
现在只要我们勾选完成按钮在已完成的数字上就会动态显示结果,因为complete数据是双向绑定的
现在我们要实现的功能就是单击清除已完成按钮实现对应的效果,修改NavFooter里的clear方法:
let clear = () => {
let arr = props.list.filter(item => {
return item.complete===false
})
ctx.emit('clear',arr)
}
在这里我们需要筛选出未完成的任务,因为我们在mutations定义的clear方法中的参数payload代表的就是筛选后的数组,所以我们得筛选出未完成的任务:
我们在根组件中再把筛选后的数据接收过来:
<nav-footer :list='list' @clear='clear'></nav-footer>
在根组件setup中定义这个clear方法,调用store.commit把mutations里的clear方法引进来:
let clear = (val) => {
store.commit('clear',val)
}
注意要在reuten里把clear返回。
我们看一下整体的效果:
这样整个todolist的功能就都完成了,我们再修改一下页面的样式,让他在页面中央:
看到这的小伙伴给个三连不过分吧😭😭😭