Crearea de aplicații front-end fără server utilizând Google Cloud Platform

Publicat: 2022-03-10
Rezumat rapid ↬ Utilizarea de către dezvoltatori a aplicațiilor fără server pentru a gestiona logica de afaceri a aplicațiilor lor este în creștere, dar cum le permite Google Cloud – un furnizor de servicii major din cloudul public – dezvoltatorilor să gestioneze aplicațiile fără server? În acest articol, veți afla ce sunt aplicațiile fără server, cum sunt utilizate pe Google Cloud și, de asemenea, scenarii în care pot fi utilizate într-o aplicație front-end.

Recent, paradigma de dezvoltare a aplicațiilor a început să se schimbe de la nevoia manuală de implementare, scalare și actualizare a resurselor utilizate într-o aplicație la bazarea pe furnizorii terți de servicii cloud pentru a face cea mai mare parte a gestionării acestor resurse.

În calitate de dezvoltator sau organizație care dorește să construiască o aplicație adaptată pieței în cel mai scurt timp posibil, s-ar putea să vă concentrați principal pe furnizarea de servicii de aplicație de bază pentru utilizatorii dvs., în timp ce petreceți o perioadă mai mică de timp configurării, implementării și testării de stres. aplicatia ta. Dacă acesta este cazul dvs. de utilizare, gestionarea logicii de afaceri a aplicației dvs. într-o manieră fără server ar putea fi cea mai bună opțiune. Dar cum?

Acest articol este benefic pentru inginerii front-end care doresc să construiască anumite funcționalități în aplicația lor sau pentru inginerii back-end care doresc să extragă și să gestioneze o anumită funcționalitate dintr-un serviciu back-end existent folosind o aplicație fără server implementată pe Google Cloud Platform.

Notă : Pentru a beneficia de ceea ce va fi acoperit aici, trebuie să aveți experiență de lucru cu React. Nu este necesară experiența anterioară în aplicații fără server.

Înainte de a începe, să înțelegem ce sunt cu adevărat aplicațiile fără server și cum poate fi utilizată arhitectura fără server atunci când construim o aplicație în contextul unui inginer front-end.

Aplicații fără server

Aplicațiile fără server sunt aplicații împărțite în funcții minuscule reutilizabile, bazate pe evenimente, găzduite și gestionate de furnizori terți de servicii cloud în cloud public în numele autorului aplicației. Acestea sunt declanșate de anumite evenimente și sunt executate la cerere. Deși sufixul „ mai puțin ” atașat cuvântului fără server indică absența unui server, acesta nu este 100% cazul. Aceste aplicații încă rulează pe servere și alte resurse hardware, dar în acest caz, acele resurse nu sunt furnizate de dezvoltator, ci mai degrabă de un furnizor de servicii cloud terț. Deci, acestea sunt fără server pentru autorul aplicației, dar încă rulează pe servere și sunt accesibile prin internetul public.

Un exemplu de caz de utilizare al unei aplicații fără server ar fi trimiterea de e-mailuri către potențiali utilizatori care vă vizitează pagina de destinație și se abonează pentru a primi e-mailuri de lansare a produsului. În această etapă, probabil că nu aveți un serviciu back-end care rulează și nu doriți să sacrificați timpul și resursele necesare pentru a crea, implementa și gestiona unul, totul pentru că trebuie să trimiteți e-mailuri. Aici, puteți scrie un singur fișier care utilizează un client de e-mail și îl puteți implementa oricărui furnizor de cloud care acceptă aplicații fără server și îi puteți permite să gestioneze această aplicație în numele dvs. în timp ce conectați această aplicație fără server la pagina dvs. de destinație.

Deși există o mulțime de motive pentru care ați putea lua în considerare utilizarea aplicațiilor fără server sau Functions As A Service (FAAS), așa cum sunt numite, pentru aplicația dvs. front-end, iată câteva motive foarte notabile pe care ar trebui să le luați în considerare:

  • Scalare automată a aplicației
    Aplicațiile fără server sunt scalate pe orizontală, iar această „ scalare ” este realizată automat de furnizorul de cloud în funcție de cantitatea de invocări, astfel încât dezvoltatorul nu trebuie să adauge sau să elimine manual resurse atunci când aplicația este supusă unei sarcini grele.
  • Eficiența costurilor
    Fiind bazate pe evenimente, aplicațiile fără server rulează numai atunci când este necesar și acest lucru se reflectă asupra taxelor, deoarece acestea sunt facturate în funcție de numărul de timp invocat.
  • Flexibilitate
    Aplicațiile fără server sunt construite pentru a fi foarte reutilizabile și asta înseamnă că nu sunt legate de un singur proiect sau aplicație. O anumită funcționalitate poate fi extrasă într-o aplicație fără server, implementată și utilizată în mai multe proiecte sau aplicații. Aplicațiile fără server pot fi scrise și în limba preferată a autorului aplicației, deși unii furnizori de cloud acceptă doar o cantitate mai mică de limbi.

Mai multe după săritură! Continuați să citiți mai jos ↓

