后端的路由就是URL与对应页面的映射关系,从用户的角度来说,就是不同的URL地址对应不同的页面展示。前端路由就是把这块工作放到前端来,不同的URL对应不同的前端渲染的页面。

现在的前端路由一般都采用HTML5的History模式了,即可以真正的切换URL,而不向过去一样采用锚点。

Vue官方提供了路由组件叫做Vue Router,由于这个路由是以插件的形式提供的,所以在模块化的开发,也就是前端工程里,必须在源码里启用路由功能,看一下具体操作吧。

这里依然使用之前Webpack搭建的Vue环境。

  1. 安装Vue Router
  2. 初步使用路由
  3. 路由应用
  4. 高级应用

安装Vue Router

安装分为两步:

  1. 安装Vue-router本身:npm install vue-router
  2. main.js中启用路由插件:
    import Vue from 'vue'
    import VueRouter from 'vue-router'
    
    Vue.use(VueRouter)

不管是前端路由还是后端路由,其本质没有改变,依然要匹配访问的URL和对应的组件。现在就来简单配置一下,做一个简单的例子。

初步使用路由

所谓展示不同的页面,在Vue中,其实就是挂载不同的组件。所以必须要设置一组规则,让URL和要挂载的组件建立映射关系。

这里可以把原来试验性质的单文件组件都删除掉,只保留作为入口的index.js文件和app.vue,然后在src目录里创建两个新的组件index.vueabout.vue

//index.vue
<template>
    <div>欢迎来到首页</div>
</template>

<script>
    export default {
        name: "about"
    }
</script>

<style scoped>
    div {
        font-size: 2rem;
        text-align: center;
    }
</style>

//about.vue
<template>
    <div>关于本站</div>
</template>

<script>
    export default {
        name: "about"
    }
</script>

<style scoped>
    div {
        font-size: 2rem;
        text-align: center;
    }
</style>

其实是两个再简单不过的组件,只有一个标题,没有其他内容。

然后需要在index.js设置路由,主要有以下步骤:

  1. 一般创建一个数组作为路由表,每一个元素是一个路由匹配规则对象。这个对象path属性对应URL,而component属性对应组件对象。
  2. 使用这个路由表数组,使用new VueRouter({routes:路由表数组})创建路由对象。
  3. 根实例渲染的组件需要编写特殊的渲染路由的标签
  4. 将路由对象设置为根实例router属性的值,然后挂载根实例。
  5. 如果要使用Webpack-dev-server,需要加上一个参数--history-api-fallback,所有的路由都会指向index.html

根据这个顺序,来编写一下index.js文件:

import Vue from "vue";
import VueRouter from "vue-router"

import App from "./app.vue";
import Index from "./index.vue";
import About from "./about.vue";

Vue.use(VueRouter);

//1 创建路由表数组
const routes = [
    {
        path: "/index",
        component: Index
    },
    {
        path: "/about",
        component: About
    }
];

//2 使用路由表数组创建路由对象

const router = new VueRouter({
    mode: "history",
    routes
});

注意标红的这一行,是指定路由模式为HTML5HISTORY模式,如果不指定,会变成锚点。现代开发一般都是使用HISTORY模式。

在挂载根实例之前还需要修改app.vue

//3 根实例组件使用特殊的标签router-view,根据URL加载不同组件。
<template>
    <div>
        <router-view></router-view>
    </div>
</template>

<script>
    export default {
        name: "app"
    }
</script>

<style scoped>
</style>

之后需要挂载根实例,继续在index.js中编写:

//4 创建Vue根实例并挂载app,路由动态渲染的标签写在app.vue中。

const app = new Vue({
    router,
    render: h => h(App)
}).$mount("#app");

还有最后一步,前端路由需要所有的页面都要指向index.html,也就是让挂载的根实例发生作用才能解析,所以需要让DevServer把路由全部指向index.html,可以在package.json里加上一条命令:

//5 配置Webpack-dev-server
"scripts": {
    "dev": "webpack-dev-server --open --history-api-fallback"
},

之后使用npm run dev就可以启动DevServer了。

访问首页,然后在地址栏中输入http://localhost:8080/indexhttp://localhost:8080/about,可以看到在会根据不同的URL挂载不同的组件,页面显示出不同的文字和样式。

