توسعه فرانت‌اند

Svelte: فریمورکی که کامپایل می‌شود

نقش واقعی compiler و runtime در Svelte، تفاوت SvelteKit، runes و نمونه کامل Vite با state، derived، binding و CSS محلی.

1404/11/26 3 دقیقه مطالعه 1,647 بازدید

کامپایل شدن چه چیزی را عوض می‌کند؟

در یک component ساده، تغییر تعداد کالا باید عدد جمع را در صفحه عوض کند. Svelte بخش زیادی از ارتباط میان کد component و به‌روزرسانی رابط را هنگام build آماده می‌کند. فایل svelte مستقیماً فایل قابل اجرای مرورگر نیست؛ compiler آن را همراه ابزار ساخت به JavaScript و CSS مناسب تبدیل می‌کند. این رویکرد با نوشتن همهٔ عملیات DOM به صورت دستی تفاوت دارد. نویسنده همچنان state، رویداد و ساختار رابط را تعریف می‌کند و ابزار، بخشی از کد اجرایی لازم را تولید می‌کند.

تعبیر بدون runtime در معرفی‌های کوتاه گمراه‌کننده است. Svelte کد زمان اجرا هم دارد و واکنش‌پذیری، رویدادها و چرخهٔ component فقط با کار زمان build انجام نمی‌شوند. همچنین حذف نیاز به Virtual DOM در این مدل، به تنهایی اثبات سریع‌تر بودن هر برنامه نیست. حجم داده، تعداد عناصر و کار انجام‌شده در event handler همچنان مهم‌اند. معرفی رسمی Svelte آن را به عنوان framework رابط با compiler توضیح می‌دهد؛ مقایسهٔ سرعت باید به برنامه و روش اندازه‌گیری مشخص وابسته باشد.

Svelte و SvelteKit مسئولیت یکسان ندارند

Svelte برای نوشتن component و رابط استفاده می‌شود. SvelteKit امکانات ساخت برنامه مانند routing، بارگذاری داده و راه‌های rendering و استقرار را سازمان می‌دهد. برای تمرین یک component می‌توان Svelte را با Vite استفاده کرد. برای سایتی که مسیرهای متعدد، پاسخ سرور و مدیریت داده دارد، انتخاب محیط برنامه بحث جداگانه‌ای است. داشتن compiler به معنی داشتن همهٔ امکانات مورد نیاز یک سایت کامل نیست. معرفی SvelteKit این تقسیم مسئولیت را روشن می‌کند.

اگر قبلاً React نوشته‌اید، بهتر است مفهوم state و مالکیت داده را با خود بیاورید و شکل دقیق API را تازه یاد بگیرید. تبدیل مکانیکی هر hook به یک نام دیگر، رفتار درست را تضمین نمی‌کند. همین نکته دربارهٔ مهاجرت از نسخه‌های قبلی Svelte هم صادق است. کد آموزشی این نوشته از مدل runes در Svelte 5 استفاده می‌کند. پروژهٔ قدیمی ممکن است syntax legacy داشته باشد؛ ترکیب نمونه‌های متفاوت بدون شناخت مدل هر component، فهم تغییر داده را سخت می‌کند.

State و مقدار مشتق‌شده

Rune با نام $state یک مقدار واکنش‌پذیر تعریف می‌کند. در نمونهٔ سبد، تعداد و قیمت واحد ورودی‌اند و جمع از آن‌ها محاسبه می‌شود. برای جمع، $derived به کار می‌رود؛ نیازی نیست هر handler علاوه بر تغییر تعداد، جمع را هم دستی عوض کند. این کار جلوی دو منبع ناسازگار برای یک نتیجه را می‌گیرد. مستندات $state دربارهٔ رفتار مقدارهای ساده و objectهای قابل proxy توضیح داده است.

هر داده‌ای با یک کپی معمولی از متغیر واکنش‌پذیر، رابطهٔ زندهٔ خود را نگه نمی‌دارد. مثلاً گرفتن مقدار فعلی در یک متغیر عادی می‌تواند یک snapshot از همان لحظه باشد. همچنین ساختارهای خاص مثل classها، Date و مجموعه‌های مختلف قراردادهای جدا دارند؛ نباید همهٔ objectها را یکسان فرض کرد. برای شروع، state ساده و محدودهٔ محلی قابل فهم‌تر است. زمانی که داده میان componentها مشترک شد، نحوهٔ انتقال props یا store باید بر اساس عمر واقعی داده طراحی شود.

