vue-todoMVC-经典框架入门项目

vue-todoMVC-经典框架入门项目


目录




内容

1、前言

TodoMVC是一个示例项目,它使用目前流行的不同JavaScript框架的来实现同一个Demo,来帮助你熟悉和选择最合适的前端框架。官网地址:http://todomvc.com,学习框架最直接有效的方式就是上手练习,接下来我们将用Vue.js来完成TodoMVC的示例。

2、准备

2.1、下载模板

  1. 下载源码:官网http://todomvc.com或者git.hubhttps://github.com/tastejs/todomvc-app-template

  2. 下载和安装依赖:解压,进入目录,

     npm i
     npm i vue
    

2.2、项目简介

  • 目录结构示例:[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Z5HCgNny-1596036791933)(./images/2020-07-29_todoMVC-struc_1.png)]

需要我们关心的就2个文件,js/app.js 和index.html 。

  • app.js :主要的逻辑代码,即vue部分
  • index.html:项目首页

页面结构和样式都已经写好,我们只想用vuejs实现功能即可。

2.3、效果图

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-n8Mhdf6G-1596036791936)(./images/2020-07-29_memo-simple.png)]

3、需求分析和实现

3.0、vue导入与创建vue实例

  • index.html导入vue模块与设置id

      <-- 此处添加id用于vue实例绑定 -->
      <section class="todoapp" id="app">
      ...
      <script src="node_modules/todomvc-common/base.js"></script>
      <-- 一定在导入app.js之前导入vue模块 -->
      <script src="./node_modules/vue/dist/vue.js"></script>
      <script src="js/app.js"></script>
    
  • app.js

      (function () {
      	const vm = new Vue({
      		el: '#app'
      	})
      })
    

3.1、列表渲染

  • 每一个条目有三个状态:

      未完成(没有样式)
      已完成(.completed )
      编辑中( .editing )   // 编辑功能中完成
    

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iMdCaJZC-1596036791937)(./images/2020-07-29_list-status.png)]

  • 列表为空时:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2eiXb184-1596036791938)(./images/2020-07-29_list-empty.png)]

  • 准备示例数据

      // app.js其余部分省略
      const itemList = [
      		{id: 1, content: '晨跑', completed: true},
      		{id: 2, content: '吃早饭', completed: true},
      		{id: 3, content: '学习前端课程', completed: false},
      		{id: 4, content: '吃午饭', completed: false}
      	]
      	
      	// vue实例
      	...
      	data: {
      	items: itemList
      	}
      	...
    
  • 改造index.html

      <section class="main" v-show="items.length">
      ...
      <ul class="todo-list">
      					<!-- These are here just to show the structure of the list items -->
      					<!-- List items should get the class `editing` when editing and `completed` when marked as completed -->
      					<li :class="{completed: item.completed, editing: currentItem === item}" v-for="(item, index) in items" :key="item.id">
      						<div class="view">
      							<input class="toggle" type="checkbox" >
      							<label >{{ item.content }}</label>
      							<button class="destroy" ></button>
      						</div>
      						<input class="edit" >
      					</li>
    
      </ul>
      ...
      <footer class="footer" v-show="items.length">
    

3.2、添加条目

  1. 功能:在最上面文本输入框添加键盘事件,敲击enter 添加新条目
  2. 条件:添加的新条目不能为空
  • index.html

      <input class="new-todo" placeholder="What needs to be done?" v-focus @keyup.enter="addItem">
    
  • app.js

      // 添加条目methods方法
      addItem(event) {
      	let content = event.target.value.trim()
      	if(!content) {
      		return
      	}
      	let id = this.items[this.items.length - 1].id + 1
      	this.items.push({
      		id,
      		content,
      		completed: false
      	})
      	event.target.value = ''
      },
    

