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"(onloadonloadendonerror)更改为它的 addEventListener(...) 对应部分。

Module Name:

on_property

Behavior with zone.js :

target.onProp will become zone aware target.addEventListener(prop)

source

但是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