mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2025-08-20 20:33:32 +08:00
fix(img): move img requests out of web workers
Due to iOS Cordova WKWebView limitations, ion-img is not able to use web workers for XMLHttpRequests.
This commit is contained in:
@ -1,83 +1,166 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
|
||||
import { Config } from '../../config/config';
|
||||
import { removeArrayItem } from '../../util/util';
|
||||
|
||||
|
||||
@Injectable()
|
||||
export class ImgLoader {
|
||||
private wkr: Worker;
|
||||
private callbacks: Function[] = [];
|
||||
private ids = 0;
|
||||
private url: string;
|
||||
private imgs: ImgData[] = [];
|
||||
|
||||
constructor(config: Config) {
|
||||
this.url = config.get('imgWorkerUrl', IMG_WORKER_URL);
|
||||
load(src: string, useCache: boolean, callback: ImgLoadCallback) {
|
||||
// see if we already have image data for this src
|
||||
let img = this.imgs.find(i => i.src === src);
|
||||
|
||||
if (img && img.datauri && useCache) {
|
||||
// we found image data, and it's cool if we use the cache
|
||||
// so let's respond with the cached data
|
||||
callback(200, null, img.datauri);
|
||||
return;
|
||||
}
|
||||
|
||||
load(src: string, cache: boolean, callback: Function) {
|
||||
if (src) {
|
||||
(<any>callback).id = this.ids++;
|
||||
this.callbacks.push(callback);
|
||||
this.worker().postMessage(JSON.stringify({
|
||||
id: (<any>callback).id,
|
||||
src: src,
|
||||
cache: cache
|
||||
}));
|
||||
}
|
||||
// so no cached image data, so we'll
|
||||
// need to do a new http request
|
||||
|
||||
if (img && img.xhr && img.xhr.readyState !== 4) {
|
||||
// looks like there's already an active http request going on
|
||||
// for this same source, so let's just add another listener
|
||||
img.xhr.addEventListener('load', (xhrEvent) => {
|
||||
onXhrLoad(callback, xhrEvent, useCache, img, this.imgs);
|
||||
});
|
||||
img.xhr.addEventListener('error', (xhrErrorEvent) => {
|
||||
onXhrError(callback, img, xhrErrorEvent);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
cancelLoad(callback: Function) {
|
||||
removeArrayItem(this.callbacks, callback);
|
||||
if (!img) {
|
||||
// no image data yet, so let's create it
|
||||
img = { src: src };
|
||||
this.imgs.push(img);
|
||||
}
|
||||
|
||||
// ok, let's do a full request for the image
|
||||
img.xhr = new XMLHttpRequest();
|
||||
img.xhr.open('GET', src, true);
|
||||
img.xhr.responseType = 'arraybuffer';
|
||||
|
||||
// add the listeners if it loaded or errored
|
||||
img.xhr.addEventListener('load', (xhrEvent) => {
|
||||
onXhrLoad(callback, xhrEvent, useCache, img, this.imgs);
|
||||
});
|
||||
img.xhr.addEventListener('error', (xhrErrorEvent) => {
|
||||
onXhrError(callback, img, xhrErrorEvent);
|
||||
});
|
||||
|
||||
// awesome, let's kick off the request
|
||||
img.xhr.send();
|
||||
}
|
||||
|
||||
abort(src: string) {
|
||||
if (src) {
|
||||
this.worker().postMessage(JSON.stringify({
|
||||
src: src,
|
||||
type: 'abort'
|
||||
}));
|
||||
const img = this.imgs.find(i => i.src === src);
|
||||
if (img && img.xhr && img.xhr.readyState !== 4) {
|
||||
// we found the image data and there's an active
|
||||
// http request, so let's abort the request
|
||||
img.xhr.abort();
|
||||
img.xhr = null;
|
||||
}
|
||||
}
|
||||
|
||||
private worker() {
|
||||
if (!this.wkr) {
|
||||
// create the worker
|
||||
this.wkr = new Worker(this.url);
|
||||
|
||||
// create worker onmessage handler
|
||||
this.wkr.onmessage = (ev: MessageEvent) => {
|
||||
// we got something back from the web worker
|
||||
// let's emit this out to everyone listening
|
||||
const msg: ImgResponseMessage = JSON.parse(ev.data);
|
||||
const callback = this.callbacks.find(cb => (<any>cb).id === msg.id);
|
||||
if (callback) {
|
||||
callback(msg);
|
||||
removeArrayItem(this.callbacks, callback);
|
||||
}
|
||||
};
|
||||
|
||||
// create worker onerror handler
|
||||
this.wkr.onerror = (ev: ErrorEvent) => {
|
||||
console.error(`ImgLoader, worker ${ev.type} ${ev.message ? ev.message : ''}`);
|
||||
this.callbacks.length = 0;
|
||||
this.wkr.terminate();
|
||||
this.wkr = null;
|
||||
};
|
||||
}
|
||||
|
||||
// return that hard worker
|
||||
return this.wkr;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const IMG_WORKER_URL = 'build/ion-img-worker.js';
|
||||
|
||||
export interface ImgResponseMessage {
|
||||
id: number;
|
||||
function onXhrLoad(callback: ImgLoadCallback, ev: any, useCache: boolean, img: ImgData, imgs: ImgData[]) {
|
||||
if (!callback) {
|
||||
return;
|
||||
}
|
||||
|
||||
// the http request has been loaded
|
||||
// create a rsp object to send back to the main thread
|
||||
const status: number = ev.target.status;
|
||||
let datauri: string = null;
|
||||
|
||||
if (status === 200) {
|
||||
// success!!
|
||||
// now let's convert the response arraybuffer data into a datauri
|
||||
datauri = getDataUri(ev.target.getResponseHeader('Content-Type'), ev.target.response);
|
||||
|
||||
if (useCache) {
|
||||
// if the image was successfully downloaded
|
||||
// and this image is allowed to be cached
|
||||
// then let's add it to our image data for later use
|
||||
img.datauri = datauri;
|
||||
img.len = datauri.length;
|
||||
|
||||
// let's loop through all our cached data and if we go
|
||||
// over our limit then let's clean it out a bit
|
||||
// oldest data should go first
|
||||
var cacheSize = 0;
|
||||
for (var i = imgs.length - 1; i >= 0; i--) {
|
||||
cacheSize += imgs[i].len;
|
||||
if (cacheSize > CACHE_LIMIT) {
|
||||
console.debug(`img-loader, clear: ${imgs[i].src}, len: ${imgs[i].len}`);
|
||||
imgs.splice(i, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// fire the callback with what we've learned today
|
||||
callback(status, null, datauri);
|
||||
}
|
||||
|
||||
|
||||
function onXhrError(callback: ImgLoadCallback, imgData: ImgData, err: ErrorEvent) {
|
||||
// darn, we got an error!
|
||||
callback && callback(0, (err.message || ''), null);
|
||||
imgData.xhr = null;
|
||||
}
|
||||
|
||||
|
||||
function getDataUri(contentType, arrayBuffer): string {
|
||||
// take arraybuffer and content type and turn it into
|
||||
// a datauri string that can be used by <img>
|
||||
const rtn: string[] = ['data:' + contentType + ';base64,'];
|
||||
|
||||
const bytes = new Uint8Array(arrayBuffer);
|
||||
const byteLength = bytes.byteLength;
|
||||
const byteRemainder = byteLength % 3;
|
||||
const mainLength = byteLength - byteRemainder;
|
||||
let i, a, b, c, d, chunk;
|
||||
|
||||
for (i = 0; i < mainLength; i = i + 3) {
|
||||
chunk = (bytes[i] << 16) | (bytes[i + 1] << 8) | bytes[i + 2];
|
||||
a = (chunk & 16515072) >> 18;
|
||||
b = (chunk & 258048) >> 12;
|
||||
c = (chunk & 4032) >> 6;
|
||||
d = chunk & 63;
|
||||
rtn.push(ENCODINGS[a] + ENCODINGS[b] + ENCODINGS[c] + ENCODINGS[d]);
|
||||
}
|
||||
|
||||
if (byteRemainder === 1) {
|
||||
chunk = bytes[mainLength];
|
||||
a = (chunk & 252) >> 2;
|
||||
b = (chunk & 3) << 4;
|
||||
rtn.push(ENCODINGS[a] + ENCODINGS[b] + '==');
|
||||
|
||||
} else if (byteRemainder === 2) {
|
||||
chunk = (bytes[mainLength] << 8) | bytes[mainLength + 1];
|
||||
a = (chunk & 64512) >> 10;
|
||||
b = (chunk & 1008) >> 4;
|
||||
c = (chunk & 15) << 2;
|
||||
rtn.push(ENCODINGS[a] + ENCODINGS[b] + ENCODINGS[c] + '=');
|
||||
}
|
||||
|
||||
return rtn.join('');
|
||||
}
|
||||
|
||||
// used by the setData function
|
||||
const ENCODINGS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
|
||||
|
||||
const CACHE_LIMIT = 1381855 * 20;
|
||||
|
||||
export interface ImgData {
|
||||
src: string;
|
||||
status?: number;
|
||||
data?: string;
|
||||
msg?: string;
|
||||
datauri?: string;
|
||||
len?: number;
|
||||
xhr?: XMLHttpRequest;
|
||||
}
|
||||
|
||||
export type ImgLoadCallback = {
|
||||
(status: number, msg: string, datauri: string): void;
|
||||
}
|
||||
|
@ -2,7 +2,7 @@ import { ChangeDetectionStrategy, Component, ElementRef, Input, NgZone, OnDestro
|
||||
|
||||
import { Content } from '../content/content';
|
||||
import { DomController } from '../../util/dom-controller';
|
||||
import { ImgLoader, ImgResponseMessage } from './img-loader';
|
||||
import { ImgLoader, ImgLoadCallback } from './img-loader';
|
||||
import { isPresent, isTrueProperty } from '../../util/util';
|
||||
import { Platform } from '../../platform/platform';
|
||||
|
||||
@ -134,7 +134,7 @@ export class Img implements OnDestroy {
|
||||
/** @internal */
|
||||
_cache: boolean = true;
|
||||
/** @internal */
|
||||
_cb: Function;
|
||||
_cb: ImgLoadCallback;
|
||||
/** @internal */
|
||||
_bounds: any;
|
||||
/** @internal */
|
||||
@ -235,9 +235,9 @@ export class Img implements OnDestroy {
|
||||
console.debug(`request ${this._src} ${Date.now()}`);
|
||||
this._requestingSrc = this._src;
|
||||
|
||||
// create a callback for when we get data back from the web worker
|
||||
this._cb = (msg: ImgResponseMessage) => {
|
||||
this._loadResponse(msg);
|
||||
this._cb = (status, msg, datauri) => {
|
||||
this._loadResponse(status, msg, datauri);
|
||||
this._cb = null;
|
||||
};
|
||||
|
||||
// post the message to the web worker
|
||||
@ -263,18 +263,18 @@ export class Img implements OnDestroy {
|
||||
}
|
||||
}
|
||||
|
||||
private _loadResponse(msg: ImgResponseMessage) {
|
||||
private _loadResponse(status: number, msg: string, datauri: string) {
|
||||
this._requestingSrc = null;
|
||||
|
||||
if (msg.status === 200) {
|
||||
if (status === 200) {
|
||||
// success :)
|
||||
this._tmpDataUri = msg.data;
|
||||
this._tmpDataUri = datauri;
|
||||
this.update();
|
||||
|
||||
} else {
|
||||
// error :(
|
||||
if (msg.status) {
|
||||
console.error(`img, status: ${msg.status} ${msg.msg}`);
|
||||
if (status) {
|
||||
console.error(`img, status: ${status} ${msg}`);
|
||||
}
|
||||
this._renderedSrc = this._tmpDataUri = null;
|
||||
this._dom.write(() => {
|
||||
@ -413,7 +413,6 @@ export class Img implements OnDestroy {
|
||||
* @private
|
||||
*/
|
||||
ngOnDestroy() {
|
||||
this._ldr.cancelLoad(this._cb);
|
||||
this._cb = null;
|
||||
this._content && this._content.removeImg(this);
|
||||
}
|
||||
|
@ -2,7 +2,7 @@ import { ElementRef, Renderer } from '@angular/core';
|
||||
import { Content } from '../../content/content';
|
||||
import { Img } from '../img';
|
||||
import { ImgLoader } from '../img-loader';
|
||||
import { mockConfig, mockContent, MockDomController, mockElementRef, mockPlatform, mockRenderer, mockZone } from '../../../util/mock-providers';
|
||||
import { mockContent, MockDomController, mockElementRef, mockPlatform, mockRenderer, mockZone } from '../../../util/mock-providers';
|
||||
import { Platform } from '../../../platform/platform';
|
||||
|
||||
|
||||
@ -79,7 +79,7 @@ describe('Img', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
content = mockContent();
|
||||
ldr = new ImgLoader(mockConfig());
|
||||
ldr = new ImgLoader();
|
||||
elementRef = mockElementRef();
|
||||
renderer = mockRenderer();
|
||||
platform = mockPlatform();
|
||||
|
Reference in New Issue
Block a user