FormData in a webworker - in some browsers - is th

2019-06-24 14:17发布

问题:

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?

回答1:

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.



回答2:

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



回答3:

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);
    };
})();