📜 ⬆️ ⬇️

Artistic approach to downloading images

I, as an artist and web developer, eventually had a need for their own gallery. Usually, galleries have two main functions: display of the showcase - all (or some) of the paintings - and a detailed display of one. The implementation of both functions is in almost any ready-made gallery. But the “worn out” appearance of the finished galleries and, having become the standard, the user interface is not suitable for the artist :). And non-standard - it requires a special architecture and implementation of the code that loads and displays the pictures. I will omit the display and user interface in this article. The focus will be given to downloading pictures from the server. The final organization of controlled loading using queues, asynchronous loader, processing of blob objects, promise cascades and with the possibility of suspension will be discussed.



Code samples written on coffeeScript

Tasks


  1. Loading all the paintings of the storefront takes time. The instant appearance of all is impossible. And the first appearance of pictures that the user will immediately see is possible.
    Therefore, one of the tasks was the ability to download pictures in the desired sequence. My gallery is visually centro-oriented, hence the loading order is centrifugal, first the pictures are loaded in the center of the screen, and then the rest are spread out in circles. Thus, small screens are filled quickly enough, and large screens allow access to view controls in the shortest possible time (the controls for moving and moving to a detailed view are concentrated around the central picture).
  2. Another task was the ability to pause the loading of images for the page from which they leave, without waiting until absolutely everything is loaded on it to immediately start loading data for the page that they are coming to. To do this, you need to pause the sending of requests, remember which pictures did not load, and after returning to the previous page, resume the download.

')
For this, a three-tier architecture was applied:
application -> download manager -> asynchronous loader


Application level


The application consistently receives the url of pictures that need to be downloaded and drawn on the screen. The way urls are supplied is not interesting. For each future picture, the application creates a DOM node img or div with a background.
imgNode = ($ '<div>') .addClass('item' + num) 

After that, he gives the task to the download manager, passing him the url of the pictures from the server. The manager returns a promise ( jQuery promise ), during which we will get the url before the blob instance with the loaded image data stored in the browser memory (the url will go to imgBlobUrl). This is a new feature that appeared in HTML5, which allows you to create urls to instances of the File or Blob classes obtained in this case as a result of an ajax request.
  loadingIntoLocal = @downloadMan.addTask image.url #       done,  imgNode    ,       ((imgNode) -> loadingIntoLocal.done (imgBlobUrl) -> imgNode.attr(src: imgBlobUrl) )(imgNode) 


Download Manager Level


The boot manager manages the queue of jobs ( queue ). Each task indicates: which url should be downloaded, which promise we will fulfill when we get the result, and, optionally, the number of the download attempt for a non-first-time-successful download. As soon as the task arrives, we put it in a queue, create a promise and return this promise to the application, so that it is not boring to wait. We start tasks.
  addTask : (url) -> downloading = new $.Deferred() task = { url: url, promise: downloading numRetries: 0 } @queue.push task @runTasks() 

To make the most efficient use of the channel, we will run several XMLHttpRequests simultaneously. The browser allows you to do this. Therefore, the @runTasks () method should ensure that at each moment in time there is not one, but N requests. In my case, 3 rickshaws were chosen experimentally. If there are free rickshaws, then we give the next task from the queue for execution.
  runTasks: -> if (@curTaskNum < @maxRunningTasks) && !@paused @runNextTask() 



"Rickshaw" takes the next task and using the asynchronous loader pulls up the image from the server, receiving the blob url.
  runNextTask: -> task = @queue.shift() @curTaskNum++ downloading = @asyncLoader.loadImage task.url 

As soon as the loader fulfills its promise, one of the rickshaws is released, and if there are still jobs in the queue, then the @runNextTask () method starts the following. In this case, we report upward that the promise made to the application is fulfilled.
  downloading.done (imgBlobUrl) => task.promise.resolve imgBlobUrl @curTaskNum-- if @queue.length != 0 && !@paused @runNextTask() 

Manager code (simplified version)
  class DownloadManager constructor: -> @queue = [] @maxRunningTasks = 3 @curTaskNum = 0 @paused = false @asyncLoader = new AsyncLoader() addTask : (url) -> downloading = new $.Deferred() task = { url: url, promise: downloading numRetries: 0 } @queue.push task @runTasks() downloading runTasks: -> if (@curTaskNum < @maxRunningTasks) && !@paused @runNextTask() runNextTask: -> task = @queue.shift() @curTaskNum++ task.numRetries++ downloading = @asyncLoader.loadImage task.url downloading.done (imgBlobUrl) => task.promise.resolve imgBlobUrl @curTaskNum-- if @queue.length != 0 && !@paused @runNextTask() downloading.fail => if task.numRetries < 3 @addTask task.url pause: -> @paused = true resume: -> @paused = false @runTasks() 


