The present and future of Vue Router

Alicante 29 May 2019

GitHub icon
Twitter icon

Eduardo

San Martin Morote

🌍 Vue core team

👨‍💻 Freelance

📍Paris

🐣 Málaga 🇪🇸

GitHub icon

Routing in you App

the 1st week

Routing in your App

after a year

Vue Router

const router = new Router({
  mode: 'history',
  routes: [
    { path: '/', component: Home },
    { path: '/users/:id', component: UserProfile },
  ]
})
<div>
  <router-link to="/">Home</router-link>
  <router-link :to="`/users/${this.user.id}`">
    My Profile
  </router-link>
</div>

🗂 HISTORY

🚦 ROUTER

📦 COMPONENTS

router.push('/search')
<router-view/>

🗂 HISTORy

  • Store visited URLs

  • JS ↔ URL

    • push() replace(), ...
    • listen()

🚦 ROUTER

  • Route matching​

    • ​match()
    • ​resolve()
  • Navigation

    • ​​currentRoute
    • push() replace(), ...
    • ​beforeEach(), ...
  • Creating routes

    • new Router({ routes })
    • addRoutes()

📦 COMPONENTES

<router-view/>
<router-link to="/">Home</router-link>

SPA
Routers

Three kind of routers

  • Imperative
  • Declarative
  • Configuration-based

Simple & Flexible
but Verbose

Page.js

✍️ Imperative

✅ Programmatic navigation

❌ Declarative navigation

Navigation Guards

❌ Dynamic routing (add/remove routes)

Dynamic routing

router.addRoute('/some-route', options)
router.removeRoute('/some-route')

⚠️ not actual API

Reach Router

<Router>
  <Home path="/" />
  <UserProfileEdit path="/users/:id/edit" />
</Router>
<div>
  <Link to="/">Home</Link>
  <Link to={`/users/${this.user.id}/edit`} />
</div>

Reach router

Idiomatic for React

✍️ Declarative

⚠️ Programmatic navigation (in-jsx)

 Declarative navigation

 Navigation Guards

Dynamic routing (add/remove routes)

 

Vue router

Decoupled:
router instance / components

✍️ Configuration-based

 Programmatic navigation

 Declarative navigation

Navigation Guards

⚠️ Dynamic routing (add/remove routes)

 

The
Good PARTS

📦 router-view
<router-view/>
  • Dynamically render current view
  • Pass params as props
$route
📦 router-link
  • Resolve target location
  • Render an anchor tag with link
  • Handles click event
  • Applies active classes
<router-link to="/">Home</router-link>
<router-link :to="{ name: 'User', params: {id: '2'}}">...</router-link>
$route
$router
this.$route
{
  path: '/users/2',
  name: 'UserProfile',
  query: {},
  params: { id: '2' },
  meta: {}
}
<p>User: {{ $route.params.id }}</p>
created () {
  fetch(`/api/users/${this.$route.params.id}`)
    .then(/* ... */)
}
this.$route
created () {
  if (someCondition) {
    this.$router.push('/other-route')
  }
}
this.$router
const router = new Router({
  mode: 'history',
  routes: [
    { path: '/', component: Home },
    { path: '/users/:id', component: UserProfile },
  ]
})

Global Guards

router.beforeEach((to, from, next) => {
  // verify roles based on `to`
  // ...

  // we can only call `next` once
  if (!isLoggedIn) next('/login') // redirect to login
  else if (!isAuthorized) next(false) // abort
  else next() // allow navigation
})

Per-route Guards

{
  path: '/admin',
  component: AdminPanel,
  beforeEnter (to, from, next) {
    if (isAdmin) next()
    else next(false)
  },
}

in-component guards

export default {
  name: 'AdminPanel',

  data: () => ({ adminInfo: null }),

  async beforeRouteEnter (to, from, next) {
    const adminInfo = await getAdminInfo()
    next(vm => {
      vm.adminInfo = adminInfo
    })
  },
}

beforeEach

beforeEnter

beforeRouteEnter

next()
next(false)

/posts ➡️ /admin

/posts

Admin.vue

beforeEnter

beforeRouteEnter

next()

/posts ➡️ /admin

await Admin()

The
Sad PARTS

No clear Division of responsibilities

History

Router

