Come costruire una barra di avanzamento per il web con Django e Sedano

La sorprendente complessità di creare qualcosa che è, alla sua superficie, ridicolmente semplice

Foto di Patrick Fore su Unsplash

Le barre di avanzamento sono uno dei componenti dell'interfaccia utente più comuni e familiari nella nostra vita. Li vediamo ogni volta che scarichiamo un file, installiamo software o alleghiamo qualcosa a un'e-mail. Vivono nei nostri browser, sui nostri telefoni e persino sui nostri televisori.

Eppure - fare una buona barra di avanzamento è un compito sorprendentemente complesso!

In questo post descriverò tutti i componenti per creare una barra di avanzamento di qualità per il Web e, si spera, alla fine avrai una buona comprensione di tutto ciò che ti serve per costruirne uno tuo.

Questo post descrive tutto ciò che ho dovuto imparare (e alcune cose che non ho fatto!) Per fare progressi sul sedano, una libreria che si spera semplifichi il rilascio di barre di avanzamento senza dipendenza nelle tue applicazioni Django / Celery.

Detto questo, la maggior parte dei concetti in questo post dovrebbe tradursi in tutte le lingue / ambienti, quindi anche se non usi Python probabilmente puoi imparare qualcosa di nuovo.

Perché le barre di avanzamento?

Questo potrebbe essere ovvio, ma solo per toglierlo di mezzo: perché utilizziamo le barre di avanzamento?

Il motivo di base è fornire feedback agli utenti per qualcosa che richiede più tempo di quello che sono abituati ad aspettare. Secondo kissmetrics, il 40% delle persone abbandona un sito Web che impiega più di 3 secondi per caricarsi! E mentre puoi usare qualcosa come uno spinner per aiutare a mitigare questa attesa, un modo provato e vero per comunicare ai tuoi utenti mentre aspettano che accada qualcosa è utilizzare una barra di avanzamento.

In genere, le barre di avanzamento sono ottime ogni volta che qualcosa richiede più di qualche secondo e puoi ragionevolmente stimarne i progressi nel tempo.

Le barre di avanzamento possono essere utilizzate per mostrare lo stato di qualcosa e il suo risultato

Alcuni esempi includono:

  • Al primo caricamento dell'applicazione (se il caricamento richiede molto tempo)
  • Durante l'elaborazione di un'importazione di dati di grandi dimensioni
  • Quando si prepara un file per il download
  • Quando l'utente è in coda in attesa che la sua richiesta venga elaborata

I componenti di una barra di avanzamento

Bene, con quello fuori mano, entriamo nel modo in cui costruire davvero queste cose!

È solo una piccola barra che si riempie su uno schermo. Quanto potrebbe essere complicato?

In realtà, abbastanza!

I seguenti componenti fanno generalmente parte di qualsiasi implementazione della barra di avanzamento:

  1. Un front-end, che in genere include una rappresentazione visiva del progresso e (facoltativamente) uno stato basato sul testo.
  2. Un backend che eseguirà effettivamente il lavoro che si desidera monitorare.
  3. Uno o più canali di comunicazione per il front-end per trasferire il lavoro al back-end.
  4. Uno o più canali di comunicazione per il back-end per comunicare i progressi al front-end.

Immediatamente possiamo vedere una fonte intrinseca di complessità. Vogliamo entrambi fare un po 'di lavoro nel backend e mostrare che il lavoro sta avvenendo sul frontend. Ciò significa immediatamente che coinvolgeremo più processi che devono interagire in modo asincrono.

Questi canali di comunicazione sono la parte della complessità. In un progetto Django relativamente standard, il browser front-end potrebbe inviare una richiesta HTTP AJAX (JavaScript) all'app Web back-end (Django). Questo a sua volta potrebbe passare quella richiesta alla coda delle attività (sedano) tramite un broker di messaggi (RabbitMQ / Redis). Quindi è necessario che tutto avvenga al contrario per ottenere informazioni al front-end!

L'intero processo potrebbe assomigliare a questo:

Il quadro generale di tutto ciò che riguarda la realizzazione di una buona barra di avanzamento