However, with this implementation of the pause through a flag indicating whether the next task can be started, stopping the download works roughly. If the transition to another page occurred at the moment when loading was full on all pairs in three streams, then the interruptions of the current tasks do not occur, the following ones simply do not start.
The implementation of a pause that makes XMLHttpRequest.abort () to tasks that are being executed is described in the “Clever Pause” section.

Asynchronous Loader Level


The asynchronous loader is the lowest level of our architecture, this is the “train station” that sends XMLHttpRequest and accepts binary image data with subsequent placement in the “fast access warehouse”.


We equip the "rickshaw" in a new trip and install handlers for its states. Note that we expect to get data that is available as an ArrayBuffer object that contains raw bytes. We send "rickshaw" in flight to the server. And then we promise to the top that we will inform as soon as he returns.
 class AsyncLoader loadImage: (url) -> xhr = new XMLHttpRequest() xhr.onprogress = (event) => ... #      xhr.onreadystatechange = => ... #     xhr.responseType = 'arraybuffer' xhr.open 'GET', url, true xhr.send() loadingImgBlob = new $.Deferred() return loadingImgBlob 

When the answer returned with the image data, create a blob object from them. Now to get the url for this object, it is enough to make objectUrl from a blob.
  imgBlobUrl = window.URL.createObjectURL blob 

The resulting address in the "local warehouse" is returned to the manager. At this point we reloaded the image.
  xhr.onreadystatechange = => if xhr.readyState == 4 if (xhr.status >= 200 and xhr.status <= 300) or xhr.status == 304 contentType = xhr.getResponseHeader 'Content-Type' contentType = contentType ? 'application/octet-binary' blob = new Blob [xhr.response], type: contentType imgBlobUrl = window.URL.createObjectURL blob loadingImgBlob .resolve imgBlobUrl 


A wise pause


For the correct solution of the second task (the suspension of the planned download for more urgent tasks), we change the average level of our DownloadManager architecture. In addition to the main job queue , the download manager, in which tasks that have not yet been submitted for execution, becomes the owner of the @enRoute queue, in which tasks are already in progress and which should be stopped in case of a pause in order to start the resume.
  class DownloadManager constructor: -> @queue = [] @enRoute = [] @maxRunningTasks = 3 @curTaskNum = 0 @paused = false @asyncLoader = new AsyncLoader() 

Thus, tasks can come in two types: the primary download and resume (if the picture has already got into the queue, started to load, and then was stopped). Moreover, Chrome is precisely pumping the missing data, and does not begin to download again. If we have already promised to download the image coming into the queue and wait for it, then we put it at the beginning of the queue. If we have not yet begun to download it, requested the first time, then - at the end of the queue. You can determine whether the picture has already been partially downloaded, by the existence of the promise object about loading it into addTask.
  addTask : (url, downloading) -> add = if !downloading then 'push' else 'unshift' downloading ?= new $.Deferred() #     ,   ,   ,      task = { xhr: null, #       XMLHttpRequest'  .     .  xhr      loadImage  asyncLoader'e url: url, promise: downloading numRetries: 0 } @queue[add] task @runTasks() return downloading 

Starter @runTasks () each time checks whether there are outstanding tasks, whether there is anyone to carry them out and whether we are not paused. If everything is so, we work.
  runTasks: -> while (@queue.length != 0) && (@curTaskNum < @maxRunningTasks) && !@paused @runNextTask() 

When paused, all requests that were in transit (@enRoute) are canceled (task.xhr. Abort () ) and re-scheduled for delivery next time. This time will come as soon as resume () restarts the job starter.
  pause: -> @paused = true while @enRoute.length != 0 task = @enRoute.shift() task.xhr.abort() @addTask task.url, task.promise #      @curTaskNum-- resume: -> @paused = false @runTasks() runNextTask: -> task = @queue.shift() @enRoute.push task @curTaskNum++ task.numRetries++ { downloading, xhr } = @asyncLoader.loadImage task.url #         xhr,    ,      . task.xhr = xhr downloading.done (imgBlobUrl) => i = @enRoute.indexOf task @enRoute.splice i, 1 task.promise.resolve imgBlobUrl @curTaskNum-- @runTasks() downloading.fail => if task.numRetries < 3 @addTask task.url 

I tried to describe the full cycle of controlled loading. A vivid example of the work of this architecture can be viewed on the gallery .
Demo for the test. The demo code for download and experiments is on github .
If you experiment with a demo, you will use another service that provides images, then you will need to configure resource sharing between different sources (Cross-origin resource sharing (CORS)) in order to allow the browser to send data to a script loaded from another domain. In the simplest case, this means that the web server should return the Access-Control-Allow-Origin header in the response: *. This will tell the browser that the server allows scripts from any other domains to make XHR requests. More details can be read on MDN .

Source: https://habr.com/ru/post/273415/


All Articles