JavaScript Promises – server-kald med progress-indikator

En dag da jeg surfede kattevideoer faglige relevante videoer på Youtube fik jeg øje på en rød progress-indikator:

Min første tanke var at det vil jeg have i mine apps. Hvordan havde de lavet det?
Hvis man sætter båndbredden yderligere ned begynder der at vise sig et mønster:

youtube-slow

Aha! Når man trykker på en video, starter Youtube en forespørgsel der skal hente informationer om den valgte video, animerer baren til 60%, hvor den venter indtil kaldet er gennemført, hvorefter den bliver animeret til 100%.

Snyd og bedrag, men en helt sikkert velovervejet løsning. Så længe man er på en tilstrækkelig hurtig forbindelse og den mængde af data der skal overføres er begrænset er illusionen fuldendt.

Det må siges at selvom Youtube snyder lidt, er det en langt bedre løsning end de spinnere man ser på størstedelen af alle sites med asynkrone forespørgsler i dag:

Det giver ingen følelse af at der sker noget eller om overførslen måske er stoppet. Jeg har oplevet mange sites der heller ikke håndterer fejl rigtigt, og man ender med en evig spinner – eller i hvertfald til man mister tålmodigheden og refresher siden.

Dan Saffer beskriver det simpelt i Designing Gestural Interfaces: Touchscreens and Interactive Devices:

Progress bars are an excellent example of responsive feedback: they don’t decrease waiting time, but they make it seem as though they do. They’re responsive.

Med de meget forskellige forbindelseshastigheder vi har i dag, vil jeg mene at behovet er endnu større – Youtube-eksemplet fra før vil måske nok ramme rigtigt på en gennemsnitlig forbindelse, men sidder man på Lars Tyndskids mark med Edge (hvis vi lige ser bort fra at man nok ikke kan se selve videoen) kan ventetiden hurtig blive så lang at man mister tålmodigheden.

Som Jakob Nielsen skriver i Usability Engineering er feedback især vigtig hvis svartiden er variabel:

10 seconds is about the limit for keeping the user’s attention focused on the dialogue. For longer delays, users will want to perform other tasks while waiting for the computer to finish, so they should be given feedback indicating when the computer expects to be done. Feedback during the delay is especially important if the response time is likely to be highly variable, since users will then not know what to expect.

Krav til løsningen

Der er efterhånden mange måder at indføre løbende feedback, men de fleste kommer med hver deres begrænsninger. For at kunne bruge løsningen i flest mulige problemområder skal der derfor stilles nogle krav.

Løsningen skal:

  • Kunne implementeres i eksisterende løsninger uden for meget ekstra arbejde og (næsten) uden ændringer på serveren.
  • Kunne fungere på tværs af forskellige domains altså når klienten lever på et domain og kommunikerer med en server på et andet (Også kaldet Cross Origin Resource Sharing eller CORS).
  • Virke i alle browsere.
  • Kunne sende en betydelig mængde data både frem og tilbage.

De første forsøg

Når man kommunikerer med en server fra JavaScript er der under normale omstændigheder tale om en request der bliver startet, hvor efter man lidt senere får svar på om det gik godt eller skidt. Altså ingen løbende feedback. Den naive løsning kunne være at lave flere enkelte forespørgsler, men det at udføre en enkelt forespørgsel er relativt dyrt så det samlede overhead vil blive for stort.

Hvis man kigger på HTTP/1.1 så er der mulighed for at dele et svar fra serveren op i flere enkeltstykker – metoden hedder chunked-http:

chunked

Hvis man sender et på forhånd aftalt antal chunks, kan man give feedback til brugeren om hvor langt en request er kommet.

Traditionelt AJAX kald

Traditionelle AJAX kald – typisk udført vha. XMLHttpRequest giver som udgangspunkt ikke løbende feedback. Ønsker man at udføre kaldet som et CORS-kald involverer det ændringer på serveren og det vil desuden give en ekstra forespørgsel hvis man kører med authorization. Supporten er begrænset i ældre browsere.

JSONP

JSONP er en gammel-kendt teknik til at komme omkring cross-server problemer med AJAX – Et kald hvor man reelt tilføjer et script der lever på en anden server som så bliver udført i scope af ens side.

I et svagt øjeblik prøvede jeg at implementere chunked-http i forbindelse med JSONP kald – altså flere JavaScript funktioner i ét script der så ville blive udført løbende. Det virkede naturligvis ikke, browseren udfører først JavaScript filer når hentning er færdig.

Andre teknologier

Jeg kiggede også på Server Sent Events, men disse findes ikke i Internet Explorer.
WebSockets kræver en større ændring af serversiden og giver også udfordringer med sikkerhed, cookies m.v. da vi er ude over den traditionelle http-model.

Failure is always an option

Adam Savage

Den endelige løsning

HiddenFrame er en teknik hvor man skaber en skjult iframe der sættes til at hente en klump HTML fra serveren. Hvis der i denne klump HTML er script-tags, vil de blive udført idet de bliver afsluttet. Der har vi altså en potentiel løsning.

Afsendelse af data er heller ikke et problem, da man kan sætte selve hentningen igang ved at udføre en form-post.

Og hvad har det så med JavaScript Promises at gøre?

For at få et godt API til løsningen har jeg benyttet mig af en Promise-implementation der tilbyder løbende feedback i form af en progress-funktion samt håndtering af både fejl og success-scenarier:

new LittleConvoy.Client('HiddenFrame').send({ url: 'http://eksempel' }, { name: 'Ford'})
    .progressed(function (progress) {
      ... progress indeholder procent-vis fremdrift og vil blive kaldt 10 gange
    })
    .then(function (data) {
      ... kaldet gik godt, data indeholder resultatet
    })
    .catch(function (message) {
       ... kaldet gik skidt, message indeholder fejlbesked
    });

På serversiden inkluderes et bibliotek, der pt. er skrevet til Microsoft ASP.NET MVC. De funktioner man allerede har der afleverer JSON formaterede beskeder dekoreres blot med en enkelt attribut, der sørger for at det hele virker:

public class DemoController : Controller
{
    [LittleConvoyAction(StartPercent = 40)]
    public ActionResult Echo(object source)
    {
      return new JsonResult {Data = source };
    }
}

Demo or it didn’t happen

  • En lille demo kan findes her.
  • Koden kan findes på GitHub
  • Biblioteket kan installeres via .NET pakkemanageren NuGet som LittleConvoy.

Fremtiden

  • Selve transportlaget er separeret ud så flere former kan tilføjes, det kunne være nærliggende at tilføje traditionel AJAX og Websockets i fremtiden hvis det kan gøres uden at øge kompleksiteten af opsætningen på serversiden.
  • Visse Promises tilbyder Cancellation, det kunne være fint hvis man også kunne afbryde et kald.
  • Afsendelse af data giver pt. ingen feedback – kunne være godt hvis man på en eller anden måde kunne få det til at virke.

Beslægtet arbejde

  • Comet er en samling af teknolgier der tilbyder push af data til browseren, den indeholder også iframe-kommunikation men er målrettet permanente kommunikationskanaler.
  • SignalR er lige som Comet bygget til push og permanente kommunikationskanaler. Men mere målrettet .NET.
  • Socket.IO er en abstraktion oven på WebSockets der indeholder fallback til iframe-kommunikation men mere målrettet Node.js.

This post is also available in English at Complexitymaze.com

Skriv et svar

Din e-mailadresse vil ikke blive publiceret. Krævede felter er markeret med *