Kirjautuminen

Haku

Tehtävät

Keskustelu: Nettisivujen teko: Parempi vaihtoehto jQuery ajaxeihin

Sivun loppuun

juhauta [23.05.2019 03:43:35]

#

Olen nyt tehnyt muutaman laajemman ja normaalia monimutkaisemman ohjelman php+jquery:llä, käyttäen myös mysql & ja bootstrappia

Mutta ratkaisemattomaksi ongelmaksi on tullut se että kun suurin osa lomakkeiden lähettämisestä, sekä yksittäisten kenttien aputoiminnoista on ajaxilla, on virheiden ja erikoistilanteiden käsittely tuskaa ja vie koodia joka kerta 200 riviä.
Esim:

- kutsu lähettää jsonia, odottaa vastauksesi jsonia
- nappi pitää disabloida kutsun jälkeen n-sekunniksi tai kun ok vastaus tulee ettei käyttäjä voi klikata montaa kertaa
- jos tulee odotettu virhe jonka olen ottanut huomioon, se pitää tunnistaa ja poimia jsonin error-muuttujasta ja näyttää, ehkä myös logittaa tms.
- jos tulee odottamaton virhe, ei palvelin palautakkaan jsonia ja tästä tulee halt errori javascriptin suoritukseen. Tämä pitää ratkaista joka kerta try catch:illä ja se on jo montakymmmentä riviä koodia
- jos palvelimeen ei saada yhteyttä, tämäkin pitää ottaa huomioon

--> 200 riviä koodia joka ajax postauksessa

Millä tämä ratkaistaan? Haluan että koodi on yhä varmatoimista kuin ennemminkin, eli kuin venäläisen junan vessa. Mutta helpompi ja lyhyempi kirjoittaa.

walkout_ [23.05.2019 07:48:34]

#

juhauta kirjoitti:

- nappi pitää disabloida kutsun jälkeen n-sekunniksi tai kun ok vastaus tulee ettei käyttäjä voi klikata montaa kertaa

Niin tai sitten teet z-index:9999 CSS märityksellä läpinäkyvän load-overlay div-hommelin joka on siis korkeudeltaan 100 % ja leveydeltään 100 % jossa on loading.gif-animaatio keskellä ruutua. Ja tämä siis aina jokaiseen AJAX-kyselyyn. Nittäin jos koko ruudun päällä on asetuksella z-index:9999 div-tagi niin sen alla olevia elementtäjä, yms. ei voi klikata. Ja tietysti koko hoidossa voisi olla asian mukainen teksti loading.gif-animaation alla että "Odota! Dataa tallennetaan!"

Grez [23.05.2019 08:44:17]

#

En näe mitän ongelmaa siinä että joka ajax-postauksessa suoritetaan 200 riviä koodia.

Ja jos järkevästi kirjoitat koodin, niin voit käyttää samaa koodia joka paikassa, joten siltäkään kannalta ei ole mitään suurempaa ongelmaa.

Sinänsä kuvittelisin että itse harvemmin ihan 200 riviä koodia olen tarvinnut noiden kuvailemiesi juttujen toteuttamiseen.

Itse en ole yleensä disabloinut nappia n sekunniksi, vaan siihen asti kunnes pyyntö on käsitelty (eli tulee joko ok tai jokin virhe). Tosin jos tilanne on sellainen että nappia ei saa painaa montaa kertaa, saattaa se jäädä disabloiduksi OK:n jälkeen pidemmäksikin aikaa tai ei palaudu ollenkaan.

Walkoutin viestin tapaan myöskin jonkinlainen latausindikaattori voi olla järkevä, en tosin sitä ihan noin tekisi. Ja mikä toi 9999 taikanumero oikein on? Itse sanoisin, että laittaa isomman z-indeksin kun sivun muilla elementeillä.

Lebe80 [23.05.2019 10:21:58]

#

Juuri se mitä Grez sanoi. Eli teet omat metodit ajax-kutsuille (joissa on sitten tämä 200 riviä koodia), vaikka niissä metodeissa käytetäänkin jQueryä.

Tietysti, tietynlaisilla kirjastoilla tuon voi kiertää niin, että joku muu on kirjoittanut nuo 200 riviä koodia, ja kutsut vain ajax-latauksissa näitä valmiiden kirjastojen metodeja.

Esim. vaikka Fetch tai vaikka Axios.
https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch

vesikuusi [23.05.2019 14:02:13]

#

Ymmärrän, että et ehkä halua välittömästi siirtyä käyttämään reaktiivisia JS-kirjastoja, mutta voin kyllä suositella esim. Vue.js. Tällaisella kehyksellä GUIn ohjelmointi muistuttaa enemmän GUIn ohjelmointia kuin HTML-dokumentin käpistelyä skripteillä.

