首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >200行纯前端Vue代码!教你写一个专属TodoList【零基础友好】

200行纯前端Vue代码!教你写一个专属TodoList【零基础友好】

作者头像
Wu_Candy
发布2022-07-05 14:52:22
发布2022-07-05 14:52:22
2K1
举报
文章被收录于专栏:无量测试之道无量测试之道
1. 前言

身为互联网农民工的我们,提起 Todo List 大家肯定一点都不陌生,因为 Todo List 是一个圈内比较知名的案例,可以通过 Java, Python 等后端语言再以少量的前端 Html 语言辅助实现整个 Todo List 的增、删、改、查等功能。

今天分享的 Todo List 案例与常见的实现方式不太一样,因为今天分享的案例是由纯前端代码 Vue 组件化来实现的,完全没有后端语言的支撑,也能实现 Todo List 功能的动态效果。

Vue.js 是什么

Vue (读音 /vjuː/,类似于 view) 是一套用于构建用户界面的渐进式框架。与其它大型框架不同的是,Vue 被设计为可以自底向上逐层应用。Vue 的核心库只关注视图层,不仅易于上手,还便于与第三方库或既有项目整合。另一方面,当与现代化的工具链以及各种支持类库结合使用时,Vue 也完全能够为复杂的单页应用提供驱动。

