Vue 不睡觉教程3 - 来点实在的:自动计算剩余时间的任务列表

目标
前两课教的是入门和文件结构。都没有什么实在的东西。此次咱们要来点实在的。咱们要作出一个待办列表。这个待办列表有如下特色:css

能够自动从文本中抽取出这件事情的开始时间
能够显示当前距离这件事情的开始时间还有多久,好比:23:40 回家 (还有 6 小时 36 分 15 秒)
若是当前时间已经超过了计划时间,则以灰色字体显示任务,并加上删除线
经过这个例子咱们能够学到如下知识点html

v-for属性
v-bind:key属性
v-on属性
在vue中使用bootstrap
在vue中使用localStorage
watch属性
computed属性
在vue中定义私有方法
webpack自动打包
v-if, v-else-if, v-else属性
v-show属性
背景
vue版本:2.5.16
文件结构基于上节课的文件结构: Vue不睡觉教程2 你们能够直接从 https://github.com/alexxiyang/learn-vue 下载源码,下载后使用git checkout lesson2 命令切换到lesson2的源码
注意事项
 在说本节课的步骤以前,先提醒你们,该完代码记得用如下命令编译后才能用浏览器看到你的更改vue

npx webpack
编译后记得要访问的页面文件不是根目录下的index.html。那只是源文件。你须要访问 dist/index.html。java

建立TodoList组件
修改App.vue
咱们先来构建项目框架。这个项目只有一个组件:TodoList。node

将App.vue中以前的 import 引用 修改成 import TodoList from './components/TodoList' 就像这样jquery

import TodoList from './components/TodoList.vue'
而后在template模板代码块中引用它,并在components对象中引用它。修改完的App.vue是这样的:webpack

<template>
<div id="app">
<TodoList/>
</div>
</template>

<script>
import TodoList from './components/TodoList.vue'

export default {
name: 'app',
components: {
TodoList
}
}
</script>
新建TodoList.vue组件git

将HelloVue.vue删掉。而后在src/components文件夹下新建TodoList.vue组件,组件内容为github

<template>
<div id="todolist">{{ message }}</div>
</template>

<script>
export default {
name: 'TodoList',
data: function() {
return {
message: '这是一个待办列表'
}
}
}
</script>
照例使用 npx webpack打包,而后访问 http://learn-vue/dist/index.html 。若是成功,你就能够 看到 “这是一个待办列表” 的字样。web

显示任务列表(v-for)
既然是一个待办列表,那么核心的数据对象就应该是一个array。让咱们来新建这个array

data: function() {
return {
taskList: [
"7:00 学英语",
"10:00 学Vue"
]
}
}
咱们来使用v-for来显示它

<template>
<div id="todolist">
<table>
<thead>
<th>任务</th>
</thead>
<tbody>
<tr v-for="task in taskList">
<td>{{ task }}</td>
</tr>
</tbody>
</table>
</div>
</template>
显示的效果为:

 

若是你的task是一个object,你可使用如下方式来显示它的属性

<tr v-for="task in taskList">
<td>{{ task.id }} {{ task.name}}</td>
</tr>
若是你使用的是 visual studio code,那么有可能看到如下错误提示:

Elements in iteration expect to have 'v-bind:key' directives.

这是由于当vue要求当使用v-for来显示列表时,须要使用v-bind:key来标定列表主键,就像这样

<tr v-for="task in taskList" v-bind:key="task.id">
<td>{{ task.id }} {{ task.name }}</td>
</tr>
由于咱们的例子过于简单了,每一个纪录只是一行字符串,因此能够忽略这个错误提示。在本例中咱们不须要理会这个错误提示。可是在实际的项目中,请必定加上:key。

为何要加上v-bind:key?
如下引用自vue官网:

当 Vue.js 用 v-for 正在更新已渲染过的元素列表时,它默认用“就地复用”策略。若是数据项的顺序被改变,Vue 将不会移动 DOM 元素来匹配数据项的顺序, 而是简单复用此处每一个元素,而且确保它在特定索引下显示已被渲染过的每一个元素。这个相似 Vue 1.x 的 track-by="$index" 。

这个默认的模式是高效的,可是只适用于不依赖子组件状态或临时 DOM 状态 (例如:表单输入值) 的列表渲染输出。

为了给 Vue 一个提示,以便它能跟踪每一个节点的身份,从而重用和从新排序现有元素,你须要为每项提供一个惟一 key 属性。理想的 key 值是每项都有的惟一 id。这个特殊的属性至关于 Vue 1.x 的 track-by ,但它的工做方式相似于一个属性,因此你须要用 v-bind 来绑定动态值 

