Właśnie usunąłem setki linijek kodu, które napisałem wczoraj i zastąpiłem je 32 linijkami nowego kodu. Było to dla funkcji TheOpenPresenter , używanej do wskazywania, czy dźwięk jest odtwarzany.
Co jakiś czas pracowałem nad funkcjonalnością, która wydaje się dość prosta do wdrożenia. W tym przypadku potrzebowałem tylko pokazać tę ikonę, gdy odtwarzany jest dźwięk.
Wystarczająco proste. Każdy z nich jest sceną zawierającą wiele wtyczek. Każda wtyczka ma swoją własną właściwość, taką jak isPlaying
. Możemy scalić wartości między wtyczkami, a jeśli flaga jest prawdziwa, możemy wyświetlić ikonę.
Głównym problemem jest to, jak uzyskać dostęp do tych danych. Widzisz, moglibyśmy uzyskać dostęp do danych bezpośrednio. Ale każda wtyczka może mieć swój własny schemat. Podczas gdy niektóre wtyczki mogą mieć prostą właściwość isPlaying , niektóre inne mogą potrzebować czegoś bardziej skomplikowanego, aby reprezentować swój status odtwarzania.
Proste, dlaczego nie pozwolić wtyczce zarejestrować wywołania zwrotnego/funkcji zwracającej stan?
To ten sam wzorzec, którego TheOpenPresenter używa w wielu swoich wtyczkach. A skoro już przy tym jesteśmy, możemy go wyabstrahować do obiektu SceneState . Więc jeśli kiedykolwiek będziemy potrzebować innego stanu, możemy go tutaj dodać. Oto jak to może wyglądać dla wtyczki:
// 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), } }, );
Zauważ, że powyższy kod jest obsługiwany na serwerze. Dzieje się tak, ponieważ TheOpenPresenter składa się z 3 oddzielnych komponentów:
Pilot – gdzie wyświetla się ten sygnał dźwiękowy
Renderer - odtwarza dźwięk
Serwer - łączy dwa
Najlepiej byłoby, gdybyśmy zajęli się tym w frontendzie (zdalnie), aby nie dodawać dodatkowego obciążenia do serwera. Jednak rejestrowanie tej funkcji może być chaotyczne. Nasz frontend używa mikro-architektury frontendu załadowanej komponentami internetowymi.
Czerwony obszar poniżej to powłoka React. Zielony obszar jest ładowany przez komponenty sieciowe i jest zarządzany przez każdą wtyczkę.
Zauważ, że ikona audio znajduje się po lewej stronie powłoki. Jak dostarczyć funkcji, której potrzebujemy do powłoki? Możemy dołączyć funkcję JS do pakietu komponentów internetowych, ale na dłuższą metę brzmi to jak bałagan.
Wydaje się, że właściwym sposobem jest obsłużenie tego na serwerze.
Mając to ustalone, czas na wdrożenie. Jest kilka rzeczy do zrobienia:
Nie będę zanudzał Cię szczegółami, więc oto przegląd. API nie było całkiem proste, ponieważ nasze dane mogą być dość mylące. Krótko mówiąc: scena może mieć wiele wtyczek. I może być wiele rendererów, z których każdy wyświetla scenę w inny sposób. Więc wtyczka może mieć wiele rendererów pokazujących ją na różne sposoby. Ale z odrobiną manipulacji danymi problem został rozwiązany.
Konsumowanie i aktualizowanie interfejsu użytkownika
Konsumpcja wartości była prosta. Rozważałem użycie protokołu świadomości Yjs do dostarczenia danych, ponieważ są one w czasie rzeczywistym, a struktura jest już na miejscu. W ten sposób przechowywany jest stan. Jednak uwzględnienie tych danych z serwera stanowi osobny problem. Dlatego zdecydowałem się użyć GraphQL — protokołu, którego używamy do wszystkiego innego na platformie.
Więc wszystko, co musimy zrobić, to wywołać punkt końcowy, nasłuchiwać go za pomocą subskrypcji GraphQL i wyświetlić ikonę, jeśli to konieczne. Gotowe.
Dostarczanie tych danych do front-endu
Na szczęście używamy Postgraphile , co sprawia, że rozszerzanie schematu GraphQL jest dość proste. Możemy również uczynić go subskrypcją, po prostu dodając @pgSubscription
do schematu GraphQL. Następnie będzie on obserwować temat i aktualizować wartość za każdym razem, gdy wywołamy pg_notify
w tym temacie. Na przykład:
await pgPool.query( `select pg_notify('graphql:sceneState:${id}','{}');`, [], );
Manipulowanie danymi było irytujące, ale wystarczyło trochę cierpliwości i gotowe!
Ostatnim elementem układanki jest wywołanie pg_notify
, gdy zajdzie taka potrzeba.
W tym celu możemy dodać obiekt nasłuchujący do stanu (Yjs) i wywołać powiadomienie za każdym razem, gdy coś się zmieni:
state.observeDeep(async () => { // Call pg_notify here });
Jedyne, co pozostało do zrobienia, to poprawa wydajności. Obecnie funkcja jest wywoływana przy każdej małej zmianie, jest również aktualizowana do frontendu. Możemy obliczyć stan wynikowy i porównać, czy coś się zmienia, zanim wprowadzimy aktualizację.
To rozwiązanie z pewnością działa. Ale nienawidziłem tego, że słuchaliśmy każdej zmiany. To niepotrzebne i nie jestem pewien, jak skalowalna będzie wydajność. Czy istnieje lepsze rozwiązanie?
Więc na chwilę się zatrzymałem i przyszedł mi do głowy pomysł: co by było, gdybyśmy wrócili do podstaw i wykorzystali dane z Yjs?
Problem polegał na tym, że każda wtyczka może używać różnych sposobów, aby wskazać stan odtwarzania. Dlatego potrzebowaliśmy sposobu, aby wiedzieć, jak obliczyć wynikowy stan samodzielnie. Ale zamiast pozwolić użytkownikowi przekazać funkcję, dlaczego nie zarezerwować właściwości, której może użyć, aby to wskazać?
Zamiast przekazywać funkcję do obliczenia stanu, każda wtyczka mogłaby ustawić zarezerwowany stan bezpośrednio obok swoich istniejących danych za pomocą właściwości, takich jak __audioIsPlaying
. Mogliby użyć tej wartości bezpośrednio lub mogliby ją zsynchronizować ze swoimi istniejącymi właściwościami, tak jak tutaj:
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")); }, ); };
Nowa metoda jest genialna. Bez dodatkowego słuchacza, bez dodatkowego API, tylko prosta zarezerwowana właściwość.
Koszt? Cóż, już napisałem 95% pierwszej implementacji 🫣
„Będzie tak szkoda to usunąć, skoro tak długo nad tym pracowałem. Wszystko inne jest idealne, poza tą jedną rzeczą!” – Mój umysł
To nie jest mój pierwszy raz. Ani drugi, ani trzeci. Tym razem to było tylko kilka godzin pracy. Im dłużej trwa implementacja, tym trudniej jest się od tego uwolnić. Ale jeśli nie powinniśmy przywiązywać się do serwerów, nie powinniśmy też przywiązywać się do kodu, który piszemy.
Oczywiste jest, że druga implementacja jest lepsza. Jest szybsza, ma mniej ruchomych części, mniej powierzchni API i mniej kodu do utrzymania. Pierwsza implementacja dodała 289 nowych linii, podczas gdy druga implementacja dodała tylko 32 nowe linie.
Jaką więc można z tego wyciągnąć lekcję?
Cóż, może najpierw znajdź najprostsze rozwiązanie. Ale czasami nie dochodzimy do najlepszego rozwiązania, po prostu o nim myśląc. Jeśli tak jest, nie kochaj swojego kodu i nie bój się go wyrzucić . I może napisz post na blogu, żeby coś z tego wyciągnąć!
Jeśli dotarłeś aż tutaj, możesz spróbować TheOpenPresenter ! To system prezentacji typu open-source, który pozwala Ci zdalnie kontrolować dowolny ekran.
Pokaż pokazy slajdów, odtwarzaj filmy, używaj jako pulpitów nawigacyjnych i wiele więcej. Oprogramowanie jest wciąż na bardzo wczesnym etapie rozwoju, jak widać z tego posta, ale jest wystarczająco stabilne, aby używać go regularnie. Osobiście używam go na moich spotkaniach co tydzień.
W razie pytań, pytaj. Lub zgłaszaj problemy w repozytorium Github .