ساخت شمارشگر معکوس با انگولار و RxJS — از صفر تا صد
در این نوشته قصد داریم یک اپلیکیشن ساده به صورت یک شمارشگر معکوس با انگولار بسازیم. اپلیکیشن ما قادر خواهد بود حالت خود را حفظ کند و با استفاده از RxJS آغاز یافته، مکث کند و ریست شود. به این ترتیب با مفاهیم RxJS آشنا میشویم و میتوانیم از آنها در اپلیکیشنهای دیگر نیز استفاده کنیم. برای مشاهده کد منبع این پروژه به این صفحه گیتهاب (+) مراجعه کنید.
توجه داشته باشید که در صفحه گیتهاب شاخه master راهحل کامل را شامل میشود. در حالی که شاخه follow-along قطعه کدهای برچسب خوردهای را شامل میشود که در این مقاله حذف شدهاند. همه این قطعه کدها برچسب دارند و از این رو یافتن آنها آسان است. برای مشاهده دستورالعملها به فایل README.md مراجعه کنید.
مروری بر اپلیکیشن
1<div class="center" appInputToCountdown>
2 <app-countdown></app-countdown>
3 <app-time-input></app-time-input>
4</div>
فایل app-countdown/countdown.component.ts به نمایش ساعت ما میپردازد و همچنین دکمههای شروع، مکث و ریست در آن قرار دارد. فایل app-time-input/time-input.component.ts سه ورودی ما را برای وارد کردن ثانیه، دقیقه و ساعت نمایش میدهند.
فایل appInputToCountdown/input-to-countdown.directive.ts مسئول برقراری ارتباط بین countdown.component و time-input.component است. ما بدین ترتیب زمان کلی را بر حسب ثانیه از ورودی دریافت کرده و در شمارشگر معکوس نمایش میدهیم.
فایل time-format.pipe.ts تعداد کل ثانیهها را به یک رشته قالببندی شده زمان تبدیل خواهد کرد.
مدیریت حالت با BehaviorSubject
فایل پیکربندی ما input-to-countdown.directive.ts به مدیریت حالت با بهرهگیری از BehaviorSubject در RxJS میپردازد. مدیریت حالت در اینجا به بیان ساده به معنی حفظ رد ثانیهها، دقیقهها، ساعتها و کل ثانیههای سپری شده است.
مفهوم کلیدی شماره 1: سوژهها را در برابر دسترسی/اصلاح گسترده محافظت کنید
یک BehaviorSubject سوژهای است که دارای یک مقدار اولیه است. ما state این BehaviorSubject خود را به صورت private نگهداری میکنیم و observable آن به نام obs$ را به صورت Public تعریف میکنیم. بدین ترتیب state در برابر اعمال تغییرات گسترده حفظ میشود و به کامپوننتهای دیگر امکان میدهیم که به موارد انتشاریافته از سوی state به وسیله obs$ توجه کنند.
1// 1.1
2private state = new BehaviorSubject({
3 seconds: 0,
4 minutes: 0,
5 hours: 0,
6 totalTime: 0
7});
8public obs$ = this.state.asObservable();
9// End 1.1
(updateState(value, command زمانی فراخوانی میشود که یکی از مقادیر ورودی ما در time-input.component تغییر یابد. بهروزرسانی state همچنین موجب تحریک یک انتشار از سوی obs$ میشود. این نخستین مفهوم کلیدی ما در عمل است. ما سوژه خود را روی یک کمینه قفل میکنیم و به observable خود امکان دسترسی آزاد برای انتشار میدهیم.
مفهوم کلیدی شماره 2: دادههای سوژهها را تمیز نگه دارید
پارامتر command به ما اعلان میکند که ورودی بین ثانیهها، دقیقهها و ساعتها تغییر یافته است. ما مقدار ورودی را به یک عدد صحیح تبدیل میکنیم و مطمئن میشویم که یک عدد مثبت است تا دادهها تمیز باقی بمانند. سپس مقدار کنونی state را به عنوان update دریافت میکنیم. توجه داشته باشید که سوژه ما میتواند صرفاً از طریق ()updateState بهروزرسانی شود. ما دادههای ورودی خود را اعتبارسنجی میکنیم چون نقطه اصلاح منفرد آن تضمین میکند که دادهها تمیز باقی بمانند.
1// 1.2
2updateState(value, command) {
3 let valToNumber = parseInt(value);
4 if (valToNumber < 0) valToNumber = 0;
5 let update = this.state.value;
6 if (command === 'seconds') update.seconds = valToNumber;
7 if (command === 'minutes') update.minutes = valToNumber;
8 if (command === 'hours') update.hours = valToNumber;
9 update.totalTime = this.calculateSeconds(update);
10 this.state.next(update);
11}
12// End 1.2
در این مرحله update طوری اصلاح میشود که شامل آخرین تغییرات از ورودی باشد و ثانیههای کلی مجدداً در (calculateSeconds(update محاسبه میشوند. سپس state از طریق (this.state.next(update بهروزرسانی میشود.
1// 1.3
2calculateSeconds(update) {
3 let totalTime = update.seconds
4 totalTime += update.minutes * 60;
5 totalTime += (update.hours * 60) * 60;
6 return totalTime;
7}
8// End 1.3
اتصال رویدادها و حالت به ورودیها
در این مرحله دایرکتیو خود را درون time-input.component.ts تزریق میکنیم. همه منطق اپلیکیشن ما در time-input.component.html اجرا میشود.
1export class TimeInputComponent implements OnInit {
2
3 constructor(public d: InputToCountdownDirective) {}
4
5 ngOnInit() {}
6
7}
جزییات کلیدی در این بخش در رویداد تغییر قرار دارد. زمانی که یکی از ورودیها، یک رویداد تغییر (change) را اجرا میکنند، دایرکتیو ما اقدام به فراخوانی (updateState(value,command میکند. به بیان دیگر ما هر ورودی را از طریق یک رویداد تغییر به فرایند بهروزرسانی state اتصال دادهایم.
نکته: با برچسبگذاری ورودی، میتوانیم از طریق name.value به جای event.target.value$ به مقدار آن دسترسی پیدا کنیم.
1<!-- 2.1 -->
2<label>
3 Hours
4 <input #hours type="number" min="0" max="59" placeholder="hours"
5 [value]="d.getHours()"
6 (change)="d.updateState(hours.value, 'hours')"/>
7</label>
8<label>
9 Minutes
10 <input #minutes type="number" min="0" max="59" placeholder="minutes"
11 [value]="d.getMinutes()"
12 (change)="d.updateState(minutes.value, 'minutes')"/>
13</label>
14<label>
15 Seconds
16 <input #seconds type="number" min="0" max="59" placeholder="seconds"
17 [value]="d.getSeconds()"
18 (change)="d.updateState(seconds.value, 'seconds')"/>
19</label>
20<!-- End 2.1 -->
مشاهدهگرهای شروع، مکث و ریست
قطعه کد زیر شامل اطلاعات زیادی است. ما آن را بر حسب استفاده از عملگر RxJS تجزیه میکنیم.
1ngAfterViewInit(): void {
2 // 3.1
3 const start$ = fromEvent(this.startBtn.nativeElement, 'click').pipe(mapTo(true));
4 const pause$ = fromEvent(this.pauseBtn.nativeElement, 'click').pipe(mapTo(false));
5 const reset$ = fromEvent(this.resetBtn.nativeElement, 'click').pipe(mapTo(null));
6 const stateChange$ = this.d.obs$.pipe(mapTo(null));
7 this.intervalObs$ = merge(start$, pause$, reset$, stateChange$).pipe(
8 switchMap(isCounting => {
9 if (isCounting === null) return of(null);
10 return isCounting ? interval(1000) : of();
11 }),
12 scan((accumulatedValue, currentValue) => {
13 if (accumulatedValue === 0) return accumulatedValue;
14 if (currentValue === null || !accumulatedValue) return this.d.getTotalSeconds();
15 return --accumulatedValue;
16 })
17 );
18 // End 3.1
19}
Map To/Merge
mapTo مقدار ارسالی ما را دریافت کرده و آن را به هر مقداری که به صورت یک آرگومان ارسال میکنیم تبدیل میکند. در این اپلیکیشن ما رویدادهای تغییر و رویدادهای کلیک را به مقادیر true/false/null تبدیل میکنیم.
مفهوم کلیدی شماره 3: از یک مشاهدهگر منفرد برای ارتقای راهبردهای مدیریت ثبت نام آسان استفاده کنید
Merge چهار مشاهدهگر ما را در یک مشاهدهگر ترکیب میکند. merge هر بار تنها یک مقدار انتشار میدهد. intervalObs$ نه مقدار true و نه false یا null بسته به رویدادی که رخ میدهد انتشار نمیدهد.
نکته: همه مشاهدهگرهای رویداد ما در یک ثبت نام منفرد و یک pipe ناهمگام منفرد ادغام خواهد شد.
1const start$ = fromEvent(this.startBtn.nativeElement, 'click').pipe(mapTo(true));
2const pause$ = fromEvent(this.pauseBtn.nativeElement, 'click').pipe(mapTo(false));
3const reset$ = fromEvent(this.resetBtn.nativeElement, 'click').pipe(mapTo(null));
4const stateChange$ = this.d.obs$.pipe(mapTo(null));
5this.intervalObs$ = merge(start$, pause$, reset$, stateChange$)
switchMap
switchMap امکان آغاز انتشار یک مشاهدهگر جدید را بر مبنای ورودی از مشاهدهگر اصلیمان میسر ساخته است. مشاهدهگر اصلی ما ادغامی از همه رویدادهای ممکن (start ،pause ،reset و state change) است که یک تغییر را تحریک میکنند. این مشاهدهگر جدید یا یک مشاهدهگر تهی است یا بازهای (interval) و یا مشاهدهگر خالی است.
نکته: مشاهدهگر ما همچنان در زمان رخداد یک رویداد انتشار مییابد.
Scan
scan یک انباشتگر و یک مقدار کنونی دارد و انباشتگر رد همه مقادیر انتشار یافته را نگهداری میکند. در این مورد ما فقط از یک انباشتگر برای یادآوری مقدار قبلی و کم کردن آن استفاده میکنیم. ما تنها از مقدار کنونی برای نشان دادن این که آیا مقداری تهی (برای ریست) یا غیر تهی (برای ادامه) است استفاده میکنیم.
مفهوم کلیدی شماره 4: از Scan برای مدیریت حالت درونی و اجتناب از عوارض جانبی استفاده کنید
d همان input-to-component.directive است. اگر مقدار کنونی (currentValue) تهی یا accumulatedValue به صورت false باشد، ثانیههای کلی سپری شده از d را بازگشت میدهیم زمانی که ثانیههای کلی بازگشت یافت، accumulatedValue برابر با این مقدار ثانیههای کلی تنظیم میشود. اگر accumulatedValue صفر باشد، مقدار صفر بازگشت میدهیم. به بیان دیگر scan امکان ردگیری یک حالت درون را داخل مشاهدهگر بدون نیاز به تغییر دادن دادههای بیرونی فراهم میسازد.
توجه کنید که وقتی ما اقدام به ارجاع switchMap به ()of میکنیم، scan مقدار accumulatedValue را کاهش نمیدهد زیرا ()of مقداری انتشار نداده است.
1this.intervalObs$ = merge(start$, pause$, reset$, stateChange$).pipe(
2 switchMap(isCounting => {
3 if (isCounting === null) return of(null);
4 return isCounting ? interval(1000) : of();
5 }),
6 scan((accumulatedValue, currentValue) => {
7 if (accumulatedValue === 0) return accumulatedValue;
8 if (currentValue === null || !accumulatedValue) return this.d.getTotalSeconds();
9 return --accumulatedValue;
10 })
11);
مقایسه بین شمارشگر معکوس ما و یک رایانه
ارسال مقدار true به شمارشگر مانند آغاز به کار یک رایانه است. اگر رایانه خاموش باشد، ما در نقطه آغازین قرار داریم. اگر رایانه در حالت sleep باشد، ما از آنجایی که باقی مانده بود از سر میگیریم. ارسال مقدار false به شمارشگر مانند این است که رایانه در حالت sleep قرار گیرد. ارسال مقدار null مانند این است که رایانه خاموش شود.
بنابراین زمانی که ورودیهای ما تغییر میکند (ثانیه، دقیقه و ساعت) یا روی ریست کلیک میکنیم میخواهیم که رایانه تا شروع بعدی خاموش شود. اگر روی pause کلیک کنیم، از رایانه میخواهیم که تا زمان شروع به کار مجدد از جایی که کار باقی مانده به خواب برود.
قالببندی ورودی با pipe
برای جمع بندی باید گفت که شمارشگر از pipe انگولار جهت قالببندی ثانیههای کلی در یک مقدار نمایشی استفاده میکند. تابع padding موجب تعیین یک صفر ابتدایی در اعداد زیر 10 میشود. برای نمونه عدد 9 به صورت 09 درمیآید.
1transform(value: any, args?: any): any {
2 // 4.1
3 const hours = Math.floor((value / 60) / 60);
4 const minutes = Math.floor(value / 60) % 60;
5 const seconds = value % 60;
6 return `${this.padding(hours)}${hours}:${this.padding(minutes)}${minutes}:${this.padding(seconds)}${seconds}`;
7 // END 4.1
8}
9
10private padding(time) {
11 return time < 10 ? '0' : '';
12}
مفهوم کلیدی شماره 5: استفاده از pipe ناهمگام برای سادهسازی مدیریت ثبت نام
به ساختار else default توجه کنید. اگر مشاهدهگر ما هنوز یک مقدار true ارسال نکرده باشد، به جای نمایش هیچ چیز مقدار پیشفرض 00:00:00 نمایش پیدا میکند.
1<!-- 4.2 -->
2<ng-container *ngIf="intervalObs$ | async as clock; else default">
3 {{clock | timeFormat}}
4</ng-container>
5<ng-template #default>00:00:00</ng-template>
6<!-- 4.2 -->
سخن پایانی
پیادهسازی یک شمارشگر معکوس در جاوا اسکریپت ممکن است منجر به نوشتن کدی پر از باگ شود و عوارض جانبی ناخواستهای داشته باشد. با استفاده از RxJS میتوانیم بدون نیاز به کدنویسی زیاد یک شمارگر معکوس کارآمد بسازیم.
اگر این مطلب برای شما مفید بوده است، آموزشهای زیر نیز به شما پیشنهاد میشوند:
- مجموعه آموزشهای JavaScript (جاوا اسکریپت)
- مجموعه آموزشهای برنامهنویسی
- آموزش مقدماتی AngularJS برای ساخت اپلیکیشن های تک صفحهای
- آموزش انگولار (Angular): ساخت یک اپلیکیشن در ۲۰ دقیقه – به زبان ساده
- ساخت رابط کاربری Login با انگولار (Angular) و متریال دیزاین – به زبان ساده
==