Saint Petersburg 24 May 2019
🌍 Vue core team
👨💻 Freelance
📍Paris
🐣 Malaga 🇪🇸
Routing in you App
the 1st week
Routing in your App
after a year
HTML5 history
Hash based history
Abstract (SSR, no url)
Phone
Creating routes
Matching logic
Lazy loading
Guards
Public API
View Link
page('/users', (context, next) => {
// change users page
next()
})
// applying multiple middlewares
page('/users/:id/edit', checkUser, userProfileEdit)
page('/') // go to home
page('/users/2/edit') // go to user edition with id 2
✍️ Imperative
✅ Programmatic navigation
❌ Declarative navigation
✅ Navigation Guards
❌ Dynamic routing (add/remove routes)
router.addRoute('/some-route', options)
router.removeRoute('/some-route')
⚠️ not actual API
<Router>
<Home path="/" />
<UserProfileEdit path="/users/:id/edit" />
</Router>
<div>
<Link to="/">Home</Link>
<Link to={`/users/${this.user.id}/edit`} />
</div>
✍️ Declarative
⚠️ Programmatic navigation (in-jsx)
✅ Declarative navigation
❌ Navigation Guards
✅ Dynamic routing (add/remove routes)
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>
✍️ Configuration-based
✅ Programmatic navigation
✅ Declarative navigation
✅ Navigation Guards
⚠️ Dynamic routing (add/remove routes)
router.push('/search')
<router-view/>
Store visited URLs
JS ↔ URL
push() replace(), ...
listen()
Route matching
match()
resolve()
Navigation
currentRoute
push() replace(), ...
beforeEach(), ...
Creating routes
new Router({ routes })
addRoutes()
<router-view/>
<router-link to="/">Home</router-link>
📦 router-view
<router-view/>
Router Instance
📦 router-link
Router Instance
<router-link to="/">Home</router-link>
<router-link :to="{ name: 'User', params: {id: '2'}}">...</router-link>
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 },
]
})
History
Router
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
})
router.afterEach((to, from) => {
// Analytics, etc
// cannot modify navigation anymore
})
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
😨 Complex codebase
🙂 Simple codebase
Store visited URLs
JS ↔ URL
push() replace(), ...
listen()
Route matching
match()
resolve()
Navigation
currentRoute
push() replace(), ...
beforeEach(), ...
Creating routes
new Router({ routes })
addRoutes()
push() replace()
listen()
API
Responsibilities / Expectations
push(to: HistoryLocation, data?: HistoryState) {
const normalized = this.utils.normalizeLocation(to)
// replace current entry state to add the forward value
this.history.replaceState({
...this.history.state,
forward: normalized
},
''
)
const state = {
back: this.location,
current: normalized,
forward: null,
replaced: false
...data,
}
this.history.pushState(state, '', normalized.fullPath)
this.location = normalized
}
private setupPopStateListener() {
const handler: PopStateListener = ({ state }: { state: StateEntry }) => {
const from = this.location
// save the location where we are going or build it
// from window.location
this.location = state ? state.current : buildFullPath()
// call all listeners
const navigationInfo = {
direction:
state.forward && from.fullPath === state.forward.fullPath
? NavigationDirection.back
: NavigationDirection.forward,
}
this._listeners.forEach(listener =>
listener(this.location, from, navigationInfo)
)
}
// setup the listener and return the handler
// so it can be removed in the future
window.addEventListener('popstate', handler)
return handler
}
vue-router/push-state.js
reach-router/history.js
Directly navigate to
/é?é=é#é
location.pathname: '/%C3%A9'
location.search: '%C3%A9=%C3%A9'
location.search: '#%C3%A9'
location.pathname: '/é'
location.search: 'é=é'
location.search: '#é'
history.pushState({}, '', '/é?é=é#é')
Route matching
resolve()
addRouteRecord
removeRouteRecord
Navigation
currentRoute
push() replace(), ...
Navigation Guards
beforeEach(), ...
addRoute / removeRoute
Route matching
resolve()
addRouteRecord
removeRouteRecord
API
Responsibilities / Expectations
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()
API
Responsibilities / Expectations
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
}
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
}
}
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
}
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
})
{
path: '/admin',
component: AdminPanel,
beforeEnter (to, from, next) {
if (isAdmin) next()
else next(false)
},
}
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
import Calendar from '@/components/Calendar.vue'
const Calendar = () => import('@/components/Calendar.vue')
beforeEnter
beforeRouteEnter
next()
/posts ➡️ /admin
await Admin()
async function navigate(
to: RouteLocationNormalized,
from: RouteLocationNormalized
): Promise<void> {
let guards: Array<() => Promise<any>>
// check beforeRouteLeave guards
guards = extractComponentsGuards(
from.matched.filter(record => to.matched.indexOf(record) < 0).reverse(),
'beforeRouteLeave',
to,
from
)
// run the queue of per beforeRouteLeave
for (const guard of guards) await guard()
// check global guards beforeEach
guards = []
for (const guard of this.beforeGuards) {
guards.push(guardToPromiseFn(guard, to, from))
}
// beforeEach queue
for (const guard of guards) await guard()
// check in components beforeRouteUpdate
guards = extractComponentsGuards(
to.matched.filter(record => from.matched.indexOf(record) > -1),
'beforeRouteUpdate',
to,
from
)
// beforeRouteUpdate queue
for (const guard of guards) await guard()
// check the route beforeEnter
guards = []
for (const record of to.matched) {
// do not trigger beforeEnter on reused views
if (record.beforeEnter && from.matched.indexOf(record) < 0) {
guards.push(guardToPromiseFn(record.beforeEnter, to, from))
}
}
// beforeEnter queue
for (const guard of guards) await guard()
// check in-component beforeRouteEnter
guards = extractComponentsGuards(
to.matched.filter(record => from.matched.indexOf(record) < 0),
'beforeRouteEnter',
to,
from
)
// beforeRouteEnter queue
await this.runGuardQueue(guards)
}
type GuardType = 'beforeRouteEnter' | 'beforeRouteUpdate' | 'beforeRouteLeave'
export async function extractComponentsGuards(
matched: MatchedRouteRecord[],
guardType: GuardType,
to: RouteLocationNormalized,
from: RouteLocationNormalized
): Array<() => Promise<void>> {
return matched.map(record => {
const { component } = record
return async () => {
// resolve async components from cache
const resolvedComponent = await resolveComponent(component)
// await the guard if it exists
const guard = resolvedComponent[guardType]
if (guard) {
await guardToPromiseFn(guard, to, from)()
}
}
})
}