Modern
FrontEnd Routing
London 25 June 2019
Eduardo
San Martin Morote
🌍 Vue core team
👨💻 Freelance
📍Paris 🇫🇷
🐣 Málaga 🇪🇸
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! 🖖
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
- 1,917