Angular 19 مع Signals و Resource API: بناء تطبيقات ويب تفاعلية في 2026

Noqta Team
بواسطة Noqta Team ·

جاري تحميل مشغل تحويل النص إلى كلام الصوتي...

Angular 19 يغيّر كل ما تعرفه عن هذا الإطار. تحلّ Signals محلّ RxJS لإدارة الحالة، ويحوّل Resource API البيانات غير المتزامنة إلى عناصر تفاعلية من الدرجة الأولى، ويمنحك وضع Zoneless أداءً يضاهي React. في هذا الدليل ستبني لوحة تحكم تفاعلية كاملة وتتقن مكدّس Angular الحديث من اليوم الأول.

ما الذي ستتعلّمه

بنهاية هذا الدليل، ستكون قادراً على:

  • إنشاء مكونات مستقلة بدون NgModules
  • استخدام Signals (signal، computed، effect) لإدارة الحالة التفاعلية
  • جلب البيانات البعيدة باستخدام Resource API الجديد و httpResource
  • تشغيل Angular في وضع zoneless لأقصى أداء
  • بناء نماذج مبنية على Signals بأمان النوع
  • الربط بين RxJS observables و Signals عبر toSignal و toObservable
  • نشر تطبيق Angular جاهز للإنتاج

المتطلبات المسبقة

قبل البدء، تأكد من توفر:

  • Node.js 20 فما فوق (يتطلب Angular 19 الإصدار 20 على الأقل)
  • معرفة أساسية بـ TypeScript (الواجهات، الـ generics، الـ decorators)
  • إلمام بـ HTML و CSS
  • محرر أكواد، ويُفضَّل VS Code مع إضافة Angular Language Service
  • خبرة مع Angular CLI مفيدة لكنها ليست إلزامية

لماذا يُعدّ Angular 19 مهماً

لسنوات طويلة بدا Angular ثقيلاً مقارنةً بـ React أو Vue. كانت NgModules تستوجب قدراً كبيراً من الكود النمطي، وكان Zone.js يعدّل دوال عامة لتشغيل اكتشاف التغييرات، وكانت RxJS الطريقة الوحيدة المقبولة للتعامل مع البيانات غير المتزامنة. يحلّ Angular 19 هذه المشاكل جميعها:

  • الاستقلالية افتراضياً — لا وجود لـ NgModules، استيراد أنظف
  • Signals — تفاعلية دقيقة بدون أعباء Zone.js
  • اكتشاف تغييرات بدون Zone — حِزم أصغر، وعرض أسرع
  • Resource API — جلب بيانات تفاعلي مع حالات التحميل والخطأ مدمجة
  • الترطيب التدريجي — تطبيقات مُصيّرة على الخادم تُرطَّب عند الطلب

النتيجة: Angular يبدو عصرياً وخفيفاً وبديلاً حقيقياً لمكونات React Server.

ما الذي ستبنيه

لوحة تحكم Signals فعلية تعرض إحصائيات مستودعات GitHub. سيقوم التطبيق بـ:

  • جلب المستودعات من واجهة GitHub API باستخدام httpResource
  • تصفيتها وترتيبها تفاعلياً باستخدام Signals
  • حفظ تفضيلات المستخدم عبر مخزن مبني على Signals
  • تحديث الواجهة فورياً بلا ngIf ولا ngFor نمطي، مستخدماً التحكّم الجديد @if و @for

الخطوة 1: إنشاء مشروع Angular 19

ثبّت آخر إصدار من Angular CLI وأنشئ المشروع:

npm install -g @angular/cli@19
ng new signals-dashboard --style=css --ssr=false --standalone
cd signals-dashboard

افتح angular.json وتأكد أن "strict": true في خيارات TypeScript. Angular 19 يُفعّل الوضع الصارم افتراضياً، وهو يتناغم تماماً مع Signals.

الآن فعّل وضع Zoneless. عدّل src/main.ts:

import { bootstrapApplication } from '@angular/platform-browser';
import { provideExperimentalZonelessChangeDetection } from '@angular/core';
import { provideHttpClient } from '@angular/common/http';
import { AppComponent } from './app/app.component';
 
bootstrapApplication(AppComponent, {
  providers: [
    provideExperimentalZonelessChangeDetection(),
    provideHttpClient(),
  ],
}).catch((err) => console.error(err));

