Angular 5 و ASP.NET Core
نشرت: 2022-03-11لقد كنت أفكر في كتابة منشور مدونة منذ أن قتل الإصدار الأول من Angular Microsoft عمليًا من جانب العميل. أصبحت تقنيات مثل ASP.Net و Web Forms و MVC Razor قديمة ، وتم استبدالها بإطار عمل JavaScript ليس بالضبط Microsoft. ومع ذلك ، منذ الإصدار الثاني من Angular ، تعمل Microsoft و Google معًا لإنشاء Angular 2 ، وهذا هو الوقت الذي بدأت فيه تقنيتا المفضلان في العمل معًا.
في هذه المدونة ، أرغب في مساعدة الناس على إنشاء أفضل هندسة تجمع بين هذين العالمين. هل أنت جاهز؟ ها نحن!
حول العمارة
ستقوم ببناء عميل Angular 5 الذي يستهلك خدمة RESTful Web API Core 2.
جانب العميل:
- الزاوي 5
- الزاوي CLI
- مادة الزاوي
جانب الخادم:
- NET C # Web API Core 2
- تبعيات الحقن
- مصادقة JWT
- رمز إطار الكيان أولاً
- خادم قاعدة البيانات
ملحوظة
في منشور المدونة هذا ، نفترض أن القارئ لديه بالفعل معرفة أساسية بـ TypeScript ، والوحدات النمطية Angular ، والمكونات ، والاستيراد / التصدير. الهدف من هذا المنشور هو إنشاء بنية جيدة تسمح للشفرة بالنمو بمرور الوقت. |
ماذا تحتاج؟
لنبدأ باختيار IDE. بالطبع ، هذا هو تفضيلي فقط ، ويمكنك استخدام الشخص الذي تشعر براحة أكبر معه. في حالتي ، سأستخدم Visual Studio Code و Visual Studio 2017.
لماذا اثنين من IDEs مختلفة؟ منذ أن أنشأت Microsoft Visual Studio Code للواجهة الأمامية ، لا يمكنني التوقف عن استخدام IDE هذا. على أي حال ، سنرى أيضًا كيفية دمج Angular 5 داخل مشروع الحل ، وهذا سيساعدك إذا كنت من النوع الذي يفضل المطور تصحيح أخطاء الواجهة الخلفية والأمامية باستخدام F5 واحد فقط.
حول النهاية الخلفية ، يمكنك تثبيت أحدث إصدار من Visual Studio 2017 والذي يحتوي على إصدار مجاني للمطورين ولكنه مكتمل للغاية: المجتمع.
إذن ، إليك قائمة الأشياء التي نحتاج إلى تثبيتها لهذا البرنامج التعليمي:
- كود الاستوديو المرئي
- مجتمع Visual Studio 2017 (أو أي)
- Node.js v8.10.0
- SQL Server 2017
ملحوظة
تحقق من أنك تقوم بتشغيل Node 6.9.x و npm 3.xx على الأقل عن طريق تشغيل node -v و npm -v في نافذة طرفية أو وحدة تحكم. تنتج الإصدارات الأقدم أخطاءً ، لكن الإصدارات الأحدث جيدة. |
الواجهة الأمامية
بداية سريعة
فلتبدأ المرح! أول شيء يتعين علينا القيام به هو تثبيت Angular CLI بشكل عام ، لذا افتح موجه أوامر node.js وقم بتشغيل هذا الأمر:
npm install -g @angular/cli
حسنًا ، لدينا الآن مجمّع الوحدات. يقوم هذا عادةً بتثبيت الوحدة النمطية ضمن مجلد المستخدم الخاص بك. لا يجب أن يكون الاسم المستعار ضروريًا بشكل افتراضي ، ولكن إذا كنت في حاجة إليه ، يمكنك تنفيذ السطر التالي:
alias ng="<UserFolder>/.npm/lib/node_modules/angular-cli/bin/ng"
الخطوة التالية هي إنشاء المشروع الجديد. angular5-app
. أولاً ، ننتقل إلى المجلد الذي نريد إنشاء الموقع تحته ، ثم:
ng new angular5-app
أول بناء
بينما يمكنك اختبار موقع الويب الجديد الخاص بك فقط بتشغيل ng serve --open
، فإنني أوصي باختبار الموقع من خدمة الويب المفضلة لديك. لماذا ا؟ حسنًا ، يمكن أن تحدث بعض المشكلات فقط في الإنتاج ، وبناء الموقع باستخدام ng build
هو أقرب طريقة للتعامل مع هذه البيئة. ثم يمكننا فتح المجلد angular5-app
باستخدام Visual Studio Code وتشغيل ng build
على Terminal bash:
سيتم إنشاء مجلد جديد يسمى dist
ويمكننا خدمته باستخدام IIS أو أي خادم ويب تفضله. ثم يمكنك كتابة عنوان URL في المتصفح ، و ... انتهى!
ملحوظة
ليس الغرض من هذا البرنامج التعليمي إظهار كيفية إعداد خادم ويب ، لذلك أفترض أن لديك بالفعل هذه المعرفة. |
المجلد src
تم تنظيم مجلد My src
على النحو التالي: داخل مجلد app
لدينا components
حيث سننشئ لكل مكون Angular ملفات css
و ts
و spec
و html
. سننشئ أيضًا مجلدًا للتهيئة للحفاظ على config
الموقع ، directives
على جميع التوجيهات المخصصة لدينا ، وسيضم helpers
رمزًا مشتركًا مثل مدير المصادقة ، وسيحتوي layout
على المكونات الرئيسية مثل الجسم والرأس واللوحات الجانبية ، models
تحافظ على ما سوف تتطابق مع نماذج عرض النهاية الخلفية ، وأخيرًا ستحتوي services
على رمز لجميع المكالمات إلى النهاية الخلفية.
خارج مجلد app
، سنحتفظ بالمجلدات التي تم إنشاؤها افتراضيًا ، مثل assets
environments
، وكذلك الملفات الجذر.
إنشاء ملف التكوين
لنقم بإنشاء ملف config.ts
داخل مجلد config
الخاص بنا واستدعاء فئة AppConfig
. هذا هو المكان الذي يمكننا فيه تعيين جميع القيم التي سنستخدمها في أماكن مختلفة في الكود الخاص بنا ؛ على سبيل المثال ، عنوان URL لواجهة برمجة التطبيقات. لاحظ أن الفئة تنفذ خاصية get
التي تتلقى ، كمعامل ، بنية مفتاح / قيمة وطريقة بسيطة للوصول إلى نفس القيمة. بهذه الطريقة ، سيكون من السهل الحصول على القيم التي تستدعي this.config.setting['PathAPI']
من الفئات التي ترث منه.
import { Injectable } from '@angular/core'; @Injectable() export class AppConfig { private _config: { [key: string]: string }; constructor() { this._config = { PathAPI: 'http://localhost:50498/api/' }; } get setting():{ [key: string]: string } { return this._config; } get(key: any) { return this._config[key]; } };
مادة الزاوي
قبل البدء في التخطيط ، لنقم بإعداد إطار عمل مكون واجهة المستخدم. بالطبع ، يمكنك استخدام برامج أخرى مثل Bootstrap ، ولكن إذا كنت تحب تصميم المواد ، فإنني أوصي بها لأنها مدعومة أيضًا من Google.
لتثبيته ، نحتاج فقط إلى تشغيل الأوامر الثلاثة التالية ، والتي يمكننا تنفيذها على محطة Visual Studio Code:
npm install --save @angular/material @angular/cdk npm install --save @angular/animations npm install --save hammerjs
الأمر الثاني هو أن بعض مكونات المواد تعتمد على Angular Animations. أوصي أيضًا بقراءة الصفحة الرسمية لفهم المتصفحات المدعومة وما هو polyfill.
الأمر الثالث هو أن بعض مكونات المواد تعتمد على HammerJS للإيماءات.
الآن يمكننا المضي قدمًا في استيراد وحدات المكونات التي نريد استخدامها في ملف app.module.ts
بنا:
import {MatButtonModule, MatCheckboxModule} from '@angular/material'; import {MatInputModule} from '@angular/material/input'; import {MatFormFieldModule} from '@angular/material/form-field'; import {MatSidenavModule} from '@angular/material/sidenav'; // ... @NgModule({ imports: [ BrowserModule, BrowserAnimationsModule, MatButtonModule, MatCheckboxModule, MatInputModule, MatFormFieldModule, MatSidenavModule, AppRoutingModule, HttpClientModule ],
الخطوة التالية هي تغيير ملف style.css
، مع إضافة نوع القالب الذي تريد استخدامه:
@import "~@angular/material/prebuilt-themes/deeppurple-amber.css";
الآن قم باستيراد HammerJS عن طريق إضافة هذا السطر في ملف main.ts
:
import 'hammerjs';
وأخيرًا ، كل ما ينقصنا هو إضافة أيقونات المواد إلى index.html
، داخل قسم الرأس:
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
تخطيط
في هذا المثال ، سننشئ تخطيطًا بسيطًا مثل هذا:
الفكرة هي فتح / إخفاء القائمة من خلال النقر على بعض الأزرار الموجودة في العنوان. Angular Response سوف تقوم ببقية العمل من أجلنا. للقيام بذلك ، سننشئ مجلد layout
ونضع بداخله ملفات app.component
التي تم إنشاؤها افتراضيًا. لكننا سننشئ أيضًا نفس الملفات لكل قسم من التخطيط كما ترى في الصورة التالية. بعد ذلك ، سيكون app.component
هو النص ، head.component
the header ، و left-panel.component
القائمة.
لنقم الآن بتغيير app.component.html
على النحو التالي:
<div *ngIf="authentication"> <app-head></app-head> <button type="button" mat-button (click)="drawer.toggle()"> Menu </button> <mat-drawer-container class="example-container" autosize> <mat-drawer #drawer class="example-sidenav" mode="side"> <app-left-panel></app-left-panel> </mat-drawer> <div> <router-outlet></router-outlet> </div> </mat-drawer-container> </div> <div *ngIf="!authentication"><app-login></app-login></div>
في الأساس ، سيكون لدينا خاصية authentication
في المكون والتي ستسمح لنا بإزالة الرأس والقائمة إذا لم يقم المستخدم بتسجيل الدخول ، وبدلاً من ذلك ، عرض صفحة تسجيل دخول بسيطة.
يبدو head.component.html
كما يلي:
<h1>{{title}}</h1> <button mat-button [routerLink]=" ['./logout'] ">Logout!</button>
مجرد زر لتسجيل خروج المستخدم - سنعود إلى هذا مرة أخرى لاحقًا. بالنسبة إلى left-panel.component.html
، الآن فقط قم بتغيير HTML إلى:
<nav> <a routerLink="/dashboard">Dashboard</a> <a routerLink="/users">Users</a> </nav>
لقد أبقينا الأمر بسيطًا: حتى الآن ، هناك رابطان فقط للتنقل عبر صفحتين مختلفتين. (سنعود إلى هذا لاحقًا.)
الآن ، هذا ما تبدو عليه ملفات TypeScript المكونة على الجانب الأيسر والرأس:
import { Component } from '@angular/core'; @Component({ selector: 'app-head', templateUrl: './head.component.html', styleUrls: ['./head.component.css'] }) export class HeadComponent { title = 'Angular 5 Seed'; }
import { Component } from '@angular/core'; @Component({ selector: 'app-left-panel', templateUrl: './left-panel.component.html', styleUrls: ['./left-panel.component.css'] }) export class LeftPanelComponent { title = 'Angular 5 Seed'; }
ولكن ماذا عن رمز TypeScript لـ app.component
؟ سنترك القليل من الغموض هنا ونوقفه لبعض الوقت ، ونعود إلى هذا بعد تنفيذ المصادقة.
التوجيه
حسنًا ، لدينا الآن Angular Material التي تساعدنا في واجهة المستخدم وتخطيط بسيط لبدء إنشاء صفحاتنا. ولكن كيف يمكننا التنقل بين الصفحات؟
من أجل إنشاء مثال بسيط ، دعنا ننشئ صفحتين: "مستخدم" ، حيث يمكننا الحصول على قائمة بالمستخدمين الحاليين في قاعدة البيانات ، و "لوحة المعلومات" ، وهي صفحة يمكننا من خلالها عرض بعض الإحصائيات.
داخل مجلد app
، سننشئ ملفًا يسمى app-routing.modules.ts
يبدو كالتالي:
import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; import { AuthGuard } from './helpers/canActivateAuthGuard'; import { LoginComponent } from './components/login/login.component'; import { LogoutComponent } from './components/login/logout.component'; import { DashboardComponent } from './components/dashboard/dashboard.component'; import { UsersComponent } from './components/users/users.component'; const routes: Routes = [ { path: '', redirectTo: '/dashboard', pathMatch: 'full', canActivate: [AuthGuard] }, { path: 'login', component: LoginComponent}, { path: 'logout', component: LogoutComponent}, { path: 'dashboard', component: DashboardComponent, canActivate: [AuthGuard] }, { path: 'users', component: UsersComponent,canActivate: [AuthGuard] } ]; @NgModule({ imports: [ RouterModule.forRoot(routes) ], exports: [ RouterModule ] }) export class AppRoutingModule {}
الأمر بهذه البساطة: فقط استيراد RouterModule
and Routes
من @angular/router
، يمكننا تعيين المسارات التي نريد تنفيذها. نحن هنا نخلق أربعة مسارات:
-
/dashboard
: صفحتنا الرئيسية -
/login
: الصفحة التي يمكن للمستخدم المصادقة عليها -
/logout
: مسار بسيط لتسجيل خروج المستخدم -
/users
: صفحتنا الأولى حيث نريد سرد المستخدمين من النهاية الخلفية
لاحظ أن dashboard
هي صفحتنا بشكل افتراضي ، لذلك إذا قام المستخدم بكتابة عنوان URL /
، فستتم إعادة توجيه الصفحة تلقائيًا إلى هذه الصفحة. ألقِ نظرة أيضًا على المعلمة canActivate
: نحن هنا بصدد إنشاء مرجع إلى فئة AuthGuard
، والتي ستسمح لنا بالتحقق مما إذا كان المستخدم قد تم تسجيل دخوله أم لا. إذا لم يكن الأمر كذلك ، فسيتم إعادة التوجيه إلى صفحة تسجيل الدخول. في القسم التالي ، سأوضح لك كيفية إنشاء هذا الفصل.
الآن ، كل ما علينا فعله هو إنشاء القائمة. تذكر في قسم التخطيط عندما أنشأنا ملف left-panel.component.html
؟
<nav> <a routerLink="/dashboard">Dashboard</a> <a routerLink="/users">Users</a> </nav>
هنا حيث يلتقي كودنا بالواقع. يمكننا الآن إنشاء الكود واختباره في عنوان URL: يجب أن تكون قادرًا على التنقل من صفحة لوحة المعلومات إلى المستخدمين ، ولكن ماذا يحدث إذا قمت بكتابة عنوان URL our.site.url/users
في المتصفح مباشرةً؟
لاحظ أن هذا الخطأ يظهر أيضًا إذا قمت بتحديث المتصفح بعد الانتقال بنجاح بالفعل إلى عنوان URL هذا عبر اللوحة الجانبية للتطبيق. لفهم هذا الخطأ ، اسمح لي بالرجوع إلى المستندات الرسمية حيث يكون واضحًا حقًا:
يجب أن يدعم التطبيق الموجه الروابط الداخلية. الرابط العميق هو عنوان URL يحدد مسارًا لمكون داخل التطبيق. على سبيل المثال ،
http://www.mysite.com/users/42
رابط لموضع معين لصفحة تفاصيل البطل التي تعرض البطل بالمعرف: 42.لا توجد مشكلة عندما ينتقل المستخدم إلى عنوان URL هذا من داخل عميل قيد التشغيل. يفسر جهاز التوجيه الزاوي عنوان URL ويوجه إلى تلك الصفحة والبطل.
ولكن النقر فوق ارتباط في رسالة بريد إلكتروني ، أو إدخاله في شريط عنوان المتصفح ، أو مجرد تحديث المتصفح أثناء وجوده في صفحة التفاصيل الرئيسية - يتم التعامل مع كل هذه الإجراءات بواسطة المتصفح نفسه ، خارج التطبيق قيد التشغيل. يقدم المتصفح طلبًا مباشرًا إلى الخادم لعنوان URL هذا ، متجاوزًا جهاز التوجيه.يقوم الخادم الثابت بشكل روتيني بإرجاع index.html عندما يتلقى طلبًا لـ
http://www.mysite.com/
. لكنه يرفضhttp://www.mysite.com/users/42
ويعيد الخطأ 404 - لم يتم العثور عليه ما لم يتم تكوينه لإرجاع index.html بدلاً من ذلك.
لإصلاح هذه المشكلة بسيط للغاية ، نحتاج فقط إلى إنشاء تكوين ملف مزود الخدمة. نظرًا لأنني أعمل مع IIS هنا ، سأوضح لك كيفية القيام بذلك في هذه البيئة ، لكن المفهوم مشابه لـ Apache أو أي خادم ويب آخر.
لذلك قمنا بإنشاء ملف داخل مجلد src
يسمى web.config
يبدو كالتالي:
<?xml version="1.0"?> <configuration> <system.webServer> <rewrite> <rules> <rule name="Angular Routes" stopProcessing="true"> <match url=".*" /> <conditions logicalGrouping="MatchAll"> <add input="{REQUEST_FILENAME}" matchType="IsFile" negate="true" /> <add input="{REQUEST_FILENAME}" matchType="IsDirectory" negate="true" /> </conditions> <action type="Rewrite" url="/index.html" /> </rule> </rules> </rewrite> </system.webServer> <system.web> <compilation debug="true"/> </system.web> </configuration>
ثم نحتاج إلى التأكد من نسخ هذا الأصل إلى المجلد المنشور. كل ما نحتاج إلى القيام به هو تغيير ملف إعدادات Angular CLI الخاص بنا angular-cli.json
:
{ "$schema": "./node_modules/@angular/cli/lib/config/schema.json", "project": { "name": "angular5-app" }, "apps": [ { "root": "src", "outDir": "dist", "assets": [ "assets", "favicon.ico", "web.config" // or whatever equivalent is required by your web server ], "index": "index.html", "main": "main.ts", "polyfills": "polyfills.ts", "test": "test.ts", "tsconfig": "tsconfig.app.json", "testTsconfig": "tsconfig.spec.json", "prefix": "app", "styles": [ "styles.css" ], "scripts": [], "environmentSource": "environments/environment.ts", "environments": { "dev": "environments/environment.ts", "prod": "environments/environment.prod.ts" } } ], "e2e": { "protractor": { "config": "./protractor.conf.js" } }, "lint": [ { "project": "src/tsconfig.app.json", "exclude": "**/node_modules/**" }, { "project": "src/tsconfig.spec.json", "exclude": "**/node_modules/**" }, { "project": "e2e/tsconfig.e2e.json", "exclude": "**/node_modules/**" } ], "test": { "karma": { "config": "./karma.conf.js" } }, "defaults": { "styleExt": "css", "component": {} } }
المصادقة
هل تتذكر كيف تم تنفيذ فئة AuthGuard
لتعيين تكوين التوجيه؟ في كل مرة ننتقل فيها إلى صفحة مختلفة ، سنستخدم هذه الفئة للتحقق مما إذا كان المستخدم قد تمت مصادقته برمز. إذا لم يكن الأمر كذلك ، فسنقوم بإعادة التوجيه تلقائيًا إلى صفحة تسجيل الدخول. ملف هذا هو canActivateAuthGuard.ts
- قم بإنشائه داخل مجلد helpers
وجعله يبدو كالتالي:
import { CanActivate, Router } from '@angular/router'; import { Injectable } from '@angular/core'; import { Observable } from 'rxjs/Observable'; import { Helpers } from './helpers'; import { ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router'; @Injectable() export class AuthGuard implements CanActivate { constructor(private router: Router, private helper: Helpers) {} canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> | boolean { if (!this.helper.isAuthenticated()) { this.router.navigate(['/login']); return false; } return true; } }
لذلك في كل مرة نقوم فيها بتغيير الصفحة ، سيتم استدعاء الطريقة canActivate
، والتي ستتحقق مما إذا كان المستخدم قد تمت مصادقته ، وإذا لم يكن الأمر كذلك ، فإننا نستخدم مثيل Router
الخاص بنا لإعادة التوجيه إلى صفحة تسجيل الدخول. ولكن ما هي هذه الطريقة الجديدة في فئة Helper
؟ تحت مجلد helpers
، لنقم بإنشاء ملف helpers.ts
. نحتاج هنا إلى إدارة localStorage
، حيث سنخزن الرمز المميز الذي نحصل عليه من النهاية الخلفية.
ملحوظة
فيما يتعلق localStorage ، يمكنك أيضًا استخدام ملفات تعريف الارتباط أو sessionStorage ، وسيعتمد القرار على السلوك الذي نريد تنفيذه. كما يوحي الاسم ، فإن sessionStorage متاح فقط لمدة جلسة المتصفح ، ويتم حذفه عند إغلاق علامة التبويب أو النافذة ؛ ومع ذلك ، فإنه ينجو من عمليات إعادة تحميل الصفحة. إذا كانت البيانات التي تقوم بتخزينها بحاجة إلى أن تكون متاحة على أساس مستمر ، فإن localStorage هو الأفضل على sessionStorage . ملفات تعريف الارتباط مخصصة بشكل أساسي للقراءة من جانب الخادم ، بينما يمكن قراءة localStorage من جانب العميل فقط. لذا فإن السؤال هو ، في تطبيقك ، من يحتاج إلى هذه البيانات - العميل أم الخادم؟ |
import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; import { Subject } from 'rxjs/Subject'; @Injectable() export class Helpers { private authenticationChanged = new Subject<boolean>(); constructor() { } public isAuthenticated():boolean { return (!(window.localStorage['token'] === undefined || window.localStorage['token'] === null || window.localStorage['token'] === 'null' || window.localStorage['token'] === 'undefined' || window.localStorage['token'] === '')); } public isAuthenticationChanged():any { return this.authenticationChanged.asObservable(); } public getToken():any { if( window.localStorage['token'] === undefined || window.localStorage['token'] === null || window.localStorage['token'] === 'null' || window.localStorage['token'] === 'undefined' || window.localStorage['token'] === '') { return ''; } let obj = JSON.parse(window.localStorage['token']); return obj.token; } public setToken(data:any):void { this.setStorageToken(JSON.stringify(data)); } public failToken():void { this.setStorageToken(undefined); } public logout():void { this.setStorageToken(undefined); } private setStorageToken(value: any):void { window.localStorage['token'] = value; this.authenticationChanged.next(this.isAuthenticated()); } }
هل رمز المصادقة لدينا منطقي الآن؟ سنعود إلى فئة Subject
لاحقًا ، ولكن دعنا الآن نعود مرة أخرى لمدة دقيقة إلى تكوين التوجيه. ألق نظرة على هذا الخط:
{ path: 'logout', component: LogoutComponent},
هذا هو المكون الخاص بنا لتسجيل الخروج من الموقع ، وهو مجرد فئة بسيطة لتنظيف localStorage
. لنقم بإنشائه ضمن مجلد components/login
باسم logout.component.ts
:
import { Component, OnInit } from '@angular/core'; import { Router } from '@angular/router'; import { Helpers } from '../../helpers/helpers'; @Component({ selector: 'app-logout', template:'<ng-content></ng-content>' }) export class LogoutComponent implements OnInit { constructor(private router: Router, private helpers: Helpers) { } ngOnInit() { this.helpers.logout(); this.router.navigate(['/login']); } }
لذلك في كل مرة نذهب إلى عنوان URL /logout
، ستتم إزالة localStorage
الموقع التوجيه إلى صفحة تسجيل الدخول. أخيرًا ، لنقم بإنشاء login.component.ts
مثل هذا:
import { Component, OnInit } from '@angular/core'; import { Router } from '@angular/router'; import { TokenService } from '../../services/token.service'; import { Helpers } from '../../helpers/helpers'; @Component({ selector: 'app-login', templateUrl: './login.component.html', styleUrls: [ './login.component.css' ] }) export class LoginComponent implements OnInit { constructor(private helpers: Helpers, private router: Router, private tokenService: TokenService) { } ngOnInit() { } login(): void { let authValues = {"Username":"pablo", "Password":"secret"}; this.tokenService.auth(authValues).subscribe(token => { this.helpers.setToken(token); this.router.navigate(['/dashboard']); }); } }
كما ترى ، قمنا في الوقت الحالي بترميز أوراق اعتمادنا هنا. لاحظ أننا هنا نستدعي فئة الخدمة ؛ سننشئ فئات الخدمات هذه للوصول إلى نهايتنا الخلفية في القسم التالي.

أخيرًا ، نحتاج إلى العودة إلى ملف app.component.ts
، تخطيط الموقع. هنا ، إذا تمت مصادقة المستخدم ، فسوف يعرض القائمة وأقسام الرأس ، ولكن إذا لم يكن الأمر كذلك ، فسيتغير التخطيط لإظهار صفحة تسجيل الدخول الخاصة بنا فقط.
export class AppComponent implements AfterViewInit { subscription: Subscription; authentication: boolean; constructor(private helpers: Helpers) { } ngAfterViewInit() { this.subscription = this.helpers.isAuthenticationChanged().pipe( startWith(this.helpers.isAuthenticated()), delay(0)).subscribe((value) => this.authentication = value ); } title = 'Angular 5 Seed'; ngOnDestroy() { this.subscription.unsubscribe(); } }
هل تتذكر فئة " Subject
" في فصل المساعدة لدينا؟ هذا Observable
. توفر Observable
دعمًا لتمرير الرسائل بين الناشرين والمشتركين في تطبيقك. في كل مرة يتغير رمز المصادقة المميز ، سيتم تحديث خاصية authentication
. بمراجعة ملف app.component.html
، فمن المحتمل أن يكون أكثر منطقية الآن:
<div *ngIf="authentication"> <app-head></app-head> <button type="button" mat-button (click)="drawer.toggle()"> Menu </button> <mat-drawer-container class="example-container" autosize> <mat-drawer #drawer class="example-sidenav" mode="side"> <app-left-panel></app-left-panel> </mat-drawer> <div> <router-outlet></router-outlet> </div> </mat-drawer-container> </div> <div *ngIf="!authentication"><app-login></app-login></div>
خدمات
في هذه المرحلة ، ننتقل إلى صفحات مختلفة ، ونصادق على جانب العميل لدينا ، ونقدم تخطيطًا بسيطًا للغاية. لكن كيف يمكننا الحصول على البيانات من النهاية الخلفية؟ أوصي بشدة بإجراء كل الوصول الخلفي من فئات الخدمة على وجه الخصوص. ستكون خدمتنا الأولى داخل مجلد services
، المسمى token.service.ts
:
import { Injectable } from '@angular/core'; import { HttpClient, HttpHeaders } from '@angular/common/http'; import { Observable } from 'rxjs/Observable'; import { of } from 'rxjs/observable/of'; import { catchError, map, tap } from 'rxjs/operators'; import { AppConfig } from '../config/config'; import { BaseService } from './base.service'; import { Token } from '../models/token'; import { Helpers } from '../helpers/helpers'; @Injectable() export class TokenService extends BaseService { private pathAPI = this.config.setting['PathAPI']; public errorMessage: string; constructor(private http: HttpClient, private config: AppConfig, helper: Helpers) { super(helper); } auth(data: any): any { let body = JSON.stringify(data); return this.getToken(body); } private getToken (body: any): Observable<any> { return this.http.post<any>(this.pathAPI + 'token', body, super.header()).pipe( catchError(super.handleError) ); } }
الاستدعاء الأول للنهاية الخلفية هو استدعاء POST لواجهة برمجة التطبيقات المميزة. لا تحتاج واجهة برمجة تطبيقات الرمز المميز إلى سلسلة الرمز المميز في الرأس ، ولكن ماذا يحدث إذا استدعينا نقطة نهاية أخرى؟ كما ترى هنا ، TokenService
(وفئات الخدمة بشكل عام) من فئة BaseService
. دعنا نلقي نظرة على هذا:
import { Injectable } from '@angular/core'; import { HttpClient, HttpHeaders } from '@angular/common/http'; import { Observable } from 'rxjs/Observable'; import { of } from 'rxjs/observable/of'; import { catchError, map, tap } from 'rxjs/operators'; import { Helpers } from '../helpers/helpers'; @Injectable() export class BaseService { constructor(private helper: Helpers) { } public extractData(res: Response) { let body = res.json(); return body || {}; } public handleError(error: Response | any) { // In a real-world app, we might use a remote logging infrastructure let errMsg: string; if (error instanceof Response) { const body = error.json() || ''; const err = body || JSON.stringify(body); errMsg = `${error.status} - ${error.statusText || ''} ${err}`; } else { errMsg = error.message ? error.message : error.toString(); } console.error(errMsg); return Observable.throw(errMsg); } public header() { let header = new HttpHeaders({ 'Content-Type': 'application/json' }); if(this.helper.isAuthenticated()) { header = header.append('Authorization', 'Bearer ' + this.helper.getToken()); } return { headers: header }; } public setToken(data:any) { this.helper.setToken(data); } public failToken(error: Response | any) { this.helper.failToken(); return this.handleError(Response); } }
لذلك في كل مرة نقوم فيها بإجراء مكالمة HTTP ، نقوم بتنفيذ رأس الطلب فقط باستخدام super.header
. إذا كان الرمز المميز في localStorage
، فسيتم إلحاقه داخل الرأس ، ولكن إذا لم يكن كذلك ، فسنقوم فقط بتعيين تنسيق JSON. شيء آخر يمكننا رؤيته هنا هو ما يحدث إذا فشلت المصادقة.
سيقوم مكون تسجيل الدخول باستدعاء فئة الخدمة وستقوم فئة الخدمة باستدعاء النهاية الخلفية. بمجرد أن نحصل على الرمز المميز ، ستدير فئة المساعد الرمز المميز ، ونحن الآن على استعداد للحصول على قائمة المستخدمين من قاعدة البيانات الخاصة بنا.
للحصول على البيانات من قاعدة البيانات ، تأكد أولاً من مطابقة فئات النموذج مع نماذج عرض النهاية الخلفية في استجابتنا.
في user.ts
:
export class User { id: number; name: string; }
ويمكننا الآن إنشاء ملف user.service.ts
:
import { Injectable } from '@angular/core'; import { HttpClient, HttpHeaders } from '@angular/common/http'; import { Observable } from 'rxjs/Observable'; import { of } from 'rxjs/observable/of'; import { catchError, map, tap } from 'rxjs/operators'; import { BaseService } from './base.service'; import { User } from '../models/user'; import { AppConfig } from '../config/config'; import { Helpers } from '../helpers/helpers'; @Injectable() export class UserService extends BaseService { private pathAPI = this.config.setting['PathAPI']; constructor(private http: HttpClient, private config: AppConfig, helper: Helpers) { super(helper); } /** GET heroes from the server */ getUsers (): Observable<User[]> { return this.http.get(this.pathAPI + 'user', super.header()).pipe( catchError(super.handleError)); }
النهاية الخلفية
بداية سريعة
مرحبًا بك في الخطوة الأولى من تطبيق Web API Core 2 الخاص بنا. أول شيء نحتاجه هو إنشاء تطبيق ويب ASP.Net Core ، والذي SeedAPI.Web.API
.
تأكد من اختيار النموذج الفارغ لبداية نظيفة كما ترى أدناه:
هذا كل شيء ، نقوم بإنشاء الحل بدءًا من تطبيق ويب فارغ. الآن ستكون هندستنا كما نذكر أدناه ، لذا سيتعين علينا إنشاء مشاريع مختلفة:
للقيام بذلك ، لكل واحد فقط انقر بزر الماوس الأيمن فوق الحل وأضف مشروع "Class Library (.NET Core)".
العمارة
في القسم السابق أنشأنا ثمانية مشاريع ، ولكن ما الغرض منها؟ فيما يلي وصف بسيط لكل واحد:
-
Web.API
: هذا هو مشروع بدء التشغيل الخاص بنا وحيث يتم إنشاء نقاط النهاية. سنقوم هنا بإعداد JWT وتبعيات الحقن ووحدات التحكم. -
ViewModels
: هنا نقوم بإجراء تحويلات من نوع البيانات التي ستعيدها وحدات التحكم في الردود على الواجهة الأمامية. من الممارسات الجيدة مطابقة هذه الفئات مع نماذج الواجهة الأمامية. -
Interfaces
: سيكون هذا مفيدًا في تنفيذ تبعيات الحقن. الفائدة المقنعة للغة المكتوبة بشكل ثابت هي أن المترجم يمكنه المساعدة في التحقق من الوفاء بالعقد الذي تعتمد عليه التعليمات البرمجية الخاصة بك بالفعل. -
Commons
: ستكون جميع السلوكيات المشتركة ورمز الأداة هنا. -
Models
: من الممارسات الجيدة عدم مطابقة قاعدة البيانات مباشرةً معViewModels
المواجهة للواجهة الأمامية ، لذا فإن الغرض منModels
هو إنشاء فئات قاعدة بيانات كيان مستقلة عن الواجهة الأمامية. سيسمح لنا ذلك في المستقبل بتغيير قاعدة البيانات الخاصة بنا دون التأثير بالضرورة على واجهتنا الأمامية. كما أنه يساعد عندما نريد ببساطة القيام ببعض إعادة البناء. -
Maps
: هنا حيث نقوم بتعيينViewModels
إلىModels
والعكس صحيح. تسمى هذه الخطوة بين وحدات التحكم والخدمات. -
Services
: مكتبة لتخزين كل منطق الأعمال. -
Repositories
: هذا هو المكان الوحيد الذي نطلق عليه قاعدة البيانات.
ستبدو المراجع كما يلي:
المصادقة المستندة إلى JWT
في هذا القسم ، سنرى التكوين الأساسي لمصادقة الرمز المميز ونتعمق قليلاً في موضوع الأمان.
لبدء تعيين رمز الويب JSON (JWT) ، دعنا ننشئ الفئة التالية داخل مجلد App_Start
المسمى JwtTokenConfig.cs
. سيبدو الرمز الموجود بالداخل كما يلي:
namespace SeedAPI.Web.API.App_Start { public class JwtTokenConfig { public static void AddAuthentication(IServiceCollection services, IConfiguration configuration) { services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(options => { options.TokenValidationParameters = new TokenValidationParameters { ValidateIssuer = true, ValidateAudience = true, ValidateLifetime = true, ValidateIssuerSigningKey = true, ValidIssuer = configuration["Jwt:Issuer"], ValidAudience = configuration["Jwt:Issuer"], IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(configuration["Jwt:Key"])) }; services.AddCors(); }); } } }
ستعتمد قيم معلمات التحقق على متطلبات كل مشروع. المستخدم والجمهور الصالح الذي يمكننا تعيينه لقراءة ملف التكوين appsettings.json
:
"Jwt": { "Key": "veryVerySecretKey", "Issuer": "http://localhost:50498/" }
ثم نحتاج فقط إلى استدعائها من طريقة ConfigureServices
في startup.cs
:
// This method gets called by the runtime. Use this method to add services to the container. public void ConfigureServices(IServiceCollection services) { DependencyInjectionConfig.AddScope(services); JwtTokenConfig.AddAuthentication(services, Configuration); DBContextConfig.Initialize(services, Configuration); services.AddMvc(); }
نحن الآن جاهزون لإنشاء أول وحدة تحكم تسمى TokenController.cs
. يجب أن تطابق القيمة التي حددناها في appsettings.json
إلى "veryVerySecretKey"
القيمة التي نستخدمها لإنشاء الرمز المميز ، ولكن أولاً ، لنقم بإنشاء LoginViewModel
داخل مشروع ViewModels
الخاص بنا:
namespace SeedAPI.ViewModels { public class LoginViewModel : IBaseViewModel { public string username { get; set; } public string password { get; set; } } }
وأخيرًا جهاز التحكم:
namespace SeedAPI.Web.API.Controllers { [Route("api/Token")] public class TokenController : Controller { private IConfiguration _config; public TokenController(IConfiguration config) { _config = config; } [AllowAnonymous] [HttpPost] public dynamic Post([FromBody]LoginViewModel login) { IActionResult response = Unauthorized(); var user = Authenticate(login); if (user != null) { var tokenString = BuildToken(user); response = Ok(new { token = tokenString }); } return response; } private string BuildToken(UserViewModel user) { var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_config["Jwt:Key"])); var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256); var token = new JwtSecurityToken(_config["Jwt:Issuer"], _config["Jwt:Issuer"], expires: DateTime.Now.AddMinutes(30), signingCredentials: creds); return new JwtSecurityTokenHandler().WriteToken(token); } private UserViewModel Authenticate(LoginViewModel login) { UserViewModel user = null; if (login.username == "pablo" && login.password == "secret") { user = new UserViewModel { name = "Pablo" }; } return user; } } }
ستنشئ طريقة BuildToken
الرمز المميز برمز الأمان المحدد. تحتوي طريقة Authenticate
على عملية ترميز ثابتة للمستخدم في الوقت الحالي ، لكننا سنحتاج إلى استدعاء قاعدة البيانات للتحقق من صحتها في النهاية.
سياق التطبيق
يعد إعداد Entity Framework أمرًا سهلاً حقًا منذ أن أطلقت Microsoft الإصدار Core 2.0 - اختصارًا EF Core 2 . سوف نتعمق في نموذج الكود الأول باستخدام identityDbContext
، لذا تأكد أولاً من أنك قمت بتثبيت جميع التبعيات. يمكنك استخدام NuGet لإدارتها:
باستخدام مشروع Models
، يمكننا إنشاء ملفين داخل مجلد Context
، ApplicationContext.cs
و IApplicationContext.cs
. أيضًا ، سنحتاج إلى فئة EntityBase
.
سيتم توريث ملفات EntityBase
بواسطة كل نموذج كيان ، لكن User.cs
عبارة عن فئة هوية والكيان الوحيد الذي سيرث من IdentityUser
. فيما يلي كلا الفئتين:
namespace SeedAPI.Models { public class User : IdentityUser { public string Name { get; set; } } }
namespace SeedAPI.Models.EntityBase { public class EntityBase { public DateTime? Created { get; set; } public DateTime? Updated { get; set; } public bool Deleted { get; set; } public EntityBase() { Deleted = false; } public virtual int IdentityID() { return 0; } public virtual object[] IdentityID(bool dummy = true) { return new List<object>().ToArray(); } } }
نحن الآن جاهزون لإنشاء ApplicationContext.cs
، والذي سيبدو كالتالي:
namespace SeedAPI.Models.Context { public class ApplicationContext : IdentityDbContext<User>, IApplicationContext { private IDbContextTransaction dbContextTransaction; public ApplicationContext(DbContextOptions options) : base(options) { } public DbSet<User> UsersDB { get; set; } public new void SaveChanges() { base.SaveChanges(); } public new DbSet<T> Set<T>() where T : class { return base.Set<T>(); } public void BeginTransaction() { dbContextTransaction = Database.BeginTransaction(); } public void CommitTransaction() { if (dbContextTransaction != null) { dbContextTransaction.Commit(); } } public void RollbackTransaction() { if (dbContextTransaction != null) { dbContextTransaction.Rollback(); } } public void DisposeTransaction() { if (dbContextTransaction != null) { dbContextTransaction.Dispose(); } } } }
نحن قريبون حقًا ، ولكن أولاً ، سنحتاج إلى إنشاء المزيد من الفئات ، هذه المرة في مجلد App_Start
الموجود في مشروع Web.API
. الفئة الأولى هي تهيئة سياق التطبيق والثانية هي إنشاء بيانات نموذجية فقط لغرض الاختبار أثناء التطوير.
namespace SeedAPI.Web.API.App_Start { public class DBContextConfig { public static void Initialize(IConfiguration configuration, IHostingEnvironment env, IServiceProvider svp) { var optionsBuilder = new DbContextOptionsBuilder(); if (env.IsDevelopment()) optionsBuilder.UseSqlServer(configuration.GetConnectionString("DefaultConnection")); else if (env.IsStaging()) optionsBuilder.UseSqlServer(configuration.GetConnectionString("DefaultConnection")); else if (env.IsProduction()) optionsBuilder.UseSqlServer(configuration.GetConnectionString("DefaultConnection")); var context = new ApplicationContext(optionsBuilder.Options); if(context.Database.EnsureCreated()) { IUserMap service = svp.GetService(typeof(IUserMap)) as IUserMap; new DBInitializeConfig(service).DataTest(); } } public static void Initialize(IServiceCollection services, IConfiguration configuration) { services.AddDbContext<ApplicationContext>(options => options.UseSqlServer(configuration.GetConnectionString("DefaultConnection"))); } } }
namespace SeedAPI.Web.API.App_Start { public class DBInitializeConfig { private IUserMap userMap; public DBInitializeConfig (IUserMap _userMap) { userMap = _userMap; } public void DataTest() { Users(); } private void Users() { userMap.Create(new UserViewModel() { id = 1, name = "Pablo" }); userMap.Create(new UserViewModel() { id = 2, name = "Diego" }); } } }
And we call them from our startup file:
// This method gets called by the runtime. Use this method to add services to the container. public void ConfigureServices(IServiceCollection services) { DependencyInjectionConfig.AddScope(services); JwtTokenConfig.AddAuthentication(services, Configuration); DBContextConfig.Initialize(services, Configuration); services.AddMvc(); } // ... // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. public void Configure(IApplicationBuilder app, IHostingEnvironment env, IServiceProvider svp) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } DBContextConfig.Initialize(Configuration, env, svp); app.UseCors(builder => builder .AllowAnyOrigin() .AllowAnyMethod() .AllowAnyHeader() .AllowCredentials()); app.UseAuthentication(); app.UseMvc(); }
حقن التبعية
It is a good practice to use dependency injection to move among different projects. This will help us to communicate between controllers and mappers, mappers and services, and services and repositories.
Inside the folder App_Start
we will create the file DependencyInjectionConfig.cs
and it will look like this:
namespace SeedAPI.Web.API.App_Start { public class DependencyInjectionConfig { public static void AddScope(IServiceCollection services) { services.AddScoped<IApplicationContext, ApplicationContext>(); services.AddScoped<IUserMap, UserMap>(); services.AddScoped<IUserService, UserService>(); services.AddScoped<IUserRepository, UserRepository>(); } } }
We will need to create for each new entity a new Map
, Service
, and Repository
, and match them to this file. Then we just need to call it from the startup.cs
file:
// This method gets called by the runtime. Use this method to add services to the container. public void ConfigureServices(IServiceCollection services) { DependencyInjectionConfig.AddScope(services); JwtTokenConfig.AddAuthentication(services, Configuration); DBContextConfig.Initialize(services, Configuration); services.AddMvc(); }
Finally, when we need to get the users list from the database, we can create a controller using this dependency injection:
namespace SeedAPI.Web.API.Controllers { [Route("api/[controller]")] [Authorize] public class UserController : Controller { IUserMap userMap; public UserController(IUserMap map) { userMap = map; } // GET api/user [HttpGet] public IEnumerable<UserViewModel> Get() { return userMap.GetAll(); ; } // GET api/user/5 [HttpGet("{id}")] public string Get(int id) { return "value"; } // POST api/user [HttpPost] public void Post([FromBody]string user) { } // PUT api/user/5 [HttpPut("{id}")] public void Put(int id, [FromBody]string user) { } // DELETE api/user/5 [HttpDelete("{id}")] public void Delete(int id) { } } }
Look how the Authorize
attribute is present here to be sure that the front end has logged in and how dependency injection works in the constructor of the class.
We finally have a call to the database but first, we need to understand the Map
project.
مشروع Maps
هذه الخطوة هي فقط لتعيين ViewModels
من وإلى نماذج قاعدة البيانات. يجب علينا إنشاء واحد لكل كيان ، وبعد المثال السابق ، سيبدو ملف UserMap.cs
على النحو التالي:
namespace SeedAPI.Maps { public class UserMap : IUserMap { IUserService userService; public UserMap(IUserService service) { userService = service; } public UserViewModel Create(UserViewModel viewModel) { User user = ViewModelToDomain(viewModel); return DomainToViewModel(userService.Create(user)); } public bool Update(UserViewModel viewModel) { User user = ViewModelToDomain(viewModel); return userService.Update(user); } public bool Delete(int id) { return userService.Delete(id); } public List<UserViewModel> GetAll() { return DomainToViewModel(userService.GetAll()); } public UserViewModel DomainToViewModel(User domain) { UserViewModel model = new UserViewModel(); model.name = domain.Name; return model; } public List<UserViewModel> DomainToViewModel(List<User> domain) { List<UserViewModel> model = new List<UserViewModel>(); foreach (User of in domain) { model.Add(DomainToViewModel(of)); } return model; } public User ViewModelToDomain(UserViewModel officeViewModel) { User domain = new User(); domain.Name = officeViewModel.name; return domain; } } }
يبدو مرة أخرى ، أن حقن التبعية يعمل في مُنشئ الفصل ، ويربط الخرائط بمشروع الخدمات.
مشروع Services
ليس هناك الكثير لنقوله هنا: مثالنا بسيط حقًا وليس لدينا منطق أو رمز عمل نكتبه هنا. سيكون هذا المشروع مفيدًا في المتطلبات المتقدمة المستقبلية عندما نحتاج إلى حساب أو القيام ببعض المنطق قبل أو بعد خطوات قاعدة البيانات أو وحدة التحكم. باتباع المثال سيبدو الفصل مكشوفًا تمامًا:
namespace SeedAPI.Services { public class UserService : IUserService { private IUserRepository repository; public UserService(IUserRepository userRepository) { repository = userRepository; } public User Create(User domain) { return repository.Save(domain); } public bool Update(User domain) { return repository.Update(domain); } public bool Delete(int id) { return repository.Delete(id); } public List<User> GetAll() { return repository.GetAll(); } } }
مشروع Repositories
نصل إلى القسم الأخير من هذا البرنامج التعليمي: نحتاج فقط إلى إجراء مكالمات إلى قاعدة البيانات ، لذلك نقوم بإنشاء ملف UserRepository.cs
حيث يمكننا قراءة أو إدراج أو تحديث المستخدمين في قاعدة البيانات.
namespace SeedAPI.Repositories { public class UserRepository : BaseRepository, IUserRepository { public UserRepository(IApplicationContext context) : base(context) { } public User Save(User domain) { try { var us = InsertUser<User>(domain); return us; } catch (Exception ex) { //ErrorManager.ErrorHandler.HandleError(ex); throw ex; } } public bool Update(User domain) { try { //domain.Updated = DateTime.Now; UpdateUser<User>(domain); return true; } catch (Exception ex) { //ErrorManager.ErrorHandler.HandleError(ex); throw ex; } } public bool Delete(int id) { try { User user = Context.UsersDB.Where(x => x.Id.Equals(id)).FirstOrDefault(); if (user != null) { //Delete<User>(user); return true; } else { return false; } } catch (Exception ex) { //ErrorManager.ErrorHandler.HandleError(ex); throw ex; } } public List<User> GetAll() { try { return Context.UsersDB.OrderBy(x => x.Name).ToList(); } catch (Exception ex) { //ErrorManager.ErrorHandler.HandleError(ex); throw ex; } } } }
ملخص
في هذه المقالة ، شرحت كيفية إنشاء بنية جيدة باستخدام Angular 5 و Web API Core 2. في هذه المرحلة ، قمت بإنشاء قاعدة لمشروع كبير برمز يدعم نموًا كبيرًا في المتطلبات.
الحقيقة هي أنه لا يوجد شيء ينافس JavaScript في الواجهة الأمامية وما الذي يمكن أن ينافس C # إذا كنت بحاجة إلى دعم SQL Server و Entity Framework في النهاية الخلفية؟ لذا كانت فكرة هذا المقال هي الجمع بين أفضل ما في العالمين وآمل أن تكون قد استمتعت به.
ماذا بعد؟
إذا كنت تعمل في فريق من مطوري Angular ، فمن المحتمل أن يكون هناك مطورون مختلفون يعملون في الواجهة الأمامية والخلفية ، لذا فإن فكرة جيدة لمزامنة جهود كلا الفريقين يمكن أن تكون دمج Swagger مع Web API 2. Swagger هو أمر رائع أداة لتوثيق واختبار واجهات برمجة تطبيقات RESTFul. اقرأ دليل Microsoft: ابدأ مع Swashbuckle و ASP.NET Core.
إذا كنت لا تزال جديدًا على Angular 5 وتواجه مشكلة في المتابعة ، فاقرأ An Angular 5 Tutorial: دليل خطوة بخطوة إلى تطبيق Angular 5 الأول الخاص بك من قبل زميلك Toptaler Sergey Moiseev.