Il concetto di "Promesse" ha cambiato il modo in cui scriviamo JavaScript asincrono. Nell'ultimo anno, molti framework hanno incorporato alcune forme del pattern Promise per rendere più semplice scrivere, leggere e mantenere il codice asincrono. Ad esempio, jQuery ha aggiunto $ .Deferred () e NodeJS ha i moduli Q e jspromise che funzionano sia su client che su server. Anche i framework MVC lato client, come EmberJS e AngularJS, implementano le proprie versioni di Promises.
Ma non deve fermarsi qui: possiamo ripensare a soluzioni vecchie e applicare Promises a loro. In questo articolo, faremo proprio questo: convalidare un modulo usando il modello Promise per esporre un'API super semplice.
Le promesse notificano il risultato di un'operazione.
In poche parole, Promises notifica il risultato di un'operazione. Il risultato può essere un successo o un fallimento, e l'operazione, di per sé, può essere qualsiasi cosa che rispetti un semplice contratto. Ho scelto di usare la parola contrarre perché puoi progettare questo contratto in molti modi diversi. Per fortuna, la comunità di sviluppo ha raggiunto un consenso e ha creato una specifica chiamata Promises / A+.
Solo l'operazione sa veramente quando è stata completata; in quanto tale, è responsabile per la notifica del risultato utilizzando il contratto Promises / A +. In altre parole, promesse per dirti il risultato finale al completamento.
L'operazione restituisce a promettere
oggetto, e puoi allegare i tuoi callback ad esso usando il fatto()
o fallire()
metodi. L'operazione può notificare il suo risultato chiamando promise.resolve ()
o promise.reject ()
, rispettivamente. Questo è illustrato nella figura seguente:
Lascia che dipinga uno scenario plausibile.
Possiamo ripensare a soluzioni vecchie e applicare promesse a loro.
La convalida del modulo sul lato client inizia sempre con le più semplici intenzioni. Potresti avere un modulo di iscrizione con Nome e E-mail campi, e devi assicurarti che l'utente fornisca input validi per entrambi i campi. Sembra abbastanza semplice e inizi a implementare la tua soluzione.
Viene quindi comunicato che gli indirizzi e-mail devono essere univoci e si decide di convalidare l'indirizzo e-mail sul server. Quindi, l'utente fa clic sul pulsante di invio, il server controlla l'univocità dell'email e la pagina si aggiorna per visualizzare eventuali errori. Sembra l'approccio giusto, giusto? No. Il tuo cliente desidera un'esperienza utente chiara; i visitatori dovrebbero vedere eventuali messaggi di errore senza aggiornare la pagina.
Il tuo modulo ha il Nome campo che non richiede alcun supporto sul lato server, ma poi si ha il E-mail campo che richiede di effettuare una richiesta al server. Significa richieste del server $ .Ajax ()
chiamate, quindi dovrai eseguire la convalida dell'e-mail nella funzione di callback. Se il tuo modulo ha più campi che richiedono il supporto lato server, il tuo codice sarà un casino annidato $ .Ajax ()
chiama in callback Richiamate all'interno di callback: "Benvenuto all'inferno del callback! Speriamo che tu abbia un soggiorno miserabile!".
Quindi, come gestiamo l'inferno del callback?
Fai un passo indietro e pensa a questo problema. Abbiamo un insieme di operazioni che possono avere successo o fallire. Uno di questi risultati può essere catturato come un Promettere
, e le operazioni possono essere qualsiasi cosa, da semplici controlli sul lato client a convalide complesse sul lato server. Le promesse offrono anche il vantaggio aggiuntivo di coerenza, oltre a consentire di evitare il controllo condizionale del tipo di convalida. Vediamo come possiamo fare questo.
Come ho notato in precedenza, ci sono diverse implementazioni di promesse in the wild, ma mi concentrerò sull'implementazione di $ .Deferred () Promise di jQuery.
Costruiremo una semplice struttura di validazione in cui ogni controllo restituisce immediatamente un risultato o una Promessa. Come utente di questo framework, devi solo ricordare una cosa: "restituisce sempre una promessa". Iniziamo.
Penso che sia più facile apprezzare la semplicità di Promises dal punto di vista del consumatore. Diciamo che ho un modulo con tre campi: Nome, Email e Indirizzo:
Per prima cosa configurerò i criteri di convalida con il seguente oggetto. Questo serve anche come API del nostro framework:
var validationConfig = '.name': checks: 'required', campo: 'Name', '.email': checks: ['required'], campo: 'Email', '.address': controlli: ['random', 'required'], campo: 'Address';
Le chiavi di questo oggetto di configurazione sono selettori jQuery; i loro valori sono oggetti con le seguenti due proprietà:
controlli
: una stringa o una matrice di convalide.campo
: il nome del campo leggibile dall'uomo, che verrà utilizzato per segnalare errori per quel campoPossiamo chiamare il nostro validatore, esposto come variabile globale V
, come questo:
V.validate (validationConfig) .done (function () // Success) .fail (function (errors) // Validations failed. Errori ha i dettagli);
Notare l'uso del fatto()
e fallire()
callback; questi sono i callback predefiniti per la consegna del risultato di una Promessa. Se dovessimo aggiungere più campi modulo, potremmo semplicemente aumentare il validationConfig
oggetto senza disturbare il resto del setup (il principio Open-Closed in azione). In effetti, possiamo aggiungere altre convalide, come il vincolo di unicità per gli indirizzi e-mail, estendendo il framework del validatore (che vedremo in seguito).
Quindi questa è l'API rivolta al consumatore per il framework del validatore. Ora, tuffiamoci e vediamo come funziona sotto il cofano.
Il validatore è esposto come un oggetto con due proprietà:
genere
: contiene i diversi tipi di convalida e serve anche come punto di estensione per aggiungere altro.convalidare
: il metodo principale che esegue le convalide in base all'oggetto di configurazione fornito.La struttura generale può essere riassunta come:
var V = (function ($) var validator = / * * Punto di estensione - basta aggiungere a questo hash * * V.type ['my-validator'] = * ok: function (value) return true; , * messaggio: 'Messaggio di errore per il mio validatore' * * / tipo: 'richiesto': ok: funzione (valore) // è valido?, messaggio: 'Questo campo è obbligatorio', ... , / ** * * @param config * * '': stringa | oggetto | [stringa] * * / validate: function (config) // 1. Normalizza l'oggetto di configurazione // 2. Converti ogni convalida in una promessa // 3. Avvia la master promise // 4. Restituisce la promessa principale ; ) (JQuery);
Il convalidare
il metodo fornisce le basi di questo quadro. Come visto nei commenti sopra, ci sono quattro passaggi che avvengono qui:
1. Normalizza l'oggetto di configurazione.
È qui che passiamo attraverso il nostro oggetto config e lo convertiamo in una rappresentazione interna. Questo è principalmente per acquisire tutte le informazioni di cui abbiamo bisogno per effettuare la convalida e segnalare gli errori se necessario:
function normalizeConfig (config) config = config || ; var validations = []; $ .each (config, function (selector, obj) // crea un array per il controllo semplificato var checks = $ .isArray (obj.checks)? obj.checks: [obj.checks]; $ .each (checks, function (idx, check) validations.push (control: $ (selector), check: getValidator (check), checkName: check, field: obj.field););); restituire convalide; function getValidator (type) if ($ .type (type) === 'string' && validator.type [type]) return validator.type [type]; return validator.noCheck;
Questo codice scorre sulle chiavi nell'oggetto config e crea una rappresentazione interna della convalida. Useremo questa rappresentazione nel convalidare
metodo.
Il getValidator ()
helper recupera l'oggetto validatore dal genere
hash. Se non ne troviamo uno, restituiamo il nOCHECK
validatore che restituisce sempre true.
2. Converti ogni convalida in una promessa.
Qui, ci assicuriamo che ogni convalida sia una promessa controllando il valore di ritorno di validation.ok ()
. Se contiene il poi()
metodo, sappiamo che è una promessa (questo è secondo le specifiche Promises / A +). In caso contrario, creiamo una promessa ad-hoc che risolve o rifiuta a seconda del valore restituito.
validate: function (config) // 1. Normalizza l'oggetto di configurazione config = normalizeConfig (config); var promises = [], checks = []; // 2. Converti ogni convalida in una promessa $ .each (config, function (idx, v) var value = v.control.val (); var retVal = v.check.ok (value); // Crea un promessa, il controllo è basato su Promises / A + spec if (retVal.then) promises.push (retVal); else var p = $ .Deferred (); if (retVal) p.resolve (); else p.reject (); promises.push (p.promise ()); checks.push (v);); // 3. Wrap into a master promise // 4. Restituisce la promessa principale
3. Avvia una master Promise.
Abbiamo creato una serie di promesse nel passaggio precedente. Quando tutti hanno successo, vogliamo risolvere una volta o fallire con informazioni dettagliate sull'errore. Possiamo farlo avvolgendo tutte le Promesse in un'unica Promessa e propagando il risultato. Se tutto va bene, risolviamo la promessa principale.
Per gli errori, possiamo leggere dalla nostra rappresentazione di convalida interna e usarlo per la segnalazione. Dal momento che possono esserci più fallimenti di convalida, eseguiamo il loop su promesse
array e leggere il stato()
risultato. Raccogliamo tutte le promesse respinte nel mancato
array e chiamata rifiutare()
sulla promessa principale:
// 3. Wrap in a master promise var masterPromise = $ .Deferred (); $ .when.apply (null, promises) .done (function () masterPromise.resolve ();) .fail (function () var failed = []; $ .each (promises, function (idx, x) if (x.state () === 'reject') var failedCheck = checks [idx]; var error = check: failedCheck.checkName, errore: failedCheck.check.message, campo: failedCheck.field, controllo: failedCheck.control; failed.push (errore);); masterPromise.reject (non riuscito);); // 4. Restituisce il master promise return masterPromise.promise ();
4. Restituire la promessa principale.
Alla fine restituiamo la promessa principale dal convalidare()
metodo. Questa è la Promessa su cui il codice client imposta il fatto()
e fallire()
callback.
I passaggi due e tre sono il punto cruciale di questa struttura. Normalizzando le convalide in una Promessa, possiamo gestirle in modo coerente. Abbiamo un maggiore controllo con un oggetto master Promise e possiamo allegare informazioni contestuali aggiuntive che potrebbero essere utili all'utente finale.
Vedi il file demo per un uso completo del framework validator. Noi usiamo il fatto()
callback per segnalare il successo e fallire()
per mostrare un elenco di errori rispetto a ciascuno dei campi. Le schermate seguenti mostrano gli stati di successo e di errore:
La demo utilizza la stessa configurazione di convalida e HTML menzionata in precedenza in questo articolo. L'unica aggiunta è il codice che visualizza gli avvisi. Notare l'uso del fatto()
e fallire()
callback per gestire i risultati della convalida.
function showAlerts (errors) var alertContainer = $ ('. alert'); $ ( 'Error') rimuovere ().; if (! errors) alertContainer.html ('Tutto passato'); else $ .each (errors, function (idx, err) var msg = $ ('') .addClass (' error ') .text (err.error); . Err.control.parent () accodare (msg); ); $ ('. validate'). click (function () $ ('. indicator'). show (); $ ('. alert'). empty (); V.validate (validationConfig) .done (function () $ ('. indicator'). hide (); showAlerts ();) .fail (function (errors) $ ('. indicator'). hide (); showAlerts (errors);); );
Ho detto prima che possiamo aggiungere più operazioni di convalida al framework estendendo il validatore genere
hash. Considera il casuale
validatore come esempio. Questo validatore ha successo o fallito casualmente. So che non è un validatore utile, ma vale la pena notare alcuni dei suoi concetti:
setTimeout ()
per rendere la convalida asincrona. Puoi anche pensare a questo come a simulare la latenza della rete.ok()
metodo.// Estendi con un validatore casuale V.type ['random'] = ok: function (value) var deferred = $ .Deferred (); setTimeout (function () var result = Math.random () < 0.5; if (result) deferred.resolve(); else deferred.reject(); , 1000); return deferred.promise(); , message: 'Failed randomly. No hard feelings.' ;
Nella demo, ho usato questa convalida su Indirizzo campo così:
var validationConfig = / * cilesso per brevità * / '.address': checks: ['random', 'required'], campo: 'Address';
Spero che questo articolo ti abbia dato una buona idea di come puoi applicare Promises ai vecchi problemi e costruire il tuo framework intorno a loro. L'approccio basato su Promessa è una soluzione fantastica per operazioni astratte che possono essere eseguite o meno in modo sincrono. Puoi anche concatenare i callback e persino comporre le Promesse di ordine superiore da una serie di altre Promesse.
Il modello Promise è applicabile in una varietà di scenari e, si spera, incontrarne alcuni e vedere una corrispondenza immediata!