Analizziamo tutti questi componenti e vediamo come funzionano in un esempio pratico.

Il front-end

Il front-end è sicuramente la parte più semplice della barra di avanzamento. Con solo poche righe di HTML / CSS, puoi creare rapidamente una barra orizzontale dall'aspetto decente usando gli attributi di colore e larghezza dello sfondo. Aggiungi un po 'di JavaScript per aggiornarlo e sei a posto!

The Backend

Il backend è altrettanto semplice. Questo è essenzialmente solo un codice che verrà eseguito sul tuo server per eseguire il lavoro che desideri monitorare. Questo verrebbe in genere scritto in qualsiasi stack di applicazione che stai utilizzando (in questo caso Python e Django). Ecco una versione eccessivamente semplificata di come potrebbe apparire il backend:

def do_work (self, list_of_work):
    per work_item in list_of_work:
        do_work_item (WORK_ITEM)
    return 'il lavoro è completo'

Fare il lavoro

Va bene, quindi abbiamo la nostra barra di avanzamento del front-end e abbiamo ottenuto il nostro lavoro. Qual è il prossimo?

Bene, in realtà non abbiamo detto nulla su come questo lavoro verrà avviato. Quindi iniziamo da qui.

The Wrong Way: farlo nell'applicazione Web

In un tipico flusso di lavoro Ajax questo funzionerebbe nel modo seguente:

  1. Il front-end avvia la richiesta all'applicazione Web
  2. L'applicazione Web funziona nella richiesta
  3. Al termine, l'applicazione Web restituisce una risposta

In una vista Django, sarebbe simile a questo:

def my_view (richiesta):
    lavora()
    return HttpResponse ('lavoro svolto!')

Nel modo sbagliato: chiamare la funzione dalla vista

Il problema qui è che la funzione do_work potrebbe fare molto lavoro che richiede molto tempo (in caso contrario, non avrebbe senso aggiungere una barra di avanzamento per esso).

Fare un sacco di lavoro in una vista è generalmente considerato una cattiva pratica per diversi motivi, tra cui:

  • Si crea un'esperienza utente scadente, poiché le persone devono attendere il completamento delle lunghe richieste
  • Apri il tuo sito fino a potenziali problemi di stabilità con molte richieste di lunga durata e di lavoro (che potrebbero essere attivate in modo dannoso o accidentale)

Per questi motivi e altri, abbiamo bisogno di un approccio migliore per questo.

The Better Way: Asynchronous Task Queues (aka Sedano)

La maggior parte dei moderni framework Web ha creato code di attività asincrone per affrontare questo problema. In Python, il più comune è il sedano. In Rails, c'è Sidekiq (tra gli altri).

I dettagli tra questi variano, ma i loro principi fondamentali sono gli stessi. Fondamentalmente, invece di svolgere un lavoro in una richiesta HTTP che potrebbe richiedere arbitrariamente tempo - ed essere attivato con frequenza arbitraria - si inserisce quel lavoro in una coda e si hanno processi in background - spesso chiamati lavoratori - che raccolgono i lavori e li eseguono .

Questa architettura asincrona presenta numerosi vantaggi, tra cui:

  • Non eseguire lavori di lunga durata nei processi Web
  • Abilitazione della limitazione della velocità del lavoro svolto: il lavoro può essere limitato dal numero di processi di lavoro disponibili
  • Consentire l'esecuzione del lavoro su macchine ottimizzate per esso, ad esempio macchine con un numero elevato di CPU

La meccanica delle attività asincrone

La meccanica di base di un'architettura asincrona è relativamente semplice e coinvolge tre componenti principali: il client (i), il lavoratore (i) e il broker dei messaggi.

Il cliente è il principale responsabile della creazione di nuove attività. Nel nostro esempio, il client è l'applicazione Django, che crea attività sull'input dell'utente tramite una richiesta Web.

I lavoratori sono i processi reali che svolgono il lavoro. Questi sono i nostri lavoratori del sedano. È possibile avere un numero arbitrario di lavoratori in esecuzione su molte macchine, il che consente un'elevata disponibilità e il ridimensionamento orizzontale dell'elaborazione delle attività.

