Acabo de eliminar cientos de líneas de código que escribí ayer y las reemplacé con 32 líneas de código nuevo. Esto era para una función de TheOpenPresenter , que se usa para indicar si se está reproduciendo un audio.
De vez en cuando, trabajo en una función que parece bastante sencilla de implementar. En este caso, solo necesitaba mostrar este ícono cuando se reproduce el audio.
Bastante simple. Cada uno de estos es una escena que contiene varios complementos. Cada complemento tiene su propia propiedad, como isPlaying
. Podemos fusionar los valores entre los complementos y, si el indicador es verdadero, podemos mostrar el ícono.
El problema principal es cómo acceder a estos datos. Verás, podríamos acceder a los datos directamente, pero cada complemento puede tener su propio esquema. Si bien algunos complementos pueden tener una propiedad isPlaying simple, otros pueden necesitar algo más complicado para representar su estado de reproducción.
Sencillo, ¿por qué no permitir que el complemento registre una devolución de llamada/función que devuelva el estado?
Este es el mismo patrón que utiliza TheOpenPresenter para muchos de sus complementos. Y mientras lo hacemos, podemos abstraerlo en un objeto SceneState . De modo que si alguna vez necesitamos otro estado, podemos agregarlo aquí. Así es como podría verse para el complemento:
// 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), } }, );
Tenga en cuenta que el código anterior se maneja en el servidor. Esto se debe a que TheOpenPresenter consta de tres componentes independientes:
El mando a distancia: donde se muestra esta indicación de audio
El Renderer - reproduce el audio
El servidor: conecta los dos
Lo ideal sería que manejáramos esto en el frontend (remoto) para no agregar carga adicional al servidor. Sin embargo, registrar esta función puede ser complicado. Nuestro frontend utiliza una arquitectura de micro-frontend cargada con componentes web.
El área roja que se muestra a continuación es un shell de React. El área verde se carga a través de componentes web y la administra cada complemento.
Observe que el icono de audio se encuentra en el lado izquierdo del shell. ¿Cómo proporcionamos la función que necesitamos al shell? Podríamos incluir una función JS en el paquete de componentes web, pero eso suena como un desastre a largo plazo.
Manejar esto en el servidor parece ser la forma adecuada de hacerlo.
Una vez decidido esto, ha llegado el momento de ponerlo en práctica. Hay algunas cosas que hacer:
No los aburriré con los detalles, así que aquí les dejo una descripción general. La API no fue del todo sencilla, ya que nuestros datos pueden ser bastante confusos. En resumen: una escena puede tener varios complementos y puede haber varios renderizadores, cada uno de los cuales visualiza una escena de una manera diferente. Por lo tanto, un complemento podría tener varios renderizadores que lo muestren de diferentes maneras. Pero con un poco de manipulación de datos, el problema se resolvió.
Consumir y actualizar la interfaz de usuario
Consumir el valor fue sencillo. Consideré usar el protocolo de reconocimiento de Yjs para proporcionar los datos, ya que son en tiempo real y el marco ya está implementado. Así es como se almacena el estado. Sin embargo, incluir estos datos desde el servidor es un problema en sí mismo. Por lo tanto, decidí usar GraphQL en su lugar, el protocolo que estamos usando para todo lo demás en la plataforma.
Entonces, todo lo que tenemos que hacer es llamar al punto final, escucharlo mediante la suscripción de GraphQL y mostrar el ícono según sea necesario. Listo.
Proporcionar estos datos al frontend
Afortunadamente, utilizamos Postgraphile , lo que hace que la extensión del esquema GraphQL sea bastante sencilla. También podemos convertirlo en una suscripción simplemente agregando @pgSubscription
al esquema GraphQL. Luego, vigilará un tema y actualizará el valor cada vez que llamemos pg_notify
en ese tema. Por ejemplo:
await pgPool.query( `select pg_notify('graphql:sceneState:${id}','{}');`, [], );
Manipular los datos era molesto, pero ¡con un poco de paciencia ya está!
La última pieza del rompecabezas es llamar pg_notify
cuando lo necesitemos.
Para esto, podemos agregar un detector al estado (Yjs) y llamar a la notificación cuando algo cambie:
state.observeDeep(async () => { // Call pg_notify here });
Lo único que queda por hacer es mejorar el rendimiento. En este momento, la función se llama para cada pequeño cambio y también se actualiza en el frontend. Podemos calcular el estado resultante y comparar si hay algún cambio antes de enviar la actualización.
Ahora bien, esta solución sin duda funciona, pero me disgustó tener que escuchar cada uno de los cambios. Es innecesario y no estoy seguro de cómo se escalará el rendimiento. ¿Existe alguna solución mejor?
Entonces di un paso atrás por un segundo y se me ocurrió una idea: ¿Qué tal si volvemos a lo básico y usamos los datos de Yjs?
El problema era que cada complemento podía utilizar distintas formas de indicar el estado de reproducción. Por lo tanto, necesitábamos una forma de saber cómo calcular nosotros mismos el estado resultante. Pero en lugar de dejar que el usuario pase una función, ¿por qué no reservar una propiedad que pueda utilizar para indicarlo?
En lugar de pasar una función para calcular el estado, cada complemento podría establecer el estado reservado directamente junto con sus datos existentes con propiedades como __audioIsPlaying
. Podrían usar este valor directamente o podrían mantenerlo sincronizado con sus propiedades existentes de la siguiente manera:
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")); }, ); };
El nuevo método es genial. No se necesita un oyente adicional ni una API adicional, solo una propiedad reservada simple.
¿El costo? Bueno, ya escribí el 95% de la primera implementación 🫣
“Sería una pena borrar esto después de haber trabajado en ello durante tanto tiempo. Todo lo demás es perfecto, ¡excepto esta cosa!” - Mi mente
No es la primera vez que lo hago, ni la segunda ni la tercera. Esta vez fueron solo unas horas de trabajo. Cuanto más tiempo lleva implementarlo, más difícil es dejarlo ir. Pero si no debemos apegarnos a los servidores, tampoco debemos apegarnos al código que escribimos.
Es obvio que la segunda implementación es mejor. Es más rápida, tiene menos partes móviles, menos superficie de API y menos código para mantener. La primera implementación agregó 289 líneas nuevas, mientras que la segunda implementación solo agregó 32 líneas nuevas.
¿Cuál es entonces la lección que debemos aprender?
Bueno, tal vez encuentres la solución más simple primero. Pero a veces no llegamos a la mejor solución solo con pensarlo. Si ese es el caso, no ames tu código y no tengas miedo de tirarlo a la basura . ¡Y tal vez escribas una publicación en el blog para que puedas sacar algo de provecho de él!
Si has leído hasta aquí, quizás quieras probar TheOpenPresenter . Es un sistema de presentación de código abierto que te permite controlar cualquiera de tus pantallas de forma remota.
Muestra presentaciones de diapositivas, reproduce videos, úsalo como panel de control y mucho más. El software aún se encuentra en una etapa muy temprana de desarrollo, como puedes ver en esta publicación, pero es lo suficientemente estable como para usarlo con regularidad. Personalmente, lo uso para mis reuniones todas las semanas.
Si tienes alguna pregunta, no dudes en informar los problemas en el repositorio de Github .