Firestore ja Fetch API - asynkroninen javascript

Tutustutaan Googlen Firestore-tietokantaan. Opetellaan myös käyttämään Javascriptin Fetch API -rajapintaa, jolla saadaan asynkronisesti kommunikoitua WWW-palvelimen kanssa. Myös Firestore-tietokantaan pääsee Javascriptilla käsiksi asynkronisen rajapinnan kautta. Tutustutaan Javascriptin Promiseihin.

Firestore

Varmista, että tietokoneellasi on uusin python. Mielellään Python 3.12.

Aloita ohjeen Get started with Cloud Firestore mukaan

Fetch API

Tutustu Javascriptin promiseihin lukemalla Using Promises -artikkeli.

Hyödynnetään Fetch-rajapintaa lomakkeen täytön apuna. Tutustu ensin ajax-luentoon ja ajax-esimerkkeihin, joissa käytetään jQuerya ja XMLHttpRequest-rajapintaa. Idea näissä on tismalleen sama kuin Fetch-rajapinnassa, mutta Fetch on tällä hetkellä järkevin rajapintavalinta käytettäväksi. Jos Javascript ei ole sinulle ennestään tuttu, niin kertaa TIEA2120 Web-käyttöliittymien ohjelmointi -kurssin ohjaustehtäviä

Malli Fetch API -versio, Lähdekoodi (javascript), Lähdekoodi (flask / python), ryhmat.xml (template)

Flask ja JSON

Ensimmäisenä tarvii valmistella palvelimella toimiva ohjelma tuottamaan JSON-muotoista dataa. Luodaan flask-ohjelma, joka palauttaa listan postitoimipaikoista JSON-muodossa.

Luo ensimmäisenä uusi SQLite3-tietokanta. Käytä oppilaitos.sql-SQL-koodia.

tietokannan rakenne

Flask ja XML

Monesti tiedonsiirtomuotona ei olekkaan JSON vaan XML.

Toteuta samaan tapaan kuin edellä kaikkien oppilaitosryhmien hakeminen, mutta palauta listaus sellaisessa XML-muodossa, että sitä voidaan suoraan käyttää HTML-dokumentissa. Käytä mediatyyppinä text/xml. Muodosta tarvittava XML Jinja-templaten avulla:

<?xml version="1.0" encoding="UTF-8"?>
<select id="{{name}}" name="{{name}}" xmlns="http://www.w3.org/1999/xhtml">
{% for o in ryhmat %}
{% if loop.first %}
    <option selected="selected" value="{{o["ryhmaid"]}}">{{ o["nimi"] }}</option>
{% else %}
    <option value="{{o["ryhmaid"]}}">{{ o["nimi"] }}</option>
{% endif %}
{% endfor %}
</select>

Kts. malli

Käyttäminen poikkeaa hieman aiemmista, koska myös nyt halutaan erikseen varmistaa mikä on merkistö ja mediatyyppi:

    resp = make_response( render_template("ryhmat.xml",ryhmat=ryhmat, name="oppilaitosryhma"))
    resp.charset = "UTF-8"
    resp.mimetype = "text/xml"
        
    return resp

Vaihtoehtoisia toteutustapoja

XML-dokumentin voit toteuttaa myös muilla tavoilla kuin Jinja-templatella. Jinja on kaikista epävarmin tapa tuottaa ehjiä XML-dokumentteja. DOM-rajapinta (minidom) tai ElementTree ovat parempia.

DOM-rajapinta

Saman voi toteuttaa myös minidom-kirjaston avulla jolloin templatea ei tarvita. Vrt. Javascript ja DOM. Kokeile:

    from xml.dom.minidom import getDOMImplementation, parse, parseString

    impl = getDOMImplementation()
    # createDocument(nimiavaruus, juurielementti, dokumenttityyppi)
    doc = impl.createDocument("http://www.w3.org/1999/xhtml", "select", None)
    # pakko laittaa seuraava, koska jostakin syystä edellinen ei riitä
    doc.documentElement.setAttribute("xmlns", "http://www.w3.org/1999/xhtml")
    doc.documentElement.setAttribute("name", "ryhmat")
    doc.documentElement.setAttribute("id", "ryhmat")

    for ryhma in cur.fetchall():
        option = doc.createElement("option");
        txt = doc.createTextNode( ryhma['ryhmanimi'] );
        option.appendChild(txt)
        # minidom ei tue textContent-ominaisuutta
        # li.textContent = ryhma['ryhmanimi']
        option.setAttribute("value", str(ryhma["ryhmaid"]))
        doc.documentElement.appendChild(option)

    resp = make_response( doc.toxml('UTF-8') )
    resp.charset = "UTF-8"
    resp.mimetype = "text/xml"
    return resp
