讀古今文學網 > Vue2實踐揭秘 > 第3章 路由與頁面間導航 >

第3章 路由與頁面間導航

真實的工程項目並不會像開篇舉出的示例僅需要一個頁面就能完成,一個完整的業務系統或者網站平台項目要編寫的頁面往往是幾十個甚至上百個,所以當建立工程化的項目結構後,擺在面前的問題就是:

(1)項目中應該有多少個頁面?

(2)頁面與頁面之間存在何種關係,應該如何進行導航?

(3)哪裡是程序的入口?應該先從哪個頁面開始入手?

先從這三個問題開始思考,我們就能給項目梳理出一個模糊的輪廓,找到項目的可視邊界。如果將每個頁面當作一項工作任務的話,那就是可以以此作為工作分解的依據,合理地分配人員與安排時間。

從思維導圖到網站地圖

本章將展示一個微信網上書店的示例,分析接到這樣的項目時我是如何通過思維導圖輔助來進行思考和設計的,如何將這個思維導圖進一步具象化、視覺化,再重新化為邏輯化的代碼的。

我很喜歡用思維導圖作為輔助我進行思考與設計的工具,如果經常使用它,你會發現一個小小的發散性的圖形工具能很有效地將很多雜亂無章的問題梳理得井井有條。如果你還沒有開始使用它,我推薦你可以看看思維導圖之父的一本書《思維導圖》(【英】東尼·博贊巴利·博讚著卜煜婷譯2015化學工業出版社)。你一定可以從那學到很多思考的方法。由於思維導圖的勾畫實在是太自由了,用於梳理一個網站的結構的話,我們只需要將每個頁面視為一個節點,思維路徑當作導航的路徑,這樣可以很快地得出下面這一張圖。

這個思維導圖很好地詮釋了整個微信網上書店的邏輯結構,整個系統的功能一目瞭然。但如果以此作為頁面間導航圖,也就是俗稱的網站地圖的話就還有欠缺。我們來為每個節點給予一個英文的命名,並且將多個提供給前台用的訪問入口進行合併,進一步梳理:

設計原型

有了清晰的網站地圖後,我們就可以用工具將這個在大腦中模糊的界面真實地呈現出來。我見過很多開發人員隨便拿張紙畫一下,又或者找些其他的圖形工具畫個示意圖就開始動手編碼。這種做法的結果是,只有設計者本人做出來的程序有可能與想像的一致,而絕大多數情況下拿這樣的草圖給10個程序員就會有10種不同的實現!設計必須明確細緻,界面設計將直接影響操作的行為,哪怕是簡單的線條顏色都應該標明具體的色值。欠下的技術債務始終要償還,在設計之初不細緻就會在交付期償還,這是多少前人從各種各樣的失敗中得到的不變鐵律。

所以,我寧願在設計圖上多花一點心思,尤其是與用戶交互的設計,盡力將圖紙做到與真實交付的產品是一致的。我推薦使用Sketch,這是一個非常實用的矢量圖工具,對於開發人員來說極易上手,因為它就是為了設計軟件原型圖而生的。無論是從Window、Web到App的原型圖,它可以確保我們能將原型圖做到與真實的產品毫無差異的程度,以下就是本示例中原型的一部分截圖。

有了網站地圖和設計原型,接下來我們就開始正式進入Vue,使用官方提供的路由庫vue-router為我們的項目建立動態、完整的程序骨架。

3.1 vue-router

從傳統意義上說,路由就是定義一系列的訪問地址規則,路由引擎根據這些規則匹配並找到對應的處理頁面,然後將請求轉發給頁進行處理。可以說所有的後端開發都是這樣做的,而前端路由是不存在「請求」一說的。前端路由是直接找到與地址匹配的一個組件或對象並將其渲染出來。改變瀏覽器地址而不向服務器發出請求有兩種做法,一是在地址中加入#以欺騙瀏覽器,地址的改變是由於正在進行頁內導航;二是使用HTML5的window.history功能,使用URL的Hash來模擬一個完整的URL。

