如何 store/stash JavaScript 事件并稍后重用?

How to store/stash JavaScript event and reuse it later?

来自https://developers.google.com/web/fundamentals/app-install-banners/#trigger-m68

let deferredPrompt;

window.addEventListener('beforeinstallprompt', (e) => {
  e.preventDefault();
  // Stash the event so it can be triggered later.
  deferredPrompt = e;
});

这段代码很好,但我想稍后在不同的地方触发隐藏的事件。为此,我不仅需要将事件存储在变量中,还需要存储在其他地方。

问题:如何使用事件的方法存储事件?

我尝试使用 serialization/deserialization 个对象的本地存储:

> localStorage.setItem('stashed-event', JSON.stringify(e))
>
> JSON.parse(localStorage.getItem('stashed-event'))

但是这种方法没有按预期工作,因为它只存储键值并丢失所有事件方法。

自从 I/O 2018 年提到从现在开始处理由开发人员驱动的 A2HS 事件后,就有大量关于此的讨论。这也包含在 official doc and inspired from it, there is a beautiful article 中,彻底解释了如何准确地实现这种情况。虽然我建议阅读完整的文章以正确理解围绕 A2HS 流程的更新动态,但请随时跳到 "The New Add To Homescreen Flow" 部分以满足您的要求。

简而言之,请按照以下步骤操作:

  1. beforeinstallprompt 事件处理程序的范围之外创建一个变量。
  2. 在上述处理程序中保存对 beforeinstallprompt 事件对象的引用。
  3. 稍后使用它按需触发添加到主屏幕提示。

本文有完整的代码片段,您可以refer/reuse。

编辑:我再次阅读了您的问题并意识到您可能特别关注的一个重要方面,即使用它 "somewhere else"。如果这意味着您指的是在不同的页面上使用它,那么我的建议是将事件对象存储在:

  1. IndexedDB 是 "object stores" 的集合,您可以将对象放入其中。 缺点 - 可能有浏览器兼容性限制。此外,可能会导致大量嵌套回调。
  2. 或者您可以选择使用不需要序列化的 "in process cache"(您的应用程序的堆内存)。 缺点 - 虽然这不能在多个服务器之间共享。

除此之外,我目前无法预见到一个免费的解决方案。但会尝试弄清楚并可能更新线程。

您不能以这种方式存储事件。你想存储一个对象。对于这样的对象,只有可序列化的属性是可存储的。 JavaScript 中的函数不可序列化。函数在许多语言中都不可序列化。

从根本上说,这基本上是因为当你反序列化一个对象时,它的签名可能会改变。如果您曾经在 java 中编程过,这类似于在读取序列化对象并尝试重建对象时出现反序列化错误。因为对象的方法函数的主体可以在对象写入某个存储和稍后读取之间发生变化,所以方法不可序列化。这是因为当您序列化一个对象时,它不会在定义方法的地方序列化它的接口定义。它只是存储数据。

序列化为 json 字符串时的相同原因,它会删除函数。

不是存储事件,而是将事件中的有用信息存储在对象中(或者让 stringify 隐式删除并直接使用事件)。

您使用哪种存储方法取决于您的问题中未提及的内容。例如它应该存储多长时间,它是否应该在您的网站来源之外可用,通常会存储多少数据,是否要存储多个对象等。根据您问题中提供的有限信息,您可能只使用 localStorage 或内存数组就可以了。

如果您发现需要存储数百个对象,那么 indexedDB 将开始更加合适。但是仅仅选择不同的存储介质对是否可以存储函数没有任何影响。您不能存储函数。

请看看这是否有帮助。

正在为 'beforeinstallprompt' 事件定义事件侦听器

window.addEventListener('beforeinstallprompt', (e) => {
  e.preventDefault();
  //do all the stufff
  console.log('Event triggered');
});

当您想要手动调度事件时。 创建一个新的事件变量。

var myevent = new Event('beforeinstallprompt');
window.dispatchEvent(myevent);

在控制台中输出 'Event triggered'。

您可以创建一个全局常量并在事件发生变化时更新它,而不是对其进行序列化和反序列化,这是一个代价高昂的过程。所以这就是你可以做到的 - 你可以创建一个 window 实例并在 window 对象中克隆事件,这样它就不会发生变化。(注意这不会跨选项卡工作)

window.addEventListener('click', (e) => {
  e.preventDefault();
  window.deferredPrompt = Object.assign(e);//Don't mutate
});

let someOtherMethod = ()=>{
  console.log(window.deferredPrompt)
}

window.setInterval(someOtherMethod, 5000);

尝试在最后 window 5 秒后点击并在 5 秒后检查

TL;DR 要完成您正在做的事情,您有三个选择:

  1. 在全局值中存储对事件的 引用(这是大多数教程(如您引用的 YouTube 视频)会建议您这样做)。这需要事件 运行 在与存储引用时相同的上下文(即网页)中
  2. 当您将 reference 存储到 localStorage 中的事件时(例如按名称或 key/value 查找),在 page/context要执行事件,请确保在执行事件
  3. 之前加载了适当的函数和库
  4. [强烈不推荐]将 javascript 源代码存储在您的 storate 中,稍后对其进行 eval() [再次强调,请不要这样做]

正如@Josh 和@SaurabhRajpal 所提到的,严格来说,您所要求的在 JavaScript 中是不可能的。你用 JSON.stringify(e) 做的事情可能会 return undefinednull,正如 MDN documentation for JSON.stringify 所说:

If undefined, a Function, or a Symbol is encountered during conversion it is either omitted (when it is found in an object) or censored to null (when it is found in an array). JSON.stringify can also just return undefined when passing in "pure" values like JSON.stringify(function(){}) or JSON.stringify(undefined).

简而言之,无法将单个函数存储到 localStorage(或任何其他离线存储)。要解释为什么这是不可能的,请看这个例子:

function foo() {
    console.log("a")
}

function bar() {
    return foo()
}

如何存储 bar() 供以后使用?为了存储 bar,您 必须存储 foo()。当您考虑引用一个位于或使用大型库(如 jQuery、下划线、D3、图表库等)的函数时,这会变得更加复杂。请记住 您的计算机已经将源代码解析为二进制文件,因此不会轻易知道如何读取每个可能的 if、for 和 switch 语句的函数以确保所有可能的相关函数和库都已保存。

如果您真的想要这样做,您将不得不编写自己的javascript解析器,而您真的不想那样做!

那么你有什么选择?首先,在同一页面上执行所有操作,并将对事件的引用存储在全局值中(您 link 在评论中看到的 youtube 视频正在使用此方法)。

您的第二个选择是对事件(而不是事件本身)使用 引用,并确保稍后使用该引用的源代码。对于 (html) 示例:

// on page #1
<script src="path/to/my/js/library.js"></script>
...
<script>
    window.addEventListener('beforeinstallprompt', (e) => {
        e.preventDefault()
        localStorage.setItem('stashed-event', 'before-install')
    })
</script>

// later, on page #2:
<script src="path/to/my/js/library.js"></script>
...
<script>
    var evt = localStorage.setItem('stashed-event', 'before-install')
    if(evt == 'before-install') {
        dosomething() // which would be a function in path/to/my/js/library.js
    }
    // another option here would be to define window listeners for all possible events
    // in your library.js file, and then simply build and trigger the event here. for
    // more about this, see: this link:
    // https://developer.mozilla.org/en-US/docs/Web/Guide/Events/Creating_and_triggering_events
</script>

最后,您 可以 存储 javascript 源代码,然后再对它进行 eval()。拜托,拜托,请不要这样做。这是不好的做法,会导致非常邪恶的事情。但是,如果你坚持:

// on page #1
window.addEventListener('beforeinstallprompt', (e) => {
    e.preventDefault()
    SomeAjaxFunction("path/to/my/js/library.js", function(responseText) {
        localStorage.setItem('stashed-event', {
            name: 'before-install',
            call: 'beforeInstallFunction()',
            src:  responseText
        })
    })
})

// later, on page #2:
var evt = localStorage.setItem('stashed-event', 'before-install')
if(evt) {
    console.log("triggering event " + evt.name)
    eval(evt.src)
    eval(evt.call)
}

就像我说的,这是一个非常糟糕的主意,但这是一个选择。

恕我直言,我认为您试图避免在以后的 page/app/whatever 中包含库或源代码,而 javascript 只是行不通。最好在内存中传递引用,并且只使用 key/value 存储名称。其他一切都是一种编码体操,以避免简单地将您的源代码包含在需要包含的地方。

看了几次你的问题,又看了几次答案,

问题:如何将任何 javascript 对象与其方法一起存储?

答案:没有办法。

不过,

Josh properly 您可以从您的事件中提取并存储所有可序列化的属性,例如数据。

我会添加你 可以创建事件 以某种方式 相同的数据 稍后在任何地方,这个新事件都将拥有任何 Event 拥有的所有方法,但现在可能 none 使用。

显然,即使在您的数据中序列化,timeStamp, isTrusted 等属性也会在创建新事件时被覆盖。

你只是错过/需要的是一个EventTargetevent.target的价值属性, reference 在 document.body 永远卸载或序列化事件对象时永远丢失。

但是如果它还活着,或者如果你知道 event.target 应该是什么,比如 DOM 元素或任何对象 你可以参考 ,从你重新创建事件的任何地方(在哪里?),只需将你的事件发送给那个对象,如果它监听那个 event.type, 你的全新活动至少应该被听到。

来自 MDN 的简单示例 EventTarget,或参见 EventTarget.dispatchEvent

作为对 by cegfault 的评论:eval,文本源代码...可以是 ... 如果您的脚本生成一个 (String) 脚本。如果不是,您最好进一步回溯到您的脚本在哪里创建了出现在您的事件中的不可序列化的东西,并考虑重新创建这些东西,而不是事件。

这是一个简单但成功的解决方案。

我们的想法是在一个变量中捕获事件,并且仅在来自同一来源(域等)的另一个 window 发出信号时触发它。

该解决方案使用 localStorage 方法作为信号量。

这是我使用的代码。我已经在 Chrome 中成功测试了它,包括移动设备和桌面设备。

//事件处理中window

//注册ServiceWorker

if('serviceWorker' in navigator) {
  navigator.serviceWorker.register('sw.js');
};

//捕获beforeInstall事件

window.addEventListener('beforeinstallprompt', function(event){
  event.preventDefault();
  window.deferredPrompt = event;
  return false;
})

//等待信号

window.onstorage = event => {
  if (event.key === 'installprompt') {
    //Fire the event when signaled.
    window.deferredPrompt.prompt();
    // Discard event
    window.deferredPrompt = null;
    //Discard storage item
    localStorage.removeItem('installprompt');
  } 
}

//在不同的 window 或同一来源的选项卡中准备就绪时触发事件。

localStorage.setItem('installprompt', 'whatever');