这是因为<router-view>会根据当前路由动态渲染配置好的组件。网页上一些不需要每次重新加载和渲染的内容比如导航条,可以写在app.vue里与<router-view>同级,就比较好看了。

路由应用

来看一下路由的一些其他应用。

路径重定向

首先是在路由列表里可以增加通配符,如果找不到,就重定向到首页或者其他路径,有点像Django里最后找一个通配符路径接着其他所有路径,也可以增加具体的重定向:

const routes = [
    {
        path: "/index",
        component: Index
    },
    {
        path: "/about",
        component: About
    },
    {
        path: "/query",
        redirect: "/about"
    },
    {
        path: "*",
        redirect: "/index"
    }
];

一般在找不到页面的时候会返回一个自制的404页面。所以一般在匹配规则里列出所有可能的URL,然后使用通配符,匹配其他路径到自己编写的404组件中。

动态路由匹配

这个也是Web开发的重头戏,所谓动态路由匹配就是获取URL变量以便操作。这个需要两部分来配合:

  1. 路由对象中设置动态路由参数,类似path: '/user/:id'
  2. 匹配的参数会被传递到this.$route.params,可以在每个组件中使用。在组件中访问this.$route得到的是当前这个组件的路由对象。

注意,我们的项目里有一个路由器对象,使用this.$router访问,不同的组件内访问路由器,都是同一个路由器对象。

this.$route是每个组件不同的,是组件自己的路由对象,不是路由器对象,这个要区分开。

此外还有$route.queryhash等用于获取URL查询和锚点的信息。

用一个例子来说明,在路由表数组内添加一个新匹配规则:

{
    path: "/user/:id",
    component: User
},

然后编写一个对应的user.vue组件:

<template>
    <div>
        <p>当前用户的id是 {{$route.params.id}}</p>
    </div>
</template>

<script>
    export default {
        name: "user",
        mounted() {
            console.log("获取了用户ID");
            console.log(this.$route.params.id);
        }
    }
</script>

<style scoped>
    div p {
        color: brown;
        font-size: 1.5rem;
    }
</style>

URL中的id匹配到的值就会存储在this.$route.params.id同名的属性中,取出来就可以进行操作,比如在加载组件的时候根据用户ID发送AJAX请求获取用户信息等。

还可以使用多个参数匹配,比如:

{
    path: "/post/:year/:month",
    component: Post
},

Vue的路由匹配规则使用了path-to-regexp插件,使用正则也可以,进一步学习可以看这里。

还需要注意的是这个很类似Django的规则,写在前边的路由先匹配,匹配成功就不会继续匹配后边的路由,所以路由表数组的顺序很重要。

导航

所谓导航,就是点击链接跳转。一般有两种方式:

  1. 使用router-link标签生成链接
  2. 使用JavaScript来捕获点击事件然后处理

先来看router-link标签,这个标签实际上会默认生成A标签,依照标签的属性生成对应的链接和文字。

修改app.vue的模板部分和绑定一个v-model用来拼接URL:

<template>
    <div>
        <h1>柚子小站</h1>
        <p>
            <router-link to="/index">Go to index</router-link>
            <router-link to="/about">Go to about</router-link>
            <label>输入用户ID
                <input type="number" v-model="url"><router-link :to="'/user/'+url">跳转到用户ID界面</router-link>
            </label>
        </p>
        <div>
            <router-view></router-view>
        </div>
    </div>
</template>

<script>
    export default {
        name: "app",
        data:function () {
            return {
                url: ""
            }
        }
    }
</script>

<style>
    h1 {
        text-align: center;
        background-color: black;
        color: orange;
        margin-top: 0;
    }

    a {
        display: block;
    }

    body{
        margin: 0;
        padding: 0;
    }
</style>

浏览器中三个router-link被渲染成了:

<a href="/index" class="router-link-exact-active router-link-active">Go to index</a>
<a href="/about" class="">Go to about</a>
<a href="/user/" class="">跳转到用户ID界面</a>

其中第三个链接没有输入,就不会拼接字符串。在input中输入用户id,再点击链接就可以跳转到对应的URL了。

