讀古今文學網 > Vue2實踐揭秘 > 第1章 例說Vue.js >

第1章 例說Vue.js

本章將通過極具代表性的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的值,所以不要在過濾器內嘗試引用組件實例內的變量或方法,否則會引發空值引用的異常。