Atunci când utilizează aplicații fără server, fiecare dezvoltator are o gamă largă de furnizori de cloud în cloud public de care să folosească. În contextul acestui articol, ne vom concentra asupra aplicațiilor fără server de pe platforma Google Cloud — cum sunt create, gestionate, implementate și cum se integrează și cu alte produse de pe Google Cloud. Pentru a face acest lucru, vom adăuga noi funcționalități la această aplicație React existentă în timp ce lucrăm prin procesul de:

  • Stocarea și preluarea datelor utilizatorului pe cloud;
  • Crearea și gestionarea joburilor cron pe Google Cloud;
  • Implementarea funcțiilor cloud în Google Cloud.

Notă : Aplicațiile fără server nu sunt legate doar de React, atâta timp cât cadrul sau biblioteca dumneavoastră preferată poate face o solicitare HTTP , aceasta poate folosi o aplicație fără server.

Funcții Google Cloud

Google Cloud le permite dezvoltatorilor să creeze aplicații fără server utilizând funcțiile Cloud și să le ruleze utilizând cadrul Functions. Așa cum sunt numite, funcțiile Cloud sunt funcții reutilizabile bazate pe evenimente implementate în Google Cloud pentru a asculta declanșatorii specifici din cele șase declanșatoare de evenimente disponibile și apoi pentru a efectua operația pentru care a fost scris.

Funcțiile cloud care sunt de scurtă durată ( cu un timp de execuție implicit de 60 de secunde și maximum 9 minute ) pot fi scrise folosind JavaScript, Python, Golang și Java și executate folosind timpul lor de execuție. În JavaScript, ele pot fi executate folosind doar unele versiuni disponibile ale Node runtime și sunt scrise sub formă de module CommonJS folosind JavaScript simplu, deoarece sunt exportate ca funcție principală care urmează să fie rulată pe Google Cloud.

Un exemplu de funcție cloud este cel de mai jos, care este un boilerplate gol pentru funcția de a gestiona datele unui utilizator.

 // index.js exports.firestoreFunction = function (req, res) { return res.status(200).send({ data: `Hello ${req.query.name}` }); }

Mai sus avem un modul care exportă o funcție. Când este executat, primește argumentele de cerere și răspuns similar cu o rută HTTP .

Notă : O funcție cloud se potrivește cu fiecare protocol HTTP atunci când se face o solicitare. Acest lucru merită remarcat atunci când se așteaptă date în argumentul cererii, deoarece datele atașate atunci când se face o solicitare pentru a executa o funcție cloud ar fi prezente în corpul cererii pentru cererile POST în timp ce în corpul interogării pentru cererile GET .

Funcțiile cloud pot fi executate local în timpul dezvoltării prin instalarea pachetului @google-cloud/functions-framework în același folder în care este plasată funcția scrisă sau făcând o instalare globală pentru a o utiliza pentru mai multe funcții rulând npm i -g @google-cloud/functions-framework din linia de comandă. Odată instalat, ar trebui adăugat la scriptul package.json cu numele modulului exportat similar cu cel de mai jos:

 "scripts": { "start": "functions-framework --target=firestoreFunction --port=8000", }

Mai sus avem o singură comandă în scripturile noastre în fișierul package.json care rulează funcțiile-cadru și specifică, de asemenea, firestoreFunction ca funcție țintă care urmează să fie rulată local pe portul 8000 .

Putem testa punctul final al acestei funcții făcând o solicitare GET către portul 8000 de pe localhost folosind curl. Lipirea comenzii de mai jos într-un terminal va face asta și va returna un răspuns.

 curl https://localhost:8000?name="Smashing Magazine Author"

Comanda de mai sus face o solicitare cu o metodă GET HTTP și răspunde cu un cod de stare 200 și un obiect de date care conține numele adăugat în interogare.

Implementarea unei funcții cloud

Dintre metodele de implementare disponibile, o modalitate rapidă de a implementa o funcție cloud de pe o mașină locală este să utilizați cloud Sdk după instalarea acestuia. Rularea comenzii de mai jos de pe terminal după autentificarea gcloud sdk cu proiectul dvs. pe Google Cloud, ar implementa o funcție creată local în serviciul Cloud Function.

 gcloud functions deploy "demo-function" --runtime nodejs10 --trigger-http --entry-point=demo --timeout=60 --set-env-vars=[name="Developer"] --allow-unauthenticated

Folosind semnalizatoarele explicate mai jos, comanda de mai sus implementează o funcție declanșată HTTP în cloud-ul Google cu numele „ funcție demo ”.

  • NUME
    Acesta este numele dat unei funcții cloud la implementarea acesteia și este necesar.
  • region
    Aceasta este regiunea în care urmează să fie implementată funcția cloud. În mod implicit, este implementat în us-central1 .
  • trigger-http
    Aceasta selectează HTTP ca tip de declanșare al funcției.
  • allow-unauthenticated
    Acest lucru permite ca funcția să fie invocată în afara Google Cloud prin Internet folosind punctul final generat, fără a verifica dacă apelantul este autentificat.
  • source
    Calea locală de la terminal la fișierul care conține funcția de implementat.
  • entry-point
    Acesta este modulul exportat specific care urmează să fie implementat din fișierul în care au fost scrise funcțiile.
  • runtime
    Acesta este limbajul de rulare care va fi folosit pentru funcția din această listă de timpi de rulare acceptați.
  • timeout
    Acesta este timpul maxim pe care îl poate rula o funcție înainte de expirarea timpului. Este de 60 de secunde în mod implicit și poate fi setat la maximum 9 minute.

