名字灵感来自我的书《HBase不睡觉书》 意为让你看了也不会想睡觉的教程 :)

目标

做出一个待办列表。这个待办列表有以下特点

  1. 可以自动从文本中抽取出这件事情的开始时间
  2. 可以显示当前距离这件事情的开始时间还有多久,比如:23:40 回家 (还有 6 小时 36 分 15 秒)
  3. 如果当前时间已经超过了计划时间,则以灰色字体显示任务,并加上删除线

通过这个例子我们可以学到以下知识点

  1. v-for属性
  2. v-bind:key属性
  3. v-on属性
  4. 在vue中使用bootstrap
  5. 在vue中使用localStorage
  6. watch属性
  7. computed属性
  8. 在vue中定义私有方法
  9. webpack自动打包
  10. v-if, v-else-if, v-else属性
  11. v-show属性

背景

注意事项

 在说本节课的步骤之前,先提醒大家,该完代码记得用以下命令编译后才能用浏览器看到你的更改

npx webpack

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

创建TodoList组件

修改App.vue

我们先来构建项目框架。这个项目只有一个组件:TodoList。

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

import TodoList from './components/TodoList.vue'

然后在template模板代码块中引用它,并在components对象中引用它。修改完的App.vue是这样的:

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

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

export default {
  name: 'app',
  components: {
    TodoList
  }
}
</script>

新建TodoList.vue组件

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

<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 。如果成功,你就可以 看到 “这是一个待办列表” 的字样。

显示任务列表(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>

显示的效果为:

Vue 不睡觉教程3 - TodoList-LMLPHP

如果你的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为了性能考虑,默认复用页面上的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("新的待办任务");
    }
  }
}

执行效果就是,每次点击添加按钮,就会新增一个任务

Vue 不睡觉教程3 - TodoList-LMLPHP

美化页面(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

然后再来看看我们的页面:

Vue 不睡觉教程3 - TodoList-LMLPHP

 

这下好看多了。

添加任务功能

接下来,我们增加“添加任务”的功能。首先我们要添加一个用来输入任务内容的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>

将其改造成我们需要的样子:

  1. 将<button>元素的文字改为添加并加上v-on:click="addTask"属性
  2. 将<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对象,页面上的任务列表也会跟着变动。现在你可以试试在任务内容框中输入任务的内容,然后点击添加按钮:

Vue 不睡觉教程3 - TodoList-LMLPHP

任务存储: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中被实际的定义出来,而是通过对实际的变量进行了计算而得出的。在这个例子中我们的需求是:

  1. 列表要能够自动计算出任务的剩余快完时间,比如:23:40 回家 (还有 6 小时 36 分 15 秒)
  2. 如果当前时间已经超过了计划时间,则不显示剩余完成时间

此时就需要用到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 来重新打包项目。完成后的效果如下

Vue 不睡觉教程3 - TodoList-LMLPHP

 

每次改完代码都要手动打包真的很烦!其实,有一个方法可以让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语法咯。

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

Vue 不睡觉教程3 - TodoList-LMLPHP

这样就完成了本节课的所有内容了。

后记

其实vue的官网教程已经写的非常棒了!没见过写的这么棒的官网文档,强力赞一个!所以原本不打算在更新新的文章了,由于有网友希望我继续更新,所以我才继续又写了一篇。但是写文太费时间了。所以未来应该不会再更新了,感谢大家的支持!这是vue官网中文文档的学习传送门:https://cn.vuejs.org/v2/guide/

 

 

10-04 09:57