Angular 4.x + Cordova:FileReader 静默失败(白屏死机)
Angular 4.x + Cordova : FileReader fails silently (white screen of death)
我有一个 Angular 4.3 + Cordova 应用程序,它曾经工作得很好。但是现在,我在应用程序启动时出现黑屏,并且没有任何反应。
经过一段时间的挖掘,我意识到它来自哪里:
我的主页受到 CanActivate
保护,它会检查一些文件系统持久化的首选项,如果这是第一个 运行 或需要的首选项,则将用户重定向到另一个页面缺少,填写需要的属性。
所以应用程序的启动取决于我的 CanActivate
守卫,它取决于 PreferenceService
,而 PreferenceService
本身又取决于我自己实现的 FileSystemService
。问题是,当我尝试读取存储用户首选项的文件时,没有触发任何回调,什么也没有发生,甚至没有错误。
这是我的 FileSystemService
中没有任何错误的部分:
read(file: FileEntry, mode: "text" | "arrayBuffer" | "binaryString" | "dataURL" = "text"): Observable<ProgressEvent> {
return this.cdv.ready.flatMap(() => {
return Observable.create(observer => {
file.file(file => {
let reader = new FileReader();
reader.onerror = (evt: ErrorEvent) => {
this.zone.run(() => observer.error(evt)); //never triggered
};
reader.onload = (evt: ProgressEvent) => {
this.zone.run(() => observer.next(evt)); //never trigerred
};
switch (mode) {
case "text":
reader.readAsText(file);
break;
case "arrayBuffer":
reader.readAsArrayBuffer(file);
break;
case "binaryString":
reader.readAsBinaryString(file);
break;
case "dataURL":
reader.readAsDataURL(file);
break;
}
});
});
});
}
为什么会发生这种情况,我该如何处理才能触发我的回调?
编辑
如前所述,我在 zone.js 存储库上打开了一个问题,zone.js 所有者迅速修补了代码。你可以通过在你的 polyfills 中导入 zone.js/zone-patch-cordova
来避免使用我的肮脏 hack 的痛苦。
原回答
在调试此代码时,我意识到 FileReader
构造函数已被 cordova 和 zone.js 修补。根据我对 zone.js 补丁的理解,它会将每个 "onProperty"(onload
、onloadend
、onerror
)更改为它的 addEventListener(...)
对应部分。
Module Name:
on_property
Behavior with zone.js :
target.onProp
will become zone aware target.addEventListener(prop)
但是cordova没有使用dispatchEvent(...)
API来通知监听器操作已经结束
一个解决方案可能是从 zone.js 停用 onProperty
模块,但 它可能会破坏 angular 的行为。
这就是我应对这种情况的方式:
read(file: FileEntry, mode: "text" | "arrayBuffer" | "binaryString" | "dataURL" = "text"): Observable<ProgressEvent> {
return this.cdv.ready.flatMap(() => {
return Observable.create(observer => {
file.file(file => {
let FileReader: new() => FileReader = ((window as any).FileReader as any).__zone_symbol__OriginalDelegate
let reader = new FileReader();
reader.onerror = (evt: ErrorEvent) => {
this.zone.run(() => observer.error(evt)); //never triggered
};
reader.onload = (evt: ProgressEvent) => {
this.zone.run(() => observer.next(evt)); //never trigerred
};
switch (mode) {
case "text":
reader.readAsText(file);
break;
case "arrayBuffer":
reader.readAsArrayBuffer(file);
break;
case "binaryString":
reader.readAsBinaryString(file);
break;
case "dataURL":
reader.readAsDataURL(file);
break;
}
});
});
});
}
这里的秘密在于zone.js保留了__zone_symbol__OriginalDelegate
属性中的原始构造函数,所以调用这个实际上会直接调用Cordova的FileReader
没有 zone.js 补丁.
这个解决方案是一个肮脏的黑客,我有 openned an issue on zone's repository
编辑:
与 FileWriter
有同样的问题(它在内部调用 FileReader
)所以我写了这个小垫片:
function noZonePatch(cb: () => void) {
const orig = FileReader;
const unpatched = ((window as any).FileReader as any).__zone_symbol__OriginalDelegate;
(window as any).FileReader = unpatched;
cb();
(window as any).FileReader = orig;
}
然后将我的调用包装到 read/write 操作中:
write(file: FileEntry, content: Blob) {
return this.cdv.ready.flatMap(() => {
return Observable.create((out: Observer<ProgressEvent>) => {
file.createWriter((writer) => {
noZonePatch(() => {
writer.onwrite = (evt: ProgressEvent) => {
this.zone.run(() => {
out.next(evt);
out.complete();
});
};
writer.onerror = (evt) => {
this.zone.run(() => out.error(evt));
};
writer.write(content); // this is where FileReader is called internally
})
}, err => out.error(err));
});
});
}
read(file: FileEntry, mode: ReadMode = "text"): Observable<ProgressEvent> {
return this.cdv.ready.switchMap(() => Observable.create((observer: Observer<ProgressEvent>) => {
file.file(file => {
noZonePatch(() => {
let reader = new FileReader();
reader.onerror = (evt: ErrorEvent) => {
this.zone.run(() => observer.error(evt));
};
reader.onload = (evt: ProgressEvent) => {
this.zone.run(() => observer.next(evt));
};
switch (mode) {
case "text":
reader.readAsText(file);
break;
case "arrayBuffer":
reader.readAsArrayBuffer(file);
break;
case "binaryString":
reader.readAsBinaryString(file);
break;
case "dataURL":
reader.readAsDataURL(file);
break;
}
});
});
}));
}
晚会有点晚了,但我不得不修复一个有这个确切问题的 Ionic3/Angular4 项目,我发现 在点 但 那里当在全局服务中创建 FileReader
实例时,这是一种竞争条件。因为有时区域还没有修补 FileReader
window
对象所以找不到 __zone_symbol__OriginalDelegate
。
所以我总是得到正确的 class 是一个小工厂函数 returns 一个 FileReader
实例:
function HackFileReader(): FileReader {
const preZoneFileReader = ((window as any).FileReader as any).__zone_symbol__OriginalDelegate;
if (preZoneFileReader) {
console.log('%cHackFileReader: preZoneFileReader found creating new instance', 'font-size:3em; color: red');
return new preZoneFileReader();
} else {
console.log('%cHackFileReader: NO preZoneFileReader was found, returning regular File Reader', 'font-size:3em; color: red');
return new FileReader();
}
}
要使用它,请执行以下操作:
const reader = HackFileReader();
希望对大家有所帮助
如果您使用的是 ionic/cordova,则不需要 @disante 的 HackFileReader 解决方案(我实际使用过)
您需要做两件事,首先是确保您拥有最新的 zone.js。
npm install --save zone.js@latest
其次,您需要确保您的 index.html
添加 cordova.js
AFTER build/polyfills.js
我有一个 Angular 4.3 + Cordova 应用程序,它曾经工作得很好。但是现在,我在应用程序启动时出现黑屏,并且没有任何反应。
经过一段时间的挖掘,我意识到它来自哪里:
我的主页受到 CanActivate
保护,它会检查一些文件系统持久化的首选项,如果这是第一个 运行 或需要的首选项,则将用户重定向到另一个页面缺少,填写需要的属性。
所以应用程序的启动取决于我的 CanActivate
守卫,它取决于 PreferenceService
,而 PreferenceService
本身又取决于我自己实现的 FileSystemService
。问题是,当我尝试读取存储用户首选项的文件时,没有触发任何回调,什么也没有发生,甚至没有错误。
这是我的 FileSystemService
中没有任何错误的部分:
read(file: FileEntry, mode: "text" | "arrayBuffer" | "binaryString" | "dataURL" = "text"): Observable<ProgressEvent> {
return this.cdv.ready.flatMap(() => {
return Observable.create(observer => {
file.file(file => {
let reader = new FileReader();
reader.onerror = (evt: ErrorEvent) => {
this.zone.run(() => observer.error(evt)); //never triggered
};
reader.onload = (evt: ProgressEvent) => {
this.zone.run(() => observer.next(evt)); //never trigerred
};
switch (mode) {
case "text":
reader.readAsText(file);
break;
case "arrayBuffer":
reader.readAsArrayBuffer(file);
break;
case "binaryString":
reader.readAsBinaryString(file);
break;
case "dataURL":
reader.readAsDataURL(file);
break;
}
});
});
});
}
为什么会发生这种情况,我该如何处理才能触发我的回调?
编辑
如前所述,我在 zone.js 存储库上打开了一个问题,zone.js 所有者迅速修补了代码。你可以通过在你的 polyfills 中导入 zone.js/zone-patch-cordova
来避免使用我的肮脏 hack 的痛苦。
原回答
在调试此代码时,我意识到 FileReader
构造函数已被 cordova 和 zone.js 修补。根据我对 zone.js 补丁的理解,它会将每个 "onProperty"(onload
、onloadend
、onerror
)更改为它的 addEventListener(...)
对应部分。
Module Name:
on_property
Behavior with zone.js :
target.onProp
will become zone awaretarget.addEventListener(prop)
但是cordova没有使用dispatchEvent(...)
API来通知监听器操作已经结束
一个解决方案可能是从 zone.js 停用 onProperty
模块,但 它可能会破坏 angular 的行为。
这就是我应对这种情况的方式:
read(file: FileEntry, mode: "text" | "arrayBuffer" | "binaryString" | "dataURL" = "text"): Observable<ProgressEvent> {
return this.cdv.ready.flatMap(() => {
return Observable.create(observer => {
file.file(file => {
let FileReader: new() => FileReader = ((window as any).FileReader as any).__zone_symbol__OriginalDelegate
let reader = new FileReader();
reader.onerror = (evt: ErrorEvent) => {
this.zone.run(() => observer.error(evt)); //never triggered
};
reader.onload = (evt: ProgressEvent) => {
this.zone.run(() => observer.next(evt)); //never trigerred
};
switch (mode) {
case "text":
reader.readAsText(file);
break;
case "arrayBuffer":
reader.readAsArrayBuffer(file);
break;
case "binaryString":
reader.readAsBinaryString(file);
break;
case "dataURL":
reader.readAsDataURL(file);
break;
}
});
});
});
}
这里的秘密在于zone.js保留了__zone_symbol__OriginalDelegate
属性中的原始构造函数,所以调用这个实际上会直接调用Cordova的FileReader
没有 zone.js 补丁.
这个解决方案是一个肮脏的黑客,我有 openned an issue on zone's repository
编辑:
与 FileWriter
有同样的问题(它在内部调用 FileReader
)所以我写了这个小垫片:
function noZonePatch(cb: () => void) {
const orig = FileReader;
const unpatched = ((window as any).FileReader as any).__zone_symbol__OriginalDelegate;
(window as any).FileReader = unpatched;
cb();
(window as any).FileReader = orig;
}
然后将我的调用包装到 read/write 操作中:
write(file: FileEntry, content: Blob) {
return this.cdv.ready.flatMap(() => {
return Observable.create((out: Observer<ProgressEvent>) => {
file.createWriter((writer) => {
noZonePatch(() => {
writer.onwrite = (evt: ProgressEvent) => {
this.zone.run(() => {
out.next(evt);
out.complete();
});
};
writer.onerror = (evt) => {
this.zone.run(() => out.error(evt));
};
writer.write(content); // this is where FileReader is called internally
})
}, err => out.error(err));
});
});
}
read(file: FileEntry, mode: ReadMode = "text"): Observable<ProgressEvent> {
return this.cdv.ready.switchMap(() => Observable.create((observer: Observer<ProgressEvent>) => {
file.file(file => {
noZonePatch(() => {
let reader = new FileReader();
reader.onerror = (evt: ErrorEvent) => {
this.zone.run(() => observer.error(evt));
};
reader.onload = (evt: ProgressEvent) => {
this.zone.run(() => observer.next(evt));
};
switch (mode) {
case "text":
reader.readAsText(file);
break;
case "arrayBuffer":
reader.readAsArrayBuffer(file);
break;
case "binaryString":
reader.readAsBinaryString(file);
break;
case "dataURL":
reader.readAsDataURL(file);
break;
}
});
});
}));
}
晚会有点晚了,但我不得不修复一个有这个确切问题的 Ionic3/Angular4 项目,我发现 FileReader
实例时,这是一种竞争条件。因为有时区域还没有修补 FileReader
window
对象所以找不到 __zone_symbol__OriginalDelegate
。
所以我总是得到正确的 class 是一个小工厂函数 returns 一个 FileReader
实例:
function HackFileReader(): FileReader {
const preZoneFileReader = ((window as any).FileReader as any).__zone_symbol__OriginalDelegate;
if (preZoneFileReader) {
console.log('%cHackFileReader: preZoneFileReader found creating new instance', 'font-size:3em; color: red');
return new preZoneFileReader();
} else {
console.log('%cHackFileReader: NO preZoneFileReader was found, returning regular File Reader', 'font-size:3em; color: red');
return new FileReader();
}
}
要使用它,请执行以下操作:
const reader = HackFileReader();
希望对大家有所帮助
如果您使用的是 ionic/cordova,则不需要 @disante 的 HackFileReader 解决方案(我实际使用过)
您需要做两件事,首先是确保您拥有最新的 zone.js。
npm install --save zone.js@latest
其次,您需要确保您的 index.html
添加 cordova.js
AFTER build/polyfills.js