Jak poprawnie skorzystać z API systemu plików w komponencie?

Wątek przeniesiony 2024-11-03 12:45 z JavaScript przez Riddle.

1

Potrzebuję porady jak zaimplementować obsługę przeglądarkowego FileSystem API w sposób zgody z "dobrymi praktykami" pisania apliakcji w React.

FilesystemAPI to jest interfejs który pozwala aplikacji przeglądarkowej na dostęp do systemu plików użytkownika. Użytkownik może wybrać sobie folder, aplikacja dostaje tzw. handle (uchwyt? 😀 ) i za jego pośrednictwem można czytać z plików tym folderze. Haczyk jest taki, że nie wiadomo kiedy uchwyt traci dostęp do folderu. Można to sprawdzić albo podczas próby dostępu do niego (leci wtedy błąd) albo wykonując metodę typu queryPermission. Ja potrzebuję w aplikacji wyświetlić alert natychmiast kiedy okaże się że straciłem dostęp, wtedy pytam użytkownika o udzielenie go ponownie.

Piszę na co dzień w językach obiektowych i naturalnym sposobem na rozwiązanie tego problemu jest dla mnie utworzenie klasy typu Singleton, która trzyma stan uchwytu (jakaś tam properta isActive). Klasa pośredniczyłaby we wszystkich próbach dostępu do systemu plików i zmieniałaby status isActive zawsze gdy poleciałby jakiś wyjątek przy próbie zrobienia tego.

Pytanie czy to rozwiązanie w React to nie jest jakiś antypattern? Podobno komponenty Reactowe powinny raczej "pure" czyli ten sam danych wejściowych powinien generować zawsze ten sam wynik. Jak dodam sobie globalnie dostępny singleton to zasada ta pewnie zostanie złamana?

0

Hej @vermaterc! Fajnie że jesteś na forum! 👋

vermaterc napisał(a):

Potrzebuję porady jak zaimplementować obsługę przeglądarkowego FileSystem API w sposób zgody z "dobrymi praktykami" pisania apliakcji w React.

To są osobne rzeczy, obsługa systemu plików to jedno, prezentacja aplikacji (React) to drugie.

Oczywiście one muszą się wywołać nawzajem, więć do pewnego stopnia muszą o sobie wiedzieć, ale to powinno być ograniczone do minimum.

1

czyli jak to w praktyce powinno wyglądać? Chcę żeby user klikając "save" zapisał coś do pliku, a jak sobie przeładuje stronę albo kliknie "reload" to żeby aplikacja odczytała plik i wyświetliła informacje z niego w UI. Co powinno zarządzać takimi operacjami i jeśli jednak klasa typu Singleton na jakim etapie ją utworzyć?

1
vermaterc napisał(a):

czyli jak to w praktyce powinno wyglądać? Chcę żeby user klikając "save" zapisał coś do pliku, a jak sobie przeładuje stronę albo kliknie "reload" to żeby aplikacja odczytała plik i wyświetliła informacje z niego w UI.

Powinieneś mieć klasę która umie operować plikiem i wystawia prosty interfejs, np. fs.saveFile('hello', onSave); oraz fs.openFile(onOpen). Komponent może wtedy wywołać te metody i przekazać swoje funkcje jako parametr żeby być powiadomionym o wyniku. Dodatkowo chcesz być powiadomiony o błędzie, czyli ta funkcja powinna oprócz wartości zwrócić informację o tym czy był błąd czy nie, może to być true/false.

Instancja Twojej klasy może być przekazana do komponentu albo przez property albo przez context/provide (przy czym property są łatwiejsze przy małych projektach). Jej instancja może być stworzona zanim React wstanie, ewentualnie któryś z komponentów może ją stworzyć (tylko wtedy widok wie więcej o aplikacji, co nie koniecznie jest dobre, bo zmiana Twojej klasy wpłynie na zmianę komponentu).

vermaterc napisał(a):

Co powinno zarządzać takimi operacjami i jeśli jednak klasa typu Singleton na jakim etapie ją utworzyć?

Część powinna być po stronie widoku, część po stronie Twojej klasy:

  • Widok (czyli komponent React) powinien:

    • jeśli user będzie chciał wczytać plik, widok powinien przekazać kontrolę do Twojej klasy
    • odebrać od klasy treść pliku kiedy jest wczytana i ją wyświetlić
    • umożliwić zapisanie pliku na klik (czyli jeśli user będzie chciał zapisać plik, to widok przekażę kontrolę do Twojej klasy)
    • odbierze treści pliku od użytkownika (przez jakieś pole, np. textarea) i przekaże ją do Twojej klasy do zapisania
    • odebranie informacji od klasy czy zapisanie się powiodło czy nie i sprezentowanie tej informacji
    • Widok mógłby też zainicjować pierwsze wczytanie pliku.
  • Twoja klasa powinna:

    • znać szczegóły API przeglądarkowego, pamiętać handle pliku
    • wystawić interfejs który widok może zawołać żeby zapisać lub wczytać plik
    • przyjąć treść pliku do zapisania,
    • przy wczytaniu, za pośrednictwem tego interfejsu wysłać do widoku treść wczytanego pliku oraz/lub informację o tym czy był błąd
    • Powinien stanowić warstwę abstrakcji między widokiem a API przeglądarki, tak żeby widok nie znał szczegółów API przeglądarki.

Przykład kodu poniżej. Zauważ że klasa MyFilesystem w ogóle nie musi używać systemu plików na potrzeby przykładu, implementację można łatwo zastąpić.

import React, {useState} from 'react';
import ReactDOM from 'react-dom/client';

class MyFilesystem {
  saveFile(content, onSaveOrError) {
    console.log('saving a file', content);
    if (Math.random() < 0.5) { // simulate error
      onSaveOrError(true); // success
    } else {
      onSaveOrError(false); // there was an error
    }
  }

  openFile(onOpenOrError) {
    console.log('reading a file');
    if (Math.random() < 0.5) { // simulate error
      onOpenOrError(true, 'content loaded from file'); // success
    } else {
      onOpenOrError(false, null); // there was an error
    }
  }
}
function MyComponent({ fs }) {
  const [text, setText] = useState('');
  const [errorMessage, setErrorMessage] = useState(null);

  function handleSave() {
    fs.saveFile(text, (success) => {
      setErrorMessage(!success ? 'There was an error saving a file.' : 'Saved file.');
    });
  }

  function handleOpen() {
    fs.openFile((success, content) => {
      if (success) {
        setText(content );
        setErrorMessage('Opened file.');
      } else {
        setText('');
        setErrorMessage('There was an error opening a file.');
      }
    });
  }

  useEffect(() => handleOpen(), []); // invoke open on first render

  return (
    <div>
      <button onClick={handleSave}>Save</button>
      <button onClick={handleOpen}>Open</button>
      <div>
        <textarea value={text} onChange={e=>setText(e.target.value)}/>
      </div>
      <p>{errorMessage}</p>
    </div>
  );
}

const myFileSystem = new MyFilesystem();

ReactDOM.createRoot(document.getElementById('root')).render(
  <MyComponent fs={myFileSystem} />
);

Zarejestruj się i dołącz do największej społeczności programistów w Polsce.

Otrzymaj wsparcie, dziel się wiedzą i rozwijaj swoje umiejętności z najlepszymi.