ElementTree

Voit myös tutustua pythonin xml.etree.ElementTree-kirjastoon ja muodostaa sen avulla tarvittavan xml-dokumentin:

    import xml.etree.ElementTree as ET
    xml = """<?xml version="1.0" encoding="UTF-8"?><select id="ryhmat" name="ryhmat"></select>"""

    root = ET.fromstring( xml )
    root.attrib["xmlns"] = "http://www.w3.org/1999/xhtml"
    for ryhma in cur.fetchall():
        option = ET.SubElement(root, "option")
        option.text = ryhma['ryhmanimi']
        option.attrib['value'] = str(ryhma["ryhmaid"])

    resp = make_response( ET.tostring( root, encoding="UTF-8", method="xml" ) )

Oppilaitokset

Toteuta samaan tapaan oppilaitosten hakeminen (vrt. edellä tehty yhden postitoimipaikan hakeminen). Rajaa haettavia oppilaitoksia jollakin oppilaitosryhmällä (ryhmaid). Jos ryhmää ei ole annettu, silloin hae kaikki oppilaitokset. Palauta oppilaitokset seuraavanlaisessa XML-muodossa. Luo XML-muoto joko DOM-rajapinnan tai ElementTreen avulla.

<oppilaitokset>
 <oppilaitos id="1">Helsingin kauppakorkeakoulu</oppilaitos>
 <oppilaitos id="2">Helsingin yliopisto</oppilaitos>
 <oppilaitos id="3">Joensuun yliopisto</oppilaitos>
 ...
</oppilaitokset>

Kts. malli

Javascript, Fetch ja JSON

Ota käyttöön valmis pohja. Pura samaan kansioon flask-sovelluksesi kanssa.

Muokkaa valmista osoite.js-tiedostoa. Toteutetaan postitoimipaikan valinta postinumeron perusteella.

  1. Lisää postinumeron change-tapahtumaan tapahtumankäsittelijä (hae_postitoimipaikka)
  2. Luo hae_postitoimipaikka-funktio, jossa Fetch-rajapinnan avulla kutsut aiemmin flaskilla tekemääsi postitoimipaikka-sivua.
    function hae_postitoimipaikka() {
    // asetukset fetch-kutsua varten
    // https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch
    let url = new URL("http://users.jyu.fi/~omatunnus/cgi-bin/ties4080/ohjaus5/flask.cgi/postitoimipaikka");
    // urliin on lisättävä ?postinumero=arvo, koska kyseessä on GET-tyyppinen request
    // Tämä tehdään seatchparams-objektin avulla
    // https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams
    // kts. https://javascriptplayground.com/url-search-params/
    url.searchParams.append("postinro", document.forms[0].elements.postinumero.value);
    
    // huomaa seuraavassa promisejen ketjutus
    fetch(url)
      .then(function(response) {
            // muunnetaan response-objektin sisältö jsoniksi
            // https://developer.mozilla.org/en-US/docs/Web/API/Response
            return response.json();
      })
    // käsitellään varsinainen tulos
      .then(function(data) {
    // data-muuttujassa saadaan javascript-objekti, josta löytyy tarvittava postitoimipaikka
    // huom. datan sisältö ja rakenne on juuri se mitä itse on muodostettu palvelimella
         try {
            document.forms[0].elements.postitoimipaikka.value = data[0]["postitoimipaikka"];
         }
        // jos jotain menee pieleen niin asetetaan postitoimipaikaksi tuntematon
         catch (e) {
            document.forms[0].elements.postitoimipaikka.value = "tuntematon";
         }
      })
    // seuraava nappaa kiinni verkkovirheet ym. 
    // Jos annettua osoitetta ei löydy, niin se ei ole virhe
      .catch(error => console.log('Virhe postitoimipaikan hakemisessa:', error));
    }
  3. Kokeile toimiiko sovellus ja muuttuuko postitoimipaikan sisältö. Katso Web Developer -työkalujen Network-välilehdeltä (CTRL+SHIFT+E) mitä tapahtuu, kun vaihdat postinumerokentän sisältöä. Tarkista myös Javascript Consolen (CTRL+SHIFT+K) tulosteet.