Il client e la coda delle attività dialogano tra loro tramite un broker di messaggi, che è responsabile dell'accettazione delle attività dal / i cliente / i e della consegna al lavoratore / i. Il broker di messaggi più comune per Celery è RabbitMQ, sebbene Redis sia anche un broker di messaggi completo comunemente usato e dotato di funzionalità.

Flusso di lavoro di base per il passaggio di messaggi a un processo di lavoro asincrono

Quando si crea un'applicazione di sedano standard, in genere si farà lo sviluppo del codice client e di lavoro, ma il broker dei messaggi sarà un pezzo di infrastruttura che devi solo alzare in piedi (e oltre che può [per lo più] ignorare).

Un esempio

Anche se tutto ciò sembra piuttosto complicato, il sedano fa un buon lavoro rendendolo abbastanza facile per noi attraverso belle astrazioni di programmazione.

Per convertire la nostra funzione lavorativa in qualcosa che può essere eseguito in modo asincrono, tutto ciò che dobbiamo fare è aggiungere un decoratore speciale:

dall'attività di importazione del sedano
# questo decoratore è tutto ciò che serve per dire al sedano che questo è un
# task lavoratore
@compito
def do_work (self, list_of_work):
    per work_item in list_of_work:
        do_work_item (WORK_ITEM)
    return 'il lavoro è completo'

Annotazione di una funzione di lavoro da chiamare dal sedano

Allo stesso modo, chiamare la funzione in modo asincrono dal client Django è altrettanto semplice:

def my_view (richiesta):
    # la chiamata .delay () qui è tutto ciò che serve
    # per convertire la funzione da chiamare in modo asincrono
    do_work.delay ()
    # non possiamo più dire "lavoro svolto" qui
    # perché tutto ciò che abbiamo fatto è stato dare il via
    return HttpResponse ('work kicked off!')

Chiamare la funzione di lavoro in modo asincrono

Con solo poche righe di codice in più, abbiamo convertito il nostro lavoro in un'architettura asincrona! Finché hai configurato e avviato i processi di lavoratore e broker, questo dovrebbe funzionare.

Monitoraggio dell'avanzamento

Bene, quindi abbiamo finalmente il nostro compito in esecuzione in background. Ma ora vogliamo tracciare i progressi su di esso. Come funziona esattamente?

Dobbiamo ancora fare alcune cose. Per prima cosa avremo bisogno di un modo per tenere traccia dei progressi all'interno del lavoro del lavoratore. Quindi dovremo comunicare i progressi fino al nostro front-end in modo da poter aggiornare la barra di avanzamento sulla pagina. Ancora una volta, questo finisce per essere un po 'più complicato di quanto si possa pensare!

Utilizzo di un oggetto Observer per tenere traccia dei progressi nel lavoratore

I lettori della Gang Seminal's Four Patterns di Design potrebbero avere familiarità con il modello di osservatore. Il tipico modello di osservatore include un soggetto che tiene traccia dello stato, nonché uno o più osservatori che fanno qualcosa in risposta allo stato. Nel nostro scenario di progresso, il soggetto è il processo / funzione del lavoratore che sta facendo il lavoro e l'osservatore è la cosa che seguirà il progresso.

Esistono molti modi per collegare il soggetto e l'osservatore, ma il più semplice è semplicemente passare l'osservatore come argomento alla funzione che svolge il lavoro.

Sembra qualcosa del genere:

@compito
def do_work (self, list_of_work, progress_observer):
    total_work_to_do = len (list_of_work)
    per i, work_item in enumerate (list_of_work):
        do_work_item (WORK_ITEM)
        # indica all'osservatore dei progressi quanti elementi del totale
        # abbiamo elaborato
        progress_observer.set_progress (i, total_work_to_do)
    return 'il lavoro è completo'

Utilizzo di un osservatore per monitorare l'avanzamento dei lavori

Ora non ci resta che passare un progress_observer valido e voilà, i nostri progressi verranno tracciati!

Ottenere progressi al cliente

