Vue.js (v2.x) - Routing İşlemi

Vue Router (vue-router) Kullanımı

Vue.js giriş yazısında Vue’nun temel kapsamının ve özelliklerinin resmi ve/veya 3. parti geliştiriciler tarafından sağlanan eklentiler vasıtasıyla genişletilebileceğinden bahsetmiştim. Hatta, bu eklentilerin yer aldığı Awesome Vue.js listesinin de güncel olarak takip edilebileceğini belirtmiştim.

AA

İki bölüm halinde, bahsi geçen resmi eklentilerden biri olan Vue-router‘dan bahsedeceğim. İlk yazı sıklıkla kullanacağımız temel özellikleri içermekte.

Vue.js Router

Vue-router, Vue.js için duyurulmuş resmi router eklentisidir ve Vue.js ile Tek Sayfa Uygulamaları (SPA) geliştirebilmek amacıyla Vue.js çekirdeği ile bütünleşir. Şu özellikleri barındırır:

  • Nested route/view mapping
  • Modüler, bileşen temelli (component-based) router yapılandırması
  • Route parametreleri, sorguları, joker karakter desteği
  • View transition özellikleri
  • Fine-grained navigasyon kontrolü
  • Otomatik aktif CSS sınıflarına sahip bağlantılar
  • HTML5 history mode ya da hash mode
  • Özelleştirilebilir Scroll davranışları

Nested routing ile ilgili de bir not eklemekte fayda var. Klasik bir GET request (istek) scheme://domain:port/path?query_string şeklindedir.

URI, URL ve URN başlıklı yazıda bu konuya istinaden bazı notlar aktarmıştım. /pageId=1&commentId=10 şeklinde bir URL örneği oluşturalım. RESTful API geliştirilirken Endpoint (URL) olarak /pages/1/comments kullanımını tercih ederiz. Bu örnekte Page ID’si 1 olan yazının tüm yorumlarını edinebiliriz. Spesifik bir yorum için işlem yapmak isteyelim ve URL’e şu eklemeyi yapalım; /pages/1/comments/10. Bu durumda ilgili yazının 10 numaralı yorumuna işlem yapabiliriz. Routing (URL Eşleştirme / Yönlendirme) başlıklı yazıda verilen örneklere ulaşabilirsiniz.

Node.js, Django, Rails, Laravel gibi Full-Stack Framework’lerde bu işlemler Resources olarak nitelendirilir1.

Tekrar örneğe dönecek olursak, /pages/1/comments10 yapısında pages route içerisinde comments route bulunması route işlemini dinamik (dynamic route maching) hale getirir. Ancak, comments yerine replies ya da shares gibi bir bileşeni de dahil etmek istersek? İşte bu kullanım bir nested route olarak ifade edilebilir ve RESTful API ve web uygulamalarında kolaylıkla kullanılabilir. Detayları aktarmaya örnekler üzerinden devam edeceğim. Şimdi, bir sonraki konuya geçebiliriz.

Vue.js Routing Kurulumu

Doğrudan veya Vue-CLI aracılığıyla eklentiyi çalışmalarımıza dahil edebiliriz.

<script src="https://unpkg.com/vue-router/dist/vue-router.js"></script>

Ya da vue add komutu ile edinebiliriz.

vue add router

Ayrıca kullandığımız paket yöneticileri (örneğin yarn) aracılığıyla da vue-router’ı çalışma alanımıza dahil edebiliriz.

npm install vue-router
# ya da
yarn add vue-router

Routing (URL Eşleştirme / Yönlendirme) Nedir? başlıklı yazıda basit bir Vue.js Routing örneği yapmıştık. Vue.js tarafından yayınlanan Simple Routing From Scratch2 3 de ayrıca incelenebilir.

Vue Router İşlemi

Vue-router kullanımında linklendirmeleri HTML etiketleri yerine biçiminde declarative olarak tanımlarız. Vue, render aşamasında ilgili link tanımını (router link) bizim için HTML etiketlerine dönüştürür. Ayrıca, programmatic olarak da linklendirme işlemi yapabilmekteyiz. Bu amaçla kullanacağımız fonksiyonumuz ise router.push(...) şeklinde kullanılmaktadır.

Dinamik olarak verileri bize iletecek View alanımızı ise etiketleriyle belirtiriz. Vue-link ile değişen URL için bu etiket cevap verir.

Toparlayacak olursak, vue-router en temelinde şu parçalardan oluşur:

<router-link to="/foo">foo</router-link>
<router-link to="/bar">bar</router-link>
<router-view></router-view>

Dinamik Route Eşleştirme

Şablonlar aracılığıyla route işlemini aynı bileşene eşleyerek ilerleyebiliriz. Örneğin, az önce nested routing açıklamasında pages ve comments’ten bahsetmiştim. Bu bağlamda pages ve comments adında bileşenlerimiz olduğunu düşünelim. Ancak, her page ve her comment birbirinden bağımsız. Dolayısıyla, vue-router ile dinamik bir şekilde eşleşen verileri görüntülemek isteyebiliriz:

const Page = { template: '<div>Page</div>' }
const NotFoundComponent = { template: '<p>Page not found</p>' }

// Vue v2.x
const router = new VueRouter({
  routes: [
    { path: '/pages/:id', component: Page }
  ]
});

// Vue v3.x
const routes = {
  '/pages/:id': Page
}

Vue.createApp({
  data: () => ({
    currentRoute: window.location.pathname
  }),

  computed: {
    CurrentComponent() {
      return routes[this.currentRoute] || NotFoundComponent
    }
  },

  render() {
    return Vue.h(this.CurrentComponent)
  }
}).mount('#app');

Bu şekilde URL /pages/foo ve /pages/bar olduğunda benzer route’a map’lenecektir. :id tanımı dinamik bir segmenti ifade eder. Bu dinamik segmentler : ile ifade edilir. Böylelikle, route eşleştiğinde dinamik segmentin değeri (value) her bileşen için this.$route.params ile gösterilir.

const Page = {
  template: '<div>Page {{ $route.params.id }}</div>'
}

Bu anlattıklarımızı Vue v2.x sürümü üzerinden bir örnek olarak toparlayalım:

<script src="https://unpkg.com/vue/dist/vue.js"></script>
<script src="https://unpkg.com/vue-router/dist/vue-router.js"></script>

<section id="app" class="section" v-cloak>
  <div class="container">
    <div class="navbar-menu">
      <router-link to="/pages/foo" class="navbar-item">Page foo</router-link>
      <router-link to="/pages/bar" class="navbar-item">Page bar</router-link>
    </div>
    <hr />
    <router-view></router-view>
  </div>
</section>

<script>
  // Vue v2.x
  const Page = {
    template: `<p class="title">It is a {{ $route.params.id }} page</p>`
  }

  const router = new VueRouter({
    routes: [{
      path: '/pages/:id',
      component: Page
    }]
  });

  const app = new Vue({
    router
  }).$mount('#app');
</script>

Vue v3.x ile ilgili basi bir örnek için vue-3.0-simple-routing-example ve daha kapsamlı örnekler için vue-router/examples/ repo'larına göz atabilirsiniz.

Vue v2.x ile ilgili örneğimize dönelim. Örnekteki /pages/1/comments/10 ifadesi /pages/:id ile eşleşti ve başarıyla ilgili içeriği bize gösterdi. Peki, comments gibi çoklu dinamik segmentleme işleminde nasıl ilerlememiz gerekir? Cevap, $route.params.

Şablon Eşleşen yol $route.params
/pages/:pageId /pages/1 { pageId: 1 }
/pages/:pageId/comments/:commentId /pages/1/comments/10 { pageId: 1, commentId: 10 }

$route.params dışında $route nesnesi ile de $route.query (eğer URL bir query içeriyor ise) ve $route.hash gibi pek çok detaya erişebilmekteyiz4.

Parametre Değişiklikleri

Yukarıdaki örnekte /pages/foo ve /pages/bar değişimlerinde aynı component cevap vermekte. Bu, her iki route’un da aynı bileşeni render etmesi eski instance’i kalırıp ve yeni instance’i oluşturmaktan daha etkili bir yöntemdir. Bu aynı zamanda, route ile işlem gören component’lerin lifecycle hooks dışında kalabileceği anlamına da gelmekte. Bu tür durumlarda $route nesnesi watch edilebilir ya da beforeRouteUpdate kullanılabilir.

const Page = {
  template: '...',
  watch: {
    '$route' (to, from) {
      // route değişikliklerine karşı uygulanacak işlemler
    }
  }
}

const Page = {
  template: '...',
  beforeRouteUpdate (to, from, next) {
    // route değişikliklerine karşı uygulanacak işlemler
    // next()'i call etmek unutulmamalı!
  }
}

Tüm Kapsam / 404 Not found Route

Düzenli ifadeler / parametreler sadece tanımlandıkları ve / ile ayrıştırıldıkları şekilde eşleşirler. Bunun yanı sıra, vue-router asterisk (*) kullanımını da desteklemektedir.

{
  // her değerler eşleşir
  path: '*'
}
{
  // `/page-` ile başlayan değerlerle eşleşir
  path: '/page-*'
}

Asterisk (*) kullanımında parametre doğrudan $route.params‘a da iletilir. İletilen bu değer pathMatch ile edinilebilir.

// Route { path: '/page-*' }
this.$router.push('/page-about')
this.$route.params.pathMatch // 'about'

// Route { path: '*' }
this.$router.push('/non-existing')
this.$route.params.pathMatch // '/non-existing'

Bu özellikle birlikte, 404 Not found gibi hata sayfaları için asterisk (*) kullanımı değerlendirilebilir. Asterisk dışında RegExp ile daha kapsamlı çözümler de üretilebilir.