احذف zone.js من polyfills في angular.json لتقليص نحو 40 كيلوبايت من الحجم النهائي.

الخطوة 2: فهم عناصر Signals الثلاثة الأساسية

Signals قيم تفاعلية تُخطر مُستهلكيها عند تغيّرها. يقدّم Angular ثلاث أدوات:

import { signal, computed, effect } from '@angular/core';
 
const count = signal(0);
const double = computed(() => count() * 2);
 
effect(() => {
  console.log(`العداد ${count()}، الضعف ${double()}`);
});
 
count.set(5);
count.update((n) => n + 1);

ثلاث ملاحظات:

  1. قراءة القيمة عبر count() كما لو كانت دالة.
  2. كتابة القيمة عبر .set() أو .update() على signal قابلة للكتابة.
  3. اشتقاق قيمة عبر computed() الذي يحفظ النتيجة تلقائياً.

تُنفَّذ دالة effect() كلّما تغيّرت أي signal تُقرأ داخلها. تُنظَّف التأثيرات تلقائياً عند تدمير المكوّن المالك — لا حاجة لإلغاء الاشتراك يدوياً.

الخطوة 3: بناء مخزن مبني على Signals

أنشئ src/app/stores/preferences.store.ts:

import { Injectable, signal, computed, effect } from '@angular/core';
 
export type SortBy = 'stars' | 'updated' | 'name';
 
@Injectable({ providedIn: 'root' })
export class PreferencesStore {
  readonly query = signal('');
  readonly sortBy = signal<SortBy>('stars');
  readonly minStars = signal(0);
 
  readonly activeFiltersCount = computed(() => {
    let count = 0;
    if (this.query().length > 0) count++;
    if (this.minStars() > 0) count++;
    return count;
  });
 
  constructor() {
    const saved = localStorage.getItem('prefs');
    if (saved) {
      const parsed = JSON.parse(saved);
      this.query.set(parsed.query ?? '');
      this.sortBy.set(parsed.sortBy ?? 'stars');
      this.minStars.set(parsed.minStars ?? 0);
    }
 
    effect(() => {
      const snapshot = {
        query: this.query(),
        sortBy: this.sortBy(),
        minStars: this.minStars(),
      };
      localStorage.setItem('prefs', JSON.stringify(snapshot));
    });
  }
 
  reset(): void {
    this.query.set('');
    this.sortBy.set('stars');
    this.minStars.set(0);
  }
}

هذا مخزن تفاعلي كامل في نحو 30 سطراً. لا Redux، ولا كود NgRx نمطي، ولا أنابيب Observable. يقوم effect() بصمت بمزامنة أي تغيير مع localStorage.

الخطوة 4: جلب البيانات عبر Resource API

Resource API هو جواب Angular 19 عن حالات التحميل والخطأ. أنشئ src/app/services/repos.service.ts:

import { Injectable, computed, inject } from '@angular/core';
import { httpResource } from '@angular/common/http';
import { PreferencesStore } from '../stores/preferences.store';
 
export interface Repo {
  id: number;
  name: string;
  full_name: string;
  description: string | null;
  stargazers_count: number;
  updated_at: string;
  html_url: string;
}
 
@Injectable({ providedIn: 'root' })
export class ReposService {
  private readonly prefs = inject(PreferencesStore);
 
  readonly reposResource = httpResource<Repo[]>(() => {
    const q = this.prefs.query().trim();
    if (!q) return undefined;
    return `https://api.github.com/search/repositories?q=${encodeURIComponent(q)}`;
  });
 
  readonly filteredRepos = computed(() => {
    const repos = this.reposResource.value() ?? [];
    const minStars = this.prefs.minStars();
    const sortBy = this.prefs.sortBy();
 
    const filtered = repos.filter((r) => r.stargazers_count >= minStars);
 
    return [...filtered].sort((a, b) => {
      if (sortBy === 'stars') return b.stargazers_count - a.stargazers_count;
      if (sortBy === 'name') return a.name.localeCompare(b.name);
      return b.updated_at.localeCompare(a.updated_at);
    });
  });
}

يستقبل httpResource دالة تُرجع رابط URL. عندما تُعاد الدالة إلى التنفيذ بسبب تغيّر أحد مدخلاتها التفاعلية، يقوم Angular تلقائياً بجلب البيانات الجديدة ويُوفّر signals value() و status() و error().