Toki siinä on jyrkkä oppimiskäyrä alussa, mutta kun on päässyt vauhtiin ja luonut/ladannut netistä taustalle toteutukset olioiden lähettämiseen ja lukemiseen XHR-kyselyillä (ts. AJAX-kyselyillä) niin käyttöliittymien koodaaminen on paljon vähemmän piinallista kuin HTML+jQuery-setillä. Yksinkertaistettu esimerkki alla.

<form @submit="laheta">
...
<button :disabled="kyselyKaynnissa">Lähetä</button>
...
</form>
<script>
{
    // ...
    laheta() {
        kyselyKaynnissa = true;
        this.sendXhr(this.item).then(() => { kyselyKaynnissa = false })
    }
    // ...
}
</script>

Tuohon "moduuliin" sitten voi importoida muualta muun muassa sendXhr toteutuksen.

Oma kokemukseni on, että tällainen reaktiivinen frameworkki sopii web-applikaatioiden kehittämiseen ja tekee siitä paljon miellyttävämpää verrattuna pelkkään jQueryyn. Se ei kuitenkaan mielestäni sovi web-sivujen kehittämiseen. Web-applikaation ja web-sivun ero on kai sitten enemmän subjektiivinen asia.

Lebe80 [23.05.2019 15:52:57]

#

vesikuusi kirjoitti:

(23.05.2019 14:02:13): Ymmärrän, että et ehkä halua välit­tö­mäs­ti...

Jep, Vue.js / React:lle (React + Redux) täältäkin äänet, mikäli web-sovelluksia haluaa kehittää.

The Alchemist [23.05.2019 20:03:47]

#

No tässä alkeellinen esimerkki pelkällä jQueryllä tehdystä lomakehärpäkkeestä, joka on kohtalaisen muokattavakin. Itse en välttämättä koskisi mihinkään isompaan js-freimikseen, ellen aikoisi rakentaa uutta saittia tyhjästä sellaisen päälle.

Eikä niillä frameworkeilla tee yhtään mitään, jos ei osaa koodata. Korkeintaan niiden ihmettely voi auttaa oppimaan vähän nopeammin, koska silloin alkaa ensishokin jälkeen nähdä, miten asiat pitää tehdä ja miksi itse on mennyt metsään. Tosin osa frameworkeistakin tekee mielestäni asiat aivan liian kompleksilla tavalla, jotta niistä olisi missään gigatavun kokoista codebasea pienemmässä palvelussa iloa.

Filut:
- form.html
- forms.js
- submit.php

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8"/>
  <style>
    form[data-busy] {
      opacity: .5;
    }
    .form-group {
      border: 3px solid #999;
      padding: 1rem;
      margin-bottom: 1rem;
    }
    label {
      display: block;
    }
    .errors {
      color: red;
      font-size: small;
    }
    .errors *:first-child {
      display: none;
    }
    .hidden {
      display: none;
    }
    #main-success {
      border: 3px solid green;
      color: green;
      padding: 1rem;
      margin-bottom: 1rem;
    }
    #main-errors {
      border: 3px solid red;
      color: red;
      padding: 1rem;
      margin-bottom: 1rem;
    }
  </style>
</head>
<body>
  <form method="post" action="submit.php" data-form>
    <div class="hidden" id="main-errors" data-error-outlet></div>
    <div class="hidden" id="main-success" data-success-outlet></div>

    <div class="form-group">
      <label for="f-username">Username</label>
      <input type="text" name="username" id="f-username" value="miska" required/>
    </div>
    <div class="form-group">
      <label for="f-email">Email</label>
      <input type="text" name="email" id="f-email" value="miska@netti.tv" required/>
    </div>
    <div class="form-group">
      <label for="f-email">Write something</label>
      <textarea name="stuff" id="f-stuff">Lorem ipsum</textarea>
    </div>
    <div class="form-group">
      <label for="f-checkbox">Check this box</label>
      <input type="checkbox" id="f-checkbox" name="checkbox"/>
    </div>
    <div>
      <button>Save</button>
    </div>
  </form>

  <script
  			  src="https://code.jquery.com/jquery-3.4.1.min.js"
  			  integrity="sha256-CSXorXvZcTkaix6Yvo6HppcZGetbYMGWSFlBw8HfCJo="
  			  crossorigin="anonymous"></script>
  <script src="/forms.js"></script>
</body>
</html>
'use strict'

const ERROR_TEMPLATE = `
  <ul class="errors" data-errors>
    <li data-error-item></li>
  </ul>
`;

function lockForm (form) {
  $(form).attr('data-busy', '').find(':submit').prop('disabled', true)
}

function unlockForm (form) {
  return $(form).removeAttr('data-busy').find(':submit').prop('disabled', false)
}

function clearErrors (form) {
  $(form).find('[data-error-outlet]').addClass('hidden');
  $(form).find('[data-errors]').remove();
}

