اخبار وب

Angular 19 و سیگنال‌های واکنش‌پذیر

راهنمای Signals در Angular 19 با نمونه کامل فیلتر مقاله، update و computed، تفاوت effect و مقدار مشتق و مرز ترکیب با RxJS.

1404/12/21 3 دقیقه مطالعه 1,017 بازدید
Angular 19 و سیگنال‌های واکنش‌پذیر

Signals در Angular راهی برای نگهداری مقدار و دنبال کردن وابستگی مصرف‌کنندگان آن است. مقدار یک signal را با فراخوانی آن می‌خوانید و با set یا update تغییر می‌دهید. computed از مقدارهای دیگر نتیجه می‌سازد. این مدل برای حالت فعلی رابط، مانند فیلتر فهرست یا انتخاب کاربر، خوانا است. اما Signals تمام مسئله‌های ناهمزمان را به یک مدل واحد تبدیل نمی‌کند و RxJS را از همه پروژه‌ها حذف نمی‌کند.

Angular 19 بخشی از APIهای مرتبط با Signals را پایدار کرد؛ مقاله قبلی این رویداد را بازنویسی کامل سیستم و جایگزینی عمومی BehaviorSubject معرفی می‌کرد. چنین تعمیمی دقیق نیست. راهنمای Signals نسخه 19 رفتار APIهای این خانواده را توضیح می‌دهد. اگر پروژه روی نسخه دیگری است، وضعیت API را در مستند همان نسخه بخوانید؛ نسخه 19 امروز موضوع آموزشی این مطلب است و توصیه خودکار برای شروع پروژه تازه نیست.

حالت اصلی و مقدار مشتق را جدا نگه دارید

فرض کنید صفحه، فهرست مقاله و عبارت جست‌وجو دارد. عبارت جست‌وجو حالت اصلی است، چون کاربر آن را تغییر می‌دهد. فهرست فیلترشده از آن و مجموعه مقاله‌ها به دست می‌آید. نگهداری نسخه دیگری از نتیجه فیلتر در یک متغیر قابل تغییر، مسئولیت هماهنگ کردن دو حالت را ایجاد می‌کند. computed کمک می‌کند رابطه نتیجه با ورودی روشن بماند.

همین قاعده برای جمع قیمت یا تعداد موارد انتخاب‌شده کاربرد دارد. اگر جمع از داده موجود به دست می‌آید، یک منبع حقیقت کافی است. هر event نباید جداگانه هم داده و هم جمع را اصلاح کند. وقتی چند مسیر تغییر وجود دارد، به‌روزرسانی فراموش‌شده می‌تواند رابط را ناسازگار کند. مقدار مشتق را بر اساس داده اصلی تعریف کنید تا این رابطه در کد دیده شود.

computed برای محاسبه نتیجه است و نباید جای انجام عملیات بیرونی قرار بگیرد. نوشتن در storage یا فرستادن درخواست، خروجی قابل محاسبه صرف نیست. جداسازی این کارها باعث می‌شود تغییر وابستگی‌ها رفتار پنهانی ایجاد نکند. تابعی که تنها مقدار را محاسبه می‌کند آسان‌تر قابل فهم است؛ خواننده از روی ورودی و خروجی آن می‌فهمد چه چیزی عوض می‌شود.

چرا فراخوانی signal در template مهم است؟

در template، signal مثل تابع خوانده می‌شود. Angular با این خواندن وابستگی را دنبال می‌کند. اگر نام signal را بدون فراخوانی به عنوان مقدار نمایش دهید، مقدار مورد انتظار را دریافت نمی‌کنید. این تفاوت کوچک در بازبینی نمونه‌ها مهم است. وجود type مناسب در فایل به تنهایی معنای خواندن template را روشن نمی‌کند؛ شکل استفاده باید با API هماهنگ باشد.

به‌روزرسانی آرایه را نیز روشن انجام دهید. دستکاری مستقیم یک شیء داخل مقدار فعلی ممکن است تغییر مورد انتظار را از مسیر مدیریت حالت خارج کند. در نمونه این مقاله، برای تغییر وضعیت ذخیره، آرایه تازه و شیء تازه مربوط به همان مقاله ساخته می‌شود. هدف پیچیده کردن کد نیست؛ مرز تغییر باید برای خواننده و سیستم قابل تشخیص باشد.

در component با OnPush، خواندن signal در template به پیگیری وابستگی کمک می‌کند. این رفتار را جایگزین همه قراردادهای Change Detection فرض نکنید. برنامه هنوز component، event و کارهای ناهمزمان دارد. اگر مشکل نمایش دارید، مسیر واقعی تغییر و مصرف مقدار را بررسی کنید. شعار «Signals سریع‌تر است» علت یک خطای خاص را توضیح نمی‌دهد.