这是 Vue 官网对 Vue 的介绍,感兴趣的小伙伴可以去 Vue 官网(https://cn.vuejs.org/v2/guide/)了解更多。

2. 动态效果图

http://mpvideo.qpic.cn/0bc3oeaacaaasmah7t5e3rrfa4odafyqaaia.f10002.mp4?

3. 框架结构

下图是实现效果图:

从效果图分析进行组件结构拆分,如下图所示。

效果图与组件结构图之间的序号是一一对应的,可以对比查看便于更好地理解。

(1). App.vue组件: 所有组件的父组件

(2). ListHeader.vue组件: 管理 input 输入框模块

(3). AllList.vue组件: 负责展示所有待办事项,即每一条 item

(4). ListItem.vue组件: 管理每一条 item 的具体内容及 checkbox 的勾选状态

(5). ListFooter.vue组件: 负责展示已完成和所有待办事项总数及删除所有已完成状态的 item 功能

4. Vue 代码实现
(1). AllList.vue组件
代码语言:javascript
复制
<template>
    <ul class="todo-main">
        <!--给子组件ListItem绑定事件,例如:todo,checkTodo,在ListItem组件中需要使用props进行声明接收后即可使用-->
        <ListItem
            v-for="todoObj in todos"
            :key="todoObj.id"
            :todo="todoObj"
            :checkTodo="checkTodo"
            :deleteTodo="deleteTodo"
        />
    </ul>
</template>

<script>
    //引入ListItem组件
    import ListItem from "./ListItem"
    export default {
        //给组件命名为AllList
        name:'AllList',
        //在当前组件内注册子组件: ListItem
        components:{ListItem},
        //props用于接收App组件传递过来的参数
        props:['todos','checkTodo','deleteTodo']
    }
</script>

<style scoped>
    .todo-main {
        margin-left: 0px;
        border: 1px solid #dddddd;
        border-radius: 2px;
        padding: 0px;
    }

    .todo-empty {
        height: 40px;
        line-height: 40px;
        border: 1px solid #dddddd;
        border-radius: 2px;
        padding-left: 5px;
        margin-top: 10px;
    }
</style>
(2). ListFooter.vue组件
代码语言:javascript
复制
<template>
    <div class="todo-footer" v-show="total">
        <label>
            <!-- v-model表示双向绑定,页面操作控制底层数据,底层数据变更影响页面展示 -->
            <input type="checkbox" v-model="isAll"/>
        </label>
        <span>
            <span>已完成:{{doneTotal}}</span> /全部:{{total}}
        </span>
        <!-- 绑定click事件调用clearAll方法用于删除已完成任务 -->
        <button class="btn btn-danger" @click="clearAll">删除已完成任务</button>
    </div>
</template>

<script>
    export default {
        //给当前组件命名为: ListFooter
        name:'ListFooter',
        //使用props进行声明接收父组件App里的数据与方法
        props:['todos','checkAllTodo','clearAllTodo'],
        computed:{
            total(){
                //返回整个todo列表的长度
                return this.todos.length
            },
            doneTotal(){
                //统计list列表中当前状态下done:true的总数和
                return this.todos.reduce((pre,todo)=> pre + (todo.done ? 1:0),0)
            },
            isAll: {
                //当所有item的done为true时,即所有事项已完成且当前列表至少有1条item,则默认自动勾选中底部的checkbox
                get(){
                    return this.doneTotal === this.total && this.total > 0
                },
                //获取底部的checkbox是选中还是未选中,然后给App里的每条item的done字段进行赋值为:true或false(以此控制每条item是选中还是未选中)
                set(value){
                    this.checkAllTodo(value)
                }
            }
        },
        methods:{
            //回调父组件App提供的删除所有已完成待办事项的方法,具体实现在App组件的clearAllTodo方法里
            clearAll(){
                this.clearAllTodo()
            }
        }
    }
</script>

<style scoped>
    .todo-footer {
        height: 40px;
        line-height: 40px;
        padding-left: 6px;
        margin-top: 5px;
    }

    .todo-footer label {
        display: inline-block;
        margin-right: 20px;
        cursor: pointer;
    }

    .todo-footer label input {
        position: relative;
        top: -1px;
        vertical-align: middle;
        margin-right: 5px;
    }

    .todo-footer button {
        float: right;
        margin-top: 5px;
    }
</style>
(3). ListHeader.vue组件
代码语言:javascript
复制
<template>
    <div class="todo-header">
        <h2>今日待办事项</h2>
        <input type="text" placeholder="请输入你的任务名称,按回车键确认" v-model='title' @keyup.enter="add"/>
    </div>
</template>

<script>
    //引入nanoid库用于每条item添加时的id值
    import {nanoid} from 'nanoid'
    export default {
        //给当前组件命名为: ListHeader
        name:'ListHeader',
        //使用props声明接收父组件App里的addTodo方法
        props:['addTodo'],
        data(){
            return {
                title: ''
            }
        },
        methods: {
            add(){
                //校验数据
                if(!this.title.trim()) return alert('输入不能为空')
                //将用户的输入包装成一个todo对象
                const todoObj = {id:nanoid(), title:this.title, done:false}
                //通知App组件去添加一个todo对象
                this.addTodo(todoObj)
                //清空输入
                this.title = ''
            }
        }
    }
</script>

<style scoped>
    .todo-header input {
        width: 560px;
        height: 28px;
        font-size: 14px;
        border: 1px solid #cccccc;
        border-radius: 4px;
        padding: 4px 7px;
    }

    .todo-header input:focus {
        outline: none;
        border-color: rgba(82, 168, 236, 0.8);
        box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(82, 168, 236, 0.8);
    }
</style>
(4). ListItem.vue组件
代码语言:javascript
复制
<template>
    <li>
        <label>
            <!--
            1.初始化时: 绑定checked事件,如果为true时则勾选中checkbox,反之不勾选中checkbox
            2.数据变更时: 绑定change事件,调用handleCheck方法并传入当前item的所属id值来操作是取消还是选中checkbox的勾选
            -->
            <input type="checkbox" :checked="todo.done" @change="handleCheck(todo.id)"/>
            <span>{{todo.title}}</span>
        </label>
        <!-- 给每条item的删除按钮绑定click点击事件,通知App组件中将对应的todo进行删除操作 -->
        <button class="btn btn-danger" @click="handleDelete(todo.id)">删除</button>
    </li>
</template>

<script>
    export default{
        //给当前组件命名为: ListItem
        name:'ListItem',
        //使用props声明接收父组件AllList里的todo对象和App组件里的checkTodo,deleteTodo方法
        props:['todo','checkTodo','deleteTodo'],
        methods: {
            //勾选or取消勾选
            handleCheck(id) {
                //通知App组件将对应的todo对象的done值取反
                this.checkTodo(id)
            },
            //删除
            handleDelete(id){
                if(confirm('确认删除吗?')){
                    //通知App组件将对应的todo对象删除掉
                    this.deleteTodo(id)
                }
            }
        }
    }
</script>

<style scoped>
    li {
        list-style: none;
        height: 36px;
        line-height: 36px;
        padding: 0 5px;
        border-bottom: 1px solid #dddddd;
    }

    li label {
        float: left;
        cursor: pointer;
    }

    li label li input {
        vertical-align: middle;
        margin-right: 6px;
        position: relative;
        top: -1px;
    }

    li button {
        float: right;
        display: none;
        margin-top: 3px;
    }

    li:before {
        content: initial;
    }

    li:last-child {
        border-bottom: none;
    }

    li:hover {
        background-color: lightskyblue;
    }

    li:hover button {
        display: block;
    }
</style>
(5). App.vue组件
代码语言:javascript
复制
<template>
    <div id="root">
        <div class="todo-container">
            <div class="todo-wrap">
                <!--
                1.给ListHeader,AllList,ListFooter子组件绑定属性
                2.在子组件中使用props进行声明接收就可以调用本组件定义的方法和数据了
                -->
                <ListHeader :addTodo="addTodo"/>
                <AllList :todos="todos" :checkTodo="checkTodo" :deleteTodo="deleteTodo"/>
                <ListFooter :todos="todos" :checkAllTodo="checkAllTodo" :clearAllTodo="clearAllTodo"/>
            </div>
        </div>
    </div>
</template>

<script>
    //引入子组件
    import ListHeader from "./components/ListHeader"
    import AllList from "./components/AllList"
    import ListFooter from "./components/ListFooter"

    export default {
        //给当前组件命名为:App
        name:'App',
        //注册引入的子组件
        components:{ListHeader, AllList, ListFooter},
        //初始化的todo list数据
        data(){
            return {
                todos:[
                    {id:'001', title:'读文学书', done:true},
                    {id:'002', title:'解函数', done:false},
                    {id:'003', title:'上瑜珈课', done:false},
                    {id:'004', title:'研究三角函数', done:false},

                ]
            }
        },
        methods: {
            //添加一个todo
            addTodo(todoObj){
                this.todos.unshift(todoObj)
            },
            //勾选or取消勾选一个todo
            checkTodo(id){
                this.todos.forEach((todo)=>{
                    if(todo.id === id) todo.done = !todo.done
                })
            },
            //删除一个todo
            deleteTodo(id){
                //精简写法
                this.todos = this.todos.filter( todo => todo.id !== id )
            },
            //全选or取消全选
            checkAllTodo(done){
                this.todos.forEach((todo)=>{
                    todo.done = done
                })
            },
            //清除所有已经完成的todo,保留done为false的todo
            clearAllTodo(){
                this.todos = this.todos.filter((todo)=>{
                    return !todo.done
                })
            }
        }
    }
</script>

<style>
    body {
        background: #ffffff;
    }

    .btn {
        display: inline-block;
        padding: 4px 12px;
        margin-bottom: 0;
        font-size: 14px;
        line-height: 20px;
        text-align: center;
        vertical-align: middle;
        cursor: pointer;
        box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0);
        border-radius: 4px;
    }

    .btn-danger {
        color: #ffffff;
        background-color: #da4f49;
        border: 1px solid #bd362f;
    }

    .btn-danger:hover {
        color: #ffffff;
        background-color: #bd362f;
    }

    .btn:focus {
        outline: none;
    }

    .todo-container {
        width: 600px;
        margin: 0 auto;
    }

    .todo-container .todo-wrap {
        padding: 10px;
        border: 1px solid #dddddd;
        border-radius: 5px;
    }
