系列文章只是基础内容的整理和记录,不具备深度。

一、Hash 路由基本原理

hash 路由是入门基础,主要依赖的是 hashchange 这个事件。

下面是我使用纯 js 实现的一个简单的 hash 路由,通过 url 和链接跳转来展示不同的 “页面” 内容。

1.gif

其实代码非常简单, 本质上,在 window 上监听 hashchange 事件之后,只要进行 if-else 或者 switch 的判断就可以:

HTML 代码:

<body>
  <div>
      <a href="#">Index</a>
      <a href="#test1">#test1</a>
      <a href="#test2">#test2</a>
      <a href="#test3">#test3</a>
    </div>
  <div id="app">
    <h1>Index 页面</h1>
  </div>
</body>

Javascript 代码:

const div = document.getElementById('app');
  window.addEventListener('hashchange', () => {
    switch(location.hash) {
      case '#test1': 
        div.innerHTML = `<h1>Test1 页面</h1>`; break;
      case '#test2': 
        div.innerHTML = `<h1>Test2 页面</h1>`; break;
      case '#test3': 
        div.innerHTML = `<h1>Test3 页面</h1>`; break;
      default:
        div.innerHTML = `<h1>Index 页面</h1>`; break;
    }
  })

二、使用 vue-router

使用 vue-router 有几个基本的流程,完成之后,就能够在 vue 项目中接入 vue-router。

1、引入 vue-router 依赖

yarn add vue-router

import VueRouter from 'vue-router';

2、构造路由承载组件

一个路由跳转完成后,需要有组件承接显示,这里定义两个组件,分别是 LoginRegist

const Login = {template: `<h1>Login</h1>`}
const Regist = {template: `<h1>Regist</h1>`}

3、定义路由表

所谓的路由表其实就是一个对象或者是数组,无论是现代化的后端框架比如:Laravel 或者是 koajs 等都是有路由的概念的,不过是服务端路由而已,也都存在路由表的概念。

而一般在 vue 项目中,会将路由表单独拆出来,存放到配置或者一个数组中,比如存到 routes 数组:

  const routes = [
    {path: '/login', component: Login},
    {path: '/regist', component: Regist},
  ]

4、构造 router 对象并加载路由表

  const router = new VueRouter({
    routes: routes
  });

5、在组件中使用挂载路由对象

  const App = {
    props:['title'],
    router: router,
    template: `<div>
      <router-link to="/login">Login</router-link>
      <router-link to="/regist">Regist</router-link>
      <router-view> </router-view>
    </div>`
  }

可以看到,上面代码的 template 中 有两个新内置组件,<router-view><router-link> 这和 react-router 的 view 和 link 的概念也差不多。

<router-view> 用来承载内容的显示,而 <router-link> 则用来进行路由跳转,等同于 a 链接。

其中 router-link 的属性 to="" 就是在理由表 routes 中配置的 path

整体效果如下:

2.gif

三、具名路由

上面定义路由表的时候使用的方式是:

  const routes = [
    {path: '/login', component: Login},
    {path: '/regist', component: Regist},
  ]

而在模板中,也直接使用了 <router-link to="/login" >login</router-link> 通过 to="/login" 指定跳转路由,然后和路由表进行匹配。

实际上这可能会增加维护成本,比如某天因为业务需要,路由表的 /login 这个 path 需要改成 /newLogin,这就意味着模板中使用 to="/login" 都需要改成 to="/newLogin",成本比较高。

而 routes 在定义得时候,路由对象除了 path 属性和 component 属性,还支持 name 属性,实际上就是声明路由的名字是什么,类似变量声明的概念。

router-linkto 属性也支持传递一个对象,而不仅仅是字符串。

新的定义路由表如下:

  const routes = [
    {name: 'login', path: '/login', component: Login},
    {name: 'regist', path: '/regist', component: Regist},
  ]

有了 name 属性之后,可以在 router-linkto 属性传递一个对象,并将 name 传入:

      <router-link :to="{name: 'login'}">Login</router-link>
      <router-link :to="{name: 'regist'}">Regist</router-link>

这样路由在匹配的时候,会直接去查找 name 属性,而不会查找 path 属性 ,因此即使 path 后面因业务需求发生变动,模板层面也无需关心。

四、路由传参

路由传参有两种形式,一种是 url/pathValue/,这里成为 params 形式,一种是 url?pathName=pathValue,这里称为 query 形式。

这两种形式在 vue-router 中都是支持的,只不过表现形式可能不太一样。

通过 path 的形式传参,在路由表定义的时候就已经体现了,如 url/:name在 url 上的表现只是 url/name,而通过 query 的形式传参,定义路由表的时候和之前没什么区别,只是体现在 url 上,和通常意义上的 url?name=name 形式差不多。

在路由参数获取上也不相同,通过 params形式 获取参数的时候,api 是 this.$route.params.name,而通过 query 形式 传参的时候,api 是 this.$route.query.name

1、路由定义

如果希望在路由中进行传参,则可以通过 to 属性传递对象的时候,指定一个 params 或者是 query 的属性和值。

因此路由定义的时候,就需要在 path 上定义 params 或者 query 的属性名称。

比如下面的路由表,在定义的时候通过 /:name 的形式声明有一个 name 属性有参数,而 /regist 路由希望通过 query 形式传递参数,因此不做变化。

  const routes = [
    {name: 'login', path: '/login/:name', component: Login},
    {name: 'regist', path: '/regist', component: Regist},
  ]

2、模板中传参

router-link 中使用的时候,传递 params 对象,并指定属性名和值,其中属性名应当与在路由表中定义的属性名一致。