إرجاع undefined من الدالة يوقف الطلب مؤقتاً، وهي طريقة أنيقة لتفادي الجلب قبل أن يكتب المستخدم شيئاً.

الخطوة 5: تحكّم حديث بـ @if و @for

الدوال الهيكلية القديمة *ngIf و *ngFor استُبدلت ببناء جُملي يُحلَّل أسرع ويُقرأ بصورة أكثر طبيعية. أنشئ src/app/components/repo-list.component.ts:

import { Component, inject } from '@angular/core';
import { ReposService } from '../services/repos.service';
 
@Component({
  selector: 'app-repo-list',
  standalone: true,
  template: `
    @if (repos.reposResource.status() === 'loading') {
      <p class="loading">جاري تحميل المستودعات...</p>
    } @else if (repos.reposResource.error()) {
      <p class="error">فشل التحميل: {{ repos.reposResource.error()?.message }}</p>
    } @else {
      @for (repo of repos.filteredRepos(); track repo.id) {
        <article class="repo-card">
          <h3>
            <a [href]="repo.html_url" target="_blank">{{ repo.full_name }}</a>
          </h3>
          @if (repo.description) {
            <p>{{ repo.description }}</p>
          }
          <div class="meta">
            <span>&#9733; {{ repo.stargazers_count }}</span>
            <span>آخر تحديث {{ repo.updated_at | date: 'mediumDate' }}</span>
          </div>
        </article>
      } @empty {
        <p class="empty">لا توجد مستودعات تطابق معايير التصفية.</p>
      }
    }
  `,
  styleUrl: './repo-list.component.css',
})
export class RepoListComponent {
  readonly repos = inject(ReposService);
}

أهم التحسينات مقارنة بـ Angular التقليدي:

  • كتلة @empty تعالج المصفوفات الفارغة بأناقة
  • الـ track إلزامي، ممّا يمنع خطأ trackBy الشائع
  • المُجمِّع يرصد أخطاء بناء الجملة وقت البناء بدلاً من وقت التشغيل

الخطوة 6: بناء شريط بحث مبني على Signals

أصبحت النماذج المبنية على Signals مستقرة في Angular 19. أنشئ src/app/components/search-bar.component.ts:

import { Component, inject } from '@angular/core';
import { PreferencesStore } from '../stores/preferences.store';
 
@Component({
  selector: 'app-search-bar',
  standalone: true,
  template: `
    <div class="search-bar">
      <input
        type="search"
        placeholder="ابحث في مستودعات GitHub..."
        [value]="prefs.query()"
        (input)="onQueryChange($event)"
      />
      <select
        [value]="prefs.sortBy()"
        (change)="onSortChange($event)"
      >
        <option value="stars">الأكثر نجوماً</option>
        <option value="updated">الأحدث تحديثاً</option>
        <option value="name">الاسم أ إلى ي</option>
      </select>
      <input
        type="number"
        min="0"
        placeholder="الحد الأدنى للنجوم"
        [value]="prefs.minStars()"
        (input)="onMinStarsChange($event)"
      />
      @if (prefs.activeFiltersCount() > 0) {
        <button (click)="prefs.reset()">
          إعادة تعيين ({{ prefs.activeFiltersCount() }})
        </button>
      }
    </div>
  `,
})
export class SearchBarComponent {
  readonly prefs = inject(PreferencesStore);
 
  onQueryChange(event: Event): void {
    const value = (event.target as HTMLInputElement).value;
    this.prefs.query.set(value);
  }
 
  onSortChange(event: Event): void {
    const value = (event.target as HTMLSelectElement).value as 'stars' | 'updated' | 'name';
    this.prefs.sortBy.set(value);
  }
 
  onMinStarsChange(event: Event): void {
    const value = Number((event.target as HTMLInputElement).value);
    this.prefs.minStars.set(Number.isFinite(value) ? value : 0);
  }
}

حين يكتب المستخدم، تتحدّث signal الخاصّة بـ query. يعيد httpResource تقييم دالته ويجلب بيانات جديدة. يعيد filteredRepos حساب نفسه، وتُعرض الواجهة من جديد، دون استدعاء markForCheck أو حَقن ChangeDetectorRef.

