Zephyrnet Logosu

SvelteKit'te Verileri Önbelleğe Alma

Tarih:

My önceki yazı SvelteKit'in web geliştirme için ne kadar harika bir araç olduğunu gördüğümüz geniş bir genel bakıştı. Bu gönderi, orada yaptığımız şeyi çatallayacak ve her geliştiricinin favori konusuna dalacak: önbelleğe alma. Bu nedenle, henüz okumadıysanız son yazımı okumayı unutmayın. Bu yazının kodu GitHub'da mevcut, Hem de canlı demo.

Bu gönderi tamamen veri işleme ile ilgili. Sayfanın sorgu dizesini değiştirecek (yerleşik SvelteKit özelliklerini kullanarak) ve sayfanın yükleyicisini yeniden tetikleyecek bazı basit arama işlevleri ekleyeceğiz. Ancak, (hayali) veritabanımızı yeniden sorgulamak yerine, biraz önbelleğe alma ekleyeceğiz, böylece önceki aramaları yeniden aramak (veya geri düğmesini kullanmak), daha önce önbellekten alınan verileri hızlı bir şekilde gösterecektir. Önbelleğe alınan verilerin geçerli kalma süresinin nasıl kontrol edileceğine ve daha da önemlisi önbelleğe alınan tüm değerlerin manuel olarak nasıl geçersiz kılınacağına bakacağız. Pastanın üzerindeki krema olarak, bir mutasyondan sonra önbelleği temizlemeye devam ederken mevcut ekrandaki istemci tarafındaki verileri manuel olarak nasıl güncelleyebileceğimize bakacağız.

Daha zor konuları ele aldığımız için bu, genellikle yazdığım yazıların çoğundan daha uzun ve daha zor bir gönderi olacak. Bu gönderi, temel olarak, popüler veri yardımcı programlarının ortak özelliklerini nasıl uygulayacağınızı gösterecektir. tepki sorgusu; ancak harici bir kitaplık çekmek yerine yalnızca web platformunu ve SvelteKit özelliklerini kullanıyor olacağız.

Ne yazık ki, web platformunun özellikleri biraz daha düşük seviyede, bu yüzden alışık olabileceğinizden biraz daha fazla iş yapacağız. Artı tarafı, herhangi bir harici kitaplığa ihtiyacımız olmayacak, bu da paket boyutlarını hoş ve küçük tutmaya yardımcı olacaktır. Lütfen iyi bir nedeniniz olmadıkça size göstereceğim yaklaşımları kullanmayın. Önbelleğe almayı yanlış yapmak kolaydır ve göreceğiniz gibi, uygulama kodunuzla sonuçlanacak biraz karmaşıklık vardır. Umarız veri deponuz hızlıdır ve kullanıcı arabiriminiz SvelteKit'in herhangi bir sayfa için ihtiyaç duyduğu verileri her zaman talep etmesine izin verecek kadar iyidir. Eğer öyleyse, kendi haline bırakın. Sadeliğin tadını çıkarın. Ancak bu gönderi, bunun sona erdiği zamanlar için size bazı püf noktaları gösterecek.

Tepki sorgusundan bahsetmişken, yeni serbest bırakıldı Svelte için! Dolayısıyla, kendinizi manuel önbelleğe alma tekniklerine yaslanmış bulursanız çok, o projeyi kontrol ettiğinizden emin olun ve yardımcı olup olmayacağına bakın.

Kurma

Başlamadan önce, birkaç küçük değişiklik yapalım. daha önce sahip olduğumuz kod. Bu bize diğer bazı SvelteKit özelliklerini görmemiz ve daha da önemlisi bizi başarıya hazırlamamız için bir bahane verecektir.

İlk olarak, yükleyicimizden veri yüklememizi şuraya taşıyalım: +page.server.js Bir için API yolu. oluşturacağız +server.js dosya routes/api/todosve ardından bir GET işlev. Bu, artık (varsayılan GET fiilini kullanarak) /api/todos yol. Daha önce olduğu gibi aynı veri yükleme kodunu ekleyeceğiz.

import { json } from "@sveltejs/kit";
import { getTodos } from "$lib/data/todoData"; export async function GET({ url, setHeaders, request }) { const search = url.searchParams.get("search") || ""; const todos = await getTodos(search); return json(todos);
}

