3.基于Vue实现TodoMVC

一、仓库模板

TodoMVC网站

  1. 下载模板到本地,重命名为 todomvc-vue。
    TodoMVC模板git仓库
    可直接下载 zip 或者用 git 命令下载。
    git clone https://github.com/tastejs/todomvc-app-template.git --depth=1
    --depth=1 表示只下载最后一次的 commit,其他历史记录不要下载,这样可以提高下载速度

  2. 切换到 todomvc-vue 目录中cd todomvc-vue,安装依赖项npm install

  3. 打开 todomvc-vue 中的 index.html 预览模板

  4. 安装 Vue 框架到模板中 npm install vue,并引入 vue.js 在 app.js 之前

<!-- Add Vue Scripts here.  ↓ -->
<script src="node_modules/vue/dist/vue.js"></script>
<script src="js/app.js"></script>

二、配置 browser-sync 浏览器同步测试工具

  1. 安装依赖
    npm install --save-dev browser-sync
  • 不安装全局的原因是希望 browser-sync 浏览器同步工具跟着项目走
  • --save-dev 参数作用是将该依赖区分到 devDependencies 中,分清楚哪些是核心依赖,哪些是非核心依赖。
  • npm install下载包含全部依赖,npm install --production只安装 dependencies 下定义的包
  1. 在 package.json 文件中配置 scripts
  "scripts": {
    "dev": "browser-sync start --server --files \"*.html,css/*.css,js/*.js\"",
    "start": "npm run dev"
  }
  • 配置dev:启动server监视指定的文件,当html、css、js文件发生变化,自动刷新浏览器
  • 配置start:可使用 npm start 命令代替 npm run dev 启动,若不配置则只能用 npm run dev 命令启动
    package.json 配置
  1. 启动开发服务
npm run dev
# 或者 npm start

三、TodoMVC 的业务逻辑梳理

  • 数据列表展示
    • 无数据:当没有 todos,#main#footer 应该被隐藏
    • 有数据:todos 数据渲染到视图
  • 添加 todo 任务
    • 页面初始化的时候文本框获得焦点
    • 敲回车将 todo 添加到任务列表中
    • 不允许有非空数据
    • 添加完成清除文本框
  • 标记所有任务完成/未完成
    • 未完成样式:checkbox 不勾选
    • 完成样式:checkbox 勾选,切换.completed 样式即任务文本灰色且带删除线
  • 任务项
    • 切换任务完成状态
      • 点击 checkbox 切换完成状态且样式联动
      • toggle-all 的 checkbox 联动切换所有任务状态
    • 删除任务项
    • 双击 label 进入编辑模式,切换 .editing 样式
  • 编辑任务项
    • 编辑文本框自动获得焦点
    • 在编辑文本框中敲回车或者失去焦点
      • 保存文本框内容到任务列表中
      • 非空校验:如果数据是空的,直接删除该元素;否则保存编辑
      • 同时去除 .editing 样式
    • 输入状态按下 esc 取消编辑
  • 显示所有未完成任务数
  • 点击 Clear completed 清除已完成任务
  • 将数据持久化存储到 localStorage 中
  • 点击 All/Active/Completed 切换路由状态,当路由改变,过滤对应的 todo list 并呈现


    todoMVC 示范

四、功能实现

1. 数据列表展示:

1.1 有数据

在 Vue 的 data选项 中定义 todos 数组,使用 v-for 将 todos 循环渲染到视图。

1.2 无数据

<template> 元素上使用 v-if 条件渲染分组,最终的渲染结果将不包含 <template> 元素。因此,无数据隐藏 #main#footer 我们可以这样:
<template v-if="todos.length">

HTML

2. 添加 todo 任务

2.1 页面初始化的时候文本框获得焦点

自定义 focus 指令从而解决在 Vue 中使用 autofocus 不生效的问题。

<input class="new-todo" 
    @keydown.enter="handleNewTodoKeyDown" 
    placeholder="What needs to be done?"  v-focus>
