socialgekon.com
  • Tärkein
  • Web-Käyttöliittymä
  • Sijoittajat Ja Rahoitus
  • Matkapuhelin
  • Teknologia
Taustaa

Kulma 5 ja ASP.NET-ydin

Olen ajatellut blogikirjoituksen kirjoittamista siitä lähtien, kun Angularin ensimmäinen versio käytännössä tappoi Microsoftin asiakaspuolella. Teknologiat, kuten ASP.Net, Web Forms ja MVC Razor, ovat vanhentuneet ja korvattu JavaScript-kehyksellä, joka ei ole aivan Microsoft. Kuitenkin Angularin toisen version jälkeen Microsoft ja Google ovat työskennelleet yhdessä Angular 2: n luomiseksi, ja juuri silloin kaksi suosikkitekniikkani alkoivat toimia yhdessä.

Haluan tässä blogissa auttaa ihmisiä luomaan parhaan arkkitehtuurin, joka yhdistää nämä kaksi maailmaa. Oletko valmis? Nyt sitä mennään!

Tietoja arkkitehtuurista

Rakennat Angular 5 -asiakasohjelman, joka käyttää RESTful Web API Core 2 -palvelua.



Asiakaspuoli:

  • Kulma 5
  • Kulma CLI
  • Kulmamateriaali

Palvelinpuoli:

  • .NET C # Web API Core 2
  • Pistosriippuvuudet
  • JWT-todennus
  • Entiteetin kehyskoodi ensin
  • SQL Server

Huomautus

Tässä blogiviestissä oletetaan, että lukijalla on jo perustiedot TypeScriptistä, Angular-moduuleista, komponenteista ja tuonnista / viennistä. Tämän viestin tavoitteena on luoda hyvä arkkitehtuuri, jonka avulla koodi kasvaa ajan myötä.

Mitä tarvitset?

Aloitetaan valitsemalla IDE. Tietenkin tämä on vain minun mieltymykseni, ja voit käyttää sitä, jonka kanssa tunnet olosi mukavammaksi. Minun tapauksessani käytän Visual Studio -koodia ja Visual Studio 2017: tä.

Miksi kaksi erilaista IDE: tä? Koska Microsoft loi Visual Studio -koodin käyttöliittymälle, en voi lopettaa tämän IDE: n käyttöä. Joka tapauksessa näemme myös, kuinka Angular 5 integroidaan ratkaisuprojektin sisälle, mikä auttaa sinua, jos olet sellainen kehittäjä, joka mieluummin etsii sekä takapään että etupuolen yhdellä F5: llä.

Tietoja käyttöliittymästä voit asentaa uusimman Visual Studio 2017 -version, jolla on ilmainen kehittäjille tarkoitettu versio, mutta joka on erittäin kattava: Yhteisö.

Joten tässä on luettelo asioista, jotka meidän on asennettava tähän opetusohjelmaan:

  • Visual Studio -koodi
  • Visual Studio 2017 -yhteisö (tai mikä tahansa)
  • Node.js v8.10.0
  • SQL Server 2017

Huomautus

Varmista, että sinulla on vähintään solmu 6.9.x ja npm 3.x.x suorittamalla node -v ja npm -v pääte- tai konsoli-ikkunassa. Vanhemmat versiot tuottavat virheitä, mutta uudemmat versiot ovat hyviä.

Etupää

Pika-aloitus

Hauskuus alkakoon! Ensimmäinen asia, joka meidän on tehtävä, on asentaa Angular CLI maailmanlaajuisesti, joten avaa node.js-komentokehote ja suorita tämä komento:

npm install -g @angular/cli

Okei, nyt meillä on moduulipakettimme. Tämä asentaa moduulin yleensä käyttäjäkansioon. Aliaksen ei pitäisi olla oletusarvoisesti tarpeellinen, mutta jos tarvitset sitä, voit suorittaa seuraavan rivin:

alias ng='/.npm/lib/node_modules/angular-cli/bin/ng'

Seuraava vaihe on luoda uusi projekti. Minä kutsun sitä angular5-app. Ensin siirrymme kansioon, johon haluamme luoda sivuston, ja sitten:

ng new angular5-app

Ensimmäinen rakennus

Vaikka voit testata uutta verkkosivustoasi juuri käynnissä ng serve --open, suosittelen testaamaan sivuston suosikkiverkkopalvelustasi. Miksi? Jotkut ongelmat voivat ilmetä vain tuotannossa ja sivuston rakentamisessa ng build on lähin tapa lähestyä tätä ympäristöä. Sitten voimme avata kansion angular5-app Visual Studio -koodilla ja suorita ng build päätelaitteessa:

kulmasovelluksen rakentaminen ensimmäistä kertaa

Uusi kansio nimeltä dist luodaan ja voimme palvella sitä IIS: llä tai haluamallasi web-palvelimella. Sitten voit kirjoittaa URL-osoitteen selaimeen ja… valmis!

uusi hakemistorakenne

Huomautus

Tämän opetusohjelman tarkoituksena ei ole näyttää verkkopalvelimen perustamista, joten oletan, että sinulla on jo tämä tieto.

Angular 5: n aloitusnäyttö

src Kansio

Src-kansion rakenne

Oma src kansion rakenne on seuraava: app kansio meillä on components missä luomme jokaiselle kulmakomponentille css, ts, spec ja html tiedostot. Luomme myös config kansio sivuston kokoonpanon säilyttämiseksi, directives on kaikki mukautetut direktiivimme, helpers sisältää yhteisen koodin, kuten todennuksen hallinta, layout sisältää pääosat, kuten rungon, pään ja sivupaneelit, models säilyttää taustanäkymämalleihin sopivan ja lopuksi services on koodi kaikille puheluille.

app: N ulkopuolella kansio, säilytämme oletuksena luodut kansiot, kuten assets ja environments sekä juuritiedostot.

Määritystiedoston luominen

Luodaan config.ts tiedosto config sisällä -kansioon ja soita luokkaan AppConfig. Tässä voimme asettaa kaikki arvot, joita käytämme koodin eri paikoissa; esimerkiksi API: n URL-osoite. Huomaa, että luokka toteuttaa a get ominaisuus, joka vastaanottaa parametrina avaimen / arvon rakenteen ja yksinkertaisen menetelmän päästä samaan arvoon. Tällä tavalla on helppo saada arvot, jotka vain kutsuvat this.config.setting['PathAPI'] luokista, jotka perivät siitä.

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]; } };

Kulmamateriaali

Määritetään käyttöliittymän komponenttikehys ennen asettelun aloittamista. Voit tietysti käyttää muita, kuten Bootstrapia, mutta jos pidät materiaalin tyylistä, suosittelen sitä, koska Google tukee myös sitä.

Sen asentamiseksi meidän on vain suoritettava seuraavat kolme komentoa, jotka voimme suorittaa Visual Studio Code -päätteessä:

npm install --save @angular/material @angular/cdk npm install --save @angular/animations npm install --save hammerjs

Toinen komento johtuu siitä, että jotkut materiaalikomponentit riippuvat kulma-animaatioista. Suosittelen myös lukemista virallinen sivu ymmärtää, mitä selaimia tuetaan ja mikä on polyfill-täyttö.

Kolmas komento johtuu siitä, että jotkut materiaalikomponentit luottavat HammerJS: ään eleissä.

Nyt voimme jatkaa komponenttimoduulien tuomista app.module.ts tiedosto:

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 ],

Seuraava vaihe on muuttaa style.css tiedosto, lisäämällä haluamasi teema:

@import ' [email protected] /material/prebuilt-themes/deeppurple-amber.css';

Tuo nyt HammerJS lisäämällä tämä rivi main.ts tiedosto:

import 'hammerjs';

Ja lopulta kaikki mitä meiltä puuttuu, on lisätä materiaalikuvakkeet index.html -pään osaan:

layout

Asettelu

Tässä esimerkissä luomme seuraavanlaisen yksinkertaisen asettelun:

Asetteluesimerkki

Ajatuksena on avata / piilottaa valikko napsauttamalla jotakin painiketta otsikossa. Kulmikas reagoiva tekee lopun työn puolestamme. Tätä varten luomme app.component kansion ja aseta sen sisälle app.component oletuksena luodut tiedostot. Mutta luomme myös samat tiedostot asettelun jokaiselle osalle, kuten näet seuraavassa kuvassa. Sitten head.component on ruumis, left-panel.component otsikko ja app.component.html ruokalista.

Korostettu määrityskansio

Muutetaan nyt Menu seuraavasti:

authentication

Pohjimmiltaan meillä on head.component.html komponentin ominaisuus, jonka avulla voimme poistaa otsikon ja valikon, jos käyttäjä ei ole kirjautunut sisään, ja näyttää sen sijaan yksinkertaisen kirjautumissivun.

Logout! näyttää tältä:

left-panel.component.html

Vain painike kirjautua ulos käyttäjästä - palaamme asiaan myöhemmin uudelleen. Mitä tulee Dashboard Users : een, muuta vain HTML: ksi:

import { Component } from '@angular/core'; @Component({ selector: 'app-head', templateUrl: './head.component.html', styleUrls: ['./head.component.css'] }) export class HeadComponent { title = 'Angular 5 Seed'; }

Olemme pitäneet sen yksinkertaisena: toistaiseksi se on vain kaksi linkkiä selaamaan kahdella eri sivulla. (Palataan myös tähän myöhemmin.)

Nyt pää- ja vasemmanpuoleiset TypeScript-tiedostot näyttävät tältä:

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'; } app.component

Mutta entä app -tyyppinen TypeScript-koodi? Jätämme tänne pienen mysteerin ja keskeytämme sen hetkeksi ja palaamme tähän todentamisen jälkeen.

Reititys

Okei, nyt meillä on kulmamateriaali, joka auttaa meitä käyttöliittymässä ja yksinkertainen asettelu sivujemme rakentamisen aloittamiseksi. Mutta miten voimme navigoida sivujen välillä?

Yksinkertaisen esimerkin luomiseksi luodaan kaksi sivua: 'Käyttäjä', josta voimme saada luettelon tietokannan nykyisistä käyttäjistä, ja 'Hallintapaneeli' -sivu, jolla voimme näyttää joitain tilastoja.

app-routing.modules.ts: N sisällä kansion luomme tiedoston nimeltä 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 {} näyttää tältä:

RouterModule

Se on niin yksinkertaista: vain tuoda Routes ja @angular/router alkaen /dashboard voimme kartoittaa polut, jotka haluamme toteuttaa. Tässä luomme neljä polkua:

  • /login: Kotisivumme
  • /logout: Sivu, jolla käyttäjä voi todentaa
  • /users: Yksinkertainen polku käyttäjän kirjautumiseen ulos
  • dashboard: Ensimmäinen sivu, johon haluamme listata käyttäjät takapäästä

Huomaa, että / on sivumme oletuksena, joten jos käyttäjä kirjoittaa URL-osoitteen canActivate, sivu ohjaa automaattisesti tälle sivulle. Katso myös AuthGuard parametri: Tässä luomme viitteen luokkaan left-panel.component.html, jonka avulla voimme tarkistaa, onko käyttäjä kirjautunut sisään. Jos ei, se ohjaa sisäänkirjautumissivulle. Seuraavassa osassa näytän sinulle, kuinka luodaan tämä luokka.

Nyt meidän tarvitsee vain luoda valikko. Muista asetteluosassa, kun loimme Dashboard Users tiedosto näyttää tältä?

our.site.url/users

Tässä koodimme kohtaa todellisuuden. Nyt voimme rakentaa koodin ja testata sen URL-osoitteessa: Sinun pitäisi pystyä siirtymään Hallintapaneeli-sivulta Käyttäjät-kohtaan, mutta mitä tapahtuu, jos kirjoitat URL-osoitteen http://www.mysite.com/users/42 suoraan selaimessa?

kuvan vaihtoehtoinen teksti

Huomaa, että tämä virhe näkyy myös, jos päivität selaimen sen jälkeen, kun olet jo siirtynyt kyseiseen URL-osoitteeseen sovelluksen sivupaneelin kautta. Sallikaa minun ymmärtää tämä virhe viralliset asiakirjat missä on todella selvää:

Reititetyn sovelluksen tulisi tukea syvälinkkejä. Syvä linkki on URL-osoite, joka määrittää polun sovelluksen sisäiseen komponenttiin. Esimerkiksi http://www.mysite.com/ on syvä linkki sankarin tietosivulle, joka näyttää sankarin, jonka tunnus on: 42.

Ei ole ongelma, kun käyttäjä siirtyy kyseiseen URL-osoitteeseen käynnissä olevan asiakkaan sisällä. Kulmareititin tulkitsee URL-osoitteen ja reitit tälle sivulle ja sankarille.

Mutta napsauttamalla linkkiä sähköpostissa, kirjoittamalla se selaimen osoiteriville tai vain päivittämällä selain ollessasi sankarin tietosivulla - kaikki nämä toimet hoitaa selain itse, käynnissä olevan sovelluksen ulkopuolella. Selain pyytää suoraa palvelimelta kyseistä URL-osoitetta ohittamalla reitittimen.

Staattinen palvelin palauttaa rutiininomaisesti index.html-tiedoston, kun se saa pyynnön http://www.mysite.com/users/42 Mutta se hylkää src ja palauttaa 404 - Ei löydy -virheen, ellei sitä ole määritetty palauttamaan sen sijaan index.html-tiedostoa.

