不要爱上你的代码

我刚刚删除了昨天写的几百行代码,并用 32 行新代码替换了它们。这是 TheOpenPresenter 的一个功能,用于指示音频是否正在播放。

时不时地,我会开发一个看起来相当容易实现的功能。在这种情况下,我只需要在播放音频时显示此图标。

Sample

很简单。每个都是包含多个插件的场景。每个插件都有自己的属性,例如“isPlaying”。我们可以合并插件之间的值,如果标志为真,我们就可以显示图标。

构建解决方案

主要问题是如何访问这些数据。瞧,我们可以直接访问数据。但每个插件可以有自己的模式。虽然有些插件可能有一个简单的 **isPlaying** 属性,但有些插件可能需要更复杂的东西来表示其播放状态。

简单,为什么不允许插件注册一个返回状态的回调/函数?

这是 TheOpenPresenter 在其许多插件中使用的相同模式。同时,我们可以将其抽象为 **SceneState** 对象。因此,如果我们需要任何其他状态,我们可以在此处添加它。插件可能如下所示:

// The pattern we use for plugins
serverPluginApi.onPluginDataCreated(pluginName, onPluginDataCreated);
serverPluginApi.onPluginDataLoaded(pluginName, onPluginDataLoaded);
serverPluginApi.registerRemoteViewWebComponent(
  pluginName,
  remoteWebComponentTag,
);

// Example of how the new API might look like
serverPluginApi.registerSceneState(
  pluginName,
  (_, rendererData) => {
    return {
      audioIsPlaying: !!rendererData.find((x) => x.isPlaying),
    }
  },
);

服务器处理

请注意,上面的代码是在服务器上处理的。这是因为 TheOpenPresenter 由 3 个独立的组件组成:

  • 遥控器 - 显示音频指示的位置
  • 渲染器-播放音频
  • 服务器——连接两者
  • 理想情况下,我们应该在前端(远程)处理这个问题,这样我们就不会给服务器增加额外的负载。但是,注册这个函数可能会很麻烦。我们的前端使用加载了 Web 组件的微前端架构。

    下图红色区域是 React 的外壳,绿色区域是通过 Web Components 加载,并由各个插件进行管理。

    Shell

    请注意,音频图标位于 shell 的左侧。我们如何向 shell 提供所需的功能?我们可以在 Web 组件包中包含一个 JS 函数,但从长远来看,这听起来很乱。

    在服务器上处理这个问题似乎是正确的方法。

    传输数据

    决定好之后,就到了实施的时候了。有几件事要做:

  • 创建插件 API
  • 将此数据提供给前端
  • 使用并更新 UI
  • 实现

    我不会用细节来烦你,所以这里只是概述。由于我们的数据可能相当混乱,因此 API 并不十分简单。简而言之:一个场景可以有多个插件。并且可以有多个渲染器,每个渲染器都以不同的方式查看场景。因此,一个插件可以有多个渲染器以不同的方式显示它。但通过一些数据操作,问题就解决了。

    **使用和更新 UI**

    使用该值很简单。我考虑使用 Yjs 的感知协议来提供数据,因为它是实时的,并且框架已经到位。这就是状态的存储方式。但是,从服务器包含这些数据本身就是一个问题。所以我决定改用 GraphQL——我们在平台中用于其他所有内容的协议。

    因此,我们需要做的就是调用端点,使用 GraphQL 的订阅来监听它,并根据需要显示图标。完成。

    **将这些数据提供给前端**

    值得庆幸的是,我们使用 Postgraphile,这使得扩展 GraphQL 模式变得非常简单。我们还可以简单地通过向 GraphQL 模式添加 `@pgSubscription` 来使其成为订阅。然后,它会监视一个主题,并在我们对该主题调用 `pg_notify` 时更新值。例如:

    await pgPool.query(
      `select pg_notify('graphql:sceneState:${id}','{}');`,
      [],
    );

    处理数据很烦人,但只要有一点耐心就可以完成!

    难题的最后一部分是在需要时调用“pg_notify”。

    为此,我们可以向状态(Yjs)添加一个监听器,并在任何变化时调用通知:

    state.observeDeep(async () => {
      // Call pg_notify here
    });

    剩下要做的就是性能改进。现在,每次发生小变化时都会调用该函数,它也会更新到前端。我们可以计算结果状态,并在推送更新之前比较是否有任何变化。

    更好的解决方案

    现在这个解决方案确实有效。但我讨厌我们监听每一个变化。这是不必要的,而且我不确定性能会如何扩展。有没有更好的解决方案?

    所以我退后一步,然后一个想法出现了:**我们从基础开始并使用来自 Yjs 的数据怎么样?**

    问题是每个插件可能使用不同的方式来指示播放状态。所以我们需要一种方法来知道如何自己计算结果状态。但是,与其让用户传递一个函数,**为什么不保留一个他们可以用来指示这一点的属性呢?**

    每个插件都可以使用 `__audioIsPlaying` 等属性直接设置保留状态以及现有数据,而无需传递函数来计算状态。他们可以直接使用此值,也可以将其与现有属性保持同步,如下所示:

    const onRendererDataLoaded = (
      rendererData,
    ) => {
      watchYjs(
          // Watch the isPlaying property
        (x) => x.isPlaying,
        () => {
            // And if it changes, sync the __audioIsPlaying property
          rendererData.set("__audioIsPlaying", rendererData.get("isPlaying"));
        },
      );
    };

    删除旧代码

    新方法很棒。没有额外的监听器,没有额外的 API,只有一个简单的保留属性。

    成本?好吧,我已经写了 95% 的第一次实现🫣

    *-我的想法*

    这不是我第一次这样做,也不是第二次或第三次。这次只花了几个小时。实施时间越长,就越难放手。但如果我们不应该依赖服务器,我们也不应该依赖我们编写的代码。

    显然,第二种实现更好。它速度更快、移动部件更少、API 界面更少,需要维护的代码也更少。第一种实现增加了 289 行新代码,而第二种实现仅增加了 32 行新代码。

    **那么我们要吸取什么教训呢?**

    好吧,也许先找到最简单的解决方案。但有时我们无法仅通过思考就找到最佳解决方案。如果是这样,**不要喜欢你的代码,也不要害怕丢弃它**。也许写一篇博客文章,这样你就能从中得到一些东西!

    如果你读到这里,你可能想试试 TheOpenPresenter!这是一个开源演示系统,可以让你远程控制任何屏幕。

    显示幻灯片、播放视频、用作仪表板等等。从这篇文章中可以看出,该软件仍处于开发初期,但它足够稳定,可以定期使用。我个人每周都会在聚会上使用它。

    如有任何问题,请随时提问。或者,您也可以在 Github repo 中报告问题。