简而言之就是:vue为了性能考虑,默认复用页面上的dom元素。为了防止你的列表元素不更新,就要用key告诉vue,这些dom元素是不同的。

添加任务按钮(v-on)
在<table>元素上面添加一个<button>组件,用来增长任务

<button>添加任务</button>
接下来,咱们须要用到v-on语法来为按钮添加对click事件的绑定

<button v-on:click="addTask">添加</button>
因而可知,v-on的语法就是 v-on:<事件名>=“js语句或者js方法名”

写好了模板,接下来就是在default对象中增长methods属性,并添加addTaks方法了

methods: {
addTask: function(event) {
this.taskList.push("新的待办任务");
}
}
完整的default对象为

export default {
name: 'TodoList',
data: function() {
return {
taskList: [
"7:00 学英语",
"10:00 学Vue"
]
}
},
methods: {
addTask: function(event) {
this.taskList.push("新的待办任务");
}
}
}
执行效果就是,每次点击添加按钮,就会新增一个任务

 

美化页面(bootstrap, css-loader, style-loader)
我以为这样的页面也太丑了,因此咱们来为页面加入bootstrap。直接使用原生bootstrap比较麻烦,咱们使用bootstrap-vue来为vue项目添加bootstrap:

$ npm i --save bootstrap-vue
而后咱们在main.js中写上对BootstrapVue的引用,以及相关css的引用

import BootstrapVue from 'bootstrap-vue'
import 'bootstrap/dist/css/bootstrap.css'
import 'bootstrap-vue/dist/bootstrap-vue.css'

Vue.use(BootstrapVue);
若是你如今执行npx webpack必定会看到以下错误

ERROR in ./node_modules/bootstrap-vue/es/components/alert/alert.css 1:0
Module parse failed: Unexpected token (1:0)
You may need an appropriate loader to handle this file type.
这是由于你目前尚未为webpack.config.js添加css的loader。因此webpack不认识.css文件。

咱们来安装跟css相关的loader

$ npm i --save style-loader css-loader
而后在webpack.config.js的rules节点中编写规则来使用它

rules: [
{
test: /\.vue$/,
loader: 'vue-loader',
},
{
test: /\.css$/,
use: [
'style-loader',
'css-loader'
]
}
]
如今咱们就可使用npx webpack命令来打包项目了。

打包好后,你再看页面,会有些许的变化,可是变化不大。这是由于咱们尚未真正的使用bootstrap。如今咱们来为页面作如下美化

 

为todoList根div增长class: container
为button按钮增长class: btn btn-primary m-4
为table增长class: table m-4
而后再来看看咱们的页面:

 

 

这下好看多了。

添加任务功能
接下来,咱们增长“添加任务”的功能。首先咱们要添加一个用来输入任务内容的input输入框。可是直接在button右边添加输入框看起来又很丑。因此我打算从bootstrap的网站上复制一段<button>和<input>都包含在内的布局代码,就像这段

<div class="input-group mb-3">
<div class="input-group-prepend">
<button class="btn btn-outline-secondary" type="button">Button</button>
</div>
<input type="text" class="form-control" placeholder="" aria-label="" aria-describedby="basic-addon1">
</div>
将其改形成咱们须要的样子:

将<button>元素的文字改成添加并加上v-on:click="addTask"属性
将<input>元素的placeholder属性修改成“请输入任务内容”,并加上id="task_content"方便定位。
而后,将以前的

<button type="button" class="btn btn-primary m-4" v-on:click="addTask">添加</button>
替换为咱们修改后的代码块

<div class="input-group mb-3">
<div class="input-group-prepend">
<button class="btn btn-outline-secondary" type="button" v-on:click="addTask">添加</button>
</div>
<input type="text" id="task_content" class="form-control" placeholder="请输入任务内容" aria-label="" aria-describedby="basic-addon1">
</div>
接下来,咱们来修改addTask任务。因为vue将对象和html dom元素进行了双向绑定,原来咱们须要用jquery来操做dom元素的大量代码就被修改为了一行代码

this.taskList.push(task_content.value);
加上获取任务内容输入框和清空输入框内容的代码,总共只须要三行代码:

addTask: function(event) {
// 获取任务内容
let task_content = document.querySelector("#task_content");

// 添加任务内容到任务列表中
this.taskList.push(task_content.value);

// 清空任务内容输入框
task_content.value = '';
}
咱们把以前任务列表中初始化的两个任务删掉

