#1 فصل الاهتمامات (Separation of Concerns)
يكمن جمال بنية Angular في تقسيم قاعدة الكود.
- المكونات (Components) - لبنات بناء قابلة لإعادة الاستخدام تحتوي على أجزاء الأعمال وتنشئ واجهات المستخدم.
- الموجّهات (Directives) - أصناف تضيف سلوكًا جديدًا أو تعدّل السلوك الحالي لعناصر واجهة المستخدم في القالب.
- الأنابيب (Pipes) - دوال تُستخدَم في القالب تأخذ بيانات كمدخل وتحوّلها إلى مخرج.
- الخدمات (Services) - أصناف تتعامل مع منطق الأعمال وتتواصل مع واجهات برمجة التطبيقات.
- الوحدات (Modules) - أصناف تجمع المكونات والموجّهات والأنابيب والخدمات في ميزات.
يحترم هذا الفصل مبدأ المسؤولية الواحدة، وينتج عنه قاعدة كود أنظف.
تجنّب استيراد جميع المكونات والموجّهات والخدمات في وحدتك الرئيسية.
بدلًا من ذلك، قسّم الوظائف حسب كل وحدة (وحدات الميزات). بما أنك تستورد فقط الأجزاء المتعلقة بميزتك داخلها، ستبقى الوحدات أصغر والكود أنظف.
يمكن استخدام هذا مع الموجّه (Router) لتمكين التحميل الكسول (Lazy Loading).
const routes: Routes = [
{
path: 'home',
component: HomeComponent,
},
{
path: 'todos',
// lazy loaded route
loadChildren: () => import('./Todos.module').then(m => m.TodosModule),
},
];في هذا المثال، لن تُحمَّل وحدة Todos واعتمادياتها إلا بعد الوصول إلى المسار المحدد. كلما قلّت الاعتماديات المستخدَمة في الوحدة الرئيسية، أسرع تحميل تطبيقك.
#2 استخدم Angular CLI
Angular CLI أداة مدمجة تسرّع تطوير التطبيق من خلال السماح لك بتوليد أجزاء التطبيق وربط الاعتماديات تلقائيًا.
للبدء، يمكنك توليد تطبيق Angular جديد باستخدام:
ng new my-appبدلًا من إنشاء الملفات يدويًا في قاعدة الكود لديك، استخدم الـ CLI:
- توليد مكوّن جديد:
ng g c component-name- توليد خدمة:
ng g s service-name- توليد أنبوب (pipe):
ng g p pipe-nameوما شابه.
يتيح لك الـ CLI أيضًا تثبيت حزم تنتمي إلى مكتبة Angular القياسية. على سبيل المثال:
ng add @angular/material #3 استخدم أدوات إطار العمل
بدلًا من إعادة اختراع العجلة، باستخدام واجهات برمجة تطبيقات JavaScript الأصلية مثل Document أو Fetch، أو إجراء عمليات التحقق اليدوية من صحة النماذج - استخدم الأدوات المدمجة في إطار العمل.
يأتي Angular مع مجموعة كبيرة من الميزات للتطوير:
- التنقل بين المسارات (Router)
- التواصل مع واجهات برمجة التطبيقات (HTTP Client)
- إدارة الحالة (Rx.js، Signals)
- التلاعب بـ DOM (ElementRef)
- إنشاء النماذج (التفاعلية والمعتمدة على القالب)
- اعتراض وتخزين استدعاءات API مؤقتًا (HTTP Client Interceptor)
- تقييد الوصول إلى المسارات (Route Guard)
- إطار الاختبار (Jasmine)
- عرض عناصر واجهة المستخدم بشكل شرطي (ngIf، ngSwitch، ngFor)، إلخ.
#4 الأنابيب (Pipes)
لنفترض أنك تحتاج لتحويل قيمة داخل قالب. بشكل طبيعي، ستنشئ دالة في ملف TypeScript الخاص بالمكوّن وتفعل شيئًا كهذا:
<h2>{{ doSomething(someFormValue) }}</h2>سيعمل هذا، لكنه سيئ جدًا للأداء لأن الدالة ستُستدعى بشكل متكرر عند كل إعادة عرض.
في هذه السيناريوهات، يجب أن تستخدم Pipes. عند توليد أنبوب:
ng g pipe doSomething@Pipe({
name: 'doSomething'
})
export class DoSomethingPipe implements PipeTransform {
transform(value: unknown, ...args: unknown[]): unknown {
// perform some transformations
console.log('inside doSomething Pipe');
return 'transformed-data';
}
}طبّقه في القالب باستخدام رمز الأنبوب ( | ).
<h2>{{ someFormValue | doSomething }}</h2>الآن لديك نفس الوظيفة تمامًا مبنية عبر Pipe. الفرق بين النهجين واضح على الفور.
#5 استفد من حقن الاعتماديات (Dependency Injection)
من أقوى ميزات Angular حقن الاعتماديات. فبدلًا من أن ينشئ المكوّن اعتمادياته الخاصة، مثلًا:
// creating instances manually
private service = new PostsService();يمكن توفيرها من الخارج وحقنها عبر باني الصنف (constructor) (أو دالة inject()):
constructor(private readonly service: PostsService) { }تُسجَّل الاعتماديات الأساسية في الوحدات كمزوّدين (providers):
@NgModule({
declarations: [
AppComponent,
],
imports: [
BrowserModule,
HttpClientModule
],
providers: [PostsService], // <-- registering PostsService dependency
bootstrap: [AppComponent]
})
export class AppModule { }يمكن أيضًا للخدمات أن تحمل خاصية providedIn توضح أين تُوفَّر. في هذه الحالة، تُوفَّر الخدمة في الوحدة الجذر افتراضيًا، ما يلغي الحاجة لظهور الخدمة في مصفوفة providers.
@Injectable({
providedIn: 'root'
})
export class PostsServiceمع وجود حقن الاعتماديات، يكون كود Angular:
- قابلًا لإعادة الاستخدام
- معياريًا
- أسهل للاختبار (إذ يمكنك ببساطة استبدال الاعتمادية الحقيقية بأخرى وهمية)
هذه مجرد بداية DI في Angular. إذا أردت معرفة المزيد، أنصحك بالاطلاع على هذه المواضيع:
- الحاقنات (Injectors)
- رموز الحقن (Injection Tokens)
- تجاوز الاعتماديات (useClass، useValue، useFactory)
- علاقات المكونات (@self، @skipSelf، @host، @optional)
#6 مشاركة البيانات بين مكونات غير مرتبطة
الممارسة الشائعة هي استخدام موجّهي Input وOutput عند مشاركة البيانات بين المكونات. هذا رائع عندما يكون لديك علاقة من الأب إلى الابن أو من الابن إلى الأب.
لكن إذا احتجت لإرسال بيانات إلى حفيد أو حفيد الحفيد أو مكوّن شقيق، فسيتطلب الأمر عملًا كثيرًا. ناهيك عن أنه إذا احتجت لتحديث البيانات، ستحتاج لإجراء التغييرات في كل الأماكن.
لتجنّب ذلك يمكنك الاستفادة من Behavior Subject في Rx.js لبناء مكان مركزي لتخزين البيانات وليستهلكها المستهلكون.
يعمل كل هذا على نمط الناشر-المشترك (Publisher-Subscriber). يدفع أحد المكونات البيانات إلى الـ Subject، بينما يستهلكها الآخر عبر الاشتراك في Observable.
إنشاء خدمة حالة
لرؤية هذا عمليًا، أنشئ خدمة جديدة وBehavior Subject بعدد افتراضي (1). من الممارسات الجيدة جعل هذا الـ Subject خاصًا (private) لمنع تعديله من الخارج.
@Injectable({
providedIn: 'root'
})
export class SharedDataService {
private readonly currentCount$ = new BehaviorSubject<number>(1);
public setCount(count: number): void {
const currentValue = this.currentCount$.value;
this.currentCount$.next(currentValue + count);
}
public getCount() {
return this.currentCount$.asObservable();
}
}باستخدام setCount() يمكن لمكوّن خارجي تغيير العدد الحالي. وباستخدام getCount() يمكن لمكوّن خارجي استرجاع العدد الحالي كـ Observable للاشتراك فيه.
مكوّن المُنتِج
احقن الخدمة وأنشئ زرين يزيدان أو ينقصان العدد عند النقر - بشكل أساسي استدعاء نفس الدالة بقيمة مختلفة.
<button (click)="increaseCount()">Count +1</button>
<button (click)="decreaseCount()">Count -1</button>constructor(private readonly service: SharedDataService) { }
increaseCount() {
this.service.setCount(1);
}
decreaseCount() {
this.service.setCount(-1);
} مكوّن المستهلِك
احقن نفس الخدمة، لكن هذه المرة استدعِ دالة getCount().
constructor(private readonly service: SharedDataService) { }
ngOnInit() {
this.service.getCount()
.subscribe(console.log)
}الآن يمكن للمكونين مشاركة البيانات دون أن يكونا متصلين ببعضهما.
#7 استخدم معاملات Rx.js
لا تهرب من Rx.js. استفد منها جيدًا. إنها أداة قوية بها العديد من الإمكانيات التي تعمل بشكل جيد مع Angular.
تحويل البيانات (Map)
يمكنك تحويل الاستجابة حسب احتياجاتك مباشرة داخل أنبوب Observable:
titles!: string[];
constructor(private readonly service: PostsService) { }
ngOnInit() {
this.service.getAllPosts()
.pipe(
map((data: Posts[]) => data.map(post => post.title))
)
.subscribe(data => this.titles = data)
}<div *ngFor="let title of titles">
{{ title }}
</div> تصفية البيانات (Filter)
بالإضافة إلى ذلك، يمكنك تصفية المحتوى بناءً على معايير معينة.
ngOnInit() {
this.service.getAllPosts()
.pipe(
map((data: Posts[]) => data.map(post => post.title)),
filter((data: string[]) => data.length > 0)
)
.subscribe(data => this.titles = data)
} مشاركة البيانات بين المشتركين
أحيانًا قد ترغب في استهلاك نفس الـ Observable في أماكن متعددة. صُمِّمت الـ Observables لتكون باردة (cold) بطبيعتها، ما يعني أن الـ Observable يُعاد إنشاؤه لكل مشترك جديد.
في هذا المثال، يعمل الـ Observable مع واجهة برمجة تطبيقات، وسيستدعي كل مشترك جديد نفس الواجهة لأغراضه.
const postsData$ = this.service.getAllPosts();
const firstSubscriber$ = postsData$;
const secondSubscriber$ = postsData$;
firstSubscriber$.subscribe(console.log);
secondSubscriber$.subscribe(console.log);
هناك استدعاءان للشبكة، واحد لكل مشترك.
لإصلاح ذلك، حوّل هذا الـ Observable البارد إلى ساخن (hot) باستخدام معامل share().
const postsData$ = this.service.getAllPosts()
.pipe(
share()
)
const firstSubscriber$ = postsData$;
const secondSubscriber$ = postsData$;
firstSubscriber$.subscribe(console.log);
secondSubscriber$.subscribe(console.log);الآن سيتشارك المشتركون المتعددون نفس الـ Observable.
استدعاء شبكة واحد لعدة مشتركين.
دمج الـ Observables
لاستهلاك بيانات الـ Observable، تحتاج للاشتراك فيها. لكن إذا كان لديك واجهة برمجة تطبيقات تعتمد على أخرى، فهل يعني ذلك أنك بحاجة للاشتراك مرتين؟ لنكتشف ذلك.
getCurrentUser(): Observable<User> {
const user = {
id: 1,
name: 'Mirza',
profession: 'DEV'
};
return of(user);
}ستعيد هذه الدالة مستخدمًا كمثال. لدمجها مع خدمة Posts، الطريقة الساذجة ستكون فعل شيء كهذا:
myPost?: Posts;
private getUserPost() {
this.getCurrentUser()
.subscribe((user: User) => {
this.service.getAllPosts()
.subscribe((posts: Posts[]) => {
this.myPost = posts.find(post => post.userId === user.id);
})
})
}سيعمل هذا، لكنه ممارسة سيئة جدًا. في مواقف كهذه، تريد دمج التدفقين في واحد باستخدام معاملات مثل mergeMap وswitchMap وما شابه.
private getUserPost() {
this.getCurrentUser()
.pipe(
mergeMap((user) => this.service.getAllPosts()
.pipe(
map((posts: Posts[]) => posts.find(post => post.userId === user.id))
))
)
.subscribe((post?: Posts) => {
this.myPost = post;
})
}هناك العديد من معاملات Rx.js الأخرى بحالات استخدام مثيرة للاهتمام بنفس القدر. أشجعك على الاطلاع على توثيق Rx.js لمعرفة المزيد.
#8 ألغِ الاشتراك من الـ Observables
من التفاصيل المهمة التي يفوّتها الكثير من مطوري Angular الجدد إلغاء الاشتراك من الـ Observables.
export class AppComponent implements OnInit {
postsData!: Posts[];
constructor(private readonly service: PostsService) { }
ngOnInit() {
this.service.getAllPosts()
.subscribe(data => this.postsData = data)
}
}رغم أن واجهات برمجة تطبيقات Angular تعمل يدًا بيد مع Rx.js، فإن Observables في Rx.js لا تعرف متى لم يعد مكوّن Angular مستخدَمًا. عندما يبقى الاشتراك نشطًا، يؤدي ذلك إلى تسرّبات في الذاكرة وسلوك غير متوقع أو أخطاء محتملة عند محاولة تحديث المكوّن.
لحل هذه المشكلة، تحتاج لإلغاء الاشتراك يدويًا من جميع الـ Observables بعد التوقف عن استخدام المكوّن. يمكن تحقيق ذلك باستخدام مجموعة متنوعة من معاملات take.
export class AppComponent implements OnInit, OnDestroy {
postsData!: Posts[];
// Creating subject that will be used to unsubscribe
private unsubscribe$ = new Subject<void>();
constructor(private readonly service: PostsService) { }
ngOnInit() {
this.service.getAllPosts()
.pipe(
// Observable will remain active until this subject completes
takeUntil(this.unsubscribe$)
)
.subscribe(data => this.postsData = data)
}
ngOnDestroy() {
this.unsubscribe$.next();
// Subject will complete when the component is destroyed
this.unsubscribe$.complete();
}
} #9 أنبوب Async
أنبوب Async مدمج في Angular، ويتيح لنا الاشتراك في الـ Observables أو حل الـ Promise مباشرة داخل القالب.
بدون أنبوب Async
تحتاج للاشتراك داخل المكوّن، وإسناد بيانات الاشتراك إلى خاصية، وإلغاء الاشتراك يدويًا.
export class AppComponent implements OnInit, OnDestroy {
postsData!: Posts[];
private unsubscribe$ = new Subject<void>();
constructor(private readonly service: PostsService) { }
ngOnInit() {
this.service.getAllPosts()
.pipe(
takeUntil(this.unsubscribe$)
)
.subscribe(data => this.postsData = data)
}
ngOnDestroy() {
this.unsubscribe$.next();
this.unsubscribe$.complete();
}
}<div *ngFor="let post of postsData">
{{ post.title }}
</div> مع أنبوب Async
يمكنك إسناد الـ Observable مباشرة إلى الخاصية والاشتراك داخل القالب. كما يقوم أنبوب Async تلقائيًا بالتخلص من (إلغاء الاشتراك من) الـ Observable عندما لا يعد المكوّن مستخدَمًا.
export class AppComponent implements OnInit {
postsData$!: Observable<Posts[]>;
constructor(private readonly service: PostsService) { }
ngOnInit() {
this.postsData$ = this.service.getAllPosts();
}
}<div *ngFor="let post of postsData$ | async">
{{ post.title }}
</div> #10 كشف التغيير OnPush
عندما يتغيّر شيء ما في أحد المكونات، يجتاز Angular شجرة المكونات بأكملها للتحقق مما إذا كان أي مكوّن قد تغيّر. هذا هو سلوك كشف التغيير الافتراضي، وهو غير فعّال إطلاقًا في المشاريع ذات المكونات الكثيرة.
لمواجهة ذلك، قدّم فريق Angular كشف التغيير OnPush الذي يعيد عرض المكوّن فقط في هذه الحالات:
- عندما يستقبل المكوّن مدخلًا جديدًا (props)
- عندما يُطلَق حدث جديد في المكوّن (أو المكونات الأبناء)
- عندما يستقبل Observable المكوّن المشترك فيه قيمة جديدة
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush // <-- applying it
})
export class AppComponent implements OnInit {
@Input() title: string = 'Default';
updateTitle() {
this.title = 'Updated';
}
}عندما تستدعي دالة updateTitle() عند النقر على الزر، بدلًا من إجراء فحص عميق، سيتحقق Angular فقط من هذا المكوّن وأبنائه.
كشف التغيير OnPush أكثر كفاءة لأنه يقلل عدد الفحوصات، ما يؤدي إلى أداء واستجابة أفضل.
خواطر ختامية
كانت هذه مجرد جزء صغير من كل ما يقدّمه Angular. لمعرفة المزيد، تأكد من الاطلاع على مقالاتي الأخرى.
إلى اللقاء!