Fetch ja XML

Toteutetaan seuraavaksi oppilaitoksen valinta oppilaitosryhmän perusteella.

  1. Tee Javascript-tiedostoon uusi funktio hae_ryhmat, jossa pyydät hae_ryhmat-sivua.
    function hae_ryhmat() {
    // muista asynkronisuus
      fetch("/~omatunnus/cgi-bin/ties4080/ohjaus5/flask.cgi/hae_ryhmat")
        .then(function(response) {
           return response.text();
        })
        .then(function(data) {
            let parser = new window.DOMParser();
            let apu = parser.parseFromString( data, "text/xml" );
            // importoidaan saatu xml-dokumentti tähän dokumenttiin
            let select = document.importNode(apu.documentElement, 1);
            document.querySelector("#oppilaitosryhma").replaceWith(select);
           console.log("Oppilaitosryhmä on päivitetty");
        })
        .catch(error => console.log('Virhe ryhmien hakemisessa:', error));
      // seuraava tulostuu _ennen_ kuin oppilaitosryhmä on päivitetty, koska fetch on asynkroninen funktio
       console.log("hae-ryhmat-funktion suoritus loppui");
    }
  2. Kutsu hae_ryhmat-funktiota sivun window.onload-tapahtumassa, niin oppilaitosryhmälistaus päivittyy heti sivun latauduttua.

Oppilaitoksen vaihtaminen oppilaitosryhmän perusteella

Tilaajan lisääminen

Async ja await

async ja await tekevät asynkronisesta ohjelmoinnista selkeämpää.

async sana lisätään funktion nimen eteen. Tämä tekee funktiosta promisen. Tästä ei kuitenkaan ole mitään iloa ellei käytä funktion sisällä await-sanaa. await toimii vain asynkronisen funktion sisällä ja saa ohjelman odottamaan promisea.

// muunnetaan seuraava async/await-versioksi 
/*
fetch("/~omatunnus/cgi-bin/ties4080/ohjaus5/flask2.cgi/tilaajat")
  .then(function(response) {
       return response.text();
  })
  .then(function(data) {
      let parser = new window.DOMParser();
      let xml = parser.parseFromString( data, "text/xml" );
      let node = document.importNode(xml.documentElement, 1);
      let id = node.getAttribute("id");
      document.querySelector('#'+id).replaceWith(node);
  })
  .catch(error => console.log('Virhe tilaajien hakemisessa:', error));
*/

async function omafetch() {
  let response = await fetch("/~omatunnus/cgi-bin/ties4080/ohjaus5/flask2.cgi/tilaajat");

  if (!response.ok) {
    throw new Error(`HTTP error! status: ${response.status}`);
  }
  let data = await response.text();
  let parser = new window.DOMParser();
  let xml = parser.parseFromString( data, "text/xml" );
  let node = document.importNode(xml.documentElement, 1);
  let id = node.getAttribute("id");
  document.querySelector('#'+id).replaceWith(node);
  console.log("Tämä tulostuu viimeisenä");
}

/* kutsutaan edellä tehtyä funktiota. Tähän ei voi laittaa await -eteen, koska
ei olla asyncilla esitellyssä funktiossa. Ohjelma ei siis jää odottamaan tätä funktiota */
omafetch()
.catch(e => {
  console.log('Virhe tilaajien hakemisessa: ' + e.message);
});
console.log("Tämä tulostuu ensin");

Yritä edellä esitellyllä tavalla muuttaa ohjelmasi fetch-kutsut async/await-mallisiksi.

Malliratkaisu async/await-versio, javascript-lähdekoodi

Firebase, Firestore ja javascript-sovellus

Lisätietoa

Vanhoja versioita:
Yksinkertaisin ajax-malli (jquery), flask-osuuden lähdekoodi
vanha malliratkaisu Lähdekoodi (jQuery)

Käyttäjien kommentit

Kommentoi Lisää kommentti
Informaatioteknologia - Jyväskylän yliopiston informaatioteknologian tiedekunta