一、仓库模板
下载模板到本地,重命名为 todomvc-vue。
TodoMVC模板git仓库
可直接下载 zip 或者用 git 命令下载。
git clone https://github.com/tastejs/todomvc-app-template.git --depth=1
--depth=1
表示只下载最后一次的 commit,其他历史记录不要下载,这样可以提高下载速度切换到 todomvc-vue 目录中
cd todomvc-vue
,安装依赖项npm install
打开
todomvc-vue
中的 index.html 预览模板安装 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 浏览器同步测试工具
- 安装依赖
npm install --save-dev browser-sync
- 不安装全局的原因是希望 browser-sync 浏览器同步工具跟着项目走
-
--save-dev
参数作用是将该依赖区分到 devDependencies 中,分清楚哪些是核心依赖,哪些是非核心依赖。 - npm install下载包含全部依赖,
npm install --production
只安装 dependencies 下定义的包
- 在 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 配置
- 启动开发服务
npm run dev
# 或者 npm start
三、TodoMVC 的业务逻辑梳理
- 数据列表展示
- 无数据:当没有 todos,
#main
和#footer
应该被隐藏 - 有数据:todos 数据渲染到视图
- 无数据:当没有 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">
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,否则显示。
(JavaScriptArray.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>