router.beforeEach
function beforeEach(guard: NavigationGuard): ListenerRemover {
  this.beforeGuards.push(guard)
  return () => {
    const i = this.beforeGuards.indexOf(guard)
    if (i > -1) this.beforeGuards.splice(i, 1)
  }
}
router.push
function push (location, onComplete, onAbort) {
  this.history.push(location, onComplete, onAbort)
}
history.push
function push (location, onComplete, onAbort) {
  const route = this.router.match(location, this.currentLocation)

  try {  
    // run navigation guards queue
    // ...

    // change the url in the browser (HTML5)
    window.history.pushState({}, '', route.fullPath)
    onComplete(route)
  } catch (error) {
    // handle the error
    // ...
    onAbort(error)
  }
}

⚠️ Simplified version

🗂 History

  • Base: +300 LoC
  • HTML5: 70 LoC
  • Hash: 130 LoC

🚦 Router

  • Router Class: ~200 LoC
  • Matcher: ~180 LoC

😨 Complex codebase

🙂 Simple codebase

No clear Division of responsibilities

  • Harder to fix bugs
  • Harder to add features
  • Harder to contribute
  • Harder to extend

Save
Vue
Router

🗂 HISTORy

  • Store visited URLs

  • JS ↔ URL

    • push() replace(), ...
    • listen()

🚦 ROUTER

  • Route matching

    • ​match()
    • ​resolve()
  • Navigation

    • ​​currentRoute
    • push() replace(), ...
    • ​beforeEach(), ...
  • Creating routes

    • new Router({ routes })
    • addRoutes()

🗂 HISTORy

  • push() replace()
  • listen()
  • URL parsing

API

  • Modify the Location
  • Parses URL
    • path
    • query
    • hash
  • Notifies when Location changes
  • Handles Encoding problems
  • Can be overloaded

Responsibilities / Expectations

🚦 ROUTER

  • Route matching

    • ​resolve()
  • Adding Route Records
    • addRouteRecord
    • removeRouteRecord
  • Navigation

    • ​​currentRoute
    • push() replace(), ...
  • Navigation Guards

    • ​beforeEach(), ...
  • Dynamic Routing
    • ​addRoute / removeRoute

🚦 ROUTER

🛣 Matcher

🛣 Matcher

  • Route matching

    • ​resolve()
  • Adding Route Records
    • addRouteRecord
    • removeRouteRecord

API

  • Resolving a Router Location to a Route Record
  • Only handles the path of URL
  • Handles priority of Route Records
  • Parses/handle params

Responsibilities / Expectations

  • Each segment gets 4 points and then…

  • Static segments get 3 more points

  • Dynamic segments 2 more

  • Root segments 1

  • and finally wildcard segments get a 1 point penalty

  • Navigation

    • ​​currentRoute
    • push() replace()
  • Navigation Guards

    • ​beforeEach()
  • Dynamic Routing
  • Lazy loading Pages
  • In-component Guards
  • Error handlers

🚦 ROUTER

API

  • Async navigation
  • Trigger Navigation guards
  • Expose current Route Location
  • Handle redirections

Responsibilities / Expectations

Browser
Quirks

vue-router/push-state.js

reach-router/history.js

Browser Quirks

URI ENcoding Issues

Directly navigate to

/é?é=é#é​
  • URL Bar:  /é?é=é#é​
  • location.pathname: '/%C3%A9'
    
  • location.search: '%C3%A9=%C3%A9'
    
  • location.search: '#%C3%A9'
  • URL Bar:  /é?é=é#é​
  • location.pathname: '/é'
    
  • location.search: 'é=é'
    
  • location.search: '#é'
history.pushState({}, '', '/é?é=é#é​')

Cool
new
Vue
Router

The Future is in Typescript

export type RouteRecord =
  | RouteRecordSingleView
  | RouteRecordMultipleViews
  | RouteRecordRedirect
  • Easier to contribute
  • Easier to use

More Tests

  • Full unit test coverage
  • Automated Cross Browser E2E tests

Going Slowly
but Surely

Making sure we cover all cases from previous router

Router
prototype

other routers

other routers

RFCs

  • Scoped Slot API for router-link
  • Better active matching
  • Dynamic History
  • a11y improvements
  • More navigation information
  • Hooks API for router link

Vue 2 & 3

Support both versions

Vue Router

Vue Router

Vue 2

Vue 3

Patreons 🙌

patreon.com/posva

Thanks! 🖖

GitHub icon