Tämän ongelman korjaaminen on hyvin yksinkertaista, meidän on vain luotava palveluntarjoajan tiedostokokoonpano. Koska työskentelen IIS: n kanssa täällä, näytän sinulle, miten se tehdään tässä ympäristössä, mutta konsepti on samanlainen Apachessa tai muussa verkkopalvelimessa.

Joten luomme tiedoston sisälle web.config kansio nimeltä angular-cli.json joka näyttää tältä:

{ '$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': {} } }

Sitten meidän on oltava varmoja siitä, että tämä resurssi kopioidaan käyttöönotettuun kansioon. Ainoa mitä meidän on tehtävä, on muuttaa Angular CLI -asetustiedostomme AuthGuard:

canActivateAuthGuard.ts

Todennus

Muistatko, kuinka meillä oli luokka helpers toteutettu reititysmääritysten asettamiseksi? Aina kun siirrymme toiselle sivulle, käytämme tätä luokkaa tarkistamaan, onko käyttäjä todennettu tunnuksella. Jos ei, ohjaamme sinut automaattisesti sisäänkirjautumissivulle. Tätä varten tiedosto on 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 { if (!this.helper.isAuthenticated()) { this.router.navigate(['/login']); return false; } return true; } } - luo se canActivate: n sisällä kansion ja näyttää siltä tältä:

Router

Joten joka kerta, kun vaihdamme sivua, menetelmä Helper kutsutaan, mikä tarkistaa, onko käyttäjä todennettu, ja jos ei, käytämme helpers esimerkiksi ohjata sisäänkirjautumissivulle. Mutta mikä tämä uusi menetelmä on helpers.ts luokka? localStorage -Kohdan alla luodaan tiedosto localStorage. Täällä meidän on hallittava sessionStorage, johon tallennamme takapäästä saamamme tunnuksen.

Huomautus

sessionStorage: Ssa voit myös käyttää evästeitä tai localStorage, ja päätös riippuu käytöksestä, jonka haluamme toteuttaa. Kuten nimestä voi päätellä, sessionStorage on käytettävissä vain selainistunnon ajan ja poistetaan, kun välilehti tai ikkuna suljetaan; se kuitenkin selviää sivujen uudelleenlatauksista. Jos tallentamiesi tietojen on oltava jatkuvasti käytettävissä, niin localStorage on parempi kuin import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; import { Subject } from 'rxjs/Subject'; @Injectable() export class Helpers { private authenticationChanged = new Subject(); constructor() { } public isAuthenticated():boolean 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()); } } Evästeet on tarkoitettu ensisijaisesti palvelinpuolen lukemiseen, kun taas Subject voidaan lukea vain asiakaspuolella. Joten kysymys on, kuka tarvitsee sovelluksessasi näitä tietoja - asiakas tai palvelin?


{ path: 'logout', component: LogoutComponent},

Onko todentamiskoodillamme järkevää nyt? Palataan localStorage -sivulle luokan myöhemmin, mutta nyt kierretään hetkeksi takaisin reititysmäärityksiin. Katsokaa tätä riviä:

components/login

Tämä on komponenttimme kirjautua ulos sivustosta, ja se on vain yksinkertainen luokka logout.component.ts: n puhdistamiseen. Luodaan se import { Component, OnInit } from '@angular/core'; import { Router } from '@angular/router'; import { Helpers } from '../../helpers/helpers'; @Component({ selector: 'app-logout', template:'' }) export class LogoutComponent implements OnInit { constructor(private router: Router, private helpers: Helpers) { } ngOnInit() { this.helpers.logout(); this.router.navigate(['/login']); } } -kohdassa kansio, jonka nimi on /logout:

localStorage

Joten joka kerta kun siirrymme URL-osoitteeseen 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']); }); } } poistetaan ja sivusto ohjaa sisäänkirjautumissivulle. Lopuksi, luodaan app.component.ts kuten tämä:

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(); } }

Kuten näette, olemme toistaiseksi koodanneet valtakirjamme täältä. Huomaa, että tässä kutsumme palveluluokkaa; Luomme nämä palveluluokat, jotta pääsemme käyttöliittymään seuraavassa osassa.

Lopuksi meidän on palattava takaisin Subject -sivulle tiedosto, sivuston asettelu. Jos käyttäjä on todennettu, se näyttää valikko- ja otsikko-osiot, mutta jos ei, asettelua muutetaan näyttämään vain kirjautumissivumme.

Observable

