Vue3简单项目快速入门

275 阅读3分钟

本人已参与[新人创作礼]活动,一起开启掘金创作之路

很多前端人都做过todoList的案例,今天用vue3重写一个todoList小项目,目的是温习一下vue3的语法,快速入门

在这个案例里,能够接触到的知识点有:

  • setup语法糖的使用
  • ref、computed,reactive,torefs等api的使用
  • 父子之间传参
  • provide、inject祖孙组件之间传参
  • typescript语法的使用

Snipaste_2022-10-13_20-26-36.png

项目分析

* 项目可以有一个父组件包裹三个子组件
* 组件一:头部组件,用来输入用户的计划
* 组件二:内容组件,用来展示用户的计划内容
* 组件三:底部组件,用来统计用户计划总数和完成总数,附带计划圈选和清除完成计划功能

实现思路

* 通过vue3脚手架工具迅速创建一个vue3项目
* components中生成三个子组件
* 因为三个子组件都需要操作用户制定的计划,所以很容易想到,将该数据存储在父组件中,通过props分别向子组件中传入,各自组件通过修改该数据,从而重新渲染页面

具体实现

header组件实现

    * header组件内容为一个input输入框,用户输入具体的计划后,通过回车将数据添加到计划数组中
    * 因此需要在父组件中创建一个数据追加函数,并传给header组件,监听用户按下回车键的时候,触发追加,然后令input中内容清除
<template>
  <div class="header-wrap">
    <input type="text" v-model="todoItem" placeholder="输入待完成任务,按回车键添加" @keyup.enter="add" />
  </div>
</template>
<script lang="ts">
import { defineComponent, ref ,PropType} from 'vue'
import {Todo,addToPlanFn} from "@/types/todo"
export default defineComponent({
  name:'Header',
  props: {
    addToPlan: {
      type: Function as PropType<addToPlanFn>,
      required: true
    }
  },
  setup(props) {
    const todoItem = ref('')
    function add(){
      let value = todoItem.value
      if(!value.trim()) return 
      const todo:Todo = {
        id:Date.now(),
        title:value.trim(),
        isCompleted:false
      }
      props.addToPlan(todo)
      todoItem.value = ''
    }
    return {
      add,
      todoItem
    }
  },
})
</script>

<style scoped>
  .header-wrap input {
    width: 100%;
        padding: 5px;
        border-radius: 5px;
        outline: none;
        border: 1px solid lightblue;
        box-shadow: 1px 1px 1px rgba(0,0,0,.2);
  }
</style>
    * addToPlan为父组件传递过来的追加计划的函数,由于ts类型约束,不能简单将addToPlan归纳为Function类型,还需要更详细的描述,这里就需要给该函数定义一个接口,通过PropType泛型来描述该类型
    * 通过数据绑定拿到input后的数据后,监听用户回车,执行后构建一个Todo类型的数据,包含了用户的计划和时间等信息,追加到计划集合中
  • 在ts中每一种数据类型都会有检查和约束,因此在定义类和函数等类型时常需要为其定义一个接口,依次来约束和描述这种类型,在本项目中华,所有需要描述的类型定义在@/types/todo.ts文件中,后面不再重复叙述
//定义一个接口
export interface Todo {
  id:number,
  title:string,
  isCompleted:boolean
}

export interface delTodoFn {
  (index:number):void
}

export interface updateStateFn{
  (todoItem:Todo,val:boolean):void
}

export interface addToPlanFn {
  (todo:Todo):void
}

export interface clearCompletedFn {
  ():void
}

export interface clearAllFn {
  (val:boolean):void
}

内容组件的实现

  • 内容组件不仅用来展示数据,同时还能通过check框,来修改数据状态,表示其是否完成,同时通过删除操作,能够把数据删除
  • 内容组件List中每一条数据都是一个独立的类型,因此可以再为其添加一个子组件ListItem,List获取父组件的所有用户计划集合,然后循环ListItem
<template>
  <div class="todo-main">
    <ul class="todo-list">
      <TodoList
        v-for="(items, index) in todos"
        :todo-item="items"
        :index="index"
        :key="items.id"
      />
    </ul>
  </div>
</template>
<script lang="ts">
import { defineComponent } from "vue";
import TodoList from "./ListItem.vue";
export default defineComponent({
  name: "List",
  components: {
    TodoList,
  },
  props: ["todos"],

  setup() {
    return {};
  },
});
</script>
<style>
   .todo-list{
        width: 100%;
    }
</style>
  • ListItem组件负责修改其对应的数据,通过监视勾选框,修改用户计划的的完成状态,通过删除按钮,删除对用的数据,这两个操作通过inject获取最外围的父组件传入的函数来执行数据
<template>
  <li class="todo-item">
    <div>
      <input type="checkbox" v-model="isCompleted" />
      <span>{{ todoItem.title }}</span>
    </div>
    <button class="delBtn" @click="del(index)">删除</button>
  </li>