Notă : a face ca o funcție să permită cereri neautentificate înseamnă că oricine are punctul final al funcției dvs. poate face și cereri fără ca dvs. să le acordați. Pentru a atenua acest lucru, ne putem asigura că punctul final rămâne privat utilizându-l prin variabile de mediu sau solicitând anteturi de autorizare pentru fiecare cerere.

Acum că funcția noastră demo a fost implementată și avem punctul final, putem testa această funcție ca și cum ar fi folosită într-o aplicație reală folosind o instalare globală a tunului automat. Rularea autocannon -d=5 -c=300 CLOUD_FUNCTION_URL de la terminalul deschis ar genera 300 de solicitări simultane către funcția cloud într-o durată de 5 secunde. Acest lucru este mai mult decât suficient pentru a porni funcția cloud și, de asemenea, pentru a genera câteva valori pe care le putem explora pe tabloul de bord al funcției.

Notă : punctul final al unei funcții va fi imprimat în terminal după implementare. Dacă nu este cazul, rulați gcloud function describe FUNCTION_NAME de la terminal pentru a obține detalii despre funcția implementată, inclusiv punctul final.

Folosind fila de metrici de pe tabloul de bord, putem vedea o reprezentare vizuală a ultimei cereri constând în câte invocări au fost făcute, cât timp au durat, amprenta de memorie a funcției și câte instanțe au fost rotite pentru a gestiona solicitările făcute.

Tabloul de bord al unei funcții care arată o diagramă cu valorile adunate din toate solicitările recente făcute.
Tabloul de bord al funcției cloud care arată toate solicitările făcute. (Previzualizare mare)

O privire mai atentă asupra diagramei Instanțe active din imaginea de mai sus arată capacitatea de scalare orizontală a Funcțiilor Cloud, deoarece putem vedea că 209 de instanțe au fost rotite în câteva secunde pentru a gestiona cererile făcute folosind autocanon.

Jurnalele funcției cloud

Fiecare funcție implementată în cloud-ul Google are un jurnal și de fiecare dată când această funcție este executată, se face o nouă intrare în acel jurnal. Din fila Jurnal de pe tabloul de bord al funcției, putem vedea o listă cu toate intrările de jurnal dintr-o funcție cloud.

Mai jos sunt intrările de jurnal din demo-function implementată, creată ca urmare a solicitărilor pe care le-am făcut folosind autocannon .

Jurnalul funcției cloud care arată jurnalele din timpul de execuție a funcției.
Fila jurnal al funcției cloud care arată toate jurnalele de execuție. (Previzualizare mare)

Fiecare dintre intrările de jurnal de mai sus arată exact când a fost executată o funcție, cât timp a durat execuția și cu ce cod de stare s-a încheiat. Dacă există erori care rezultă dintr-o funcție, detaliile erorii, inclusiv linia în care a apărut, vor fi afișate în jurnalele aici.

Logs Explorer de pe Google Cloud poate fi folosit pentru a vedea detalii mai cuprinzătoare despre jurnalele dintr-o funcție cloud.

Funcționează în cloud cu aplicații front-end

Funcțiile cloud sunt foarte utile și puternice pentru inginerii front-end. Un inginer front-end fără cunoștințe de gestionare a aplicațiilor back-end poate extrage o funcționalitate într-o funcție cloud, poate implementa în Google Cloud și poate utiliza într-o aplicație front-end făcând solicitări HTTP către funcția cloud prin punctul său final.

Pentru a arăta cum pot fi utilizate funcțiile cloud într-o aplicație front-end, vom adăuga mai multe caracteristici acestei aplicații React. Aplicația are deja o rutare de bază între autentificare și configurarea paginilor de pornire. Îl vom extinde pentru a folosi API-ul React Context pentru a gestiona starea aplicației noastre, deoarece utilizarea funcțiilor cloud create s-ar face în cadrul reducerilor de aplicație.

Pentru a începe, creăm contextul aplicației noastre folosind API-ul createContext și, de asemenea, creăm un reductor pentru gestionarea acțiunilor din aplicația noastră.

 // state/index.js import { createContext } from “react”;

export const UserReducer = (action, state) => { switch (action.type) { case „CREATE-USER”: break; caz „ÎNCĂRCARE-UTILIZATOR-IMAGE”: pauză; case „FETCH-DATA” : break case „LOGOUT” : break; implicit: console.log( ${action.type} is not recognized ) } };

export const userState = { utilizator: nul, isLoggedIn: false };

export const UserContext = createContext(userState);