الخطوة 7: توصيل كل شيء في المكوّن الجذر

عدّل src/app/app.component.ts:

import { Component } from '@angular/core';
import { SearchBarComponent } from './components/search-bar.component';
import { RepoListComponent } from './components/repo-list.component';
 
@Component({
  selector: 'app-root',
  standalone: true,
  imports: [SearchBarComponent, RepoListComponent],
  template: `
    <header>
      <h1>لوحة تحكم Angular 19 Signals</h1>
    </header>
    <main>
      <app-search-bar />
      <app-repo-list />
    </main>
  `,
})
export class AppComponent {}

شغّل التطبيق بـ ng serve وافتح http://localhost:4200. اكتب كلمة بحث، عدّل فلتر النجوم، وراقب القائمة تتحدّث فوراً بدون اشتراك يدوي واحد.

الخطوة 8: كبح إدخال المستخدم بـ RxJS Interop

أحياناً تظل بحاجة إلى أسلوب Observables، مثلاً لكبح حقل البحث. يوفر Angular toSignal و toObservable لتناغم سلس.

حدّث ReposService لكبح الاستعلام:

import { Injectable, computed, inject } from '@angular/core';
import { httpResource } from '@angular/common/http';
import { toObservable, toSignal } from '@angular/core/rxjs-interop';
import { debounceTime, distinctUntilChanged } from 'rxjs';
import { PreferencesStore } from '../stores/preferences.store';
 
@Injectable({ providedIn: 'root' })
export class ReposService {
  private readonly prefs = inject(PreferencesStore);
 
  private readonly debouncedQuery = toSignal(
    toObservable(this.prefs.query).pipe(
      debounceTime(300),
      distinctUntilChanged(),
    ),
    { initialValue: '' }
  );
 
  readonly reposResource = httpResource<{ items: Repo[] }>(() => {
    const q = this.debouncedQuery().trim();
    if (!q) return undefined;
    return `https://api.github.com/search/repositories?q=${encodeURIComponent(q)}`;
  });
}

هذا هو النمط الذي يوصي به فريق Angular للأكواد الهجينة: استعمل Signals للحالة، والجأ إلى RxJS فقط عندما تحتاج إلى عوامل مثل debounceTime أو switchMap أو retryWhen.

الخطوة 9: اختبار الكود المبني على Signals

Signals دوال نقية، مما يجعل اختبارها سهلاً. أنشئ preferences.store.spec.ts:

import { TestBed } from '@angular/core/testing';
import { PreferencesStore } from './preferences.store';
 
describe('PreferencesStore', () => {
  let store: PreferencesStore;
 
  beforeEach(() => {
    localStorage.clear();
    TestBed.configureTestingModule({});
    store = TestBed.inject(PreferencesStore);
  });
 
  it('يبدأ بالقيم الافتراضية', () => {
    expect(store.query()).toBe('');
    expect(store.sortBy()).toBe('stars');
    expect(store.activeFiltersCount()).toBe(0);
  });
 
  it('يحسب عدد الفلاتر النشطة', () => {
    store.query.set('angular');
    store.minStars.set(100);
    expect(store.activeFiltersCount()).toBe(2);
  });
 
  it('يعيد تعيين كل التفضيلات', () => {
    store.query.set('test');
    store.reset();
    expect(store.query()).toBe('');
  });
});

لا fakeAsync، لا TestScheduler، لا نداءات done. عيِّن signal، اقرأ أخرى، ثمّ تحقّق. هذه البساطة هي أقوى حجّة للانتقال إلى Signals.

الخطوة 10: البناء والنشر

أنتج بناء إنتاج محسّن:

ng build --configuration production

يحتوي مجلد dist/signals-dashboard/browser على ملفات جاهزة لأي CDN. على Vercel أو Netlify، الإعداد الافتراضي يعمل دون تعديل. على VPS تقليدي، قدِّم index.html مع مسار احتياطي لمعالجة التوجيه من جانب العميل.

تحقّق من حجم الحِزمة:

npx source-map-explorer dist/signals-dashboard/browser/main-*.js

تطبيق Angular 19 بلا Zone يصل إلى نحو 110 كيلوبايت مضغوطاً لهذه اللوحة، وهو ما يعادل مشروع React + React Query بتعقيد مماثل.