</style>
(6). main.js入口文件
代码语言:javascript
复制
//引入Vue
import Vue from 'vue'
//引入App
import App from './App.vue'
//关闭Vue的生产提示
Vue.config.productionTip = false

//创建vm
new Vue({
    el:'#app',
    render: h => h(App)
})
(7). index.html网页展示文件
代码语言:javascript
复制
<!DOCTYPE html>
<html lang="">
  <head>
    <meta charset="utf-8">
    <!-- 针对IE浏览器的一个特殊配置,含义是让IE浏览器以最高的渲染级别渲染页面 -->
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <!-- 开启移动端的理想视口 -->
    <meta name="viewport" content="width=device-width,initial-scale=1.0">
    <!-- 配置页签图标 -->
    <link rel="icon" href="<%= BASE_URL %>favicon.ico">
    <!-- 配置网页标题 -->
    <title><%= htmlWebpackPlugin.options.title %></title>
  </head>

  <body>
    <!-- 当浏览器不支持js时noscript中的元素就会被渲染 -->
    <noscript>
      <strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
    </noscript>
    <!-- 容器 -->
    <div id="app"></div>
    <!-- built files will be auto injected -->
  </body>

</html>

这个文件中的核心代码只有一行(倒数第3行):

代码语言:javascript
复制
 <div id="app"></div>
5. 总结

(1). 以上7个文件的源码都有对代码实现功能进行详细的解释说明,请注意查看。

(2). 使用 Vue 组件化编码时,应注意以下三点:

1). 静态组件要按照功能点进行拆分。

例如:

ListHeader.vue 组件负责管理 input 输入功能, 可以提取为一个单独的组件, 组件命名时最好是见名知意。

2). 实现动态组件时要考虑好数据的存放位置。

如果数据是一个组件在使用, 则可直接放在当前使用的组件里即可;

如果是多个组件都在使用共同的数据, 则应该把数据放在多个组件所属的父组件里, 这样有利于组件间数据的调用。

例如:

这里的 Todo 列表数据, 因为 ListHeader.vue 组件会通过输入新增待办事项来改变 Todo 列表数据。

AllList.vue 组件需要展示所有待办事项, 也用到了 Todo 列表数据, 因此这份数据应该放在这两个组件所属的父组件 App.vue 里。

3). 组件间交互使用绑定事件来实现。

例如:

App.vue 组件里的这段代码, 使用 :addTodo 的方式给子组件 ListHeader.vue 绑定: 添加todo的事件

(3). props属性

1). 父组件传递数据给子组件:

父组件中绑定事件, 子组件使用 props 声明接收后使用。

2). 子组件传递数据给父组件:

父组件中绑定事件, 子组件使用 props 声明接收后再调用父组件中定义好的函数进行回调,将数据传递给父组件。

例如:

ListItem.vue 组件中的这段代码

鼓励大家亲自动手实现一下 Todo List 案例,无论开发还是测试开发,Vue.js 作为优秀的前端框架都值得一学。

代码注释较为详细,在实践过程中,可以仔细阅读以便于加深理解。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2022-06-13,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 无量测试之道 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1. 前言
    • Vue.js 是什么
  • 2. 动态效果图
  • 3. 框架结构
  • 4. Vue 代码实现
    • (1). AllList.vue组件
    • (2). ListFooter.vue组件
    • (3). ListHeader.vue组件
    • (4). ListItem.vue组件
    • (5). App.vue组件
    • (6). main.js入口文件
    • (7). index.html网页展示文件
  • 5. 总结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档