$derived برای محاسبهٔ وابسته به داده است و بهتر است محاسبهٔ آن side effect نداشته باشد. درخواست شبکه یا نوشتن در storage، جمع زدن قیمت نیست. وابستگی‌ها بر اساس خواندن مقدارهای واکنش‌پذیر در محاسبه تعیین می‌شوند. راهنمای $derived شکل عبارت و محاسبهٔ چندخطی را توضیح می‌دهد. در نمونهٔ فعلی یک ضرب ساده داریم و لازم نیست برای نشان دادن APIهای بیشتر، منطق مصنوعی اضافه کنیم.

نمونهٔ کامل با تعداد و قیمت انتخابی

در پوشهٔ خالی، package.json، vite.config.js و index.html را در ریشه بگذارید. پوشهٔ src شامل main.js و App.svelte است. Node 24 و npm پیش‌نیاز این مجموعه‌اند. فایل‌های ارائه‌شده را با نام‌های مقصد ذخیره کنید؛ پسوند svelte باید حفظ شود. سپس npm install و npm run dev را اجرا کنید. نشانی نمایش را Vite اعلام می‌کند. دستور npm run build خروجی می‌سازد و npm run preview آن خروجی را محلی نمایش می‌دهد. فایل lock واقعی حاصل نصب را کنار package نگه دارید.

{"name":"svelte-basket","version":"1.0.0","private":true,"type":"module","scripts":{"dev":"vite","build":"vite build","preview":"vite preview"},"devDependencies":{"svelte":"^5.0.0","vite":"^8.0.0","@sveltejs/vite-plugin-svelte":"^7.0.0"}}
import { defineConfig } from 'vite';
import { svelte } from '@sveltejs/vite-plugin-svelte';
export default defineConfig({ plugins: [svelte()] });
<!doctype html><html lang="fa" dir="rtl"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>سبد نمونه</title></head><body><div id="app"></div><script type="module" src="/src/main.js"></script></body></html>
import { mount } from 'svelte';
import App from './App.svelte';
mount(App, { target: document.getElementById('app') });
<script>
  let quantity = $state(1);
  let unitPrice = $state(120000);
  let total = $derived(quantity * unitPrice);
  const format = (value) => value.toLocaleString('fa-IR');
</script>