Potresti pensare "aspetta un minuto ... hai appena chiamato una funzione chiamata set_progress, in realtà non hai fatto nulla!"

Vero! Quindi, come funziona davvero?

Ricorda: il nostro obiettivo è ottenere queste informazioni sui progressi fino alla pagina Web in modo da poter mostrare ai nostri utenti cosa sta succedendo. Ma il tracciamento dei progressi sta avvenendo completamente nel processo di lavoro! Ora stiamo affrontando un problema simile che abbiamo avuto con la consegna dell'attività asincrona in precedenza.

Per fortuna, Celery fornisce anche un meccanismo per restituire i messaggi al client. Questo viene fatto tramite un meccanismo chiamato backend dei risultati e, come i broker, hai la possibilità di diversi backend diversi. Sia RabbitMQ che Redis possono essere utilizzati come broker e backend di risultati e sono scelte ragionevoli, sebbene tecnicamente non vi sia accoppiamento tra broker e backend dei risultati.

Ad ogni modo, come i broker, i dettagli in genere non arrivano a meno che tu non stia facendo qualcosa di piuttosto avanzato. Ma il punto è che si attacca il risultato dall'attività da qualche parte (con l'ID univoco dell'attività), e quindi altri processi possono ottenere informazioni sulle attività tramite ID chiedendone il backend.

In Sedano, questo viene estratto abbastanza bene dallo stato associato all'attività. Lo stato ci consente di impostare uno stato generale, nonché di allegare metadati arbitrari all'attività. Questo è un posto perfetto per memorizzare i nostri progressi attuali e totali.

Impostazione dello stato

task.update_state (
    state = PROGRESS_STATE,
    meta = {'current': current, 'total': total}
)

Lettura dello stato

da celery.result import AsyncResult
risultato = AsyncResult (task_id)
print (result.state) # sarà impostato su PROGRESS_STATE print (result.info) # i metadati saranno qui

Ottenere progressi Aggiornamenti al front-end

Ora che possiamo ottenere aggiornamenti sui progressi dai lavoratori / attività e in qualsiasi altro client, il passaggio finale è semplicemente quello di ottenere tali informazioni al front-end e mostrarle all'utente.

Se vuoi divertirti, puoi usare qualcosa come i websocket per farlo in tempo reale. Ma la versione più semplice è semplicemente sondare un URL ogni tanto per verificare i progressi. Possiamo semplicemente fornire le informazioni sullo stato di avanzamento come JSON tramite una vista ed elaborazione Django e renderle sul lato client.

Vista Django:

def get_progress (request, task_id):
    risultato = AsyncResult (task_id)
    response_data = {
        'state': result.state,
        "dettagli": self.result.info,
    }
    return HttpResponse (
        json.dumps (RESPONSE_DATA),
        content_type = 'application / json'
    )

Vista Django per restituire i progressi come JSON.

Codice JavaScript:

funzione updateProgress (progressUrl) {
    fetch (progressUrl) .then (funzione (risposta) {
        response.json (). then (function (data) {
            // aggiorna i componenti dell'interfaccia utente appropriati
            setProgress (data.state, data.details);
            // e fallo di nuovo ogni mezzo secondo
            setTimeout (updateProgress, 500, progressUrl);
        });
    });
}

Codice Javascript per eseguire il polling per i progressi e aggiornare l'interfaccia utente.

Mettere tutto insieme

Questo è stato un bel po 'di dettagli su ciò che è - a prima vista - una parte molto semplice e quotidiana della nostra vita con i computer! Spero che tu abbia imparato qualcosa.

Se hai bisogno di un modo semplice per rendere le barre di avanzamento per le tue applicazioni Django / sedano, puoi dare un'occhiata a sedano-progresso - una biblioteca che ho scritto per rendere tutto questo un po 'più semplice. C'è anche una demo in azione su Build with Django.

Grazie per aver letto! Se desideri ricevere una notifica ogni volta che pubblico contenuti come questo sulla creazione di cose con Python e Django, registrati per ricevere gli aggiornamenti di seguito!

Originariamente pubblicato su buildwithdjango.com.