3.3、删除条目

  1. 功能:点击条目后面的X号,删除对应的条目
  2. 实现:X号对应的元素添加鼠标点击事件,完成上述功能
  • index.html

      <button class="destroy" @click="deleteItem(item.id)"></button>
    
  • app.js

      // 删除给定ID的条目
      deleteItem(id) {
      	this.items = this.items.filter(item => item.id !== id)
      },
    

3.4、统计未完成事项

  • 图示:[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lMFBAIO5-1596036791939)(./images/2020-07-29_remain.png)]

  • 功能:显示未完成事项数量,为1时,后面显示’item’;不为1时,后面显示’items’;

  • 实现:添加一个计算属性 total,用来统计数组中事项completed为true的条目数量

  • index.html

      <span class="todo-count"><strong>{{ total }}</strong> item{{ total === 1? '': 's'}} left</span>
    
  • app.js

      computed: {
      			total() {
      				return this.items.filter(item => !item.completed).length
      			}
      }
    

3.5、清除已完成事项

  • 图示3.5-1:[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wzHyCJLU-1596036791941)(./images/2020-07-29_clear-completed.png)]

  • 功能:点击清除已完成事项,当没有已完成事项时,隐藏

  • 实现:添加点击事件,用于删除已完成事项;添加v-show指令,当没有已完成事项时(总条目-上面未完成条目)> 0,隐藏

  • index.html

      <button class="clear-completed" v-show="items.length > total" @click="clearCompleted">清除已完成事项</button>
    
  • app.js

      // 清除已完成条目
      clearCompleted() {
      	this.items = this.items.filter(item => !item.completed)
      },
    

3.6、全选复选框切换状态

  • 图示

    • 事项全部选中,复选框状态改为选中:[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-M7JEzlJ1-1596036791942)(./images/2020-07-29_checkbox-get.png)]

    • 点击复选框,复选框选中,下面条目选中;复选框未选中,下面条码全部未选中:[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2azMVO8Z-1596036791943)(./images/2020-07-29_checkbox-set-checked.png)][外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OX6vttQj-1596036791944)(./images/2020-07-29_checkbox-set-unchecked.png)]

    在这里插入图片描述

  • index.html

      <input id="toggle-all" class="toggle-all" type="checkbox" v-model="toggleAll">
    
  • app.js

      toggleAll: {
      	get() {
      		return this.total === 0
      	},
      	set(newStatus) {
      		this.items.forEach(item => {
      			item.completed = newStatus
      		})
      	}
      },
    

3.7、路由状态切换

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yuODVSFP-1596036791944)(./images/2020-07-29_router.png)]

功能:点击图示3个不同路由,显示不同的内容
实现:给3个a标签href绑定不同的hash值,通过监听window.onhashchange事件,获取不同的filterStatus(属性)
,根据不同的filterStatus,渲染不同的数组内容filterItems(计算属性)

  • index.html

      <li :class="{completed: item.completed}" v-for="(item, index) in filterItems" :key="item.id">
    
  • app.js

      // vue实例data内
      	filterStatus: '',
      // vue实例vm外
      window.onhashchange =function () {
      	// console.log(location.hash.slice(1));
      	vm.filterStatus = location.hash.slice(1)
      }
    
      // 计算属性filterItems
      filterItems() {
      	// console.log(this.filterStatus);
      	let items
      	switch(this.filterStatus) {
      		case '/active':
      			// console.log(1);
      			items = this.items.filter(item => !item.completed)
      			break
      		case '/completed':
      			// console.log(2);
      			items = this.items.filter(item => item.completed)
      			break
      		default:
      			// console.log(3);
      			items = this.items
      			break;	
      	}
      	return items
      }
    

