Vue 系列 | Vue-Router

注意: 本文所有示例代码详见:vue-rouer-demo

1.What | 什么是Vue Router

Vue Router是Vue.js提供的官方路由管理器,它和Vue.js深度集成,使构建单页面应用非常方便。

2.Why | 为什么要使用Vue Router

大家打开LorneNote个网站,这是我的一个blog网站,采用传统的开发模式,鼠标右击,在出现的菜单里选择View Page Source 查看资源文件,大家会看到它对应的是一个HTML文件,然后你回到网站里点击归档栏,再次右击查看源文件,你会看到整个页面被重新加载,对应归档的HTML文件,即:

  • https://lornenote.com -----> LorneNote HTML文件
  • https://lornenote.com/archives/ -----> 归档 | LorneNote HTML文件

也就是说每一个URL对应一个HTML文件,这样每次切换页面的时候我们都需要重新加载我们的页面,非常影响用户体验。

然后就诞生了单页面形式SPA(single page applications)。在单页面模式下,不管我们访问什么页面,都返回index.html,类似这样:



  
    
    
    
    
    hello-world
  
  
    
    

在单文件模式里,我们依然可以跳到不同的HTML文件,但我们一般不会这样做,一个index.html就满足了我们的需求,用户切换URL的时候,不再是重新加载我们的页面,而是根据URL的变化,执行相应的逻辑,数据会通过接口的形式返回给我们,而后做页面更新。Vue Router就是为了解决这样的事情,它可以做这些事情:

  • 提供URL和组件之间的映射关系,并在URL变化的时候执行对象逻辑
  • 提供多种方式改变URL的API(URL的变化不会导致浏览器刷新)

3.How | Vue Router 如何使用

核心使用步骤

  1. 安装并导入VueRouter,调用Vue.use(VueRouter),这样的话可以使用等全局组件
// 安装命令
$ npm install vue-router

// main.js
import App from './App.vue'
import VueRouter from 'vue-router'
Vue.use(VueRouter)
  1. 定义路由组件入口
// App.vue

  1. 使用定义好的路由组件配置路由列表
// routes.js
import HelloWorld from './components/HelloWorld'
const routes = [
    {path: '/', component: HelloWorld, name:'home'}
]

export default routes
  1. 创建路由实例,并通过路由列表初始化路由实例
// main.js
import routes from'./routes'
const router = new VueRouter({
  routes
})
  1. 将创建好的路由实例挂载到跟实例,这样我们就可以使用this.$route全局钩子来操作路由管理器为我们提供的一些属性。
// main.js
new Vue({
  router,
  render: h => h(App),
}).$mount('#app')

注意:
this.$router代表我们刚刚实例化的全局路由实例,它和我们导入router相同,只是为了使用方便,不用在组件中导入了。
this.$route,写法上少了一个r,代表任何组件的当前路由。

路由动态匹配

有时候我们需要映射不同的路由给相同的组件

// routes.js
{ path:'/user/:id', component:User}

// HelloWorld.vue
张三-single params

在User组件中,可以以this.$route.params的形式接收参数id


动态匹配是以冒号区隔的,我们可以有多个动态段:

// routes.js
{ path:'/user/:userName/userId/:id', component:User},  

// HelloWorld.vue
王五-mutiple params

// User.vue

路由routes与path、$route.params三者的关系见下表:

路由模式 匹配path $route.params
/user/:userName/userId/:id /user/王五/userId/789 { “userName”: “王五”, “id”: “789” }

除了$route.parmas,$route还封装了其他的信息如:
$route.query:用于获取URL中的查询,
$route.hash:用于获取url中#之后的信息(自己写的路径带#,值会包含#,如果是history模式下自动追加的#,则值不包含这个#),如果没有,值为空,如果URL中有多个#,从最后一个开始。

我们重写一下上文的例子:

// HelloWorld.vue
      Query
Hash
// User.vue

由于vue-router使用 path-to-regexp作为路径path的匹配引用,所以我们可以使用正则表达式作为路径:

// routes.js
{ path:'/icon-:flower(\\d+).png', component:User},

// HelloWorld.vue
 正则匹配
// User.vue

我们可以使用星号(*)来代表通配符,只有*则匹配所有,常代表404页面,因为路由的优先级是优先配置优先匹配,相同的组件谁在前面先匹配谁。所以404页面通常写在最后一个:

// routes.js
const routes = [
  // ...
  { path:'/user-*', component:User},
  { path:'*', component:User}
]

// HelloWorld.vue
*Admin
404
// User.vue--*Admin // User.vue--404

如上:
当使用*号路由时,一个参数pathMatch会自动追加给$route.params,它匹配星号后面的部分。

嵌套路由

真实的应用中常常用到组件之间的嵌套,使用vue-router可以很简单的表达这种嵌套关系,我们可以在子组件中嵌套使用,如:

// NestedRoutes.vue

为了做嵌套渲染,我们使用children操作符在路由中做结构配置:

// routes.js 嵌套路由
  {path:'/nestedroutes', component: NestedRoutes, name: 'nestedroutes',
   children: [
     {path:'profile', component: resolve => require(['./components/nested-routes/Profile'],resolve)}, // 匹配  /nestedroutes/profile
     {path:'archive', component: resolve => require(['./components/nested-routes/Archive'],resolve)}   // 匹配  /nestedroutes/archive
   ]
  },

嵌套路由以/为根路径,拼接路径的剩余部分,children其实和routes的最外层一样,是数组,所以我们可以根据需要继续嵌套。

当我们访问/nestedroutes的时候,不会在这里渲染任何内容,因为没有子路由,如果我们想要渲染,可以在路由里配置一个空的path的子路由:

// 嵌套路由
  {path:'/nestedroutes', component: NestedRoutes, name: 'nestedroutes',
   children: [
     {path:'', component: resolve => require(['./components/nested-routes/NestedRoutesHome'],resolve)},
     {path:'profile', component: resolve => require(['./components/nested-routes/Profile'],resolve)},
     {path:'archive', component: resolve => require(['./components/nested-routes/Archive'],resolve)}
   ]
  },

编程式的导航

除了以的标签形式导航之外,我们还可以使用编程的方式导航,vue-router为我们提供了对应的实例方法this.$router.push

这个方法会将URL页面推到历史堆栈,所以当我们点击浏览器的返回按钮的时候指向前一个URL。当我们点击的时候,内部其实调用的是router.push(...),所以这两个形式功能是等同的。

router.push(location, onComplete?, onAbort?)

location:导航位置,是一个字符串path或位置描述符对象,
第二个和第三个为可选参数,导航执行完成会执行这两个参数。
onComplete导航执行完会执行这个参数(所有的异步钩子都被执行完之后)
使用方式如下:

       
      



注意:
如果使用了pathparams就被忽略了,但上面的query不会被忽略。所以如果我们要传递参数,可以用两种方式,一种是提供一个name路由,一种是在path里面手动拼接参数:

// routes.js
{ path:'/programmatic-navigation/params=/:id', component: resolve => require(['./components/programmatic-navigation/ProgrammaticNavigation'],resolve), name: 'programmatic-navigation-params' },

// ProgrammaticNavigation.vue

      

这个规则也同样适用于router-linkto属性。

如果我们的当前路径和跳转路径相同,那么我们需要在beforeRouteUpdate方法中响应数据更新:

ProgrammaticNavigation.vue



router.replace
router.replacerouter.push类似,唯一不同的是它不是推送到一个新的历史页,而是替换当前页。

router.go(n)
这个方法和window.history.go(n)类似,n用整数表示,表示在历史页中向前后向后走多少页。

// 后退一页
router.go(-1)
// 前进一页
router.go(1)
// 如果没有,导航失败,页面保持不变
router.go(10)

操作History
路由的router.push,router,replacerouter.go对应window.history.pushState, window.history.replaceStatewindow.history.go, 他们模仿的是window.historyAPIs。

Vue Router的导航方法(push, replace, go)在所有的路由模式下都可以工作(historyhashabstract)。

命名路由

有时候给路由一个名字更方便,用法也很简单:

// routes.js
  {
    path:'/named-routes/:id',
    component: resolve => require(['./components/named-routes/NamedRoutes'],resolve),
    name:'named-routes'
  },

// HelloWorld.vue
named-routes router-link

这两种方式都是对象传递,注意to前面要加冒号的,表示内部是对象表达式而不是单纯的字符串。

