Kun työskentelemme Angularin ja RxJS:n kanssa, on tärkeää ymmärtää, kuinka virtaus (observable stream) hallitaan oikein, jotta vältämme muistivuodot ja suorituskykyongelmat. Yksi yleisimmistä ongelmista, johon kehittäjät törmäävät, on se, että virtaus ei lopu automaattisesti tilauksen jälkeen. Tämä voi johtaa muistin vuotamiseen ja jopa sovelluksen hidastumiseen. On olemassa useita strategioita, joiden avulla voimme varmistaa, että virtaus päättyy ennakoitavasti ja että muistivuotoja ei synny.
Ensimmäinen lähestymistapa on käyttää first()-operaattoria RxJS:ssä. Tämä takaa, että virtaus päättyy heti, kun se on saanut ensimmäisen tuloksen. Esimerkiksi WeatherService-luokassa, joka vastaa säätietojen hakemisesta, voimme käyttää tätä operaattoria seuraavasti:
Tässä esimerkissä, kun käyttäjä hakee säätietoja, virtaus päättyy automaattisesti ensimmäisen tuloksen jälkeen. Tämä estää muistin vuotamisen, koska tilaus päättyy heti, kun tulos on saatu.
Toinen yleisesti käytetty strategia on takeUntilDestroyed. Tämä lähestymistapa on erityisen hyödyllinen komponentteissa, jotka tarvitsevat useita päivityksiä, kuten komponentit, jotka vastaanottavat uutta dataa käyttäjän syötteiden perusteella. Esimerkiksi CurrentWeatherComponent-komponentti voi tarvita jatkuvia päivityksiä säätiedoista aina, kun käyttäjä syöttää uuden hakutekstin. Tällöin voimme käyttää takeUntilDestroyed-operaattoria seuraavasti:
Tässä ratkaisussa takeUntilDestroyed toimii siten, että se lopettaa tilauksen automaattisesti, kun komponentti tuhoutuu. Tämä estää muistivuodot ja varmistaa, että komponentti vastaanottaa päivityksiä vain silloin, kun se on aktiivinen.
On kuitenkin tärkeää huomata, että takeUntilDestroyed voi olla käytettävissä vain sellaisissa konteksteissa, joissa injektoidaan DestroyRef. Tämä tarkoittaa, että se toimii vain konstruktorissa, ei elinkaaren muiden vaiheiden aikana, kuten ngOnInit:ssa.
Vaikka nämä lähestymistavat auttavat estämään muistivuotoja, on myös tärkeää miettiä, miten käsittelemme tilauksen tekemistä ja tilan hallintaa ylipäätään. RxJS ja reaktiivinen ohjelmointi tarjoavat erinomaisen tavan hallita asynkronisia tapahtumia, mutta meidän on varottava sekoittamasta reaktiivista ja imperatiivista ohjelmointityyliä.
Reaktiivinen ohjelmointi on osa Angularin arkkitehtuuria, ja sen etuna on, että me voimme käsitellä dataa ilman, että meidän tarvitsee huolehtia tilausten manuaalisesta hallinnasta. Tämä mahdollistaa koodin, joka on puhdas, helposti ylläpidettävä ja vähemmän alttiina virheille.
Tavanomaisessa imperatiivisessa ohjelmoinnissa tapahtuma käsitellään suoraan, ja koodia kirjoitetaan "askel askeleelta" sen sijaan, että käytettäisiin reaktiivisia striimejä. Tämä ei ole ongelma sinänsä, mutta se voi johtaa vaikeasti hallittavaan koodiin, erityisesti suurissa sovelluksissa, joissa on paljon tilauksia ja eri komponenttien välistä vuorovaikutusta.
Angularin reaktiivinen ohjelmointi mahdollistaa sen, että voimme käsitellä virtauksia suoraan komponenteissa ilman, että joudumme tekemään imperatiivista koodia. Esimerkiksi voimme käyttää async-putkea Angularin templaateissa, jotta voimme sitoa Observable-virran suoraan käyttöliittymään. Tämä vähentää koodin määrää ja poistaa tarpeen huolehtia tilausten purkamisesta.
Tässä on esimerkki, jossa async-putki korvasi perinteisen tilauksen:
Templaatissa voimme käyttää async-putkea seuraavasti:
Tämä lähestymistapa poistaa tarpeen käsitellä tilausta suoraan, ja Angular hoitaa sen puolestamme, mukaan lukien tilauksen purkaminen komponentin tuhoutuessa. Tämä on erittäin tehokas tapa hallita asynkronisia operaatioita, ja se on tyypillinen käytäntö reaktiivisessa ohjelmoinnissa Angularin kanssa.
On myös huomattava, että vaikka reaktiivinen ohjelmointi tuo monia etuja, meidän on silti oltava tarkkoja siitä, missä ja miten käsittelemme tilauksia. On paikkoja, joissa tilauksen suorittaminen on edelleen järkevää, kuten silloin, kun käytämme form-control-komponentteja ja seuraamme käyttäjän syötteitä. Tässä tapauksessa voimme käyttää debounceTime- ja filter-operaattoreita yhdessä takeUntilDestroyed-operaattorin kanssa, jotta voimme reaktiivisesti käsitellä käyttäjän syötteet ja tehdä tarvittavat hakuoperaatiot ilman imperatiivista koodia.
Tässä koodissa käytämme reaktiivista ohjelmointia, jossa kaikki tilaukset hallitaan RxJS-operaattoreiden avulla ja sovelluksen elinkaaren aikana virtaus päättyy automaattisesti, kun komponentti tuhoutuu. Tämä tekee koodista puhdasta ja ylläpidettävää, samalla kun vältämme imperatiivisen ohjelmoinnin virheitä.
Lopuksi on tärkeää ymmärtää, että reaktiivinen ohjelmointi ei ole vain tiettyjen operaatioiden käyttöä RxJS:ssä; se on lähestymistapa, joka vaikuttaa koko sovelluksen arkkitehtuuriin. Angularin tarjoamat työkalut mahdollistavat tehokkaan ja selkeän tavan hallita asynkronisia operaatioita, ja niiden käyttö oikein voi parantaa merkittävästi sovelluksen laatua ja suorituskykyä.
Kuinka toteuttaa kirjautumis- ja uloskirjautumismekanismi Angular-sovelluksessa oikein?
Yksinkertainen kirjautumismekanismi voidaan toteuttaa Angularissa hyödyntämällä RxJS:n tarjoamia operaatioita ja AuthService-luokan tilavirtauksia. HomeComponent-luokan login()-funktio käynnistää kirjautumisprosessin AuthServicen avulla, ja onnistuneen autentikoinnin jälkeen käyttäjä ohjataan automaattisesti /manager-reitille. Tämä reaktiivinen navigointi mahdollistuu yhdistämällä kaksi havaintovirtaa: authStatus$ ja currentUser$. Mikäli authStatus.isAuthenticated on tosi ja currentUser._id ei ole tyhjä merkkijono, tunnistus katsotaan onnistuneeksi.
Tämä toteutus perustuu combineLatest-operaattoriin, joka yhdistää molemmat tilavirtaukset ja suodattaa arvot filter-operaattorilla. Navigointi tapahtuu tap-operaattorilla heti kun ehto täyttyy. Lopuksi havaintovirta aktivoidaan subscribe()-kutsulla, mikä on kriittinen osa — ilman sitä kirjautumisprosessi jäisi passiiviseksi.
Kirjautumisen yhteydessä JWT-tunnus tallennetaan selaimen localStorageen. Tämä mahdollistaa istunnon säilyttämisen sivun uudelleenlatauksen yhteydessä. JWT voidaan tarkistaa selaimen kehitystyökaluilla, kohdassa Application → Local Storage. On kuitenkin tärkeää muistaa, ettei kehitysvaiheessa käytettyjä kirjastoja, kuten InMemoryAuthService tai fake-jwt-sign, tule käyttää tuotantoympäristössä.
HomeComponentin lisäksi uloskirjautuminen toteutetaan LogoutComponentin avulla. Sen ngOnInit-metodi kutsuu authService.logout(true)-funktiota ja ohjaa käyttäjän takaisin etusivulle. Tällä varmistetaan, että JWT poistetaan säilöstä, ja tilavirrat tyhjennetään. Kirjautumisen ja uloskirjautumisen testaaminen tulisi aina tehdä selaimen tallennustilan tarkkailulla ja koodin läpikäynnillä askel kerrallaan.
Istunnon jatkaminen ilman uudelleenkirjautumista on olennainen osa käyttäjäystävällistä käyttökokemusta. Tätä varten JWT:n voimassaolon tarkistaminen tehdään hasExpiredToken()-funktiolla, joka dekoodaa tokenin ja vertaa sen vanhenemisaikaa nykyhetkeen. Mikäli token on edelleen voimassa, käyttäjän kirjautumistila rekonstruoidaan getAuthStatusFromToken()-funktiolla. Tällöin AuthServicen konstruktorissa tarkastetaan tilanne: jos token on vanhentunut, käyttäjä kirjataan ulos ja token poistetaan säilöstä, muutoin autentikointitila palautetaan tilavirtaan.
Tärkeää on myös käyttäjän tietojen uudelleenlataus istunnon jatkuessa. Tämä toteutetaan RxJS-putkella nimeltä getAndUpdateUserIfAuthenticated, joka suodattaa tilavirran autentikoitujen tilojen perusteella, ja hakee käyttäjätiedot getCurrentUser()-metodilla. Näin currentUser$ päivittyy oikeaksi reaktiivisesti. Tämä putki liitetään kirjautumisen yhteyteen login()-funktiossa sekä istunnon jatkamisen yhteydessä resumeCurrentUser$-ominaisuuden kautta.
On tärkeää ymmärtää, että istunnon jatkaminen ei ole pelkkä mukavuustekijä, vaan turvallisuusnäkökohta. Vanhenemattoman tokenin hyväksyminen ilman tarkistusta voi johtaa tilaturvallisuuden heikentymiseen. Siksi tokenin tarkistuksen lisäksi käyttöoikeudet ja käyttäjätilan hallinta tulee varmistaa erikseen jokaisessa komponentissa, jossa luotetaan kirjautumiseen.
Lisäksi tuotantokoodissa tokenit tulee säilöä turvallisesti, esimerkiksi HttpOnly-evästeinä eikä pelkästään localStoragessa, jotta ne eivät ole haavoittuvaisia XSS-hyökkäyksille. Kirjautumisprosessiin tulisi myös lisätä virheenkäsittelylogiikka ja käyttäjälle näkyvät viestit, jos tunnistus epäonnistuu.
Istunnon jatkuminen tulisi tarkistaa aina sovelluksen käynnistyessä, jotta käyttäjätilan eheys säilyy. Tämä prosessi kannattaa eristää keskitettyyn palveluun, joka vastaa kirjautumistilasta, ja jota kaikki komponentit voivat hyödyntää tilavirtana.
Miten suojata REST API -pohjaisia sovelluksia JWT-autentikoinnilla ja salauksella?
Kun rakennat sovelluksia, jotka hyödyntävät REST- ja GraphQL-rajapintoja, on tärkeää muistaa, että API:n turvallisuus ei ole vain mahdollista toteuttaa, vaan se on myös pakollista. Yksi tapa suojata API:n käyttöä on käyttää JSON Web Token (JWT) -pohjaisia tunnistautumismenetelmiä ja tehokasta salasanojen käsittelyä, kuten bcryptjs-kirjastoa salauksineen. Tässä osassa käydään läpi, kuinka voit luoda turvallisen käyttäjäautentikoinnin, joka varmistaa, ettei käyttäjien salasanat tallennu avoimesti tietokantaan ja että vain oikeat käyttäjät voivat käyttää suojattuja resursseja.
Kun toteutat käyttäjähallintaa API:ssa, ensimmäinen askel on varmistaa, että käyttäjän salasanat eivät jää näkyville. Salasana tulee suojata salauksella ennen sen tallentamista tietokantaan. Tähän käytämme bcryptjs-kirjastoa, joka tarjoaa tehokkaan ja turvallisen tavan suolata ja hajauttaa salasanoja. Salasanan hajauttamisen jälkeen emme voi enää palauttaa alkuperäistä salasanaa, mutta voimme vertailla hajautettuja versioita käyttäjän syöttämän salasanan ja tallennetun salasanan välillä, varmistaen siten, että oikea salasana on annettu.
JWT toimii tunnistusjärjestelmänä, joka tuottaa käyttäjälle tunnuksen onnistuneen kirjautumisen jälkeen. Tämä tunnus, tai access token, liitetään API-pyyntöihin, jolloin varmistetaan, että pyynnön lähettäjä on autentikoitu ja että hänellä on oikeudet suorittaa pyytämänsä toiminto. JWT:llä on se etu, että se on itsenäinen ja kantaa mukanaan kaikki tarvittavat tiedot, kuten käyttäjän roolin ja sähköpostin, jotka voidaan liittää suoraan API-pyyntöihin.
Kun käyttäjä kirjautuu sisään, autentikointipalvelu tarkistaa käyttäjän syöttämän sähköpostin ja salasanan. Jos ne ovat oikein, JWT luodaan ja palautetaan käyttäjälle. Tämän jälkeen tämä token liitetään kaikkiin seuraaviin pyyntöihin, jolloin palvelin voi tunnistaa, että pyyntö tulee autentikoidulta käyttäjältä.
Autentikointimiddleware eli väliohjelmointi toimii API:ssa portinvartijana. Väliohjelmointi tarkistaa aina, onko käyttäjä autentikoitu ja omaa tarvittavat oikeudet pyydetyn resurssin käyttöön. Esimerkiksi, jos API vaatii käyttäjältä tiettyä roolia, kuten "Manager", väliohjelmointi varmistaa, että vain tämä rooli voi käyttää API-pistettä. Middleware voi myös tarkistaa, onko käyttäjä oikeutettu muokkaamaan vain omia tietojaan vai voiko hän muokata myös muiden käyttäjien tietoja, kuten pääkäyttäjä.
On tärkeää huomata, että salasanan käsittelyssä on otettava huomioon kaikki mahdolliset tietoturvariskit, kuten käyttäjän syöttämä sähköposti ja salasana. Esimerkiksi sähköpostiosoite on kirjainkoon suhteen herkät tiedot, joten se tulisi aina käsitellä pieniksi kirjaimiksi ennen vertailua tietokannan tallennettuun sähköpostiin. Samoin käyttäjän syöttämä salasanan tulee olla suojattu asianmukaisella suolaamisella ja hajautuksella, jotta mahdolliset tietovuodot eivät altistaisi käyttäjien henkilökohtaisia tietoja.
Jatkossa on myös tärkeää ottaa käyttöön vahva validointi ja sanitointi kaikelle käyttäjältä saapuville tiedoille, kuten sähköpostiosoitteille ja mahdollisille syötteille, jotka voivat sisältää haitallisia skriptejä tai ei-toivottuja merkkejä. Näihin tarkoituksiin voidaan käyttää kirjastoja, kuten express-validator tai express-sanitizer, jotka voivat poistaa haitalliset elementit pyynnöistä ennen kuin ne käsitellään sovelluksessa.
Kun toteutat turvallisia rajapintoja ja varmista, että vain autentikoidut ja valtuutetut käyttäjät pääsevät käsiksi suojattuihin resursseihin, tulee olla tarkkana myös virheviestien käsittelyssä. Väärät virheviestit voivat paljastaa järjestelmän toiminnallisuutta ja mahdollisesti antaa hyödyn haitallisille toimijoille. Esimerkiksi "väärä sähköpostiosoite tai salasana" -tyyppiset viestit voivat auttaa hyökkääjiä kohdistamaan tiettyjä syötteitä, joten viestien tulee olla mahdollisimman epäselviä ja yleisluonteisia.
Näiden lisäksi kannattaa aina seurata uusimpia turvallisuussuosituksia ja -käytäntöjä, sillä tietoturvauhat kehittyvät jatkuvasti. Käyttäjätietojen salassapito ja autentikointiprosessin turvallisuus ovat keskeisiä tekijöitä, joita ei saa jättää huomiotta.
Miksi Angularin yksikkötestit eivät oikeastaan ole yksikkötestejä?
Ohjelmistokehitys on täynnä kompromisseja: kiireisiä, palkkiovetoisia työistuntoja, kopioituja StackOverflow-vastauksia, irrallisia koodinpätkiä blogeista, npm-paketteja tai massiivisia kirjastoja kuten Angular. Meiltä odotetaan laadukkaita lopputuloksia arvioissa, jotka olemme heittäneet ilmoille ennen kuin todellinen työ edes alkoi. Näissä olosuhteissa virheet ovat väistämättömiä. Aikataulupaineet, kunnianhimo ja epäonniset arkkitehtuuriratkaisut saavat ongelmat vain kärjistymään.
Automaattiset testit varmistavat, että kirjoittamamme koodi toimii oikein – ja ennen kaikkea, että se pysyy oikeana muutosten keskellä. CI/CD-putket luovat toistettavia prosesseja, joihin inhimilliset virheet eivät vaikuta, mutta putki on vain yhtä hyvä kuin sen sisältämät testit. Angularissa testit jaetaan kahteen pääluokkaan: yksikkötesteihin (unit) ja päättymispisteiden testeihin (e2e). Yksikkötestien tulisi olla nopeita ja kevyitä, e2e-testit taas raskaampia ja hitaampia. Ongelma on kuitenkin siinä, että Angularin yksikkötestit eivät ole varsinaisia yksikkötestejä.
Ymmärtääksemme miksi näin on, täytyy palata yksikkötestauksen perusperiaatteisiin ja testivetoisen kehityksen etuihin. Hyvä yksikkötesti testaa tarkasti vain yhden funktion tai luokan toimintaa – eristettynä kaikista ulkoisista riippuvuuksista. Tämä vaatii, että testit ovat FAST-periaatteen mukaisia: nopeita (Fast), eristettyjä (Isolated), toistettavia (Repeatable), itsevarmentuvia (Self-verifying) ja oikea-aikaisia (Timely).
Yksikkötestin täytyy olla nopea – millisekunteja, ei sekunteja. Tällöin niitä voidaan ajaa tuhansia muutamassa minuutissa. Nopeus perustuu eristämiseen: testin ei tule keskustella tietokannan kanssa, ei tehdä verkkopyyntöjä, ei käsitellä DOM:ia. Tämä eristäminen tuo mukanaan toistettavuuden – testin tuloksen tulee olla aina sama. Vain silloin testi voi olla itsevarmentuva – sen ei tarvitse ulkoista tulkintaa. Ja ennen kaikkea: testi pitää kirjoittaa heti koodin rinnalle, ei päivää tai kahta myöhemmin, kun yksityiskohdat ovat jo unohtuneet.
Eristäminen onnistuu testiduplikaattien avulla. Sen sijaan, että injektoidaan oikea HttpService, käytetään väärennettyä versiota, joka palauttaa kontrolloituja tuloksia. Näin voidaan testata luokan toimintaa ilman sen ulkoisia riippuvuuksia.
Testikattavuuden on oltava riittävä – vähintään yhtä paljon testikoodia kuin tuotantokoodia. Jos näin ei ole, et testaa tarpeeksi. Testien tyypit voidaan jakaa kolmeen: yksikkötestit, integraatiotestit ja UI-testit. Yksikkötestit ovat nopeita ja halpoja, mutta testaavat vain yksittäistä yksikköä. Integraatiotestit yhdistävät komponentteja – mukaan lukien tietokantakutsut ja DOM-interaktiot – ja ovat hitaita ja raskaita ylläpitää. UI-testit simuloivat käyttäjän toimintaa ja ovat hitain ja haavoittuvin testityyppi. Sovelluksen UI muuttuu usein, ja sen testaaminen luotettavasti on haastavaa.
Angularin arkkitehtuuri tekee yksikkötestauksesta vaikeaa. Komponentti koostuu luokasta ja templatesta. Testataksesi komponenttia sinun on kosketettava DOM:ia, ja tämä vaatii TestBed:in käyttöä. TestBed on hidas ja hauras, sen konfigurointi on raskasta. Tämä tekee Angularin yksikkötesteistä väistämättä epäyksikkömäisiä. Parempi vaihtoehto on erottaa liiketoimintalogiikka palveluihin ja testata ne erikseen, jolloin varsinainen komponentti voidaan testata vain e2e- tai integraatiotestillä.
Työkalut kuten Spectator voivat auttaa yksinkertaistamaan Angularin testejä poistamalla tarpeetonta pohjakoodia ja mahdollistamalla nopeamman kirjoituksen. Erityisesti standalone-komponentit helpottavat testausta, koska niiden riippuvuudet määritellään suoraan eikä tarvitse luoda erillisiä moduleita.
Angularin nykyisessä tilassa komponentin yksikkötestaus ei ole ajan arvoista. Fokus kannattaa siirtää palveluiden ja logiikan yksikkötestaukseen sekä komponenttien integraatiotestaukseen. Cypress tarjoaa modernin tavan testata komponenttien ja UI:n toiminnallisuutta, ja se voidaan konfiguroida oletuksena Angular-sovellukselle. Cypressin avulla voidaan kirjoittaa automatisoituja hyväksymistestejä (AAT) loppukäyttäjän näkökulmasta – interaktioiden kautta.
Cypressin komponenttitestit, joita voidaan käyttää yksittäisten komponenttien testaukseen, tarjoavat selkeämmän ja vähemmän hauraan vaihtoehdon verrattuna perinteisiin Angularin TestBed-testeihin. Angular 17.1:n myötä Karma voidaan myös korvata modernilla Web Test Runnerilla, joka nopeuttaa testien suoritusta ja yksinkertaistaa konfiguraatiota. E2E-testeissä, jotka mallintavat loppukäyttäjän toimintaa, Cypress tarjoaa visuaalisen debuggerin ja CI-yhteensopivan testiajon.
Loppujen lopuksi yksikkötestien tarkoitus ei ole testata kaikkea – vaan testata oikein. Angularin arkkitehtuuri rikkoo yksikkötestauksen perustaa jo lähtökohtaisesti. Oikea lähestymistapa on siirtää testauksen painopiste sinne, missä yksikkötestaus todella toimii: puhtaaseen liiketoimintalogiikkaan. Komponenttien testi on integraatiotesti – eikä sitä pidä naamioida muuksi.
Ymmärtääkseen testauksen todellisen arvon, lukijan on myös ymmärrettävä testien kustannusrakenne ja ylläpidettävyys pitkällä aikavälillä. Halvat ja nopeat testit ovat investointi, jonka arvo kasvaa ajan myötä. Kalliit ja hauraat testit voivat nopeasti muuttua taakaksi. Testien määrä ei ole itseisarvo – niiden laatu, kohdistus ja jatkuva toistettavuus määrittelevät, tuovatko ne lisäarvoa vai pelkkää painolastia kehitystyöhön.
Miksi surrogaattimallit ja Bayesiläiset neuroverkot ovat tärkeitä raiteiden analyysissä?
Miten tyrannit nousevat valtaan ja miten me voimme elää paremmin
Mormonien maailmankuvan kohtalokkaat hetket ja niiden vaikutus yksilöihin
Miten sylkirauhaset ja mahalaukku toimivat ruoansulatuksessa?

Deutsch
Francais
Nederlands
Svenska
Norsk
Dansk
Suomi
Espanol
Italiano
Portugues
Magyar
Polski
Cestina
Русский