Poglavje 5 P2.2 Node.js, MVC in različna okolja
P2.2
Vsi testni primeri, predstavljeni v okviru predavanja P2.2, so na voljo v GitHub repozitoriju UL-FRI-DevOps-WebDevelopment/Snippets.
5.1 Node.js
Do sedaj smo si pogledali osnove jezika JavaScript v poglavju 4 in uporabo v okolju spletnega brskalnika, kar je bilo predstavljeno v prejšnjih podpoglavjih. V nadaljevanju pa si bomo pogledali okolje Node.js, kjer bomo lahko uporabili znanje jezika JavaScript izven konteksta spletnega brskalnika.
Okolje Node.js deluje na različnih platformah, in sicer Windows, macOS in Linux, ter si ga lahko prenesete in namestite iz uradne spletne strani, kjer je na voljo v obliki namestitvenega paketa.
5.1.1 Ozadje
Eden najbolj zahtevnih problemov pri implementaciji aplikacij, ki komunicirajo preko omrežja, je obvladovanje vhoda in izhoda, t.j. branje in pisanje podatkov na omrežje, trdi disk in ostale naprave. Posredovanje podatkov med različnimi lokacijami traja in pametno razporejanje je zelo pomembno pri odzivnem času sistema na uporabniško zahtevo.
Tradicionalni način obvladovanja vhoda in izhoda je z uporabo funkcije (npr. preberiDatoteko), ki začne brati datoteko in vrne odgovor šele, ko je datoteka v celoti prebrana. Takšen način se imenuje sinhroni V/I120.
Node.js je bil prvotno razvit z namenom enostavne podpore asinhronemu V/I. Takšen način komunikacije smo že srečali v kontekstu spletnega brskalnika z objektom XMLHttpRequest, ki smo ga spoznali v poglavju 4.17.2.
Način izvajanja V/I operacij pri Node.js je v okviru asinhronega vmesnika, ki omogoča nadaljevanje izvajanja programa, medtem ko se opravlja druga funkcionalnost in ob zaključku programa se izvede povratni klic
callback.
5.1.2 Asinhronost
Spletne strežnike, ki delujejo v enonitnem oz. večnitnem delovanju, smo si že ogledali v poglavju 1.3.1. Sedaj pa bomo še enkrat podrobneje primerjali sinhroni in asinhroni V/I na manjšem primeru, kjer program dostopa do dveh virov na internetu in nato izvede manjše procesiranje z rezultati.
V sinhronem okolju je očitno, da omenjeni scenarij izpeljemo tako, da zahtevi izvedemo eno za drugo. Ta metoda ima pomanjkljivost, da se bo druga zahteva začela šele, ko bo prva končana (glej Sinhrono, ena nadzorna nit na sliki 5.1). Skupni čas bo tako vsaj seštevek obeh odzivnih časov. To zagotovo ni učinkovita uporaba virov, večino časa ne izvaja operacij, ko se podatki prenašajo in sprejemajo preko omrežja.
Če želimo ta problem rešiti še vedno v sinhronem okolju, lahko dodamo novo nit nadzora. Obe zahtevi po omrežnih virih bi lahko izvedli istočasno, nato pa obe niti počakata na rezultate in se ponovno sinhronizirata, da združita rezultate (glej Sinhrono, dve nadzorni niti na sliki 5.1).
Na sliki 5.1 je prikazan diagram, kjer debelejše črte predstavljajo čas, ko se program normalno izvaja in tanjše črte predstavljajo čas, ko program čaka na V/I. Pri sinhronem modelu dostopa, je čas, potreben za V/I, del časovnice za določeno nadzorno nit. Pri asinhronem modelu dostopa predstavlja začetek V/I zahteve konceptualno delitev na časovni premici (glej Asinhrono na sliki 5.1). Nit, ki je sprožila V/I operacijo, nadaljuje sama, V/I operacija pa se zaključi s povratnim klicem callback.
Slika 5.1: Primerjava sinhronega in asinhronega načina delovanja
Drug način prikaza razlike je čakanje na zaključek V/I operacije, ki je pri sinhronem modelu implicitno, medtem ko je pri asinhronem modelu eksplicitno. Posledice asinhronosti so tako pozitivne kot negativne. Implementacija programov, ki ne ustrezajo enostavnemu modelu nadzora, je bolj enostavno, vendar pa po drugi strani programi, ki imajo enostaven model nadzora, izgledajo nenavadno, zaradi implementacije z asinhronim pristopom.
Že v prejšnjih poglavjih 4.17.4 in 4.17.5 smo ugotovili, da vsi povratni klici callback dodajajo predvsem odvečno kodo in težje sledenje izvajanju. Še vedno je na mestu vprašanje ali je asinhronost v splošnem dober način razvoja aplikacij - v vsakem primeru zahteva nekaj časa, da se na takšen način razvoja navadimo.
V kontekstu JavaScript sistemov je uporaba povratnih klicev callback za dosego asinhronosti zagotovo smiselna rešitev. Ena izmed prednosti jezika JavaScript je njegova enostavnost, kjer bi z vpeljavo več nadzornih niti zagotovo povečali kompleksnost. Čeprav uporaba povratnih klicev callback ne prinaša enostavne kode v konceptualnem smislu, je pa takšen pristop zagotovo dovolj enostaven in hkrati funkcionalno močan za razvoj visoko zmogljivih spletnih strežnikov.
5.1.3 Ukaz node
Ko je na sistemu nameščeno okolje Node.js, lahko izvajanje določenega programa zahtevamo z ukazom node. Recimo, da imamo testni program, ki je prikazan spodaj.
lep_pozdrav.js
Iz ukazne vrstice lahko program nato poženemo z ukazom node in ime datoteke, v kateri je shranjena JavaScript koda.
Konzola
Metoda console.log v Node.js je podobna funkcionalnosti v spletnem brskalniku in izpiše besedilo. V Node.js se besedilo izpiše na standardni izhod izvajalnega procesa, v nasprotju z JavaScript konzolo spletnega brskalnika.
Če poženemo node brez dodatnega parametra datoteke z izvorno kodo, pridobimo pozivnik, kjer lahko pišemo JavaScript kodo in takoj dobimo rezultat izvajanja.
Podobno kot spremenljivka console, je na voljo tudi spremenljivka process, ki je v okolju Node.js na voljo globalno. Omogoča različne načine interakcije s trenutnim procesom. Metoda exit poskrbi za izhod iz procesa, v parametru pa lahko podamo tudi status izhoda. Status se vrne programu, ki je zagnal node (v našem primeru je to ukazna lupina) in sicer \(0\), če je bil konec uspešen oz. katerokoli drugo število, če je prišlo do napake.
Če želimo pridobiti argumente ukazne vrstice, s katerimi smo pognali program, lahko dostopamo do process.argv.
pokazi_argumente.js
Konzola
$ node pokazi_argumente.js prvi --in drugi
[ 'node', 'pokazi_argumente.js', 'prvi', '--in', 'drugi' ]V Node.js okolju imamo na voljo vse standardne JavaScript globalne spremenljivke, kot so Array, Math in JSON. Funkcionalnosti, povezane s spletnim brskalnikom, kot sta npr. document in alert, pa niso prisotne.
Objekt globalnega konteksta, ki se v izvajalnem okolju spletnega brskalnika imenuje window, ga v Node.js okolju najdemo pod imenom global.
5.1.4 Moduli
Poleg nekaj spremenljivk, omenjenih v predhodnem poglavju, je v globalnem kontekstu Node.js na voljo malo funkcionalnosti. Če želimo dostopati do vgrajenih funkcionalnosti, moramo za to zahtevati sistemski modul.
Sistem modulov temelji na funkciji require, ki je vgrajena v Node.js in se uporablja za nalaganje vseh dodatnih modulov in knjižnic, ki so del programa v Node.js.
Pri klicu funkcije require mora Node.js najprej na podlagi podanega niza poti določi dejansko datoteko, ki jo je potrebno naložiti. Pot do datoteke se lahko začne za različnimi predponami:
/predstavlja korensko mapo datotečnega sistema,./predstavlja trenutno mapo in../predstavlja relativno lokacijo do trenutne poti modula.
Če npr. zahtevamo ./pomoc/pomoc v datoteki /home/dejan/pozeni.js, bo Node.js poskušal naložiti datoteko /home/dejan/pomoc/pomoc.js. Opazimo, da lahko končnico .js izpustimo. Ko pot do datoteke ni v obliki relativne poti, Node.js predvideva da gre za vgrajeni modul ali modul, nameščen v mapi node_modules. Če npr. zahtevamo require("fs") bomo naložili vgrajeni modul za dostop do datotečnega sistema, v primeru require("dejan") pa bo Node.js poskušal naložiti knjižnico, ki se nahaja v mapi node_modules/dejan/.
Za demonstracijo uporabe funkcije require si zamislimo enostaven primer z dvema JavaScript datotekama. Osnovna datoteka s programom se nahaja v datoteki uporabi_modul.js, ki uporabi metodo popaci, implementirano v datoteki popaci.js.
popaci.js
module.exports = function (niz) {
return niz
.split("")
.map(function (znak) {
return String.fromCharCode(znak.charCodeAt(0) + 5);
})
.join("");
};uporabi_modul.js
const popaci = require("./popaci");
// v 2. indeksu je dejanski argument, ki ga potrebujemo
let argument = process.argv[2];
console.log(popaci(argument));Konzola
5.1.5 Nameščanje z npm
Npm je spletni repozitorij JavaScript modulov, kjer jih je večina razvitih specifično za okolje Node.js. Ob namestitvi Node.js okolja na računalnik je rezultat namestitve tudi program npm, ki je priročen vmesnik za dostopanje do tega repozitorija.
Npr. eden izmed modulov na npm je figlet, ki pretvori besedilo v ASCII umetnino. Najprej je potrebno namestiti zahtevano knjižnico, nato pa poženemo program, kjer zahtevamo uporabe te knjižnice.
Konzola
$ npm install figlet
+ figlet@1.5.2
added 1 package from 1 contributor and audited 1 package in 0.996sPo zagonu ukaza npm install figlet se v mapi node_modules kreira nova podmapa figlet, kjer se nahaja omenjena knjižnica. Pri zahtevi po modulu z require("figlet") se naloži ta knjižnica in s klicem metode text se izvede pretvorba in izpiše na standardni izhod.
figlet.js
const figlet = require("figlet");
figlet.text("Spletno programiranje", (napaka, podatki) => {
if (napaka) {
console.error(napaka);
} else {
console.log(podatki);
}
});Mogoče nekoliko nenavadno funkcija text, namesto da bi vrnila rezultat, svoj odgovor vrača v 2. funkciji (podatki), ki je povratni klic callback, poleg tega pa vrača tudi 1. funkcijo (napaka), ki je tudi povratni klic callback. To je pogost načrtovalski vzorec v Node.js kodi, kjer se rezultat obdelave vrača asinhrono.
Konzola
$ node figlet.js
____ _ _ _ _
/ ___| _ __ | | ___| |_ _ __ ___ _ __ _ __ ___ __ _ _ __ __ _ _ __ ___ (_)_ __ __ _ _ __ (_) ___
\___ \| '_ \| |/ _ \ __| '_ \ / _ \ | '_ \| '__/ _ \ / _` | '__/ _` | '_ ` _ \| | '__/ _` | '_ \ | |/ _ \
___) | |_) | | __/ |_| | | | (_) | | |_) | | | (_) | (_| | | | (_| | | | | | | | | | (_| | | | || | __/
|____/| .__/|_|\___|\__|_| |_|\___/ | .__/|_| \___/ \__, |_| \__,_|_| |_| |_|_|_| \__,_|_| |_|/ |\___|
|_| |_| |___/ |__/ Npm ponuja še veliko več funkcionalnosti kot zgolj ukaz npm install. Po navadi dostopa do datoteke package.json, ki vsebuje številne dodatne informacije o programu ali knjižnici, npr. medsebojne odvisnosti ipd.
5.1.6 Modul za dostop do datotečnega sistema
Eden najbolj pogosto uporabljanih Node.js modulov, ki je že na voljo v privzeti namestiti Node.js okolja, je modul fs, ki pomeni filesystem. V modulu najdemo funkcije za delo z datotekami in mapami. V naslednjem primeru je prikazan dostop do datoteke, kjer v povratnem klicu dobimo vsebino datoteke v obliki niza. V prvem argumentu klica funkcije je pot do datoteke, drugi argument je nabor znakov (najbolj pogost je utf-8) za dekodiranje vsebine datoteke v obliki niza.
datotecni_sistem_besedilo.js
const fs = require("fs");
fs.readFile("datoteka.txt", "utf8", function(napaka, vsebina) {
if (napaka)
throw napaka;
console.log("Datoteka vsebuje:", vsebina);
});datoteka.txt
Maroko je obmorska država v severozahodni Afriki. Na severu meji na Sredozemsko morje in Gibraltarsko ožino, na zahodu na Atlantski ocean, na vzhodu na Alžirijo, s katero ima zaprto mejo, ter na jugozahodu na Zahodno Saharo, katere del sta okupirala skupaj z Mavretanijo, tako da ima prek zasedbenega območja dostop tudi do Mavretanije.
Konzola
$ node datotecni_sistem_besedilo.js
Datoteka vsebuje: Maroko je obmorska država v severozahodni Afriki. Na severu meji na Sredozemsko morje in Gibraltarsko ožino, na zahodu na Atlantski ocean, na vzhodu na Alžirijo, s katero ima zaprto mejo, ter na jugozahodu na Zahodno Saharo, katere del sta okupirala skupaj z Mavretanijo, tako da ima prek zasedbenega območja dostop tudi do Mavretanije.Če pri klicu metode readFile izpustimo drugi parameter, kjer nastavimo nabor znakov, bo metoda vrnila binarne podatke, saj kot rezultat namesto niza, dobimo objekt Buffer (vsebuje števila, ki predstavljajo bajte v datoteki). V našem primeru izpišemo prvi bajt (glej izpis v konzoli), kjer dobimo število \(77\), kar je ASCII koda za črko M (prva črka v datoteki datoteka.txt).
datotecni_sistem_binarno.js
const fs = require("fs");
fs.readFile("data/datoteka.txt", (napaka, medpomnilnik) => {
if (napaka) {
throw napaka;
}
console.log(
"Datoteka vsebuje " +
medpomnilnik.length +
" bajtov. " +
"Prvi bajt je " +
medpomnilnik[0] +
"."
);
});Konzola
Podobno obstaja tudi metoda writeFile za pisanje v datoteko, kot to prikazuje naslednji primer.
datotecni_sistem_pisanje.js
const fs = require("fs");
fs.writeFile(
"data/grafiti.txt",
"Spletno programiranje in action.",
(napaka) => {
if (napaka) {
console.log("Napaka pri pisanju v datoteko: " + napaka);
} else {
console.log("Pisanje v datoteko je uspešno končano.");
}
}
);Konzola
Modul fs vsebuje še številne druge koristne funkcije: readdir vrne datoteke v mapi, stat vrne podatke o datoteki, rename preimenuje datoteko, unlink odstrani datoteko itd. Celotno dokumentacijo z opisom vseh funkcij, ki so na voljo, lahko najdete na uradni strani Node.js.
Številne funkcije v modulu fs so na voljo v sinhroni in asinhroni različici. Kot primer je v nadaljevanju predstavljeno sinhrono branje datoteke.
datotecni_sistem_besedilo_sinhrono.js
Konzola
$ node datotecni_sistem_besedilo_sinhrono.js
Maroko je obmorska država v severozahodni Afriki. Na severu meji na Sredozemsko morje in Gibraltarsko ožino, na zahodu na Atlantski ocean, na vzhodu na Alžirijo, s katero ima zaprto mejo, ter na jugozahodu na Zahodno Saharo, katere del sta okupirala skupaj z Mavretanijo, tako da ima prek zasedbenega območja dostop tudi do Mavretanije.Uporaba sinhronih funkcij je sicer res bolj enostavna in se izkaže kot koristna v manjših skriptah, kjer hitrejše izvajanje asinhronega V/I ni tako pomembno. Vendar se je potrebno zavedati, da se pri sinhronih operacijah izvajanje programa popolnoma ustavi. Če mora program odgovarjati na uporabniške zahteve preko omrežja, potem bo sinhron V/I močno poslabšal uporabniško izkušnjo.
5.1.7 Modul http
Še en osrednji modul se imenuje http in vsebuje funkcionalnosti za zagon HTTP strežnika in posredovanje HTTP zahtev. V nadaljevanju je prikazan primer enostavnega HTTP strežnika, ki ob prejetju zahteve zahtevani URL naslov izpiše na spletni strani. Funkcijo, ki jo podamo kot parameter metodi createServer, se kliče ob vsakem poskusu povezave odjemalca na spletni strežnik. Parametra zahteva in odgovor sta objekta, ki vsebujeta vhodne (npr. lastnost url, ki pove, katero spletno stran zahtevamo) in izhodne podatke zahteve. Za posredovanje odgovora pokličemo metode na objektu odgovor. Najprej določimo status odgovora na \(200\) in nato določimo še vsebino odgovora. S klicem metode end zaključimo odgovor in ga posredujemo odjemalcu. V našem primeru smo kot parameter metode listen določili še vrata \(8080\), na katerih naj strežnik posluša in sporočilo (2. parameter kot anonimna funkcija), ki se izpiše, ko je strežnik uspešno pognan.
Program, kot je naš testni, se ne bo sam ustavil, saj čaka na uporabniške zahteve, zato ga lahko ustavimo s kombinacijo tipk CTRL + C.
http_streznik.js
const http = require("http");
http
.createServer((zahteva, odgovor) => {
odgovor.writeHead(200, { "Content-Type": "text/html" });
zahteva.on("data", (delcek) => {
odgovor.write(delcek.toString().toUpperCase());
});
zahteva.on("end", () => {
odgovor.end();
});
})
.listen(process.env.port || 8080, function () {
console.log(`Strežnik posluša na ${process.env.port || 8080}.`);
});Konzola
Slika 5.2: Delovanje testnega HTTP strežnika
Če želimo modul http uporabiti v vlogi spletnega odjemalca, lahko to dosežemo s klicem funkcije request, kot to prikazuje naslednji primer. Ko zahtevamo začetno stran te skripte na spletnem naslovu http://teaching.lavbic.net/SP, nam strežnik odgovori s statusom \(301\), kar pomeni, da je stran premaknjena na drugo lokacijo (na strežniku je, zaradi večje varnosti, aktivna stalna preusmeritev iz http v https zahtevo). Če bi želeli izvesti https zahtevo, lahko za to uporabimo modul https, ki je prav tako na voljo v Node.js.
http_odjemalec.js
const http = require("http");
let zahteva = http.request(
{
hostname: "teaching.lavbic.net",
path: "/SP",
method: "GET",
headers: { Accept: "text/html" },
},
(odgovor) => {
console.log(`Strežnik je odgovoril s statusom ${odgovor.statusCode}.`);
}
);
zahteva.end();Konzola
5.1.8 Tok podatkov
V prejšnjem poglavju smo v kontekstu HTTP spoznali dva primera tokov podatkov, kamor lahko pišemo - objekt odgovora, kjer piše strežnik in objekt zahteve spletnemu odjemalcu, ki je odgovor klica funkcije http.request.
Koncept toka podatkov, kamor lahko pišemo, je zelo razširjen koncept pri Node.js vmesnikih. Vsi takšni toki podatkov imajo metodo write, ki sprejme niz ali objekt Buffer. S pomočjo metode end tok podatkov zapremo, če pa jo pokličemo s parametrom, potem izpišemo še vrednost tega parametra pred dejanskim zaprtjem toka podatkov.
Spoznali smo tudi tokove podatkov, ki jih beremo - zahteva pri spletnem strežniku in odgovor pri spletnem odjemalcu. Objekti, ki v Node.js prožijo dogodke imajo metodo on, ki je podobna metodi addEventListener v kontekstu spletnega brskalnika. Podamo ime dogodka in funkcije, ki se pokliče, ko se zgodi dogodek. Tokovi dogodkov, ki jih lahko beremo imajo opredeljena dogodka data (proži se vedno, ko pridejo podatki) in end (proži se ob koncu toka podatkov).
V naslednjem primeru je prikazano delovanje spletnega strežnika, ki prebere telo zahteve in jih v obliki toka podatkov posreduje nazaj odjemalcu tako, da vsebino zahteve pretvori v velike črke. Delček toka podatkov je v binarni obliki, zato ga pretvorimo v niz.
http_streznik_tok.js
const http = require("http");
http
.createServer((zahteva, odgovor) => {
odgovor.writeHead(200, { "Content-Type": "text/html" });
zahteva.on("data", (delcek) =>
odgovor.write(delcek.toString().toUpperCase())
);
zahteva.on("end", () => odgovor.end());
})
.listen(process.env.port || 8080, () =>
console.log(`Strežnik posluša na ${process.env.port || 8080}.`)
);V naslednjem primeru je prikazan spletni odjemalec, ki dostopa do našega spletnega strežnika. Odjemalec strežniku posreduje zahtevo in ko dobi odgovor od strežnika, ga izpiše na standardni izhod. Pri izpisu odgovora uporabimo process.stdout in ne console.log, saj console.log po vsakem izpisu doda prehod v novo vrstic, kar v našem primeru ni primerno.
http_odjemalec_tok.js
const http = require("http");
const zahteva = http.request(
{
hostname: "localhost",
port: process.env.PORT || 8080,
method: "POST",
},
(odgovor) =>
odgovor.on("data", (delcek) => process.stdout.write(delcek.toString()))
);
zahteva.end("Še malo in bomo končali.\n");Konzola strežnika
Konzola odjemalca
5.1.9 Enostavni datotečni strežnik
Z novo pridobljenim znanjem pogosto uporabljanih Node.js modulov http in fs bomo implementirali enostavni datotečni strežnik, kjer bomo kombinirali spletni strežnik z oddaljenim dostopom do datotečnega sistema. Takšen strežnik je koristen za številne scenarije, omogoča nam npr. shranjevanje podatkov oz. deljenje vsebine različnim uporabnikom.
Ko dostopamo do HTTP virov, uporabljamo HTTP metode GET (branje), PUT (pisanje) in DELETE (brisanje). Ker ne želimo deliti celotne vsebine našega trdega diska, bomo omogočili dostop zgolj do mape, v kateri bomo pognali strežnik.
Najprej uvozimo potrebne module, pripravimo objekt dovoljenih metod in pripravimo ter poženemo strežnik. Če uporabnik zahteva metodo, ki jo naš strežnik ne podpira, vrnemo status \(405\), ki vsebinsko predstavlja ravno ta scenarij.
Funkcija odgovori se v večini primerov posreduje kot povratni klic in se izvede ob zaključku zahteve. Če je vrednost, ki jo posredujemo kot vsebina, bralni tok podatkov, ima na voljo metodo pipe, s pomočjo katere bralni tok podatkov posredujemo naprej v tok podatkov, v katerega lahko pišemo.
Za pridobitev poti do datoteke iz URL zahteve je opredeljena funkcija urlVPot, ki uporablja sistemski modul url in poskrbi za pretvorbo. Na koncu ga pretvorimo v varno obliko z metodo decodeURIComponent, dodamo piko ., kar pomeni, da dostopamo do trenutne mape in celotno pot vrnemo ob klicu funkcije.
Najprej bomo opredelili metodo GET, ki nam vrača seznam datotek, ko dostopamo do mape in vsebino datoteke, ko dostopamo do datoteke. Pri vračanju vsebine datoteke moramo poskrbeti tudi za tip datoteke, kjer lahko uporabimo modul mime in ga namestimo z ukazom, prikazanim v nadaljevanju.
Konzola strežnika
Ko zahtevana datoteka ne obstaja, vrnemo status zahteve na \(404\), kar lahko ugotovimo s klicem funkcije fs.stat. Ker lahko omenjena operacija traja dlje časa, saj dostopa do trdega diska, jo izvedemo na asinhroni način in odgovor vrnemo v povratnem klicu. Če datoteka ne obstaja, dobimo kodo napake ENOENT. V primeru, da pride do napake na strežniku, ki je ne pričakujemo, vrnemo status zahteve \(500\). Če vračamo seznam datotek v mapi, jih pridobimo s klicem funkcije fs.readdir.
Brisanje vsebine z metodo DELETE je nekoliko enostavneje. Mogoče je nenavadno, kaj se zgodi, ko hočemo brisati datoteko, ki ne obstaja, kar je implementirano v metodi odgovoriNapakoAliNic. V tem primeru namreč vrnemo status \(204\) in ne napake, saj HTTP standard zahteva, da so zahteve idempotentne, kar pomeni, da večkratno izvajanje iste akcije vrača enak odgovor.
Pri dodajanju datoteke z metodo PUT nam ni potrebno preverjati, če datoteka obstaja. Če obstaja, jo bomo namreč preprosto prepisali.
datotecni_streznik.js
const http = require("http"),
fs = require("fs");
let metode = Object.create(null);
http
.createServer((zahteva, odgovor) => {
function odgovori(kodaStatus, vsebina, tip) {
if (!tip) tip = "text/plain";
odgovor.writeHead(kodaStatus, { "Content-Type": tip });
if (vsebina?.pipe) vsebina.pipe(odgovor);
else odgovor.end(vsebina);
}
if (zahteva.method in metode)
metode[zahteva.method](urlVPot(zahteva.url), odgovori, zahteva);
else odgovori(405, "Metoda " + zahteva.method + " ni dovoljena.");
})
.listen(process.env.PORT || 8080, () => {
console.log(`Strežnik posluša na ${process.env.PORT || 8080}.`);
});
function urlVPot(url) {
let pot = require("url").parse(url).pathname;
return "." + decodeURIComponent(pot);
}
metode.GET = (pot, odgovori) => {
fs.stat(pot, (napaka, podatki) => {
if (napaka && napaka.code == "ENOENT")
odgovori(404, "Datoteke ni mogoče najti");
else if (napaka) odgovori(500, napaka.toString());
else if (podatki.isDirectory())
fs.readdir(pot, (napaka, datoteke) => {
if (napaka) odgovori(500, napaka.toString());
else odgovori(200, datoteke.join("\n"));
});
else odgovori(200, fs.createReadStream(pot), require("mime").getType(pot));
});
};
metode.DELETE = (pot, odgovori) => {
fs.stat(pot, function (napaka, podatki) {
if (napaka && napaka.code == "ENOENT") odgovori(204);
else if (napaka) odgovori(500, napaka.toString());
else if (podatki.isDirectory())
fs.rmdir(pot, odgovoriNapakoAliNic(odgovori));
else fs.unlink(pot, odgovoriNapakoAliNic(odgovori));
});
};
function odgovoriNapakoAliNic(odgovori) {
return function (napaka) {
if (napaka) odgovori(500, napaka.toString());
else odgovori(204);
};
}
metode.PUT = (pot, odgovori, zahteva) => {
let odhodniTokPodatkov = fs.createWriteStream(pot);
odhodniTokPodatkov.on("error", function (napaka) {
odgovori(500, napaka.toString());
});
odhodniTokPodatkov.on("finish", function () {
odgovori(204);
});
zahteva.pipe(odhodniTokPodatkov);
};Če želimo strežnik stestirati, ga preprosto poženemo, kot je prikazano spodaj.
Konzola strežnika
Za testiranje lahko uporabimo kar orodje curl, ki je na voljo na vseh Unix sistemih in se lahko uporabi za posredovanje HTTP zahtev. S serijo spodnjih ukazov lahko na hitro preverimo delovanje našega strežnika. Opazimo lahko, da s pomočjo parametra -X opredelimo metodo, medtem ko se parameter -d uporablja za vključitev vsebine zahteve.
Konzola odjemalca
$ curl http://localhost:8080/test.txt
Datoteke ni mogoče najti
$ curl -X PUT -d Yellow http://localhost:8080/test.txt
$ curl http://localhost:8080/test.txt
Yellow
$ curl -X DELETE http://localhost:8080/test.txt
$ curl http://localhost:8080/test.txt
Datoteke ni mogoče najtiOpazimo lahko, da prva zahteva ni uspešna, saj omenjena datoteka še ne obstaja. S PUT zahtevo dodamo datoteko, zato je naslednja zahteva uspešna. Po brisanju datoteke z DELETE zahtevo je zadnja zahteva zopet neuspešna, saj smo datoteko izbrisali.
5.1.10 Obvladovanje napak
Pri našem enostavnem datotečnem strežniku iz prejšnjega poglavja na več mestih eksplicitno preverjamo izjeme, za katere ne vemo kako jih obravnavati. Ker izjeme niso samodejno posredovane povratnim klicem, ampak kot parametri, jih moramo neposredno obravnavati vsakokrat. Če pri našem programu pride do napake, se bo ta posredovala do zadnjega klica (t.j. zagon strežnika), kar bo pomenilo izhod iz programa. Naš strežnik se zato ustavi, ko pride do napake v kodi sistema, v nasprotju z asinhronimi problemi, ki se posredujejo kot argumenti povratnemu klicu. Če bi želeli obvladovati vse izjeme, bi morali dodati try/catch kodo vsakemu povratnemu klicu. To zagotovo ni primeren pristop, zato bomo uporabili Promises, ki smo jih že predstavili v kontekstu spletnega brskalnika v poglavju 4.17.5.
Konzola
$ npm install promise
+ promise@8.1.0
added 2 packages from 1 contributor and audited 4 packages in 0.786sLe nekatere Node.js knjižnice imajo integriran pristop s Promises, vendar jih je po drugi strani zelo enostavno vključiti. V modulu promise imamo na voljo funkcijo denodeify, ki obstoječo asinhrono funkcijo pretvori v funkcijo, ki vrača odgovor v obliki Promises, kot je prikazano v naslednjem primeru.
uporaba_promises.js
const promise = require("promise");
const fs = require("fs");
let readFile = promise.denodeify(fs.readFile);
readFile("data/datoteka.txt", "utf8").then(
(vsebina) => console.log(`Datoteka vsebuje: ${vsebina}`),
(napaka) => console.log(`Napaka pri branju datoteke: ${napaka}`)
);Konzola
$ node uporaba_promises.js
Datoteka vsebuje: Maroko je obmorska država v severozahodni Afriki. Na severu meji na Sredozemsko morje in Gibraltarsko ožino, na zahodu na Atlantski ocean, na vzhodu na Alžirijo, s katero ima zaprto mejo, ter na jugozahodu na Zahodno Saharo, katere del sta okupirala skupaj z Mavretanijo, tako da ima prek zasedbenega območja dostop tudi do Mavretanije.5.2 Vzpostavitev in nastavitev projekta
V tem poglavju bomo začeli z gradnjo demo aplikacije, katere zasnovo smo že predhodno predstavili v okviru poglavja 1.6.2. Aplikacija prikazuje bližnje zanimive lokacije, ki so za potencialnega turista zanimive za ogled. Poleg same lokacije so na voljo tudi dodatne informacije, katere aktivnosti so na voljo na lokaciji, odpiralni čas, ocena in podrobna lokacija na zemljevidu. Uporabniki aplikacije bodo lahko posamezno lokacijo ocenili in podali komentar.
Pri uporabi MEAN sklada tehnologij Express in Node predstavljata ogrodje za razvoj spletnih aplikacij. Pri razvoju spletne aplikacije sta najbolj pomembni komponenti, zato bomo začeli z njima.
Slika 5.3: Kreiranje projekta in opredelitev Express aplikacije ter začetek gradnje osrednje aplikacije
Slika 5.3 prikazuje naša prva koraka pri razvoju:
- kreiranje projekta in opredelitev Express aplikacije, kjer bo izvorna koda strežniške aplikacije brez podatkovne baze in
- začetek gradnje osrednje Express aplikacije.
5.2.1 Pregled uporabljenih orodij Express, Node in npm
5.2.1.1 Opredelitev paketov v package.json
V vsaki Node.js aplikaciji bi morala v korenski mapi projekta obstajati datoteka package.json, ki vsebuje različne metapodatke o projektu, vključno s paketi oz. moduli, ki jih potrebuje za svoje delovanje. V nadaljevanju je prikazan primer package.json datoteke. Na začetku so opredeljeni različni metapodatki aplikacije. Nato pa je v nadaljevanju seznam vseh odvisnih paketov, ki jih aplikacija potrebuje pri delovanju.
package.json
{
"name": "demo",
"version": "0.1.0",
"description": "Demo application for Web Development course",
"main": "server.js",
"scripts": {
"start": "node server.js"
},
"author": "Dejan Lavbič <dejan@lavbic.net>",
"license": "ISC",
"private": true,
"dependencies": {
"express": "~4.18.2",
"hbs": "~4.2.0"
}
}Poleg imen posameznih odvisnosti je navedena tudi številka verzije paketa, ki ga aplikacija potrebuje. Opazimo lahko, da imajo vse vrednosti predpono ~. Če si npr. podrobneje ogledamo opredelitev odvisnosti paketa "express": "~4.18.2", je verzija navedena na treh ravneh:
- glavna verzija (
4), - manjša verzija (
18), - verzija popravka (
2).
Če pred vrednostjo verzije vključimo predpono ~, to pomeni, da naj naša aplikacija uporabi zadnjo verzijo popravka, ki je na voljo. Pred vrednostjo verzije lahko vključimo tudi predpono ^, kar pomeni, da naj naša aplikacija uporabi zadnje manjše verzije in verzije popravka. Če želimo vedno uporabiti zadnjo verzijo, uporabimo *.
Pri opredelitvi odvisnosti od verzij lahko uporabite še mnogo drugih načinov, ki so opisani v dokumentaciji.
5.2.1.2 Nameščanje odvisnosti z npm
Vsaka Node.js aplikacija ima lahko odvisnosti opredeljene v datoteki package.json. Nameščanje je zelo preprosto, saj zgolj poženemo naslednji ukaz:
S tem ukazom namestimo vse odvisnosti, ki so navedene v datoteki package.json. Če knjižnica že obstaja, npm preveri, ali je nameščena zadnja verzija popravka (če uporabljamo predpono ~). Ko poženemo omenjeni ukaz, se vse odvisnosti iz spleta povlečejo v podmapo node_modules na lokalnem računalniku, kot to prikazuje slika 5.4.
Slika 5.4: npm moduli so opredeljeni v datoteki package.json in so nameščeni v podmapo node_modules, znotraj aplikacije, z zagonom ukaza npm install iz ukazne vrstice
Če želimo namestiti nov paket v okviru obstoječega programa in njegovo odvisnost vključiti v package.json datoteko, lahko preprosto izvedemo naslednji ukaz:
Opomba: Zastavico
--savelahko izpustite, sajnpmprivzeto vse nameščene knjižnice shrani kot odvisnosti vpackage.json. Lahko pa uporabite zastavico--save-dev, ki knjižnice shrani zgolj med razvojne odvisnosti.
5.2.2 Kreiranje projekta Express
Za začetek razvoja aplikacije s skladom tehnologij MEAN bomo potrebovali naslednja ključna orodja:
5.2.2.1 Namestitev okolja
Če ste si namestili razvojno okolje Node.js, potem je podpora za npm že na voljo. Z naslednjimi ukazi lahko preverite, ali so omenjena okolja nameščena in delujejo (verzije posameznih orodij so lahko enake ali novejše):
Pri razvoju vedno uporabljajte sodo številko zadnje LTS121 verzije (npr.
16.x.x) okolja Node.js. Za lažje obvladovanje verzij lahko uporabite orodje Node Version Managernvm, kjer preprosto z ukazomnvm install --lts --latest-npmnamestite zadnjo LTS verzijo okolja Node.js in zadnjo verzijo orodjanpm.
5.2.2.2 Kreiranje mape projekta in konfiguracija Express namestitve
Najprej kreiramo mapo Demo, kjer se bo nahajal naš projekt.
5.2.2.2.1 Predloge Handlebars
Pri našem projektu bomo za generiranje spletnih strani na strežniku uporabljali predloge Handlebars. Gre za regularne HTML dokumente z vgrajenimi Handlebars {{...}} izrazi, ki se ob zahtevi odjemalca najprej obdelajo na strežniku in odjemalcu vrnejo HTML stran.
Handlebars izraz se začne z znakoma {{, potem sledi vsebina in se zaključi z znakoma }}. Primer Handlebars predloge je prikazan v nadaljevanju, kjer predloga pričakuje spremenljivki vzdevek in avatar.
Če sta vrednosti spremenljivk nastavljeni na avatar = '<img src="https://www.krneki.org/slika.png">' in vzdevek = 'Janez', se predloga prevede v naslednji HTML dokument.
Na tem mestu velja omeniti razliko med izrazom z dvojnima zavitima oklepajema {{...}}, ki se izvede in trojnimi zavitimi oklepaji {{{...}}}, ki neposredno vključi HTML kodo. Podrobnosti in uporabo dodatnih funkcij si bomo ogledali v nadaljevanju, neposredno na primerih.
5.2.2.3 Kreiranje Express projekta in testiranje delovanja
Najprej bomo pripravil opis projekta in odvisnosti od knjižnic express in hbs, ki jih bomo potrebovali pri razvoju, v datoteki package.json.
package.json
{
"name": "demo",
"description": "Demo application for Web Development course",
"author": "Dejan Lavbič <dejan@lavbic.net>",
"version": "0.1.0",
"private": true,
"scripts": {
"start": "node server.js"
},
"dependencies": {
"express": "~4.18.2",
"hbs": "~4.2.0"
}
}Nato knjižnice namestimo z naslednjim ukazom:
$ npm install
added 66 packages, and audited 67 packages in 4s
8 packages are looking for funding
run `npm fund` for details
found 0 vulnerabilitiesRezultat omenjene akcije je nova datoteka package-lock.json, kjer je shranjeno celotno drevo odvisnosti knjižnic (tudi posredno odvisne knjižnice od prvotno zahtevanih) in mapa node_modules, kjer so nameščene vse zahtevane knjižnice in njihove odvisnosti.
V datoteki package.json smo opredelili tudi skripto start, ki ob zagonu npm start izvede ukaz node server.js in posledično požene naš strežnik. Strežnika še nismo opredelili, zato v nadaljevanju pripravimo vsebino datoteke server.js.
server.js
const express = require("express");
/**
* Create server
*/
const port = process.env.PORT || 3000;
const app = express();
/**
* Default response
*/
app.get("/", (req, res) => {
res.send("<h1>Welcome to Web Development!</h1>");
});
/**
* Start server
*/
app.listen(port, () => {
console.log(`Demo app listening on port ${port}!`);
});Začetna strežniška aplikacija je zelo preprosta, zgolj z opredeljeno odvisnostjo od knjižnice express, opredelitev privzetih vrat, privzeto besedilo odziva ter zagon strežnika.
Strežnik nato poženemo z ukazom
Ko zahtevamo spletno stran na naslovu http://localhost:3000, se nam prikaže vnaprej opredeljen odziv Welcome to Web Development!.
Opomba: Express aplikacija je na voljo na vratih
3000, razen če nastavimo drugačno vrednost v okoljsko spremenljivkoPORT.
5.2.2.4 Ponovni zagon aplikacije
Node.js aplikacija se pred zagonom prevede, zato ob spremembi aplikacijske kode med samim izvajanjem aplikacije spremembe ne bodo vidne, dokler Node procesa ne ustavimo in ponovno poženemo. Zavedati se moramo, da to velja zgolj za aplikacijsko kodo, vse ostale spremembe na statičnih datotekah, kot so predloge Handlebars, CSS datoteke in JavaScript na strani odjemalca, bodo vidne takoj.
Trenutni Node.js proces lahko ustavimo s kombinacijo tipk CTRL + C in ga nato z ukazom npm start ponovno poženemo. To sicer ni zelo zapleten postopek, vendar pri razvoju spletne aplikacije in ob nenehnih spremembah postane ta postopek moteč in odvečen. Zato bomo v našem primeru uporabili boljši pristop, ki je opisan v nadaljevanju.
5.2.2.4.1 Samodejni ponovni zagon aplikacije z nodemon
Uporabili bomo Node aplikacijo nodemon, ki predstavlja preprosto ovojnico okrog obstoječe Node aplikacije ter nenehno spremlja spremembe izvorne kode aplikacije.
nodemon namestimo z ukazom
Prepričamo se, da aplikacija ni pognana v nobenem drugem procesu, ter da se nahajamo v mapi Demo in poženemo ukaz
$ nodemon
[nodemon] 3.0.1
[nodemon] to restart at any time, enter `rs`
[nodemon] watching path(s): *.*
[nodemon] watching extensions: js,mjs,json
[nodemon] starting `node server.js`
Demo app listening on port 3000!Namig:
nodemonse uporablja za poenostavitev razvojnega procesa v razvojnem okolju, medtem ko uporaba v produkcijskem okolju ni priporočljiva.
Pri zagonu aplikacije morate poskrbeti, da so zahtevana vrata (v našem primeru
3000) na voljo in da tam ne posluša kakšen drug program ali že obstoječi primerek vašega programa.Pri tem si lahko pomagate tudi s programom
npx kill-port 3000, ki takšen program ustavi.
5.2.2.5 Dodajanje podpore za JSHint
Za sprotno statično preverjanje izvorne kode, ki nas bo opozarjala na napake, bomo uporabili orodje JSHint, ki ga nastavimo preprosto z vključitvijo datoteke .jshintrc v repozitorij, medtem ko mora razvojno orodje podpirati funkcionalnost JSHint.
.jshintrc
{
"strict": false,
"node": true,
"browser": true,
"esversion": 11,
"multistr": true,
"sub": true,
"globals": {
"module": false,
"$": false,
"formatPercentage": false,
"formatFlag": false,
"formatDateOnly": false,
"formatCurrency": false,
"formatNull": false,
"moment": false,
"indexArray": false,
"getData": false,
"round": false
}
}5.2.2.6 Priporočljivi vtičniki razvojnegao kolja Visual Studio Code
Če boste razvijali v razvojnem okolju Visual Studio Code, potem se priporoča uporaba naslednjih vtičnikov:
- Prettier - Code formatter pomaga pri doslednem uveljavljanju izbranega sloga programiranja, ki izvorno kodo preoblikuje tako, da upošteva pravila (npr. največja dolžina vrstice, deljenje po vrsticah, uporaba presledkov oz. tabulatorjev ipd.),
- SonarLint pomaga pri odkrivanje napake med razvojem in opozarja na napake, ki lahko vodijo do hroščev, ranljivosti in slabe kode,
- GitHub Copilot za pridobivanje predlogov pri programiranju v realnem času, neposredno v razvojno okolje (potrebna je prijava na storitev GitHub Copilot).
5.3 Gradnja aplikacije z MVC
MVC (Model-View-Controller) arhitektura predstavlja delitev programa na podatke (model), pogled (view) in aplikacijsko logiko (controller). Takšna delitev poskuša odstraniti neposredne povezave med komponentami, kar lahko posledično olajša vzdrževanje in ponovno uporabnost izvorne kode. Takšna delitev tudi zelo ustreza našemu pristopu hitrega prototipiranja, saj se lahko naenkrat osredotočamo na posamezen vidik programa.
5.3.1 Kratek pregled MVC arhitekture
Večina spletnih aplikacij, ki jih razvijamo, je načrtovanih tako, da na podlagi prejete zahteve izvedejo določeno procesiranje na strežniku in nato vrnejo odgovor. Če pogledamo poenostavljeno, je takšna zanka v MVC arhitekturi sestavljena iz naslednjih korakov (glej sliko 5.5):
- prihod zahteve v aplikacijo,
- zahteva je usmerjena na krmilnik,
- krmilnik lahko posreduje zahtevo v model,
- model odgovori krmilniku,
- krmilnik posreduje odgovor pogledu in
- pogled posreduje odgovor uporabniku.
Slika 5.5: Procesni tok zahteve in odgovora pri osnovni MVC arhitekturi
5.3.2 Kako Express obdela zahteve?
Klasična začetna spletna stran, ki jo postreže Express strežnik, je ponavadi zelo enostavna. Vsebuje nekaj HTML kode, ki se zgenerira na strani strežnika, dodana je lahko tudi CSS datoteka, kjer je opredeljen uporabljen slog.
Vse zahteve na Express strežniku se najprej obdelajo na opredeljenih vmesnih slojih (npr. obdelava zahteve z vidika piškotov). Poleg ostalih vmesnih slojev je eden izmed njih tudi strežba statičnih datotek. Ko vmesni sloj identificira takšno datoteko, jo Express vrne na asinhron način, da ne blokira delovanja ostale logike spletne aplikacije. Ko Express obdela vse vmesne sloje, se začne v usmerjevalniku iskanje ujemanja zahteve. Na sliki 5.6 so prikazane ključne interakcije in procesi, ki se izvedejo pri odgovoru ogrodja Express na zahtevo po začetni spletni strani.
Slika 5.6: Ključne interakcije in procesi, ki se izvedejo pri odgovoru ogrodja Express na zahtevo po začetni spletni strani. HTML spletno stran sprocesira Node, ki prevede podatke in predlogo pogleda, CSS datoteka pa je asinhrono vrnjena iz mape “statične” vsebine
5.3.3 Gradnja statične aplikacije z Node in Express
Naš cilj je sedaj implementacija statične spletne strani, ki navidezno deluje in kjer lahko uporabnik klika na povezave. Kar se tiče arhitekture, se bomo ukvarjali z aplikacijo, ki je prikazana na sliki 5.7.
Slika 5.7: Uporaba Express in Node pri gradnji statične spletne strani za testiranje pogledov
5.3.4 Prilagoditev ogrodja Express za MVC
Na strani strežnika bomo uporabili pristop MVC, kjer bodo funkcionalnosti opredeljene v novi mapi hbs s posameznimi komponentami, organiziranimi v podmapah controllers, routes in views, kot je prikazano spodaj.
-
hbs -
controllers -
index.js
-
-
routes -
hbs.js
-
-
views -
index.hbs
-
-
- …
- server.js
Najprej bomo opredelili pogled v index.hbs, ki ima pričakuje vhodni parameter title te ga zgolj prikaže kot naslov in temu doda enostavno besedilo.
hbs/views/index.hbs
Strežnik v datoteki server.js je treba nastaviti za delo s pogledi, in sicer opredelimo mapo hbs/views, kjer se pogledi nahajajo ter registriramo izvajalno okolje hbs, ki skrbi za generiranje pogledov na strani strežnika.
Usmeranje uporabniških zahtev je trenutno zelo poenostavljeno, saj vedno vrnemo isti odgovor, tj. Welcome to Web Development!. To funkcionalnost preslikovalne tabele bomo predali naprej usmerjevalniku, ki bo implementiran v hbs/routes/hbs.js.
server.js
const express = require("express");
const path = require("path");
const hbsRouter = require("./hbs/routes/hbs");
/**
* Create server
*/
const port = process.env.PORT || 3000;
const app = express();
/**
* View engine (HBS) setup
*/
app.set("views", path.join(__dirname, "hbs", "views"));
app.set("view engine", "hbs");
/**
* HBS routing
*/
app.use("/", hbsRouter);
/**
* Default response
*/
app.get("/", (req, res) => {
res.send("<h1>Welcome to Web Development!</h1>");
});
/**
* Start server
*/
app.listen(port, () => {
console.log(`Demo app listening on port ${port}!`);
});Pomembno je, da v usmerjevalniku ne vključujemo dejanske logike, ki se izvede ob obdelavi zahteve, ampak je na tem mestu opredeljena zgolj preslikovalna tabela med zahtevo uporabnika in metodo krmilnika, ki naj zahtevo obdela. Usmerjevalnik je tako nov modul, opredeljen v datoteki hbs.js, ki vrača nastavitve v objektu router.
hbs/routes/hbs.js
const express = require("express");
const router = express.Router();
const ctrlMain = require("../controllers/main");
router.get("/", ctrlMain.main);
module.exports = router;Sedaj manjka še krmilnik main.js, ki dejansko obdela uporabniško zahtevo, kar v našem primeru pomeni klic pogleda index.hbs s parametrom title nastavljenim na vrednost Welcome to Web Programming!.
hbs/controllers/main.js
const main = (req, res) => {
res.render("index", { title: "Welcome to Web Development!" });
};
module.exports = {
main,
};Do tega trenutka smo vzpostavili arhitekturo, ki je prikazana na sliki 5.8, kjer v datoteki server.js zahtevamo routes/hbs.js, ki nato zahteva še controllers/main.js.
Slika 5.8: Ločitev logike krmilnika in usmerjevalnika
5.3.5 Dodajanje zunanjih knjižnic za prilagoditev izgleda strani
Naša aplikacija bo uporabljala Bootstrap ogrodje za lažji razvoj prilagodljive spletne aplikacije, dodatno pa jo bomo prilagodili še z uporabo teme.
CSS stile in podporno JavaScript knjižnico ogrodja Bootstrap, ter ikone Font Awesome bomo uporabljali neposredno preko uvoženih datotek v našem projektu, da ne bomo odvisni od zunanjih virov (v okviru predavanja P1.2 smo uporabili gostovanje CDN). Za prilagoditev izgleda bomo uporabili temo Sandstone na strani Bootswatch, ki nadgradi Bootstrap s celovito spremenjenim izgledom. Prav tako pa bomo za naslove dodali tudi pisavo Black Ops One ter ikono spletne strani favicon.ico.
Omenjeno dodatno gradivo bomo hranili v mapi public, zato moramo strežniku sporočiti naj vse datoteke v tej mapi postreže kot statično vsebino.
server.js
const express = require("express");
const path = require("path");
const hbsRouter = require("./hbs/routes/hbs");
/**
* Create server
*/
const port = process.env.PORT || 3000;
const app = express();
/**
* Static pages
*/
app.use(express.static(path.join(__dirname, "public")));
/**
* View engine (HBS) setup
*/
app.set("views", path.join(__dirname, "hbs", "views"));
app.set("view engine", "hbs");
/**
* HBS routing
*/
app.use("/", hbsRouter);
/**
* Start server
*/
app.listen(port, () => {
console.log(`Demo app listening on port ${port}!`);
});Statično gradivo v mapi public nadalje organiziramo v podmape javascript, styles in webfonts, kot je prikazano spodaj.
-
public -
javascript -
bootstrap.bundle.min.js ← podporna JavaScript koda za Bootstrap -
bootstrap.bundle.min.js.map ← kazalci na izvorno kodo za zgornjo datoteko
-
-
styles -
bootstrap.sandstone.min.css ← Sandstone prilagoditev Bootstrap ogrodja -
fa.all.min.css ← stilske predloge za ikone Font Awesome -
style.css ← lastne stilske prilagoditve
-
-
webfonts -
BlackOpsOneLatin.woff2 ← Google Fonts pisava Blacks Ops One (Latin) -
BlackOpsOneLatinExt.woff2 ← Google Fonts pisava Blacks Ops One (Latin Extended) -
fa-regular-400.ttf ← Font Awesome pisava (Regular 400) -
fa-solid-900.woff2 ← Font Awesome pisava (Solid 900) -
fa-brands-400.ttf ← Font Awesome pisava (Brands 400) -
fa-regular-400.woff2 ← Font Awesome pisava (Regular 400) -
fa-v4compatibility.ttf ← Font Awesome pisava (združljivost za nazaj) -
fa-brands-400.woff2 ← Font Awesome pisava (Brands 400) -
fa-solid-900.ttf ← Font Awesome pisava (Solid 900) -
fa-v4compatibility.woff2 ← Font Awesome pisava (združljivost za nazaj)
-
-
favicon.ico ← ikona spletne strani
-
5.3.5.1 Uporaba Bootstrapa v aplikaciji
Ko imamo pripravljene vse datoteke, jih moramo referencirati v projektu, in sicer moramo prilagoditi predloge Handlebars. Pri uporabi predlog Handlebars je pogosto uporabljen pristop, da opredelimo osrednjo postavitveno datoteko z različnimi območji, ki so opredeljena v ločenih datotekah. To se izkaže še posebej koristno pri straneh, kjer se deli strani ponavljajo na več straneh in jih moramo v tem primeru opredeliti zgolj enkrat.
Trenutno imamo opredeljen zgolj en pogled index.hbs, kateremu bomo dodali predlogo layout.hbs, ki bo vseboval povezave na prej omenjene statične datoteke.
hbs/views/layout.hbs
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
<link rel="stylesheet" href="/styles/fa.all.min.css" />
<link rel="stylesheet" href="/styles/bootstrap.sandstone.min.css" />
<link rel="stylesheet" href="/styles/style.css" />
<title>{{title}}</title>
</head>
<body>
<div class="container p-4">
{{{body}}}
</div>
<script src="/javascript/bootstrap.bundle.min.js"></script>
</body>
</html>Vse Handlebars predloge privzeto razširjajo predlogo layout.hbs. V datoteki index.hbs nato zgolj opredelimo sekcijo, ki se bo vključila v predlogo layout.hbs, znotraj katere bomo izpisali vrednost parametra title v naslov in kot del odstavka. Z manjšo spremembo pokažemo, da lahko sedaj v pogledu index.hbs neposredno uporabljamo Bootstrap razrede, ki so opredeljeni v predlogi layout.hbs.
app_server/views/index.hbs
V datoteki index.hbs ni nobenih referenc na HTML gradnike head ali body in tudi nobenih referenc na zunanje CSS sloge. Vse te informacije so namreč zajete v datoteki layout.hbs, ki predstavlja globalno strukturo naše spletne strani. Opazimo lahko zgolj sekcijo {{{body}}}, ki pa je podrobneje definirana v datoteki index.hbs.
Nekoliko prilagodimo tudi obstoječe lastne sloge v datoteki style.css, kjer za naslove vseh velikosti od h1 do h6 uporabimo pisavo Black Ops One.
public/styles/style.css
/* latin-ext */
@font-face {
font-family: 'Black Ops One';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(../webfonts/BlackOpsOneLatinExt.woff2) format('woff2');
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Black Ops One';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(../webfonts/BlackOpsOneLatin.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
h1,
h2,
h3,
h4,
h5,
h6 {
font-family: "Black Ops One", sans-serif;
}Ko preverimo delovanje aplikacije, lahko opazimo, da je sedaj apliciran Bootstrap slog z vključeno Sandstone temo in pisavo Black Ops One, kar prikazuje tudi slika 5.9.
Slika 5.9: Začetna stran testne Express spletne strani z vključenimi prilagoditvami
5.3.6 Opredelitev usmerjevalnikov v Express
Že pri predavanju P1.1 v okviru poglavja 1.6.2.1 smo predstavili osnovne zaslone (glej sliko 5.10) in opisali ključne korake pri razvoju naše spletne aplikacije.
Slika 5.10: Vsebinsko združevanje načrtovanih zaslonov uporabniškega vmesnika.
Predstavitev aplikacije v obliki zaslonov je zelo pomembna, vendar jih moramo sedaj v nadaljevanju povezati z URL spletnimi naslovi, kjer se bodo nahajali, kar je predstavljeno v tabeli 5.1.
| Sklop | Zaslonska maska | URL naslov |
|---|---|---|
| Lokacije | seznam lokacij (to bo naša začetna spletna stran) |
/
|
| Lokacije | podrobnosti lokacije |
/location
|
| Lokacije | obrazec za dodajanje komentarja lokacije |
/location/comment/new
|
| Ostalo | dodatne informacije o aplikaciji |
/about
|
To predstavlja osnovo za naš mehanizem usmerjanja, ki bo implementiran v obliki usmerjevalnikov znotraj Node.js aplikacije. Ko bo uporabnik obiskal spletni naslov /about, se mu bo prikazala zaslonska maska z dodatnimi informacijami o aplikaciji.
Naša aplikacija bo uporabljala 2 sklopa krmilnikov (glej sliko 5.11):
- Lokacije (v povezavi z lokacijo - seznam, podrobnosti in obrazec za dodajanje komentarja) in
- Ostalo (dodatne informacije o aplikaciji).
Slika 5.11: Ločitev logike krmilnika in usmerjevalnika
Zato bomo v usmerjevalniku hbs.js najprej razdelili obstoječi krmilnik ctrlMain v omenjena 2 krmilnika in opredelili preslikovalno tabelo s funkcijami krmilnikom, kot je predvideno v tabeli 5.1.
app_server/routes/hbs.js
const express = require("express");
const router = express.Router();
const ctrlMain = require("../controllers/main");
const ctrlLocations = require("../controllers/locations");
const ctrlOther = require("../controllers/other");
router.get("/", ctrlMain.main);
router.get("/", ctrlLocations.list);
router.get("/location", ctrlLocations.details);
router.get("/location/comment/new", ctrlLocations.addComment);
router.get("/about", ctrlOther.about);
module.exports = router;5.3.7 Izdelava osnovnih krmilnikov
Na začetku bomo pripravili zelo preprosto različico krmilnikov, ki bodo zgolj izpisali nekaj informacij, da bomo lahko preverili delovanje naše aplikacije.
5.3.7.1 Nastavitev krmilnikov
Sedaj moramo pripraviti še datoteki z izvorno kodo krmilnikov locations.js in other.js v mapi hbs/controllers. Za začetek preimenujmo obstoječo datoteko main.js v datoteko other.js, ter jo posodobimo, kot je prikazano spodaj:
app_server/controllers/other.js
const aboutmain = (req, res) => {
res.render("index", { title: "Information about the application!Welcome to Web Programming!" });
};
module.exports = {
aboutmain,
};Nato dodajmo še krmilnik locations.js in pripravimo ustrezne metode, ki smo jih zahtevali v usmerjevalniku hbs.js.
app_server/controllers/locations.js
const list = (req, res) => {
res.render("index", { title: "List of cultural heritages!" });
};
const details = (req, res) => {
res.render("index", { title: "Details of cultural heritage!" });
};
const addComment = (req, res) => {
res.render("index", { title: "Add comment!" });
};
module.exports = {
list,
details,
addComment
};5.3.7.2 Testiranje krmilnikov in usmerjevalnikov
Sedaj lahko preverimo delovanje naše aplikacije na naslednjih spletnih naslovih:
http://localhost:3000/(list of cultural heritage),http://localhost:3000/location(details of cultural heritage),http://localhost:3000/location/comment/new(add comment) inhttp://localhost:3000/about(information about the application).
5.3.8 Dopolnitev pogledov
Pri izgledu vseh strani aplikacije imamo določene skupne zahteve, ki so prikazane na sliki 5.12. Na vrhu strani potrebujemo standardno navigacijsko vrstico, na dnu strani je noga in osrednji del je namenjen razširljivi in izmenljivi osrednji vsebini strani.
Slika 5.12: Osnovna struktura ponovno uporabljive postavitve, ki jo sestavlja standardna navigacijska vrstica in noga z razširljivo in izmenljivo osrednjo vsebino strani
Na obstoječo spletno stran dodajmo možnost navigacije z glavo in nogo spletne strani, kar naj velja kar za vse strani, zato bomo spremenili datoteko layout.hbs.
hbs/views/layout.hbs
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
<link rel="stylesheet" href="/styles/fa.all.min.css" />
<link rel="stylesheet" href="/styles/bootstrap.sandstone.min.css" />
<link rel="stylesheet" href="/styles/style.css" />
<title>{{title}}</title>
</head>
<body>
<div class="container p-4">
<nav class="mb-4"><a
href="/"
class="btn btn-sm btn-outline-dark mt-4"
role="button"
><i class="fa-solid fa-house-chimney pe-1"></i>List of locations</a>
<a
href="/location"
class="btn btn-sm btn-outline-dark mt-4"
role="button"
><i class="fa-solid fa-map-location-dot pe-1"></i>Location details</a>
<a
href="/location/comment/new"
class="btn btn-sm btn-outline-dark mt-4"
role="button"
><i class="fa-solid fa-map-location-dot pe-1"></i>Add comment</a>
<a
href="/about"
class="btn btn-sm btn-outline-dark mt-4"
role="button"
><i class="fa-solid fa-map-location-dot pe-1"></i>About</a></nav>
{{{body}}}
<footer class="mt-4">© Dejan Lavbič</footer>
</div>
<script src="/javascript/bootstrap.bundle.min.js"></script>
</body>
</html>Povzetek vlog krmilnika in pogleda sta podrobneje predstavljeni na sliki 5.13, kjer je razvidno, da se vsebina med krmilnikom in pogledom pošilja preko vrednosti spremenljivk, za katere poskrbi krmilnik, pogled pa vsebino teh spremenljivk vključi pri pripravi končne oblike HTML strani.
Slika 5.13: Vloga krmilnika in pogleda pri MVC načrtovalskem vzorcu
5.4 Strežba aplikacije v različnih okoljih
5.4.1 Strežba aplikacije v okolju Docker
Docker predstavlja množico orodij, ki podpirajo PaaS (Platform as a Service) in omogočajo razvoj, izvajanje in obvladovanje aplikacije brez kompleksnosti vzdrževanja infrastrukture, ki je ponavadi povezana z razvojem in gostovanjem aplikacij.
Z uporabo Docker-ja želimo omogočiti enostavno izgradnjo in zagon razvite aplikacije, kjer vnaprej poskrbimo za vse ustrezne nastavitve in tako s pomočjo virtualizacije omogočimo zagon na poljubnem sistemu z nameščenim Docker-jem.
Namig: Pri nameščanju okolja Docker na operacijskem sistemu Windows poskrbite tudi za WSL2 Linux kernel.
Najprej pripravimo datoteko dockerfile, kjer opredelimo pravila za pripravo Docker slike. V datoteki opredelimo kateri operacijski sistem bomo uporabljali kot osnovo. V našem primeru je to node:16-alpine, ki temelji na osnovni verziji Linux-a, kjer je nameščen Node.js v18. Ob kreiranju Docker slike se najprej požene npm install in na koncu izvede zagon z ukazom npm run start.
dockerfile
FROM node:16-alpine
ARG NODE_ENV=production
ENV NODE_ENV=${NODE_ENV}
# Default application directory for all subsequent commands (COPY, RUN, CMD etc.).
WORKDIR /usr/src/app
# Copy package.json and install dependencies.
# Docker will cache node_modules, if package.json and package-lock.json are not changed.
COPY package.json ./
RUN npm install
# Copy source code of the application
COPY . .
EXPOSE 3000
CMD [ "npm", "run", "start" ]Nato v datoteko .dockerignore dodamo poti, ki jih pri ukazu COPY iz prejšnjega koraka izpustimo (tj. datoteke, ki jih v sliki naše aplikacije ne potrebujemo).
.dockerignore
Na koncu pripravimo še datoteko docker-compose.yml, kjer lažje obvladujemo vse komponente, ki sodelujejo v naši aplikaciji. Zaenkrat je ta postavitev sicer zelo enostavna, saj imamo zgolj Node.js strežnik, kasneje pa bomo dodali še ostale komponente, npr. podatkovno bazo.
docker-compose.yml
version: "3.5"
services:
node-api:
container_name: web-dev-app
ports:
- "3000:3000"
image: web-dev-server
build: ./
networks:
- web-dev-network
networks:
web-dev-network:
name: web-dev-network
driver: bridgeZagon aplikacije izvedemo z ukazom docker-compose up, kjer s parametrom --detach zahtevamo izvajanje v ozadju.
Ob uspešnem zagonu lahko do aplikacije dostopate preko spletne povezave http://localhost:3000.
Če želite Docker vsebnik ustaviti, uporabite naslednji ukaz.
Za ponovni zagon ustavljenega Docker vsebnika je na voljo ukaz start.
Za dostop do dnevniških zapisov Docker vsebnika lahko uporabite ukaz logs, kjer s parametrov --follow zahtevamo posodabljanje v realnem času.
$ docker-compose logs --follow
Attaching to sp-app
sp-app |
sp-app | > demo@0.1.0 start
sp-app | > node server.js
sp-app |
sp-app | Demo app listening on port 3000!Če želite izbrisati Docker vsebnik, omrežje in pripadajočo sliko, lahko uporabite naslednji ukaz.
5.4.2 Strežba aplikacije na Fly
Fly je oblačna platforma za gostovanje celostnih aplikacij in podatkovnih baz. V okviru paketa uporabe Hobby Plan omogoča brezplačno uporabo platorme, in sicer pod naslednjimi pogoji:
- obvezna registracija kreditne kartice, ki se uporabi ob prekoračenju brezplačnih virov,
- brezplačni viri so naslednji:
- do 3x virtualni stroj v skupni rabi z 1 CPU in 256mb RAM-a,
- 3GB skupne trajne hrambe podatkov in
- 160GB odhodnega prenosa podatkov.
5.4.2.1 Fly CLI
Flyctl je pripomoček v ukazni vrstici, ki omogoča delo s platformo Fly.io, od kreiranja računa do nameščanja aplikacij. Deluje na lokalni napravi, kjer so na voljo namestitve za vse priljubljene operacijske sisteme.
Če sistem uporabljate prvič, se je treba naprej registrirati.
$ fly auth signup
Opening https://fly.io/app/auth/cli/...
Waiting for session... Done
successfully logged in as dejan@lavbic.netV nasprotnem primeru, ko račun že imate, se zgolj prijavite.
$ fly auth login
Opening https://fly.io/app/auth/cli/...
Waiting for session... Done
successfully logged in as dejan@lavbic.netSledi namestitev aplikacije, kjer lahko poženemo poljubno aplikacijo, ki je zapakirana v sliko Docker. To pomeni, da lahko našo aplikacijo, ki smo jo v prejšnjem poglavju 5.4.1 že pripravili za okolje Docker, neposredno namestimo na oblačno platformo Fly z naslednjim ukazom.
$ fly launch
Creating app in ./Demo
Scanning source code
Detected a NodeJS app
? Choose an app name (leave blank to generate one): web-dev
automatically selected personal organization: Dejan Lavbič
Some regions require a paid plan (bom, fra, maa).
See https://fly.io/plans to set up a plan.
? Choose a region for deployment: Amsterdam, Netherlands (ams)
App will use 'ams' region as primary
Created app 'web-dev' in organization 'personal'
Admin URL: https://fly.io/apps/web-dev
Hostname: web-dev.fly.dev
Wrote config file fly.toml
Validating ./Demo/fly.toml
Platform: machines
✓ Configuration is valid
$ fly deploy
==> Verifying app config
Validating ./Demo/fly.toml
Platform: machines
✓ Configuration is valid
--> Verified app config
==> Building image
Remote builder fly-builder-lively-snow-2550 ready
==> Building image with Docker
--> docker host: 20.10.12 linux x86_64
[+] Building 11.6s (10/10) FINISHED
=> [1/5] FROM docker.io/library/node:18-alpine@sha256:3482a20c97e401b56ac50ba8920cc7b5b2022bfc6aa7d4e4c231755770cf892f
=> [2/5] WORKDIR /usr/src/app
=> [3/5] COPY package.json ./
=> [4/5] RUN npm install
=> [5/5] COPY . .
--> Building image done
==> Pushing image to fly
The push refers to repository [registry.fly.io/web-dev]
85597e9e02ca: Pushed
3a1776e8c3d1: Pushed
b5bd01a59a6f: Pushed
b4ba75c3c172: Pushed
498c77a984f9: Pushed
69b50a78845f: Pushed
ea2a97a3209c: Pushed
4693057ce236: Pushed
deployment-01H9N2G61TPFGFQBPQDPPEBRQ3: digest: sha256:62b33f83d525140d52a7fb5cb6683e488cbba85b206a0f0f718bc06fe3434271 size: 1993
--> Pushing image done
image: registry.fly.io/web-dev:deployment-01H9N2G61TPFGFQBPQDPPEBRQ3
image size: 193 MB
Watch your deployment at https://fly.io/apps/web-dev/monitoring
Provisioning ips for web-dev
Dedicated ipv6: 2a09:8280:1::37:6cdf
Shared ipv4: 66.241.125.62
Add a dedicated ipv4 with: fly ips allocate-v4
This deployment will:
* create 2 "app" machines
No machines in group app, launching a new machine
Machine 6e8257ec749758 [app] update finished: success
Creating a second machine to increase service availability
Machine 9080543a150528 [app] update finished: success
Finished launching new machines
NOTE: The machines for [app] have services with 'auto_stop_machines = true' that will be stopped when idling
Visit your newly deployed app at https://web-dev.fly.dev/Ob prvi namestitvi aplikacije, sistem Fly od zahteva določene podatke (npr. naziv aplikacije, lokacija gostovanja ipd.), na podlagi katerih se kreira nova datoteka fly.toml. Pri vsaki naslednji namestitvi se postavi naslednja višja verzija od v0 naprej.
fly.toml
app = "web-dev"
primary_region = "ams"
[build]
[http_service]
internal_port = 3000
force_https = true
auto_stop_machines = true
auto_start_machines = true
min_machines_running = 0
processes = ["app"]
Ko je aplikacija nameščena, lahko z naslednjim ukazom preverimo status namestitve.
$ fly status
App
Name = web-dev
Owner = personal
Hostname = web-dev.fly.dev
Image = web-dev:deployment-01H9N2G61TPFGFQBPQDPPEBRQ3
Platform = machines
Machines
PROCESS ID VERSION REGION STATE ROLE CHECKS LAST UPDATED
app 6e8257ec749758 1 ams started 2023-09-06T11:02:57Z
app 9080543a150528 1 ams started 2023-09-06T11:03:09ZČe je bila namestitev uspešna, to potrjuje status running, na voljo pa je tudi spletni naslov na katerem je aplikacija na voljo - v našem primeru je to https://web-dev.fly.dev/
Na voljo pa nam je tudi ukaz, s katerim lahko aplikacijo odpremo.
Dodatno gradivo
|
|
|
|
|
|
|
|
|