Muista Observable luokka auttajaluokassamme? Tämä on authentication. app.component.html s tarjoavat tukea viestien lähettämiseen julkaisijoiden ja tilaajien välillä sovelluksessasi. Aina kun todennustunnus muuttuu, Menu ominaisuus päivitetään. Tarkastellaan services tiedosto, sillä on todennäköisesti järkevämpää nyt:

token.service.ts

Palvelut

Tässä vaiheessa olemme siirtymässä eri sivuille, todentamassa asiakkaan puolta ja tekemällä hyvin yksinkertaisen asettelun. Mutta miten voimme saada tietoja takapäästä? Suosittelen voimakkaasti, että kaikki taustakäyttö tehdään palvelu luokkiin. Ensimmäinen palvelumme on 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 { return this.http.post(this.pathAPI + 'token', body, super.header()).pipe( catchError(super.handleError) ); } } -palvelun sisällä kansio, nimeltään TokenService:

BaseService

Ensimmäinen kutsu takapäähän on POST-kutsu tunnussovellusliittymään. Tunnussovellusliittymä ei tarvitse tunnussarjaa otsikossa, mutta mitä tapahtuu, jos kutsumme toisen päätepisteen? Kuten voit nähdä täältä, 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); } } (ja palveluluokat yleensä) perivät super.header luokassa. Katsotaanpa tätä:

localStorage

Joten joka kerta, kun soitamme HTTP-puhelun, toteutamme pyynnön otsikon vain user.ts: lla. Jos tunnus on export class User { id: number; name: string; } sitten se liitetään otsikkoon, mutta jos ei, asetamme vain JSON-muodon. Toinen asia, jonka voimme nähdä tässä, on se, mitä tapahtuu, jos todennus epäonnistuu.

Kirjautumiskomponentti soittaa palveluluokkaan ja palveluluokka soittaa loppupäähän. Kun tunnus on saatu, auttajaluokka hallitsee tunnusta, ja nyt olemme valmiita hakemaan luettelon käyttäjistä tietokannastamme.

Saadaksesi tietoja tietokannasta, varmista ensin, että sovitamme malliluokat vastauksessamme taustanäkymämallien kanssa.

Sisään 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 { return this.http.get(this.pathAPI + 'user', super.header()).pipe( catchError(super.handleError)); }

Ja voimme nyt luoda SeedAPI.Web.API tiedosto:

Web.API

Takapää

Pika-aloitus

Tervetuloa Web API Core 2 -sovelluksen ensimmäiseen vaiheeseen. Ensimmäinen asia, mitä tarvitsemme, on luoda ASP.Net-ydinverkkosovellus, jota kutsumme ViewModels.

Uuden tiedoston luominen

Muista valita Tyhjä-malli puhtaaseen alkuun, kuten näet alla:

valitse Tyhjä malli

Siinä kaikki, luomme ratkaisun tyhjällä verkkosovelluksella. Nyt arkkitehtuurimme on sellainen kuin alla luetellaan, joten meidän on luotava erilaiset projektit:

nykyinen arkkitehtuurimme

Voit tehdä tämän napsauttamalla vain jokaisen kohdalla hiiren kakkospainikkeella ratkaisua ja lisäämällä 'Class Library (.NET Core)' -projektin.

lisää

Arkkitehtuuri

Edellisessä osiossa loimme kahdeksan projektia, mutta mihin ne on tarkoitettu? Tässä on yksinkertainen kuvaus kustakin:

  • Interfaces: Tämä on käynnistysprojektimme ja missä päätepisteet luodaan. Tässä asetamme JWT: n, ruiskutusriippuvuudet ja ohjaimet.
  • Commons: Tässä suoritetaan muunnokset tietotyypistä, jonka ohjaimet palauttavat vastauksissa käyttöliittymään. On hyvä tapa sovittaa nämä luokat etupään malleihin.
  • Models: Tästä on apua injektointiriippuvuuksien toteuttamisessa. Staattisesti kirjoitetun kielen pakottava etu on, että kääntäjä voi auttaa varmistamaan, että koodi, johon koodi perustuu, tosiasiallisesti täytetään.
  • ViewModels: Kaikki jaetut toiminnot ja apuohjelmakoodi ovat täällä.
  • Models: On hyvä käytäntö olla sovittamatta tietokantaa suoraan käyttöliittymän suuntaan Maps, joten ViewModels on luoda etupäästä riippumaton entiteettitietokantaluokka. Tämä antaa meille mahdollisuuden tulevaisuudessa muuttaa tietokantaamme ilman, että sillä on välttämättä vaikutusta käyttöliittymään. Se auttaa myös silloin, kun haluamme yksinkertaisesti tehdä joitakin korjauksia.
  • Models: Tässä kartoitamme Services kohteeseen Repositories ja päinvastoin. Tätä vaihetta kutsutaan ohjaimien ja palveluiden välillä.
  • App_Start: Kirjasto koko liiketoimintalogiikan tallentamiseen.
  • JwtTokenConfig.cs: Tämä on ainoa paikka, jossa kutsumme tietokantaa.