Mai sus, am început cu crearea unei funcții UserReducer care conține o instrucțiune switch, permițându-i să efectueze o operație bazată pe tipul de acțiune trimis în ea. Declarația switch are patru cazuri și acestea sunt acțiunile pe care le vom gestiona. Deocamdată nu fac încă nimic, dar când vom începe integrarea cu funcțiile noastre cloud, vom implementa progresiv acțiunile care trebuie efectuate în ele.

De asemenea, am creat și exportat contextul aplicației noastre folosind API-ul React createContext și i-am dat o valoare implicită a obiectului userState care conține o valoare utilizator în prezent, care ar fi actualizată de la nul la datele utilizatorului după autentificare și, de asemenea, o valoare booleană isLoggedIn pentru a ști dacă utilizatorul este autentificat sau nu.

Acum putem continua să ne consumăm contextul, dar înainte de a face asta, trebuie să împachetăm întregul arbore al aplicației cu Furnizorul atașat la UserContext pentru ca componentele copii să se poată abona la schimbarea valorii contextului nostru.

 // index.js import React from "react"; import ReactDOM from "react-dom"; import "./index.css"; import App from "./app"; import { UserContext, userState } from "./state/"; ReactDOM.render( <React.StrictMode> <UserContext.Provider value={userState}> <App /> </UserContext.Provider> </React.StrictMode>, document.getElementById("root") ); serviceWorker.unregister();

Încheiem aplicația noastră enter cu furnizorul UserContext la componenta rădăcină și am transmis valoarea implicită userState creată anterior în valoarea prop.

Acum că avem starea aplicației complet configurată, putem trece la crearea modelului de date al utilizatorului folosind Google Cloud Firestore printr-o funcție cloud.

Gestionarea datelor aplicației

Datele unui utilizator din această aplicație constau dintr-un id unic, un e-mail, o parolă și adresa URL a unei imagini. Folosind o funcție cloud, aceste date vor fi stocate în cloud folosind Serviciul Cloud Firestore, care este oferit pe platforma Google Cloud.

Google Cloud Firestore , o bază de date NoSQL flexibilă a fost scoasă din Firebase Realtime Database cu noi funcții îmbunătățite, care permit interogări mai bogate și mai rapide, alături de suport pentru date offline. Datele din cadrul serviciului Firestore sunt organizate în colecții și documente similare cu alte baze de date NoSQL, cum ar fi MongoDB.

Firestore poate fi accesat vizual prin Google Cloud Console. Pentru a-l lansa, deschideți panoul de navigare din stânga și derulați în jos la secțiunea Bază de date și faceți clic pe Firestore. Aceasta ar afișa lista de colecții pentru utilizatorii cu date existente sau ar solicita utilizatorului să creeze o nouă colecție atunci când nu există nicio colecție existentă. Am crea o colecție de utilizatori care să fie folosită de aplicația noastră.

Similar cu alte servicii de pe Google Cloud Platform, Cloud Firestore are, de asemenea, o bibliotecă client JavaScript creată pentru a fi utilizată într-un mediu nod ( se va genera o eroare dacă este folosită în browser ). Pentru a improviza, folosim Cloud Firestore într-o funcție cloud folosind pachetul @google-cloud/firestore .

Utilizarea Cloud Firestore cu o funcție Cloud