data: function() {
return {
taskList: []
}
},
如今你只须要操做taskList对象,页面上的任务列表也会跟着变更。如今你能够试试在任务内容框中输入任务的内容,而后点击添加按钮:

 

任务存储:localStorage和watch方法
如今有一个问题,那就是你一刷新页面,你新建的任务就消失了。因此咱们新建一个store.js来处理任务的存储。store.js利用localstorage来存储任务:

const STORAGE_KEY='todo_list'
export default{
fetch(){
return JSON.parse(window.localStorage.getItem(STORAGE_KEY)||'[]')
},
save(items){
window.localStorage.setItem(STORAGE_KEY,JSON.stringify(items))
}
}
而后,在TodoList.vue中引用 store.js

import Store from './store.js'
如今 data.taskList 就不仅是用[]来初始化了,咱们要改为从store中获取

data: function() {
return {
taskList: Store.fetch()
}
},
如今我要介绍一个全新的属性 watch。该属性的做用是当你改变某个属性的时候能够同时作一些其余的事情。好比如今咱们就须要在增长任务的同时将taskList保存到localStorage中。你能够这样写

watch:{
taskList:{
handler:function(tasks){
Store.save(tasks)
}
}
},
注意:watch跟data, methods属性是同级的。

动态解析任务时间(computed)
如今咱们要使用computed属性来作这个神奇的功能。当你想在页面上显示通过处理的变量时,你可使用各类函数,好比 若是咱们要将名字中的逗号都换成下划线,而后截取第一个空格以前的文字。咱们可能会这么写

name.replace(',', '_').substring(0, name.indexOf(' '));
偶尔写一次还好,要是项目的每一个地方都要这么写一遍就太恶心了。因此vue提供了一种属性叫 computed。使用这个属性咱们能够定义出“虚拟的”变量,这个变量并不在data中被实际的定义出来,而是经过对实际的变量进行了计算而得出的。在这个例子中咱们的需求是:

列表要可以自动计算出任务的剩余快完时间,好比:23:40 回家 (还有 6 小时 36 分 15 秒)
若是当前时间已经超过了计划时间,则不显示剩余完成时间
此时就须要用到computed属性。使用computed属性能够定义虚拟的变量。这种变量依赖于data中的变量计算得出,而且能够在html中像使用data中的属性同样的使用他们。在咱们这个例子中,咱们在html模板中使用一个虚拟变量parsedTaskList。

<tr v-for="task in parsedTaskList">
<td>{{ task }}</td>
</tr>
咱们在跟watch属性同级的节点下增长computed属性,并在其中增长parsedTaskList属性。咱们会在partedTaskList属性中对taskList进行转换,生成新的任务列表

computed: {
parsedTaskList: function () {
let parsedTaskList = [];
const regex = /[0-9]+:[0-9]+/;
// 遍历taskList
for (let i=0; i<this.taskList.length; i++) {
let task = this.taskList[i];

// 解析任务中的计划时间
let result = task.match(regex);
if (result != null && result.length > 0) {
let taskTime = result[0];
let thisMoment = moment();
let currentDate = thisMoment.format('YYYY-MM-DD');
let taskMoment = moment(currentDate + " " + taskTime, 'YYYY-MM-DD HH:mm');
if (taskMoment.valueOf() < thisMoment.valueOf()) {
parsedTaskList.push(task);
continue;
}
let duration = moment.duration(taskMoment.diff(thisMoment));
let durationText = duration.hours() + " 小时 " + duration.minutes() + " 分 " + duration.seconds() + " 秒";
// 将剩余时间拼接到任务上
parsedTaskList.push(task + "(还有 " + durationText + ")'></span>");
}
parsedTaskList.push(task);
}
// 返回新的任务列表
return parsedTaskList;
}
},
抽取剩余时间的具体的过程很简单,你们也不须要如今理解它,由于它并非这课的核心内容,只须要知道该函数能够实现自动拼接上任务的剩余完成时间就好了。

作到这里我遇到了一个问题,那就是:为了项目结构的简洁,我但愿能够把这段代码中由任务字符串转换为带着剩余时间的任务字符串代码抽取到一个私有函数中去。可是在这没有像java中的private关键字可让咱们定义私有函数。