Ardından, sahip olduğumuz sayfa yükleyiciyi alalım ve dosyayı şu adresten yeniden adlandıralım: +page.server.js için +page.js (Ya da .ts projenizi TypeScript kullanmak için iskele kurduysanız). Bu, yükleyicimizi bir sunucu yükleyicisi yerine "evrensel" bir yükleyici olarak değiştirir. SvelteKit belgeleri farkı açıklamak, ancak evrensel bir yükleyici hem sunucuda hem de istemcide çalışır. Bizim için bir avantaj, fetch yeni uç noktamıza yapılan çağrı, tarayıcının yerel özelliğini kullanarak doğrudan tarayıcımızdan (ilk yüklemeden sonra) çalışır. fetch işlev. Birazdan standart HTTP önbelleğe almayı ekleyeceğiz, ancak şimdilik tek yapacağımız uç noktayı çağırmak.

export async function load({ fetch, url, setHeaders }) { const search = url.searchParams.get("search") || ""; const resp = await fetch(`/api/todos?search=${encodeURIComponent(search)}`); const todos = await resp.json(); return { todos, };
}

Şimdi dosyamıza basit bir form ekleyelim. /list sayfa:

<div class="search-form"> <form action="/tr/list"> <label>Search</label> <input autofocus name="search" /> </form>
</div>

Evet, formlar doğrudan normal sayfa yükleyicilerimizi hedefleyebilir. Artık arama kutusuna bir arama terimi ekleyebiliriz, Keşfet, ve URL'nin sorgu dizesine bir "arama" terimi eklenir, bu da yükleyicimizi yeniden çalıştırır ve yapılacak iş öğelerimizi arar.

Arama formu

Gecikmeyi de arttıralım. todoData.js dosya /lib/data. Bu, biz bu gönderi üzerinde çalışırken verilerin ne zaman önbelleğe alınıp alınmadığını görmeyi kolaylaştıracaktır.

export const wait = async amount => new Promise(res => setTimeout(res, amount ?? 500));

Unutmayın, bu gönderi için tam kod hepsi GitHub'da, başvurmanız gerekirse.

Temel önbelleğe alma

Dosyamıza biraz önbellek ekleyerek başlayalım. /api/todos uç nokta. bizimkine geri döneceğiz +server.js dosya ve ilk önbellek kontrol başlığımızı ekleyin.

setHeaders({ "cache-control": "max-age=60",
});

… bu da tüm işlevi şu şekilde bırakacaktır:

export async function GET({ url, setHeaders, request }) { const search = url.searchParams.get("search") || ""; setHeaders({ "cache-control": "max-age=60", }); const todos = await getTodos(search); return json(todos);
}

Manuel geçersiz kılmaya kısaca bakacağız, ancak bu işlevin söylediği tek şey, bu API çağrılarını 60 saniye boyunca önbelleğe almak. Bunu istediğiniz gibi ayarlayınve kullanım durumunuza bağlı olarak, stale-while-revalidate da incelemeye değer olabilir.

Ve aynen böyle, sorgularımız önbelleğe alınıyor.

DevTools'ta önbellek.

not emin olun işareti kaldırmak geliştirme araçlarında önbelleğe almayı devre dışı bırakan onay kutusu.

Uygulamadaki ilk gezintiniz liste sayfasıysa, bu arama sonuçlarının dahili olarak SvelteKit'te önbelleğe alınacağını unutmayın, bu nedenle o aramaya dönerken DevTools'ta herhangi bir şey görmeyi beklemeyin.

Ne önbelleğe alınır ve nerede

Uygulamamızın sunucu tarafından oluşturulan ilk yüklemesi (en baştan başladığımızı varsayarsak) /list sayfa) sunucuda alınacaktır. SvelteKit bu verileri seri hale getirecek ve müşterimize gönderecektir. Dahası, gözlemleyecek Cache-Control başlık ve bu önbelleğe alınmış verileri önbellek penceresinde (put örneğinde 60 saniyeye ayarladık) bu uç nokta çağrısı için kullanmayı bilecek.

Bu ilk yüklemeden sonra, sayfada arama yapmaya başladığınızda, tarayıcınızdan ağ isteklerini görmelisiniz. /api/todos liste. Halihazırda aradığınız şeyleri ararken (son 60 saniye içinde), önbelleğe alındıklarından yanıtlar hemen yüklenmelidir.