اختبار التنفيذ

اتبع قائمة التحقق هذه:

  1. يعمل التطبيق بلا أخطاء في الـ console
  2. الكتابة في صندوق البحث تجلب البيانات بعد كبح 300 مللي ثانية
  3. قائمة الترتيب تعيد ترتيب النتائج فوراً
  4. تعيين حدّ أدنى للنجوم يُخفي المستودعات الأقل منه
  5. تحديث الصفحة يُعيد آخر استعلام وترتيب وفلتر
  6. تظهر حالة التحميل للطلبات البطيئة
  7. قطع الإنترنت يُظهر خطأً مرئياً، وليس فشلاً صامتاً

استكشاف الأخطاء

NG0203: inject() must be called from an injection context تستدعي inject() خارج constructor أو حقل مُهيّأ أو دالة factory. انقله إلى constructor أو استخدمه كمُهيِّئ حقل.

Error: NG02200: Cannot find a differ supporting object كتلة @for تفتقر إلى track. أضف track item.id أو أي مفتاح فريد.

Resource تُعيد الجلب عند كل عرض دالة الـ URL لديك تعتمد على قيمة تتغير في كل دورة اكتشاف تغيير. غلّف القيمة المزعجة في computed() لتثبيتها.

إعادة التحميل الحارّ معطّلة بعد إضافة Signals الإصدارات القديمة من Angular CLI تخزّن بقوة. نفّذ rm -rf .angular ثم أعد تشغيل ng serve.

الخطوات التالية

  • أضف تحميل بيانات على مستوى المسار باستخدام Resource API داخل حُرّاس resolve
  • فعّل الترطيب التدريجي للصفحات المُصيَّرة على الخادم عبر @defer
  • استكشف NgRx SignalStore الذي يبني متجراً كامل الميزات فوق Signals
  • أدمج TanStack Query لـ Angular للتخزين المؤقت المتقدم والجلب الخلفي
  • اقرأ دليل Angular Signals الرسمي لمعالجة الحالات الطرفية

ربما تستمتع أيضاً بهذه الدروس ذات الصلة:

الخلاصة

Angular 19 عاد ليكون إطاراً حديثاً بحق. تمنحك Signals تفاعلية دقيقة بدون أعباء Zone.js، ويقلّل Resource API الكود النمطي للبيانات غير المتزامنة إلى الحد الأدنى، وأخيراً يمكنك كتابة Angular بدون رؤية NgModule واحدة.

إن كنت قد قيّمت Angular قبل خمس سنوات وتركته، فهذا هو الإصدار الذي يستحقّ نظرة ثانية. تحصل الفرق التي لديها كود Angular كبير على مسار انتقال تدريجي واضح. أما الفرق التي تبدأ من الصفر، فتحصل على إطار متكامل مع TypeScript من الدرجة الأولى، وحقن تبعيات مدمج، ودورة إصدار مستدامة طويلة الأمد.

لوحة التحكم التي بنيتها للتو هي الهيكل العظمي لأي تطبيق Angular 19 جدّي: حالة تفاعلية، جلب بيانات مُحكم، إدخال مكبوح، واختبار نظيف. استنسخها، ووسّع عليها، واشحن مشروعك التالي بالأسلوب الحديث لـ Angular.


هل تريد قراءة المزيد من الدروس التعليمية؟ تحقق من أحدث درس تعليمي لدينا على البدء مع Laravel 11: التثبيت والتكوين وهيكل المجلدات.

ناقش مشروعك معنا

نحن هنا للمساعدة في احتياجات تطوير الويب الخاصة بك. حدد موعدًا لمناقشة مشروعك وكيف يمكننا مساعدتك.

دعنا نجد أفضل الحلول لاحتياجاتك.

مقالات ذات صلة

بناء وكلاء الذكاء الاصطناعي من الصفر باستخدام TypeScript: إتقان نمط ReAct مع Vercel AI SDK

تعلّم كيفية بناء وكلاء الذكاء الاصطناعي من الأساس باستخدام TypeScript. يغطي هذا الدليل التعليمي نمط ReAct، واستدعاء الأدوات، والاستدلال متعدد الخطوات، وحلقات الوكلاء الجاهزة للإنتاج مع Vercel AI SDK.

35 د قراءة·