Vue.js官方提供了一套專用的路由工具庫vue-router。vue-router的使用和配置都非常簡單,而且代碼清晰易讀,很容易上手。

將單頁程序分割為各自功能合理的組件或者頁面,路由起到了一個非常重要作用。它就是連接單頁程序中各頁面之間的鏈條,除了在本章中會重點對其用法通過開發實例進行詳細介紹,我還將其他一些關於路由的細小的運用方法分散在各個章節之中,既然它是「鏈條」,那麼在每個環節都將出現它的身影。

安裝
    $ npm i vue-router -D 

vue-router實例是一個Vue的插件,我們需要在Vue的全局引用中通過Vue.use 將它接入到Vue實例中。在我們的工程中,main.js 是程序入口文件,所有的全局性配置都會 在這個文件中進行。

打開main.js文件並加入以下的引用:

    import Vue from \'vue\'
    import VueRouter from \'vue-router\'
    Vue.use(VueRouter)  

這樣就完成了vue-router最基本的安裝工作了。

路由配置

接下來就需要開始一項對整個項目來說都起到關鍵性意義的工作了,這就是路由表的定義,或者叫路由配置。

在開始之前,我們得先建立一些基本的概念,這樣會更便於我們的設計與實現。

單頁式應用是沒有「頁」的概念的,更準確地說,Vue.js是沒有頁面這個概念的,Vue.js的容器就只有組件。但我們用vue-router配合組件又會重新形成各種的「頁」,那麼我們可以這樣來約定和理解:

(1)頁面是一個抽像的邏輯概念,用於劃分功能場景。

(2)組件是頁面在Vue的具體實現方式。

一定要謹記以上這兩點,因為在後面的內容中還會圍繞這個約定對我們的項目進行結構性的優化。

這裡的路由的定義方法就是將思維導圖演化為頁面導航圖 ,在上一節中我們已經設計出了一份完整的網站地圖:

我們先來實現網站地圖右側的功能,也就是面向最終用戶的前台功能。按照我們的設計,統一程序入口應該就是一個帶有分頁導航欄的頁面容器,用戶打開我們的程序看到的第一個頁面應該是「首頁」,也稱之為默認路由 ,而其他的頁面(分類、購物車、個人)都是根頁面,在Tab導航欄內的是頂層頁面。按照上文的約定:頁面就是組件,那麼一個路由定義就該與一個組件相對應,具體應該如下表所示。

名 稱 路 由 組 件 首頁 /home Home.vue 分類 /explorer Explorer.vue 購物車 /cart Cart.vue 我 /me Me.vue

*.vue文件是Vue的單頁式組件文件格式,它可以同時包括模板定義、樣式定義和組件模塊定義。

首先,我們在項目目錄下分別建立這四個頂層頁面的Vue組件文件:

    ├── src
    │    ├── App.vue
    │    ├── assets
    │    ├── Home.vue
    │    ├── Explorer.vue
    │    ├── Cart.vue
    │    ├── Me.vue
    │    └── main.js
    └── webpack.config.js  

這些新建的頁面組件內容暫時都可以是同樣的結構:

    <!--/Home.vue-->
    <template>
      <p>首頁</p>
    </template>
    <style></style>
    <script>
      export default {}
    </script>  

接下來就是在main.js文件中定義路由與這些組件的匹配規則了。VueRouter的定義非常簡單易懂,只需要創建一個VueRouter實例,將路由path指定到一個組件類型上就可以了,代碼如下所示。

    main.js
    import Vue from \'vue\'
    import VueRouter from \'vue-router\'
    import App from \'./App.vue\'

    // 引入創建的四個頁面
    import Home from \'./Home.vue\'
    