###命名视图
有时候我们可能需要在同一页展示多个视图,而不是做视图嵌套,比如说主页面或者是sidebar侧边栏。这个时候我们就会用到命名视图,也就是说在同一页面下使用多个,给每个一个不同的名字,如果没给名字,默认名为default

  // routes.js 命名View
  {
    path:'/named-views',
    component: resolve => require(['./components/named-views/NamedViews'],resolve),
    children: [
      {
        path:'',
        components: {
          default:resolve => require(['./components/named-views/ViewA'],resolve),
          b: resolve => require(['./components/named-views/ViewB'],resolve),
          c: resolve => require(['./components/named-views/ViewC'],resolve),
        },
      }
    ]
  },

// NamedViews.vue

嵌套命名视图
我们可以在嵌套视图里使用命名视图创造更复杂的布局。我们来看一个例子:

// NestedNamedViews.vue

这里:

  • NestedNamedViews本身是View组件
  • ViewNav是一个常规的组件
  • router-view内部是被嵌套的视图组件

路由配置这样来实现:

// routes.js
{ path: '/nested-named-views',
    component: resolve => require(['./components/named-views/NestedNamedViews'], resolve),
    children: [
      {
        path:'profile',
        component: resolve => require(['./components/nested-routes/Profile'],resolve)
      },
      {
        path:'archive',
        components: {
          default:resolve => require(['./components/named-views/ViewA'],resolve),
          b: resolve => require(['./components/named-views/ViewB'],resolve),
        }
      }
    ]
  },

转发和别名

转发是我们可以在访问a的时候跳转b。
支持路径和命名访问两种形式:

// routes.js
  {
    path:'/orignal01' ,redirect: '/forward'
  },
  {
    path:'/orignal02' ,redirect: {name: 'forward'}
  },

别名是这样的,如果/b是组件a的别名,那意味着我们访问/b的时候,匹配的组件依然是a

// routes.js
{
    path: '/forward',
    name: 'forward',
    component: resolve => require(['./components/redirect-views/RedirectViews'], resolve),
    alias: '/alias'
  },

传递Props给路由组件

为了使路由的组件更加灵活,vue支持在路由组件中传递Props,使用props操作符。支持三种模式:
布尔值模式
props设置为true的时候,route.params将被设置作为组件的props。

// routes.js
{
    path: '/props-to-route/:id',  // http://10.221.40.28:8080/props-to-route/123
    component: resolve => require(['./components/props-route/PropsRoute'], resolve),
    props: true
  },

对象模式
当props是一个对象的时候,这个对象也一样会被设置给组件的props,这在props为固定值的时候很有用:

// routes.js
  {
    path: '/static',  // http://10.221.40.28:8080/static
    component: resolve => require(['./components/props-route/PropsRoute'], resolve),
    props: {name: 'static'}
  },

函数模式
我们可以创建一个函数返回props,这可以让你转化参数为其他类型,经静态值与基于路由的值结合,等等。

// routes.js
  {
    path: '/function-mode', // http://10.221.40.28:8080/function-mode?keyword=%27beijing%27
    component: resolve => require(['./components/props-route/PropsRoute'], resolve),
    props: (route) => ({ query: route.query.keyword})
  },

如果URL为/function-mode?keyword='beijing'将传递{query: 'beijing'}作为组件的props。

注意:
props函数是无状态的,仅仅计算路由的改变。如果需要状态去定义props,那么Vue官方建议封装一个组件,这样vue就能够对状态做出反应。

路由进阶

导航警卫

这是vue-router提供的一些控制路由进程的函数,有三种表现方式:全局定义,路由内定义和组件中定义。

全局定义
分为前置警卫、解析警卫和后置钩子。我们依次看一下:

1.前置警卫

router.beforeEach((to, from, next) => {
  // ...
})

无论哪个导航先触发之前都会先调用它,警卫的解析可能是异步的,所以在所有的钩子解析完成之前,导航处于悬停状态(pending)。实践中经常在这里判断是否携带了进入页面的必要信息,否则做跳转。(比如通过URL地址栏手动输入地址非法进入子页面,需要跳转到登录页让用户登录)

注意:别忘了写next函数,否则钩子函数将不会被解析。

2.全局解析警卫

router.afterEach((to, from) => {
  // ...
})

