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 版權聲明:本文爲博主原創文章,轉載請附上博文連接!

相關文章
相關標籤/搜索