Share
Embed
Understanding Client Side Routing with Vue.js
Presentation slides to the JSCamp 2018 talk: Understanding Client Side Routing with Vue.js.
https://jscamp.tech/speakers/hassan-djirdeh/
djirdehh
July 19, 2018
Transcript
84 slides
-
Vue is the Progressive JavaScript framework that is approachable, versatile,
and performant. Progressive approachable versatile performant -
Routing often refers to splitting an applications UI based on
rules derived from the browser URL. -
Refresh the page and keep our location in the app
Maintain browser back/forward history functionality Bookmark the URL and even share it with others With Routing, we can… -
Server Side Routing The client (i.e. the browser) makes a
request to the server on every URL change. Client Side Routing The client only makes a request to the server upon initial-page load. First Meaningful Paint Consistent SEO Performance Faster Routing after Initial Load Smooth transitions and changes Two main methods to achieving routing… -
Single-page applications (SPAs) Client side applications are often labelled as…
In modern web apps; JS is the driving force to re-render content in SPAs -
We’ll be building this Pokemon App that displays details of
a Pokemon based on the route. Live version: http://pokemon-routing.surge.sh/ -
vue-router By the end of the presentation, we’ll build the
app with vue-router... but before that, we’ll build our own custom router in Vue. -
main.js import Vue from 'vue'; import App from './App'; new
Vue({ el: '#app', render: h => h(App) }); -
Vue Components Markup (HTML) Logic (JS) Styles (CSS) we’ll take
a step back and talk about Vue components -
<template> <h1>Hello Folks!</h1> </template> <script> export default { name: 'NewComponent',
data() {}, methods: {} } </script> <style> </style> Vue.component('new-component', { template: ‘<h1>Hello Folks!</h1>’, data() {}, methods: {} }); Vue.component('new-component', { template: ‘<h1>Hello Folks!</h1>’, data() {}, methods: {} }); <template> <h1>Hello Folks!</h1> </template> <script> export default { name: 'NewComponent', data() {}, methods: {} } </script> <style> </style> .vue* -
App.vue <template> <div class="pokemon"> <div class="card-image"> ... <div class="card-content has-text-centered">
<div class="main"> <div class="title has-text-white">Charizard</div> ... </div> </div> </div> </template> <script> export default { name: "App", }; </script> <template> <div class="pokemon"> <div class="card-image"> ... <div class="card-content has-text-centered"> <div class="main"> <div class="title has-text-white">Charizard</div> ... </div> </div> </div> </template> <script> export default { name: "App", }; </script> <template> <div class="pokemon"> <div class="card-image"> ... <div class="card-content has-text-centered"> <div class="main"> <div class="title has-text-white">Charizard</div> ... </div> </div> </div> </template> <script> export default { name: "App", }; </script> -
<template> <div class="pokemon"> <div class="card-image"> ... <div class="card-content has-text-centered"> <div
class="main"> <div class="title has-text-white">Charizard</div> ... </div> </div> </div> </template> <script> export default { name: "CharizardCard", }; </script> CharizardCard.vue <template> <div class="pokemon"> <div class="card-image"> ... <div class="card-content has-text-centered"> <div class="main"> <div class="title has-text-white">Charizard</div> ... </div> </div> </div> </template> <script> export default { name: "CharizardCard", }; </script> <template> <div class="pokemon"> <div class="card-image"> ... <div class="card-content has-text-centered"> <div class="main"> <div class="title has-text-white">Charizard</div> ... </div> </div> </div> </template> <script> export default { name: "CharizardCard", }; </script> -
<template> <charizard-card></charizard-card> </template> <script> import CharizardCard from './components/CharizardCard'; export default
{ name: "App", components: { 'charizard-card': CharizardCard } }; </script> App.vue <template> <charizard-card></charizard-card> </template> <script> import CharizardCard from './components/CharizardCard'; export default { name: "App", components: { 'charizard-card': CharizardCard } }; </script> <charizard-card></charizard-card> components: { 'charizard-card': CharizardCard } import CharizardCard from './components/CharizardCard'; -
app/ main.js App.vue components/ BlastoiseCard.vue CharizardCard.vue VenusaurCard.vue We’ll create other
files for other components -
routes.js import BlastoiseCard from '../components/BlastoiseCard'; import CharizardCard from '../components/CharizardCard'; import
VenusaurCard from '../components/VenusaurCard'; export default const routes = [ {path: '/', component: CharizardCard}, {path: '/charizard', component: CharizardCard}, {path: '/blastoise', component: BlastoiseCard}, {path: '/venusaur', component: VenusaurCard} ]; import BlastoiseCard from '../components/BlastoiseCard'; import CharizardCard from '../components/CharizardCard'; import VenusaurCard from '../components/VenusaurCard'; export default const routes = [ {path: '/', component: CharizardCard}, {path: '/charizard', component: CharizardCard}, {path: '/blastoise', component: BlastoiseCard}, {path: '/venusaur', component: VenusaurCard} ]; import BlastoiseCard from '../components/BlastoiseCard'; import CharizardCard from '../components/CharizardCard'; import VenusaurCard from '../components/VenusaurCard'; export default const routes = [ {path: '/', component: CharizardCard}, {path: '/charizard', component: CharizardCard}, {path: '/blastoise', component: BlastoiseCard}, {path: '/venusaur', component: VenusaurCard} ]; {path: '/', component: CharizardCard}, {path: '/charizard', component: CharizardCard}, {path: '/blastoise', component: BlastoiseCard}, {path: '/venusaur', component: VenusaurCard} export default const routes = [ {path: '/', component: CharizardCard}, {path: '/charizard', component: CharizardCard}, {path: '/blastoise', component: BlastoiseCard}, {path: '/venusaur', component: VenusaurCard} ]; pathname! -
<template> <div class="pokemon"> <component :is="currentView"></component> </div> </template> <script> import routes
from './routes'; export default { name: "RouterView", data() { return { currentView: {} } } methods: { getRouteObject() { return routes.find( route => route.path === window.location.pathname RouterView.vue <template> <div class="pokemon"> <component :is="currentView"></component> </div> </template> <script> import routes from './routes'; export default { name: "RouterView", data() { return { currentView: {} } } methods: { getRouteObject() { return routes.find( route => route.path === window.location.pathname data() { return { currentView: {} } } <component :is="currentView"></component> We’re using the Dynamic <component /> keyword to create a component that will render another component based on a currentView property -
</div> </template> <script> import routes from './routes'; export default {
name: "RouterView", data() { return { currentView: {} } } methods: { getRouteObject() { return routes.find( route => route.path === window.location.pathname ); } } created() { if (!this.getRouteObject()) { RouterView.vue import routes from './routes'; methods: { getRouteObject() { return routes.find( route => route.path === window.location.pathname ); } } We’ll set up a getRouteObject() method responsible in getting the correct route object from routes based on the window location -
getRouteObject() { return routes.find( route => route.path === window.location.pathname );
} } created() { if (!this.getRouteObject()) { this.currentView = { template: ` <h3 class="subtitle has-text-white"> Sorry, we couldn't find that Pokémon :(. </h3> ` }; } else { this.currentView = this.getRouteObject().component; } } }; </script> RouterView.vue created() { if (!this.getRouteObject()) { this.currentView = { template: ` <h3 class="subtitle has-text-white"> Sorry, we couldn't find that Pokémon :(. </h3> ` }; } else { this.currentView = this.getRouteObject().component; } } When the component gets created, we’ll use the getRouteObject method to bind the correct component to currentView -
<template> <div class="pokemon"> <charizard-card></charizard-card> </div> </template> <script> import CharizardCard from
'./components/CharizardCard'; export default { name: "App", components: { 'charizard-card': CharizardCard } }; </script> App.vue <template> <div class="pokemon"> <router-view></router-view> </div> </template> <script> import RouterView from './router/RouterView'; export default { name: "App", components: { 'router-view': RouterView } }; </script> Now we’ll have App render the RouterView component -
RouterLink.vue <template> <a @click="navigate" :href="to">{{ to }}</a> </template> <script> export
default { name: "RouterLink", props: { to: { type: String, required: true } }, methods: { navigate(evt) { evt.preventDefault(); window.history.pushState(null, null, this.to); } } }; </script> <template> <a @click="navigate" :href="to">{{ to }}</a> </template> <script> export default { name: "RouterLink", props: { to: { type: String, required: true } }, methods: { navigate(evt) { evt.preventDefault(); window.history.pushState(null, null, this.to); } } }; </script> <template> <a @click="navigate" :href="to">{{ to }}</a> </template> <script> export default { name: "RouterLink", props: { to: { type: String, required: true } }, methods: { navigate(evt) { evt.preventDefault(); window.history.pushState(null, null, this.to); } } }; </script> methods: { navigate(evt) { evt.preventDefault(); window.history.pushState(null, null, this.to); } } To prevent the browser from making a web request, we’ll create a RouterLink element that pushes a new location with window.history.pushState. -
We’ve bound an href attribute to the <a> of RouterLink
to allow users to hover and see where they lead to. -
RouterLink RouterView created() RouterView only picks the correct Pokemon component
when it gets created. With RouterLink, we may need to send some event to RouterView when clicked. -
An Event Bus is a Vue instance that is used
to enable isolated components to subscribe and publish custom events between each other. -
import Vue from 'vue'; export const EventBus = new Vue();
EventBus = new Vue(); event-bus.js -
RouterLink.vue <template> <a @click="navigate" :href="to">{{ to }}</a> </template> <script> import
EventBus from './event-bus'; export default { name: "RouterLink", props: { to: { type: String, required: true } }, methods: { navigate(evt) { evt.preventDefault(); window.history.pushState(null, null, this.to); EventBus.$emit('navigate'); } } }; </script> methods: { navigate(evt) { evt.preventDefault(); window.history.pushState(null, null, this.to); EventBus.$emit('navigate'); } } -
RouterView.vue <template>...</template> <script> import routes from './routes'; import EventBus from
'./event-bus'; ... created() { // Get correct component upon page load ... // Get correct component upon redirect EventBus.$on('navigate', () => { this.currentView = this.getRouteObject().component; }); }, methods: { getRouteObject() { return routes.find( route => route.path === window.location.pathname ); } } ... </script> <template>...</template> <script> import routes from './routes'; ... created() { // Get correct component upon page load ... // Get correct component upon redirect EventBus.$on('navigate', () => { this.currentView = this.getRouteObject().component; }); }, methods: { getRouteObject() { return routes.find( route => route.path === window.location.pathname ); } } ... </script> import EventBus from './event-bus'; -
App.vue <template> <div class="pokemon"> <router-view></router-view> <div class="pokemon-links has-text-centered"> <router-link to="/charizard"></router-link>
<router-link to="/blastoise"></router-link> <router-link to="/venusaur"></router-link> </div> </div> </template> <script> import RouterView from './router/RouterView'; import RouterLink from './router/RouterLink'; export default { name: "App", components: { 'router-view': RouterView, ‘router-link': RouterLink } }; </script> import RouterLink from ‘./router/RouterLink'; <template> <div class="pokemon"> <router-view></router-view> <div class="pokemon-links has-text-centered"> <router-link to="/charizard"></router-link> <router-link to="/blastoise"></router-link> <router-link to="/venusaur"></router-link> </div> </div> </template> <script> import RouterView from './router/RouterView'; import RouterLink from './router/RouterLink'; export default { name: "App", components: { 'router-view': RouterView, 'router-link': RouterLink } }; </script> -
popstate Last item to take care of is browser navigation
events. For that we’ll use the popstate event. -
App.vue <template> ... </template> <script> import RouterView from './router/RouterView'; import
RouterLink from './router/RouterLink'; import EventBus from './event-bus'; export default { name: "App", created() { window.addEventListener('popstate', () => { EventBus.$emit('navigate'); }); }, components: { 'router-view': RouterView, 'router-link': RouterLink } }; </script> <template> ... </template> <script> import RouterView from './router/RouterView'; import RouterLink from './router/RouterLink'; import EventBus from './event-bus'; export default { name: "App", created() { window.addEventListener('popstate', () => { EventBus.$emit('navigate'); }); }, components: { 'router-view': RouterView 'router-link': RouterLink } }; </script> -
Responsible in mapping components to respective URL pathnames - routes
Component responsible in rendering another specified component based on the app’s location - router-view Component that allows the user to change the location of the browser without making a web request - router-link These are also the same three pieces of the vue-router library. So we’ll now switch over to using it. -
vue-router is the official router of Vue. It deeply integrates
with Vue to make building Single Page applications a breeze. -
import Vue from 'vue'; import VueRouter from 'vue-router'; import ..Card
from '.components/..Card'; Vue.use(VueRouter); const routes = [ {path: '/', component: CharizardCard}, {path: '/charizard', component: CharizardCard}, {path: '/blastoise', component: BlastoiseCard}, {path: '/venusaur', component: VenusaurCard}, { path: '*', component: { template: ` <h3 class="subtitle has-text-white"> Sorry. We couldn't find that Pokémon :(. </h3> ` } } ]; router.js import Vue from 'vue'; import VueRouter from 'vue-router'; import ..Card from '.components/..Card'; Vue.use(VueRouter); const routes = [ {path: '/', component: CharizardCard}, {path: '/charizard', component: CharizardCard}, {path: '/blastoise', component: BlastoiseCard}, {path: '/venusaur', component: VenusaurCard}, { path: '*', component: { template: ` <h3 class="subtitle has-text-white"> Sorry. We couldn't find that Pokémon :(. </h3> ` } } ]; -
const routes = [ {path: '/', component: CharizardCard}, {path: '/charizard',
component: CharizardCard}, {path: '/blastoise', component: BlastoiseCard}, {path: '/venusaur', component: VenusaurCard}, { path: '*', component: { template: ` <h3 class="subtitle has-text-white"> Sorry. We couldn't find that Pokémon :(. </h3> ` } } ]; export default const router = new VueRouter({ mode: 'history', routes }); router.js export default const router = new VueRouter({ mode: 'history', routes }); -
https://localhost:8080/#/blastoise Never sent to the server Hash mode URLs vue-router’s
default mode is hash which allows to have multiple client side routes without having to provide necessary server-side fallbacks. -
https://localhost:8080/blastoise History mode URLs Since our app is a SPA
(Single-page app), we don’t need hash-based URLs. We’ll switch our URLS to mode: history. -
import Vue from 'vue'; import App from './App'; import router
from './router' new Vue({ el: '#app', router, render: h => h(App) }); main.js import Vue from 'vue'; import App from './App'; import router from './router'; new Vue({ el: '#app', router, render: h => h(App) }); To make our app router-aware, we’ll declare the router instance within the entire application Vue instance. -
App.vue <template> <div class="container"> <div class="pokemon"> <router-view></router-view> <div class="pokemon-links has-text-centered">
<router-link to="/charizard">/charizard</router-link> <router-link to="/blastoise">/blastoise</router-link> <router-link to="/venusaur">/venusaur</router-link> </div> </div> </div> </template> <script> export default { name: "App" }; </script> <template> <div class="container"> <div class="pokemon"> <router-view></router-view> <div class="pokemon-links has-text-centered"> <router-link to="/charizard">/charizard</router-link> <router-link to="/blastoise">/blastoise</router-link> <router-link to="/venusaur">/venusaur</router-link> </div> </div> </div> </template> <script> export default { name: "App" }; </script> We can use the router-view and router-link elements like we’ve done previously. -
- Well Tested - Consistency between different browsers - Dynamic
Route Matching - Nested Routes - Navigation Guards vue-router > custom router -
Dynamic Route Matching app/ components/ BlastoiseCard.vue CharizardCard.vue VenusaurCard.vue AlakazamCard.vue EeveeCard.vue
GolbatCard.vue WigglytuffCard.vue MachampCard.vue OnixCard.vue MoltresCard.vue ...Card.vue app/ components/ PokemonCard.vue Having a component file for every Pokemon is unrealistic for a much larger app. With Dynamic Route Matching, we can have multiple routes with the same pattern be matched to a single component. -
Dynamic Route Matching router.js const routes = [ {path: '/',
component: CharizardCard}, {path: '/charizard', component: CharizardCard}, {path: '/blastoise', component: BlastoiseCard}, {path: '/venusaur', component: VenusaurCard}, ... ]; Before -
Dynamic Route Matching router.js const routes = [ {path: '/',
component: CharizardCard}, {path: '/pokemon/:id', component: PokemonCard } ]; After -
<template> ... </template> <script> export default { name: "PokemonCard", created()
{ console.log(this.$route.params.id); } }; </script> Dynamic Route Matching <template> ... </template> <script> export default { name: "PokemonCard", created() { console.log(this.$route.params.id); } }; </script> charizard /pokemon/charizard PokemonCard.vue <template> ... </template> <script> export default { name: "PokemonCard", created() { console.log(this.$route.params.id); } }; </script> -
data: [ { id: 'charizard', hp: 78, type: , weight_lbs:
199, height_m: 1.7, stats: [ ... ], evolutions: [ ... ] }, ... ] Dynamic Route Matching data: [ { id: 'charizard', hp: 78, type: , weight_lbs: 199, height_m: 1.7, stats: [ ... ], evolutions: [ ... ] }, ... ] data: [ { id: 'charizard', hp: 78, type: , weight_lbs: 199, height_m: 1.7, stats: [ ... ], evolutions: [ ... ] }, ... ] With the id route param available in our component - we can use it to retrieve data with a server, make an GET request, etc. -
<template> <div class="pokemon"> <div class="card-image"> <img :src="pokemonData.img" /> <div class="card-content
has-text-centered"> <div class="main"> <div class="title has-text-white">{{ pokemonData.id }}</div> ... </div> </div> </div> </template> <script> export default { name: "PokemonCard", data() { return { pokemonData: {...} } } ... }; </script> PokemonCard.vue <template> <div class="pokemon"> <div class="card-image"> <img :src="pokemonData.img" /> <div class="card-content has-text-centered"> <div class="main"> <div class="title has-text-white">{{ pokemonData.id }}</div> ... </div> </div> </div> </template> <script> export default { name: "PokemonCard", data() { return { pokemonData: {...} } } ... }; </script> Found with - this.$route.params.id; Bind all data dynamically to component -
const routes = [ { path: '/pokemon/:id', component: PokemonCard, children:
[ { path: '/stats', component: PokemonStats }, { path: '/evolutions', component: PokemonEvolutions } ] } ] Nested Routes router.js const routes = [ { path: '/pokemon/:id', component: PokemonCard, children: [ { path: '/stats', component: PokemonStats }, { path: '/evolutions', component: PokemonEvolutions } ] } ] const routes = [ { path: '/pokemon/:id', component: PokemonCard, children: [ { path: '/stats', component: PokemonStats }, { path: '/evolutions', component: PokemonEvolutions } ] } ] -
Nested Routes PokemonCard.vue <template> <div class="pokemon"> <div class="card-image"> ... <router-view></router-view>
</div> </div> </template> <script> export default { name: "PokemonCard", }; </script> <template> <div class="pokemon"> <div class="card-image"> ... <router-view></router-view> </div> </div> </template> <script> export default { name: "PokemonCard", }; </script> <template> <div class="pokemon"> <div class="card-image"> ... <router-view></router-view> </div> </div> </template> <script> export default { name: "CharizardCard", }; </script> RouterView responsible in rendering the correct nested component based on the nested route. -
Navigation Guards 1. Global - for all navigation routes 2.
Per-route - for a certain route 3. In-component - for a certain component -
const router = new VueRouter({ ... }) router.beforeEach((to, from, next)
=> { // ... }); Global Navigation Guards const router = new VueRouter({ ... }) router.beforeEach((to, from, next) => { // ... }); const router = new VueRouter({ ... }) router.beforeEach((to, from, next) => { // ... }); const router = new VueRouter({ ... }) router.beforeEach((to, from, next) => { // ... }); const router = new VueRouter({ ... }) router.beforeEach((to, from, next) => { // ... }); to.path next(); next('/about'); next(false); next() function must be invoked to complete routing process -
const router = new VueRouter({ ... }) router.afterEach((to, from) =>
{ // ... }); Global After Hooks router.afterEach((to, from) => { // ... }); -
Per-Route Navigation Guards const routes = [ { path: '/pokemon/:id',
component: PokemonCard, beforeEnter: (to, from, next) => { ... } } ] -
In-Component Navigation Guards <template> ... </template> <script> export default {
name: "PokemonCard", beforeRouteEnter (to, from, next) {}, beforeRouteUpdate (to, from, next) {}, beforeRouteLeave (to, from, next) {} }; </script> <template> ... </template> <script> export default { name: "PokemonCard", beforeRouteEnter (to, from, next) {}, beforeRouteUpdate (to, from, next) {}, beforeRouteLeave (to, from, next) {} }; </script> beforeRouteEnter (to, from, next) {}, beforeRouteUpdate (to, from, next) {}, beforeRouteLeave (to, from, next) {}, -
- Programmatic Navigation - Redirects/Aliases - Transitions - Lazy Loading
vue-router also provides other additional capabilities… -
- Vue Router Documentation https://router.vuejs.org/ - Let’s build a Custom
Vue Router https://css-tricks.com/build-a-custom-vue-router/