Pentru a începe, vom redenumi prima funcție pe care am creat-o din demo-function în firestoreFunction și apoi o vom extinde pentru a se conecta cu Firestore și a salva datele în colecția utilizatorilor noștri.

 require("dotenv").config(); const { Firestore } = require("@google-cloud/firestore"); const { SecretManagerServiceClient } = require("@google-cloud/secret-manager"); const client = new SecretManagerServiceClient(); exports.firestoreFunction = function (req, res) { return { const { email, password, type } = req.body; const firestore = new Firestore(); const document = firestore.collection("users"); console.log(document) // prints details of the collection to the function logs if (!type) { res.status(422).send("An action type was not specified"); } switch (type) { case "CREATE-USER": break case "LOGIN-USER": break; default: res.status(422).send(`${type} is not a valid function action`) } };

Pentru a gestiona mai multe operațiuni care implică depozitul de incendiu, am adăugat o declarație de comutare cu două cazuri pentru a gestiona nevoile de autentificare ale aplicației noastre. Declarația noastră switch evaluează o expresie de type pe care o adăugăm la corpul solicitării atunci când facem o solicitare la această funcție din aplicația noastră și ori de câte ori aceste date de type nu sunt prezente în corpul cererii noastre, cererea este identificată ca o Solicitare greșită și un cod de stare 400 . alături de un mesaj pentru a indica type lipsă este trimis ca răspuns.

Stabilim o conexiune cu Firestore folosind biblioteca Application Default Credentials (ADC) din biblioteca client Cloud Firestore. Pe linia următoare, numim metoda de colectare într-o altă variabilă și trecem numele colecției noastre. Vom folosi aceasta pentru a efectua în continuare alte operațiuni de colectare a documentelor conținute.

Notă : bibliotecile client pentru serviciile de pe Google Cloud se conectează la serviciul respectiv folosind o cheie creată de cont de serviciu transmisă la inițializarea constructorului. Când cheia contului de serviciu nu este prezentă, se utilizează implicit acreditările implicite ale aplicației, care la rândul lor se conectează folosind rolurile IAM atribuite funcției cloud.

După editarea codului sursă al unei funcții care a fost implementată local utilizând SDK-ul Gcloud, putem rula din nou comanda anterioară de pe un terminal pentru a actualiza și reimplementa funcția cloud.

Acum că a fost stabilită o conexiune, putem implementa cazul CREATE-USER pentru a crea un utilizator nou folosind datele din corpul solicitării.

 require("dotenv").config(); const { Firestore } = require("@google-cloud/firestore"); const path = require("path"); const { v4 : uuid } = require("uuid") const cors = require("cors")({ origin: true }); const client = new SecretManagerServiceClient(); exports.firestoreFunction = function (req, res) { return cors(req, res, () => { const { email, password, type } = req.body; const firestore = new Firestore(); const document = firestore.collection("users"); if (!type) { res.status(422).send("An action type was not specified"); } switch (type) { case "CREATE-USER": if (!email || !password) { res.status(422).send("email and password fields missing"); } const id = uuid() return bcrypt.genSalt(10, (err, salt) => { bcrypt.hash(password, salt, (err, hash) => { document.doc(id) .set({ id : id email: email, password: hash, img_uri : null }) .then((response) => res.status(200).send(response)) .catch((e) => res.status(501).send({ error : e }) ); }); }); case "LOGIN": break; default: res.status(400).send(`${type} is not a valid function action`) } }); };

Am generat un UUID utilizând pachetul uuid pentru a fi folosit ca ID-ul documentului care urmează să fie salvat, trecându-l în metoda set pe document și, de asemenea, ID-ul utilizatorului. În mod implicit, un ID aleatoriu este generat pe fiecare document inserat, dar în acest caz, vom actualiza documentul atunci când gestionăm încărcarea imaginii, iar UUID-ul este cel care va fi folosit pentru ca un anumit document să fie actualizat. În loc să stocăm parola utilizatorului în text simplu, o sărăm mai întâi folosind bcryptjs, apoi stocăm hashul rezultat ca parolă a utilizatorului.

Integrând funcția firestoreFunction cloud în aplicație, o folosim din cazul CREATE_USER din reductor de utilizator.

După ce faceți clic pe butonul Creare cont , o acțiune este trimisă reductorilor cu un tip CREATE_USER pentru a face o solicitare POST care conține e-mailul introdus și parola către punctul final al funcției firestoreFunction .

 import { createContext } from "react"; import { navigate } from "@reach/router"; import Axios from "axios"; export const userState = { user : null, isLoggedIn: false, }; export const UserReducer = (state, action) => { switch (action.type) { case "CREATE_USER": const FIRESTORE_FUNCTION = process.env.REACT_APP_FIRESTORE_FUNCTION; const { userEmail, userPassword } = action; const data = { type: "CREATE-USER", email: userEmail, password: userPassword, }; Axios.post(`${FIRESTORE_FUNCTION}`, data) .then((res) => { navigate("/home"); return { ...state, isLoggedIn: true }; }) .catch((e) => console.log(`couldnt create user. error : ${e}`)); break; case "LOGIN-USER": break; case "UPLOAD-USER-IMAGE": break; case "FETCH-DATA" : break case "LOGOUT": navigate("/login"); return { ...state, isLoggedIn: false }; default: break; } }; export const UserContext = createContext(userState);

Mai sus, am folosit Axios pentru a face cererea către firestoreFunction și după ce această solicitare a fost rezolvată, am setat starea inițială a utilizatorului de la null la datele returnate din cerere și, în sfârșit, direcționăm utilizatorul către pagina de pornire ca utilizator autentificat. .

În acest moment, un utilizator nou poate crea cu succes un cont și poate fi direcționat către pagina de pornire. Acest proces demonstrează cum folosim Firestore pentru a realiza o creare de bază a datelor dintr-o funcție cloud.

Gestionarea stocării fișierelor

Stocarea și preluarea fișierelor unui utilizator într-o aplicație este de cele mai multe ori o caracteristică foarte necesară în cadrul unei aplicații. Într-o aplicație conectată la un backend node.js, Multer este adesea folosit ca middleware pentru a gestiona datele din mai multe părți/form în care vine un fișier încărcat. Dar, în absența backend-ului node.js, am putea folosi un fișier online. serviciu de stocare, cum ar fi Google Cloud Storage, pentru a stoca activele statice ale aplicațiilor.

Google Cloud Storage este un serviciu de stocare a fișierelor disponibil la nivel global, folosit pentru a stoca orice cantitate de date ca obiecte pentru aplicații în compartimente. Este suficient de flexibil pentru a gestiona stocarea activelor statice atât pentru aplicații de dimensiuni mici, cât și pentru cele mari.

Pentru a folosi serviciul de stocare în cloud în cadrul unei aplicații, am putea folosi punctele finale disponibile API de stocare sau utilizând biblioteca client oficială a nodului Storage. Cu toate acestea, biblioteca client Node Storage nu funcționează într-o fereastră de browser, așa că am putea folosi o funcție Cloud în care vom folosi biblioteca.

Un exemplu în acest sens este Funcția Cloud de mai jos, care conectează și încarcă un fișier într-un Cloud Bucket creat.

 const cors = require("cors")({ origin: true }); const { Storage } = require("@google-cloud/storage"); const StorageClient = new Storage(); exports.Uploader = (req, res) => { const { file } = req.body; StorageClient.bucket("TEST_BUCKET") .file(file.name) .then((response) => { console.log(response); res.status(200).send(response) }) .catch((e) => res.status(422).send({error : e})); }); };

