# 为每次页面更改装饰CookedElement？

**URL:** <https://meta.discourse.org/t/decoratecookedelement-for-each-page-change/214218>\
**Category:** Development\
**Created:** [2022年一月8日 04:54 UTC](https://meta.discourse.org/t/decoratecookedelement-for-each-page-change/214218 "2022-01-08T04:54:17Z")\
**Posts on this page:** 6\
**Page:** 1

<div class="post-metadata">

**Author:** ![jimkleiber](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/jimkleiber/32/121814_2.png) [@jimkleiber](https://meta.discourse.org/u/jimkleiber)\
**Post date:** [2022年一月8日 04:54 UTC](https://meta.discourse.org/t/decoratecookedelement-for-each-page-change/214218/1 "2022-01-08T04:54:17Z")

</div>

我正在尝试使用 Mediaelement.js 库来覆盖自定义音频 onebox，并且在页面加载/刷新时它能正常工作。但是，当我在不同主题之间导航时，它似乎没有重新加载，而是显示了原生的 Discourse 音频 onebox。

加载/刷新时：

 ![image](https://global.discourse-cdn.com/meta/original/3X/c/d/cd90a2e43ceb19d2e8e3757c29607679b5d5b9c2.png)

如果我然后导航到另一个主题：

 ![image](https://global.discourse-cdn.com/meta/original/3X/0/d/0d66cf09db9427ee42139408e71dce8b0afa89f1.png)

我很确定我在 [mejs 加载](https://github.com/mediaelement/mediaelement/blob/d8a667fd65560093fb227700a82805600ec9e8e3/docs/usage.md) 方面做错了什么，但又觉得可能不是，并且我必须对 onPageChange 做一些花哨的事情。

**`<html>`**

```javascript
<script type="text/discourse-plugin" version="0.8.42">

let loadScript = require('discourse/lib/load-script').default;

    api.decorateCookedElement(
      element => {
        const audioplayers = element.querySelectorAll('audio');
        // console.log("player: " + audioplayers[0]);
        if (Object.entries(audioplayers).length > 0) {
            // console.log("audioplayers has length");
            loadScript(
                `https://cdnjs.cloudflare.com/ajax/libs/mediaelement/5.0.4/mediaelement.min.js`
            );
            loadScript(
                `https://cdnjs.cloudflare.com/ajax/libs/mediaelement/5.0.4/mediaelement-and-player.min.js`
            );
        }

        audioplayers.forEach(function (el) {
            el.classList.add("mejs__player");
            const controls = settings.theme_uploads.mejs-controls;
            // console.log("controls: " + controls);
            el.setAttribute("data-mejsoptions",
                `{
                    "pluginPath": "https://cdnjs.cloudflare.com/ajax/libs/mediaelement/5.0.4/",
                    "iconSprite": "https://friends.jimkleiber.com/uploads/default/original/1X/a17d9708a19654d9155dd9b79a79a05dea580067.svg",
                    "alwaysShowControls": "true",
                    "features": ["playpause", "current", "progress", "duration", "volume"]
                }`
            );
            el.setAttribute("preload", "auto");            
        });            
      },
      { id: "mediaelement-js", onlyStream: true}
    );
</script>

```

**`css`**

```css
@import "https://cdnjs.cloudflare.com/ajax/libs/mediaelement/5.0.4/mediaelementplayer.min.css";

```

 ![image](https://global.discourse-cdn.com/meta/original/3X/7/8/789e8bd4152f6fcb19c81b4ae8ebaf2417a27307.png)

有人有什么建议，如何调整才能让它在每次页面加载时自动重新加载吗？

---

<div class="post-metadata">

**Author:** ![Johani](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/johani/32/176920_2.png) [@Johani](https://meta.discourse.org/u/Johani)\
**Post date:** [2022年一月11日 18:58 UTC](https://meta.discourse.org/t/decoratecookedelement-for-each-page-change/214218/2 "2022-01-11T18:58:07Z")

</div>

由于你在 #Development 频道发帖，这篇回复可能会稍长一些。不过，内容非常通用，主要是在解释 Discourse 中应遵循的模式，而不论你打算集成哪个库，或者想要针对哪些帖子元素。恰好，这里以你选择的脚本作为主要示例。

MediaElement 提供了两种初始化新音频播放器的方式：

1. 你可以给元素添加一个 CSS 类和一些属性，它会自动为你处理。
2. 你可以手动初始化它。

你目前使用的是第一种方式，所以我们先来看看这种方式。当某个脚本提供“元素自动初始化”功能时，这通常是脚本作者为了提升用户体验而添加的便利功能。在脚本内部，作者通常会监听文档加载事件（document load event），并对那些具有指定类名或属性的 DOM 元素执行相应操作。

那么，为什么它不工作呢？正如你所见，它在初次页面加载时运行正常，但在随后的应用内导航中却失效了。这是怎么回事？

简短的回答是：Discourse 是一个单页应用（Single-Page Application）。像 `<HTML>` 和 `<body>` 这样的标签只会被发送一次。因此，从某种意义上说，文档只加载了一次。所以，当你在初始页面加载后进行导航时，不会再有原生的“加载”事件被触发。请记住，文档在初次页面查看时已经加载完成。此后的所有操作都由 Discourse 自行处理。

当然，这并不意味着在后续导航中没有任何事件被触发。然而，这些是 Discourse 特有的事件。因此，第三方脚本作者无法提前知晓这些事件。想象一下，如果你是一个脚本作者，需要适配成百上千个不同的平台，那会怎样？这显然不可行，对吧？

所以，我们无法使用脚本作者精心添加的那种“便利功能”。接下来该怎么办？别忘了，我们仍然可以手动在目标元素上初始化脚本。让我们试试这个方法。

我之前提到过，浏览器层面只有一个原生的“加载”事件，但像 Discourse 这样的平台如果没有自己的事件系统，就无法良好运行。例如，插件 API 提供了一个方法，可以[在虚拟页面导航时触发脚本](https://github.com/discourse/discourse/blob/main/app/assets/javascripts/discourse/app/lib/plugin-api.js#L614-L622)。

你应该使用这个方法吗？不。这个方法非常适合用于分析统计等场景。但对于只处理 `<audio>` 标签的脚本来说，在每次页面加载时都触发它并没有意义——尤其是当页面上根本没有这些标签时。

那么，下一步是什么？好消息是，你已经找到了答案。[decorateCookedElement](https://github.com/discourse/discourse/blob/main/app/assets/javascripts/discourse/app/lib/plugin-api.js#L301-L323) 就是这里应该使用的正确方法。

它提供了一种方式……等等……来装饰帖子：🎉

Discourse **保证** 你添加的任何装饰器都会应用于每一个帖子。

既然你是在帖子装饰器中加载脚本，那么它应该会被添加并正常工作。那为什么在后续导航中却不生效呢？

要理解这一点，你需要了解 `loadScript()` 的工作原理。你的代码在加载脚本之前已经检查了是否存在有效的目标元素，这点做得很好：+1

但是，想象一下这样一种情况：你有 20 到 30 个连续的帖子，每个帖子都包含有效的元素。在这种情况下，将脚本加载 20 到 30 次有意义吗？显然没有。

`loadScript()` 足够智能，可以检测脚本是否已经加载。它不会重复加载，如果脚本已经下载完成，也不会重新加载。你可以在这里看到相关代码：

> <https://github.com/discourse/discourse/blob/1472e47aae5bfdfb6fd9abfe89beb186c751f514/app/assets/javascripts/discourse/app/lib/load-script.js#L60-L74>

上面的 `fullUrl` 就是你调用 `loadScript()` 时传入的 URL，就像你的示例中那样。

现在，既然我们知道了这一点，就能大致理解为什么它在后续导航中不工作了。

```plaintext
你访问主题 A → 它包含一个音频元素 → loadScript() 加载脚本 → 脚本执行“自动初始化”操作 → 脚本在你的元素上初始化 → 你获得自定义音频播放器

然后……

你访问主题 B → 它也包含音频元素 → loadScript() 发现脚本已加载 → 不再执行“自动初始化” → 你得到默认的音频元素 → 于是问题出现了

```

那么，如何解决这个问题呢？这正是前面提到的第二种方法的作用所在：

> 1. 你可以给元素添加一个 CSS 类和一些属性，它会自动为你处理。
> 2. 你可以手动初始化它。

让我们采用第二种方法。这在[你分享的页面](https://github.com/mediaelement/mediaelement/blob/d8a667fd65560093fb227700a82805600ec9e8e3/docs/usage.md)上已有文档说明。我们需要像这样在我们的目标元素上调用它：

```javascript
// 你可以使用字符串形式的播放器 ID（例如 `player`），
// 或者使用 `document.querySelector()` 来选择任意元素
var player = new MediaElementPlayer("player", {
  // ... 选项
});

```

你的代码已经分别处理了每个音频元素：+1，所以我们只需要修改这部分：

```javascript
audioplayers.forEach(function (el) {
  el.classList.add("mejs__player");
  const controls = settings.theme_uploads.mejs - controls;
  // console.log("controls: " + controls);
  el.setAttribute(
    "data-mejsoptions",
    `{
                    "pluginPath": "https://cdnjs.cloudflare.com/ajax/libs/mediaelement/5.0.4/",
                    "iconSprite": "https://friends.jimkleiber.com/uploads/default/original/1X/a17d9708a19654d9155dd9b79a79a05dea580067.svg",
                    "alwaysShowControls": "true",
                    "features": ["playpause", "current", "progress", "duration", "volume"]
                }`
  );
  el.setAttribute("preload", "auto");
});

```

改为如下：

```diff
audioplayers.forEach(function (el) {
-- el.classList.add("mejs__player");
-- const controls = settings.theme_uploads.mejs - controls;
-- // console.log("controls: " + controls);
-- el.setAttribute(
-- "data-mejsoptions",
-- `{
-- "pluginPath": "https://cdnjs.cloudflare.com/ajax/libs/mediaelement/5.0.4/",
-- "iconSprite": "https://friends.jimkleiber.com/uploads/default/original/1X/a17d9708a19654d9155dd9b79a79a05dea580067.svg",
-- "alwaysShowControls": "true",
-- "features": ["playpause", "current", "progress", "duration", "volume"]
-- }`
-- );
-- el.setAttribute("preload", "auto");
++ 
++ new MediaElementPlayer(el, {
++ // ... 选项
++ });
});

```

然后，我们将之前作为属性添加的选项作为对象传递，如下所示：

```diff
audioplayers.forEach(function (el) {
 new MediaElement(el, {
++ pluginPath: "//cdnjs.cloudflare.com/ajax/libs/mediaelement/5.0.4/",
++ iconSprite: settings.theme_uploads["mejs-controls"],
++ alwaysShowControls: "true",
++ features: ["playpause", "current", "progress", "duration", "volume"]
 });
});

```

让我们暂时放下这部分，先看看装饰器的其余部分。目前我们已有如下代码：

```javascript
let loadScript = require("discourse/lib/load-script").default;

api.decorateCookedElement(
  element => {
    const audioplayers = element.querySelectorAll("audio");
    // console.log("player: " + audioplayers[0]);
    if (Object.entries(audioplayers).length > 0) {
      // console.log("audioplayers has length");
      loadScript(
        `https://cdnjs.cloudflare.com/ajax/libs/mediaelement/5.0.4/mediaelement.min.js`
      );
      loadScript(
        `https://cdnjs.cloudflare.com/ajax/libs/mediaelement/5.0.4/mediaelement-and-player.min.js`
      );
    }

    // forEach 曾经在这里
  },
  { id: "mediaelement-js", onlyStream: true }
);

```

如果你注意到，你调用了两个不同的 `loadScript()`。我不确定这是否是有意为之，但实际上你只需要其中一个。可以将其视为完整包和轻量级包。你需要的是自定义音频播放器，因此需要完整包。让我们移除另一个。

```diff
let loadScript = require("discourse/lib/load-script").default;

api.decorateCookedElement(
  element => {
    const audioplayers = element.querySelectorAll("audio");
    // console.log("player: " + audioplayers[0]);
    if (Object.entries(audioplayers).length > 0) {
      // console.log("audioplayers has length");
-- loadScript(
-- `https://cdnjs.cloudflare.com/ajax/libs/mediaelement/5.0.4/mediaelement.min.js`
-- );
      loadScript(
        `https://cdnjs.cloudflare.com/ajax/libs/mediaelement/5.0.4/mediaelement-and-player.min.js`
      );
    }

    // forEach 曾经在这里
  },
  { id: "mediaelement-js", onlyStream: true }
);

```

你正在检查帖子中是否存在音频播放器，并据此有条件地加载脚本。这可以简化如下。首先，直接检查长度：

```diff
let loadScript = require("discourse/lib/load-script").default;

api.decorateCookedElement(
  element => {
    const audioplayers = element.querySelectorAll("audio");
    // console.log("player: " + audioplayers[0]);
-- if (Object.entries(audioplayers).length > 0) {
++ if (audioplayers.length) {
      // console.log("audioplayers has length");

      loadScript(
        `https://cdnjs.cloudflare.com/ajax/libs/mediaelement/5.0.4/mediaelement-and-player.min.js`
      );
    }

    // forEach 曾经在这里
  },
  { id: "mediaelement-js", onlyStream: true }
);

```

然后将其移到顶部，如果长度为假（length \< 0），直接 `return`。我还移除了代码中的注释：

```diff
let loadScript = require("discourse/lib/load-script").default;

api.decorateCookedElement(
  element => {
    const audioplayers = element.querySelectorAll("audio");

++ if (!audioplayers.length) {
++ return;
++ }

    loadScript(
      `https://cdnjs.cloudflare.com/ajax/libs/mediaelement/5.0.4/mediaelement-and-player.min.js`
    );

    // forEach 曾经在这里
  },
  { id: "mediaelement-js", onlyStream: true }
);

```

由于脚本的 `src` 永远不会改变，让我们将其设为一个 `const`。`loadScript()` 调用也始终相同，我们也可以将其设为 `const`。

```diff
++ const loadScript = require("discourse/lib/load-script").default;

++ const MEDIA_ELEMENT_SRC =
++ "https://cdnjs.cloudflare.com/ajax/libs/mediaelement/5.0.4/mediaelement-and-player.min.js";

api.decorateCookedElement(
  element => {
    const audioplayers = element.querySelectorAll("audio");

    if (!audioplayers.length) {
      return;
    }

++ loadScript(MEDIA_ELEMENT_SRC);

    // forEach 曾经在这里
  },
  { id: "mediaelement-js", onlyStream: true }
);

```

让我们暂时先搁置这部分。在继续之前，我们需要再深入了解一下 `loadScript()` 的工作原理。

如果你想使用脚本的某部分，你肯定希望确保它在执行任何操作之前已经加载完成，对吧？`loadScript()` 正是为你处理这一点。它返回一个 [Promise](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Promise)。Promise 听起来可能有点吓人，但其实很简单。Promise 字面上就是……_一个承诺_。

你想做一些工作……你向浏览器承诺，当工作完成后会通知它……浏览器会等待你。就是这么简单。剩下的只是理解语法。

我不会花太多时间在这上面，因为这稍微超出了本话题的范围。

让我们继续。`loadScript()` 是基于 Promise 的。Discourse 向浏览器承诺，当脚本完全加载后会通知它——无论脚本是否不存在需要加载，还是仅仅检查它是否已经加载。

所以，如果我们这样做：

```diff
const loadScript = require("discourse/lib/load-script").default;

const MEDIA_ELEMENT_SRC =
  "https://cdnjs.cloudflare.com/ajax/libs/mediaelement/5.0.4/mediaelement-and-player.min.js";

api.decorateCookedElement(
  element => {
    const audioplayers = element.querySelectorAll("audio");

    if (!audioplayers.length) {
      return;
    }

++ loadScript(MEDIA_ELEMENT_SRC).then(() => {
++ // 这只有在脚本已加载或正在加载时才会执行
++ console.log("my script has loaded");
++ });

    // forEach 曾经在这里
  },
  { id: "mediaelement-js", onlyStream: true }
);

```

现在，我们可以回到之前的 `forEach` 循环，并将其直接放在这里，这样我们就能够确保脚本是可用的。

```diff
const loadScript = require("discourse/lib/load-script").default;

const MEDIA_ELEMENT_SRC =
  "https://cdnjs.cloudflare.com/ajax/libs/mediaelement/5.0.4/mediaelement-and-player.min.js";

api.decorateCookedElement(
  element => {
    const audioplayers = element.querySelectorAll("audio");

    if (!audioplayers.length) {
      return;
    }

    loadScript(MEDIA_ELEMENT_SRC).then(() => {
++ audioplayers.forEach(function (el) {
++ new MediaElementPlayer(el, {
++ pluginPath: "//cdnjs.cloudflare.com/ajax/libs/mediaelement/5.0.4/",
++ iconSprite: settings.theme_uploads["mejs-controls"],
++ alwaysShowControls: "true",
++ features: ["playpause", "current", "progress", "duration", "volume"]
++ });
++ });
    });
  },
  { id: "mediaelement-js", onlyStream: true }
);

```

现在，看看上面的代码片段，看看是否有让你觉得不妥的地方……

我们传递给脚本实例的选项始终相同，但我们没有将它们放在一个 `const` 中。让我们修复这个问题。

```diff
    const loadScript = require("discourse/lib/load-script").default;
    
    const MEDIA_ELEMENT_SRC =
      "https://cdnjs.cloudflare.com/ajax/libs/mediaelement/5.0.4/mediaelement-and-player.min.js";
    
++ const MEDIA_ELEMENT_CONFIG = {
++ pluginPath: "//cdnjs.cloudflare.com/ajax/libs/mediaelement/5.0.4/",
++ iconSprite: settings.theme_uploads["mejs-controls"],
++ alwaysShowControls: "true",
++ features: ["playpause", "current", "progress", "duration", "volume"]
++ };
    
    api.decorateCookedElement(
      element => {
        const audioplayers = element.querySelectorAll("audio");
    
        if (!audioplayers.length) {
          return;
        }
    
        loadScript(MEDIA_ELEMENT_SRC).then(() => {
          audioplayers.forEach(function (el) {
++ new MediaElementPlayer(el, MEDIA_ELEMENT_CONFIG);
          });
        });
      },
      { id: "mediaelement-js", onlyStream: true }
    );

```

差不多就是这样了。你的代码已经很接近了，只需要更深入地理解 `loadScript()` 的工作原理。

现在，我们将所有内容整合在一起。

`common > header` 标签页

```xml
<script type="text/discourse-plugin" version="0.8.42">
  const loadScript = require("discourse/lib/load-script").default;

  const MEDIA_ELEMENT_SRC =
    "https://cdnjs.cloudflare.com/ajax/libs/mediaelement/5.0.4/mediaelement-and-player.min.js";

  const MEDIA_ELEMENT_CONFIG = {
    pluginPath: "//cdnjs.cloudflare.com/ajax/libs/mediaelement/5.0.4/",
    iconSprite: settings.theme_uploads["mejs-controls"],
    alwaysShowControls: "true",
    features: ["playpause", "current", "progress", "duration", "volume"]
  };

  api.decorateCookedElement(
    element => {
      const audioplayers = element.querySelectorAll("audio");

      if (!audioplayers.length) {
        return;
      }

      loadScript(MEDIA_ELEMENT_SRC).then(() => {
        audioplayers.forEach(function (el) {
          new MediaElementPlayer(el, MEDIA_ELEMENT_CONFIG);
        });
      });
    },
    { id: "mediaelement-js", onlyStream: true }
  );
</script>

```

然后添加一些 CSS 来加载脚本的样式，并防止脚本替换元素时出现闪烁：

`common > css`

```scss
@import "https://cdnjs.cloudflare.com/ajax/libs/mediaelement/5.0.4/mediaelementplayer.min.css";

.cooked {
  --audio-player-height: 40px;

  .mejs__container,
  audio {
    // 匹配 Media-Element.js 播放器高度以防止闪烁
    height: var(--audio-player-height);
    display: block;
  }
}

```

现在，你应该在每个包含有效元素的帖子中看到自定义播放器。

解决了这个问题后，你需要注意，你选择的这个库已经相当老旧了。它是为古老浏览器编译的，并且尝试为许多现在已经标准化的功能提供 polyfill。

如果你知道为什么要使用它，那当然没问题。但如果你只是用它来定制播放器的外观，我建议避免使用它。我还没有检查，但很可能存在更轻量的现代替代方案。

所有这一切最好的地方在于，实现方式与上述内容并无不同。无论你想要针对哪些元素，或者使用哪些脚本，相同的模式都适用。唯一需要改变的是初始化自定义脚本的部分。每个优秀的库都有很好的文档，可以指导你完成这一过程。然后，你只需将其放入上述模式中即可。

---

<div class="post-metadata">

**Author:** ![jimkleiber](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/jimkleiber/32/121814_2.png) [@jimkleiber](https://meta.discourse.org/u/jimkleiber)\
**Post date:** [2022年一月11日 19:21 UTC](https://meta.discourse.org/t/decoratecookedelement-for-each-page-change/214218/3 "2022-01-11T19:21:58Z")

</div>

正是这些时刻，我才希望 Meta 正在使用 [https://meta.discourse.org/t/discourse-reactions-beyond-likes/183261，因为一个“喜欢”不足以表达我现在感受到的爱和深深的感激之情。](https://meta.discourse.org/t/discourse-reactions-beyond-likes/183261%EF%BC%8C%E5%9B%A0%E4%B8%BA%E4%B8%80%E4%B8%AA%E2%80%9C%E5%96%9C%E6%AC%A2%E2%80%9D%E4%B8%8D%E8%B6%B3%E4%BB%A5%E8%A1%A8%E8%BE%BE%E6%88%91%E7%8E%B0%E5%9C%A8%E6%84%9F%E5%8F%97%E5%88%B0%E7%9A%84%E7%88%B1%E5%92%8C%E6%B7%B1%E6%B7%B1%E7%9A%84%E6%84%9F%E6%BF%80%E4%B9%8B%E6%83%85%E3%80%82)

我曾希望有人至少给我一点提示，但你却花时间写了一份非常详尽的解释，说明我是如何走到这一步的，并指导我如何让它奏效，在此过程中教会了我很多东西（甚至还有关于抖动的细节！）

我正在学习 Discourse 的结构如何能鼓励这种行为，这意味着如果我一次性回答好了，其他人也能看到，我就不必再回答一遍——这鼓励我继续用它来建立社区；然而，我认为这并不能完全解释你为什么为我写下这些，而你的意愿可能会鼓励我更多地使用这个平台。

谢谢你。

* * *

关于 Mediaelement，是的，它很老了，但它与我现有的 WordPress 网站搭配得很好，并且我在那里进行了大量定制，试图为用户提供熟悉的外观（而且目前也不想学习另一个库 😃）

---

<div class="post-metadata">

**Author:** ![jimkleiber](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/jimkleiber/32/121814_2.png) [@jimkleiber](https://meta.discourse.org/u/jimkleiber)\
**Post date:** [2022年一月11日 20:54 UTC](https://meta.discourse.org/t/decoratecookedelement-for-each-page-change/214218/4 "2022-01-11T20:54:12Z")

</div>

一个后续问题，也许是个愚蠢的问题：我正在尝试加载多个脚本，因为 Mediaelement 允许插件脚本。我想确保所有脚本都加载完毕后再返回 Promise。

我尝试通过循环遍历脚本源的常量，然后创建一个 Promise 数组，再使用 `Promise.all()` 来初始化播放器，但当我这样做时，我收到了一个错误，说找不到 `mejs`，我认为这是 Mediaelement-and-player 中的命名空间或用于调用不同函数的某个东西。

 ![image](https://global.discourse-cdn.com/meta/original/3X/f/6/f671c5ea5f12f4a5fc848e0003d4b333806a9970.png)

```javascript
      let scripts_loaded = [];

      MEDIA_ELEMENT_SCRIPTS.forEach(function (src, index){
          scripts_loaded[index] = loadScript(src);
      });

      Promise.all(scripts_loaded).then(() => {
            audioplayers.forEach(function (el) {
              new MediaElementPlayer(el, MEDIA_ELEMENT_CONFIG);
            });
      });

```

但是，如果我手动链接它们，它似乎可以工作，如下所示：

```javascript
      loadScript(MEDIA_ELEMENT_SRC).then(() => {
          loadScript(MEDIA_ELEMENT_SPEED_SRC).then(() => {
            loadScript(MEDIA_ELEMENT_SKIP_BACK_SRC).then(() => {
                loadScript(MEDIA_ELEMENT_JUMP_FORWARD_SRC).then(() => {
                    audioplayers.forEach(function (el) {
                        new MediaElementPlayer(el, MEDIA_ELEMENT_CONFIG);
                    });
                });
            });
        });
      });

```

在这种情况下，我只使用少量脚本，所以我只是手动输入了所有脚本，我只是好奇我是否遗漏了关于 `Promise.all()` 函数的明显内容，或者是否有 Discourse 函数允许我从数组加载多个脚本。

---

<div class="post-metadata">

**Author:** ![Johani](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/johani/32/176920_2.png) [@Johani](https://meta.discourse.org/u/Johani)\
**Post date:** [2022年一月11日 22:09 UTC](https://meta.discourse.org/t/decoratecookedelement-for-each-page-change/214218/5 "2022-01-11T22:09:06Z")

</div>

您的代码应该可以正常工作。您只是遇到了 Discourse 中的一个 bug。

`loadScript()` 不会为其加载的脚本设置 `async` 属性。因此，它默认为 `async="true"`，这会扰乱您的加载顺序。这是一个浏览器怪癖。您必须强制使用 JS 加载的脚本的 `async="false"`。

插件较小，因此加载速度比主包快，但由于它们是 `async` 的，因此不再尊重加载顺序——等待主包加载并执行后再执行。

它可能未被注意到，因为据我所知，`loadScript` 在核心中没有嵌套。您通常会将需要协同工作的文件打包在一起。所以，回答您的另一个问题。不，没有 Discourse 函数可以处理那种事情。

您的其他代码片段也应该可以工作。为了让它更容易阅读，也许可以尝试链接它们而不要嵌套它们。

```javascript
// This goes outside the decorator
const PLUGINS = {
  speed: "https://example.com/foo.js",
  skipBack: "https://example.com/bar.js",
  jumpForward: "https://example.com/baz.js"
};

// Then do your work inside the decorator
loadScript(MEDIA_ELEMENT_SRC)
  .then(() => loadScript(PLUGINS.speed))
  .then(() => loadScript(PLUGINS.skipBack))
  .then(() => loadScript(PLUGINS.jumpForward))
  .then(() => {
    audioplayers.forEach(function (el) {
      new MediaElementPlayer(el, MEDIA_ELEMENT_CONFIG);
    });
  });

```

---

<div class="post-metadata">

**Author:** ![jimkleiber](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/jimkleiber/32/121814_2.png) [@jimkleiber](https://meta.discourse.org/u/jimkleiber)\
**Post date:** [2022年一月11日 22:15 UTC](https://meta.discourse.org/t/decoratecookedelement-for-each-page-change/214218/6 "2022-01-11T22:15:12Z")

</div>

> [@Johani](#):
>
> 您的代码应该可以正常工作。您只是遇到了 Discourse 的一个 bug。

哦天哪，谢谢您告诉我！

> [@Johani](#):
>
> 据我所知，它可能未被注意到，因为 `loadScript` 没有嵌套在核心的任何地方。您通常会将需要协同工作的文件打包在一起。所以，回答您的另一个问题。不，没有 Discourse 函数可以处理那种事情。

好的，听起来不错。

> [@Johani](#):
>
> 您的另一个代码片段也应该可以工作。为了让它更容易阅读，也许可以尝试将它们链接起来，而不是嵌套它们。

哇，我非常感激。干净多了。再次感谢 🙏