function formSubmitHandler (event) {
  event.preventDefault()

  let form = event.target
  let data = $(form).serialize()

  let onSuccess = formSuccessHandler.bind(null, form)
  let onError = formErrorHandler.bind(null, form)

  lockForm(form)
  $.post(form.action, data).then(onSuccess, onError)
}

function formSuccessHandler (form, response) {
  unlockForm(form)
  clearErrors(form)

  $(form).find('[data-success-outlet]').removeClass('hidden').text('Data was saved!')
}

function formErrorHandler (form, response) {
  unlockForm(form)
  clearErrors(form)

  $(form).find('[data-success-outlet]').addClass('hidden')
  $(form).find('[data-error-outlet]').removeClass('hidden')

  switch (response.status) {
    case 0:
      return formNetworkErrorHandler(form, response)

    case 400:
      return formValidationErrorHandler(form, response)

    case 500:
      return formServerErrorHandler(form, response)
  }
}

function formNetworkErrorHandler(form, response) {
  $(form).find('[data-error-outlet]').text('Network error, check connection')
}

function formServerErrorHandler (form, response) {
  $(form).find('[data-error-outlet]').text('Server error (500)')
}

function formValidationErrorHandler (form, response) {
  $(form).find('[data-error-outlet]').text('Form validation failed')

  const allErrors = response.responseJSON

  for (let [field, errors] of Object.entries(allErrors)) {
    const errorElement = $(ERROR_TEMPLATE)
    errors.forEach(message => appendElementError(errorElement, message))
    $(form).find(`[name="${field}"]`).after(errorElement)
  }
}

function appendElementError (errorElement, message) {
  errorElement.find('[data-error-item]').first().clone().text(message).appendTo(errorElement)
}

$('[data-form]').on('submit', formSubmitHandler)
<?php

function validate_not_empty($value) {
    if (empty($value)) {
        throw new Exception('This field is required');
    }
}

function validate_username($value) {
    switch ($value) {
        case 'miska':
        case 'ankka':
            throw new Exception('Username is reserved.');
    }
}

function validate_email($value) {
    if ($value == 'miska@netti.tv') {
        throw new Exception('Email address is reserved.');
    }
}

function validate_stuff($value) {
    $blacklist = ['lorem', 'kissa'];

    foreach ($blacklist as $word) {
        if (stripos($value, $word) !== false) {
            throw new Exception("Forbidden word: {$word}");
        }
    }

}

if ($_SERVER['REQUEST_METHOD'] ?? '' == 'POST') {
    sleep(2);

    if (rand(0, 3) === 0) {
        http_response_code(500);
        exit('Server error');
    }

    $rules = [
        ['username', 'validate_not_empty'],
        ['username', 'validate_username'],
        ['email', 'validate_not_empty'],
        ['email', 'validate_email'],
        ['stuff', 'validate_stuff'],
        ['checkbox', 'validate_not_empty'],
    ];

    $errors = [];

    foreach ($rules as list($field, $validate)) {
        try {
            $validate($_POST[$field] ?? null);
        } catch (Exception $e) {
            $errors[$field][] = $e->getMessage();
        }
    }

    if ($errors) {
        http_response_code(400);
        header('Content-Type: application/json');
        print json_encode($errors);
    } else {
        header('Content-Type: application/json');
        print json_encode($_POST);
    }
}

Tronic [01.07.2019 15:18:05]

#

Voit käyttää FormData-objektia tietojen lukemiseen ja lähettämiseen uudempaa fetch-rajapintaa, joka tukee async/awaitia:

<!DOCTYPE html><title>Form test</title>
<form><input name=foo><input name=bar type=file><input type=submit></form>
<script>
'use strict'
const submit = async form => {
    const data = new FormData(form)
    // Do something with the data (if needed)
    for (const [name, value] of data) console.log('Form field:', name, value)
    // POST multipart/form-data
    const res = await fetch(form.action, {method: 'POST', body: data})
    console.log('Response status', res.status)
    // Read the response as json (as opposed to res.text() etc)
    const msg = await res.json()
    console.log('Response body', msg)
}
// Register a non-async wrapper (for preventDefault) on all forms
function _submit(ev) { ev.preventDefault(); submit(this) }
for (const form of document.querySelectorAll('form'))
    form.addEventListener('submit', _submit)
</script>

Voinet helposti lisätä tarvitsemasi muut toiminnot, esim. virheenkäsittely, jos res.status > 400. Backend yleensä vastaa POST-requestiin redirectillä, jota fetch automaattisesti seuraa (et siis saa 302-koodia vastauksena, vaan sivun, jolle ohjattiin).

Esimerkissä ei ole formille määritelty enctype=multipart/form-data, koska tämä fetchillä toteutettu palautus tekee niin kuitenkin. Ilman javascriptiä oleville käyttäjille se on syytä määritellä.


Sivun alkuun

Vastaus

Aihe on jo aika vanha, joten et voi enää vastata siihen.

Tietoa sivustosta