router-link有很多属性,默认情况下会被渲染为A标签,其他常用的有:

  1. tag,指定渲染为什么标签,比如tag=”li”就会渲染成LI标签
  2. replace,一个属性,加上就可以生效,不会在HISTORY里留下记录,所以无法回退
  3. active-class,如果点击的链接的路由匹配成功,会自动给当前对应的这个标签添加一个router-link-active,如果精确匹配,还会添加一个router-link-exact-active。可以修改这个样式名称,但一般不推荐修改。

A标签浏览器默认就有跳转功能,再来看看使用JavaScript跳转,这种方式经常使用在BUTTON元素上,用于自定义需要跳转的内容。

给about.vue添加一个按钮跳转到66号用户:

<template>
    <div>
        <p>关于本站</p>
        <button @click="handleClick">跳转到66号用户</button>
    </div>
</template>

<script>
    export default {
        name: "about",
        methods: {
            handleClick() {
                this.$router.push("/user/66");
            }
        }
    }
</script>

<style scoped>
    div {
        font-size: 2rem;
        text-align: center;
    }
</style>

这里使用了路由器对象:this.$router.push("/user/66");,调用其push方法,就会“推动”路由器转到其中的路径。

这个push()方法。实际上是向HISTORY栈里加入一条数据,所以页面会转向最新的URL,还可以使用回退来返回上一个路径。实际上<router-link to="/about">Go to about</router-link>内部就是调用这个方法。

$router路由器对象还有其他功能:

  1. replace,与路由对象的replace方法类似,不在HISTORY中留下记录。
  2. go(Number),和window.history.go()类似,指定回退的步数。

嵌套路由

一个URL实际上可以认为是一个目录,所以可能会有嵌套的路由用于结构化配置路由。类似/users/admin/team1/66这种URL,直接写成一个单独的路由,不如使用嵌套路由更加方便修改和扩展。

嵌套的路由使用起来其实很简单,就是在组件中再嵌入<router-view></router-view>,然后在路由表数组中给对应的一级路由对象使用children配置。children属性的内容,也是一个路由表数组。

举个简单的例子,现在想匹配/index/home,采用嵌套路由的方式:

const routes = [
    {
        path: "/index",
        component: Index,
        children: [
            {
                path: "home",
                component: Home
            }
        ]
    },

然后在index组件内增加一个路由出口,创建home.vue组件:

// index.vue
<template>
    <div>
        <p>首页内容</p>
        <router-view></router-view>
    </div>
</template>

<script>
    export default {
        name: "about"
    }
</script>

<style scoped>
    div {
        font-size: 3rem;
        text-align: center;
    }
</style>

//home.vue
<template>
    <div>HOME组件的内容</div>
</template>

<script>
    export default {
        name: "home"
    }
</script>

<style scoped>

</style>

这样配置之后,访问/index,不会渲染出home.vue组件,访问/index/home,就可以渲染出home.vue组件。

命名路由和命名视图

这两个提供了一些使用的便利,直接可以看官方文档,提供了命名的一大好处就是可以动态的解析导航的链接。

高级应用

在页面中跳转,由于我们实际上并没有在HTML文件中跳转,所以页面的HEAD结构始终是同一个,那么如何改变标题呢。

可以在每个组件加载的时候调用mounted方法来修改,但是组件多了需要反复写这个方法。

vue-router提供了导航守卫的功能,就是路由发生变化的时候,执行的功能,这种功能很方便对页面进行一些预处理。

导航守卫何时出发,分为全局的,路由级别的和组件级别的,先来看全局的。

全局导航守卫

全局导航守卫需要在index.js中注册,需要先生成router对象,然后在router对象上注册:

这里还需要了解元数据,也是每个路由规则对象可以添加的属性之一,把标题内容存到每个路由规则对象的meta中,然后进行注册。

Vue-router提供了beforeEachafterEach,它们会在路由即将改变前和改变后触发。先来实际注册一个前置的钩子:

// index.js
......
const routes = [
    {
        path: "/index",
        component: Index,
        children: [
            {
                path: "home",
                component: Home
            }
        ],
        meta:{
            title: "网站首页"
        }
    },
    {
        path: "/about",
        component: About,
        meta:{
            title: "关于本站"
        }
    },
    {
        path: "/user/:id",
        component: User,
        meta:{
            title: "用户信息"
        }
    },
    {
        path: "/post/:year/:month",
        component: Post,
        meta:{
            title: "文章信息"
        }
    },
    {
        path: "*",
        redirect: "index",
        meta:{
            title: "无法匹配URL"
        }
    },
];

......

// 注册全局前置守卫
router.beforeEach(function(to, from, next){
    window.document.title = to.meta.title;
    next();
});

每个前置守卫方法都接受3个参数,to是即将要进入的目标路由对象(就是路由表数组里的一个元素),from是即将要离开的路由对象,next调用之后,才能完成这个钩子,进入下一个钩子。类似于Gulp里边任务函数的的done()

next还可以加一些参数:

  1. next(false),中止导航,即跳转不生效。如果用户手动按了后退,会变成from参数的地址
  2. next(URL),可以跳转到具体的地址,这个地址可以是写死的,也可以是路由的名称,此时需要传入一个对象:{ path: '/' },凡是用在router-link中的属性都可以使用。
  3. next(error),抛出一个错误给router.onError()注册过的回调函数。

设置好之后就会发现页面的标题已经正常变化了。后置守卫afterEach方法只有tofrom两个参数,没有next参数。

这两个钩子的用处非常多。比如每次跳转都默认回到页面顶端:

router.beforeEach(function(to, from, next){
    window.document.title = to.meta.title;
    window.scrollTo(0, 0);
    next();
});

还有重要的应用就是跳转页面的时候,通过后端鉴权,如果成功再跳转,不成功就跳转到指定页面,现在由于没有后端支持,先用伪代码看个意思:

router.beforeEach(function (to, from, next) {
    var token = sendajax(username, password);
    if (validate(token)) {
        next();
    }else{
        next('/login');
    }
});

大概就是每次跳转的时候,都去鉴权,成功的话就继续,否则就跳转到登录页。

全局导航顺序

可以注册多个全局守卫:

router.beforeEach(function(to, from, next){
    console.log("第一个守卫");
    window.document.title = to.meta.title;
    next();
});

router.beforeEach(function(to, from, next){
    console.log("第二个守卫");
    window.scrollTo(0, 0);
    next();
});

router.beforeEach(function (to, from, next) {
    console.log("第三个守卫");
    next();
});

在执行的时候,是按照注册的顺序进行执行的,在上一个守卫执行完next()正常resolve后,下一个才会执行。如果某一个next(false)或者跳转走了,其后的守卫也不会被执行。

路由级别的导航守卫

路由的守卫直接写在路由对象里即可:

const router = new VueRouter({
  routes: [
    {
      path: '/foo',
      component: Foo,
      beforeEnter: (to, from, next) => {
        // ...
      }
    }
  ]
})

组件级别的导航守卫

组件内的守卫可以直接写在组件的属性里,可以看官方文档

const Foo = {
  template: `...`,
  beforeRouteEnter (to, from, next) {
    // 在渲染该组件的对应路由被 confirm 前调用
    // 不!能!获取组件实例 `this`
    // 因为当守卫执行前,组件实例还没被创建
  },
  beforeRouteUpdate (to, from, next) {
    // 在当前路由改变,但是该组件被复用时调用
    // 举例来说,对于一个带有动态参数的路径 /foo/:id,在 /foo/1 和 /foo/2 之间跳转的时候,
    // 由于会渲染同样的 Foo 组件,因此组件实例会被复用。而这个钩子就会在这个情况下被调用。
    // 可以访问组件实例 `this`
  },
  beforeRouteLeave (to, from, next) {
    // 导航离开该组件的对应路由时调用
    // 可以访问组件实例 `this`
  }
}

官网还有一个完整的导航解析流程可以参考。
在使用Vue-CLI创建的项目中,只需要把所有的组件复制到/src/components目录下,app.vue重命名为App.vue替代原来的文件,index.js中的内容复制到main.js里。由于相对路径有变化,修改一下导入即可,启动项目,即可完美将Webpack项目弄过来,自动会配置好路由。

感叹一下技术确实在进步,在一年前刚开始学编程和Web开发的时候,IDE里都是单独的HTML,JS,CSS文件,一个一个学着用。到了现在自学了一圈基础技术,使用了前端工程化,打开的就是一个一个Vue文件了。变化确实快。