Bu yaklaşımın özellikle harika yanı, bu, tarayıcının yerel önbelleği aracılığıyla önbelleğe alma olduğundan, bu aramaların (bakacağımız önbellek bozmayı nasıl yönettiğinize bağlı olarak) sayfayı yeniden yükleseniz bile önbelleğe alınmaya devam edebilmesidir (bunun aksine) son 60 saniye içinde yapmış olsa bile uç noktayı her zaman taze olarak çağıran ilk sunucu tarafı yükü).

Açıktır ki veriler her an değişebilir, bu yüzden bu önbelleği manuel olarak temizlemenin bir yoluna ihtiyacımız var, buna daha sonra bakacağız.

önbellek geçersiz kılma

Şu anda, veriler 60 saniye boyunca önbelleğe alınacak. Ne olursa olsun, bir dakika sonra veri depomuzdan yeni veriler çekilecek. Daha kısa veya daha uzun bir süre isteyebilirsiniz, ancak bazı verileri değiştirirseniz ve bir sonraki sorgunuzun güncel olması için önbelleğinizi hemen temizlemek isterseniz ne olur? Bunu, yeni adresimize gönderdiğimiz URL'ye sorgu bozan bir değer ekleyerek çözeceğiz. /todos uç nokta.

Bu önbellek bozma değerini bir çerezde saklayalım. Bu değer sunucuda ayarlanabilir ancak yine de istemcide okunabilir. Bazı örnek kodlara bakalım.

biz bir +layout.server.js dosyamızın en kökünde routes dosya. Bu, uygulama başlangıcında çalışır ve ilk çerez değerini ayarlamak için mükemmel bir yerdir.

export function load({ cookies, isDataRequest }) { const initialRequest = !isDataRequest; const cacheValue = initialRequest ? +new Date() : cookies.get("todos-cache"); if (initialRequest) { cookies.set("todos-cache", cacheValue, { path: "/", httpOnly: false }); } return { todosCacheBust: cacheValue, };
}

Fark etmiş olabilirsiniz isDataRequest değer. Unutmayın, düzenler her müşteri kodu çağrısında yeniden çalışır invalidate()veya herhangi bir sunucu eylemi çalıştırdığımızda (varsayılan davranışı kapatmadığımız varsayılarak). isDataRequest bu yeniden çalıştırmaları gösterir ve bu nedenle tanımlama bilgisini yalnızca şu durumlarda ayarlarız: false; Aksi takdirde, zaten orada olanı göndeririz.

The httpOnly: false bayrak da önemlidir. Bu, müşteri kodumuzun şu çerez değerlerini okumasına izin verir: document.cookie. Bu normalde bir güvenlik endişesi olurdu, ancak bizim durumumuzda bunlar önbelleğe almamıza veya önbelleğe almamıza izin veren anlamsız sayılardır.

Önbellek değerlerini okuma

Üniversal yükleyicimiz, bizim /todos uç nokta. Bu, sunucuda veya istemcide çalışır ve nerede olursak olalım, az önce kurduğumuz önbellek değerini okumamız gerekir. Sunucudaysak nispeten kolaydır: arayabiliriz await parent() üst düzenlerden veri almak için. Ancak istemcide ayrıştırmak için bazı brüt kodlar kullanmamız gerekecek. document.cookie:

export function getCookieLookup() { if (typeof document !== "object") { return {}; } return document.cookie.split("; ").reduce((lookup, v) => { const parts = v.split("="); lookup[parts[0]] = parts[1]; return lookup; }, {});
} const getCurrentCookieValue = name => { const cookies = getCookieLookup(); return cookies[name] ?? "";
};

Neyse ki, sadece bir kez ihtiyacımız var.

Önbellek değerini gönderme

Ama şimdi ihtiyacımız var göndermek bu değer bize /todos uç nokta.

import { getCurrentCookieValue } from "$lib/util/cookieUtils"; export async function load({ fetch, parent, url, setHeaders }) { const parentData = await parent(); const cacheBust = getCurrentCookieValue("todos-cache") || parentData.todosCacheBust; const search = url.searchParams.get("search") || ""; const resp = await fetch(`/api/todos?search=${encodeURIComponent(search)}&cache=${cacheBust}`); const todos = await resp.json(); return { todos, };
}