Vue.directive('focus', {
    inserted: function (el) {
        el.focus()
    }
})
2.2 敲回车将 todo 添加到任务列表中

通过 v-on 指令给 input 文本框注册 enter 键的点击事件 handleNewTodoKeyDown。 由于该事件直接绑定事件,默认自带一个事件源 e 参数,而e.target 对象是当前的 DOM 对象。这个 target 就是 input 这个 DOM 元素,因为这个事件是 input 触发的 keydown 事件。所以 e.target.value 就可以拿到 input 表单用户输入的值。将这个值通过 todos.push() 方法存入 todos 数组中,功能 1.1就可以再次将他渲染出来。

2.3 非空校验

在存 todo 数据之前,需要判断 value.length 若为 0,则跳出不存值。

2.4 添加完成清除文本框

通过 e.target.value=''直接操作 DOM 就可以清除文本框了。

<input class="new-todo" @keydown.enter="handleNewTodoKeyDown" placeholder="What needs to be done?" autofocus>
const todos = [
        {
            id:1,
            title:'吃饭',
            completed: true
        },
        {
            id:2,
            title:'看书',
            completed: false
        }
    ]
window.app = new Vue({
        data: {
            todos: todos
        },
        methods: {
            handleNewTodoKeyDown (e) {
                const value = e.target.value.trim()
                if(!value.length) {
                    return
                }
                this.todos.push({
                    id: this.todos.length? this.todos[this.todos.length-1].id+1:1,
                    title: value,
                    completed: false
                })
                e.target.value=''
            }
        }
    }).$mount('#app')

3. 标记所有任务完成/未完成

  • 处理 checkbox 勾选状态:
    通过 v-model 双向绑定 todos 的 completed 值。
  • 处理样式
    任务项有三种样式状态:
    • 未完成:无样式
    • 已完成:.completed
    • 编辑:.editing
      通过 v-bind 指令在任务列表上绑定 class 样式已完成的项目为 .completed

4. 任务项

4.1 切换任务完成状态
  • 点击 checkbox 切换完成状态且样式联动
    通过上述 功能 3 的 v-model 双向绑定已经完成了点击 checkbox 切换完成状态且样式联动。

  • toggle-all 的 checkbox 联动切换所有任务状态
    (1)点击 toggle-all 的 checkbox 切换所有任务状态
    通过 v-on 指令给 toggle-all 注册点击事件, e.target.checked 就可以拿该 checkbox 的 checked 属性值。

<input id="toggle-all" class="toggle-all" type="checkbox" @click="handleToggleAllChange">
handleToggleAllChange(e) {
    const checked = e.target.checked
    this.todos.forEach(item => {
    item.completed = checked
    }) 
}

(2)当所有任务状态完成时联动切换 toggle-all 的 checkbox 选中状态
增加计算属性 toggleAllState 获取 todos 是否全部完成。

<input id="toggle-all" class="toggle-all" type="checkbox" 
    @click="handleToggleAllChange" 
    :checked="toggleAllState">
computed:{
    toggleAllState: {
        get() {
            return this.todos.every(t => t.completed)
        }
    }
}

上述(1)(2),还可以通过计算属性进行进一步优化:

<input id="toggle-all" class="toggle-all" type="checkbox" v-model="toggleAllState">
computed: {
    toggleAllState: {
        get() {
            return this.todos.every(t => t.completed)
        },
        set() {
            const checked = !this.toggleAllState
            this.todos.forEach(item => {
                item.completed = checked
            }) 
        }
    }
}
4.2 删除任务项

通过 v-on 指令给删除键注册点击事件,在任务列表的 v-for 循环传入 index 参数,并给该点击事件传入 index 参数。