Viitteet näyttävät tältä:

Viitekaavio

JWT-pohjainen todennus

Tässä osiossa näemme tunnuksen todennuksen peruskokoonpanon ja menemme hieman syvemmälle turvallisuuden aiheeseen.

Aloita JSON-verkkotunnuksen (JWT) asettaminen luomalla seuraava luokka 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(); }); } } } : n sisällä kansio nimeltä appsettings.json. Sisällä oleva koodi näyttää tältä:

'Jwt': { 'Key': 'veryVerySecretKey', 'Issuer': 'http://localhost:50498/' }

Vahvistusparametrien arvot riippuvat kunkin projektin vaatimuksista. Voimassa oleva käyttäjä ja yleisö, jonka voimme asettaa lukemaan määritystiedoston ConfigureServices:

startup.cs

Sitten meidän tarvitsee kutsua sitä vain // 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(); } : sta menetelmä TokenController.cs:

appsettings.json

Nyt olemme valmiita luomaan ensimmäisen ohjaimen nimeltä 'veryVerySecretKey'. Asetettu arvo LoginViewModel kohteeseen ViewModels pitäisi olla sama kuin käytämme tunnuksen luomiseen, mutta ensin luodaan namespace SeedAPI.ViewModels { public class LoginViewModel : IBaseViewModel { public string username { get; set; } public string password { get; set; } } } sisällä 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; } } } projekti:

BuildToken

Ja lopuksi ohjain:

Authenticate

identityDbContext menetelmä luo tunnuksen annetulla turvakoodilla. Models methodilla on juuri käyttäjän vahvistaminen tällä hetkellä kovakoodattu, mutta meidän on kutsuttava tietokanta vahvistamaan se lopulta.

Sovelluksen konteksti

Entity Frameworkin asentaminen on todella helppoa, koska Microsoft lanseerasi Core 2.0 -version - EF-ydin 2 lyhyesti. Aiomme perehtyä syvemmälle koodin ensimmäinen malli, joka käyttää Context, joten varmista ensin, että olet asentanut kaikki riippuvuudet. NuGetin avulla voit hallita sitä:

Riippuvuuksien saaminen

ApplicationContext.cs voimme luoda täällä IApplicationContext.cs kansioon kaksi tiedostoa, EntityBase ja EntityBase. Tarvitsemme myös User.cs luokassa.

Luokat

IdentityUser kukin entiteettimalli perii tiedostot, mutta namespace SeedAPI.Models { public class User : IdentityUser { public string Name { get; set; } } } on identiteettiluokka ja ainoa entiteetti, joka perii 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().ToArray(); } } } Alla ovat molemmat luokat:

ApplicationContext.cs namespace SeedAPI.Models.Context { public class ApplicationContext : IdentityDbContext, IApplicationContext { private IDbContextTransaction dbContextTransaction; public ApplicationContext(DbContextOptions options) : base(options) { } public DbSet UsersDB { get; set; } public new void SaveChanges() { base.SaveChanges(); } public new DbSet Set() where T : class { return base.Set(); } 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(); } } } }

Nyt olemme valmiita luomaan App_Start, joka näyttää tältä:

Web.API

Olemme todella lähellä, mutta ensin meidän on luotava lisää luokkia, tällä kertaa 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(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' }); } } } -kansiossa oleva kansio projekti. Ensimmäinen luokka on alustaa sovelluskonteksti ja toinen on luoda näytetietoja vain testausta varten kehityksen aikana.

// 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(); } App_Start

Ja kutsumme heitä käynnistystiedostostamme:

DependencyInjectionConfig.cs

Riippuvuuden injektio

On hyvä käytäntö käyttää riippuvuusinjektiota eri projektien välillä liikkumiseen. Tämä auttaa meitä kommunikoimaan ohjainten ja kartoittajien, kartoittajien ja palvelujen sekä palveluiden ja arkistojen välillä.

Kansiossa namespace SeedAPI.Web.API.App_Start { public class DependencyInjectionConfig { public static void AddScope(IServiceCollection services) { services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); } } } luomme tiedoston Map ja se näyttää tältä:

Service

kuvan vaihtoehtoinen teksti

Meidän on luotava jokaiselle uudelle entiteetille uusi Repository, startup.cs ja // 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(); } ja sovitettava ne tähän tiedostoon. Sitten meidän on vain soitettava sille 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 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) { } } } tiedosto:

Authorize

Lopuksi, kun meidän on saatava käyttäjäluettelo tietokannasta, voimme luoda ohjaimen käyttämällä tätä riippuvuusinjektiota:

Map

Katso miten Maps attribute on täällä, jotta voidaan varmistaa, että käyttöliittymä on kirjautunut sisään ja miten riippuvuuden ruiskutus toimii luokan konstruktorissa.

Meillä on vihdoin kutsu tietokantaan, mutta ensin meidän on ymmärrettävä ViewModels projekti.

UserMap.cs Projekti

Tämä vaihe on vain kartoittaa 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 GetAll() { return DomainToViewModel(userService.GetAll()); } public UserViewModel DomainToViewModel(User domain) { UserViewModel model = new UserViewModel(); model.name = domain.Name; return model; } public List DomainToViewModel(List domain) { List model = new List(); 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; } } } tietokantamalleihin ja niistä. Meidän on luotava yksi kullekin entiteetille ja edellisen esimerkkimme mukaisesti Services tiedosto näyttää tältä:

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 GetAll() { return repository.GetAll(); } } }

Näyttää siltä, ​​että jälleen kerran riippuvuuden injektointi toimii luokan rakentajalla, joka yhdistää Mapsin Services-projektiin.

Repositories Projekti

