首页 » Vue2实践揭秘 » Vue2实践揭秘全文在线阅读

《Vue2实践揭秘》 第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导航栏内的是顶层页面。按照上文的约定:页面就是组件,那么一个路由定义就该与一个组件相对应,具体应该如下表所示。

名 称路 由组 件首页/homeHome.vue分类/explorerExplorer.vue购物车/cartCart.vue我/meMe.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程序的边界和用户导航的路径,我们必须确保有足够的页面来完成相应的工作流程,每个页面之间都能顺畅地互相导航,否则就会出现死链或者死节点的情况。而路由定义则可以切实地实现这一蓝图,从一开始就将要开发的页都做出来,内容可以是空的,但在将它运行到浏览器中时应该确保每个页面之间都能与我们设计的网站地图的导航方式一致,因为这是确定业务流程与导航流程是否一致的一种最佳实践。