Akışları olan daha hızlı çok sayfalı uygulamalar

Günümüzde web siteleri (veya dilerseniz web uygulamaları) iki gezinme şemasından birini kullanıyor:

  • Tarayıcıların varsayılan olarak sağladığı gezinme şeması, tarayıcınızın adres çubuğuna bir URL girersiniz ve gezinme isteği yanıt olarak bir doküman döndürür. Ardından, bir bağlantıyı tıkladığınızda mevcut dokümandaki ad infinitum adlı başka bir doküman kaldırılır.
  • Uygulama kabuğunu yüklemek için ilk gezinme isteğini içeren ve uygulama kabuğunu her "gezinme" için bir arka uç API'sinden alınmış içerikle istemci tarafından oluşturulmuş işaretlemeyle doldurmak üzere JavaScript'ten yararlanan tek sayfalık uygulama kalıbı.

Her iki yaklaşımın faydaları da savunucuları tarafından dile getirilmiştir:

  • Rotalar JavaScript'in erişilebilir olmasını gerektirmediğinden tarayıcıların varsayılan olarak sağladığı gezinme şeması esnektir. İşaretlemenin JavaScript yoluyla istemci tarafından oluşturulması da pahalı olabilecek bir süreç olabilir. Bu da, cihazın içerik sağlayan komut dosyalarının işlenmesini engellemesi nedeniyle, içeriğin gecikmesi nedeniyle düşük teknolojili cihazların ortaya çıkabileceği anlamına gelir.
  • Diğer yandan, Tek Sayfalık Uygulamalar (SPA'lar), ilk yüklemeden sonra daha hızlı gezinme sağlayabilir. Tamamen yeni bir doküman için tarayıcının bir dokümanı kaldırmasına güvenmek (ve her gezinmede bunu tekrarlamak) yerine, daha hızlı, "uygulama benzeri" bir deneyim sunabilirler deneyimini sağlamalısınız.

Bu gönderide, yukarıda açıklanan iki yaklaşım arasında denge sağlayan üçüncü bir yöntemden bahsedeceğiz: bir web sitesinin ortak öğelerini (üstbilgi ve altbilgi işaretlemesi gibi) önbelleğe almak için bir service Worker'dan yararlanmak ve istemciye olabildiğince hızlı bir şekilde HTML yanıtı sağlamak için akışları kullanmak (tümünü tarayıcının varsayılan gezinme şemasını kullanarak).

Neden HTML yanıtlarını hizmet çalışanında aktarmalısınız?

Akış, web tarayıcınızın istek gönderirken zaten yaptığı bir işlemdir. Bu, tarayıcının doküman işaretlemesini ayrıştırmaya ve bir sayfa oluşturmaya başlamadan önce yanıtın tamamını beklerken engellenmemesini sağladığından, gezinme istekleri açısından son derece önemlidir.

Akışsız HTML ve akışlı HTML'yi gösteren bir şema. İlk durumda, işaretleme yükünün tamamı gelene kadar işlenmez. İkincisinde işaretleme, ağdan parçalar halinde ulaştıkça aşamalı olarak işlenir.

Hizmet çalışanları için akış, JavaScript Streams API'yi kullandığından biraz farklıdır. Bir hizmet çalışanının yerine getirdiği en önemli görev, gezinme istekleri de dahil olmak üzere isteklere müdahale etmek ve bunları yanıtlamaktır.

Bu istekler önbellek ile çeşitli şekillerde etkileşim kurabilir ancak işaretleme için yaygın bir önbelleğe alma kalıbı, önce ağdan gelen bir yanıtı kullanmaya öncelik vermek, ancak eski bir kopya varsa önbelleğe almak ve kullanılabilir bir yanıt önbellekte yoksa isteğe bağlı olarak genel bir yedek yanıtı sağlamak şeklindedir.

Bu, iyi çalışan ve zamanla test edilmiş bir kalıptır. Ancak çevrimdışı erişim açısından güvenilirlik sağlamaya yardımcı olsa da, öncelikli ağ veya yalnızca ağ stratejisine dayanan gezinme istekleri için doğal performans avantajı sağlamaz. Bu noktada akış devreye girer. Ayrıca çok sayfalı web sitenizde gezinme isteklerini hızlandırmak için Workbox hizmet çalışanınızda Streams API destekli workbox-streams modülünü nasıl kullanacağınızı öğreneceğiz.

Tipik bir web sayfasının dökümünü alma

Web siteleri, yapısal olarak her sayfada ortak öğeler kullanma eğilimi gösterir. Sayfa öğelerinin tipik bir düzeni genellikle aşağıdaki gibidir:

  • Başlık.
  • İçerik.
  • Altbilgi.

Örnek olarak web.dev üzerinden yaygın öğelerin dökümü şu şekilde görünür:

web.dev web sitesindeki ortak öğelerin dökümü. Betimlenen ortak alanlar "başlık", "içerik" ve "altbilgi" olarak işaretlenmiştir.

Bir sayfanın bölümlerinin tanımlanmasındaki amaç, ağa gitmeden nelerin önbelleğe alınabileceğini ve alınabileceğini (yani tüm sayfalarda ortak olan üstbilgi ve altbilgi işaretlemesini) ve sayfanın her zaman ilk olarak ağa gideceğimiz bölümünü, yani bu örnekte içeriği belirlemektir.

Bir sayfanın bölümlerini nasıl segmentlere ayıracağımızı ve ortak öğeleri tanımladığımızda, yalnızca ağdaki içeriği isterken üst bilgi ve alt bilgi işaretlemesini her zaman önbellekten anında alan bir service çalışanı yazabiliriz.

Daha sonra, workbox-streams üzerinden Streams API'sini kullanarak, tüm bu parçaları birleştirebilir ve gezinme isteklerine anında yanıt verebilir, aynı zamanda ağdan gereken minimum işaretleme miktarını isteyebiliriz.

Akış hizmeti çalışanı oluşturma

Service Worker'da kısmi içerik akışı söz konusu olduğunda birçok hareketli parça vardır. Ancak siz ilerledikçe web sitenizi nasıl yapılandıracağınızdan başlayarak sürecin her adımı ayrıntılı olarak incelenecektir.

Web sitenizi bölümlere ayırma

Akış hizmeti çalışanı yazmaya başlamadan önce üç şey yapmanız gerekir:

  1. Yalnızca web sitenizin başlık işaretlemesini içeren bir dosya oluşturun.
  2. Yalnızca web sitenizin altbilgi işaretlemesini içeren bir dosya oluşturun.
  3. Her sayfanın ana içeriğini ayrı bir dosyaya alın veya arka ucunuzu, HTTP istek başlığına göre yalnızca sayfa içeriğini koşullu olarak sunacak şekilde ayarlayın.
ziyaret edin.

Tahmin edebileceğiniz gibi, özellikle web siteniz statikse son adım en zor adımdır. Bu durumda, her sayfanın iki sürümünü oluşturmanız gerekir: Bir sürüm tam sayfa işaretlemesini, diğeri ise yalnızca içeriği barındırır.

Akış hizmeti çalışanı oluşturma

workbox-streams modülünü yüklemediyseniz şu anda yüklediğiniz Workbox modüllerine ek olarak yükleme yapmanız gerekir. Bu özel örnekte aşağıdaki paketleri içerir:

npm i workbox-navigation-preload workbox-strategies workbox-routing workbox-precaching workbox-streams --save

Sonraki adım, yeni hizmet çalışanınızı oluşturmak ve üst bilgi ve alt bilgi kısmilerinizi önbelleğe almaktır.

Kısmi öğeleri önbelleğe alma

İlk olarak projenizin kök dizininde sw.js (veya tercih ettiğiniz dosya adı) adında bir hizmet çalışanı oluşturun. Bu kurs içinde aşağıdakilerle başlayacaksınız:

// sw.js
import * as navigationPreload from 'workbox-navigation-preload';
import {NetworkFirst} from 'workbox-strategies';
import {registerRoute} from 'workbox-routing';
import {matchPrecache, precacheAndRoute} from 'workbox-precaching';
import {strategy as composeStrategies} from 'workbox-streams';

// Enable navigation preload for supporting browsers
navigationPreload.enable();

// Precache partials and some static assets
// using the InjectManifest method.
precacheAndRoute([
  // The header partial:
  {
    url: '/partial-header.php',
    revision: __PARTIAL_HEADER_HASH__
  },
  // The footer partial:
  {
    url: '/partial-footer.php',
    revision: __PARTIAL_FOOTER_HASH__
  },
  // The offline fallback:
  {
    url: '/offline.php',
    revision: __OFFLINE_FALLBACK_HASH__
  },
  ...self.__WB_MANIFEST
]);

// To be continued...

Bu kodun birkaç işlevi vardır:

  1. Bunu destekleyen tarayıcılar için gezinme önyüklemesini etkinleştirir.
  2. Üstbilgi ve altbilgi işaretlemesini önbelleğe alır. Bu, ağ tarafından engellenmeyeceğinden, her sayfa için üstbilgi ve altbilgi işaretlemesinin anında alınacağı anlamına gelir.
  3. injectManifest yöntemini kullanan __WB_MANIFEST yer tutucusundaki statik öğeleri önbelleğe alır.
ziyaret edin.

Yanıtların akış şeklinde gösterilmesi

Hizmet çalışanınızın birleştirilmiş yanıtlar aktarmasını sağlamak bu çabanın en büyük parçasıdır. Yine de Workbox ve workbox-streams ürünü, tüm bunları tek başınıza yapmak zorunda olduğunuz durumlara kıyasla bunu çok daha kısa ve öz hale getirir:

// sw.js
import * as navigationPreload from 'workbox-navigation-preload';
import {NetworkFirst} from 'workbox-strategies';
import {registerRoute} from 'workbox-routing';
import {matchPrecache, precacheAndRoute} from 'workbox-precaching';
import {strategy as composeStrategies} from 'workbox-streams';

// ...
// Prior navigation preload and precaching code omitted...
// ...

// The strategy for retrieving content partials from the network:
const contentStrategy = new NetworkFirst({
  cacheName: 'content',
  plugins: [
    {
      // NOTE: This callback will never be run if navigation
      // preload is not supported, because the navigation
      // request is dispatched while the service worker is
      // booting up. This callback will only run if navigation
      // preload is _not_ supported.
      requestWillFetch: ({request}) => {
        const headers = new Headers();

        // If the browser doesn't support navigation preload, we need to
        // send a custom `X-Content-Mode` header for the back end to use
        // instead of the `Service-Worker-Navigation-Preload` header.
        headers.append('X-Content-Mode', 'partial');

        // Send the request with the new headers.
        // Note: if you're using a static site generator to generate
        // both full pages and content partials rather than a back end
        // (as this example assumes), you'll need to point to a new URL.
        return new Request(request.url, {
          method: 'GET',
          headers
        });
      },
      // What to do if the request fails.
      handlerDidError: async ({request}) => {
        return await matchPrecache('/offline.php');
      }
    }
  ]
});

// Concatenates precached partials with the content partial
// obtained from the network (or its fallback response).
const navigationHandler = composeStrategies([
  // Get the precached header markup.
  () => matchPrecache('/partial-header.php'),
  // Get the content partial from the network.
  ({event}) => contentStrategy.handle(event),
  // Get the precached footer markup.
  () => matchPrecache('/partial-footer.php')
]);

// Register the streaming route for all navigation requests.
registerRoute(({request}) => request.mode === 'navigate', navigationHandler);

// Your service worker can end here, or you can add more
// logic to suit your needs, such as runtime caching, etc.

Bu kod, aşağıdaki gereksinimleri karşılayan üç ana bölümden oluşur:

  1. NetworkFirst stratejisi, içerik kısmi isteklerinin işlenmesi için kullanılır. Bu strateji kullanıldığında, gezinme önceden yüklenmesini desteklemeyen (ve dolayısıyla Service-Worker-Navigation-Preload üstbilgisi göndermeyen) tarayıcılar için X-Content-Mode istek üst bilgisi ayarlanıp ayarlanmayacağını işleyen özel bir eklentinin yanı sıra içerik kısmilerini içerecek özel bir content önbellek adı belirtilir. Bu eklenti, geçerli istek için önbelleğe alınmış hiçbir sürümün saklanmaması durumunda içeriğin önbelleğe alınmış son sürümünün kısmi mi gönderileceğini veya çevrimdışı yedek sayfanın mı gönderileceğini belirler.
  2. workbox-streams içindeki strategy yöntemi (burada composeStrategies olarak adlandırılmıştır), önceden önbelleğe alınmış üst bilgi ve alt bilgi kısmilerini, ağdan istenen içeriğin bir kısmı ile birleştirmek için kullanılır.
  3. Şemanın tamamı, gezinme istekleri için registerRoute aracılığıyla kurulur.

Bu mantık doğrultusunda, yanıt akışı özelliğini ayarladık. Bununla birlikte, ağdaki içeriğin, önbelleğe alınmış kısmi sayfalarla birleştirebileceğiniz kısmi bir sayfa olduğundan emin olmak için arka uçta bazı işlemler yapmanız gerekebilir.

Web sitenizin bir arka ucu varsa

Gezinmeyi önceden yükleme özelliği etkinleştirildiğinde, tarayıcının true değerine sahip bir Service-Worker-Navigation-Preload üstbilgisi gönderdiğini hatırlarsınız. Ancak yukarıdaki kod örneğinde, etkinlik gezinme önceden yüklemesinde tarayıcıda desteklenmeyen X-Content-Mode özel üstbilgisini gönderdik. Arka uçta, bu üstbilgilerin varlığına göre yanıtı değiştirirsiniz. PHP arka ucunda, belirli bir sayfa için bu kod aşağıdaki gibi görünebilir:

<?php
// Check if we need to render a content partial
$navPreloadSupported = isset($_SERVER['HTTP_SERVICE_WORKER_NAVIGATION_PRELOAD']) && $_SERVER['HTTP_SERVICE_WORKER_NAVIGATION_PRELOAD'] === 'true';
$partialContentMode = isset($_SERVER['HTTP_X_CONTENT_MODE']) && $_SERVER['HTTP_X_CONTENT_MODE'] === 'partial';
$isPartial = $navPreloadSupported || $partialContentMode;

// Figure out whether to render the header
if ($isPartial === false) {
  // Get the header include
  require_once($_SERVER['DOCUMENT_ROOT'] . '/includes/site-header.php');

  // Render the header
  siteHeader();
}

// Get the content include
require_once('./content.php');

// Render the content
content($isPartial);

// Figure out whether to render the footer
if ($isPartial === false) {
  // Get the footer include
  require_once($_SERVER['DOCUMENT_ROOT'] . '/includes/site-footer.php');

  // Render the footer
  siteFooter();
}
?>

Yukarıdaki örnekte, içerik kısımları işlev olarak çağrılır. Bu kısmi bölümlerin oluşturulma şeklini değiştirmek için $isPartial değerini alır. Örneğin, content oluşturucu işlevi kısmi olarak alındığında yalnızca bazı koşullarda belirli işaretlemeleri içerebilir. Bu konuya kısa süre içinde değineceğiz.

Dikkat edilmesi gereken noktalar

Kısmi öğeleri akışla birleştirmek için bir hizmet çalışanı dağıtmadan önce göz önünde bulundurmanız gereken bazı noktalar vardır. Bir hizmet çalışanının bu şekilde kullanılmasının tarayıcının varsayılan gezinme davranışını temelden değiştirmediği doğru olsa da ilgilenmeniz gereken bazı konular vardır.

Gezinirken sayfa öğelerini güncelleme

Bu yaklaşımın en zor yanı, istemcide bazı şeylerin güncellenmesinin gerekmesidir. Örneğin, başlık işaretlemesinin önbelleğe alınması, sayfanın <title> öğesinde aynı içeriğe sahip olacağı veya gezinme öğelerinin açık/kapalı durumlarının yönetilmesinin her gezinmede güncellenmesi gerektiği anlamına gelir. Her gezinme isteği için bu bilgilerin ve diğerlerinin istemcide güncellenmesi gerekebilir.

Bunu aşmanın yolu, birkaç önemli şeyi güncellemek için ağdan gelen içerik bölümüne satır içi bir <script> öğesi yerleştirmek olabilir:

<!-- The JSON below contains information about the current page. -->
<script id="page-data" type="application/json">'{"title":"Sand Wasp &mdash; World of Wasps","description":"Read all about the sand wasp in this tidy little post."}'</script>
<script>
  const pageData = JSON.parse(document.getElementById('page-data').textContent);

  // Update the page title
  document.title = pageData.title;
</script>
<article>
  <!-- Page content omitted... -->
</article>

Bu, Service Worker kurulumuyla devam etmeye karar vermeniz durumunda yapmanız gerekenlere dair yalnızca bir örnektir. Örneğin, kullanıcı bilgileri içeren daha karmaşık uygulamalar için alakalı verilerin bir kısmını localStorage gibi bir web mağazasında depolamanız ve sayfayı buradan güncellemeniz gerekebilir.

Yavaş ağlarla başa çıkma

Önbellekten işaretlemeyi kullanan akış yanıtlarının bir dezavantajı, ağ bağlantıları yavaş olduğunda ortaya çıkabilir. Sorun, önbellekten başlık işaretlemesinin anında gelmesidir, ancak başlık işaretlemesi ilk kez boyandıktan sonra, ağdaki kısmi içeriğin ulaşması uzun sürebilir.

Bu durum kafa karıştırıcı bir deneyime neden olabilir ve ağlar çok yavaşsa sayfanın bozuk olduğu ve artık hiç görüntülenmediği hissine kapılabilir. Bu gibi durumlarda, içerik kısmi işaretlemesine, içerik yüklendikten sonra gizleyebileceğiniz bir yükleme simgesi veya mesaj yerleştirmeyi seçebilirsiniz.

Bunu CSS aracılığıyla yapabilirsiniz. Başlığınızın kısmi bir açılış <article> öğesiyle sonlandığını ve içeriğin kısmi olarak doldurulana kadar boş olduğunu varsayalım. Şuna benzer bir CSS kuralı yazabilirsiniz:

article:empty::before {
  text-align: center;
  content: 'Loading...';
}

Bu komut çalışır ancak ağ hızından bağımsız olarak istemcide bir yükleme mesajı gösterir. Tuhaf bir mesaj oluşmasını önlemek istiyorsanız seçiciyi yukarıdaki snippet'e bir slow sınıfı içine yerleştirdiğimiz şu yaklaşımı deneyebilirsiniz:

.slow article:empty::before {
  text-align: center;
  content: 'Loading...';
}

Buradan, belirli bağlantı türlerinde <html> öğesine slow sınıfını eklemek için etkili bağlantı türünü (en azından Chromium tarayıcılarında) okumak üzere kısmi bir JavaScript kullanabilirsiniz:

<script>
  const effectiveType = navigator?.connection?.effectiveType;

  if (effectiveType !== '4g') {
    document.documentElement.classList.add('slow');
  }
</script>

Bu, 4g türünden daha yavaş olan etkili bağlantı türlerinin bir yükleme mesajı almasını sağlar. Daha sonra içerik bölümünde, yükleme iletisinden kurtulmak amacıyla slow sınıfını HTML'den kaldırmak için satır içi bir <script> öğesi yerleştirebilirsiniz:

<script>
  document.documentElement.classList.remove('slow');
</script>

Yedek yanıt sağlama

İçerik kısmi öğeleri için ağ öncelikli bir strateji kullandığınızı varsayalım. Kullanıcı çevrimdışıysa ve daha önce gitmiş olduğu bir sayfaya giderse kapsam dışında kalır. Ancak henüz gitmedikleri bir sayfaya giderse hiçbir şey görmezler. Bunu önlemek için bir yedek yanıt yayınlamanız gerekir.

Yedek yanıtı elde etmek için gereken kod önceki kod örneklerinde gösterilmiştir. Bu işlem iki adımdan oluşur:

  1. Çevrimdışı bir yedek yanıtı önbelleğe alın.
  2. Bir sayfanın en son erişilen sürümü için önbelleği kontrol etmek üzere ağ öncelikli stratejiniz için eklentide bir handlerDidError geri çağırma ayarlayın. Sayfaya hiç erişilmediyse yedek yanıtı ön önbellekten almak için workbox-precaching modülünden matchPrecache yöntemini kullanmanız gerekir.

Önbelleğe alma ve CDN'ler

Service Worker'ınızda bu akış kalıbını kullanıyorsanız aşağıdaki durumun sizin durumunuz için geçerli olup olmadığını değerlendirin:

  • Bir CDN veya başka herhangi bir tür ara/herkese açık önbellek kullanırsınız.
  • public yönergesiyle birlikte, sıfır olmayan bir max-age ve/veya s-maxage yönergesi içeren bir Cache-Control başlığı belirttiniz.

Bunların ikisi de sizin için geçerliyse ara önbellek, gezinme isteklerine ilişkin yanıtları bekleyebilir. Ancak, bu modeli kullandığınızda herhangi bir URL için iki farklı yanıt sunuyor olabileceğinizi unutmayın:

  • Üstbilgi, içerik ve altbilgi işaretlemesini içeren tam yanıt.
  • Yalnızca içeriği barındıran kısmi yanıt.

Bu durum, hizmet çalışanı CDN önbelleğinden tam yanıt alıp bunu önceden önbelleğe alınmış üstbilgi ve altbilgi işaretlemenizle birleştirdiği için bazı istenmeyen davranışlara neden olarak üst bilgi ve alt bilgi işaretlemesinin iki katına çıkmasına neden olabilir.

Bunu başarmak için Vary başlığını kullanmanız gerekir. Bu başlık, istekte mevcut olan bir veya daha fazla üstbilgiye önbelleğe alınabilir yanıtları anahtarlayarak önbelleğe alma davranışını etkiler. Gezinme isteklerine verilen yanıtları Service-Worker-Navigation-Preload ve özel X-Content-Mode istek başlıklarına göre değiştirdiğimiz için yanıtta şu Vary başlığını belirtmemiz gerekir:

Vary: Service-Worker-Navigation-Preload,X-Content-Mode

Bu üstbilgi sayesinde tarayıcı, gezinme istekleri için tam ve kısmi yanıtları ayırt ederek, ara önbelleklerde olduğu gibi, iki kez başlık ve altbilgi işaretlemesiyle ilgili sorunları ortadan kaldırır.

Sonuç

Yükleme süresindeki performans önerilerinin çoğu "ne yaptığınızı gösterin" şeklindedir. Hiçbir şey beklemeden, kullanıcıya bir şey göstermeden önce elinizden gelenin en iyisini yapana kadar beklemeyin.

Jake Archibald, Fun Hacks for Faster Content

Tarayıcılar, büyük HTML yanıt gövdeleri için bile gezinme isteklerine verilen yanıtları ele alma konusunda başarılıdır. Varsayılan olarak tarayıcılar, işaretlemeyi kademeli olarak küçük parçalar halinde işler ve akışlar yapar. Böylece uzun görevlerden kaçınmış olurlar. Bu, başlangıç performansı için iyi bir şeydir.

Akış hizmeti çalışanı kalıbı kullandığımızda bu yöntem bize avantaj sağlıyor. Hizmet çalışanı önbelleğinden gelen bir isteğe başından itibaren yanıt verdiğinizde yanıtın başlangıcı neredeyse anında gelir. Önbelleğe alınmış üstbilgi ve altbilgi işaretlemesini ağdan gelen bir yanıtla birleştirdiğinizde önemli performans avantajları elde edersiniz:

  • İlk Bayt'a Kadar Geçen Süre (TTFB), gezinme isteğine verilen yanıtın ilk baytı anında olduğu için genellikle büyük ölçüde azalır.
  • İlk Zengin İçerikli Boyama (FCP), önceden önbelleğe alınmış başlık işaretlemesi önbelleğe alınmış bir stil sayfasına başvuru içereceğinden çok hızlı olacaktır. Bu, sayfanın çok hızlı bir şekilde boyanacağı anlamına gelir.
  • Bazı durumlarda, özellikle en büyük ekrandaki öğe, önbelleğe alınmış üstbilgi kısmı tarafından sağlanıyorsa Largest Contentful Paint (LCP) özelliği de daha hızlı olabilir. Buna rağmen, hizmet çalışanı önbelleğinden bir şeyi en kısa sürede daha küçük işaretleme yükleriyle birlikte yayınlamak daha iyi bir LCP elde edilmesini sağlayabilir.

Çok sayfalı mimarileri yayınlamak ve yinelemek biraz zor olabilir. Ancak gereken karmaşıklık genellikle teorideki SPA'lardan daha zahmetli değildir. En büyük faydası, tarayıcının varsayılan gezinme şemasını değiştirmek yerine iyileştirmeniz olur.

Daha da iyisi, Workbox bu mimariyi sadece mümkün değildir, aynı zamanda kendi başınıza uygulamaya kıyasla daha kolay hale getirir. Özelliği kendi web sitenizde deneyin ve alandaki kullanıcılar için çok sayfalı web sitenizin ne kadar daha hızlı olabileceğini görün.

Kaynaklar