【译】Cordova vs Zone.js 为什么Angular的document event listener不在zone内?

467 阅读3分钟

前言

在调研基于Cordova打包Angular的iPad应用有时无法自动触发变更检测的问题时,发现的一篇文章,希望对你有帮助 :)

先说结论

原因:Cordova由于重写window和document事件时破坏了zone对该事件触发时的猴子补丁。

  • zone的猴子补丁是基于EventTarget原型修改的(window和document都继承于EventTarget);而Cordova将window和document重写赋值新的function了,破坏了继承性。使得Cordova打包的app在触发相关事件时无法通过zone触发变更检测。
  • 使用Cordova打包后,Cordova的补丁是在Zone.js之前引入,使得Zone.js的猴子补丁不起作用

解决方法

在Angular项目的index.html中引入Cordova前手动添加如下补丁:

<script>
  window.addEventListener = function () {
    EventTarget.prototype.addEventListener.apply(this, arguments);
  };
  window.removeEventListener = function () {
    EventTarget.prototype.removeEventListener.apply(this, arguments);
  };
  document.addEventListener = function () {
    EventTarget.prototype.addEventListener.apply(this, arguments);
  };
  document.removeEventListener = function () {
    EventTarget.prototype.removeEventListener.apply(this, arguments);
  };
</script>
// 然后引入Cordova,例如 <script src="cordova.js"></script>

翻译正文

翻译自2017.2.28发布的文章 Cordova vs. Zone.js or "Why is Angular's document event listener not in a zone?

If you put an Angular Single Page Application into a Cordova wrapped application any "document:*" host listener (e.g. "document:click") won't be triggered with a zone update. This means Angular is not running the change detection to see if there is any update to be processed in the view etc.

如果将 Angular 单页应用程序放入 Cordova 打包的应用程序中,"document:*"host监听(例如"document:click")事件不会通过zone触发任何更新。这意味着 Angular 没有运行变更检测来查看视图中是否有任何要处理的更新等。

The reason for this is that Cordova is monkey-patching window.addEventListener and document.addEventListener. But wait! Isn't Zone.js also monkey-patching these, too? Yes, indeed. But this is done on EventTarget's prototype directly. Cordova however is assigning a new function to those methods. And here comes the problematic part:

原因是Cordova通过猴子补丁重写了window.addEventListenerdocument.addEventListener。但是等等!Zone.js不也是写了这些事件的猴子补丁吗?的确,但Zone.js是直接在EventTarget的原型上完成的。然而,Cordova将这些方法重新赋值了一个新的function。这是问题所在:

This is breaking the prototype inheritance. Zone.js is not involved anymore! Now, any event listener added won't run in a zone (and trigger the change detection).

重新赋值破坏了原型继承,于是Zone.js不能再通过原型继承起作用!现在,添加的任何事件监听都不会在zone中运行(并触发变更检测)。

But what could you do? Maybe call run of NgZone every time in your callback? This would trigger a useless global change detection run in a non-Cordova environment. Fairly not the best idea. Could we just some kind of restore the previous prototype inheritance way? Yes we can! But we need to break it by ourself first. ;)

但是你能做些什么呢?也许每次在回调中调用NgZone?这将触发在非 Cordova 环境中运行无用的全局变更检测。这不是最好的主意。我们能不能某种方式"恢复"以前的原型继承方式?我们可以!但我们需要先自己打破它。;)

(function () {
  'use strict';

  window.addEventListener = function () {
    EventTarget.prototype.addEventListener.apply(this, arguments);
  };

  window.removeEventListener = function () {
    EventTarget.prototype.removeEventListener.apply(this, arguments);
  };

  document.addEventListener = function () {
    EventTarget.prototype.addEventListener.apply(this, arguments);
  };

  document.removeEventListener = function () {
    EventTarget.prototype.removeEventListener.apply(this, arguments);
  };
})();

That's all? Yes! But what is happening here? Just some fancy JavaScript magic. When these methods get called, the method of the EventTarget prototype will be called right in the new implementation. The this context will be switched to window and all provided arguments will be forwarded. And the secret is that this needs to be done before cordova.js is executed. In that case Cordova is monkey-patching our implementation which is redirecting the call to the prototype  (later monkey-patched by Zone.js).

就这样?是的!但这里发生了什么?只是一些JavaScript魔术。当调用这些方法时,将在新实现中直接调用 EventTarget 原型的方法。this 上下文将切换到 window,并且将转发所有提供的参数。秘密在于,这需要在执行cordova.js之前完成。在这种情况下,即使Cordova对我们的实现进行猴子补丁,该实现将调用重定向到原型(后来由Zone.js猴子补丁的部分)。

And there is the zone again!

然后zone又开始运作啦!

其他参考资料

MDN EventTarget: 了解EventTarget与window,document之间的关系

Github # bug: Angular application (Zone.js) + cordova.js file:Ionic在安卓和IOS上也有类似的问题

Github # Bug router angular v5.2.4+ with cordova (BrowserAnimationsModule bug): 安卓上退后按钮引发app崩溃