本章將通過極具代表性的Todo的示例作為引領讀者進入Vue.js大門的引子。我會以實踐為第一出發點,從零開始一步一步地構造一個單頁式的Todo應用,在這個過程中會將Vue.js相關的知識點融入其中,在實際應用中展現這個「小」而「強」的界面框架。
我們先來看看最終希望構造出一個什麼樣的App:
Vue.js與Angular2和React相比,讓我感覺最舒適的是它在一開始就為我們鋪平了入門的道路,這就是它的腳手架vue-cli。因為它的存在,省去了手工配置開發環境、運行環境和測試環境的步驟,開發者可以直接步入Vue.js開發的殿堂。然而,現在我並不打算詳細地介紹這個腳手架工具,先讓我們一起從使用體驗來感性地認識它,在後面的章節中我會詳細地介紹這個工具。
在開始動手之前,必須先得在機器上安裝好npm,然後輸入以下指令將vue-cli安裝到機器的全局環境中:
$ npm i vue-cli -g
然後,我們就可以開始建立工程了,鍵入以下的指令:
$ vue init webpack-simple vue-todos
此時控制台會提出一些關於這個新建項目的基本問題,直接「回車」跳過就行了。然後進入vue-todo目錄,安裝腳手架項目的基本支持包:
$ npm i
安裝完支持包後鍵入以下指令就可以運行一個由腳手架構建的基本Vue.js程序了:
$ npm run dev
是不是很簡單?進入代碼中看看vue-cli到底為我們構造了一個什麼樣的代碼結構:
├── README.md ├── index.html # 默認啟動頁面 ├── package.json # npm 包配置文件 ├── src │ ├── App.vue # 啟動組件 │ ├── assets │ │ └── logo.png │ └── main.js # Vue 實例啟動入口 └── webpack.config.js # webpack 配置文件
Vue2與Vue1.x相比有了很大的區別,從最小化的運行程序開始瞭解Vue是一種絕佳的途徑,先從main.js文件入手:
import Vue from \'vue\' import App from \'./App.vue\' new Vue({ el: \'#app\', render: h => h(App) })
這裡就運用了Vue2新增的特色Render方法,如果你曾用過React,是不是有一種似曾相識之感?確實,Vue2甚至連渲染機制都與React一樣了。為了得到更好的運行速度,Vue2也採用了Virtual DOM。如果你還沒有接觸過Virtual DOM,並不要緊,現在只需要知道它是一種比瀏覽器原生的DOM具有更好性能的虛擬組件模型就行了,我們會在稍後的章節中再來討論它。
我們需要知道的是,通過import將一個Vue.js的組件文件引入,並創建一個Vue對象的實例,在Vue實例中用Render方法來繪製這個Vue組件(App)就完成了初始化。
然後,將Vue實例綁定到一個頁面上,真實存在的元素App Vue程序就引導成功了。
打開index.html文件就能看到Vue實例與頁面的對應關係:
<!DOCTYPE html> <html lang=\"en\"> <head> <meta charset=\"utf-8\"> </head> <body> <!-- Vue實例所對應的頁面元素 --> <p></p> <!-- 由Webpack編譯後的運行文件 --> <script src="https://p.2015txt.com//dist/build.js\"></script> </body> </html>
也就是說,一個Vue實例必須與一個頁面元素綁定。Vue實例一般用作Vue的全局配置來使用,例如向實例安裝路由、資源插件,配置應用於全局的自定義過濾器、自定義指令等。在本章示例中,我們只需要知道它的作用就可以了。
我們需要瞭解的是App.vue這個文件,*.vue是Vue.js特有的文件格式,表示的就是一個Vue組件,它也是Vue.js的最大特色,被稱為單頁式組件。「*.vue」文件可以同時承載「視圖模板」、「樣式定義」和組件代碼,它使得組件的文件組織更加清晰與統一。
Vue.js的組件系統提供了一種抽像,讓我們可以用獨立可復用的小組件來構建大型應用。如果我們考慮到這一點,幾乎任意類型應用的界面都可以抽像為一個組件樹:
Vue2具有很高的兼容性,我們也可以用「.js」文件來單純地定義組件的邏輯,甚至可以使用React的JSX格式的組件(需要babel-plugin-transform-vue-jsx支持)。
腳手架為我們創建的這個App組件內加入了不少介紹性的文字,將這個文件「淨化」後就可以得到一個最簡單的Vue組件定義模板:
<template> <p> </p> </template> <style></style> <script> export default { name: \'app\' } </script>
由以上的代碼我們可以瞭解到,單頁組件由以下三個部分組成:
● <template>——視圖模板;
● <style>——組件樣式表;
● <script>——組件定義。
接下來我們就從這個示例開始,一步步學習Vue的基本組成部分,在實踐中理解它們的作用。
1.1 插值
Vue的視圖模板是基於DOM實現的。這意味著所有的Vue模板都是可解析的有效的HTML,而且它對一些特殊的特性做了增強。接下來,我們就在模板上定義一個網頁標題,並通過數據綁定語法將App組件上定義的數據模型綁定到模板上。
首先,在組件腳本定義中使用data定義用於內部訪問的數據模型:
export default { ... data { return { title: \"vue-todos\" } } }
data可以是一個返回Object對象的函數,也可以是一個對像屬性,也就是說,可以寫成以下的方式:
export default { ... data : { title: \"vue-todos\" } }
使用函數返回是為了可以具有更高的靈活性,例如對內部數據進行一些初始化的處理,官方推薦的用法是採用返回Object對象的函數。
在模板中引用data.title數據時我們並不需要寫上data,這只是Vue定義時的一個內部數據容器,通過Vue模塊的插值方式直接寫上title即可:
<h1>{{ title }}</h1>
用雙大括號{{ }}引住的內容被稱為「Mustache」語法,Mustache標籤會被相應數據對象的title屬性的值替換。每當這個屬性變化時它也會更新。
插值是Vue模板語言的最基礎用法,很多的變量輸出都會採用插值的方式,而且插值還可以支持JavaScript表達式運算和過濾器(下文將會提及)。{{}}引用的內容都會被編碼,如果要輸出未被編碼的文本,可以使用{{{}}}對變量進行引用。
完整代碼如下所示。
<template> <p> <h1>{{ title }}</h1> </p> </template> <style></style> <script> export default { name: \'app\', data { return { title: \"vue-todos\" } } } </script>
從Vue2開始,組件模板必須且只能有一個頂層元素,如果在組件模塊內設置多個頂層元素將會引發編譯異常。
請注意,在上述代碼中template屬性是V,也就是視圖,title屬性是M,也就是模型,這個概念是必須要瞭解的。
1.2 數據綁定
我們需要一個稍微複雜一點的數據模型來表述Todo,它的結構應該是這樣的:
{ value: \'事項1\', // 待辦事項的文字內容 done: false // 標記該事項是否已完成 }
由於是多個事項,那麼這個數據模型應該是一個數組,為了能先顯示這些待辦事項,我們需要先設定一些樣本數據。在Vue實例定義中的data屬性中加入以下代碼:
export default { data { return { title: \'vue-todos\', todos: [ { value: \"閱讀一本關於前端開發的書\", done: false }, { value: \"補充範例代碼\", done: true }, { value: \"寫心得\", done: false } ] } } }
初學者可能會問data有什麼作用?我們可以將Vue實例定義看作一個類的定義,data相當於這個類的內部字段屬性的定義區域。在Vue實例內的其他地方可以直接用this引用data內定義的任何屬性,比如this.title就是引用了data.title。
我們要顯示todos的數據就需要使用Vue模板的一個最常用的v-for指令標記,它可以用於枚舉一個數組並將對像渲染成一個列表。這個指令使用與JS類似的語法對items進行枚舉,形式為item in items,items是數據數組,item是當前數組元素的別名:
<ul> <li v-for=\"todo in todos\"> <label>{{ todo.value }}</label> </li> </ul>它的輸出結果如下所示。
<ul> <li> <label>閱讀一本關於前端開發的書</label> </li> <li> <label>補充範例代碼</label> </li> <li> <label>寫心得</label> </li> </ul>
如果我們要輸出待辦事項的序號,可以用v-for中隱藏的一個index值來進行輸出,具體用法如下:
<ul> <li v-for=\"(todo,index) in todos\" :id=\"index\"> <label>{{ index + 1 }}.{{ todo.value }}</label> </li> </ul>
這個用法有點像Python的元組引用方式,只要用括號括住引用參數,最後一個值就是循環的索引。索引是由0開始計數的,而我們要輸出的序號應該從1開始,正好我們使用了一個JavaScript的表達式插值來輸出一個index+1的從1開始計數的序號。
這裡除了用插值綁定,還使用了屬性綁定語法,就是上面的id=\"index\",這樣的寫法是一種縮寫,下文中會有解釋,意思是將index的值輸出到DOM的id屬性上。如果index=1,那麼輸出結果就是id=\"1\",如果沒有在id前面加上「:」,那麼Vue就會認為我們正在為id屬性賦予一個字符串。
完成這一步,我們打開終端輸入:
$ npm run dev
npm將自動打開流瀏覽器並顯示以下的結果:
v-for不單單可以循環渲染數組,還可以渲染對像屬性,例如:
<ul> <li v-for=\"value in object\"> {{ value }} </li> </ul> data { return { object { first_name : \"Ray\", last_name : \"Liang\" } } }
輸出
● \"Ray\"
● \"Liang\"
小結
對於從來沒有接觸過Angular和Vue的初學者,可能對上述的代碼感到疑惑,為什麼我們的代碼內沒有任何一個地方操作DOM並且將data內的變量設置到DOM上面呢?
首先,在Vue的代碼中直接操作DOM是不被推薦的,如果你之前是jQuery的開發者,這一點一定要牢記;其次,DOM是被Vue直接托管的,所有「綁定」到DOM上的變量一旦發生變化,DOM所對應的屬性就會被Vue自動重繪而不需要像jQuery那樣通過編碼來顯式地操作,這才是綁定的意義所在。
1.3 樣式綁定
沒有樣式的輸出結果樣子很醜,此時我們就需要用CSS來美化我們的App。我個人並不推薦直接使用CSS語法來編寫樣式表,因為純CSS的代碼量很大,而且需要不斷地重複,我很討厭重複而且對DRY(Don\'t Repeat Yourself)有一種偏執。由於CSS總是充滿各種不得不重複的寫法,所以我更願意使用less,以下是安裝webpack支持less編譯的包的方法:
$ npm i less style-loader css-loader less-loader -D
安裝完成後在webpack.config.js的modules設置內加入以下的配置:
module : { rules: [ // ...省略 { test: /.less$/, loader: \"style!css!less\" } ] }
在/assets/中添加一個todos.less文件,並在App.vue的組件定義內引入less樣式表:
import \'./assets/todos.less\' export default { // ...省略 }
使用import將樣式表直接導入到代碼的效果是:webpack的less-loader會生成一些代碼,在頁面運行的時候將編譯後的less代碼生成到<style>標籤內並自動插入到頁面的 <head>中。有一點要注意的是,這種做法是全局的,在後面介紹路由部分時會有多個組件頁面加載到同一個頁上,如果使用import導入樣式的話,樣式就會長期駐留頁面直至Vue的根(root)實例被銷毀。
關於這個less樣式表的定義屬於HTML的基礎,由於篇幅問題就不在此羅列出來了,讀者可以到本書的github地址http://www.github.com/dotnetage/vue-in-action上下載。
運行效果如下:
現在終於舒服多了。這裡所有的待辦事項都沒有顯示任何的狀態,此時就需要使用Vue的樣式綁定 功能了。
通過import將樣式文件導入是一種全局性的做法,也就是說,在每一個頁面內的<head>中都會有這一個樣式表,這樣做的缺點是很容易導致樣式衝突。如果希望樣式表僅應用於當前組件,可以使用<style scoped>,然後用CSS的@import導入樣式表:
<style scoped> @import \'./assets/todos.less\' </style>
前文我們只提到如何將data內定義的值以文本插值的方式輸出到頁面,並沒有介紹如何將值「綁定」到屬性內。樣式的綁定和屬性的綁定方式是一樣的,我們這裡就將done==true的待辦事項<li>綁定一個checked的樣式類:
<li v-for=\"(todo,index) in todos\" :class=\"{\'checked\': todo.done}\" > <!-- 省略... --> </li>
Vue的屬性綁定語法是通過v-bind實現的,完整的寫法是這樣:
<li v-for=\"(todo,index) in todos\" v-bind:class=\"{\'checked\': todo.done}\">
但v-bind可以採用縮寫方式「:」表示,採用完整寫法又將出現各種重複,所以建議還是直接使用縮寫方式,這樣會更直觀。
由此可見,Vue的屬性綁定語法是attribute=\"expression\",attribute就是元素接收的屬性值(既可以是原生的也可以是自定義的),expression則是在Vue組件內由data或props內定義的對象屬性,又或是一個合法的表達式。
要謹記一點: 如果在元素屬性中不加上「:」,Vue認為是向這個屬性賦上字符串值而不是Vue組件上定義的屬性引用!
上例中:class=\"{\'checked\': todo.done}\"的意思是:當todo.done為true時,向<li>元素的class添加checked樣式類。這是Vue樣式綁定與普通屬性綁定最大的不同點,凡是樣式綁定必然是綁定到判斷對像上的,不能直接寫CSS類名,即使要綁定一個固定的CSS類也都要這樣寫,即:class=\"{\'btn\':true}\",除非不使用樣式綁定。
以下是應用樣式綁定後的輸出效果:
小結
這裡推薦一個簡單的記憶方法來學習Vue的樣式綁定,無論綁定的是樣式類還是樣式屬性,:class和:style表達式內一定是一個JSON對象。
● :class的JSON對象的值一定是布爾型的,true表示加上樣式,false表示移除樣式類。
● :style的JSON對像則像是一個樣式配置項,key聲明屬性名,value則是樣式屬性的具體值。
1.4 過濾器
我們在待辦事項的右側增加一個時間字段created,並用<time>元素表示,修改後完整的代碼如下所示。
<template> <p> <h1>{{ title }}</h1> <ul> <li v-for=\"(todo,index) in todos\" :class=\"{\'checked\': todo.done}\"> <label>{{ index + 1 }}.{{ todo.value }}</label> <time>{{ todo.created }}</time> </li> </ul> </p> </template> <script> import \'./assets/todos.less\' export default { name: \'app\', data { return { title: \'vue-todos\', todos: [ { value: \"閱讀一本關於前端開發的書\", done: false, created : Date.now }, { value: \"補充範例代碼\", done: true , created: Date.now + 300000 }, { value: \"寫心得\", done: false , created: Date.now - 30000000 } ] } } } </script>
查看輸出結果:
很明顯,時間的輸出並不是我們想要的結果,這裡輸出的是一個整數,因為將Date對像直接輸出的話,JavaScript引擎會將其時間戳作為值輸出,所以我們需要對這個時間戳來一個漂亮的格式化。
此時我們可以用一個很出名的時間格式化專用的包——moment.js,先安裝moment.js:
$ npm i moment -S
Vue.js用「過濾器」進行模板格式化,過濾器實質上是一個只帶單一輸入參數的函數,在Vue2中已經將原有的內置過濾器移除了,甚至將一些相關的特色功能也移除了,例如雙向過濾器。官方的說法是「計算方法」會比使用「過濾器」更明確,代碼更容易讀。我認為這有點矯枉過正,過濾器並不是Vue和Angular這類前端框架所獨有的,在很多的服務端視圖框架中也是一種很常見的用法。過濾器有用的地方是可以以管道方式進行傳遞調用。在此,對日期的格式化我還是傾向於使用過濾器的方式,在Vue組件中加入自定義過濾器非常簡單,只要在filters屬性內加入方法定義就可以在模塊上使用了。
首先,我們要引入moment,並設定moment的區域為中國:
import moment from \'moment\' import \'moment/locale/zh-cn\' moment.locale(\'zh-cn\')
然後加入一個date的過濾器:
export default { // 省略... filters: { date(val) { return moment(val).calendar } } }
最後在模板上應用這個過濾器:
<time>{{ todo.created | date }}</time>
我們可以看到瀏覽器的顯示結果將變為下圖的方式:
在所有的過濾器中是沒有this引用的,過濾器內的this是一個undefined的值,所以不要在過濾器內嘗試引用組件實例內的變量或方法,否則會引發空值引用的異常。