和前置警卫一样,只不过是在导航确认之前,所有组件内警卫和异步路由组件被解析之后会立即调用。

3.全局后置钩子
这些钩子和警卫不同的是没有next函数,并且不影响导航。

router.afterEach((to, from) => {
  // ...
})

路由独享的警卫
我们可以在路由的配置对象里配置:beforeEnter

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

这个和全局前置警卫作用相同。

组件内警卫
在组件内可以定义一些路由导航警卫(会被传递给路由配置表):

  • beforeRouteEnter
  • beforeRouteUpdate
  • beforeRouteLeave

beforeRouteEnter不能访问this,因为组件还没有被创建。然后我们可以在next回调里访问,在导航被确认时,组件实例会被当做参数传递给这个回调:

beforeRouteEnter (to, from, next) {
  next(vm => {
    // access to component instance via `vm`
  })
}

而在beforeRouteUpdatebeforeRouteLeavethis已经可用。所以next回调没有必要因此也就不支持了。

beforeRouteUpdate (to, from, next) {
  // just use `this`
  this.name = to.params.name
  next()
}

离开警卫常常用于阻止用户意外离开未经保存的内容,导航可用通过next(false)取消。

beforeRouteLeave (to, from, next) {
  const answer = window.confirm('Do you really want to leave? you have unsaved changes!')
  if (answer) {
    next()
  } else {
    next(false)
  }
}

完整的导航解析流程

  1. 导航被触发
  2. beforeRouteLeave警卫在失活组件被调用
  3. 全局的beforeEach警卫被调用
  4. 在可复用的组件内调用beforeRouteUpdate
  5. 在路由配置中调用beforeEnter
  6. 解析异步路由组件
  7. 在激活组件中调用beforeRouterEnter
  8. 调用全局beforeResolve警卫
  9. 导航被确认
  10. 调用全局afterEach钩子
  11. DOM更新被触发
  12. 用创建好的实例调用beforeRouteEnter守卫中传给next的回调函数。

路由Meta字段

定义路由的时候可以包含一个meta字段:

const router = new VueRouter({
  routes: [
    {
      path: '/foo',
      component: Foo,
      children: [
        {
          path: 'bar',
          component: Bar,
          // a meta field
          meta: { requiresAuth: true }
        }
      ]
    }
  ]
})

怎么样使用这个字段呢?

首先,在routes配置里的每一个路由对象称为路由记录。路由可能会被嵌套,因此当一个路由被匹配的时候,可能会匹配超过一个记录。

例如,上面的路由配置,/foo/bar将匹配父路由记录和子路由记录两个路由配置。

所有的被匹配的路由记录都被导出在$route对象(也在导航警卫的路由对象里)作为$route.matched数组。因此我们需要迭代这个数组来找到路由记录里的meta字段。

在全局导航警卫中检查meta字段的一个例子:

router.beforeEach((to, from, next) => {
  if (to.matched.some(record => record.meta.requiresAuth)) {
    // 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() // make sure to always call next()!
  }
})

过渡

简单说就是给路由加一些过渡效果。有几种方式可以实现:
1.在统一的router-view入口给一个一致的过渡


  

查看所有的过渡APIs

2.在每一个组件内部给一个特别的过渡

const Foo = {
  template: `
    
      
...
` } const Bar = { template: `
...
` }

3.还可以根据当前路由和目标路由之间的关系设计动态过渡






数据获取

在实践中,很多时候在进入一个新的路由页面时,我们都是需要从服务器请求数据的,有两种方式:

  • 在导航之后获取:首先导航到新的页面,在导航的生命周期钩子中(比如created方法)做数据获取。
  • 在导航之前获取:在路由导航进入警卫之前获取数据,数据获取完成之后执行导航。

技术上都可以实现,用哪种取决于用户体验的目标。

导航之后获取数据
使用这种方式,我们会立即导航到新的页面,渲染组件,在组件的created钩子中渲染组件。当获取数据是我们可以展示一个loading的状态,并且每一页可以有自己的loading视图。

我们看一个代办事项的例子:





// 网络请求API -- todo.js
const _todoList = [
    {"id": 1, "title": "购物"},
    {"id": 2, "title": "回复邮件" }
]