要如何定义私有函数呢?
写在export中的东西意思是要暴露出去的东西,因此只要你的函数写在export中,就至关因而public函数了。要想函数不被暴露出去,只须要将函数块写到export之外就行了。如今咱们将转换任务字符串的代码抽取出来,放在 export default { 这行代码之上:

import Store from './store.js'
import * as moment from 'moment';

const regex = /[0-9]+:[0-9]+/;
/**
* 该函数做用是解析出字符串中的时间,并将其跟当前时间比较,
* 计算出还剩多久才会到达计划时间,将剩余时间拼接在字符串后。
* 若是当前时间已通过了计划时间,则不对字符串作任何改变
* 例子:
* 23:40 回家 -> 23:40 回家 (还有 6 小时 36 分 15 秒)
*/
const addRemainTime = (task) => {
let result = task.match(regex);
if (result != null && result.length > 0) {
let taskTime = result[0];
let thisMoment = moment();
let currentDate = thisMoment.format('YYYY-MM-DD');
let taskMoment = moment(currentDate + " " + taskTime, 'YYYY-MM-DD HH:mm');
if (taskMoment.valueOf() < thisMoment.valueOf()) {
return task;
}
let duration = moment.duration(taskMoment.diff(thisMoment));
let durationText = duration.hours() + " 小时 " + duration.minutes() + " 分 " + duration.seconds() + " 秒";
return task + " (还有 " + durationText + ")";
}
return task;
}

export default {
这样作了以后,parsedTaskList属性的内容就变成异常简洁了:

computed: {
parsedTaskList: function () {
let parsedTaskList = [];
for (let i=0; i<this.taskList.length; i++) {
parsedTaskList.push(addRemainTime(this.taskList[i]));
}
return parsedTaskList;
}
},
好了。在刷新页面以前不要忘记运行 npx webpack 来从新打包项目。完成后的效果以下

 

 

每次改完代码都要手动打包真的很烦!其实,有一个方法可让webpack自动跟踪你的改动,并自动打包

Webpack自动打包(watch)
经过带上 --watch参数,好比

npx webpack --watch
或者,在webpack.config.js中增长watch相关属性可让webpack自动的检测当前项目是否有变更,若是有变更webpack会自动打包。如下我采起在 webpack.config.js 中增长watch相关属性的方式来打开watch模式:

watch属性默认是关闭的。因此咱们须要在webpack.config.js中加上watch属性:

watch: true,
加上watch的设置

watchOptions: {
aggregateTimeout: 3000, // 编译的超时时间,单位:毫秒
poll: 30 // 扫描项目的间隔时间,单位:秒
},
改动后的webpack.config.js文件内容是

var path = require('path');
const { VueLoaderPlugin } = require('vue-loader')
const HtmlWebpackPlugin = require('html-webpack-plugin')

module.exports = {
mode: 'development',
entry: './src/main.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'bundle.js'
},
watch: true,
watchOptions: {
aggregateTimeout: 3000, // 编译的超时时间,单位:毫秒
poll: 30 // 扫描项目的间隔时间,单位:秒
},
module: {
rules: [
{
test: /\.vue$/,
loader: 'vue-loader',
},
{
test: /\.css$/,
use: [
'style-loader',
'css-loader'
]
}
]
},
plugins: [
new VueLoaderPlugin(),
// 如下是HtmlWebpackPlugin的配置
new HtmlWebpackPlugin({
template: 'index.html',
filename: './index.html',
hash: true
})
]
};
设置完watch属性后,咱们就可使用 npx webpack 来启动自动打包了

npx webpack
启动后命令行工具处于监听状态,一有代码改动就会自动打包

vagrant@homestead:~/Code/learn-vue$ npx webpack

webpack is watching the files…

Hash: a38478266809719e3c32
Version: webpack 4.12.1
Time: 3706ms
Built at: 2018-10-03 17:43:01
Asset Size Chunks Chunk Names
bundle.js 1.85 MiB main [emitted] main
./index.html 273 bytes [emitted]
[./node_modules/moment/locale sync recursive ^\.\/.*$] ./node_modules/moment/locale sync ^\.\/.*$ 2.91 KiB {main} [optional] [built]
[./node_modules/vue-loader/lib/index.js??vue-loader-options!./src/App.vue?vue&type=script&lang=js] ./node_modules/vue-loader/lib??vue-loader-options!./src/App.vue?vue&type=script&lang=js 136 bytes {main} [built]
[./node_modules/vue-loader/lib/loaders/templateLoader.js??vue-loader-options!./node_modules/vue-loader/lib/index.js??vue-loader-options!./src/App.vue?vue&type=template&id=7ba5bd90] ./node_modules/vue-loader/lib/loaders/templateLoader.js??vue-loader-options!./node_modules/vue-loader/lib??vue-loader-options!./src/App.vue?vue&type=template&id=7ba5bd90 259 bytes {main} [built]
[./node_modules/webpack/buildin/global.js] (webpack)/buildin/global.js 489 bytes {main} [built]
[./node_modules/webpack/buildin/module.js] (webpack)/buildin/module.js 497 bytes {main} [built]
[./src/App.vue] 1.02 KiB {main} [built]
[./src/App.vue?vue&type=script&lang=js] 246 bytes {main} [built]
[./src/App.vue?vue&type=template&id=7ba5bd90] 194 bytes {main} [built]
[./src/main.js] 269 bytes {main} [built]
+ 316 hidden modules
Child html-webpack-plugin for "index.html":
1 asset
[./node_modules/html-webpack-plugin/lib/loader.js!./index.html] 399 bytes {0} [built]
[./node_modules/webpack/buildin/global.js] (webpack)/buildin/global.js 489 bytes {0} [built]
[./node_modules/webpack/buildin/module.js] (webpack)/buildin/module.js 497 bytes {0} [built]
+ 1 hidden module
watch的反作用
watch的反作用就是cpu占用率会提升,个人macbook一运行 watch模式风扇的声音就变大,致使我一直没敢用这个模式。

为已完成任务增长删除线(v-if)
剩下最后一个需求了,那就是若是当前时间超过了计划时间,则任务须要变灰并增长删除线。咱们使用v-if来实现这个功能

经过在dom元素中增长 v-if="表达式" 咱们能够灵活的控制该dom元素的显示与否。就像这样:

<div v-if="type === 'A'">
A
</div>
<div v-else-if="type === 'B'">
B
</div>
<div v-else-if="type === 'C'">
C
</div>
<div v-else>
Not A/B/C
</div>
若是v-if中的表达式结果为true,则该元素会被渲染出来,反之则该元素不会被渲染。在这个例子中还用到了 v-else-if 和 v-else,有着丰富编程经验的你确定一下就看懂了它们的含义,因此在此我就不解释了。

跟v-show的区别
还有一个跟v-if用法很像的属性叫 v-show。一样也是定义一个表达式,根据表达式的返回结果来决定该元素是否出现。不一样的是v-if的表达式返回结果为false,则该元素彻底不出如今html中,而v-show无论表达式结果怎样都会渲染该元素,只是当表达式为false时为元素增长 display:none的样式而已。

好,如今咱们就来根据任务是否已经完成来显示不一样的任务样式。检验的条件是任务字符串中是否出现“还有 xx 小时 xx 分 xx 秒” 字样。

先把html模板改为

<tr v-for="task in parsedTaskList">
<td>
<span v-if="isDone(task)" style="color:gray;text-decoration:line-through;">{{ task }}</span>
<span v-else >{{ task }}</span>
</td>
</tr>
能够看到在v-if中咱们使用了一个函数isDone来判断该任务是否完成。因此咱们须要在method属性中增长isDone方法(如下方法的定义使用了ES2015语法)

isDone (task) {
let result = task.match(/还有\s[0-9]+\s小时\s[0-9]+\s分\s[0-9]+\s秒/);
return result == null || result.length == 0;
}
不使用ES2015语法的版本是

isDone: function (task) {
let result = task.match(/还有\s[0-9]+\s小时\s[0-9]+\s分\s[0-9]+\s秒/);
return result == null || result.length == 0;
}
若是你使用的是Chrome,那么就能够放心大胆的使用ES2015语法咯。

完成后,打包,刷新页面,效果以下

 

这样就完成了本节课的全部内容了。

method和computed有什么区别呢?
这是我学习vue时最大的疑问,我以为method和computed用法彻底就没区别!其实他们的区别在于:computed是带缓存的,若是被依赖的变量不发生变化,则下次调用computed时不会从新计算结果。可是method则是每次调用都会从新运行以得出实时的结果。

后记其实vue的官网教程已经写的很是棒了!没见过写的这么棒的官网文档,强力赞一个!因此本来不打算在更新新的文章了,因为有网友但愿我继续更新,因此我才继续又写了一篇。可是写文太费时间了。因此将来应该不会再更新了,感谢你们的支持!这是vue官网中文文档的学习传送门:https://cn.vuejs.org/v2/guide/--------------------- 做者:alexxiyang 来源:CSDN 原文:https://blog.csdn.net/nsrainbow/article/details/80892551 版权声明:本文为博主原创文章,转载请附上博文连接!

相关文章
相关标签/搜索