import Explorer from \'./Explorer.vue\'
    import Cart from \'./Cart.vue\'
    import Me from \'./Me.vue\'

    // 使用路由實例插件
    Vue.use(VueRouter)

    const router = new VueRouter({
        mode: \'history\',
        base: __dirname,
        routes:   

這樣定義以後,vue-router就會自動匹配所有/books/1、/books/2、…、/books/n形式的路由模式,因為這樣定義的路由的數量是不確定的,所以也被稱為「動態路由」。

在<router-link>中我們就可以加入一個params的屬性來指定具體的參數值:

    <router-link :to=\"{name:\'BookDetails\', params: { id: 1 }}\">
        <!-- ... -->
    </router-link>  

如果同時要傳遞多個參數,只要按以上的命名方法來加入參數,傳遞時在params中對應地聲明參數值即可,vue-router只要匹配到路由模式的定義就會自動對參數進行分解取值。

那在圖書詳情頁內又如何從路由中重新將這個:id參數讀取出來呢?做法非常簡單,可以通過$router.params這個屬性獲取指定的參數值,例如:

    export default {
        created  {
           const bookID = this.$router.params.id
        }
    }  

順便提一下,當使用路由參數時,例如從/books/1導航到books/2,原來的組件實例會被復用。因為兩個路由都渲染同一個組件,比起銷毀再創建,復用則顯得更加高效。不過,這也意味著組件的生命週期鉤子不會再被調用 ,也就是說created、mounted等鉤子函數在頁面第二次加載時將失效。那麼,當復用組件時,想對路由參數的變化做出響應的話,就需要在watch對像內添加對$route對像變化的跟蹤函數:

    export default {
      template: \'...\',
      watch: {
        \'$route\' (to, from) {
          // 對路由變化作出響應
        }
      }
    }  

$router.params定義的參數必然是整個路由的其中一部分,vue-router還可以讓我們使用「/path?參數=值」的方式,也就是俗稱的查詢字符串(Query string)傳遞數據。如果要從$router中讀取Query string的參數,可以使用$router.query.參數名的方式讀取。除了params和query,vue-router還提供一種常量參數定義meta,我們可以在路由定義中先定義meta的值,然後在路由實例中通過$router.meta參數獲取具體常量值。

嵌套式路由

當我們將前文中首頁的設計圖與圖書詳情頁的設計圖放在一起就會發現一個問題,如果按照之前的做法,那麼所有的頁面內都應該具有與首頁相同的底部導航條,也就是說如果按前文的App.vue結構定義是不可以導航到圖書詳情頁的,請看以下的示意圖:

此時就有必要對我們之前的路由結構按照界面的需要進行一次調整了。首先,所有的頁面都應該處於一個大的容器內,相應地路由就需要一個根入口,其導航效果應該如下圖所示。

由上圖可知,App.vue頁面除了<router-view>就不需要其他的元素了,說白了就是一個最大的頁面容器。原有導航部分的內容應該移到一個新的頁面上,也就是上圖中的Main.vue。Main.vue中的<router-view>相對於App.vue中的<router-view>,就是一個用於顯示子路由的視圖。

關於視圖的代碼此處就不再重複了,現在重點是怎樣重構routes.js中的路由配置,要將路由顯示到子視圖 中就要相應的子路由與之對應,那麼只要在路由定義中用children數組屬性就可以定義子路由,具體做法如下所示。

    export default = new VueRouter({
       mode: \'history\',
       base: __dirname,
       linkActiveClass: \"active\",
       routes: .[ext]?[hash]\'
              }
          }
       ]
    }  

這個意義在於,我們不需要再去在意程序引入了哪些資源,在發佈時應該對這些資源進行哪些處理,因為webpack已經為我們做了。

3.6 關於Fallback

