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

Modern FrontEnd Routing

By Eduardo San Martin Morote

Modern FrontEnd Routing

When you develop a Single Page Application, you have to use a Router. It's a common use case, yet every single framework has its own router, React even has multiple ones you can choose from. And even though each framework is different and every router takes a different approach, they all share the same principles. Understanding those helps us handle routing in our SPA because behind all those simple, different APIs provided by framework specific routers, a more complex architecture is hidden from us. Let's talk about the different types of routers out there and what makes them different and understand if you really need a router for every front end framework by using Vue Router implementation as an example

  • 320

More from Eduardo San Martin Morote