Tässä ei ole liikaa sanottavaa: Esimerkkimme on todella yksinkertainen, eikä meillä ole liiketoimintalogiikkaa tai -koodia kirjoittaa tähän. Tämä projekti osoittautuu hyödylliseksi tulevissa vaativissa vaatimuksissa, kun meidän on laskettava tai tehtävä logiikkaa ennen tietokannan tai ohjaimen vaiheita tai niiden jälkeen. Esimerkin mukaan luokka näyttää melko paljaalta:

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(domain); return us; } catch (Exception ex) { //ErrorManager.ErrorHandler.HandleError(ex); throw ex; } } public bool Update(User domain) { try { //domain.Updated = DateTime.Now; UpdateUser(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); return true; } else { return false; } } catch (Exception ex) { //ErrorManager.ErrorHandler.HandleError(ex); throw ex; } } public List GetAll() { try { return Context.UsersDB.OrderBy(x => x.Name).ToList(); } catch (Exception ex) { //ErrorManager.ErrorHandler.HandleError(ex); throw ex; } } } } Projekti

Olemme siirtymässä tämän opetusohjelman viimeiseen osioon: Meidän on vain soitettava puheluita tietokantaan, joten luomme

|_+_|
tiedosto, josta voimme lukea, lisätä tai päivittää käyttäjiä tietokantaan.

|_+_|

Yhteenveto

Tässä artikkelissa selitin, kuinka luoda hyvä arkkitehtuuri Angular 5: n ja Web API Core 2: n avulla. Tässä vaiheessa olet luonut perustan suurelle projektille, jonka koodi tukee vaatimusten suurta kasvua.

Totuus on, mikään ei kilpaile Java-käyttöjärjestelmän kanssa käyttöliittymässä, ja mikä voi kilpailla C #: n kanssa, jos tarvitset SQL Serverin ja Entity Frameworkin tukea takapäässä? Joten tämän artikkelin idea oli yhdistää kahden maailman parhaat puolet, ja toivon, että olet nauttinut siitä.

Mitä seuraavaksi?

Jos työskentelet ryhmässä Kulmalliset kehittäjät luultavasti etu- ja takapäässä voi olla erilaisia ​​kehittäjiä, joten hyvä idea synkronoida molempien joukkueiden ponnistelut voi olla integroida Swagger Web API 2: een. Swagger on loistava työkalu RESTFul-sovellusliittymiesi dokumentointiin ja testaamiseen. Lue Microsoft-opas: Aloita Swashbuckle ja ASP.NET Core .

Jos olet vielä hyvin uusi Angular 5: ssä ja sinulla on vaikeuksia seurata, lue Angular 5 -opetusohjelma: Vaiheittainen opas ensimmäiseen Angular 5 -sovellukseesi kaveri ApeeScapeer Sergey Moiseev.

Perustietojen ymmärtäminen

Pitäisikö sinun käyttää Visual Studio -koodia tai Microsoft Visual Studiota käyttöliittymän muokkaamiseen?

Voit käyttää kumpaakin IDE: tä käyttöliittymässä, monet ihmiset haluavat liittyä käyttöliittymään Web-sovelluskirjastoon ja automatisoida käyttöönoton. Pidän parempana pitää käyttöliittymäkoodi erillään taustasta ja löytänyt Visual Studio -koodin todella hyväksi työkaluksi, erityisesti älykäs, kanssa Typescript-koodille.

Mikä on kulmamateriaalisuunnittelu, ja pitäisikö sitä käyttää vai ei?

Kulmamateriaali on käyttöliittymäkomponenttikehys, jota ei tarvitse käyttää. UI-komponenttikehykset auttavat meitä järjestämään ulkoasun ja reagoivan verkkosivustolla, mutta meillä on paljon niitä markkinoilla, kuten bootstrap ja muut, ja voimme valita haluamasi ulkoasun.

Mikä on kulmareitti ja navigointi?

Kulmareititin mahdollistaa navigoinnin yhdestä näkymästä toiseen, kun käyttäjät suorittavat sovellustehtäviä. Se voi tulkita selaimen URL-osoitteen ohjeeksi siirtyä asiakkaan luomaan näkymään. Kehittäjä saa asettaa URL-osoitteita, parametreja ja voimme saastuttaa CanAuthenticate-sovelluksella, että voimme vahvistaa käyttäjän autentikaation

Mitä hyötyä riippuvuusinjektiosta on C #: ssä?

Usein luokat tarvitsevat pääsyn toisiinsa, ja tämä suunnittelumalli osoittaa, kuinka luodaan löyhästi kytkettyjä luokkia. Kun kaksi luokkaa on tiukasti kytketty, ne ovat yhteydessä binääriseen assosiaatioon.

Mikä on JWT-pohjainen todennus?

JSON Web Token (JWT) on kompakti kirjasto tietojen turvalliseen lähettämiseen osapuolten välillä JSON-objektina. JWT: t voidaan salata myös salassapitoon osapuolten välillä, keskitymme allekirjoitettuihin tunnuksiin.

10 TikTok-editorisovellusta, jotka ylittävät alkuperäisen muokkausliittymän

Muokkaus

10 TikTok-editorisovellusta, jotka ylittävät alkuperäisen muokkausliittymän
Tulevaisuuden ajoneuvokäyttöliittymät tulevat olemaan hämmästyttäviä

Tulevaisuuden ajoneuvokäyttöliittymät tulevat olemaan hämmästyttäviä

Ui-Suunnittelu

Suosittu Viestiä
Kulttuurienvälinen suunnittelu ja UX: n rooli
Kulttuurienvälinen suunnittelu ja UX: n rooli
Blockchainin eston purkaminen: Pelaaminen on miinusadoptio
Blockchainin eston purkaminen: Pelaaminen on miinusadoptio
Salamavalokuvaus iPhonella: Milloin ja miten salamaa käytetään
Salamavalokuvaus iPhonella: Milloin ja miten salamaa käytetään
Kuinka hyödyntää temaattista analyysiä paremmasta käyttöjärjestelmästä
Kuinka hyödyntää temaattista analyysiä paremmasta käyttöjärjestelmästä
Suunnittele tulevaisuus: työkalut ja tuotteet, jotka odottavat meitä
Suunnittele tulevaisuus: työkalut ja tuotteet, jotka odottavat meitä
 
Johdanto SQL-ikkunatoimintoihin
Johdanto SQL-ikkunatoimintoihin
Valloita merkkijonohaku Aho-Corasick-algoritmilla
Valloita merkkijonohaku Aho-Corasick-algoritmilla
Työskentely Google Sheetsin ja Apps Scriptin kanssa
Työskentely Google Sheetsin ja Apps Scriptin kanssa
Kontekstitietoiset sovellukset ja monimutkainen tapahtumien käsittelyarkkitehtuuri
Kontekstitietoiset sovellukset ja monimutkainen tapahtumien käsittelyarkkitehtuuri
Suunnittelun periaatteet - johdanto visuaaliseen hierarkiaan
Suunnittelun periaatteet - johdanto visuaaliseen hierarkiaan
Luokat
MobiilisuunnitteluLiikevaihdon KasvuTietojenkäsittely Ja TietokannatKetteräSuunnittelijan ElämäInnovaatioTeknologiaUi-SuunnitteluAmmuntaMuokkaus

© 2023 | Kaikki Oikeudet Pidätetään

socialgekon.com