export default ({
    getTodoList (cb,errorCb) {
        setTimeout(() => {
            Math.random() > 0.5
            ? cb(_todoList)
            : errorCb("数据请求失败")
        },1000)
    }
})

滚动行为

当我们使用vue-router的时候,我们可能想要打开新页在某一个位置,或使返回历史页保持在上次浏览的位置。vue-router允许我们自定义导航行为。

注意
这个功能仅仅在浏览器支持history.pushState的时候可以用。

当我们创建一个路由实例的时候,我们可以创建一个scrollBehavior函数。

const scrollBehavior = function (to, from, savedPosition) {
  if (savedPosition) {
    return savedPosition
  } else {
    const postion = {}

    if (to.hash) {
      postion.selector = to.hash
      if (to.hash === '#anchor2') {
        postion.offset = {y: 100 }
      }
      if (/^#\d/.test(to.hash) || document.querySelector(to.hash)) {
        return postion
      }
      return false
    }

    return new Promise(resolve => {
      if (to.matched.some(m => m.meta.scrollToTop)) {
        postion.x = 0
        postion.y = 0
      }

      this.app.$root.$once('triggerScroll', () => {
        resolve(postion)
      })
    })
  }
}

const router = new  VueRouter({
  mode: 'history',
  scrollBehavior,
  routes,
})

这里面接收to,from两个路由对象,第三个参数savedPosition,第三个参数仅仅在popstate导航中可用(浏览器的向前/后退按钮被触发的时候)。

这个对象返回滚动位置对象,格式如:

  • {x: nubmer, y:number }
  • {selector: string, offset? : { x: number, y: number}}(offset 仅仅被支持在2.6.0+以上)

如果是空值或无效值,则不会发出滚动。

scrollBehavior (to, from, savedPosition) {
  if (savedPosition) {
    return savedPosition
  } else {
    return { x: 0, y: 0 }
  }
}

如果返回savedPosition,和我们使用浏览器的向前向后按钮一下,如果有历史滚动页,会返回到历史页查看的位置,如果返回{ x: 0, y: 0 }则滚动到顶部。

在开始的例子里,我们可以这样模拟滚动到某处的行为:

if (to.hash) {
      postion.selector = to.hash

      if (to.hash === '#anchor2') {
        postion.offset = {y: 100 }
      }

     // ...
    }

以下我们返回了一个Promise,来返回滚动的位置

return new Promise(resolve => {
// 如果匹配scrollToTop,我们就返回顶部
      if (to.matched.some(m => m.meta.scrollToTop)) {
        postion.x = 0
        postion.y = 0
      }

      // 如果空值或无效值,返回当前滚动位置
      this.app.$root.$once('triggerScroll', () => {
        resolve(postion)
      })
    })

懒加载路由

当使用一个捆绑器构建应用时,JavaScript包会变得非常大,因此影响页面加载的时间。如果我们可以把每一个路由组件分离单独的块,等浏览页面时再加载他们会效率比较高。

Vue的异步组件功能和webpack的代码分离功能可以很容易的做到这一点。

首先,可以将异步组件定义为返回Promise的工厂函数(应解析为组件本身):

const Foo = () => Promise.resolve({ /* component definition */ })

其次,在webpack2,我们可以使用动态导入语法去表示代码分离点:

import('./Foo.vue') // returns a Promise

注意
如果你使用Babel,你需要添加syntax-dynamic-import以便Babel可以正确解析。

把这两个步骤联合起来,我们就可以通过webpack自定定义一个异步的代码分离组件,所以我们可以把我们的路由配置修改为:

{
    path: '/lazy-route', component: () => import('./components/lazy-route/LazyHome')
  }

在同一个块中分组组件

有时,我们想要把所有组件分组到相同的异步块中,为了实现这个,在webpack > 2.4中,我们可以使用特殊的注释语法提供命名块:

{
    path: '/lazyTwo', component: () => import(/* webpackChunkName: "lazy" */ './components/lazy-route/LazyTwo'),
    children: [
      {path: 'lazyThree', component: () => import (/* webpackChunkName: "lazy" */ './components/lazy-route/LazyThree')}
    ]
  },

webpack将使用相同块名称的任何异步模块分组到相同的异步块中。


参考:Vue Router

你可能感兴趣的:(Vue 系列 | Vue-Router)