Modern
FrontEnd Routing

London 25 June 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

SPA
Routers

🗂 History

🚦 Router

📦 Componentes

HTML5 history

Hash based history

Abstract (SSR, no url)

Phone

Creating routes

Matching logic

Lazy loading

Guards

Public API

View
Link

Three kind of routers

  • Imperative
  • Declarative
  • Configuration-based

Creating routes in Page.js

page('/users', (context, next) => {
  // change users page
  next()
})

// applying multiple middlewares
page('/users/:id/edit', checkUser, userProfileEdit)

Navigating in Page.js

page('/') // go to home
page('/users/2/edit') // go to user edition with id 2

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

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>

📦 COMPONENTES

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

Vue router

Decoupled:
router instance / components

✍️ Configuration-based

 Programmatic navigation

 Declarative navigation

Navigation Guards

⚠️ Dynamic routing (add/remove routes)

 

🗂 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()

The
Good PARTS

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 },
  ]
})
📦 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

Navigation
Guards

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

🛣 add Route Record

function addRouteRecord(
  record: RouteRecord,
  parent?: RouteMatcher
): void {
  // create the matcher, link it to parent
  const matcher = createMatcher(record, parent)
  if (parent) parent.children.push(matcher)
  
  // handle nested routes
  if ('children' in record) {
    for (const childRecord of record.children) {
      this.addRouteRecord(childRecord, matcher)
    }
  }

  this.insertMatcher(matcher)
}
  • 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

🚦 Push / Replace

async function push(to: RouteLocation): Promise<RouteLocationNormalized> {
  // match the location
  const toLocation = this.resolveLocation(to, this.currentRoute)
  
  // navigate, handle guards
  try {
    await this.navigate(toLocation, this.currentRoute)
  } catch (error) {
    if (error instanceof NavigationGuardRedirect) {
      // trigger the whole navigation again
      return this.push(error.to)
    } else {
      throw error
    }
  }

  // change the URL
  if (to.replace === true) this.history.replace(toLocation)
  else this.history.push(toLocation)

  // save current location
  const from = this.currentRoute
  this.currentRoute = toLocation

  // navigation is confirmed, call afterGuards
  for (const guard of this.afterGuards) guard(toLocation, from)

  return this.currentRoute
}

🚦 resolve Location

function resolveLocation(
  location: MatcherLocation,
  currentLocation: MatcherLocationNormalized,
  redirectedFrom?: MatcherLocationNormalized
): MatcherLocationNormalized {
  // resolve against the matcher
  const matchedRoute = this.matcher.resolve(location, currentLocation)

  // handles in-record redirects
  if ('redirect' in matchedRoute) {
    const { redirect, normalizedLocation } = matchedRoute
    // match the redirect instead
    return this.resolveLocation(
      this.history.utils.normalizeLocation(redirect),
      currentLocation,
      // pass down the location we tried to navigate to
      normalizedLocation
    )
  } else {
    // save the redirection stack
    matchedRoute.redirectedFrom = redirectedFrom
    return matchedRoute
  }
}

🚦 ROUTER

🛣 Matcher

🗂 History

  • Try to equally distribute code complexity and size
  • Use Typescript
  • Refactor often
  • Tests in many browsers
  • Make it easier to contribute

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

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
Made with Slides.com