使用Vue3开发TodoMVC
最近在学习Vue3.0的一些新特性,就想着使用Vue3来编写一个todoMVC的示例。本示例是模仿官网的TodoMVC,但是本示例中所有代码都是使用了Vue3的语法。
功能上基本上实现了,不过官方的示例上使用了Local Storage本地缓存来缓存数据,我在本示例中没有使用。另外ui样式我没有完全还原,也算是偷下懒吧。
官网示例:https://cn.vuejs.org/v2/examples/todomvc.html
先来看一下效果

开发中的几个问题
主要用到Vue3的conposition API有:ref, reactive, computed, watchEffect, watch, toRefs, nextTick,
功能我就不细讲了,后面会附上完整代码,主要讲几点在开发过程中遇到的问题,也是Vue3中的一些小改动的问题。
1.列表item的input输入框自动获取焦点
在官网示例中是使用了自定义指令去完成的,先自己定义一个自定义指令,之后再在input标签中去使用
<input
class="edit"
type="text"
v-model="todo.title"
v-todo-focus="todo == editedTodo"
@blur="doneEdit(todo)"
@keyup.enter="doneEdit(todo)"
@keyup.esc="cancelEdit(todo)"
/>
directives: {
"todo-focus": function(el, binding) {
if (binding.value) {
el.focus();
}
}
}
而我在本例中想使用ref来获取dom元素,从而触发input的onfocus事件。
我们知道在Vue2.x中可以使用this.$refs.xxx来获取到对应的dom元素,可是Vue3.0中是没办法使用这种方法去获取的。
查阅了Vue3.0官方文档之后,发现Vue3对ref的使用做了修改。
- Vue2:
<div ref="myRef"></div>
<script>
this.$refs.myRef
</script>
- Vue3:
<template>
<div ref="myRef">获取DOM元素</div>
</template>
<script>
import { ref, onMounted, nextTick } from 'vue';
export default {
//方式1:
setup() {
const myRef = ref(null);
onMounted(() => {
console.dir(myRef);
})
return {
myRef
};
}
//方式2:
setup() {
let myRef = '';
const setRef = el => {
myRef = el;
}
nextTick(() => {
console.dir(myRef);
})
return {
setRef
};
}
};
</script>
2. 在v-for中获取ref
而对在v-for中使用ref,Vue3不再在 $ref 中自动创建数组,而是需要用一个函数来绑定。(参考文档:https://composition-api.vuejs.org/zh/api.html#模板-refs)
<div v-for="item in list" :ref="setItemRef"></div>
//Vue2
export default {
data() {
return {
itemRefs: []
}
},
methods: {
setItemRef(el) {
this.itemRefs.push(el)
}
}
}
//Vue3
import { ref } from 'vue'
export default {
setup() {
let itemRefs = []
const setItemRef = el => {
itemRefs.push(el)
}
onBeforeUpdate(() => {
itemRefs = []
})
onUpdated(() => {
console.log(itemRefs)
})
return {
itemRefs,
setItemRef
}
}
}
在本例中使用了另一种写法,也是一样。
<input
v-show="item.isEdit"
class="edit-input"
:ref="(el) => (editRefList[item.id] = el)"
type="text"
v-model="itemInputValue"
@blur="handleBlur(item)"
/>
setup() {
const editRefList = ref([]);
watchEffect(async () => {
if (state.itemInputValue) {
await nextTick();
editRefList.value[state.currentTodoId].focus();
}
});
return {editRefList}
}
3. nextTick的使用
在Vue2中我们会这样使用nextTick
this.$nextTick(()=> {
//获取更新后的DOM
})
而在Vue3中这样使用
import { createApp, nextTick } from 'vue'
const app = createApp({
setup() {
const message = ref('Hello!')
const changeMessage = async newMessage => {
message.value = newMessage
// 这里获取DOM的value是旧值
await nextTick()
// nextTick 后获取DOM的value是更新后的值
console.log('Now DOM is updated')
}
}
})
4. watchEffect 和 watch
(1)watchEffect
vue3中新增了watchEffect的方法,也是可以用来监听数据。watchEffect() 会立即执行传入的函数,并响应式侦听其依赖,并在其依赖变更时重新运行该函数。
- 基本用法
const count = ref(0)
// 初次直接执行,打印出 0
watchEffect(() => console.log(count.value))
setTimeout(() => {
// 被侦听的数据发生变化,触发函数打印出 1
count.value++
}, 1000)
- 停止侦听
watchEffect() 使用时返回一个函数,当执行这个返回的函数时,就停止侦听。
const stop = watchEffect(() => {
/* ... */
})
// 停止侦听
stop()
(2)watch
watch的写法与vue2稍稍有点不同
watch侦听单个数据源
侦听的数据可以是个 reactive 创建出的响应式数据(拥有返回值的 getter 函数),也可以是个 ref
watch接收三个参数:
参数1:监听的数据源,可以是一个ref获取是一个函数
参数2:回调函数(val, oldVal)=> {}
参数3:额外的配置 是一个是对象时进行深度监听,添加 { deep:true, immediate: true}
// 侦听一个 getter
const state = reactive({ count: 0 })
watch(
() => state.count,
(count, prevCount) => {
/* ... */
},
{ deep:true, immediate: true}
)
// 直接侦听一个 ref
const count = ref(0)
watch(count, (count, prevCount) => {
/* ... */
})
watch侦听多个数据源
在侦听多个数据源时,把参数以数组的形式给 watch
watch([ref1, ref2], ([newRef1, newRef2], [prevRef1, prevRef2]) => {
/* ... */
})
最后
本例也是自己刚接触vue3之后写的,可能写的并不是很好,如果有哪里有错误或者可优化的请多多指导。
完整代码
<template>
<div class="todo-list">
<h1>todos</h1>
<div class="todo-list-content">
<section class="content-input-box">
<input
class="todo-input"
type="text"
autofocus
autocomplete="off"
placeholder="What needs to be done?"
v-model="inputValue"
@keyup.enter="handleAddTodo($event.target)"
/>
</section>
<input
v-show="todoList.length"
type="checkbox"
v-model="isSelectAll"
class="toggle-all"
:class="{'toggle-all-active': isSelectAll }"
/>
<ul class="content" v-show="filterTodoList.length">
<li
class="list-item"
@mouseenter="mouseEnter(item)"
@mouseleave="mouseLeave(item)"
v-for="(item,index) in filterTodoList"
:key="item.id"
>
<div v-show="!item.isEdit" class="list-item-box">
<input
class="checkbox"
type="checkbox"
:checked="item.isCompleted"
@change="handleChangeCheckbox(item)"
/>
<div
:class="[{ complete: item.isCompleted }, 'text']"
@dblclick="dbClick(item)"
>
{{ item.content }}
</div>
<span
v-show="item.isActive && !item.isEdit"
class="delete-icon"
@click="handleDelete(index)"
>X</span
>
</div>
<!-- 使用v-for循环时, 使用ref总会获取到的是最后的元素, 必须使用函数, 手动赋值 -->
<input
v-show="item.isEdit"
class="edit-input"
:ref="(el) => (editRefList[item.id] = el)"
type="text"
v-model="itemInputValue"
@blur="handleBlur(item)"
/>
</li>
</ul>
<section class="footer" v-show="todoList.length">
<span>{{ isActiveTodos.length }} items left</span>
<div class="status-buttons">
<button
:class="{ 'active-status-button': status === button }"
v-for="(button, index) in statusButtons"
@click="handleChange(button)"
:key="index"
>
{{ button }}
</button>
</div>
<p
v-show="isCompletedTodos.length"
class="clear-button"
@click="handleClear"
>
Clear completed
</p>
</section>
</div>
</div>
</template>
<script lang="ts">
import {
ref,
reactive,
computed,
watchEffect,
watch,
toRefs,
nextTick,
} from "vue";
export default {
name: "todoList",
setup() {
const state = reactive({
inputValue: "",
todoList: [],
itemInputValue: "",
todoId: 0,
currentTodoId: 0,
status: "All",
statusButtons: ["All", "Active", "Completed"],
});
// 自动获取input焦点
// 因为在循环里,所以要定义一个ref数组,然后根据id来获取当前input的焦点
const editRefList = ref([]);
watchEffect(async () => {
if (state.itemInputValue) {
await nextTick();
editRefList.value[state.currentTodoId].focus();
}
});
//或者用watch也可以
// watch(
// () => state.itemInputValue,
// async (val) => {
// if(val) {
// await nextTick();
// editRefList.value[state.currentTodoId].focus();
// }
// },
// {
// immediate: true
// }
// );
//vue3.0去除了filter过滤器,官方建议用计算属性或方法代替过滤器。
const filterTodoList = computed(() => {
switch (state.status) {
case "All":
return state.todoList;
break;
case "Active":
return isActiveTodos.value;
break;
case "Completed":
return isCompletedTodos.value;
break;
}
});
const isActiveTodos = computed(() =>
state.todoList.filter((item) => !item.isCompleted)
);
const isCompletedTodos = computed(() =>
state.todoList.filter((item) => item.isCompleted)
);
const isSelectAll = computed({
get: () => isActiveTodos.value.length === 0 && !!state.todoList.length,
set: (val) => {
state.todoList.forEach((todo) => {
todo.isCompleted = val;
});
},
});
// 添加todo
const handleAddTodo = (e) => {
//如果输入内容为空则立即返回
if (e.value === "") {
return;
}
state.todoList.push({
id: state.todoId++,
content: state.inputValue,
isCompleted: false, //是否已完成
isActive: false, //是否正在进行
isEdit: false, //是否在编辑状态
});
state.inputValue = "";
};
// 删除单条
const handleDelete = (index) => {
state.todoList.splice(index, 1);
};
// 鼠标进入
const mouseEnter = (item) => {
item.isActive = true;
};
// 点击按钮改变todoList显示
const handleChange = (status) => {
state.status = status;
};
// 清空completed状态的todo
const handleClear = () => {
state.todoList = isActiveTodos.value;
};
// 鼠标移出
const mouseLeave = (item) => {
item.isActive = false;
};
// 双击item编辑
const dbClick = (item) => {
state.itemInputValue = item.content;
state.currentTodoId = item.id;
item.isEdit = true;
};
// 失焦事件
const handleBlur = (item) => {
item.content = state.itemInputValue;
item.isEdit = false;
state.itemInputValue = "";
};
// 点击checkbox切换状态
const handleChangeCheckbox = (item) => {
item.isCompleted = !item.isCompleted;
};
return {
...toRefs(state),
handleAddTodo,
handleDelete,
handleClear,
handleChangeCheckbox,
mouseLeave,
mouseEnter,
dbClick,
handleBlur,
editRefList,
isActiveTodos,
isCompletedTodos,
handleChange,
filterTodoList,
isSelectAll,
};
},
};
</script>
<style>
* {
padding: 0;
margin: 0;
box-sizing: border-box;
}
input {
outline: none;
}
ul,
li,
ol {
list-style: none;
}
::-webkit-input-placeholder {
color: #d5d5d5;
font-size: 25px;
}
.todo-list {
display: flex;
flex-direction: column;
align-items: center;
background-color: #f5f5f5;
width: 100%;
height: 500px;
}
h1 {
margin: 10px;
font-size: 100px;
color: rgba(175, 47, 47, 0.15);
}
/* content部分样式 */
.todo-list .todo-list-content {
position: relative;
width: 600px;
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 0 25px 50px 0 rgba(0, 0, 0, 0.1);
}
.todo-list-content .content-input-box {
display: flex;
align-items: center;
box-shadow: inset 0 -2px 1px rgba(0, 0, 0, 0.03);
}
.toggle-all {
position: absolute;
left: 42px;
top: 27px;
width: 0px;
height: 0px;
transform: rotate(90deg);
cursor: pointer;
}
.toggle-all:before {
content: "❯";
font-size: 22px;
color: #e6e6e6;
}
.toggle-all-active:before {
color: #737373;
}
.todo-list-content .todo-input {
font-size: 24px;
width: 100%;
padding: 16px 16px 16px 60px;
border: 1px solid transparent;
background: rgba(0, 0, 0, 0.003);
}
.content .list-item {
width: 100%;
display: flex;
justify-content: space-between;
align-items: center;
font-size: 24px;
border-bottom: 1px solid #ececec;
}
.list-item .edit-input {
width: 100%;
padding: 16px;
margin-left: 42px;
font-size: 24px;
box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2);
border: 1px solid #999;
}
.list-item .list-item-box {
display: flex;
flex-direction: row;
align-items: center;
width: 100%;
padding: 16px;
}
.list-item .checkbox {
cursor: pointer;
width: 20px;
height: 20px;
}
.list-item .text {
margin-left: 30px;
width: 100%;
text-align: left;
}
.list-item .delete-icon {
color: red;
cursor: pointer;
}
.complete {
color: #d9d9d9;
text-decoration: line-through;
}
/* footer部分样式 */
.footer {
padding: 12px 15px;
display: flex;
justify-content: space-between;
}
.footer .status-buttons {
position: absolute;
left: 50%;
transform: translateX(-50%);
}
.footer .status-buttons button {
padding: 2px 8px;
margin-left: 5px;
}
.footer .clear-button {
cursor: pointer;
}
.active-status-button {
background-color: #777;
outline: -webkit-focus-ring-color auto 1px;
}
</style>

浙公网安备 33010602011771号