<button class="destroy" @click="handleRemoveTodoClick(index)"></button>
handleRemoveTodoClick(index) {
    this.todos.splice(index, 1)
}
4.3 双击 label 进入编辑模式,切换 .editing 样式
  • 通过 v-on 指令给任务列表的 label 注册双击事件,并给该事件传入 item 参数。在 data 中定义一个 currentEditing 变量用来记录当前用户编辑的任务项,当用户双击的时候,就把当前双击的任务项赋值给 currentEditing
  • 通过 v-bind 指令在任务列表循环的时候上将 currentEditing === item 的那个 li 绑定 class 样式为.editing
<ul class="todo-list">
  <li :class="{
    completed: item.completed,
    editing: currentEditing === item
    }"
    v-for="(item,index) in todos">
    <div class="view">
      <input class="toggle" v-model="item.completed" type="checkbox">
      <label @dblclick="handleGetEditingDblClick(item)">{{ item.title }}</label>
      <button class="destroy" @click="handleRemoveTodoClick(index)"></button>
    </div>
    <!--由于后面还有esc取消编辑不保存的功能,采用 `v-bind` 而不是 `v-model` 指令-->
    <input class="edit" :value="item.title">
  </li>
</ul>
handleGetEditingDblClick(todo) {
    this.currentEditing = todo
}

5. 编辑任务项

5.1 编辑文本框自动获得焦点

自定义 todo-focus 指令,使用 update 钩子函数,该指令被作用到了任务列表所有 li 的 input 上了,而在双击编辑的时候,模板更新,触发 todo-focus 的钩子函数。在指令中就可以得到作用该指令的 DOM 元素。所以我们需要找到当前双击编辑的那个 input DOM 元素,并将 focus 作用到该元素。有以下两种方案:
(1)由于当多个元素都 focus 的时候只有第一个会生效,所以会聚焦当前的 input 。这种方法不严谨。

<input class="edit"
    :value="item.title"
    @keyup.enter="handleSaveEditKeyDown(item,index,$event)"
    @blur="handleSaveEditKeyDown(item,index,$event)"
    @keyup.esc="handleCancelEditEsc"
    v-todo-focus>
Vue.directive('todo-focus', {
    update: function (el, binding) {
            el.focus()
    }
})

(2)更为严谨的做法是给指令传入参数,判断当前指令所作用的元素是否是当前编辑的元素,即 currentEditing 指向的元素。

<input class="edit"
      :value="item.title"
      @keydown.enter="handleSaveEditKeydown(item, index, $event)"
      @blur="handleSaveEditKeydown(item, index, $event)"
      @keydown.esc="handleCancelEditEsc"
      v-todo-focus="currentEditing === item">
Vue.directive('todo-focus', {
    update: function (el, binding) {
        if(binding.value) {
            el.focus()
        }
    }
})
5.2 在编辑文本框中敲回车或者失去焦点
  • 保存文本框内容到任务列表中
    通过 v-on 指令给任务列表的编辑框 input 注册 enter 点击事件 keyup.enter 与失去焦点事件 blur,由于需要传参所以将特殊变量 $event 把它传入方法,e.target.value 是编辑框 input 表单用户输入的值。获取将该值存入当前编辑 todo 的 title 属性。
  • 非空校验
    在存当前编辑 todo 的 title 数据之前,需要判断 value.length 若为 0,则删除该 todo。
  • 去除 .editing 样式
    data 中的 currentEditing 变量是用来记录当前用户编辑的任务项的,赋空值即没有编辑的任务项,即可去除.editing 样式。
5.3 输入状态按下 esc 取消编辑

通过 v-on 指令给任务列表的编辑框 input 注册 esc 点击事件 keydown.esc,同样将 currentEditing 赋空值去除 .editing 样式。

<input class="edit" 
    :value="item.title" 
    @keydown.enter="handleSaveEditKeyDown(item, index, $event)"
    @blur="handleSaveEditKeyDown(item, index, $event)"
    @keydown.esc="handleCancelEditEsc"
    v-todo-focus="currentEditing === item">
handleCancelEditEsc() {
    this.currentEditing = null
}

6. 显示所有未完成任务数

增加计算属性 remainingCount 获取未完成的任务数。