Din funcția cloud de mai sus, efectuăm următoarele două operațiuni principale:

  • În primul rând, creăm o conexiune la Cloud Storage în Storage constructor și folosește caracteristica Application Default Credentials (ADC) de pe Google Cloud pentru a se autentifica cu Cloud Storage.

  • În al doilea rând, încărcăm fișierul inclus în corpul cererii în TEST_BUCKET apelând metoda .file și introducând numele fișierului. Deoarece aceasta este o operațiune asincronă, folosim o promisiune pentru a ști când această acțiune a fost rezolvată și trimitem un răspuns de 200 înapoi, terminând astfel ciclul de viață al invocării.

Acum, putem extinde funcția Uploader Cloud de mai sus pentru a gestiona încărcarea imaginii de profil a unui utilizator. Funcția de cloud va primi imaginea de profil a unui utilizator, o va stoca în compartimentul cloud al aplicației noastre și apoi va actualiza datele img_uri ale utilizatorului din colecția utilizatorilor noștri în serviciul Firestore.

 require("dotenv").config(); const { Firestore } = require("@google-cloud/firestore"); const cors = require("cors")({ origin: true }); const { Storage } = require("@google-cloud/storage"); const StorageClient = new Storage(); const BucketName = process.env.STORAGE_BUCKET exports.Uploader = (req, res) => { return Cors(req, res, () => { const { file , userId } = req.body; const firestore = new Firestore(); const document = firestore.collection("users"); StorageClient.bucket(BucketName) .file(file.name) .on("finish", () => { StorageClient.bucket(BucketName) .file(file.name) .makePublic() .then(() => { const img_uri = `https://storage.googleapis.com/${Bucket}/${file.path}`; document .doc(userId) .update({ img_uri, }) .then((updateResult) => res.status(200).send(updateResult)) .catch((e) => res.status(500).send(e)); }) .catch((e) => console.log(e)); }); }); };

Acum am extins funcția de încărcare de mai sus pentru a efectua următoarele operații suplimentare:

  • În primul rând, face o nouă conexiune la serviciul Firestore pentru a obține colecția users noștri prin inițializarea constructorului Firestore și folosește acreditările implicite ale aplicației (ADC) pentru a se autentifica cu Cloud Storage.
  • După încărcarea fișierului adăugat în corpul solicitării, îl facem public pentru a fi accesibil printr-o adresă URL publică apelând metoda makePublic pe fișierul încărcat. Conform controlului de acces implicit al Cloud Storage, fără a face public un fișier, un fișier nu poate fi accesat prin internet și pentru a putea face acest lucru atunci când se încarcă aplicația.

Notă : publicarea unui fișier înseamnă că oricine care utilizează aplicația dvs. poate copia linkul fișierului și poate avea acces nerestricționat la fișier. O modalitate de a preveni acest lucru este utilizarea unei adrese URL semnate pentru a acorda acces temporar la un fișier din compartimentul dvs., în loc să îl faceți complet public.

  • Apoi, actualizăm datele existente ale utilizatorului pentru a include adresa URL a fișierului încărcat. Găsim datele utilizatorului specific folosind interogarea WHERE a Firestore și folosim userId -ul inclus în corpul solicitării, apoi setăm câmpul img_uri să conțină adresa URL a imaginii nou actualizate.

Funcția de Upload în cloud de mai sus poate fi utilizată în orice aplicație care are utilizatori înregistrați în cadrul serviciului Firestore. Tot ceea ce este necesar pentru a face o solicitare POST la punctul final, punând IS-ul utilizatorului și o imagine în corpul cererii.

Un exemplu în acest sens în cadrul aplicației este cazul UPLOAD-FILE care face o cerere POST către funcție și pune link-ul de imagine returnat de la cerere în starea aplicației.

 # index.js import Axios from 'axios' const UPLOAD_FUNCTION = process.env.REACT_APP_UPLOAD_FUNCTION export const UserReducer = (state, action) => { switch (action.type) { case "CREATE-USER" : # .....CREATE-USER-LOGIC .... case "UPLOAD-FILE": const { file, id } = action return Axios.post(UPLOAD_FUNCTION, { file, id }, { headers: { "Content-Type": "image/png", }, }) .then((response) => {}) .catch((e) => console.log(e)); default : return console.log(`${action.type} case not recognized`) } }

Din cazul de comutare de mai sus, facem o solicitare POST folosind Axios la UPLOAD_FUNCTION , trecând fișierul adăugat pentru a fi inclus în corpul cererii și am adăugat, de asemenea, o imagine Content-Type în antetul solicitării.