由於我們將路由配置成History模式,假如用戶點擊Home上的<router-link>時,瀏覽器的地址欄就會自動改變成對應的URL(http://localhost/home)。如果我們直接在瀏覽器輸入http://localhost/home,你會驚奇地發現瀏覽器會出現404的錯誤!

這是由於直接在瀏覽器輸入http://localhost/home,瀏覽器就會直接將這個地址請求發送至服務器,先由服務器處理路由,而客戶端路由的啟動條件是要訪問/index.html,這樣的話客戶端路由就完全失效了!

解決的辦法是將所有發到服務端的請求利用服務端的URLRewrite模板重新轉發給/index.html,啟動VueRouter進行處理,而瀏覽器地址欄的URL保持不變。

這個問題在開發期是不會出現的,因為我們在開發環境中使用的是webpack的DevServer,DevServer是對這個問題進行了處理的,只要打開webpack.config.js,找到devServer配置屬性就可以見到:

       // ...
        

devServer: {
           historyApiFallback: true
        },  

而當我們部署到生產環境時,就需要在Web服務器上進行一些簡單配置以支持Fallback了。

Apache

如果使用Apache就要在它的配置文件內加入以下URLRewrite模塊的配置:

    <IfModule mod_rewrite.c>
      RewriteEngine On
      RewriteBase /
      RewriteRule ^index.html$ - [L]
      RewriteCond %{REQUEST_FILENAME} !-f
      RewriteCond %{REQUEST_FILENAME} !-d
      RewriteRule . /index.html [L]
    </IfModule>  
Nginx

Nginx則更加簡單,當出現404時將自動重定向至index.html:

    location / {
      try_files $uri $uri/ /index.html;
    }  
Node.js (Express)

如果使用Nodek.js作為服務端的話,可以安裝一個Fallback插件以支持此功能,可以到https://github.com/bripkens/connect-history-api-fallback下載並安裝此插件。

其他後端程序

如果你使用的是Python或者Ruby on Rails這一類後端程序,單純修改Web服務端的設置是不夠的,因為Nginx或者Apache會將請求通過語言解釋插件轉發至Python或者Rails的處理程序,由它們的路由系統去判定應如何操作,所以我們只能在後端程序中加入一些特殊的處理以支持Fallback。

Flask(Python)

如果使用Flask的話,增加Fallback會比較簡單,只要增加一個全局的錯誤捕獲裝飾器進行重定義即可:

    from flask import Flask,render_template
    

app = Flask(__name__)

    @app.route(\'/\')
    def index:
      return render_template(\'index.html\')

    @app.app_errorhandler(404)
    def api_fall_back(e):
      return index  
Ruby on Rails

以下是Rails的Fallback支持,假定index頁面在HomeController下的是Action,那麼在路由文件內的設置將是這樣的:

    # ~/config/routes.rb
    root \'home#index\'

    # ....

    match \'path*\', :to \'home#index\'  

注意

一旦我們進行了上述的配置,你的服務器就不再返回404錯誤頁面,因為對於所有路徑都會返回index.html文件。為了避免發生這種情況,應該在Vue應用裡面覆蓋所有的路由情況,然後再給出一個404頁面。

    const router = new VueRouter({
      mode: \'history\',
      routes: [
        { path: \'*\', component: NotFoundComponent }
      ]
    })  

或者,如果用Node.js開發後台,可以使用服務端的路由來匹配URL,當沒有匹配到路由的時候返回404,從而實現Fallback。

3.7 小結

本章的內容雖短,但卻融合了我多年Web項目開發的經驗,無論項目的大與小,路由 定義必然是最重要的第一件工作。思維導圖中畫出的站點地圖是一種純粹的邏輯思維過程,也可以說是一個最簡單的設計過程。這個過程設計的是Web程序的邊界和用戶導航的路徑,我們必須確保有足夠的頁面來完成相應的工作流程,每個頁面之間都能順暢地互相導航,否則就會出現死鏈或者死節點的情況。而路由定義則可以切實地實現這一藍圖,從一開始就將要開發的頁都做出來,內容可以是空的,但在將它運行到瀏覽器中時應該確保每個頁面之間都能與我們設計的網站地圖的導航方式一致,因為這是確定業務流程與導航流程是否一致的一種最佳實踐。