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.
İ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.