不要爱上你的代码
我刚刚删除了昨天写的几百行代码,并用 32 行新代码替换了它们。这是 TheOpenPresenter 的一个功能,用于指示音频是否正在播放。
时不时地,我会开发一个看起来相当容易实现的功能。在这种情况下,我只需要在播放音频时显示此图标。

很简单。每个都是包含多个插件的场景。每个插件都有自己的属性,例如“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 提供所需的功能?我们可以在 Web 组件包中包含一个 JS 函数,但从长远来看,这听起来很乱。
在服务器上处理这个问题似乎是正确的方法。
传输数据
决定好之后,就到了实施的时候了。有几件事要做:
实现
我不会用细节来烦你,所以这里只是概述。由于我们的数据可能相当混乱,因此 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 中报告问题。