به روز رسانی
در Angular 5، خاصیت ngOutletContext به ngTemplateOutletContext تغییر نام یافتهاست.
در Angular 5، خاصیت ngOutletContext به ngTemplateOutletContext تغییر نام یافتهاست.
به روز رسانی
در Angular 5، خاصیت ngOutletContext به ngTemplateOutletContext تغییر نام یافتهاست.
در Angular 5، خاصیت ngOutletContext به ngTemplateOutletContext تغییر نام یافتهاست.
در قسمت سوم، کار ورود به سیستم و امکان مشاهدهی صفحهی محافظت شدهی پس از لاگین را پیاده سازی کردیم. در این قسمت میخواهیم امکان دسترسی به مسیر http://localhost:4200/protectedPage را کنترل کنیم. تا اینجا اگر کاربر بدون لاگین کردن نیز این مسیر را درخواست کند، میتواند حداقل ساختار ابتدایی آنرا مشاهده کند که باید مدیریت شود و این مدیریت دسترسی میتواند بر اساس وضعیت لاگین کاربر و همچنین نقشهای او در سیستم باشد:
طراحی بخشهایی از این قسمت، از پروژهی «کنترل دسترسیها در Angular با استفاده از Ng2Permission» ایده گرفته شدهاند.
استخراج اطلاعات کاربر وارد شدهی به سیستم از توکن دسترسی او
یکی از روشهای دسترسی به اطلاعات کاربر در سمت کلاینت، مانند نقشهای او، تدارک متدی در سمت سرور و بازگشت Claims او به سمت کلاینت است:
اما با توجه به اینکه در زمان لاگین، نقشهای او (و سایر Claims دلخواه) نیز به توکن دسترسی نهایی اضافه میشوند، میتوان این کوئری گرفتن مدام از سرور را تبدیل به روش بسیار سریعتر استخراج آنها از توکنی که هم اکنون در کش مرورگر ذخیره شدهاست، کرد.
همچنین باید دقت داشت چون این توکن دارای امضای دیجیتال است، کوچکترین تغییری در آن در سمت کاربر، سبب برگشت خوردن خودکار درخواست ارسالی به سمت سرور میشود (یکی از مراحل اعتبارسنجی کاربر در سمت سرور، اعتبارسنجی توکن دریافتی (قسمت cfg.TokenValidationParameters) و همچنین بررسی خودکار امضای دیجیتال آن است). بنابراین نگرانی از این بابت وجود ندارد.
اگر اطلاعات کاربر در سمت سرور تغییر کنند، با اولین درخواست ارسالی به سمت سرور، رخداد OnTokenValidated وارد عمل شده و درخواست ارسالی را برگشت میزند (در مورد پیاده سازی سمت سرور این مورد، در مطلب «اعتبارسنجی مبتنی بر JWT در ASP.NET Core 2.0 بدون استفاده از سیستم Identity» بیشتر بحث شدهاست). در این حالت کاربر مجبور به لاگین مجدد خواهد شد که این مورد سبب به روز رسانی خودکار اطلاعات توکنهای ذخیره شدهی او در مرورگر نیز میشود.
اگر از قسمت دوم این سری بهخاطر داشته باشید، توکن decode شدهی برنامه، چنین شکلی را دارد:
به همین جهت متدی را برای تبدیل این اطلاعات به شیء کاربر، ایجاد خواهیم کرد و در سراسر برنامه از این اطلاعات آماده، برای تعیین دسترسی او به قسمتهای مختلف برنامهی سمت کلاینت، استفاده میکنیم.
برای این منظور اینترفیس src\app\core\models\auth-user.ts را به صورت ذیل ایجاد میکنیم:
پس از آن متد getAuthUser را جهت استخراج خواص ویژهی توکن دسترسی decode شدهی فوق، به صورت ذیل به سرویس Auth اضافه میکنیم:
کار با این متد بسیار سریع است و نیازی به رفت و برگشتی به سمت سرور ندارد؛ چون تمام اطلاعات استخراجی توسط آن هم اکنون در کش سریع مرورگر کلاینت موجود هستند. استفادهی از متد Object.freeze هم سبب read-only شدن این خروجی میشود.
همچنین در اینجا تمام نقشهای دریافتی، تبدیل به LowerCase شدهاند. با اینکار مقایسهی بعدی آنها با نقشهای درخواستی، حساس به بزرگی و کوچکی حروف نخواهد بود.
تعریف نقشهای دسترسی به مسیرهای مختلف سمت کلاینت
مرسوم است اطلاعات اضافی مرتبط با هر مسیر را به خاصیت data آن route انتساب میدهند. به همین جهت به فایل dashboard-routing.module.ts مراجعه کرده و نقشهای مجاز به دسترسی به مسیر protectedPage را به خاصیت data آن به صورت ذیل اضافه میکنیم:
که در اینجا ساختار اینترفیس AuthGuardPermission، در فایل جدید app\core\models\auth-guard-permission.ts به صورت ذیل تعریف شدهاست:
به این ترتیب هر قسمتی از برنامه که نیاز به اطلاعات سطوح دسترسی مسیری را داشت، ابتدا به دنبال route.data["permission"] خواهد گشت (کلیدی به نام permission در خاصیت data یک مسیر) و سپس اطلاعات آنرا بر اساس ساختار اینترفیس AuthGuardPermission، تحلیل میکند.
در اینجا تنها باید یکی از خواص permittedRoles (نقشهای مجاز به دسترسی/صدور دسترسی فقط برای این نقشهای مشخص، منهای مابقی) و یا deniedRoles (نقشهای غیرمجاز به دسترسی/دسترسی همهی نقشهای ممکن، منهای این نقشهای تعیین شده)، مقدار دهی شوند.
افزودن کامپوننت «دسترسی ندارید» به ماژول Authentication
در ادامه میخواهیم اگر کاربری به مسیری دسترسی نداشت، به صورت خودکار به صفحهی «دسترسی ندارید» هدایت شود. به همین جهت این کامپوننت را به صورت ذیل به ماژول authentication اضافه میکنیم:
با این خروجی که سبب درج خودکار آن در قسمت declaration فایل authentication.module نیز میشود:
سپس به فایل authentication-routing.module.ts مراجعه کرده و مسیریابی آنرا نیز اضافه میکنیم:
قالب access-denied.component.html را نیز به صورت ذیل تکمیل میکنیم:
که دکمهی Back آن به کمک سرویس Location، صورت ذیل پیاده سازی شدهاست:
در اینجا اگر کاربر به سیستم وارد نشده باشد، دکمهی لاگین نیز به او نمایش داده میشود. همچنین وجود "queryParamsHandling="merge در لینک مراجعهی به صفحهی لاگین، سبب خواهد شد تا query string موجود در صفحه نیز حفظ شود و به صفحهی لاگین انتقال پیدا کند. در صفحهی لاگین نیز جهت پردازش این نوع کوئری استرینگها، تمهیدات لازم درنظر گرفته شدهاند.
دکمهی back آن نیز توسط سرویس توکار Location واقع در مسیر angular/common@ پیاده سازی شدهاست.
ایجاد یک محافظ مسیر سمت کلاینت برای بررسی وضعیت کاربر جاری و همچنین نقشهای او
پس از تعریف متد getAuthUser و استخراج اطلاعات کاربر از توکن دسترسی دریافتی که شامل نقشهای او نیز میشود، اکنون میتوان متد بررسی این نقشها را نیز به سرویس Auth اضافه کرد:
متد some در جاوا اسکریپت شبیه به متد Any در C# LINQ عمل میکند. همچنین در مقایسهی صورت گرفته، با توجه به اینکه user.roles را پیشتر به LowerCase تبدیل کردیم، حساسیتی بین نقش Admin و admin و کلا کوچکی و بزرگی نام نقشها وجود ندارد.
اکنون در هر قسمتی از برنامه که نیاز به بررسی امکان دسترسی یک کاربر به نقش یا نقشهایی خاص وجود داشته باشد، میتوان AuthService را به سازندهی آن تزریق و سپس از متد فوق جهت بررسی نهایی، استفاده کرد.
در ادامه یک Route Guard جدید را در مسیر app\core\services\auth.guard.ts ایجاد میکنیم. کار آن بررسی خودکار امکان دسترسی به یک مسیر درخواستی است:
در اینجا در ابتدا وضعیت لاگین کاربر بررسی میگردد. این وضعیت نیز از طریق سرویس Auth که به سازندهی کلاس تزریق شدهاست، تامین میشود. اگر کاربر هنوز لاگین نکرده باشد، به صفحهی عدم دسترسی هدایت خواهد شد.
سپس خاصیت permission اطلاعات مسیر استخراج میشود. اگر چنین مقداری وجود نداشت، همینجا کار با موفقیت خاتمه پیدا میکند.
در آخر وضعیت دسترسی به نقشهای استخراجی deniedRoles و permittedRoles که از اطلاعات مسیر دریافت شدند، توسط متد isAuthUserInRoles سرویس Auth بررسی میشوند.
در متد showAccessDenied کار ارسال آدرس درخواستی (state.url) به صورت یک کوئری استرینگ (queryParams) با کلید returnUrl به صفحهی accessDenied صورت میگیرد. در این صفحه نیز دکمهی لاگین به همراه "queryParamsHandling="merge است. یعنی کامپوننت لاگین برنامه، کوئری استرینگ returnUrl را دریافت میکند:
و پس از لاگین موفق، در صورت وجود این کوئری استرینگ، هدایت خودکار کاربر، به مسیر returnUrl پیشین صورت خواهد گرفت:
محل معرفی این AuthGuard جدید که در حقیقت یک سرویس است، در ماژول Core، در قسمت providers آن، به صورت ذیل میباشد:
در آخر برای اعمال این Guard جدید، به فایل dashboard-routing.module.ts مراجعه کرده و خاصیت canActivate را مقدار دهی میکنیم:
به این ترتیب با درخواست این مسیر، پیش از فعالسازی و نمایش آن، توسط AuthGuard معرفی شدهی به آن، کار بررسی وضعیت کاربر و نقشهای او که از خاصیت permission خاصیت data دریافت میشوند، صورت گرفته و اگر عملیات تعیین اعتبار اطلاعات با موفقیت به پایان رسید، آنگاه کاربر مجوز دسترسی به این قسمت از برنامه را خواهد یافت.
اگر قصد آزمایش آنرا داشتید، فقط کافی است بجای نقش Admin، مثلا Admin1 را در permittedRoles مقدار دهی کنید، تا صفحهی access denied را در صورت درخواست مسیر protectedPage، بتوان مشاهده کرد.
کدهای کامل این سری را از اینجا میتوانید دریافت کنید.
برای اجرای آن فرض بر این است که پیشتر Angular CLI را نصب کردهاید. سپس از طریق خط فرمان به ریشهی پروژهی ASPNETCore2JwtAuthentication.AngularClient وارد شده و دستور npm install را صادر کنید تا وابستگیهای آن دریافت و نصب شوند. در آخر با اجرای دستور ng serve -o، برنامه ساخته شده و در مرورگر پیش فرض سیستم نمایش داده خواهد شد (و یا همان اجرای فایل ng-serve.bat). همچنین باید به پوشهی ASPNETCore2JwtAuthentication.WebApp نیز مراجعه کرده و فایل dotnet_run.bat را اجرا کنید، تا توکن سرور برنامه نیز فعال شود.
طراحی بخشهایی از این قسمت، از پروژهی «کنترل دسترسیها در Angular با استفاده از Ng2Permission» ایده گرفته شدهاند.
استخراج اطلاعات کاربر وارد شدهی به سیستم از توکن دسترسی او
یکی از روشهای دسترسی به اطلاعات کاربر در سمت کلاینت، مانند نقشهای او، تدارک متدی در سمت سرور و بازگشت Claims او به سمت کلاینت است:
public IActionResult Get() { var user = this.User.Identity as ClaimsIdentity; var config = new { userName = user.Name, roles = user.Claims.Where(x => x.Type == ClaimTypes.Role).Select(x => x.Value).ToList() }; return Ok(config); }
همچنین باید دقت داشت چون این توکن دارای امضای دیجیتال است، کوچکترین تغییری در آن در سمت کاربر، سبب برگشت خوردن خودکار درخواست ارسالی به سمت سرور میشود (یکی از مراحل اعتبارسنجی کاربر در سمت سرور، اعتبارسنجی توکن دریافتی (قسمت cfg.TokenValidationParameters) و همچنین بررسی خودکار امضای دیجیتال آن است). بنابراین نگرانی از این بابت وجود ندارد.
اگر اطلاعات کاربر در سمت سرور تغییر کنند، با اولین درخواست ارسالی به سمت سرور، رخداد OnTokenValidated وارد عمل شده و درخواست ارسالی را برگشت میزند (در مورد پیاده سازی سمت سرور این مورد، در مطلب «اعتبارسنجی مبتنی بر JWT در ASP.NET Core 2.0 بدون استفاده از سیستم Identity» بیشتر بحث شدهاست). در این حالت کاربر مجبور به لاگین مجدد خواهد شد که این مورد سبب به روز رسانی خودکار اطلاعات توکنهای ذخیره شدهی او در مرورگر نیز میشود.
اگر از قسمت دوم این سری بهخاطر داشته باشید، توکن decode شدهی برنامه، چنین شکلی را دارد:
{ "jti": "d1272eb5-1061-45bd-9209-3ccbc6ddcf0a", "iss": "http://localhost/", "iat": 1513070340, "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier": "1", "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name": "Vahid", "DisplayName": "وحید", "http://schemas.microsoft.com/ws/2008/06/identity/claims/serialnumber": "709b64868a1d4d108ee58369f5c3c1f3", "http://schemas.microsoft.com/ws/2008/06/identity/claims/userdata": "1", "http://schemas.microsoft.com/ws/2008/06/identity/claims/role": [ "Admin", "User" ], "nbf": 1513070340, "exp": 1513070460, "aud": "Any" }
برای این منظور اینترفیس src\app\core\models\auth-user.ts را به صورت ذیل ایجاد میکنیم:
export interface AuthUser { userId: string; userName: string; displayName: string; roles: string[]; }
getAuthUser(): AuthUser { if (!this.isLoggedIn()) { return null; } const decodedToken = this.getDecodedAccessToken(); let roles = decodedToken["http://schemas.microsoft.com/ws/2008/06/identity/claims/role"]; if (roles) { roles = roles.map(role => role.toLowerCase()); } return Object.freeze({ userId: decodedToken["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier"], userName: decodedToken["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name"], displayName: decodedToken["DisplayName"], roles: roles }); }
همچنین در اینجا تمام نقشهای دریافتی، تبدیل به LowerCase شدهاند. با اینکار مقایسهی بعدی آنها با نقشهای درخواستی، حساس به بزرگی و کوچکی حروف نخواهد بود.
تعریف نقشهای دسترسی به مسیرهای مختلف سمت کلاینت
مرسوم است اطلاعات اضافی مرتبط با هر مسیر را به خاصیت data آن route انتساب میدهند. به همین جهت به فایل dashboard-routing.module.ts مراجعه کرده و نقشهای مجاز به دسترسی به مسیر protectedPage را به خاصیت data آن به صورت ذیل اضافه میکنیم:
import { ProtectedPageComponent } from "./protected-page/protected-page.component"; import { AuthGuardPermission } from "../core/models/auth-guard-permission"; const routes: Routes = [ { path: "protectedPage", component: ProtectedPageComponent, data: { permission: { permittedRoles: ["Admin"], deniedRoles: null } as AuthGuardPermission } } ];
export interface AuthGuardPermission { permittedRoles?: string[]; deniedRoles?: string[]; }
در اینجا تنها باید یکی از خواص permittedRoles (نقشهای مجاز به دسترسی/صدور دسترسی فقط برای این نقشهای مشخص، منهای مابقی) و یا deniedRoles (نقشهای غیرمجاز به دسترسی/دسترسی همهی نقشهای ممکن، منهای این نقشهای تعیین شده)، مقدار دهی شوند.
افزودن کامپوننت «دسترسی ندارید» به ماژول Authentication
در ادامه میخواهیم اگر کاربری به مسیری دسترسی نداشت، به صورت خودکار به صفحهی «دسترسی ندارید» هدایت شود. به همین جهت این کامپوننت را به صورت ذیل به ماژول authentication اضافه میکنیم:
>ng g c Authentication/AccessDenied
AccessDenied create src/app/Authentication/access-denied/access-denied.component.html (32 bytes) create src/app/Authentication/access-denied/access-denied.component.ts (296 bytes) create src/app/Authentication/access-denied/access-denied.component.css (0 bytes) update src/app/Authentication/authentication.module.ts (550 bytes)
import { LoginComponent } from "./login/login.component"; import { AccessDeniedComponent } from "./access-denied/access-denied.component"; const routes: Routes = [ { path: "login", component: LoginComponent }, { path: "accessDenied", component: AccessDeniedComponent } ];
<h1 class="text-danger"> <span class="glyphicon glyphicon-ban-circle"></span> Access Denied </h1> <p>Sorry! You don't have access to this page.</p> <button class="btn btn-default" (click)="goBack()"> <span class="glyphicon glyphicon-arrow-left"></span> Back </button> <button *ngIf="!isAuthenticated" class="btn btn-success" [routerLink]="['/login']" queryParamsHandling="merge"> Login </button>
import { Component, OnInit } from "@angular/core"; import { Location } from "@angular/common"; import { AuthService } from "../../core/services/auth.service"; @Component({ selector: "app-access-denied", templateUrl: "./access-denied.component.html", styleUrls: ["./access-denied.component.css"] }) export class AccessDeniedComponent implements OnInit { isAuthenticated = false; constructor( private location: Location, private authService: AuthService ) { } ngOnInit() { this.isAuthenticated = this.authService.isLoggedIn(); } goBack() { this.location.back(); // <-- go back to previous location on cancel } }
در اینجا اگر کاربر به سیستم وارد نشده باشد، دکمهی لاگین نیز به او نمایش داده میشود. همچنین وجود "queryParamsHandling="merge در لینک مراجعهی به صفحهی لاگین، سبب خواهد شد تا query string موجود در صفحه نیز حفظ شود و به صفحهی لاگین انتقال پیدا کند. در صفحهی لاگین نیز جهت پردازش این نوع کوئری استرینگها، تمهیدات لازم درنظر گرفته شدهاند.
دکمهی back آن نیز توسط سرویس توکار Location واقع در مسیر angular/common@ پیاده سازی شدهاست.
ایجاد یک محافظ مسیر سمت کلاینت برای بررسی وضعیت کاربر جاری و همچنین نقشهای او
پس از تعریف متد getAuthUser و استخراج اطلاعات کاربر از توکن دسترسی دریافتی که شامل نقشهای او نیز میشود، اکنون میتوان متد بررسی این نقشها را نیز به سرویس Auth اضافه کرد:
isAuthUserInRoles(requiredRoles: string[]): boolean { const user = this.getAuthUser(); if (!user || !user.roles) { return false; } return requiredRoles.some(requiredRole => user.roles.indexOf(requiredRole.toLowerCase()) >= 0); } isAuthUserInRole(requiredRole: string): boolean { return this.isAuthUserInRoles([requiredRole]); }
اکنون در هر قسمتی از برنامه که نیاز به بررسی امکان دسترسی یک کاربر به نقش یا نقشهایی خاص وجود داشته باشد، میتوان AuthService را به سازندهی آن تزریق و سپس از متد فوق جهت بررسی نهایی، استفاده کرد.
در ادامه یک Route Guard جدید را در مسیر app\core\services\auth.guard.ts ایجاد میکنیم. کار آن بررسی خودکار امکان دسترسی به یک مسیر درخواستی است:
import { Injectable } from "@angular/core"; import { CanActivate, Router, ActivatedRouteSnapshot, RouterStateSnapshot } from "@angular/router"; import { AuthService } from "./auth.service"; import { AuthGuardPermission } from "../models/auth-guard-permission"; @Injectable() export class AuthGuard implements CanActivate { constructor(private authService: AuthService, private router: Router) { } canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) { if (!this.authService.isLoggedIn()) { this.showAccessDenied(state); return false; } const permissionData = route.data["permission"] as AuthGuardPermission; if (!permissionData) { return true; } if (Array.isArray(permissionData.deniedRoles) && Array.isArray(permissionData.permittedRoles)) { throw new Error("Don't set both 'deniedRoles' and 'permittedRoles' in route data."); } if (Array.isArray(permissionData.permittedRoles)) { const isInRole = this.authService.isAuthUserInRoles(permissionData.permittedRoles); if (isInRole) { return true; } this.showAccessDenied(state); return false; } if (Array.isArray(permissionData.deniedRoles)) { const isInRole = this.authService.isAuthUserInRoles(permissionData.deniedRoles); if (!isInRole) { return true; } this.showAccessDenied(state); return false; } } private showAccessDenied(state: RouterStateSnapshot) { this.router.navigate(["/accessDenied"], { queryParams: { returnUrl: state.url } }); } }
سپس خاصیت permission اطلاعات مسیر استخراج میشود. اگر چنین مقداری وجود نداشت، همینجا کار با موفقیت خاتمه پیدا میکند.
در آخر وضعیت دسترسی به نقشهای استخراجی deniedRoles و permittedRoles که از اطلاعات مسیر دریافت شدند، توسط متد isAuthUserInRoles سرویس Auth بررسی میشوند.
در متد showAccessDenied کار ارسال آدرس درخواستی (state.url) به صورت یک کوئری استرینگ (queryParams) با کلید returnUrl به صفحهی accessDenied صورت میگیرد. در این صفحه نیز دکمهی لاگین به همراه "queryParamsHandling="merge است. یعنی کامپوننت لاگین برنامه، کوئری استرینگ returnUrl را دریافت میکند:
this.returnUrl = this.route.snapshot.queryParams["returnUrl"];
if (this.returnUrl) { this.router.navigate([this.returnUrl]); } else { this.router.navigate(["/protectedPage"]); }
محل معرفی این AuthGuard جدید که در حقیقت یک سرویس است، در ماژول Core، در قسمت providers آن، به صورت ذیل میباشد:
import { AuthGuard } from "./services/auth.guard"; @NgModule({ providers: [ AuthGuard ] }) export class CoreModule {}
import { ProtectedPageComponent } from "./protected-page/protected-page.component"; import { AuthGuardPermission } from "../core/models/auth-guard-permission"; import { AuthGuard } from "../core/services/auth.guard"; const routes: Routes = [ { path: "protectedPage", component: ProtectedPageComponent, data: { permission: { permittedRoles: ["Admin"], deniedRoles: null } as AuthGuardPermission }, canActivate: [AuthGuard] } ];
اگر قصد آزمایش آنرا داشتید، فقط کافی است بجای نقش Admin، مثلا Admin1 را در permittedRoles مقدار دهی کنید، تا صفحهی access denied را در صورت درخواست مسیر protectedPage، بتوان مشاهده کرد.
کدهای کامل این سری را از اینجا میتوانید دریافت کنید.
برای اجرای آن فرض بر این است که پیشتر Angular CLI را نصب کردهاید. سپس از طریق خط فرمان به ریشهی پروژهی ASPNETCore2JwtAuthentication.AngularClient وارد شده و دستور npm install را صادر کنید تا وابستگیهای آن دریافت و نصب شوند. در آخر با اجرای دستور ng serve -o، برنامه ساخته شده و در مرورگر پیش فرض سیستم نمایش داده خواهد شد (و یا همان اجرای فایل ng-serve.bat). همچنین باید به پوشهی ASPNETCore2JwtAuthentication.WebApp نیز مراجعه کرده و فایل dotnet_run.bat را اجرا کنید، تا توکن سرور برنامه نیز فعال شود.
اشتراکها
ReSharper 2017.2 منتشر شد
ReSharper 2017.2 understands .NET Core 2.0and C# 7.1, gets better at C# 7.0,provides more code inspections and context actions including new IEnumerable
inspections, delivers many powerful navigation and search improvements, brings new C# typing assists, levels up support for TypeScript, JavaScript, JSON and Angular.
Angular یکی از محبوبترین فریم ورکها، برای ساختن برنامههای تک صفحهای میباشد. اگرچه گفتیم تک صفحهای، اما ضرورتا منظور این نیست که پروژهی شما تنها شامل یک صفحه باشد. شما میتوانید با Angular یک وب سایت را با هزاران صفحه نیز ایجاد کنید. با این حال وقتی بحث از کارآیی باشد، بهتر است همیشه در رابطه با تعدادی فکر کنیم که باعث میشوند پروژه، نرم و سریع اجرا شود.
توجه کنید که اگر خروجی برنامه شما مستقیما در dist میباشد، به صورت بالا عمل کنید؛ ولی اگر خروجی برنامه شما در پوشهی dist/YourApplicationName باشد، آن را به حالت زیر ویرایش کنید:
دستور بالا یک بیلد را برای پروژه برای حالت ارائهی نهایی (Production) همراه با آمارهایی در رابطه با هر باندل ایجاد میکند. در اینجا میتوانیم ببینیم که چه ماژولها/فایلهایی در هر باندل استفاده شدهاست. این مورد فوق العاده کمک میکند. هم چنین میتوانیم به صورت بصری ببینیم که چه چیزهایی در هر ماژول شامل شدهاند که بهتر بود آنجا نباشند:
#1 Optimize main bundle with Lazy Loading
وقتی که پروژه را برای حالت ارائهی نهایی (Production ) بدون در نظر گرفتن Lazy Load، بیلد میکنیم احتمالا فایلهای تولید شده زیر را در پوشهی dist خواهیم دید:
- polyfills.js : برای ساختن برنامههای سازگار با انواع مرورگرها میباشد. به دلیل اینکه وقتی کدها را با جدیدترین ویژگیها مینویسیم، ممکن است که همهی مرورگرها توانایی پشتیبانی از آن ویژگیها را نداشته باشند.
- scripts.js : شامل اسکریپتهایی میباشد که در بخش scripts، در فایل angular.json تعریف کردهایم.
- webpack loader : runtime.js میباشد. این فایل شامل webpack utilitiesهایی میباشد که برای بارگذاری دیگر فایلها مورد نیاز است.
- styles.css : شامل style هایی است که در بخش styles، در فایل angular.json تعریف کردهایم.
- main.js : شامل تمامی کدها از قبیل کامپوننتها ( کدهای مربوط به css ، html ، ts) ، دایرکتیوها، pipes و سرویسها و ماژولهای ایمپورت شده از جمله third partyهای میباشد.
همانطور که متوجه هستید، فایل main.js در طول زمان بزرگتر و بزرگتر خواهد شد که این یک مشکل است. در این حالت برای مشاهدهی وب سایت، مرورگر نیاز دارد که فایل main.js را دانلود کرده و سپس در صفحه، آن را اجرا و رندر کند که این یک چالش برای کاربران موبایل با اینترنت ضعیف و هم چنین کاربران دسکتاپ میباشد.
آسانترین راه برای برطرف کردن این مشکل این است که پروژه را به چندین ماژول lazy load، تقسیم کنیم. وقتی که از lazy moduleها استفاده میکنیم، انگیولار chunk مربوط به آن را تولید میکند که در ابتدا بارگذاری نخواهد شد؛ مگر اینکه مورد نیاز باشند (معمولا با فعال سازی یک مسیر اتفاق میافتد).
وقتی که از lazy loading استفاده میکنیم، بعد از فرایند build، فایلهای جدیدی تولید خواهند شد، مثل 4.386205799sfghe4.js که یک چانک از یک lazy module میباشد و در زمان راه اندازی صفحه اول اجرا نخواهد شد که نتیجهی آن داشتن فایل main.js ای با حجم کم میباشد. بنابراین بارگذاری صفحهی اول، خیلی سریع انجام خواهد شد.
با این حال، بارگذاری هر قسمت میتواند بر روی کارآیی تاثیر داشته باشد (بارگذاری ممکن است کند باشد). خوشبختانه انگیولار یک راه را برای برطرف کردن این مشکل فراهم کرده است ( PreloadingStrategy ) . بعد از اینکه فایل main.js به صورت کامل بارگذاری و اجرا شد، کار پیش واکشی ماژولها را انجام میدهد و زمانیکه کاربری مسیری را درخواست میدهد، آْن مسیر را بدون درنگ مشاهده خواهد کرد.
مطالعه بیشتر : مسیریابی در Angular - قسمت دهم - Lazy loading
#2 Debug bundles with Webpack Bundle Analyzer
حتی ممکن است بعد از تقسیم کردن منطق برنامه به چند ماژول lazy load، شما یک فایل main.js بزرگ داشته باشید. در این حالت میتوانید بهینه سازی بیشتری را با استفاده از Webpack Bundle Analyzer انجام دهید. با استفاده از این پکیج میتوانید آمارهایی را در رابطه با هر باندل داشته باشید. در ابتدا با استفاده از دستور زیر پکیج آنرا نصب کنید:
npm install --save-dev webpack-bundle-analyzer
سپس فایل package.json را باز کرده و در بخش scripts، مدخل زیر را اضافه کنید:
"bundle-report": "ng build --prod --stats-json && webpack-bundle-analyzer dist/stats.json"
"bundle-report": "ng build --prod --stats-json && webpack-bundle-analyzer dist/YourApplicationName/stats.json"
در نهایت دستور زیر را اجرا کنید:
npm run bundle-report
#3 Use Lazy Loading for images that are not visible in page
وقتی که صفحه اصلی را در اولین بار، بارگذاری میکنیم، میتوانیم تصاویری را داشته باشیم که در صفحهی نمایش، نمایان نباشند (منظور viewport میباشد) و کاربر برای دیدن آن تصاویر باید صفحه را به پایین اسکرول کند. با این وجود وقتی که صفحه بارگذاری میشود، تصاویر هم بلافاصله دانلود میشوند. اگر تعداد تصاویر زیاد باشند این مورد میتواند بر روی performance تاثیر منفی داشته باشد. برای برطرف کردن این مشکل میتوان از lazy loading تصاویر استفاده کرد. در این حالت تصاویر زمانی بارگذاری میشوند که کاربر به آنها میرسد. یک JavaScript API به نام Intersection Observer API وجود دارد که باعث میشود پیاده سازی lazy load خیلی آسان شود. علاوه بر این میتوان یک دایرکتیو را با قابلیت استفاده مجدد طراحی کرد ( lazy loading برای تصاویر با استفاده از Intersection Observer).
#4 Use virtual scrolling for large lists
با استفاده از virtual scrolling میتوان المنتها را در Dom بر اساس بخشهای قابل مشاهدهای از یک لیست، Load یا unload کرد که برنامه را فوق العاده سریع میکند.
#5 Use FOUT instead of FOIT for fonts
در بیشتر وب سایتها میتوان فونتهای سفارشی زیبایی را به جای فونتهای معمول دید. با این حال برای استفاده از فونتهای فراهم شده توسط سرویسهای دیگر لازم است که مرورگر آنها را دانلود و parse کند. اگر از فونتهای سفارشی استفاده کنیم، مثل Google Fonts، چه اتفاقی میافتد؟ در اینجا دو سناریو وجود دارد:
- در این حالت مرورگر منتظر میماند تا فونت دانلود شود و آن را parse کند و تنها بعد از آن متن را بر روی صفحه نمایش میدهد. متن روی صفحه تا زمانیکه فونت دانلود و parse نشده باشد، قابل مشاهده نیست؛ این FOIT است (Flash of invisible text) .
- مرورگر در ابتدا متن را با استفاده از فونت معمول نمایش میدهد و بعد از آن سعی میکند که ساختارهای فونت خارجی را دریافت کند. وقتی که دانلود انجام شد و سپس آن را parse کرد، فونت سفارشی دانلود شده، با فونت معمول جایگزین خواهد شد؛ این FOUT است ( Flash of unstyled text ).
بیشتر مرورگرها از FOIT استفاده میکنند و تنها Internet Explorer از FOUT استفاده میکند. برای برطرف کردن این مشکل میتوان از توصیفگر font-display استفاده کرد و به مرورگر بگوییم که میخواهیم در ابتدا متن را با فونت معمول نمایش دهیم و جایگزینی فونت، بعد از دانلود باشد (بیشتر).
در حین import ماژولهای TypeScript ایی پس از مدتی به یک چنین کدهایی خواهیم رسید:
در این حالت، در یک پوشه برای import ماژولی مشخص، چنین import ایی را خواهیم داشت:
و در پوشهی تو در توی دیگری، این تعریف به صورت زیر تغییر میکند:
و در آخر برنامه پر میشود از مسیرهای نسبی ‘../../../..’ مانند. به این ترتیب جابجا کردن فایلها و Refactoring آنها، مشکل میشود.
خوشبختانه کامپایلر TypeScript به همراه تنظیمات baseUrl و paths است که توسط آنها میتوان این مسیرهای نسبی را به مسیرهای مطلق تبدیل کرد و در این حالت اهمیتی ندارد که ماژول مدنظر از چه سطحی و درون چه پوشهی تو در تویی فراخوانی میشود، این مسیر import همواره ثابت خواهد بود.
تنظیمات فایل tsconfig.json برای معرفی مسیرهای مطلق ماژولها
فرض کنید میخواهید از یکی از سرویسهای Core Module استفاده کنید:
بسته به عمق پوشهی استفاده کننده، به یک چنین تعریفی خواهید رسید:
برای بهبود این وضعیت، فایل tsconfig.json و یا همان تنظیمات کامپایلر TypeScript را به نحو ذیل تکمیل میکنیم:
در اینجا baseUrl به پوشهی src برنامه اشاره میکند و مسیرهای بعدی بر این اساس محاسبه میشوند. در ادامه در قسمت paths، ابتدا یک نام مستعار ذکر میشود و سپس مسیری که ارائه دهندهی آن است. ذکر @ در اینجا اختیاری است؛ اما ذکر */ها اجباری است.
پس از این تغییرات، اکنون افزونهی پیشنهاد دهندهی imports، هر دو حالت استفادهی از مسیر مطلق بر اساس نام مستعار تعریف شده:
و یا استفادهی از مسیر نسبی را نیز پیشنهاد میدهد:
برای مثال اگر دقت کرده باشید، روش import اجزای خود Angular به صورت زیر است:
علت اینجا است که Angular از تعریف مشابهی به صورت زیر برای نگاشت پوشهی node_modules آن به angular@ استفاده میکند:
و ذکر @ اختیاری هم از اینجا اقتباس شدهاست.
یک نکتهی مهم: تنظیمات فوق بدون تنظیمات معادل webpack ناقص هستند
اگر از برنامهی Angular CLI استفاده میکنید، تنظیمات ذکر شده، تا همینجا به پایان میرسند؛ چون webpack جزئی از Angular CLI است و تنظیمات پیش فرض آن، قسمت baseUrl و paths فایل tsconfig.json را به صورت خودکار پردازش میکند. اما اگر از TypeScript در محیطهای دیگری استفاده میکنید که از webpack به صورت مجزایی استفاده میکنند، نیاز است قسمت resolve.alias فایل webpack.config.js را نیز جهت معرفی این تغییرات، اصلاح کنید. از این جهت که کامپایلر TypeScript این مسیرهای مطلق را در حین تولید فایلهای نهایی JavaScript ایی معادل، به مسیرهای نسبی بازنویسی نمیکند و در این حالت webpack نمیداند که چطور باید این ماژولها را یافته و یکی کند.
کوتاه کردن مسیرهای مطلق با معرفی فایل ویژهی index.ts
تا اینجا بجای ذکر مسیر
به مسیر مطلق زیر رسیدیم (صرفنظر از محل قرارگیری ماژولی که قرار است آنرا import کند):
این را هم میخواهیم به صورت زیر کوتاهتر کنیم:
یعنی فقط app/core@ را ذکر کنیم.
برای اینکار نیاز است فایل ویژهای را به نام index.ts، در ریشهی پوشهی core ایجاد کنیم (src\app\core\index.ts)، با این محتوا:
در اینجا تمام ماژولهایی که توسط Core Module ارائه میشوند را یکبار export میکنیم.
برای نمونه اگر به پوشهی node_modules\@angular خود مجموعهی Angular هم مراجعه کنید، هر پوشهی src آن به همراه یک فایل index.d.ts شبیه تعاریف فوق نیز هست.
پس از افزودن فایل index.ts به ریشهی پوشهی مدنظر، اکنون در لیست پیشنهادات، ذکر app/core@ تنها نیز ظاهر شده و استفادهی از آن مجاز است:
import { SpecialCollection } from "../../special"; import { LoginComponent } from "../login"; import { TextUtils } from ".../../utils/text"; import { Router } from "../../../core/router";
import { Data } from '../data';
import { Data } from '../../../data';
خوشبختانه کامپایلر TypeScript به همراه تنظیمات baseUrl و paths است که توسط آنها میتوان این مسیرهای نسبی را به مسیرهای مطلق تبدیل کرد و در این حالت اهمیتی ندارد که ماژول مدنظر از چه سطحی و درون چه پوشهی تو در تویی فراخوانی میشود، این مسیر import همواره ثابت خواهد بود.
تنظیمات فایل tsconfig.json برای معرفی مسیرهای مطلق ماژولها
فرض کنید میخواهید از یکی از سرویسهای Core Module استفاده کنید:
بسته به عمق پوشهی استفاده کننده، به یک چنین تعریفی خواهید رسید:
import { BrowserStorageService } from "./../../core/browser-storage.service";
{ "compilerOptions": { "baseUrl": "src", "paths": { "@app/*": [ "app/*" ], "@app/core/*": [ "app/core/*" ], "@app/shared/*": [ "app/shared/*" ], "@env/*": [ "environments/*" ] } } }
پس از این تغییرات، اکنون افزونهی پیشنهاد دهندهی imports، هر دو حالت استفادهی از مسیر مطلق بر اساس نام مستعار تعریف شده:
import { BrowserStorageService } from "@app/core/browser-storage.service";
import { BrowserStorageService } from "./../../core/browser-storage.service";
برای مثال اگر دقت کرده باشید، روش import اجزای خود Angular به صورت زیر است:
import { Component } from '@angular/core';
"paths": { "@angular/*": ["node_modules/@angular/*"] },
یک نکتهی مهم: تنظیمات فوق بدون تنظیمات معادل webpack ناقص هستند
اگر از برنامهی Angular CLI استفاده میکنید، تنظیمات ذکر شده، تا همینجا به پایان میرسند؛ چون webpack جزئی از Angular CLI است و تنظیمات پیش فرض آن، قسمت baseUrl و paths فایل tsconfig.json را به صورت خودکار پردازش میکند. اما اگر از TypeScript در محیطهای دیگری استفاده میکنید که از webpack به صورت مجزایی استفاده میکنند، نیاز است قسمت resolve.alias فایل webpack.config.js را نیز جهت معرفی این تغییرات، اصلاح کنید. از این جهت که کامپایلر TypeScript این مسیرهای مطلق را در حین تولید فایلهای نهایی JavaScript ایی معادل، به مسیرهای نسبی بازنویسی نمیکند و در این حالت webpack نمیداند که چطور باید این ماژولها را یافته و یکی کند.
resolve: { extensions: ['*', '.js', '.ts'], modules: [ rootDir, path.join(rootDir, 'node_modules') ], alias: { '@app': 'src/app' } },
کوتاه کردن مسیرهای مطلق با معرفی فایل ویژهی index.ts
تا اینجا بجای ذکر مسیر
import { BrowserStorageService } from "./../../core/browser-storage.service";
import { BrowserStorageService } from "@app/core/browser-storage.service";
import { BrowserStorageService } from "@app/core";
برای اینکار نیاز است فایل ویژهای را به نام index.ts، در ریشهی پوشهی core ایجاد کنیم (src\app\core\index.ts)، با این محتوا:
export * from "./browser-storage.service"; export * from "./app-config.service"; export * from "./seo-service";
برای نمونه اگر به پوشهی node_modules\@angular خود مجموعهی Angular هم مراجعه کنید، هر پوشهی src آن به همراه یک فایل index.d.ts شبیه تعاریف فوق نیز هست.
پس از افزودن فایل index.ts به ریشهی پوشهی مدنظر، اکنون در لیست پیشنهادات، ذکر app/core@ تنها نیز ظاهر شده و استفادهی از آن مجاز است:
قابلیتهای جدید از جمله پشتیبانی از فایلهای sass و json