Nested Routes

Nested routing ile ilgili açıklamaya yazının ilk bölümlerinde yer vermiştim. Uygulamaların çoğu zaman pek çok bileşenin iç içe ve bir araya gelmesinden oluştuğunu ve bir URL’in birden fazla segment barındırabildiğini biliyoruz. Bu segmentlerin : ile ifade edildiğinden bahsetmiştim. Şimdi konuyu biraz daha detaylandırıp örneklendirelim ve şu şekilde URL yapıları oluşturalım:

  • pages/foo/comments
  • pages/foo/shares
  • pages/foo/replies

Bu örnekte Pages Comments’i, Shares’i ve Replies’i ayrı kapsamaktadır. Comments, Shares ve Replies ise birbirinden bağımsızdır. O halde, yukarıda oluşturduğumuz örneğimizi bu yapıya uygun şekilde güncelleyelim:

<script src="https://unpkg.com/vue/dist/vue.js"></script>
<script src="https://unpkg.com/vue-router/dist/vue-router.js"></script>

  <section id="app" class="section" v-cloak>
    <div class="container">
      <div class="navbar-menu">
        <router-link to="/pages/1/comments/11" class="navbar-item">Comments</router-link>
        <router-link to="/pages/2/replies/22" class="navbar-item">Replies</router-link>
        <router-link to="/pages/3/shares/33" class="navbar-item">Shares</router-link>
      </div>
      <router-view></router-view>
    </div>
  </section>

  <script>
    // Vue v2.x
    const Page = {
      template: `
        <div class="user">
          <h2>Params: {{ $route.params }}</h2>
          <router-view></router-view>
        </div>
      `
    }

    const userComments = {
      template: `<div>Subpage: Comments</div>`
    }
    const userShares = {
      template: `<div>Subpage: Sharings</div>`
    }
    const userReplies = {
      template: `<div>Replies</div>`
    }

    const router = new VueRouter({
      routes: [{
        path: '/pages/:pageId/:pageParts/:partId/',
        component: Page,
        children: [{
            path: 'comments',
            component: userComments
          },
          {
            path: 'shares',
            component: userShares
          },
          {
            path: 'replies',
            component: userReplies
          }
        ]
      }]
    })

    const app = new Vue({
      router
    }).$mount('#app')
  </script>

Evet, menüde yer alan linkleri kullanarak parametreleri görüntüleyebilir, child compotent’ler ile uygulama yapısını daha kapsamlı bir hale getirebiliriz.

HTML5 History Mode

Bu yazı kapsamında son bahsedeceğim konu HTML5 Geçmiş Modu (History Mode). Yukarıdaki örneklerde de görüldüğü üzere vue-router ön tanımlı olarak hash (#) modu kullanmaktadır. Bu sayede, URL’i sembolize eden hash ile sayfa yenilenmeden URL değişiklikleri uygulanabilmekte. Çeşitli gereksinimler doğrultusunda hash mode yerine history mode kullanmamız gerekebilir. Bu durumda mod değişikliğini router’a belirtmemiz gerekir.

const router = new VueRouter({
  mode: 'history',
  routes: [...]
})

History mode’un aktifleştirilmesinin ardından URL’lerimiz daha “normal” görünecektir. Ancak, history mode ile ilgili unutmamamız gereken bir konu var. SPA uygulamalarda bir server yapılandırması söz konusu değilse history mode 404 hatasına neden olur. Apache için örnek yapılandırma:

<IfModule mod_rewrite.c>
  RewriteEngine On
  RewriteBase /
  RewriteRule ^index\.html$ - [L]
  RewriteCond %{REQUEST_FILENAME} !-f
  RewriteCond %{REQUEST_FILENAME} !-d
  RewriteRule . /index.html [L]
</IfModule>

Nginx için örnek yapılandırma:

location / {
  try_files $uri $uri/ /index.html;
}

Firebase hosting için örnek yapılandırma:

{
  "hosting": {
    "public": "dist",
    "rewrites": [
      {
        "source": "**",
        "destination": "/index.html"
      }
    ]
  }
}

Son olarak, server yapılandırması neticesinde artık eşleşmeyen durumlar için 404 hatası oluşturamayabilirsiniz. Çünkü, her durumda uygulama bir sayfa üzerinden URL eşleştirmesi sağlayacaktır, ancak server bir sayfa hatası almayacaktır. Bu gibi durumlarda yazı içerisinde de bahsi geçen asterisk (*) kullanımını tercih edebilir ve eşleşmeyen URL’ler için hata içeriği döndürebiliriz.

const router = new VueRouter({
  mode: 'history',
  routes: [
    { path: '*', component: NotFoundComponent }
  ]
})

Evet, vur-router ile ilgili sıklıkla karşılaştığım durumlardan bahsetmiş olduk. Bir sonraki yazıda biraz daha teknik ayrıntılara değinece ve sonrasında vue-router ile örnek bir uygulama hazırlayacağız.