După o încărcare cu succes, răspunsul returnat de la funcția cloud conține documentul de date al utilizatorului care a fost actualizat pentru a conține o adresă URL validă a imaginii încărcate în stocarea în cloud Google. Putem apoi actualiza starea utilizatorului pentru a conține noile date, iar acest lucru va actualiza și elementul src al imaginii de profil al utilizatorului din componenta de profil.

Pagina de profil a unui utilizator care cu o imagine de profil actualizată
Pagina de profil a unui utilizator care tocmai a fost actualizată pentru a afișa imaginea de profil nou actualizată. (Previzualizare mare)

Gestionarea joburilor Cron

Sarcinile automate repetitive, cum ar fi trimiterea de e-mailuri către utilizatori sau efectuarea unei acțiuni interne la un moment dat, sunt de cele mai multe ori o caracteristică inclusă a aplicațiilor. Într-o aplicație obișnuită node.js, astfel de sarcini ar putea fi gestionate ca job-uri cron folosind node-cron sau node-schedule. Când construiți aplicații fără server folosind Google Cloud Platform, Cloud Scheduler este, de asemenea, proiectat pentru a efectua o operație cron.

Notă : Deși Cloud Scheduler funcționează similar cu utilitarul cron Unix în crearea de joburi care sunt executate în viitor, este important de reținut că Cloud Scheduler nu execută o comandă așa cum o face utilitarul cron. Mai degrabă, efectuează o operațiune folosind o țintă specificată.

După cum sugerează și numele, Cloud Scheduler permite utilizatorilor să programeze o operațiune pentru a fi efectuată într-un moment viitor. Fiecare operațiune se numește job și joburile pot fi create vizual, actualizate și chiar distruse din secțiunea Scheduler a Cloud Console. Pe lângă un câmp de nume și descriere, locurile de muncă din Cloud Scheduler constau în următoarele:

  • Frecvență
    Acesta este folosit pentru a programa execuția jobului Cron. Programele sunt specificate folosind formatul unix-cron care este utilizat inițial la crearea de joburi de fundal pe tabelul cron într-un mediu Linux. Formatul unix-cron constă dintr-un șir cu cinci valori, fiecare reprezentând un punct de timp. Mai jos putem vedea fiecare dintre cele cinci șiruri și valorile pe care le reprezintă.
 - - - - - - - - - - - - - - - - minute ( - 59 ) | - - - - - - - - - - - - - hour ( 0 - 23 ) | | - - - - - - - - - - - - day of month ( 1 - 31 ) | | | - - - - - - - - - month ( 1 - 12 ) | | | | - - - - - -- day of week ( 0 - 6 ) | | | | | | | | | | | | | | | | | | | | | | | | | * * * * *

Instrumentul generator Crontab este util atunci când se încearcă generarea unei valori de frecvență-timp pentru o lucrare. Dacă vă este dificil să puneți împreună valorile de timp, generatorul Crontab are un drop-down vizual unde puteți selecta valorile care alcătuiesc un program și copiați valoarea generată și utilizați ca frecvență.

  • Fus orar
    Fusul orar de unde este executat jobul cron. Datorită diferenței de timp dintre fusurile orare, joburile cron executate cu diferite fusuri orare specificate vor avea timpi de execuție diferiți.
  • Ţintă
    Acesta este ceea ce este folosit în execuția jobului specificat. O țintă ar putea fi un tip HTTP în care jobul face o solicitare la ora specificată către URL sau un subiect Pub/Sub în care jobul poate publica mesaje sau extrage mesaje și, în sfârșit, o aplicație App Engine.

Cloud Scheduler se combină perfect cu funcțiile cloud declanșate prin HTTP. Când este creată o lucrare în Cloud Scheduler cu ținta setată la HTTP, această sarcină poate fi folosită pentru a executa o funcție cloud. Tot ceea ce trebuie făcut este să specificați punctul final al funcției cloud, să specificați verbul HTTP al cererii, apoi să adăugați orice date care trebuie transmise pentru a funcționa în câmpul de corp afișat. După cum se arată în exemplul de mai jos:

Câmpurile necesare pentru crearea unui job cron folosind consola cloud
Câmpurile necesare pentru crearea unui job cron folosind consola cloud. (Previzualizare mare)

Lucrarea cron din imaginea de mai sus va rula până la ora 9:00 în fiecare zi, făcând o solicitare POST către punctul final de probă al unei funcții cloud.

