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/

051bc9fde06b7e15f7439670089c9054?s=128

djirdehh

July 19, 2018
Tweet

Transcript

  1. Hassan Djirdeh Understanding Client Side Routing with Vue.js @djirdehh

  2. Engineering Present ~ 2 years

  3. None
  4. None
  5. Vue is the Progressive JavaScript framework that is approachable, versatile,

    and performant. Progressive approachable versatile performant
  6. Progressive <script src="https://cdn.jsdelivr.net/npm/vue"></script> Get started with a single script tag

  7. approachable HTML CSS JS

  8. performant Virtual DOM

  9. Vuex Vue Router Vue Test Utils versatile

  10. Routing Today’s subject: Routing

  11. Routing often refers to splitting an applications UI based on

    rules derived from the browser URL.
  12. https://www.twitter.com/

  13. /{userName} https://www.twitter.com/ Dynamic route

  14. https://www.twitter.com/{userName} protocol hostname pathname

  15. 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…
  16. 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…
  17. 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
  18. We’ll be building this Pokemon App that displays details of

    a Pokemon based on the route.
 
 Live version: http://pokemon-routing.surge.sh/
  19. 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.
  20. public/ app/ main.js App.vue .babelrc .gitignore .index.html .package.json src/ node_modules/

  21. main.js import Vue from 'vue'; import App from './App'; new

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

    a step back and talk about Vue components
  23. <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*
  24. 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>
  25. http://localhost:8080/ App CharizardCard Let’s externalize the markup to a unique

    component
  26. <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>
  27. <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';
  28. CharizardCard.vue App.vue

  29. app/ main.js App.vue components/ BlastoiseCard.vue CharizardCard.vue VenusaurCard.vue We’ll create other

    files for other components
  30. BlastoiseCard.vue CharizardCard.vue VenusaurCard.vue App.vue

  31. app/ main.js App.vue components/ router/ RouterLink.vue RouterView.vue routes.js

  32. 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!
  33. <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
  34. </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
  35. 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
  36. <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
  37. /charizard /blastoise /venusaur /pikachu

  38. <a href="?"></a>

  39. 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.
  40. We’ve bound an href attribute to the <a> of RouterLink

    to allow users to hover and see where they lead to.
  41. 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.
  42. EventBus

  43. An Event Bus is a Vue instance that is used

    to enable isolated components to subscribe and publish custom events between each other.
  44. import Vue from 'vue'; export const EventBus = new Vue();

    EventBus = new Vue(); event-bus.js
  45. 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'); } }
  46. 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';
  47. 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>
  48. popstate Last item to take care of is browser navigation

    events. For that we’ll use the popstate event.
  49. 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>
  50. None
  51. 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.
  52. vue-router

  53. vue-router is the official router of Vue. It deeply
 integrates

    with Vue to make building Single Page applications a breeze.
  54. npm i vue-router —-save

  55. import Vue from 'vue'; import VueRouter from 'vue-router'; Vue.use(VueRouter);

  56. app/ main.js App.vue components/ router.js

  57. 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> ` } } ];
  58. 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 });
  59. 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.
  60. 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.
  61. 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.
  62. 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.
  63. /charizard /blastoise /venusaur /gyrados

  64. - Well Tested - Consistency between different browsers - Dynamic

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

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

    component: CharizardCard}, {path: '/pokemon/:id', component: PokemonCard } ]; After
  68. <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>
  69. 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.
  70. <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
  71. App PokemonCard PokemonStats

  72. Nested Routes PokemonCard PokemonStats PokemonCard PokemonEvolutions /pokemon/:id/stats /pokemon/:id/evolutions

  73. 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 } ] } ]
  74. 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.
  75. Navigation Guards 1. Global - for all navigation routes 2.

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

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

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

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

    Vue Router https://css-tricks.com/build-a-custom-vue-router/
  82. None
  83. Details https://www.fullstack.io/vue

  84. @djirdehh Hassan Djirdeh