3.8、编辑事项

  1. 双击<label>标签,进入编辑状态
    • 绑定双击事件
  2. 编辑状态下,按ESC,退出编辑
    • 绑定keyup.esc事件
  3. 编辑状态下,按ENTER或者失去焦点,完成编辑
    • 绑定keyup.enter,blur事件,执行相同的功能
  4. 内容为空,删除当前条目;内容不为空,更新当前条目
    • 3执行的逻辑
  • index.html

      <label @dblclick=toEdit(item)>{{ item.content }}</label>
      ...
      <input class="edit" :value="item.content"
      @keyup.esc="cancelEdit"
      @keyup.enter="$event.target.blur"
      @blur="finishEdit(item, index, $event)"
      v-focus="item === currentItem">
    
  • app.js

      // vue实例data
      currentItem: {}
      // vue实例methods
      // 双击开始编辑
      toEdit(item) {
      	this.currentItem = item
    
      },
      // 取消编辑
      cancelEdit() {
      	this.currentItem = {}
      },
      // 完成编辑
      finishEdit(item, index, event) {
      	const content = event.target.value.trim()
      	if(!content) {
      		// console.log(item);
      		// console.log(event);
      		this.deleteItem(item.id)
      		return
      	}
      	item.content = content
      	this.currentItem = {}
      }
    

3.9、数据的本地持久存储

3.9.1、localStorage与sessionStorage简介

localStorage

  1. 浏览器本地持久化存储,如果不手动清空localStorage,默认永久存储
  2. 大小一般为5M,不同浏览器存储空间不同
  3. 类型为json字符串,值类型为字符串
  4. 不同浏览器不能共享
  5. 同一个浏览器不同页面或者标签共享相同属性名的数据

sessionStorage

  1. 当前会话的浏览器本地存储
  2. 关闭浏览器或者会话结束,数据消失
  3. 其他同lcalStorage

详细使用自行查阅相关文档。

3.9.2、查看方式

打开浏览器,右键查看或者f12点击开发者窗口->Application选项卡↓localStorage

  • 图示:[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KDzJLiib-1596036791945)(./images/2020-07-29_browser-application.png)]

3.9.3、javascript的localStorage常用API

  • localStorage.setItem(key, value)
  • localStorage.getItem(key)
  • localStorage.removeItem(key)
  • 解析:
    • key,value必须为字符串
    • getItem如果key不存在,结果:undefied
  • 存取JSON对象
    • setItem(key, JSON.stringify(object))
    • JSON.parse(getItem(key))

3.9.4、本项目的本地持久存储

  1. 监听(深度)vue实例items的变化,一旦改变,存储到本地
  2. vue实例初始化的时候,调用created钩子函数读取本地存储的数据条目
  • app.js

      // created钩子函数
      created() {
      	this.getItems()
      },
    
      // methods
      // 获取条目
      getItems() {
      	this.items = JSON.parse(localStorage.getItem('item')) || []
      },
      // watch侦听器
      watch: {
      	items: {
      		handler(val) {
      			localStorage.setItem('item', JSON.stringify(val))
      		},
      		deep: true,
      		immediate: true
      	}
      }
    
  • 查看存储图示:[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fuf4jY8C-1596036791946)(./images/2020-07-29_localStorage.png)]

3.10,其他功能

  • 文本输入框,自动获取焦点

实现:自定义全局指令,添加到表单表上

  • index.html

      ...
      <input class="new-todo" placeholder="添加待办事项..." v-focus @keyup.enter="addItem">
      ...
      <input class="edit" :value="item.content"
      @keyup.esc="cancelEdit"
      @keyup.enter="$event.target.blur"
      @blur="finishEdit(item, index, $event)"
      v-focus="item === currentItem">
    
  • app.js

      Vue.directive('focus', {
      		inserted(el, binding) {
      			el.focus()
      		},
      		update(el, binding) {
      			// console.log(binding.value);
      			if(binding.value){ 
      				el.focus()
      			}
      		}
      	})
    