Un caz de utilizare mai realist al unui job cron este trimiterea de e-mailuri programate către utilizatori la un interval dat folosind un serviciu de corespondență extern, cum ar fi Mailgun. Pentru a vedea acest lucru în acțiune, vom crea o nouă funcție cloud care trimite un e-mail HTML la o adresă de e-mail specificată folosind pachetul JavaScript nodemailer pentru a vă conecta la Mailgun:

 # index.js require("dotenv").config(); const nodemailer = require("nodemailer"); exports.Emailer = (req, res) => { let sender = process.env.SENDER; const { reciever, type } = req.body var transport = nodemailer.createTransport({ host: process.env.HOST, port: process.env.PORT, secure: false, auth: { user: process.env.SMTP_USERNAME, pass: process.env.SMTP_PASSWORD, }, }); if (!reciever) { res.status(400).send({ error: `Empty email address` }); } transport.verify(function (error, success) { if (error) { res .status(401) .send({ error: `failed to connect with stmp. check credentials` }); } }); switch (type) { case "statistics": return transport.sendMail( { from: sender, to: reciever, subject: "Your usage satistics of demo app", html: { path: "./welcome.html" }, }, (error, info) => { if (error) { res.status(401).send({ error : error }); } transport.close(); res.status(200).send({data : info}); } ); default: res.status(500).send({ error: "An available email template type has not been matched.", }); } };

Using the cloud function above we can send an email to any user's email address specified as the receiver value in the request body. It performs the sending of emails through the following steps:

  • It creates an SMTP transport for sending messages by passing the host , user and pass which stands for password, all displayed on the user's Mailgun dashboard when a new account is created.
  • Next, it verifies if the SMTP transport has the credentials needed in order to establish a connection. If there's an error in establishing the connection, it ends the function's invocation and sends back a 401 unauthenticated status code.
  • Next, it calls the sendMail method to send the email containing the HTML file as the email's body to the receiver's email address specified in the to field.

Note : We use a switch statement in the cloud function above to make it more reusable for sending several emails for different recipients. This way we can send different emails based on the type field included in the request body when calling this cloud function.

Now that there is a function that can send an email to a user; we are left with creating the cron job to invoke this cloud function. This time, the cron jobs are created dynamically each time a new user is created using the official Google cloud client library for the Cloud Scheduler from the initial firestoreFunction .

We expand the CREATE-USER case to create the job which sends the email to the created user at a one-day interval.

 require("dotenv").config();cloc const { Firestore } = require("@google-cloud/firestore"); const scheduler = require("@google-cloud/scheduler") const cors = require("cors")({ origin: true }); const EMAILER = proccess.env.EMAILER_ENDPOINT const parent = ScheduleClient.locationPath( process.env.PROJECT_ID, process.env.LOCATION_ID ); exports.firestoreFunction = function (req, res) { return cors(req, res, () => { const { email, password, type } = req.body; const firestore = new Firestore(); const document = firestore.collection("users"); const client = new Scheduler.CloudSchedulerClient() if (!type) { res.status(422).send({ error : "An action type was not specified"}); } switch (type) { case "CREATE-USER":

      const job = { httpTarget: { uri: process.env.EMAIL_FUNCTION_ENDPOINT, httpMethod: "POST", body: { email: email, }, }, schedule: "*/30 */6 */5 10 4", timezone: "Africa/Lagos", }
 if (!email || !password) { res.status(422).send("email and password fields missing"); } return bcrypt.genSalt(10, (err, salt) => { bcrypt.hash(password, salt, (err, hash) => { document .add({ email: email, password: hash, }) .then((response) => {
                  client.createJob({ parent : parent, job : job }).then(() => res.status(200).send(response)) .catch(e => console.log(`unable to create job : ${e}`) )
 }) .catch((e) => res.status(501).send(`error inserting data : ${e}`) ); }); }); default: res.status(422).send(`${type} is not a valid function action`) } }); };

From the snippet above, we can see the following:

  • A connection to the Cloud Scheduler from the Scheduler constructor using the Application Default Credentials (ADC) is made.
  • We create an object consisting of the following details which make up the cron job to be created:
    • uri
      The endpoint of our email cloud function in which a request would be made to.
    • body
      This is the data containing the email address of the user to be included when the request is made.
    • schedule
      The unix cron format representing the time when this cron job is to be performed.
  • After the promise from inserting the user's data document is resolved, we create the cron job by calling the createJob method and passing in the job object and the parent.
  • The function's execution is ended with a 200 status code after the promise from the createJob operation has been resolved.

After the job is created, we'll see it listed on the scheduler page.

List of all scheduled cron jobs including the last created job.
List of all scheduled cron jobs including the last created job. (Previzualizare mare)

From the image above we can see the time scheduled for this job to be executed. We can decide to manually run this job or wait for it to be executed at the scheduled time.

Concluzie

Within this article, we have had a good look into serverless applications and the benefits of using them. We also had an extensive look at how developers can manage their serverless applications on the Google Cloud using Cloud Functions so you now know how the Google Cloud is supporting the use of serverless applications.

Within the next years to come, we will certainly see a large number of developers adapt to the use of serverless applications when building applications. If you are using cloud functions in a production environment, it is recommended that you read this article from a Google Cloud advocate on “6 Strategies For Scaling Your Serverless Applications”.

The source code of the created cloud functions are available within this Github repository and also the used front-end application within this Github repository. The front-end application has been deployed using Netlify and can be tested live here.

Referințe

  • Google Cloud
  • Cloud Functions
  • Cloud Source Repositories
  • Cloud Scheduler overview
  • Cloud Firestore
  • “6 Strategies For Scaling Your Serverless Applications,” Preston Holmes