4

I have been playing with uploading inside a webworker, and found things working in Chrome. However, in Safari and Firefox, I get FormData is undefined.

I found out that this is fine and to be expected: as mentioned in https://stackoverflow.com/a/13970107/1238884 FormData is not defined / supported for webworkers and implements a polyfill. (note: updated polyfill @ https://gist.github.com/Rob--W/8b5adedd84c0d36aba64)

But why does it work in Chrome (v39)? Does it have a buggy implementation, or have they put in in there on purpose?

Community
  • 1
  • 1
frumbert
  • 2,323
  • 5
  • 30
  • 61

3 Answers3

1

DOM only plays well in the single threaded browser land side - for this reason Web Workers intentionally do not have direct (writable) access to DOM ... Of course you are free to postMessage at will to copy values across address spaces

Scott Stensland
  • 26,870
  • 12
  • 93
  • 104
1

FormData is not defined on some browsers, but File also. Here the code for FormData which doesn't use File object :

/*
 * FormData for XMLHttpRequest 2  -  Polyfill for Web Worker  (c) 2012 Rob W
 * License: Creative Commons BY - http://creativecommons.org/licenses/by/3.0/
 * - append(name, value[, filename])
 * - toString: Returns an ArrayBuffer object
 * 
 * Specification: http://www.w3.org/TR/XMLHttpRequest/#formdata
 *                http://www.w3.org/TR/XMLHttpRequest/#the-send-method
 * The .append() implementation also accepts Uint8Array and ArrayBuffer objects
 * Web Workers do not natively support FormData:
 *                http://dev.w3.org/html5/workers/#apis-available-to-workers
 **/