5、完整代码

  • 需求分析

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-T0ntqZOr-1596036791946)(./images/2020-07-29_需求分析.png)]

  • 效果图:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1A9R37lu-1596036791947)(./images/2020-07-29_memo-simple_1.png)]

  • index.html

      <!doctype html>
      <html lang="en">
      	<head>
      		<meta charset="utf-8">
      		<meta name="viewport" content="width=device-width, initial-scale=1">
      		<title>Template • TodoMVC</title>
      		<link rel="stylesheet" href="node_modules/todomvc-common/base.css">
      		<link rel="stylesheet" href="node_modules/todomvc-app-css/index.css">
      		<!-- CSS overrides - remove if you don't need it -->
      		<link rel="stylesheet" href="css/app.css">
      	</head>
      	<body>
      		<section class="todoapp" id="app">
      			<header class="header">
      				<h1>简易备忘录</h1>
      				<input class="new-todo" placeholder="添加待办事项..." v-focus @keyup.enter="addItem">
      			</header>
      			<!-- This section should be hidden by default and shown when there are todos -->
      			<section class="main" v-show="items.length">
      				<input id="toggle-all" class="toggle-all" type="checkbox" v-model="toggleAll">
      				<label for="toggle-all">Mark all as complete</label>
      				<ul class="todo-list">
      					<!-- These are here just to show the structure of the list items -->
      					<!-- List items should get the class `editing` when editing and `completed` when marked as completed -->
      					<li :class="{completed: item.completed, editing: currentItem === item}" v-for="(item, index) in filterItems" :key="item.id">
      						<div class="view">
      							<input class="toggle" type="checkbox" v-model="item.completed">
      							<label @dblclick=toEdit(item)>{{ item.content }}</label>
      							<button class="destroy" @click="deleteItem(item.id)"></button>
      						</div>
      						<input class="edit" :value="item.content"
      						@keyup.esc="cancelEdit"
      						@keyup.enter="$event.target.blur"
      						@blur="finishEdit(item, index, $event)"
      						v-focus="item === currentItem">
      					</li>
    
      				</ul>
      			</section>
      			<!-- This footer should hidden by default and shown when there are todos -->
      			<footer class="footer" v-show="items.length">
      				<!-- This should be `0 items left` by default -->
      				<span class="todo-count"><strong>{{ total }}</strong> item{{ total === 1? '': 's'}} left</span>
      				<!-- Remove this if you don't implement routing -->
      				<ul class="filters">
      					<li>
      						<a :class="{selected: filterStatus === '/'}" href="#/">全部</a>
      					</li>
      					<li>
      						<a href="#/active" :class="{selected: filterStatus === '/active'}">未完成事项</a>
      					</li>
      					<li>
      						<a href="#/completed" :class="{selected: filterStatus === '/completed'}">已完成事项</a>
      					</li>
      				</ul>
      				<!-- Hidden if no completed items are left ↓ -->
      				<button class="clear-completed" v-show="items.length > total" @click="clearCompleted">清除已完成事项</button>
      			</footer>
      		</section>
      		<footer class="info">
      			<p>双击编辑事项</p>
      			<!-- Remove the below line ↓ -->
      			<p>Template by <a href="http://sindresorhus.com">gaogzhen</a></p>
      			<!-- Change this out with your name and url ↓ -->
      			<p>Created by <a href="http://todomvc.com">gaogzhern</a></p>
      			<p>Part of <a href="http://todomvc.com">TodoMVC</a></p>
      		</footer>
      		<!-- Scripts here. Don't remove ↓ -->
      		<script src="node_modules/todomvc-common/base.js"></script>
      		<script src="./node_modules/vue/dist/vue.js"></script>
      		<script src="js/app.js"></script>
      	</body>
      </html>
    
  • app.js

      (function () {
      	// 'use strict';
    
      	// Your starting point. Enjoy the ride!
      	const itemList = [
      		{
      			id: 1,
      			content: '晨跑',
      			completed: true
      		},
      		{
      			id: 2,
      			content: '吃早饭',
      			completed: true
      		},
      		{
      			id: 3,
      			content: '学习前端课程',
      			completed: false
      		},
      		{
      			id: 4,
      			content: '吃午饭',
      			completed: false
      		}
      	]
    
      	Vue.directive('focus', {
      		inserted(el, binding) {
      			el.focus()
      		},
      		update(el, binding) {
      			// console.log(binding.value);
      			if(binding.value){ 
      				el.focus()
      			}
      		}
      	})
      	const vm = new Vue({
      		el: '#app',
      		data: {
      			items: itemList,
      			filterStatus: '',
      			currentItem: {}
      		},
      		directives: {
      			// focus: {
      			// 	inserted(el) {
      			// 		el.focus()
      			// 	}
      			// }
      		},
      		computed: {
      			total() {
      				return this.items.filter(item => !item.completed).length
      			},
      			toggleAll: {
      				get() {
      					return this.total === 0
      				},
      				set(newStatus) {
      					this.items.forEach(item => {
      						item.completed = newStatus
      					})
      				}
      			},
      			filterItems() {
      				// console.log(this.filterStatus);
      				let items
      				switch(this.filterStatus) {
      					case '/active':
      						// console.log(1);
      						items = this.items.filter(item => !item.completed)
      						break
      					case '/completed':
      						// console.log(2);
      						items = this.items.filter(item => item.completed)
      						break
      					default:
      						// console.log(3);
      						items = this.items
      						break;	
      				}
      				return items
      			}
      		},
      		methods: {
      			// 获取条目
      			getItems() {
      				this.items = JSON.parse(localStorage.getItem('item')) || []
      			},
      			// 添加条目
      			addItem(event) {
      				let content = event.target.value.trim()
      				if(!content) {
      					return
      				}
      				let id = this.items[this.items.length - 1].id + 1
      				this.items.push({
      					id,
      					content,
      					completed: false
      				})
      				event.target.value = ''
      			},
      			// 删除给定ID的条目
      			deleteItem(id) {
      				this.items = this.items.filter(item => item.id !== id)
      			},
      			// 清除已完成条目
      			clearCompleted() {
      				this.items = this.items.filter(item => !item.completed)
      			},
      			// 双击开始编辑
      			toEdit(item) {
      				this.currentItem = item
    
      			},
      			// 取消编辑
      			cancelEdit() {
      				this.currentItem = {}
      			},
      			// 完成编辑
      			finishEdit(item, index, event) {
      				const content = event.target.value.trim()
      				if(!content) {
      					// console.log(item);
      					// console.log(event);
      					this.deleteItem(item.id)
      					return
      				}
      				item.content = content
      				this.currentItem = {}
      			}
      		},
      		mounted() {
    
      		},
      		created() {
      			this.getItems()
      		},
      		watch: {
      			items: {
      				handler(val) {
      					localStorage.setItem('item', JSON.stringify(val))
      					// console.log(newVal);
      					// console.log(oldVal);
      					// console.log(val);
      					// console.log(this.items);
      				},
      				deep: true,
      				immediate: true
      			}
      		}
      	})
    
      	window.onhashchange =function () {
      		// console.log(location.hash.slice(1));
      		vm.filterStatus = location.hash.slice(1)
      	}
    
      })();
    

至此本项目完结,做到到这里的小伙伴,说明你们vue基础已经掌握熟练,可以继续学习vue高级知识部分。

幸福是什么样子呢?

后记

vue官网地址:https://cn.vuejs.org/

本项目为参考某图培训开发,相关视频及配套资料可自行度娘或者联系本人。上面为自己编写的开发文档,持续更新。欢迎交流,本人QQ:806797785

前端项目源代码地址:https://gitee.com/gaogzhen/vue
后端JAVA源代码地址:https://gitee.com/gaogzhen/JAVA
©️2020 CSDN 皮肤主题: 技术黑板 设计师:CSDN官方博客 返回首页