AJAX & Snippets
Στην εποχή των σύγχρονων διαδικτυακών εφαρμογών, όπου η λειτουργικότητα συχνά κατανέμεται μεταξύ του διακομιστή και του προγράμματος περιήγησης, το AJAX είναι ένα απαραίτητο συνδετικό στοιχείο. Ποιες επιλογές μας προσφέρει το Nette Framework σε αυτόν τον τομέα;
- αποστολή τμημάτων του template, τα λεγόμενα snippets
- μεταβίβαση μεταβλητών μεταξύ PHP και JavaScript
- εργαλεία για την αποσφαλμάτωση αιτήσεων AJAX
Αίτηση AJAX
Μια αίτηση AJAX δεν διαφέρει ουσιαστικά από μια κλασική αίτηση HTTP. Καλείται ένας presenter με συγκεκριμένες παραμέτρους. Και εξαρτάται από τον presenter πώς θα ανταποκριθεί στην αίτηση – μπορεί να επιστρέψει δεδομένα σε μορφή JSON, να στείλει ένα τμήμα κώδικα HTML, ένα έγγραφο XML κ.λπ.
Στην πλευρά του προγράμματος περιήγησης, αρχικοποιούμε την αίτηση AJAX
χρησιμοποιώντας τη συνάρτηση fetch()
:
fetch(url, {
headers: {'X-Requested-With': 'XMLHttpRequest'},
})
.then(response => response.json())
.then(payload => {
// επεξεργασία της απάντησης
});
Στην πλευρά του διακομιστή, αναγνωρίζουμε μια αίτηση AJAX
χρησιμοποιώντας τη μέθοδο $httpRequest->isAjax()
της υπηρεσίας που ενσωματώνει την αίτηση HTTP. Χρησιμοποιεί
την κεφαλίδα HTTP X-Requested-With
για την ανίχνευση, γι' αυτό είναι
σημαντικό να την στέλνετε. Μέσα στον presenter, μπορείτε να χρησιμοποιήσετε
τη μέθοδο $this->isAjax()
.
Αν θέλετε να στείλετε δεδομένα σε μορφή JSON, χρησιμοποιήστε τη μέθοδο
sendJson()
. Η μέθοδος
τερματίζει επίσης τη δραστηριότητα του presenter.
public function actionExport(): void
{
$this->sendJson($this->model->getData);
}
Αν σκοπεύετε να απαντήσετε με ένα ειδικό template σχεδιασμένο για AJAX, μπορείτε να το κάνετε ως εξής:
public function handleClick($param): void
{
if ($this->isAjax()) {
$this->template->setFile('path/to/ajax.latte');
}
// ...
}
Snippets
Το πιο ισχυρό εργαλείο που προσφέρει το Nette για τη σύνδεση του διακομιστή με τον client είναι τα snippets. Χάρη σε αυτά, μπορείτε να μετατρέψετε μια συνηθισμένη εφαρμογή σε μια εφαρμογή AJAX με ελάχιστη προσπάθεια και λίγες γραμμές κώδικα. Το παράδειγμα Fifteen, του οποίου ο κώδικας βρίσκεται στο GitHub, δείχνει πώς λειτουργεί όλο αυτό.
Τα snippets, ή αποσπάσματα, επιτρέπουν την ενημέρωση μόνο τμημάτων της σελίδας, αντί για την επαναφόρτωση ολόκληρης της σελίδας. Αυτό δεν είναι μόνο ταχύτερο και πιο αποτελεσματικό, αλλά παρέχει επίσης μια πιο άνετη εμπειρία χρήστη. Τα snippets μπορεί να σας θυμίζουν το Hotwire για Ruby on Rails ή το Symfony UX Turbo. Είναι ενδιαφέρον ότι το Nette εισήγαγε τα snippets 14 χρόνια νωρίτερα.
Πώς λειτουργούν τα snippets; Κατά την πρώτη φόρτωση της σελίδας (αίτηση μη-AJAX), φορτώνεται ολόκληρη η σελίδα, συμπεριλαμβανομένων όλων των snippets. Όταν ο χρήστης αλληλεπιδρά με τη σελίδα (π.χ. κάνει κλικ σε ένα κουμπί, υποβάλλει μια φόρμα κ.λπ.), αντί να φορτωθεί ολόκληρη η σελίδα, γίνεται μια αίτηση AJAX. Ο κώδικας στον presenter εκτελεί την ενέργεια και αποφασίζει ποια snippets πρέπει να ενημερωθούν. Το Nette αποδίδει αυτά τα snippets και τα στέλνει με τη μορφή ενός πίνακα σε μορφή JSON. Ο κώδικας χειρισμού στο πρόγραμμα περιήγησης εισάγει τα ληφθέντα snippets πίσω στη σελίδα. Έτσι, μεταδίδεται μόνο ο κώδικας των αλλαγμένων snippets, εξοικονομώντας εύρος ζώνης και επιταχύνοντας τη φόρτωση σε σύγκριση με τη μετάδοση του περιεχομένου ολόκληρης της σελίδας.
Naja
Για τον χειρισμό των snippets στην πλευρά του προγράμματος περιήγησης, χρησιμοποιείται η βιβλιοθήκη Naja. Εγκαταστήστε την ως πακέτο node.js (για χρήση με εφαρμογές Webpack, Rollup, Vite, Parcel και άλλες):
npm install naja
…ή εισάγετέ την απευθείας στο template της σελίδας:
<script src="https://unpkg.com/naja@2/dist/Naja.min.js"></script>
Πρώτα, πρέπει να αρχικοποιήσετε τη βιβλιοθήκη:
naja.initialize();
Για να μετατρέψετε έναν συνηθισμένο σύνδεσμο (signal) ή την υποβολή μιας
φόρμας σε αίτηση AJAX, απλά επισημάνετε τον σχετικό σύνδεσμο, φόρμα ή
κουμπί με την κλάση ajax
:
<a n:href="go!" class="ajax">Go</a>
<form n:name="form" class="ajax">
<input n:name="submit">
</form>
ή
<form n:name="form">
<input n:name="submit" class="ajax">
</form>
Επανασχεδίαση Snippets
Κάθε αντικείμενο της κλάσης Control
(συμπεριλαμβανομένου του ίδιου του Presenter) παρακολουθεί εάν έχουν γίνει
αλλαγές που απαιτούν την επανασχεδίασή του. Η μέθοδος redrawControl()
χρησιμοποιείται για αυτό:
public function handleLogin(string $user): void
{
// μετά τη σύνδεση, το σχετικό τμήμα πρέπει να επανασχεδιαστεί
$this->redrawControl();
// ...
}
Το Nette επιτρέπει ακόμη πιο λεπτομερή έλεγχο του τι πρέπει να επανασχεδιαστεί. Η αναφερόμενη μέθοδος μπορεί να δεχτεί το όνομα του snippet ως όρισμα. Έτσι, μπορείτε να ακυρώσετε (δηλαδή: να επιβάλετε την επανασχεδίαση) σε επίπεδο τμημάτων του template. Εάν ακυρωθεί ολόκληρο το component, κάθε snippet του θα επανασχεδιαστεί επίσης:
// ακυρώνει το snippet 'header'
$this->redrawControl('header');
Snippets στο Latte
Η χρήση snippets στο Latte είναι εξαιρετικά εύκολη. Για να ορίσετε ένα τμήμα
του template ως snippet, απλά περικλείστε το με τις ετικέτες {snippet}
και
{/snippet}
:
{snippet header}
<h1>Hello ... </h1>
{/snippet}
Το snippet δημιουργεί ένα στοιχείο <div>
στη σελίδα HTML με ένα
ειδικό, παραγόμενο id
. Κατά την επανασχεδίαση του snippet, το
περιεχόμενο αυτού του στοιχείου ενημερώνεται. Επομένως, είναι
απαραίτητο κατά την αρχική απόδοση της σελίδας να αποδοθούν επίσης όλα
τα snippets, ακόμα κι αν μπορεί να είναι αρχικά κενά.
Μπορείτε επίσης να δημιουργήσετε ένα snippet με ένα στοιχείο
διαφορετικό από το <div>
χρησιμοποιώντας ένα n:attribute:
<article n:snippet="header" class="foo bar">
<h1>Hello ... </h1>
</article>
Περιοχές Snippet
Τα ονόματα των snippets μπορούν επίσης να είναι εκφράσεις:
{foreach $items as $id => $item}
<li n:snippet="item-{$id}">{$item}</li>
{/foreach}
Αυτό δημιουργεί πολλά snippets item-0
, item-1
, κ.λπ. Αν ακυρώναμε
απευθείας ένα δυναμικό snippet (για παράδειγμα item-1
), τίποτα δεν θα
επανασχεδιαζόταν. Ο λόγος είναι ότι τα snippets λειτουργούν πραγματικά ως
αποσπάσματα και αποδίδονται μόνο αυτά τα ίδια. Ωστόσο, στο template, δεν
υπάρχει στην πραγματικότητα κανένα snippet με το όνομα item-1
. Αυτό
δημιουργείται μόνο κατά την εκτέλεση του κώδικα γύρω από το snippet,
δηλαδή του βρόχου foreach. Επομένως, επισημαίνουμε το τμήμα του template που
πρέπει να εκτελεστεί χρησιμοποιώντας την ετικέτα {snippetArea}
:
<ul n:snippetArea="itemsContainer">
{foreach $items as $id => $item}
<li n:snippet="item-{$id}">{$item}</li>
{/foreach}
</ul>
Και ζητάμε την επανασχεδίαση τόσο του ίδιου του snippet όσο και ολόκληρης της γονικής περιοχής:
$this->redrawControl('itemsContainer');
$this->redrawControl('item-1');
Ταυτόχρονα, είναι καλό να διασφαλίσουμε ότι ο πίνακας $items
περιέχει μόνο τα στοιχεία που πρέπει να επανασχεδιαστούν.
Αν εισάγουμε ένα άλλο template που περιέχει snippets στο template
χρησιμοποιώντας την ετικέτα {include}
, είναι απαραίτητο να
συμπεριλάβουμε ξανά την εισαγωγή του template σε ένα snippetArea
και να
το ακυρώσουμε μαζί με το snippet:
{snippetArea include}
{include 'included.latte'}
{/snippetArea}
{* included.latte *}
{snippet item}
...
{/snippet}
$this->redrawControl('include');
$this->redrawControl('item');
Snippets σε Components
Μπορείτε επίσης να δημιουργήσετε snippets σε components και το Nette θα τα επανασχεδιάζει
αυτόματα. Ωστόσο, υπάρχει ένας περιορισμός: για την επανασχεδίαση των
snippets, καλεί τη μέθοδο render()
χωρίς παραμέτρους. Επομένως, η
μεταβίβαση παραμέτρων στο template δεν θα λειτουργήσει:
OK
{control productGrid}
δεν θα λειτουργήσει:
{control productGrid $arg, $arg}
{control productGrid:paginator}
Αποστολή Δεδομένων Χρήστη
Μαζί με τα snippets, μπορείτε να στείλετε οποιαδήποτε άλλα δεδομένα στον
client. Απλά γράψτε τα στο αντικείμενο payload
:
public function actionDelete(int $id): void
{
// ...
if ($this->isAjax()) {
$this->payload->message = 'Success';
}
}
Μεταβίβαση Παραμέτρων
Αν στέλνουμε παραμέτρους σε ένα component μέσω μιας αίτησης AJAX, είτε
πρόκειται για παραμέτρους signal είτε για persistent παραμέτρους, πρέπει να
καθορίσουμε το καθολικό τους όνομα στην αίτηση, το οποίο περιλαμβάνει
και το όνομα του component. Η μέθοδος getParameterId()
επιστρέφει το πλήρες
όνομα της παραμέτρου.
let url = new URL({link //foo!});
url.searchParams.set({$control->getParameterId('bar')}, bar);
fetch(url, {
headers: {'X-Requested-With': 'XMLHttpRequest'},
})
Και η μέθοδος handle με τις αντίστοιχες παραμέτρους στο component:
public function handleFoo(int $bar): void
{
}