<main>
  <h1>سبد آموزشی</h1>
  <p>این نمونه پرداخت یا اتصال به فروشگاه ندارد.</p>
  <label for="quantity">تعداد</label>
  <select id="quantity" bind:value={quantity}>
    {#each [1, 2, 3, 4, 5] as amount}<option value={amount}>{format(amount)}</option>{/each}
  </select>
  <label for="price">قیمت واحد</label>
  <select id="price" bind:value={unitPrice}>
    <option value={120000}>۱۲۰٬۰۰۰ تومان</option>
    <option value={180000}>۱۸۰٬۰۰۰ تومان</option>
  </select>
  <p role="status">جمع: {format(total)} تومان</p>
  <button type="button" onclick={() => { quantity = 1; unitPrice = 120000; }}>بازنشانی</button>
</main>

<style>
  :global(body) { margin: 0; padding: 2rem 1rem; background: #eef3f4; color: #1d333b; font-family: system-ui,sans-serif; }
  main { max-width: 36rem; margin-inline: auto; padding: 1.5rem; border-radius: 1rem; background: white; display: grid; gap: .8rem; }
  h1,p { margin: 0; }
  select,button { font: inherit; padding: .7rem; border: 1px solid #687f87; border-radius: .4rem; }
  button { background: #075c55; color: white; cursor: pointer; }
  :focus-visible { outline: 3px solid #bd6800; outline-offset: 3px; }
</style>

قیمت‌ها دادهٔ فرضی آموزش‌اند و به فروشگاه یا نتیجهٔ مشتری واقعی مربوط نیستند. select مقدارهای عددی مشخص دارد؛ بنابراین تعداد منفی و متن نامعتبر در این رابط وارد نمی‌شود. bind:value انتخاب کاربر را با state مرتبط می‌کند و دکمهٔ بازنشانی هر دو ورودی را به مقدار اولیه برمی‌گرداند. مقدار جمع از state مشتق می‌شود. از HTML استاندارد label و select استفاده شده تا ارتباط کنترل و نامش روشن باشد. CSS component هم همراه همان markup ارائه شده و selector پنهان ندارد.

در فایل main، mount برنامه را داخل عنصر app قرار می‌دهد. این API با الگوی سازندهٔ component در آموزش‌های قدیمی فرق دارد. import فایل App با نام درست و حساس به حروف نوشته شده است. کد نمونه API، secret و تصویر ندارد و با reload مقدارهای اولیه دوباره ساخته می‌شوند. روش اجرای آن توضیح داده شده، اما این مقاله ادعای اجرای آزمایش در دستگاه خواننده نمی‌کند. اگر پروژه را با template دیگری شروع کردید، تنظیم compiler و ساختار آن template را با این فایل‌ها آگاهانه تطبیق دهید.

رویدادها و markup را همان‌جا بخوانید

در syntax فعلی از onclick برای handler دکمه استفاده کرده‌ایم. handler کوتاه دو مقدار را تغییر می‌دهد و کار دیگری نمی‌کند. اگر منطق بازنشانی بزرگ شد، تابع نام‌دار خواناتر است. وجود template با دستور each، مجوز حذف key در فهرست‌های قابل جابه‌جایی نیست؛ در این مثال گزینه‌ها ثابت و ساده‌اند. فهرست دادهٔ قابل حذف و مرتب‌سازی به هویت پایدار آیتم نیاز دارد. این تفاوت در نمونه‌های کوچک دیده نمی‌شود و هنگام تبدیل تمرین به محصول اهمیت پیدا می‌کند.

CSS محلی component به محدود کردن اثر style کمک می‌کند، ولی قواعد global همچنان ممکن‌اند. در نمونه فقط body با :global تنظیم شده تا پس‌زمینه و فاصلهٔ صفحه تعریف شود. با زیاد شدن componentها، رنگ و فاصلهٔ مشترک را می‌توان در قرارداد طراحی یا Custom Properties نگه داشت. مقالهٔ متغیرهای CSS و Scope برای همین مرحله مفید است. Compiler جای تصمیم دربارهٔ style مشترک و مسئولیت هر component را نمی‌گیرد.

Effect را برای کار بیرونی نگه دارید

اگر لازم است یک سامانهٔ بیرونی با state هماهنگ شود، $effect یکی از ابزارهای موجود است. تبدیل هر محاسبهٔ ساده به effect باعث می‌شود مسیر تغییر داده غیرمستقیم شود. برای جمع سبد، derived کافی است؛ effect برای کپی کردن آن در state دیگری، یک مرحلهٔ اضافی می‌سازد. Effect در مرورگر اجرا می‌شود و هنگام بازاجرا یا حذف ممکن است cleanup لازم داشته باشد. مستندات $effect محدودیت‌ها و زمان اجرای آن را توضیح می‌دهد.

Timer، subscription یا observer باید هنگام تمام شدن عمر صاحبش آزاد شود. این کار را با یک handler کلی و بدون شناخت عمر component نمی‌توان حل کرد. راهنمای lifecycle دربارهٔ onMount و onDestroy توضیح دارد. اگر callback ایجاد منبع غیرهم‌زمان است، مراقب باشید تابع cleanup را چگونه برمی‌گردانید. مقالهٔ Intersection Observer نمونه‌ای از منبعی است که پایان عمر آن باید مشخص باشد.

امنیت و دادهٔ سرور بیرون از سادگی syntax می‌مانند

قیمت واقعی سفارش باید در سرور معتبر تعیین شود. تغییر دادن مقدار در مرورگر، مبلغ قابل اعتماد برای پرداخت ایجاد نمی‌کند. اگر مثال را به فروشگاه وصل کردید، وضعیت loading، خطا و نتیجهٔ درخواست هم لازم می‌شود. پاسخ قدیمی می‌تواند پس از انتخاب تازه برسد؛ این مسئله را در Promise و async/await بررسی کرده‌ایم. واکنش‌پذیری رابط به معنی کنترل خودکار ترتیب پاسخ‌های شبکه نیست.

انتخاب framework را به نیاز تیم وصل کنید

برای بررسی Svelte، یک بخش واقعی اما محدود از پروژه را انتخاب کنید: فرم با اعتبارسنجی، فهرست قابل تغییر یا صفحه‌ای با دادهٔ سرور. شیوهٔ نگهداری، ابزار build و مهارت اعضای تیم را کنار خوانایی component بسنجید. آسان بودن یک شمارنده، هزینهٔ مهاجرت یک برنامهٔ کامل را تعیین نمی‌کند. این رویکرد کامپایل‌شونده امکان نوشتن رابط با syntax مختصر را فراهم می‌کند؛ مسئولیت داده، دسترس‌پذیری و مسیر انتشار همچنان با تیم پروژه است.

واکنش به این مقاله

اشتراک‌گذاری این مقاله:
دیدگاه‌ها 0
۰ / ۲۰۰۰

هنوز دیدگاهی ثبت نشده

اولین نفری باشید که نظر می‌دهد!