</template>
<script lang="ts">
import { defineComponent, inject, reactive } from "vue";
import { Todo ,delTodoFn,updateStateFn} from "@/types/todo";
import { computed } from "@vue/reactivity";
export default defineComponent({
  name: "ListItem",
  props: {
    todoItem: {
      type: Object as () => Todo,
      required: true,
    },
    index: {
      type: Number,
      required:true
    },
  },
  setup(props, context) {
    
    //从祖组件获取操作函数
    const delTodo: delTodoFn |undefined = inject("delTodo");
    const updateState: updateStateFn |undefined = inject("updateState");
    const del = (index: number) => {
      if (window.confirm("确认删除吗")) {
        if (typeof delTodo == "function") {
          delTodo(index);
        }
      }
    };
    const isCompleted = computed({
      get() {
        return props.todoItem.isCompleted;
      },
      set(val:boolean) {
        if (typeof updateState == "function") {
          updateState(props.todoItem, val);
        }
      },
    });
    return {
      isCompleted,
      del,
    };
  },
});
</script>
<style>
.todo-item {
        border: 1px solid lightblue;
        list-style: none;
        width: 100%;
        padding: 5px;
        margin-left: -40px;
        border-radius: 5px;
        display: flex;
        justify-content: space-between;
    }
    .delBtn {
        background-color: #e95b47;
        color: #fff;
        border-radius: 5px;
        border: none;
        display: none;
    }
    .todo-item:hover {
        color:lightcoral;
        background-color: rgba(137,190,78,.3);
    }
    .todo-item:hover .delBtn {
        display: block;
    }
</style>

底部组件

  • 底部组件需要统计计划总数及完成的计划数,因此需要父组件传入计划集合,同时有一个清除所有操作和清除已完成操作,需要父组件传入这两个函数
  • 全选操作:当用户把所有计划都完成后,该勾选框自动勾选,表示已全选,反之,当用户勾选这个框,所有的计划的勾选框相同改变
<template>
  <div class="todo-footer">
    <div>
      <label for="">
        <input type="checkbox" v-model="isCheckAll" />
      </label>
      <span class="todo-tag">
        <span>已完成 {{ count }}</span
        >/全部 {{ todos.length }}
      </span>
    </div>
    <button class="btn btn-danger" @click="clearCompleted">
      清除已完成任务
    </button>
  </div>
</template>
<script lang="ts">
import { defineComponent ,computed,PropType} from "vue";
import { Todo,clearCompletedFn,clearAllFn } from "@/types/todo";
export default defineComponent({
  name: "Footer",
  props: {
    todos: {
      type: Array as PropType<Todo[]>,
      required: true,
    },
    clearAll: {
      type: Function as PropType<clearAllFn>,
      required: true,
    },
    clearCompleted: {
      type: Function as PropType<clearCompletedFn>,
      reqquired: true,
    },
  },
  setup(props, context) {
    const count = computed(() => {
      return props.todos.filter((todo) => todo.isCompleted).length;
    });
    //全选是否为true
    const isCheckAll = computed({
      get() {
        return count.value>0 && count.value === props.todos.length
      },
      set(val:boolean){
        props.clearAll(val)
      }
    })

    return {
      count,
      isCheckAll
    }
  },
});
</script>
<style scoped>
    .todo-footer {
        display: flex;
        justify-content: space-between;
    }
    .btn-danger {
        color: #fff;
        background-color: #e95b47;
        border: none;
        border-radius: 5px;
        padding: 5px;
        cursor: pointer;
    }
    .todo-tag {
        margin-left: 15px;
    }
</style>

父组件实现

  • 父组件包含数据集合以及所有和数据相关的操作函数
<template>
 <div class="todo-wrap">
    <h2>todoList</h2>
    <Header :addToPlan="addToPlan"/>
    <List :todos="todos"/>
    <Footer :todos="todos" :clearCompleted="clearCompleted" :clearAll="clearAll"/>
 </div>
</template>

<script lang="ts">

import {  defineComponent, provide, reactive, toRef, toRefs} from 'vue';
import Header from '@/components/Header.vue'
import Footer from '@/components/Footer.vue'
import List from '@/components/List.vue'
import { addToPlanFn, Todo,delTodoFn,updateStateFn } from '@/types/todo'
export default defineComponent({
  name: 'App',
  components:{
    Header,
    Footer,
    List
  },
  setup(props){
    const state = reactive<{todos:Todo[]}>({
      todos:[]
    })
    const addToPlan:addToPlanFn = (todo:Todo)=>{
      state.todos.unshift(todo)
    }
    const clearCompleted =()=>{
     state.todos =  state.todos.filter((todo)=>{
        if(todo.isCompleted){
          return false
        }
        return true
      })
    }
    const clearAll = (val:boolean) => {
      state.todos.forEach(todo => {
        todo.isCompleted = val
      })
    }
    const delTodo = (index:number)=>{
      state.todos.splice(index,1)
    }
    const updateState = (todo:Todo,val:boolean)=>{
      todo.isCompleted = val
    }
    provide('delTodo',delTodo)
    provide('updateState',updateState)
   return {
    ...toRefs(state),
      addToPlan,
      clearCompleted,
      clearAll 
   }
  }
})
</script>

<style >
  .todo-wrap {
    width: 500px;
    margin: 0 auto;
    border: 1px solid lightblue;
    border-radius: 5px;
    padding: 20px;
  }
  .todo-wrap h2 {
    text-align: center;
  }
</style>