【问题标题】:How to store/stash JavaScript event and reuse it later?如何存储/隐藏 JavaScript 事件并在以后重用它?
【发布时间】:2018-10-31 20:39:49
【问题描述】:

来自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;
});

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

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

我尝试了对对象进行序列化/反序列化的本地存储:

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

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

【问题讨论】:

  • 如果您想保留方法,您需要避免序列化(这意味着没有持久性)或反序列化为原始类型的新实例。
  • 后面触发是什么意思?在不同的页面上?
  • @EliottRobson youtube.com/watch?v=NITk4kXMQDw 14:28 - 15:17
  • 描述你想要什么,不要指望我们坐着看视频。
  • @deceze 我指定了时间跨度 14:28 - 15:17,不到 1 分钟。

标签: javascript events local-storage web-storage


【解决方案1】:

自从 I/O 2018 提到从现在开始由开发人员驱动的 A2HS 事件处理时,已经有大量讨论。这也在official doc 中得到了体现,并从中得到了启发,beautiful article 彻底解释了如何准确地实现这种情况。虽然我建议您阅读完整的文章以正确理解围绕 A2HS 流程的更新动态,但请随时根据您的要求跳到“新添加到主屏幕流程”部分。

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

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

文章有完整的代码sn-ps,可以参考/复用。

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

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

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

【讨论】:

  • 您能否详细说明“进程中缓存”?那是什么?我如何谷歌如何使用它?
  • 嗨@LimonMonte,对于进程内缓存,您可以尝试使用像地图(developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/…)这样简单的东西。您还可以探索 immutable-js (facebook.github.io/immutable-js)。
  • 这个答案不正确,我投了反对票。这是当前投票最多的,因为它是第一个。
  • @LimonMonte - 很高兴您通过其他建议使事情顺利进行。关于你最后的评论 - “答案不正确”:这里没有什么不正确的......也许说“找到更好的推荐”是正确的说法...... :)“这是当前最受好评的,因为它是第一个。” - 不知道人们会根据时间戳对答案进行投票。猜猜其他用户/专家一定在这个答案中找到了一些价值见解,因此会投赞成票。至少我是这样做的。没关系,为你的工作顺利进行而欢呼。万事如意!
  • 您建议“将事件对象存储在 IndexedDB 中”,这是一种误导,我浪费了一些时间,直到我意识到在这种情况下存储类型并不重要。
【解决方案2】:

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

问题:如何存储任何 javascript 对象及其方法?

答案:没有办法。

但是,

Josh 正确地explained 你可以从你的事件中提取和存储所有可序列化的属性,比如数据。

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

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

您错过/需要的是EventTargetevent.target 属性的值, reference 在 document.body 永远卸载或序列化事件 Object 时永远丢失。

但如果它仍然存在,或者如果您知道 event.target 应该是什么,例如 DOM 元素或任何对象您可以引用,无论您从哪里重新创建事件(在哪里?),只需将您的事件发送到该对象,如果它侦听该event.type, 至少应该听到您的全新活动。

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

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

【讨论】:

  • 我不确定我应该接受哪个答案——这个答案还是 Josh 的答案或 cegfault 的答案——所有答案都是正确且解释清楚的。这个对我来说是最容易理解的,非常感谢@lucchi!
【解决方案3】:

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

  1. 将事件的reference存储在一个全局值中(这是大多数教程 - 例如您引用的 youtube 视频 - 会建议您这样做)。这要求事件在与您存储引用时相同的上下文(即网页)中运行
  2. 当您将事件的引用存储在 localStorage 中(例如通过名称或键/值查找)时,在要执行事件的页面/上下文中,确保执行事件之前加载适当的函数和库
  3. [强烈不推荐] 将 javascript 源代码存储在您的存储中,稍后 eval() [再次重申,请不要这样做]

正如@Josh 和@SaurabhRajpal 所提到的,严格来说,您所要求的在JavaScript 中是不可能的。正如MDN documentation for JSON.stringify 所说,您对JSON.stringify(e) 所做的操作可能会返回undefinednull

如果在转换过程中遇到未定义、函数或符号,则将其省略(当在对象中找到时)或审查为 null(当在数组中找到时)。 JSON.stringify 也可以在传递“纯”值时返回 undefined,例如 JSON.stringify(function(){}) 或 JSON.stringify(undefined)。

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

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

function bar() {
    return foo()
}

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

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

那么你有什么选择?首先,在同一页面上执行所有操作,并将对事件的引用存储在全局值中(您在评论中链接到的 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)
}

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

恕我直言,我认为您试图避免在以后的页面/应用程序/任何内容中包含库或源代码,而 javascript 就是无法以这种方式工作。最好在内存中传递引用,并且只对名称使用键/值存储。其他一切都是一种编码体操,以避免将源代码简单地包含在不需要包含的地方。

【讨论】:

  • 非常感谢@cegfault 的回答,这很有帮助。我不知道该接受哪一个——你的还是 lucchi 的。我接受了 lucchi 的回答可能是因为我喜欢简短的参考答案:)
【解决方案4】:

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

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

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

window.setInterval(someOtherMethod, 5000);

在最后一个窗口5秒后尝试点击,5秒后检查

【讨论】:

  • 在 JavaScript 中,任何全局变量实际上都是窗口对象的一个​​属性。使用一个等同于(并且可以互换)使用另一个。此外,这只会在同一页面上持续存在。一旦用户移动到另一个页面,这个事件就会丢失。您可以使用 window.name 来保存信息,但它只存储字符串值,从而违背了这里的目的。此外,它也只有在使用相同的窗口/选项卡时才有效。
  • 谢谢 saurabh .. 我不是已经在答案中提到了这些作为预防措施吗?
  • 除了localStorage(存储为字符串)之外,我们还有其他方式可以跨标签进行通信。最好的方法是创建一个 API 来持久化事件并在新选项卡中启动应用程序时获取,这始终是一个选择。
  • 对于您的第一点,您确实提到它不能跨标签工作。我补充说的是,如果用户从页面内导航到其他链接,它甚至不会在同一个标​​签中工作.. 就像从页面 A 中单击页面 B 超链接 .. 现在到第二点,我知道没有其他方法.. 拥有一个 API 并将其存储在您的后端始终是一种选择,但这不是他的选择想要..此外,在从后端存储/检索时,您需要再次序列化/去实现..
【解决方案5】:

请看看这是否有帮助。

为 'beforeinstallprompt' 事件定义事件监听器

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

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

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

在控制台中输出“事件触发”。

【讨论】:

    【解决方案6】:

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

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

    同样的原因,当你序列化为一个 json 字符串时,它会丢弃函数。

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

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

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

    【讨论】:

    • 非常感谢@Josh 的回答,这很有帮助。但我更喜欢 lucchi 的回答,因为它有 TL;DR 和链接 :)
    猜你喜欢
    • 2021-12-10
    • 1970-01-01
    • 2020-10-21
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-04-06
    相关资源
    最近更新 更多