اشتراکها
پیشتر در مورد HTTP Client جدید Angular، مطلب «ارتقاء به HTTP Client در Angular 4.3» را بررسی کردیم. یکی دیگر از قابلیتهای HttpClient که روش اصلی دسترسی به اطلاعات از راه دور در Angular 5 نیز میباشد، امکان تعریف Interceptors سفارشی برای آن است. به کمک Interceptors میتوان به تمامی درخواستهای HTTP ارسالی و پاسخهای دریافتی از سرور گوش فرا داد و در این بین منطق ویژهای را پیاده سازی کرد؛ مانندcaching ،logging و غیره. در مطلب جاری قصد داریم از این قابلیت برای نمایش یک loading bar مشخص کنندهی شروع و پایان تمامی درخواستهای HTTP استفاده کنیم.
دریافت و نصب پیشنیازها
برای نمایش loading bar در بالای صفحه، از کامپوننت ng2-slim-loading-bar استفاده خواهیم کرد. دمویی از آنرا در اینجا میتوانید مشاهده کنید.
برای نصب آن، ابتدا دستور ذیل را در ریشهی پروژه اجرا کنید:
پس از آن فایل angular-cli.json. را گشوده و فایل css آنرا تعریف نمائید:
در ادامه به فایل app.module.ts مراجعه کرد و ماژول آنرا نیز به نحو ذیل به برنامه معرفی کنید:
پس از این مقدمات، هرجایی که نیاز به استفادهی از آن با کدنویسی باشد، ابتدا سرویس SlimLoadingBarService آنرا به سازندهی کلاس کامپوننت مدنظر تزریق میکنیم و سپس از متدهای start ،stop و complete آن میتوان استفاده کرد.
همچنین برای نمایش این کامپوننت باید selector آن را در فایل app.component.html در بالای صفحه اضافه کرد:
تدارک یک LoaderInterceptor برای استفاده از ng2-slim-loading-bar
در ادامه میخواهیم هر زمانیکه در سراسر برنامه، درخواست HTTP ایی شروع شد، این کامپوننت نمایش داده شود و در پایان درخواست و یا درصورت بروز خطایی، پایان یابد و مخفی شود. برای این منظور یک Interceptor جدید را به صورت ذیل به پوشهی Core برنامه اضافه میکنیم:
با این کدها:
توضیحات:
- برای پیاده سازی یک interceptor جدید، نیاز است کلاس سرویسی را که HttpInterceptor را پیاده سازی میکند، ایجاد کنیم. برای تکمیل این پیاده سازی نیاز است متد intercept را با امضایی که مشاهده میکنید، تعریف کنیم:
- در اینجا پارامتر req به خود درخواست Http اشاره میکند.
- کار پارامتر next تنظیم و بازگشت یک HttpEvent observable توسط متد handle آن است.
برای نمونه در اینجا ابتدا سرویس SlimLoadingBarService به سازندهی کلاس interceptor تزریق شدهاست. سپس توسط آن میتوان به متدهای start و complete این کامپوننت دسترسی یافت. برای مثال در ابتدای کار گوش فرادادن به درخواست جاری، متد start فراخوانی شدهاست و سپس زمانیکه پاسخی از سرور دریافت شده و یا خطایی رخداده، متد complete آن فراخوانی شدهاست.
ثبت و معرفی LoaderInterceptorService به سیستم
برای معرفی interceptor تهیه شده، به فایل app.module.ts مراجعه کرد و قسمت providers آنرا به نحو ذیل تکمیل میکنیم:
آزمایش برنامه
اکنون اگر قسمتهای مختلف برنامه را که با HttpClient جدید کار میکنند بررسی کنید، متوجه خواهید شد که با شروع هر درخواست، loading bar قرمزی در بالای صفحه ظاهر میشود و در پایان درخواست، به صورت خودکار مخفی میگردد. نکتهی مهم این روش عدم نیاز به تغییری در قسمتهای مختلف برنامه است. این interceptor سراسری است و به صورت یکسانی بر روی کل برنامه تاثیر میگذارد.
دریافت و نصب پیشنیازها
برای نمایش loading bar در بالای صفحه، از کامپوننت ng2-slim-loading-bar استفاده خواهیم کرد. دمویی از آنرا در اینجا میتوانید مشاهده کنید.
برای نصب آن، ابتدا دستور ذیل را در ریشهی پروژه اجرا کنید:
npm install ng2-slim-loading-bar --save
"styles": [ "../node_modules/ng2-slim-loading-bar/style.css", "styles.css" ],
import {SlimLoadingBarModule} from 'ng2-slim-loading-bar'; @NgModule({ imports: [ //... SlimLoadingBarModule.forRoot() ] }) export class AppModule { }
پس از این مقدمات، هرجایی که نیاز به استفادهی از آن با کدنویسی باشد، ابتدا سرویس SlimLoadingBarService آنرا به سازندهی کلاس کامپوننت مدنظر تزریق میکنیم و سپس از متدهای start ،stop و complete آن میتوان استفاده کرد.
import {SlimLoadingBarService} from 'ng2-slim-loading-bar';
<ng2-slim-loading-bar></ng2-slim-loading-bar>
تدارک یک LoaderInterceptor برای استفاده از ng2-slim-loading-bar
در ادامه میخواهیم هر زمانیکه در سراسر برنامه، درخواست HTTP ایی شروع شد، این کامپوننت نمایش داده شود و در پایان درخواست و یا درصورت بروز خطایی، پایان یابد و مخفی شود. برای این منظور یک Interceptor جدید را به صورت ذیل به پوشهی Core برنامه اضافه میکنیم:
ng g s core/interceptors/LoaderInterceptor --spec false
import { Injectable } from "@angular/core"; import { HttpEvent, HttpHandler, HttpInterceptor, HttpRequest, HttpResponse } from "@angular/common/http"; import { Observable } from "rxjs/Observable"; import "rxjs/add/operator/do"; import { SlimLoadingBarService } from "ng2-slim-loading-bar"; @Injectable() export class LoaderInterceptorService implements HttpInterceptor { constructor(private loadingBar: SlimLoadingBarService) { } intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> { // start our loader here this.loadingBar.start(); return next.handle(req).do( (event: HttpEvent<any>) => { if (event instanceof HttpResponse) { this.loadingBar.complete(); } }, (err: any) => { this.loadingBar.complete(); }); } }
- برای پیاده سازی یک interceptor جدید، نیاز است کلاس سرویسی را که HttpInterceptor را پیاده سازی میکند، ایجاد کنیم. برای تکمیل این پیاده سازی نیاز است متد intercept را با امضایی که مشاهده میکنید، تعریف کنیم:
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
- کار پارامتر next تنظیم و بازگشت یک HttpEvent observable توسط متد handle آن است.
برای نمونه در اینجا ابتدا سرویس SlimLoadingBarService به سازندهی کلاس interceptor تزریق شدهاست. سپس توسط آن میتوان به متدهای start و complete این کامپوننت دسترسی یافت. برای مثال در ابتدای کار گوش فرادادن به درخواست جاری، متد start فراخوانی شدهاست و سپس زمانیکه پاسخی از سرور دریافت شده و یا خطایی رخداده، متد complete آن فراخوانی شدهاست.
ثبت و معرفی LoaderInterceptorService به سیستم
برای معرفی interceptor تهیه شده، به فایل app.module.ts مراجعه کرد و قسمت providers آنرا به نحو ذیل تکمیل میکنیم:
@NgModule({ providers: [ { provide: HTTP_INTERCEPTORS, useClass: LoaderInterceptorService, multi: true } ] }) export class AppModule {}
آزمایش برنامه
اکنون اگر قسمتهای مختلف برنامه را که با HttpClient جدید کار میکنند بررسی کنید، متوجه خواهید شد که با شروع هر درخواست، loading bar قرمزی در بالای صفحه ظاهر میشود و در پایان درخواست، به صورت خودکار مخفی میگردد. نکتهی مهم این روش عدم نیاز به تغییری در قسمتهای مختلف برنامه است. این interceptor سراسری است و به صورت یکسانی بر روی کل برنامه تاثیر میگذارد.
اضافه کردن یک ستون در آوت لوک کار سادهای است. برای مثال زمانیکه inbox باز است ، بر روی قسمت نمایش ایمیلها کلیک راست کرده و مسیر زیر را طی کنید:
در اینجا فرض بر این است که از منوی اصلی view->reading pane->bottom انتخاب شده است.
Customize current view -> fields -> new field
اما مقدار دهی ردیفهای این ستون ایجاد شده کار سادهای نیست و نیاز است تا با نکته زیر آشنا بود:
<view type="table">
<viewname>Messages</viewname>
<column>
<heading>تاریخ دریافت</heading>
<prop>http://schemas.microsoft.com/mapi/string/{00020329-0000-0000-C000-000000000046}/تاریخ%20دریافت</prop>
<type>string</type>
<width>150</width>
<style>padding-left:3px;;text-align:left</style>
<editable>1</editable>
</column>
</view>
اگر علاقمند بودید که مقدار کامل این ساختار را مشاهده نمائید در تابع addNewCol ، پس از تعریف curView سطر زیر را اضافه کنید:
MessageBox.Show(curView.XML);
جهت مدیریت تک وهلهای ApplicationUserManger این تغییرات را اعمال کنید؛ یا از این فایل نهایی استفاده کنید.
یک نکته تکمیلی
جهت لود کردن descriptionهای هر action و controller در لایبرری swagger بدین شکل میتوان عمل کرد.
services.AddSwaggerGen(c => { c.SwaggerDoc("v1", new Info { Version = "v1", Title = "My API", Description = "My Web API", TermsOfService = "None", Contact = new Contact { Name = "Mohammad Ahmadi", Email = string.Empty, Url = "https://www.dntips.ir" }, License = new License { Name = "Use under LICX", Url = "https://example.com/license" } }); var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml"; var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile); c.IncludeXmlComments(xmlPath); });
<PropertyGroup> <GenerateDocumentationFile>true</GenerateDocumentationFile> </PropertyGroup>
<PropertyGroup> <GenerateDocumentationFile>true</GenerateDocumentationFile> <NoWarn>$(NoWarn);1591</NoWarn> </PropertyGroup>
اگر به دو مطلب استفاده از Quartz.Net (^ و ^) و خصوصا نظرات آن دقت کرده باشید به این نتیجه خواهید رسید که ... این کتابخانهی در اصل جاوایی گنگ طراحی شدهاست. در سایت جاری برای انجام کارهای زمانبندی شده (مانند ارسال ایمیلهای روزانه خلاصه مطالب، تهیه خروجی PDF و XML سایت، تبدیل پیش نویسها به مطالب، بازسازی ایندکسهای جستجو و امثال آن) از یک Thread timer استفاده میشود که حجم نهایی کتابخانهی محصور کننده و مدیریت کنندهی وظایف آن جمعا 8 کیلوبایت است؛ متشکل از ... سه کلاس. در ادامه کدهای کامل و نحوهی استفاده از آن را بررسی خواهیم کرد.
دریافت کتابخانه DNT Scheduler و مثال آن
DNTScheduler
در این بسته، کدهای کتابخانهی DNT Scheduler و یک مثال وب فرم را، ملاحظه خواهید کرد. از این جهت که برای ثبت وظایف این کتابخانه، از فایل global.asax.cs استفاده میشود، اهمیتی ندارد که پروژهی شما وب فرم است یا MVC. با هر دو حالت کار میکند.
نحوهی تعریف یک وظیفهی جدید
کار با تعریف یک کلاس و پیاده سازی ScheduledTaskTemplate شروع میشود:
برای نمونه :
- در اینجا Order، ترتیب اجرای وظیفهی جاری را در مقایسه با سایر وظیفههایی که قرار است در یک زمان مشخص اجرا شوند، مشخص میکند.
- متد RunAt ثانیهای یکبار فراخوانی میشود (بنابراین بررسی now.Second را فراموش نکنید). زمان ارسالی به آن UTC است و اگر برای نمونه میخواهید بر اساس ساعت ایران کار کنید باید 3.5 ساعت به آن اضافه نمائید. این مساله برای سرورهایی که خارج از ایران قرار دارند مهم است. چون زمان محلی آنها برای تصمیم گیری در مورد زمان اجرای کارها مفید نیست.
در متد RunAt فرصت خواهید داشت تا منطق زمان اجرای وظیفهی جاری را مشخص کنید. برای نمونه در مثال فوق، این وظیفه هر دو دقیقه یکبار اجرا میشود. یا اگر خواستید اجرای آن فقط در سال 23 و 33 دقیقه هر روز باشد، تعریف آن به نحو ذیل خواهد بود:
- خاصیت IsShuttingDown موجود در کلاس پایه ScheduledTaskTemplate، توسط کتابخانهی DNT Scheduler مقدار دهی میشود. این کتابخانه قادر است زمان خاموش شدن پروسهی فعلی IIS را تشخیص داده و خاصیت IsShuttingDown را true کند. بنابراین در حین اجرای وظیفهای مشخص، به مقدار IsShuttingDown دقت داشته باشید. اگر true شد، یعنی فقط 30 ثانیه وقت دارید تا کار را تمام کنید.
خاصیت Pause هر وظیفه را برنامه میتواند تغییر دهد. به این ترتیب در مورد توقف یا ادامهی یک وظیفه میتوان تصمیم گیری کرد. خاصیت ScheduledTasksCoordinator.Current.ScheduledTasks، لیست وظایف تعریف شده را در اختیار شما قرار میدهد.
- در متد Run، منطق وظیفهی تعریف شده را باید مشخص کرد. برای مثال ارسال ایمیل یا تهیهی بک آپ.
- Name نیز نام وظیفهی جاری است که میتواند در گزارشات مفید باشد.
همین مقدار برای تعریف یک وظیفه کافی است.
نحوهی ثبت و راه اندازی وظایف تعریف شده
پس از اینکه چند وظیفه را تعریف کردیم، برای مدیریت بهتر آنها میتوان یک کلاس ثبت و معرفی کلی را مثلا به نام ScheduledTasksRegistry ایجاد کرد:
- شیء ScheduledTasksCoordinator.Current، نمایانگر تنها وهلهی مدیریت وظایف برنامه است.
- توسط متد ScheduledTasksCoordinator.Current.AddScheduledTasks، تنها کافی است کلاسهای وظایف مشتق شده از ScheduledTaskTemplate، معرفی شوند.
- به کمک متد ScheduledTasksCoordinator.Current.Start، کار Thread timer برنامه شروع میشود.
- اگر در حین اجرای متد Run، استثنایی رخ دهد، آنرا توسط یک Action delegate به نام ScheduledTasksCoordinator.Current.OnUnexpectedException میتوانید دریافت کنید. کتابخانهی DNT Scheduler برای اجرای وظایف، از یک ترد با سطح تقدم Below normal استفاده میکند تا در حین اجرای وظایف، برنامهی جاری با اخلال و کندی مواجه نشده و بتواند به درخواستهای رسیده پاسخ دهد. در این بین اگر استثنایی رخ دهد، میتواند کل پروسهی IIS را خاموش کند. به همین جهت این کتابخانه کار try/catch استثناهای متد Run را نیز انجام میدهد تا از این لحاظ مشکلی نباشد.
- متد ScheduledTasksCoordinator.Current.Dispose کار مدیر وظایف برنامه را خاتمه میدهد.
- از متد WakeUp تعریف شده میتوان برای بیدار کردن مجدد برنامه استفاده کرد.
استفاده از کلاس ScheduledTasksRegistry تعریف شده
پس از اینکه کلاس ScheduledTasksRegistry را تعریف کردیم، نیاز است آنرا به فایل استاندارد global.asax.cs برنامه به نحو ذیل معرفی کنیم:
- متد ScheduledTasksRegistry.Init در حین آغاز برنامه فراخوانی میشود.
- متد ScheduledTasksRegistry.End در پایان کار برنامه جهت پاکسازی منابع باید فراخوانی گردد.
همچنین در اینجا با فراخوانی ScheduledTasksRegistry.WakeUp، میتوانید برنامه را مجددا زنده کنید! IIS مجاز است یک سایت ASP.NET را پس از مثلا 20 دقیقه عدم فعالیت (فعالیت به معنای درخواستهای رسیده به سایت است و نه کارهای پس زمینه)، از حافظه خارج کند (این عدد در application pool برنامه قابل تنظیم است). در اینجا در فایل web.config برنامه میتوانید آدرس یکی از صفحات سایت را برای فراخوانی مجدد تعریف کنید:
همینکه درخواست مجددی به این صفحه برسد، مجددا برنامه توسط IIS بارگذاری شده و اجرا میگردد. به این ترتیب وظایف تعریف شده، در طول یک روز بدون مشکل کار خواهند کرد.
گزارشگیری از وظایف تعریف شده
برای دسترسی به کلیه وظایف تعریف شده، از خاصیت ScheduledTasksCoordinator.Current.ScheduledTasks استفاده نمائید:
لیست حاصل را به سادگی میتوان در یک Grid نمایش داد.
دریافت کتابخانه DNT Scheduler و مثال آن
DNTScheduler
در این بسته، کدهای کتابخانهی DNT Scheduler و یک مثال وب فرم را، ملاحظه خواهید کرد. از این جهت که برای ثبت وظایف این کتابخانه، از فایل global.asax.cs استفاده میشود، اهمیتی ندارد که پروژهی شما وب فرم است یا MVC. با هر دو حالت کار میکند.
نحوهی تعریف یک وظیفهی جدید
کار با تعریف یک کلاس و پیاده سازی ScheduledTaskTemplate شروع میشود:
public class SendEmailsTask : ScheduledTaskTemplate
using System; namespace DNTScheduler.TestWebApplication.WebTasks { public class SendEmailsTask : ScheduledTaskTemplate { /// <summary> /// اگر چند جاب در یک زمان مشخص داشتید، این خاصیت ترتیب اجرای آنها را مشخص خواهد کرد /// </summary> public override int Order { get { return 1; } } public override bool RunAt(DateTime utcNow) { if (this.IsShuttingDown || this.Pause) return false; var now = utcNow.AddHours(3.5); return now.Minute % 2 == 0 && now.Second == 1; } public override void Run() { if (this.IsShuttingDown || this.Pause) return; System.Diagnostics.Trace.WriteLine("Running Send Emails"); } public override string Name { get { return "ارسال ایمیل"; } } } }
- متد RunAt ثانیهای یکبار فراخوانی میشود (بنابراین بررسی now.Second را فراموش نکنید). زمان ارسالی به آن UTC است و اگر برای نمونه میخواهید بر اساس ساعت ایران کار کنید باید 3.5 ساعت به آن اضافه نمائید. این مساله برای سرورهایی که خارج از ایران قرار دارند مهم است. چون زمان محلی آنها برای تصمیم گیری در مورد زمان اجرای کارها مفید نیست.
در متد RunAt فرصت خواهید داشت تا منطق زمان اجرای وظیفهی جاری را مشخص کنید. برای نمونه در مثال فوق، این وظیفه هر دو دقیقه یکبار اجرا میشود. یا اگر خواستید اجرای آن فقط در سال 23 و 33 دقیقه هر روز باشد، تعریف آن به نحو ذیل خواهد بود:
public override bool RunAt(DateTime utcNow) { if (this.IsShuttingDown || this.Pause) return false; var now = utcNow.AddHours(3.5); return now.Hour == 23 && now.Minute == 33 && now.Second == 1; }
خاصیت Pause هر وظیفه را برنامه میتواند تغییر دهد. به این ترتیب در مورد توقف یا ادامهی یک وظیفه میتوان تصمیم گیری کرد. خاصیت ScheduledTasksCoordinator.Current.ScheduledTasks، لیست وظایف تعریف شده را در اختیار شما قرار میدهد.
- در متد Run، منطق وظیفهی تعریف شده را باید مشخص کرد. برای مثال ارسال ایمیل یا تهیهی بک آپ.
- Name نیز نام وظیفهی جاری است که میتواند در گزارشات مفید باشد.
همین مقدار برای تعریف یک وظیفه کافی است.
نحوهی ثبت و راه اندازی وظایف تعریف شده
پس از اینکه چند وظیفه را تعریف کردیم، برای مدیریت بهتر آنها میتوان یک کلاس ثبت و معرفی کلی را مثلا به نام ScheduledTasksRegistry ایجاد کرد:
using System; using System.Net; namespace DNTScheduler.TestWebApplication.WebTasks { public static class ScheduledTasksRegistry { public static void Init() { ScheduledTasksCoordinator.Current.AddScheduledTasks( new SendEmailsTask(), new DoBackupTask()); ScheduledTasksCoordinator.Current.OnUnexpectedException = (exception, scheduledTask) => { //todo: log the exception. System.Diagnostics.Trace.WriteLine(scheduledTask.Name + ":" + exception.Message); }; ScheduledTasksCoordinator.Current.Start(); } public static void End() { ScheduledTasksCoordinator.Current.Dispose(); } public static void WakeUp(string pageUrl) { try { using (var client = new WebClient()) { client.Credentials = CredentialCache.DefaultNetworkCredentials; client.Headers.Add("User-Agent", "ScheduledTasks 1.0"); client.DownloadData(pageUrl); } } catch (Exception ex) { //todo: log ex System.Diagnostics.Trace.WriteLine(ex.Message); } } } }
- توسط متد ScheduledTasksCoordinator.Current.AddScheduledTasks، تنها کافی است کلاسهای وظایف مشتق شده از ScheduledTaskTemplate، معرفی شوند.
- به کمک متد ScheduledTasksCoordinator.Current.Start، کار Thread timer برنامه شروع میشود.
- اگر در حین اجرای متد Run، استثنایی رخ دهد، آنرا توسط یک Action delegate به نام ScheduledTasksCoordinator.Current.OnUnexpectedException میتوانید دریافت کنید. کتابخانهی DNT Scheduler برای اجرای وظایف، از یک ترد با سطح تقدم Below normal استفاده میکند تا در حین اجرای وظایف، برنامهی جاری با اخلال و کندی مواجه نشده و بتواند به درخواستهای رسیده پاسخ دهد. در این بین اگر استثنایی رخ دهد، میتواند کل پروسهی IIS را خاموش کند. به همین جهت این کتابخانه کار try/catch استثناهای متد Run را نیز انجام میدهد تا از این لحاظ مشکلی نباشد.
- متد ScheduledTasksCoordinator.Current.Dispose کار مدیر وظایف برنامه را خاتمه میدهد.
- از متد WakeUp تعریف شده میتوان برای بیدار کردن مجدد برنامه استفاده کرد.
استفاده از کلاس ScheduledTasksRegistry تعریف شده
پس از اینکه کلاس ScheduledTasksRegistry را تعریف کردیم، نیاز است آنرا به فایل استاندارد global.asax.cs برنامه به نحو ذیل معرفی کنیم:
using System; using System.Configuration; using DNTScheduler.TestWebApplication.WebTasks; namespace DNTScheduler.TestWebApplication { public class Global : System.Web.HttpApplication { protected void Application_Start(object sender, EventArgs e) { ScheduledTasksRegistry.Init(); } protected void Application_End() { ScheduledTasksRegistry.End(); //نکته مهم این روش نیاز به سرویس پینگ سایت برای زنده نگه داشتن آن است ScheduledTasksRegistry.WakeUp(ConfigurationManager.AppSettings["SiteRootUrl"]); } } }
- متد ScheduledTasksRegistry.End در پایان کار برنامه جهت پاکسازی منابع باید فراخوانی گردد.
همچنین در اینجا با فراخوانی ScheduledTasksRegistry.WakeUp، میتوانید برنامه را مجددا زنده کنید! IIS مجاز است یک سایت ASP.NET را پس از مثلا 20 دقیقه عدم فعالیت (فعالیت به معنای درخواستهای رسیده به سایت است و نه کارهای پس زمینه)، از حافظه خارج کند (این عدد در application pool برنامه قابل تنظیم است). در اینجا در فایل web.config برنامه میتوانید آدرس یکی از صفحات سایت را برای فراخوانی مجدد تعریف کنید:
<?xml version="1.0"?> <configuration> <appSettings> <add key="SiteRootUrl" value="http://localhost:10189/Default.aspx" /> </appSettings> </configuration>
گزارشگیری از وظایف تعریف شده
برای دسترسی به کلیه وظایف تعریف شده، از خاصیت ScheduledTasksCoordinator.Current.ScheduledTasks استفاده نمائید:
var jobsList = ScheduledTasksCoordinator.Current.ScheduledTasks.Select(x => new { TaskName = x.Name, LastRunTime = x.LastRun, LastRunWasSuccessful = x.IsLastRunSuccessful, IsPaused = x.Pause, }).ToList();
نظرات اشتراکها
تحریم شکن بگذر!
اگه به وب سایتش مراجعه کنین گفته شده که سایتهای که ما رو تحریم کردن مثل Google Developer باز میشن نه سایتهایی که از داخل فیلتر شدن.
من گوگل رو تست کردم و بدون مشکل در دسترس قرار گرفت. همونطور که توی وب سایتشون گفته شده باید DNSها رو بر روی «کانکشن اینترنت» تون ست کنین
نظرات اشتراکها
استفاده از پلاگین مترجم گوگل در asp.net
سلام
برای سایتهای بزرگ که قرار از api گوگل تعداد بالا استفاده بشه، مناسب نیست، چون تا یک تعداد محدود رکویست رو رایگان هست و به ازای درخواستهای بیشتر باید پول پرداخت بشه.
من یه مترجم نوشتم، البته مترجم من برای ترجمه کلمات هست، از خود Google Translate استفاده کردم
در قسمت قبل، در هر دو حالت ثبت نام یک کاربر جدید و همچنین ورود به سیستم، یک JSON Web Token را از سرور دریافت کرده و در local storage مرورگر، ذخیره کردیم. اکنون قصد داریم محتوای این توکن را استخراج کرده و از آن جهت نمایش اطلاعات کاربر وارد شدهی به سیستم، استفاده کنیم. همچنین کار بهبود کیفیت کدهایی را هم که تاکنون پیاده سازی کردیم، انجام خواهیم داد.
نگاهی به محتوای JSON Web Token تولیدی
اگر مطلب قسمت قبل را پیگیری کرده باشید، پس از لاگین، یک چنین خروجی را در کنسول توسعه دهندگان مرورگر میتوان مشاهده کرد که همان return Ok(new { access_token = jwt }) دریافتی از سمت سرور است:
اکنون این رشتهی طولانی را در حافظه کپی کرده و سپس به سایت https://jwt.io/#debugger-io مراجعه و در قسمت دیباگر آن، این رشتهی طولانی را paste میکنیم تا آنرا decode کند:
برای نمونه payload آن حاوی یک چنین اطلاعاتی است:
در اینجا یکسری از اطلاعات کاربر، مانند id ، name ، DisplayName و یا حتی role او درج شدهاست؛ به همراه تاریخ صدور (iat) و انقضای (exp) این token که به صورت Unix time format بیان میشوند. به هر کدام از این خواصی که در اینجا ذکر شدهاند، یک user claim گفته میشود. به عبارتی، این token ادعا میکند (claims) که نقش کاربر وارد شدهی به سیستم، Admin است. برای بررسی صحت این ادعا نیز یک امضای دیجیتال (مشخص شدهی با رنگ آبی) را به همراه این توکن سه قسمتی (قسمتهای مختلف آن، با 2 نقطه از هم جدا شدهاند که در تصویر نیز با سه رنگ متمایز، مشخص است)، ارائه کردهاست. به این معنا که اگر قسمتی از اطلاعات این توکن، در سمت کاربر دستکاری شود، دیگر در سمت سرور تعیین اعتبار مجدد نخواهد شد؛ چون نیاز به یک امضای دیجیتال جدید را دارد که کلیدهای خصوصی تولید آن، تنها در سمت سرور مهیا هستند و به سمت کلاینت ارسال نمیشوند.
استخراج اطلاعات کاربر وارد شدهی به سیستم، از JSON Web Token دریافتی
همانطور که در payload توکن دریافتی از سرور نیز مشخص است، اطلاعات ارزشمندی از کاربر، به همراه آن ارائه شدهاند و مزیت کار با آن، عدم نیاز به کوئری گرفتن مداوم از سرور و بانک اطلاعاتی، جهت دریافت مجدد این اطلاعات است. بنابراین اکنون در برنامهی React خود، قصد داریم مشابه کاری را که سایت jwt.io انجام میدهد، پیاده سازی کرده و به این اطلاعات دسترسی پیدا کنیم و برای مثال DisplayName را در Navbar نمایش دهیم. برای این منظور فایل app.js را گشوده و تغییرات زیر را به آن اعمال میکنیم:
- میخواهیم اطلاعات کاربر جاری را در state کامپوننت مرکزی App قرار دهیم. سپس زمانیکه کار رندر کامپوننت NavBar درج شدهی در متد رندر آن فرا میرسد، میتوان این اطلاعات کاربر را به صورت props به آن ارسال کرد؛ و یا به هر کامپوننت دیگری در component tree برنامه.
- بنابراین ابتدا کامپوننت تابعی بدون حالت App را تبدیل به یک کلاس کامپوننت استاندارد مشتق شدهی از کلاس پایهی Component میکنیم. اکنون میتوان state را نیز به آن اضافه کرد:
- سپس متد componentDidMount را به این کامپوننت اضافه میکنیم؛ در آن ابتدا token ذخیره شدهی در local storage را دریافت کرده و سپس decode میکنیم تا payload اطلاعات کاربر وارد شدهی به سیستم را استخراج کنیم. در آخر state را توسط این اطلاعات به روز میکنیم.
- برای decode کردن توکن، نیاز به نصب کتابخانهی زیر را داریم:
- پس از نصب آن، ابتدا امکانات آنرا import کرده و سپس از آن در متد componentDidMount استفاده میکنیم:
ابتدا آیتمی با کلید token از localStorage استخراج میشود. سپس توسط متد jwtDecode، تبدیل به یک شیء حاوی اطلاعات کاربر جاری وارد شدهی به سیستم گشته و در آخر در state درج میشود. در اینجا درج try/catch ضروری است؛ از این جهت که متد jwtDecode، در صورت برخورد به توکنی غیرمعتبر، یک استثناء را صادر میکند و این استثناء نباید بارگذاری برنامه را با اخلال مواجه کند. از این جهت که اگر توکنی غیرمعتبر است (و یا حتی در localStorage وجود خارجی ندارد؛ برای کاربران لاگین نشده)، کاربر باید مجددا برای دریافت نمونهی معتبر آن، لاگین کند.
- اکنون میتوان شیء currentUser را به صورت props، به کامپوننت NavBar ارسال کرد:
نمایش اطلاعات کاربر وارد شدهی به سیستم در NavBar
پس از ارسال شیء کاربر به صورت props به کامپوننت src\components\navBar.jsx، کدهای این کامپوننت را به صورت زیر جهت نمایش نام کاربر جاری وارد شدهی به سیستم تغییر میدهیم:
چون این کامپوننت به صورت یک کامپوننت تابعی بدون حالت تعریف شده، برای دریافت props میتوان یا آنرا به صورت مستقیم به عنوان پارامتر تعریف کرد و یا خواص مدنظر را با استفاده از Object Destructuring به عنوان پارامتر دریافت نمود.
سپس میتوان لینکهای Login و Register را به صورت شرطی رندر کرد و نمایش داد:
در اینجا اگر شیء user تعریف شده باشد (یعنی کاربر، توکن ذخیره شدهای در local storage داشته باشد)، دیگر لینکهای login و register نمایش داده نمیشوند. به علاوه برای اعمال && به چند المان React، نیاز است آنها را داخل یک والد، مانند React.Fragment محصور کرد.
شبیه به همین حالت را برای هنگامیکه کاربر، تعریف شدهاست، جهت نمایش نام او و لینک به Logout، نیاز داریم:
user.DisplayName درج شدهی در اینجا، اطلاعات خودش را از payload توکن decode شدهی دریافتی از سرور، تامین میکند؛ با این خروجی:
فعلا تا پیش از پیاده سازی Logout، برای آزمایش آن، به کنسول توسعه دهندگان مرورگر مراجعه کرده و توکن ذخیره شدهی در ذیل قسمت application->storage را دستی حذف کنید. سپس صفحه را ریفرش کنید. اینبار لینکهای به Login و Register نمایان میشوند.
یک مشکل! در این حالت (زمانیکه توکن حذف شدهاست)، از طریق قسمت Login به برنامه وارد شوید. هرچند این قسمتها به درستی کار خود را انجام میدهند، اما هنوز در منوی بالای سایت، نام کاربری و لینک به Logout ظاهر نشدهاند. علت اینجا است که در کامپوننت App، کار دریافت توکن در متد componentDidMount انجام میشود و این متد نیز تنها یکبار در طول عمر برنامه فراخوانی میشود. برای رفع این مشکل به src\components\loginForm.jsx مراجعه کرده و بجای استفاده از history.push برای هدایت کاربر به صفحهی اصلی برنامه، نیاز خواهیم داشت تا کل برنامه را بارگذاری مجدد کنیم. یعنی بجای:
باید از سطر زیر استفاده کرد:
این سطر سبب full page reload برنامه شده و در نتیجه متد componentDidMount کامپوننت App، یکبار دیگر فراخوانی خواهد شد. شبیه به همین کار را در کامپوننت src\components\registerForm.jsx نیز باید انجام داد.
پیاده سازی Logout کاربر وارد شدهی به سیستم
برای logout کاربر تنها کافی است توکن او را از local storage حذف کنیم. به همین جهت مسیریابی جدید logout را که به صورت لینکی به NavBar اضافه کردیم:
به فایل src\App.js اضافه میکنیم.
البته برای اینکار نیاز است کامپوننت جدید src\components\logout.jsx را با محتوای زیر ایجاد کنیم:
که در متد componentDidMount آن، کار حذف توکن ذخیره شدهی در localStorage انجام شده و سپس کاربر را با یک full page reload، به ریشهی سایت هدایت میکنیم.
بهبود کیفیت کدهای نوشته شده
اگر به کامپوننت App دقت کنید، کلید token استفاده شدهی در آن، در چندین قسمت برنامه مانند login و logout، تکرار و پراکنده شدهاست. بنابراین بهتر است جزئیات پیاده سازی مرتبط با اعتبارسنجی کاربران، به ماژول مختص به آنها (src\services\authService.js) منتقل شود تا سایر قسمتهای برنامه، به صورت یکدستی از آن استفاده کنند و اگر در این بین نیاز به تغییری بود، فقط یک ماژول نیاز به تغییر، داشته باشد.
برای این منظور، ابتدا متد login قبلی را طوری تغییر میدهیم که کار ذخیره سازی توکن را نیز در authService.js انجام دهد:
سپس متد doSumbit کامپوننت src\components\loginForm.jsx، به صورت زیر ساده میشود:
همینکار را برای logout نیز در authService انجام داده:
و در ادامه متد componentDidMount کامپوننت Logout را برای استفادهی از آن، اصلاح میکنیم:
سپس متد componentDidMount کامپوننت App، به صورت زیر خلاصه خواهد شد:
جای دیگری که از localStorage استفاده شده، متد doSumbit کامپوننت ثبت نام کاربران است. این قسمت را نیز به صورت زیر به authService اضافه میکنیم:
سپس ابتدای متد doSumbit را برای استفادهی از آن به صورت زیر تغییر میدهیم:
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: sample-27-backend.zip و sample-27-frontend.zip
نگاهی به محتوای JSON Web Token تولیدی
اگر مطلب قسمت قبل را پیگیری کرده باشید، پس از لاگین، یک چنین خروجی را در کنسول توسعه دهندگان مرورگر میتوان مشاهده کرد که همان return Ok(new { access_token = jwt }) دریافتی از سمت سرور است:
اکنون این رشتهی طولانی را در حافظه کپی کرده و سپس به سایت https://jwt.io/#debugger-io مراجعه و در قسمت دیباگر آن، این رشتهی طولانی را paste میکنیم تا آنرا decode کند:
برای نمونه payload آن حاوی یک چنین اطلاعاتی است:
{ "jti": "b2921057-32a4-fbb2-0c18-5889c1ab8e70", "iss": "https://localhost:5001/", "iat": 1576402824, "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier": "1", "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name": "Vahid N.", "DisplayName": "Vahid N.", "http://schemas.microsoft.com/ws/2008/06/identity/claims/userdata": "1", "http://schemas.microsoft.com/ws/2008/06/identity/claims/role": "Admin", "nbf": 1576402824, "exp": 1576402944, "aud": "Any" }
استخراج اطلاعات کاربر وارد شدهی به سیستم، از JSON Web Token دریافتی
همانطور که در payload توکن دریافتی از سرور نیز مشخص است، اطلاعات ارزشمندی از کاربر، به همراه آن ارائه شدهاند و مزیت کار با آن، عدم نیاز به کوئری گرفتن مداوم از سرور و بانک اطلاعاتی، جهت دریافت مجدد این اطلاعات است. بنابراین اکنون در برنامهی React خود، قصد داریم مشابه کاری را که سایت jwt.io انجام میدهد، پیاده سازی کرده و به این اطلاعات دسترسی پیدا کنیم و برای مثال DisplayName را در Navbar نمایش دهیم. برای این منظور فایل app.js را گشوده و تغییرات زیر را به آن اعمال میکنیم:
- میخواهیم اطلاعات کاربر جاری را در state کامپوننت مرکزی App قرار دهیم. سپس زمانیکه کار رندر کامپوننت NavBar درج شدهی در متد رندر آن فرا میرسد، میتوان این اطلاعات کاربر را به صورت props به آن ارسال کرد؛ و یا به هر کامپوننت دیگری در component tree برنامه.
- بنابراین ابتدا کامپوننت تابعی بدون حالت App را تبدیل به یک کلاس کامپوننت استاندارد مشتق شدهی از کلاس پایهی Component میکنیم. اکنون میتوان state را نیز به آن اضافه کرد:
class App extends Component { state = {};
- برای decode کردن توکن، نیاز به نصب کتابخانهی زیر را داریم:
> npm install --save jwt-decode
import jwtDecode from "jwt-decode"; // ... class App extends Component { state = {}; componentDidMount() { try { const jwt = localStorage.getItem("token"); const currentUser = jwtDecode(jwt); console.log("currentUser", currentUser); this.setState({ currentUser }); } catch (ex) { console.log(ex); } }
- اکنون میتوان شیء currentUser را به صورت props، به کامپوننت NavBar ارسال کرد:
render() { return ( <React.Fragment> <ToastContainer /> <NavBar user={this.state.currentUser} /> <main className="container">
نمایش اطلاعات کاربر وارد شدهی به سیستم در NavBar
پس از ارسال شیء کاربر به صورت props به کامپوننت src\components\navBar.jsx، کدهای این کامپوننت را به صورت زیر جهت نمایش نام کاربر جاری وارد شدهی به سیستم تغییر میدهیم:
const NavBar = ({ user }) => {
سپس میتوان لینکهای Login و Register را به صورت شرطی رندر کرد و نمایش داد:
{!user && ( <React.Fragment> <NavLink className="nav-item nav-link" to="/login"> Login </NavLink> <NavLink className="nav-item nav-link" to="/register"> Register </NavLink> </React.Fragment> )}
شبیه به همین حالت را برای هنگامیکه کاربر، تعریف شدهاست، جهت نمایش نام او و لینک به Logout، نیاز داریم:
{user && ( <React.Fragment> <NavLink className="nav-item nav-link" to="/logout"> Logout </NavLink> <NavLink className="nav-item nav-link" to="/profile"> {user.DisplayName} </NavLink> </React.Fragment> )}
فعلا تا پیش از پیاده سازی Logout، برای آزمایش آن، به کنسول توسعه دهندگان مرورگر مراجعه کرده و توکن ذخیره شدهی در ذیل قسمت application->storage را دستی حذف کنید. سپس صفحه را ریفرش کنید. اینبار لینکهای به Login و Register نمایان میشوند.
یک مشکل! در این حالت (زمانیکه توکن حذف شدهاست)، از طریق قسمت Login به برنامه وارد شوید. هرچند این قسمتها به درستی کار خود را انجام میدهند، اما هنوز در منوی بالای سایت، نام کاربری و لینک به Logout ظاهر نشدهاند. علت اینجا است که در کامپوننت App، کار دریافت توکن در متد componentDidMount انجام میشود و این متد نیز تنها یکبار در طول عمر برنامه فراخوانی میشود. برای رفع این مشکل به src\components\loginForm.jsx مراجعه کرده و بجای استفاده از history.push برای هدایت کاربر به صفحهی اصلی برنامه، نیاز خواهیم داشت تا کل برنامه را بارگذاری مجدد کنیم. یعنی بجای:
this.props.history.push("/");
window.location = "/";
پیاده سازی Logout کاربر وارد شدهی به سیستم
برای logout کاربر تنها کافی است توکن او را از local storage حذف کنیم. به همین جهت مسیریابی جدید logout را که به صورت لینکی به NavBar اضافه کردیم:
<NavLink className="nav-item nav-link" to="/logout"> Logout </NavLink>
import Logout from "./components/logout"; // ... class App extends Component { render() { return ( // ... <Switch> // ... <Route path="/logout" component={Logout} />
import { Component } from "react"; class Logout extends Component { componentDidMount() { localStorage.removeItem("token"); window.location = "/"; } render() { return null; } } export default Logout;
بهبود کیفیت کدهای نوشته شده
اگر به کامپوننت App دقت کنید، کلید token استفاده شدهی در آن، در چندین قسمت برنامه مانند login و logout، تکرار و پراکنده شدهاست. بنابراین بهتر است جزئیات پیاده سازی مرتبط با اعتبارسنجی کاربران، به ماژول مختص به آنها (src\services\authService.js) منتقل شود تا سایر قسمتهای برنامه، به صورت یکدستی از آن استفاده کنند و اگر در این بین نیاز به تغییری بود، فقط یک ماژول نیاز به تغییر، داشته باشد.
برای این منظور، ابتدا متد login قبلی را طوری تغییر میدهیم که کار ذخیره سازی توکن را نیز در authService.js انجام دهد:
const tokenKey = "token"; export async function login(email, password) { const { data: { access_token } } = await http.post(apiEndpoint + "/login", { email, password }); console.log("JWT", access_token); localStorage.setItem(tokenKey, access_token); }
const { data } = this.state; await auth.login(data.username, data.password); window.location = "/";
همینکار را برای logout نیز در authService انجام داده:
export function logout() { localStorage.removeItem(tokenKey); }
import * as auth from "../services/authService"; class Logout extends Component { componentDidMount() { auth.logout();
منطق دریافت اطلاعات کاربر جاری نیز باید به authService منتقل شود؛ چون مسئولیت دریافت توکن و سپس decode آن، نباید به کامپوننت App واگذار شود:
import jwtDecode from "jwt-decode"; //... export function getCurrentUser() { try { const jwt = localStorage.getItem(tokenKey); const currentUser = jwtDecode(jwt); console.log("currentUser", currentUser); return currentUser; } catch (ex) { console.log(ex); return null; } }
import * as auth from "./services/authService"; class App extends Component { state = {}; componentDidMount() { const currentUser = auth.getCurrentUser(); this.setState({ currentUser }); }
جای دیگری که از localStorage استفاده شده، متد doSumbit کامپوننت ثبت نام کاربران است. این قسمت را نیز به صورت زیر به authService اضافه میکنیم:
export function loginWithJwt(jwt) { localStorage.setItem(tokenKey, jwt); }
import * as auth from "../services/authService"; // ... const response = await userService.register(this.state.data); auth.loginWithJwt(response.headers["x-auth-token"]);
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: sample-27-backend.zip و sample-27-frontend.zip
نظرات اشتراکها
سرویس دهی رایگان Google Maps از فردا خاتمه مییابد
سرویس Google Places هم همین سیاستها را دنبال خواهد کرد؟