نمونه کامل فهرست با فیلتر و ذخیره

نمونه زیر برای Angular 19 با Standalone Component نوشته شده است. با Node.js سازگار با همین نسخه و فرمان npx @angular/cli@19 new signals-demo --standalone --routing=false --style=css یک پروژه آموزشی بسازید. نسخه CLI را صریح نگه داشته‌ایم تا نام فایل و API با این راهنما هماهنگ باشد. برای پروژه تازه تجاری، نسخه پشتیبانی‌شده را از مستند رسمی انتخاب کنید.

در پروژه ساخته‌شده، محتوای src/app/app.component.ts را با فایل زیر جایگزین کنید. template و styles در همان component قرار دارند، پس فایل CSS یا HTML اضافه‌ای لازم نیست. سه مقاله ثابت، داده آموزشی هستند. نمونه به FormsModule، API بیرونی یا پایگاه داده نیاز ندارد و از event ورودی برای دریافت متن استفاده می‌کند.

import { ChangeDetectionStrategy, Component, computed, signal } from "@angular/core";

interface Article { id: number; title: string; saved: boolean; }

@Component({
  selector: "app-root",
  standalone: true,
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    <main class="library">
      <h1>فهرست آموزشی مقاله‌ها</h1>
      <label for="query">جست‌وجوی عنوان</label>
      <input id="query" type="search" [value]="query()" (input)="onInput($event)">
      <p>تعداد نتیجه: {{ filtered().length }}</p>
      <ul>
        @for (article of filtered(); track article.id) {
          <li>
            <span>{{ article.title }}</span>
            <button type="button" [attr.aria-pressed]="article.saved" (click)="toggleSaved(article.id)">
              {{ article.saved ? 'لغو ذخیره' : 'ذخیره' }}
            </button>
          </li>
        } @empty { <li>مقاله‌ای با این عبارت پیدا نشد.</li> }
      </ul>
    </main>
  `,
  styles: `
    .library { max-width: 44rem; margin: 2rem auto; padding: 1rem; font-family: sans-serif; line-height: 1.8; }
    input, button { font: inherit; padding: .5rem; }
    input { display: block; width: 100%; box-sizing: border-box; }
    ul { list-style: none; padding: 0; }
    li { display: flex; flex-wrap: wrap; align-items: center; gap: 1rem; padding-block: .8rem; border-bottom: 1px solid #aaa; }
    :focus-visible { outline: 3px solid #1262bc; outline-offset: 2px; }
  `,
})
export class AppComponent {
  readonly query = signal("");
  readonly articles = signal<Article[]>([
    { id: 1, title: "راهنمای HTML", saved: false },
    { id: 2, title: "یادگیری CSS", saved: false },
    { id: 3, title: "شروع Angular", saved: false },
  ]);
  readonly filtered = computed(() => {
    const query = this.query().trim().toLowerCase();
    return this.articles().filter((article) => article.title.toLowerCase().includes(query));
  });
  onInput(event: Event): void {
    const input = event.target;
    if (input instanceof HTMLInputElement) this.query.set(input.value);
  }
  toggleSaved(id: number): void {
    this.articles.update((articles) => articles.map((article) =>
      article.id === id ? { ...article, saved: !article.saved } : article));
  }
}

فایل src/main.ts باید همین component را bootstrap کند. اگر starter فایل تنظیم app.config.ts دارد، برای این نمونه ساده الزام به استفاده از آن نیست، چون router یا provider اختصاصی نداریم. ساختار bootstrap زیر entry کامل نمونه است. در پروژه واقعی، providerهای ضروری برنامه را بدون بررسی حذف نکنید؛ این کد برای پوشه آموزشی مستقل ارائه شده است.

import { bootstrapApplication } from "@angular/platform-browser";
import { AppComponent } from "./app/app.component";

bootstrapApplication(AppComponent).catch((error) => console.error(error));

در src/index.html تولیدشده، lang را به fa و dir را به rtl تغییر دهید و عنصر app-root را نگه دارید. برای اجرا وارد پوشه signals-demo شوید و npm start را اجرا کنید. دکمه ذخیره فقط وضعیت محلی همان صفحه را تغییر می‌دهد. با بازخوانی صفحه، داده دوباره از مقدار اولیه ساخته می‌شود؛ نمونه ادعایی درباره ذخیره دائمی یا حساب کاربر ندارد.

query و articles دو signal قابل تغییرند. filtered از هر دو می‌خواند و تعداد نتیجه از همان خروجی به دست می‌آید. toggleSaved فقط مقاله دارای ID مشخص را عوض می‌کند. در template، @for با track article.id هویت ردیف را مشخص می‌کند و @empty پیام نتیجه خالی را می‌دهد. راهنمای Control flow مرجع این syntax است.

دکمه aria-pressed دارد تا وضعیت روشن یا خاموش ذخیره را بیان کند. متن دکمه نیز تغییر می‌کند. این رفتار برای همین کنترل دوحالته طراحی شده است و تضمین دسترسی‌پذیری تمام صفحه نیست. ترتیب Focus و خوانایی متن باید با نیاز کاربر هماهنگ شود. مقاله دسترسی‌پذیری از روز اول به این تصمیم‌های رابط مربوط است.

effect را برای هماهنگ کردن دو حالت به کار نبرید

effect برای واکنش جانبی به خواندن signalها مطرح است، اما استفاده از آن برای کپی نتیجه یک signal به signal دیگر، رابطه داده را پنهان می‌کند. اگر مقدار قابل محاسبه است، computed معمولاً معنای روشن‌تری دارد. در مثال فهرست، effect اضافه نکرده‌ایم چون نتیجه فیلتر به اجرای جانبی نیاز ندارد. کم بودن APIهای استفاده‌شده نتیجه محدود بودن مسئله است، نه ناقص بودن نمونه.

برای کاری مانند اتصال به یک کتابخانه غیر Angular یا همگام‌سازی محدود با storage، چرخه عمر و cleanup مهم می‌شود. اگر هر تغییر listener تازه ایجاد کند و قبلی حذف نشود، رفتار تکراری یا مصرف حافظه ایجاد می‌شود. ثبت effect بدون شرح همین مسئولیت، فقط محل مسئله را عوض می‌کند. طراحی کار جانبی باید بگوید چه زمانی آغاز، تکرار و متوقف می‌شود.

RxJS و Signals دو نقش قابل ترکیب دارند

Observable برای جریان رخدادها و ترکیب عملیات ناهمزمان مناسب است. Signal معمولاً مقدار فعلی قابل مصرف رابط را بیان می‌کند. برای جست‌وجوی شبکه‌ای، لغو درخواست قبلی، ترتیب پاسخ‌ها و debounce مسئله‌های مستقلی‌اند. تبدیل نتیجه به signal، خودبه‌خود این قراردادها را تعریف نمی‌کند. جایگزینی مکانیکی BehaviorSubject با signal ممکن است semantics جریان را تغییر دهد.

راهنمای RxJS interop ابزار ارتباط میان این دو مدل را توضیح می‌دهد. اگر از toSignal استفاده می‌کنید، subscription، مقدار اولیه و lifetime را بفهمید و برای هر خواندن دوباره آن را نسازید. اگر Observable هنوز پاسخی نداده، رابط باید وضعیت انتظار داشته باشد. نداشتن مقدار با خطا و نتیجه خالی سه معنای متفاوت‌اند.

برای مهاجرت، اول نیاز موجود را بنویسید. آیا مصرف‌کننده فقط آخرین مقدار را می‌خواهد یا به هر رخداد و ترتیب آن نیاز دارد؟ آیا stream عملیات را لغو می‌کند؟ آیا چند مصرف‌کننده دارد؟ پاسخ این سؤال‌ها تعیین می‌کند تبدیل مناسب است یا همان مدل فعلی خواناتر می‌ماند. انتخاب ابزار تازه نباید قرارداد رفتار قبلی را بی‌صدا عوض کند.

Componentها را با مرز داده روشن ترکیب کنید

Standalone بودن component یعنی وابستگی template به‌صورت روشن وارد می‌شود؛ به معنی بی‌نیازی از ساختار برنامه نیست. راهنمای Importing components این مدل را شرح می‌دهد. در نمونه ساده، control flow داخلی استفاده شده و component دیگری لازم نیست. با اضافه کردن قابلیت، import و مسئولیت هر جزء را مشخص کنید.

APIهای signal-based input نیز موضوع جداگانه‌ای هستند و با signal محلی یکی نیستند. راهنمای Inputs به قرارداد دریافت داده از والد مربوط است. یک component قابل استفاده مجدد باید بداند کدام مقدار را از بیرون می‌گیرد و کدام event یا تغییر را گزارش می‌کند. تبدیل تمام مقدارهای ورودی به حالت محلی قابل تغییر می‌تواند مالکیت داده را مبهم کند.

Zoneless هم یک انتخاب پیکربندی جداست. Signals به این مسیر مربوط‌اند، ولی افزودن signal به یک component به معنی مهاجرت کامل برنامه به Zoneless نیست. راهنمای نسخه 19 وضعیت و شرایط همان نسخه را توضیح می‌دهد. قدم بعدی برای پروژه موجود، انتخاب یک بخش کوچک با حالت محلی و مقدار مشتق است؛ ابتدا مالکیت داده آن را روشن کنید و سپس درباره تغییر مدل ناهمزمان تصمیم بگیرید.

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

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

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

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