使用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>
posted @ 2020-11-30 18:38  OwenLin  阅读(519)  评论(2)    收藏  举报