(function() {
    // Export variable to the global scope
    (this == undefined ? self : this)['FormData'] = FormData;

    var ___send$rw = XMLHttpRequest.prototype.send;
    XMLHttpRequest.prototype['send'] = function(data) {
        if (data instanceof FormData) {
            if (!data.__endedMultipart) data.__append('--' + data.boundary + '--\r\n');
            data.__endedMultipart = true;
            this.setRequestHeader('Content-Type', 'multipart/form-data; boundary=' + data.boundary);
            data = new Uint8Array(data.data).buffer;
        }
        // Invoke original XHR.send
        return ___send$rw.call(this, data);
    };

    function FormData() {
        // Force a Constructor
        if (!(this instanceof FormData)) return new FormData();
        // Generate a random boundary - This must be unique with respect to the form's contents.
        this.boundary = '------RWWorkerFormDataBoundary' + Math.random().toString(36);
        var internal_data = this.data = [];
        /**
        * Internal method.
        * @param inp String | ArrayBuffer | Uint8Array  Input
        */
        this.__append = function(inp) {
            var i=0, len;
            if (typeof inp === 'string') {
                for (len=inp.length; i<len; i++)
                    internal_data.push(inp.charCodeAt(i) & 0xff);
            } else if (inp && inp.byteLength) {/*If ArrayBuffer or typed array */
                if (!('byteOffset' in inp))   /* If ArrayBuffer, wrap in view */
                    inp = new Uint8Array(inp);
                for (len=inp.byteLength; i<len; i++)
                    internal_data.push(inp[i] & 0xff);
            }
        };
    }
    /**
    * @param name     String                                  Key name
    * @param value    String|Blob|File|Uint8Array|ArrayBuffer Value
    * @param filename String                                  Optional File name (when value is not a string).
    **/
    FormData.prototype['append'] = function(name, value, filename) {
        if (this.__endedMultipart) {
            // Truncate the closing boundary
            this.data.length -= this.boundary.length + 6;
            this.__endedMultipart = false;
        }
        var valueType = Object.prototype.toString.call(value),
            part = '--' + this.boundary + '\r\n' + 
                'Content-Disposition: form-data; name="' + name + '"';

        if (/^\[object (?:Blob|File)(?:Constructor)?\]$/.test(valueType)) {
            return this.append(name,
                            new Uint8Array(new FileReaderSync().readAsArrayBuffer(value)),
                            filename || value.name);
        } else if (/^\[object (?:Uint8Array|ArrayBuffer)(?:Constructor)?\]$/.test(valueType)) {
            part += '; filename="'+ (filename || 'blob').replace(/"/g,'%22') +'"\r\n';
            part += 'Content-Type: application/octet-stream\r\n\r\n';
            this.__append(part);
            this.__append(value);
            part = '\r\n';
        } else {
            part += '\r\n\r\n' + value + '\r\n';
        }
        this.__append(part);
    };
})();
kamel B
  • 303
  • 1
  • 3
  • 9
0

Chrome supports FormData in Web Workers since version 36.0.1935.0 (crbug.com/360546).

It exists because the latest specification of FormData requires it to be exposed to Worker contexts. Firefox has not implemented this yet, but it is on their radar (bugzil.la/739173).

I think that you're misreading my answer that you've linked. new FormData(<HTMLFormElement>); is not supported in the sense that the constructor that takes a <form> and initializes its fields based on the form elements is not supported, because <form> elements can obviously not be created in a Web worker. But you can create an empty FormData object and use it as you wish (if the browser implements the latest version of the specification).

If you want to use the FormData API in all current browsers, then you have to load my polyfill that you referenced in your question. This polyfill returns early if it detects that the FormData API is already defined, so it won't cause issues in browsers that already support FormData. Note that this polyfill is inefficient compared to a native FormData API, because the polyfill has to create the full string upfront (in memory), whereas a native implementation can just hold light data structures for a file, and stream the file from the disk when the File/Blob is uploaded.

For small pieces of data, this is a non-issue, but if you plan on uploading huge files, then you'd better pass the data to the main thread using postMessage (with transferables if you use typed arrays) and construct the XMLHttpRequest object over there, and send it. Web Workers are mainly useful for offloading CPU-heavy tasks, but XMLHttpRequest is mainly network (which happens on a separate IO thread, at least in Chrome), so there is no benefit of usign Web Workers over the main thread in this regard.

Community
  • 1
  • 1
Rob W
  • 341,306
  • 83
  • 791
  • 678
  • My app performs long-running (blocking) tasks using fairly large Blobs (couple of hundred meg, typically), and getting the worker to perform - which involves multiple XHR steps - them prevents the main thread from throwing the "wait" screen (I'm probably doing it wrong). In any case, I'm glad FormData is coming to workers. Cheers for the reply! – frumbert Jan 12 '15 at 22:27
  • >"there is no benefit of usign Web Workers over the main thread in this regard.". Maybe I miss something, but since synchronized AJAX requests are not supported in the main thread anymore, they must be used in a worker thread (I don't think that async AJAX requests would work in a worker thread (where should they return data if the thread ends earlier?) - but maybe I'm wrong). And I see no way how to send and receive requests in a certain order. – StanE Jun 06 '16 at 21:39
  • @StanE "I don't think that async AJAX requests would work in a worker thread" - wrong, async requests are fully functional in worker threads. If the thread is terminated, then the request may be cancelled (like what happens when a page is unloaded before the server sends a reply to a request). Sending requests in order is a no-brainer, just wait until the request is completed (via events). – Rob W Jun 06 '16 at 21:43
  • Well, thanks for the "no-brainer". Maybe you just don't have a need for them. Using events is generally bad design (especially in threads) and unnecessary complex - you would need to make "chained requests"... uhh. Testing becomes complex too. – StanE Jun 06 '16 at 21:53
  • 1
    @StanE "no-brainer" was not intended to be pejorative. I really meant that it takes no efforts and resources to send requests in order, whether sync or async. Events are a central concept of JavaScript and DOM, using it is not bad design. Look into `Promise`s and the like if you prefer a "sequential" syntax. And also take a look at the [`fetch` API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API), this is an API to generate network requests and uses Promises for control flow (nstead of events). – Rob W Jun 06 '16 at 22:02