getCurrentCookieValue('todos-cache') istemcide olup olmadığımızı görmek için (belgenin türünü kontrol ederek) bir check-in yapar ve öyleysek hiçbir şey döndürmez, bu noktada sunucuda olduğumuzu biliriz. Ardından, düzenimizdeki değeri kullanır.

önbelleği bozma

Fakat Nasıl gerçekten ihtiyacımız olduğunda bu önbellek bozma değerini güncelliyor muyuz? Bir çerezde saklandığından, onu herhangi bir sunucu eyleminden şu şekilde çağırabiliriz:

cookies.set("todos-cache", cacheValue, { path: "/", httpOnly: false });

Hayata geçirme

Bundan sonra hep inişli; zor işi yaptık. İhtiyacımız olan çeşitli web platformu ilkellerini ve nereye gittiklerini ele aldık. Şimdi biraz eğlenelim ve hepsini birbirine bağlamak için uygulama kodu yazalım.

Birazdan netleşecek nedenlerden dolayı, dosyamıza bir düzenleme işlevi ekleyerek başlayalım. /list sayfa. Her yapılacak iş için bu ikinci tablo satırını ekleyeceğiz:

import { enhance } from "$app/forms";
<tr> <td colspan="4"> <form use:enhance method="post" action="?/editTodo"> <input name="id" value="{t.id}" type="hidden" /> <input name="title" value="{t.title}" /> <button>Save</button> </form> </td>
</tr>

Ve tabii ki, bizim için bir form eylemi eklememiz gerekecek. /list sayfa. Eylemler yalnızca girebilir .server sayfalar, bu yüzden bir ekleyeceğiz +page.server.js bizimkinde /list dosya. (Evet A +page.server.js dosya yanında bir arada bulunabilir +page.js dosya.)

import { getTodo, updateTodo, wait } from "$lib/data/todoData"; export const actions = { async editTodo({ request, cookies }) { const formData = await request.formData(); const id = formData.get("id"); const newTitle = formData.get("title"); await wait(250); updateTodo(id, newTitle); cookies.set("todos-cache", +new Date(), { path: "/", httpOnly: false }); },
};

Form verilerini alıyoruz, gecikmeye zorluyoruz, yapılacak işlerimizi güncelliyoruz ve en önemlisi önbellek bozma çerezimizi temizliyoruz.

Bir şans verelim. Sayfanızı yeniden yükleyin, ardından yapılacak işlerden birini düzenleyin. Bir süre sonra tablo değeri güncellemesini görmelisiniz. DevToold'daki Ağ sekmesine bakarsanız, /todos yeni verilerinizi döndüren uç nokta. Basit ve varsayılan olarak çalışır.

Veri kaydetme

Anında güncellemeler

Yapılacaklar öğemizi güncelledikten sonra gerçekleşen getirme olayını önlemek ve bunun yerine değiştirilen öğeyi doğrudan ekranda güncellemek istersek ne olur?

Bu sadece bir performans meselesi değil. "Gönderi" için arama yapar ve ardından listedeki herhangi bir yapılacak iş öğesinden "gönderi" kelimesini kaldırırsanız, artık o sayfanın arama sonuçlarında olmadıkları için düzenlemeden sonra listeden kaybolurlar. Yapılacak işler için zevkli bir animasyonla kullanıcı deneyimini daha iyi hale getirebilirsiniz, ancak diyelim ki yapmak istedik. değil o sayfanın yükleme işlevini yeniden çalıştırın, ancak yine de önbelleği temizleyin ve kullanıcının düzenlemeyi görebilmesi için değiştirilen yapılacak işi güncelleyin. SvelteKit bunu mümkün kılıyor — bakalım nasıl olacak!

Öncelikle yükleyicimizde küçük bir değişiklik yapalım. Yapılacak işlerimizi iade etmek yerine, bir yazılabilir mağaza yapılacaklarımızı içeren.

return { todos: writable(todos),
};

Daha önce, yapılacak işlerimize internet üzerinden erişiyorduk. data sahip olmadığımız ve güncelleyemeyeceğimiz prop. Ancak Svelte, verilerimizi kendi mağazalarında iade etmemize izin veriyor (evrensel bir yükleyici kullandığımızı varsayarsak, ki öyleyiz). Sadece bir ince ayar daha yapmamız gerekiyor /list gidin.

Bunun yerine:

{#each todos as t}

…çünkü bunu yapmamız gerekiyor todos kendisi artık bir mağaza.:

{#each $todos as t}

Artık verilerimiz eskisi gibi yükleniyor. Ama beri todos yazılabilir bir mağazadır, onu güncelleyebiliriz.

Öncelikle dosyamıza bir fonksiyon tanımlayalım. use:enhance özellik:

<form use:enhance={executeSave} on:submit={runInvalidate} method="post" action="?/editTodo"
>

Bu, bir göndermeden önce çalışır. Devamını yazalım:

function executeSave({ data }) { const id = data.get("id"); const title = data.get("title"); return async () => { todos.update(list => list.map(todo => { if (todo.id == id) { return Object.assign({}, todo, { title }); } else { return todo; } }) ); };
}

Bu fonksiyon bir data form verilerimizle nesne. Biz dönüş çalışacak bir zaman uyumsuz işlev sonra düzenlememiz tamamlandı. dokümanlar bunların hepsini açıklayın, ancak bunu yaparak SvelteKit'in yükleyicimizi yeniden çalıştıracak olan varsayılan form işlemesini kapattık. Tam olarak istediğimiz bu! (Dokümanların açıkladığı gibi, bu varsayılan davranışı kolayca geri alabiliriz.)

şimdi aradık update üzerinde bizim todos bir mağaza olduğu için dizi. İşte bu kadar. Bir yapılacak iş öğesini düzenledikten sonra, değişikliklerimiz hemen görünür ve önbelleğimiz temizlenir (önbelleğimizde yeni bir çerez değeri belirlediğimiz için daha önce olduğu gibi). editTodo eylem formu). Bu nedenle, arama yapıp bu sayfaya geri dönersek, yükleyicimizden yeni veriler alırız ve bu veriler, güncellenmiş yapılacak iş öğelerini doğru şekilde hariç tutar.

Anında güncellemeler için kod GitHub'da mevcuttur.

Daha derin kazmak

Tanımlama bilgilerini yalnızca kök düzeninde değil, herhangi bir sunucu yükleme işlevinde (veya sunucu eyleminde) ayarlayabiliriz. Dolayısıyla, bazı veriler yalnızca tek bir düzende veya hatta tek bir sayfada kullanılıyorsa, o çerez değerini orada ayarlayabilirsiniz. Ayrıca, eğer değil az önce gösterdiğim numarayı yaparak ekrandaki verileri manuel olarak güncelliyor ve bunun yerine yükleyicinizin bir mutasyondan sonra yeniden çalışmasını istiyorsanız, o zaman herhangi bir kontrol yapmadan doğrudan bu yükleme işlevinde her zaman yeni bir çerez değeri ayarlayabilirsiniz. isDataRequest. Başlangıçta ayarlanır ve ardından bir sunucu eylemi çalıştırdığınızda, bu sayfa düzeni otomatik olarak yükleyicinizi geçersiz kılar ve evrensel yükleyiciniz çağrılmadan önce önbellek bozma dizesini yeniden ayarlayarak yükleyicinizi yeniden çağırır.

Yeniden yükleme işlevi yazma

Son bir özellik oluşturarak toparlayalım: yeniden yükle düğmesi. Kullanıcılara önbelleği temizleyecek ve ardından mevcut sorguyu yeniden yükleyecek bir düğme verelim.

Çok basit bir form eylemi ekleyeceğiz:

async reloadTodos({ cookies }) { cookies.set('todos-cache', +new Date(), { path: '/', httpOnly: false });
},

Gerçek bir projede, aynı tanımlama bilgisini birden çok yerde aynı şekilde ayarlamak için muhtemelen aynı kodu kopyalayıp yapıştırmazsınız, ancak bu gönderi için basitlik ve okunabilirlik için optimize edeceğiz.

Şimdi göndermek için bir form oluşturalım:

<form method="POST" action="?/reloadTodos" use:enhance> <button>Reload todos</button>
</form>

Bu işe yarıyor!

Yeniden yükledikten sonra kullanıcı arayüzü.

Buna bitti diyebilir ve devam edebiliriz, ancak bu çözümü biraz geliştirelim. Özellikle, kullanıcıya yeniden yüklemenin gerçekleştiğini bildirmek için sayfada geri bildirimde bulunalım. Ayrıca, varsayılan olarak SvelteKit işlemleri geçersiz kılınır her şey. Geçerli sayfanın hiyerarşisindeki her düzen, sayfa vb. yeniden yüklenir. Kök düzende bir kez yüklenen ve geçersiz kılmamız veya yeniden yüklememiz gerekmeyen bazı veriler olabilir.

O halde, işleri biraz odaklayalım ve bu işlevi çağırdığımızda yalnızca yapılacak işlerimizi yeniden yükleyelim.

İlk olarak, geliştirmek için bir işlev iletelim:

<form method="POST" action="?/reloadTodos" use:enhance={reloadTodos}>
import { enhance } from "$app/forms";
import { invalidate } from "$app/navigation"; let reloading = false;
const reloadTodos = () => { reloading = true; return async () => { invalidate("reload:todos").then(() => { reloading = false; }); };
};

yeni ayarlıyoruz reloading değişken true at başlama bu eylemin Ve sonra, her şeyi geçersiz kılmanın varsayılan davranışını geçersiz kılmak için, bir async işlev. Bu işlev, sunucu eylemimiz bittiğinde çalışır (bu sadece yeni bir tanımlama bilgisi ayarlar).

Bu olmadan async işlev döndürülürse, SvelteKit her şeyi geçersiz kılar. Bu işlevi sağladığımız için hiçbir şeyi geçersiz kılmayacaktır, bu nedenle ona neyi yeniden yükleyeceğini söylemek bize kalmıştır. Bunu ile yapıyoruz invalidate işlev. değeri ile diyoruz reload:todos. Bu işlev, geçersiz kılma tamamlandığında çözülen bir söz döndürür; bu noktada reloading geri false.

Son olarak, yükleyicimizi bu yeni sürümle senkronize etmemiz gerekiyor. reload:todos geçersiz kılma değeri Bunu yükleyicimizde şu şekilde yapıyoruz: depends işlevi:

export async function load({ fetch, url, setHeaders, depends }) { depends('reload:todos'); // rest is the same

İşte bu kadar. depends ve invalidate inanılmaz kullanışlı işlevlerdir. Harika olan şu ki invalidate bizim yaptığımız gibi sağladığımız rastgele değerleri almaz. Ayrıca, SvelteKit'in izleyeceği ve bu URL'ye bağlı tüm yükleyicileri geçersiz kılacağı bir URL sağlayabiliriz. Bu amaçla, çağrıyı atlayıp atlayamayacağımızı merak ediyorsanız depends ve bizim geçersiz kılmak /api/todos tamamen uç nokta, yapabilirsiniz, ancak sağlamanız gerekir kesin dahil olmak üzere URL search terim (ve önbellek değerimiz). Böylece, geçerli arama için URL'yi bir araya getirebilir veya yol adını şu şekilde eşleştirebilirsiniz:

invalidate(url => url.pathname == "/api/todos");

Şahsen, kullanan çözümü buluyorum depends daha açık ve basit. Ama bakın dokümanlar elbette daha fazla bilgi için ve kendiniz karar verin.

Yeniden yükle düğmesini çalışırken görmek isterseniz, bunun kodu deponun bu dalı.

Ayrılık düşünceleri

Uzun bir yazı oldu ama umarım bunaltıcı değildir. SvelteKit'i kullanırken verileri önbelleğe almanın çeşitli yollarını araştırdık. Bunun çoğu, bilgisi SvelteKit'in ötesinde genel olarak web geliştirmede size yardımcı olacak doğru önbelleği ve çerez değerlerini eklemek için web platformu ilkellerini kullanma meselesiydi.

Ayrıca, bu kesinlikle her zaman gerek yok. Muhtemelen, bu tür gelişmiş özelliklere yalnızca aslında onlara ihtiyacım var. Veri deponuz verileri hızlı ve verimli bir şekilde sunuyorsa ve herhangi bir ölçeklendirme sorunuyla uğraşmıyorsanız, burada bahsettiğimiz şeyleri yaparken uygulama kodunuzu gereksiz karmaşıklıkla şişirmenin bir anlamı yoktur.

Her zaman olduğu gibi net, temiz, basit kod yazın ve gerektiğinde optimize edin. Bu gönderinin amacı, gerçekten ihtiyaç duyduğunuzda size bu optimizasyon araçlarını sağlamaktı. Umarım beğenmişsinizdir!

spot_img

En Son İstihbarat

spot_img

Bizimle sohbet

Merhaba! Size nasıl yardım edebilirim?