Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Understanding Client Side Routing with Vue.js

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
Tweet

More Decks by djirdehh

Other Decks in Technology

Transcript

  1. Vue is the Progressive JavaScript framework that is approachable, versatile,

    and performant. Progressive approachable versatile performant
  2. 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…
  3. 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…
  4. 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
  5. We’ll be building this Pokemon App that displays details of

    a Pokemon based on the route.
 
 Live version: http://pokemon-routing.surge.sh/
  6. 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.
  7. main.js import Vue from 'vue'; import App from './App'; new

    Vue({ el: '#app', render: h => h(App) });
  8. Vue Components Markup (HTML) Logic (JS) Styles (CSS) we’ll take

    a step back and talk about Vue components
  9. <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*
  10. 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>
  11. <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>
  12. <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';
  13. 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!
  14. <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
  15. </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
  16. 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
  17. <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
  18. 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.
  19. We’ve bound an href attribute to the <a> of RouterLink

    to allow users to hover and see where they lead to.
  20. 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.
  21. An Event Bus is a Vue instance that is used

    to enable isolated components to subscribe and publish custom events between each other.
  22. 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'); } }
  23. 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';
  24. 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>
  25. popstate Last item to take care of is browser navigation

    events. For that we’ll use the popstate event.
  26. 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>
  27. 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.
  28. vue-router is the official router of Vue. It deeply
 integrates

    with Vue to make building Single Page applications a breeze.
  29. 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> ` } } ];
  30. 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 });
  31. 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.
  32. 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.
  33. 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.
  34. 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.
  35. - Well Tested - Consistency between different browsers - Dynamic

    Route Matching - Nested Routes - Navigation Guards vue-router > custom router
  36. 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.
  37. Dynamic Route Matching router.js const routes = [ {path: '/',

    component: CharizardCard}, {path: '/charizard', component: CharizardCard}, {path: '/blastoise', component: BlastoiseCard}, {path: '/venusaur', component: VenusaurCard}, ... ]; Before
  38. Dynamic Route Matching router.js const routes = [ {path: '/',

    component: CharizardCard}, {path: '/pokemon/:id', component: PokemonCard } ]; After
  39. <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>
  40. 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.
  41. <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
  42. 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 } ] } ]
  43. 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.
  44. Navigation Guards 1. Global - for all navigation routes 2.

    Per-route - for a certain route 3. In-component - for a certain component
  45. 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
  46. const router = new VueRouter({ ... }) router.afterEach((to, from) =>

    { // ... }); Global After Hooks router.afterEach((to, from) => { // ... });
  47. Per-Route Navigation Guards const routes = [ { path: '/pokemon/:id',

    component: PokemonCard, beforeEnter: (to, from, next) => { ... } } ]
  48. 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) {},
  49. - Programmatic Navigation - Redirects/Aliases - Transitions - Lazy Loading

    vue-router also provides other additional capabilities…
  50. - Vue Router Documentation https://router.vuejs.org/ - Let’s build a Custom

    Vue Router https://css-tricks.com/build-a-custom-vue-router/