<span class="todo-count"><strong>{{ remainingCount }}</strong> item left</span>
computed: {
    remainingCount: {
        get() {
            return this.todos.filter(t => !t.completed).length
        }
    }
}

7. 点击 Clear completed 清除已完成任务

  • 通过 v-if 显示全部项目已完成则隐藏 Clear completed 的 button,否则显示。
    (JavaScript Array.some() 方法显示会依次执行数组的每个元素,如果有一个元素满足条件,则表达式返回true , 剩余的元素不会再执行检测。如果没有满足条件的元素,则返回false。)
  • 通过 v-on 指令给该 button 注册 click 点击事件,过滤 todos 中的已完成任务。
<button class="clear-completed" 
    v-if="todos.some(t=>t.completed)"
    @click="handleClearAllDoneClick">Clear completed</button>
handleClearAllDoneClick() {
    this.todos = this.todos.filter(t => !t.completed)
}

8. 将数据持久化存储到 localStorage 中

  • 我们不再在代码里面写 hard code 定义 todos,而是将 todos 数组持久化存储到 localStorage 中。


    LocalStorage 使用
  • 通过 watch 监视 todos 的改变,当 todos 发生变化的时候就往 localStorage 中存值,因为我们监视的对象是数组,所以需要配置深度监视。

data: {
    todos: JSON.parse(window.localStorage.getItem('todos') || '[]'),
},
watch: {
    todos: {
        handler (val, oldVal) {
            window.localStorage.setItem('todos', JSON.stringify(this.todos))
        },
        deep: true
    }
}

9. 点击 All/Active/Completed 切换路由状态,当路由改变,过滤对应的 todo list 并呈现

  • 增加计算属性 filterTodos 来显示 todos 数组的过滤结果。将循环渲染任务列表的数据改为 filterTodos 。
  • data 选项中定义一个 filterText 变量,用来记录 All/Active/Completed 切换时候的 hash 值。
  • 通过 v-bind 指令绑定样式是否为 selected。
computed: {
    filterTodos: {
        get() {
            switch (this.filterText) {
                case 'active':
                    return this.todos.filter(t => !t.completed)
                    break
                case 'completed':
                    return this.todos.filter(t => t.completed)
                    break
                default:
                    return this.todos
                    break
            }
        }
    }
}
// 该事件只有 hash change的时候才会执行
window.onhashchange = function() {
    app.filterText = window.location.hash.substring(2)
}
// 让页面初始化的时候也执行这个方法,这样就能在刷新的时候保持住状态
window.onhashchange()
<ul class="filters">
    <li>
        <a :class="{selected: filterText === ''}" href="#/">All</a>
    </li>
    <li>
        <a :class="{selected: filterText === 'active'}" href="#/active">Active</a>
    </li>
    <li>
        <a :class="{selected: filterText === 'completed'}" href="#/completed">Completed</a>
    </li>
</ul>
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 220,458评论 6 513
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 94,030评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 166,879评论 0 358
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,278评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,296评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 52,019评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,633评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,541评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 46,068评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,181评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,318评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,991评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,670评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,183评论 0 23
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,302评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,655评论 3 375
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,327评论 2 358

推荐阅读更多精彩内容

  • 33、JS中的本地存储 把一些信息存储在当前浏览器指定域下的某一个地方(存储到物理硬盘中)1、不能跨浏览器传输:在...
    萌妹撒阅读 2,085评论 0 2
  • 基本格式 以json的形式、将数据(支持所有格式)挂载在vue的data上、方法挂载在vue的methods上。 ...
    kirito_song阅读 773评论 0 21
  • feisky云计算、虚拟化与Linux技术笔记posts - 1014, comments - 298, trac...
    不排版阅读 3,864评论 0 5
  • 写文章[https://www.jianshu.com/writer#/]注册[https://www.jians...
    过场_ab79阅读 317评论 1 1
  • 一、npm npm:是一款基于nodeJS的JavaScript包管理工具 1、全局安装 npm install ...
    恋煜阅读 3,118评论 0 5