需要注意的是,下面两个 router-link 的 to 属性传递的内容并不相同,一个是 params 一个是 query

<router-link :to="{name: 'login', params: {name: 'ptbird'}}">Login</router-link>
<router-link :to="{name: 'regist', query: {name: 'ptbird'}}">Regist</router-link>

3、js 中获取参数

两种形式获取参数的时候, API 也是不相同的

  const Login = {template: `<h1>Login</h1>` , created() {console.log(this.$route.params.name)}}
  const Regist = {template: `<h1>Regist</h1>` , created() {console.log(this.$route.query.name)}}

4、两种形式在 URL 上的表现

a1.jpg

a2.jpg

5、this.$route 基本属性

a3.jpg

五、嵌套路由

所谓的嵌套路由在 url 上的表现举个例子说明:

  • 用户的个人主页的路由是: /my,而个人主页中有一部分信息是固定显示的,比如头像、名称等
  • 用户的修改密码的路由是:/my/password,当用户跳转到修改密码页面时,实际上前面的头像和名称都是存在的,只是将部分信息显示成了 修改密码的界面。
  • 用户的修改信息的路由是: /my/info,当用户跳转到修改信息页面的时候,实际上也还在 /my 这个容器中,前面头像和昵称等也都在,但是下面只显示修改密码。

这种情况可以使用 slot 去实现,但是使用 slot 需要特别多的 具名 slot,并且因为 url 没改变会导致在分享的时候,可能直接通过 url 访问会有问题。

这时候就需要嵌套路由,嵌套路由首先表现在路由表的定义上,每个路由对象多了一个 children 属性:

下面的路由定义中,首先定义了 /my 路由,name 、path 和 component 属性不多阐述,主要是 children 属性,实际上 children 属性的值又是一个路由表,我一般叫做子路由表,用来继续描述新的路由信息。

const routes = [
  {
    name: 'my',
    path: 'my',
    component: My,
    children: [
      {name: 'my.login', path: '/login/:name', component: Login},
      {name: 'my.regist', path: '/regist', component: Regist},
    ]
  }
]

而在 My 这个组件中,也需要一个 router-view 标签,用来渲染 My 这个组件的子路由内容。

  const My = {
    template: `
      <div>
        <h1> My 组件 </h1>
        <router-view> </router-view>
      </div>
    `,
  }

因为所有的路由都是具名路由,因此在模板中使用的时候,还是通过名字指定即可:

      <router-link :to="{name: 'my', params: {name: 'ptbird'}}">My</router-link>
      <router-link :to="{name: 'my.login', params: {name: 'ptbird'}}">Login</router-link>
      <router-link :to="{name: 'my.regist', query: {name: 'ptbird'}}">Regist</router-link>
      <router-view> </router-view>

最终效果如下:

a4.gif

六、路由 meta 信息和 路由守卫

meta信息

在定义路由表的时候,可以指定一个 meta 信息,传递一个对象,而这个对象被称为路由元信息,可以在路由收尾中获取这个对象,从而在跳转前进行一些控制。

比较常见的控制一般都是权限控制,比如限制用户登录状态等。

比如下面定义的路由表:

const routes = [
  {name: 'my', path: 'my', component: My, meta: {isNeedLogin: true} },
  {name: 'login', path: '/login/:name', component: Login},
  {name: 'regist', path: '/regist', component: Regist},
]

路由守卫

路由守卫分为全局守卫和组件内守卫,同时有前置守卫和后置守卫等多种类型的守卫,其中可以通过路由前置守卫beforeEach 来进行权限控制。

上述就已经定义好了一个 meta元信息的路由表,如果要获取,可以在路由守卫中获取这个元信息。

路由守卫 beforeEach 参数是一个方法,而这个方法有三个参数 to/from/next,其中必须保证路由守卫中返回 next() 方法,这就类似 koa 中的 middleware,需要需要 next(),否则是走不到下一步的。

tofrom 属性则分别代表去向路由和来源路由。

因为路由还存在子路由,因此可能一次匹配到了多个路由,形成一个数组,这个数组存放在 to.matched 中,所以可以通过to.matched 这个数组进行遍历判断是否需要进行一些操作,比如权限控制。

而上面我们已经定义了一个 {isNeedLogin : true} 这个 meta 信息,因此可以通过 record.meta.isNeedLogin 来获取这个值,判断是 true,则进行一些操作。

如果想要重定向到其他的路由页面(比如 /login),则可以通过 next 方法,next({path: '/'}) 这个方法支持传递一个对象,如同 to 属性传递的对象一样。

router.beforeEach((to, from, next) => {
  if (to.matched.some(record => record.meta.isNeedLogin)) {
    // this route requires auth, check if logged in
    // if not, redirect to login page.
    if (!auth.loggedIn()) {
      next({
        path: '/login',
        query: { redirect: to.fullPath }
      })
    } else {
      next()
    }
  } else {
    next() // 确保一定要调用 next()
  }
})

七、编程式导航

所谓编程式导航其实就是通过 js 主动去跳转,比如点了一个 button,需要判断一些内容,然后跳转到不同的地方。

编程式导航的文档:https://router.vuejs.org/zh/guide/essentials/navigation.html

主要的 API 分为:

  • router.push(location, onComplete?, onAbort?)
  • router.replace(location, onComplete?, onAbort?)
  • router.go(n)

其中 go 类似于 window.history.go 方法的使用

push 则是压入栈,在点击浏览器返回按钮的时候,会将压入栈的内容给出栈,会回到 push 之前的路由。

replace 则是直接替换当前的内容,不会压入新的内容,在点击返回的时候,不会返回到 replace 之前的路由。