Kiwix PWA でインターネットからギガバイト単位のデータをオフライン保存する方法

左側にプラスチック製の椅子があるシンプルなテーブルの上に立っているノートパソコンの周りに集まる人々。背景は発展途上国の学校のようなものです。

このケーススタディでは、非営利団体である Kiwix が、プログレッシブ ウェブアプリ テクノロジーと File System Access API を使用して、ユーザーがオフライン用に大規模なインターネット アーカイブをダウンロードして保存できるようにする方法を紹介します。Origin Private File System(OPFS)を扱うコードの技術的な実装について説明します。OPFS は Kiwix PWA 内の新しいブラウザ機能で、ファイル管理を強化し、許可プロンプトなしでアーカイブへのアクセスを改善します。この記事では、この新しいファイル システムの課題と、将来起こり得る展開について説明します。

Kiwix について

国際電気通信連合によると、ウェブが誕生してから 30 年以上経ちましたが、世界人口の 3 分の 1 が、いまだにインターネットへの安定したアクセスを待っています。物語はここで終わりますか?答えはもちろん「ノー」であり、スイスを拠点とする非営利団体 Kiwix の人々は、インターネット アクセスが限られている人やまったくアクセスできない人にも知識を提供することを目的とした、オープンソースのアプリとコンテンツのエコシステムを開発しました。そのアイデアは、ユーザーがインターネットに簡単にアクセスできない場合、接続が利用可能な場所とタイミングでユーザーのキーリソースをダウンロードして、後でオフラインで使用できるようにローカルに保存できる、というものです。Wikipedia、Project Gutenberg、Stack Exchange、TED トークなど、多くの重要なサイトを ZIM ファイルと呼ばれる高圧縮アーカイブに変換し、Kiwix ブラウザでオンザフライで読み込めるようになりました。

ZIM アーカイブでは、非常に効率的な Zstandard(ZSTD)圧縮が使用されます(以前のバージョンでは XZ が使用されていました)。主に HTML、JavaScript、CSS の保存に使用され、画像は通常、圧縮 WebP 形式に変換されます。各 ZIM には URL とタイトル インデックスも含まれています。ここで重要なのは圧縮です。英語版 Wikipedia(640 万の記事と画像)は ZIM 形式に変換すると 97 GB に圧縮されます。人間の知識の総量がミドルレンジの Android スマートフォンに収まることに気づくまでは、かなりの難しさのように聞こえます。また、数学や医学などのテーマ別バージョンの Wikipedia など、多数の小規模なリソースも提供されています。

Kiwix には、パソコン(Windows/Linux/macOS)とモバイル(iOS/Android)での使用をターゲットとするさまざまなネイティブ アプリが用意されています。ただし、このケーススタディではプログレッシブ ウェブアプリ(PWA)に焦点を当てます。PWA は、最新のブラウザを搭載したあらゆるデバイス向けのユニバーサルでシンプルなソリューションになることを目指しています。

大規模なコンテンツ アーカイブに完全にオフラインですばやくアクセスする必要があるユニバーサル ウェブアプリの開発における課題と、そうした課題に対する革新的でエキサイティングなソリューションを提供する最新の JavaScript API、特に File System Access APIOrigin Private File System について説明します。

オフライン用のウェブアプリ

Kiwix のユーザーは多種多様であり、ニーズも多岐にわたります。また、Kiwix は、コンテンツにアクセスするデバイスやオペレーティング システムをほとんど、またはまったく制御できません。こうしたデバイスの中には、特に低所得地域では、動作が遅いものや古いものもあります。Kiwix は可能な限り多くのユースケースに対応するよう努めていますが、あらゆるデバイスで最も普遍的なソフトウェアであるウェブブラウザを使用することで、さらに多くのユーザーにリーチできることにも気づきました。そこで、アトウッドの法則に触発されて、JavaScript で記述できるすべてのアプリケーションは、最終的には JavaScript で記述されると述べています。10 年ほど前に Kiwix の開発者たちは、Kiwix ソフトウェアを C++ から JavaScript に移植することにしました。

Kiwix HTML5 と呼ばれるこのポートの最初のバージョンは、現在は廃止された Firefox OS とブラウザ拡張機能用でした。その中心となるのは、Emscripten コンパイラを使用して、C++ 解凍エンジン(XZ と ZSTD)を、ASM.js の中間 JavaScript 言語、後に Wasm(WebAssembly)にコンパイルしたものです。その後、Kiwix JS と改名されましたが、ブラウザ拡張機能は現在も積極的に開発されています。

Kiwix JS オフライン ブラウザ

そこで登場するのがプログレッシブ ウェブアプリ(PWA)です。Kiwix のデベロッパーは、このテクノロジーの可能性を認識し、Kiwix JS の専用の PWA バージョンを構築し、特にオフラインの使用、インストール、ファイル処理、ファイル システム アクセスの分野でネイティブに近い機能を提供できる OS 統合の追加に着手しました。

オフラインファースト PWA は非常に軽量であるため、断続的または高コストのモバイル インターネットがある状況に最適です。その背後にあるテクノロジーは、Service Worker API と関連する Cache API で、Kiwix JS をベースとするすべてのアプリで使用されます。これらの API を使用すると、アプリがサーバーとして機能し、表示中のメイン ドキュメントまたは記事からの取得リクエストをインターセプトして、(JS)バックエンドにリダイレクトして、ZIM アーカイブからレスポンスを抽出して作成できるようになります。

あらゆる場所に保管できるストレージ

ZIM アーカイブのサイズが大きいことを考えると、特にモバイル デバイスでは、ストレージとそれへのアクセスが Kiwix デベロッパーにとって最大の悩みの種となっています。Kiwix のエンドユーザーの多くは、後でオフラインで使用できるように、インターネットが利用可能なときにアプリ内でコンテンツをダウンロードします。Torrent を使用して PC にダウンロードし、モバイル デバイスやタブレット デバイスに転送するユーザーもいます。また、パッチがあって高価なモバイル インターネットが整備されている地域では、USB スティックやポータブル ハードドライブでコンテンツを交換する場合があります。ユーザーがアクセス可能な任意の場所からコンテンツにアクセスする上記のすべての方法が、Kiwix JS と Kiwix PWA でサポートされている必要があります。

当初 Kiwix JS がメモリの少ないデバイスでも数百 GB の膨大なアーカイブ(1 つの ZIM アーカイブは 166 GB)を読み取ることが可能になったのが、File API です。この API は、非常に古いブラウザも含め、あらゆるブラウザで普遍的にサポートされているため、新しい API がサポートされていない場合の普遍的なフォールバックとして機能します。Kiwix の場合は、HTML で input 要素を定義するのと同じくらい簡単です。

<input
  type="file"
  accept="application/octet-stream,.zim,.zimaa,.zimab,.zimac, ..."
  value="Select folder with ZIM files"
  id="archiveFilesLegacy"
  multiple
/>

選択すると、入力要素は File オブジェクトを保持します。File オブジェクトは基本的にはストレージ内の基盤となるデータを参照するメタデータです。厳密には、純粋なクライアントサイドの JavaScript で記述された Kiwix のオブジェクト指向バックエンドが、必要に応じて大規模なアーカイブの小さなスライスを読み取ります。これらのスライスを圧縮解除する必要がある場合、バックエンドは Wasm 解凍にそれらを渡し、リクエストがあれば、完全な blob(通常は記事またはアセット)が解凍されるまで、さらにスライスを取得します。つまり、サイズの大きいアーカイブを完全にメモリに読み込む必要はありません。

汎用性のある File API には、ネイティブ アプリと比べて Kiwix JS アプリが扱いづらく古く見えるという欠点があります。この API では、次のセッションへのアクセス権限を維持する方法がないため、ユーザーはファイル選択ツールを使用してアーカイブを選択するか、アプリにファイルをドラッグ&ドロップする必要があります。

多くのデベロッパーと同様に、この質の悪い UX を軽減するために、Kiwix JS デベロッパーは当初 Electron のルートを採用しました。ElectronJS は、Node API を使用したファイル システムへの完全アクセスなどの強力な機能を備えた優れたフレームワークです。ただし、よく知られている欠点がいくつかあります。

  • デスクトップ オペレーティング システムでのみ動作します。
  • 大きくて重いです(70 MB ~ 100 MB)。

Electron アプリのサイズは、すべてのアプリに Chromium の完全なコピーが含まれているため、最小化されバンドルされた PWA のわずか 5.1 MB に比べて非常に好ましくありません。

では、Kiwix が PWA の状況を改善できる方法はあるのでしょうか。

File System Access API による問題解決

Kiwix は、2019 年頃、Chrome 78 でオリジン トライアルが実施され、ネイティブ ファイル システム API と呼ばれる緊急 API を認識しました。ファイルまたはフォルダのファイル ハンドルを取得し、それを IndexedDB データベースに保存できることが約束されていました。このハンドルはアプリ セッション間で維持されるため、ユーザーがアプリを再起動する際にファイルやフォルダを再度選択する必要はありません(ただし、権限に関するクイック プロンプトに応答する必要があります)。本番環境になるまでに、この API は File System Access API という名称に変更され、コア部分は WHATWG によって File System API(FSA)として標準化されました。

では、API のファイル システム アクセスの部分はどのように機能するのでしょうか。次の点にご注意ください。

  • 非同期 API である(ウェブ ワーカーの特殊な関数を除く)。
  • ファイルまたはディレクトリの選択ツールは、ユーザー操作(UI 要素のクリックまたはタップ)をキャプチャしてプログラムで起動する必要があります。
  • ユーザーが(新しいセッションで)以前に選択したファイルへのアクセス権限を再度付与するには、ユーザー操作も必要です。実際、ユーザーの操作によって開始されない場合、ブラウザは権限プロンプトの表示を拒否します。

コードは比較的簡単ですが、扱いにくい IndexedDB API を使用してファイルやディレクトリのハンドルを保存する必要があります。幸いなことに、browser-fs-access のように、負荷の大きい処理の多くを自動的に行ういくつかのライブラリがあります。Kiwix JS では、詳細に文書化されている API を直接使用することに決めました。

ファイルとディレクトリの選択ツールを開く

ファイル選択ツールを開くと、次のようになります(ここでは Promise を使用していますが、async/await シュガーを使用する場合は、Chrome for Developers のチュートリアルをご覧ください)。

return window
  .showOpenFilePicker({ multiple: false })
  .then(function (fileHandles) {
    return processFileHandle(fileHandles[0]);
  })
  .catch(function (err) {
    // This is normal if app is launching
    console.warn(
      'User cancelled, or cannot access fs without user gesture',
      err,
    );
  });

わかりやすくするために、このコードでは最初に選択されたファイルのみを処理しています(複数のファイルを選択することは禁止されています)。{ multiple: true } で複数のファイルを選択できるようにするには、各ハンドルを処理するすべての Promise を Promise.all().then(...) ステートメントでラップします。次に例を示します。

let promisesForFiles = fileHandles.map(function (fileHandle) {
    return processFileHandle(fileHandle);
});
return Promise.all(promisesForFiles).then(function (arrayOfFiles) {
    // Do something with the files array
    console.log(arrayOfFiles);
}).catch(function (err) {
    // Handle any errors that occurred during processing
    console.error('Error processing file handles!', err);
)};

ただし、複数のファイルを選択する場合は、個々のファイルではなく、それらのファイルを含むディレクトリを選択するようにユーザーに求めることをおすすめします。特に Kiwix のユーザーは、すべての ZIM ファイルを同じディレクトリに整理する傾向があるためです。ディレクトリ選択ツールを起動するためのコードは、window.showDirectoryPicker.then(function (dirHandle) { … }); を使用する点を除き、上記とほぼ同じです。

ファイルまたはディレクトリ ハンドルの処理

ハンドルを取得したら、それを処理する必要があります。関数 processFileHandle は次のようになります。

function processFileHandle(fileHandle) {
  // Serialize fileHandle to indexedDB
  serializeFSHandletoIdxDB('pickedFSHandle', fileHandle, function (val) {
    console.debug('IndexedDB responded with ' + val);
  });
  return fileHandle.getFile().then(function (file) {
    // Do something with the file
    return file;
  });
}

なお、ファイル ハンドルを格納する関数を用意する必要があります。抽象化ライブラリを使用する場合を除き、便利なメソッドはありません。Kiwix によるこれの実装は cache.js ファイルで確認できますが、ファイルやフォルダ ハンドルの保存と取得にのみ使用する場合は、大幅に簡素化できます。

ディレクトリの処理は少し複雑です。選択したディレクトリ内のエントリを非同期 entries.next() を使用して反復処理して、必要なファイルやファイル形式を見つける必要があります。設定にはさまざまな方法がありますが、Kiwix PWA で使用するコードの概要を次に示します。

let iterableEntryList = dirHandle.entries();
return iterateAsyncDirEntries(iterableEntryList, []).then(function (entryList) {
  // Do something with the entry list
  return entryList;
});

/**
 * Iterates FileSystemDirectoryHandle iterator and adds entries to an array
 * @param {Iterator} entries An asynchronous iterator of entries
 * @param {Array} archives An array to which to add the entries (may be empty)
 * @return {Promise<Array>} A Promise for an array of entries in the directory
 */
function iterateAsyncDirEntries(entries, archives) {
  return entries
    .next()
    .then(function (result) {
      if (!result.done) {
        let entry = result.value[1];
        // Filter for the files you want
        if (/\.zim(\w\w)?$/i.test(entry.name)) {
          archives.push(entry);
        }
        return iterateAsyncDirEntryArray(entries, archives);
      } else {
        // We've processed all the entries
        if (!archives.length) {
          console.warn('No archives found in the picked directory!');
        }
        return archives;
      }
    })
    .catch(function (err) {
      console.error('There was an error processing the directory!', err);
    });
}

entryList の各エントリでは、後で必要になったときに entry.getFile().then(function (file) { … }) でファイルを取得するか、async functionconst file = await entry.getFile() を使用して同等のものを取得します。

さらに踏み込むことはできるか?

ユーザーが、アプリの以降の起動時にユーザーの操作で開始された権限を付与する必要があるため、ファイルとフォルダを開く(再)際に若干の摩擦が発生します。ただし、ファイルを再選択しなければならないというよりもはるかに流動的です。Chromium のデベロッパーは現在、インストール済みの PWA に永続的な権限を許可するコードを確定しています。これは多くの PWA デベロッパーが求めており、強く期待されています。

では、まだ待っている時間がない場合はどうすればよいのでしょうか?Kiwix のデベロッパーは最近、Chromium と Firefox の両方のブラウザでサポートされている File Access API の画期的な新機能を使用することで、すべての権限プロンプトを今すぐ排除できることを発見しました(また、Safari では部分的にサポートされていますが、依然として FileSystemWritableFileStream が欠落しています)。この新機能が Origin Private File System です。

完全ネイティブ化: オリジンのプライベート ファイル システム

Origin Private File System(OPFS)は Kiwix PWA の試験運用版機能ですが、チームはネイティブ アプリとウェブアプリのギャップを主に埋めるため、ユーザーにこの機能を試すよう強く呼びかけています。主なメリットは次のとおりです。

  • OPFS 内のアーカイブには、起動時にも権限プロンプトなしでアクセスできます。ユーザーは、前のセッションで中断したところから、まったくスムーズに記事の読み取りやアーカイブのブラウジングを再開できます。
  • 保存されたファイルへのアクセスが高度に最適化されます。Android では、速度が 5 ~ 10 倍速くなっています。

Android で File API を使用した標準のファイル アクセスを使用する場合、特に(Kiwix ユーザーにはよくあることです)、大きなアーカイブがデバイス ストレージではなく microSD カードに保存されている場合は、非常に遅くなります。これらすべてがこの新しい API によって変わります。ほとんどのユーザーは 97 GB のファイルを OPFS に保存することはできませんが(microSD カード ストレージではなく、デバイスのストレージを消費します)、小中規模のアーカイブを保存するのに最適です。WikiProject Medicine の最も充実した医学百科事典をお探しですか?問題ありません。1.7 GB なので、OPFS に楽に収まります。(ヒント: アプリ内ライブラリothermdwiki_en_all_maxi を探します)。

OPFS の仕組み

OPFS はブラウザによって提供されるファイル システムであり、オリジンごとに分離されているため、Android のアプリを対象としたストレージと同様に考えることができます。ファイルは、ユーザーに表示されるファイル システムから OPFS にインポートすることも、直接ダウンロードすることもできます(この API では OPFS 内にファイルを作成することもできます)。OPFS に入ると、それらはデバイスの他の部分から分離されます。パソコンの Chromium ベースのブラウザでは、OPFS からユーザーに表示されるファイル システムにファイルをエクスポートすることもできます。

OPFS を使用するには、まず navigator.storage.getDirectory() を使用して OPFS へのアクセスをリクエストします(await を使用したコードを確認する場合は、元のプライベート ファイル システムをご覧ください)。

return navigator.storage
  .getDirectory()
  .then(function (handle) {
    return processDirHandle(handle);
  })
  .catch(function (err) {
    console.warn('Unable to get the OPFS directory entry', err);
  });

ここで取得されるハンドルは、前述の window.showDirectoryPicker() から取得される FileSystemDirectoryHandle とまったく同じです。つまり、そのハンドルを処理するコードを再利用できます(なお、これを indexedDB に保存する必要はなく、必要なときに取得するだけで済みます)。すでに OPFS にいくつかのファイルがあり、それらを使用するとします。前述のように、関数 iterateAsyncDirEntries() を使用すると、次のようなことができます。

return navigator.storage.getDirectory().then(function (dirHandle) {
  let entries = dirHandle.entries();
  return iterateAsyncDirEntries(entries, [])
    .then(function (archiveList) {
      return archiveList;
    })
    .catch(function (err) {
      console.error('Unable to iterate OPFS entries', err);
    });
});

archiveList 配列から操作するエントリには、引き続き getFile() を使用する必要があります。

OPFS へのファイルのインポート

そもそも、ファイルを OPFS に取り込むにはどうすればよいでしょうか。それほど速くありません。まず、必要な保存容量を見積もる必要があります。また、ユーザーが 97 GB のファイルを収めることができない場合は、容量を使い切らないようにする必要があります。

推定割り当ては、navigator.storage.estimate().then(function (estimate) { … }); で簡単に取得できます。これをどうやってユーザーに表示するかは、少し難しい作業です。Kiwix アプリでは、ユーザーが OPFS を試せるように、チェックボックスのすぐ横に表示される小さなアプリ内パネルを選択しました。

使用済み保存容量の割合(%)と使用可能な残りの保存容量(GB)を示すパネル。

このパネルは、estimate.quotaestimate.usage を使用して入力されます。次に例を示します。

let OPFSQuota; // Global variable, so we don't have to keep checking it
return navigator.storage.estimate().then(function (estimate) {
  const percent = ((estimate.usage / estimate.quota) * 100).toFixed(2);
  OPFSQuota = estimate.quota - estimate.usage;
  document.getElementById('OPFSQuota').innerHTML =
    '<b>OPFS storage quota:</b><br />Used:&nbsp;<b>' +
    percent +
    '%</b>; ' +
    'Remaining:&nbsp;<b>' +
    (OPFSQuota / 1024 / 1024 / 1024).toFixed(2) +
    '&nbsp;GB</b>';
});

ご覧のとおり、ユーザーに表示されるファイル システムから OPFS にファイルを追加できるボタンもあります。File API を使用するだけで、インポートされる必要なファイル オブジェクトを取得できます。実際、window.showOpenFilePicker() は使用しないことが重要です。この方法は Firefox でサポートされていませんが、OPFS は確実にサポートされています

上のスクリーンショットに表示されている [Add file(s)] ボタンは以前のファイル選択ツールではありませんが、クリックまたはタップすると、非表示の以前のファイル選択ツール(<input type="file" multiple … /> 要素)が click() されます。次に、アプリは隠しファイル入力の change イベントをキャプチャし、ファイルのサイズをチェックして、サイズが大きすぎて割り当てに収まらない場合は拒否します。問題がない場合は、追加するかどうかをユーザーに尋ねます。

archiveFilesLegacy.addEventListener('change', function (files) {
  const filesArray = Array.from(files.target.files);
  // Abort if user didn't select any files
  if (filesArray.length === 0) return;
  // Calculate the size of the picked files
  let filesSize = 0;
  filesArray.forEach(function (file) {
    filesSize += file.size;
  });
  // Check the size of the files does not exceed the quota
  if (filesSize > OPFSQuota) {
    // Oh no, files are too big! Tell user...
    console.log('Files would exceed the OPFS quota!');
  } else {
    // Ask user if they're sure... if user said yes...
    return importOPFSEntries(filesArray)
      .then(function () {
        // Tell user we successfully imported the archives
      })
      .catch(function (err) {
        // Tell user there was an error (error catching is important!)
      });
  }
});

元のプライベート ファイル システムに .zim ファイルのリストをユーザーに追加するかどうかをユーザーに尋ねるダイアログ。

Android などの一部のオペレーティング システムでは、アーカイブのインポートは最も高速なオペレーションではないため、アーカイブのインポート中は Kiwix にバナーと小さなスピナーが表示されます。このオペレーションの進捗状況インジケーターを追加する方法はわかっていません。うまくいったら、ポストカードへの回答をお願いします。

Kiwix では、importOPFSEntries() 関数をどのように実装したのでしょうか。これには、fileHandle.createWriteable() メソッドを使用します。これにより、各ファイルを OPFS にストリーミングできます。面倒な作業はすべてブラウザが処理します。(Kiwix では、以前のコードベースを使用するために Promise を使用していますが、この場合、await の方がシンプルな構文を生成し、デラミッド効果を回避しています)。

function importOPFSEntries(files) {
  // Get a handle on the OPFS directory
  return navigator.storage
    .getDirectory()
    .then(function (dir) {
      // Collect the promises for each file that we want to write
      let promises = files.map(function (file) {
        // Create the file and get a writeable handle on it
        return dir
          .getFileHandle(file.name, { create: true })
          .then(function (fileHandle) {
            // Get a writer for the file
            return fileHandle.createWritable().then(function (writer) {
              // Show a banner / spinner, then write the file
              return writer
                .write(file)
                .then(function () {
                  // Finished with this writer
                  return writer.close();
                })
                .catch(function (err) {
                  console.error('There was an error writing to the OPFS!', err);
                });
            });
          })
          .catch(function (err) {
            console.error('Unable to get file handle from OPFS!', err);
          });
      });
      // Return a promise that resolves when all the files have been written
      return Promise.all(promises);
    })
    .catch(function (err) {
      console.error('Unable to get a handle on the OPFS directory!', err);
    });
}

ファイル ストリームを OPFS に直接ダウンロードする

これのバリエーションとして、インターネットから OPFS またはディレクトリ ハンドルを持つ任意のディレクトリ(つまり、window.showDirectoryPicker() で選択されたディレクトリ)にファイルを直接ストリーミングする機能があります。上記のコードと同じ原則を使用しますが、ReadableStream と、リモート ファイルから読み取られたバイトをキューに登録するコントローラで構成される Response を構築します。結果の Response.body は、OPFS 内の新しいファイルのライターにパイプされます。

この場合、Kiwix は ReadableStream を通過するバイト数をカウントできるため、進行状況インジケーターをユーザーに提供します。また、ダウンロード中にアプリを終了しないようユーザーに警告します。コードは少し複雑すぎてここに表示できませんが、このアプリは FOSS アプリであるため、同様のことに関心がある場合はソースを確認できます。Kiwix UI は次のようになります(以下の進行状況の値は、割合が変化したときにのみバナーが更新されるため、[Download progress] パネルがより定期的に更新されるためです)。

アプリを終了しないようユーザーに警告するバーと、.zim アーカイブのダウンロードの進行状況が表示されている Kiwix のユーザー インターフェース。

ダウンロードは長時間を要する場合があるため、Kiwix では処理中もアプリを自由に使用できますが、バナーは常に表示するため、ダウンロードが完了するまでアプリを閉じないようにユーザーに通知します。

アプリ内にミニ ファイル マネージャーを実装する

この時点で、Kiwix PWA デベロッパーは、OPFS にファイルを追加するだけでは不十分であることに気づきました。また、このストレージ領域から不要になったファイルを削除する手段をユーザーに提供する必要がありました。また、OPFS にロックされているファイルをユーザーに表示されるファイル システムにエクスポートして戻すのが理想的です。事実上、アプリにミニ ファイル管理システムを実装することが必要になりました。

ここで、Chrome 用の優れた OPFS Explorer 拡張機能をご紹介します(Edge でも機能します)。デベロッパー ツールにタブが追加され、OPFS の内容を正確に確認したり、不正なファイルや失敗したファイルを削除したりできます。これは、コードが機能しているかどうかの確認、ダウンロードの動作のモニタリング、開発テスト全般のクリーンアップに非常に有用でした。

ファイルのエクスポートは、Kiwix がエクスポートしたファイルを保存しようとしている選択したファイルまたはディレクトリのファイル ハンドルを取得できるかどうかに依存するため、window.showSaveFilePicker() メソッドを使用できるコンテキストでのみ機能します。Kiwix のファイルが数 GB 未満の場合は、メモリ内に blob を作成して URL を指定し、ユーザーに表示されるファイル システムにダウンロードできます。残念ながら、それほど大きなアーカイブでは不可能です。サポートされている場合、エクスポートは非常に簡単です。逆に、ファイルを OPFS に保存するのとほぼ同じです(保存するファイルのハンドルを取得し、window.showSaveFilePicker() で保存する場所を選択するようにユーザーに依頼してから、saveHandlecreateWriteable() を使用します)。リポジトリでコードを確認できます。

ファイルの削除はすべてのブラウザでサポートされており、シンプルな dirHandle.removeEntry('filename') で実行できます。Kiwix の場合、上記のように OPFS エントリを反復処理して、選択したファイルが存在することを最初に確認して確認を求めることができるようにしました。ただし、この作業はすべての人に必要というわけではありません。ここでも、興味があればコードを確認できます。

Kiwix UI にこれらのオプションを提供するボタンを配置せず、アーカイブ リストのすぐ下に小さなアイコンを配置することにしました。これらのアイコンのいずれかをタップすると、アーカイブ リストの色が変わり、ユーザーが何をしようとしているのかを視覚的に把握できるようになります。ユーザーがいずれかのアーカイブをクリックまたはタップすると、対応する操作(エクスポートまたは削除)が実行されます。

.zim ファイルを削除するかどうかをユーザーに尋ねるダイアログ。

最後に、上記で説明したすべてのファイル管理機能(OPFS へのファイルの追加、OPFS へのファイルの直接ダウンロード、ファイルの削除、ユーザーに表示されるファイル システムへのエクスポート)のスクリーンキャスト デモを紹介します。

デベロッパーの仕事に終わりはない

OPFS は PWA のデベロッパーにとって優れたイノベーションであり、ネイティブ アプリとウェブアプリのギャップを埋めるための非常に強力なファイル管理機能を提供します。けれども、開発者たちは悲惨で、これほど満足することはありません。OPFS はほぼ完璧ですが、完璧ではありません。主な機能が Chromium と Firefox の両方のブラウザで動作し、パソコンだけでなく Android にも実装できる点は素晴らしいことです。完全な機能セットが Safari と iOS にも実装されることを期待しています。次のような問題が残っています。

  • Firefox では現在、基となるディスク容量に関係なく、OPFS の割り当ての上限を 10 GB に設定しています。PWA 作成者の多くはこれで十分かもしれませんが、Kiwix の場合はかなり制限されます。幸いなことに Chromium ブラウザは かなり寛容です
  • 現在のところ、window.showSaveFilePicker() が実装されていないため、モバイル ブラウザやパソコンの Firefox でユーザーに表示されるファイル システムに OPFS の大きなファイルをエクスポートすることはできません。これらのブラウザでは、大きなファイルは事実上 OPFS に閉じ込められます。これは、コンテンツへのオープン アクセスや、ユーザー間でのアーカイブ共有という Kiwix の理念に反します。特に、インターネット接続が断続的または高価な分野では、この原則に反します。
  • OPFS 仮想ファイル システムが利用するストレージをユーザーが制御することはできません。これは、モバイル デバイスでは特に問題となります。ユーザーの microSD カードの容量は大量でも、デバイス ストレージの容量はごくわずかです。

しかし全体として、これらは PWA でのファイル アクセスに大きな一歩を踏み出した小さな問題です。Kiwix PWA チームは、File System Access API を最初に提案して設計した Chromium デベロッパーとアドボケイトに深く感謝しています。また、オリジンのプライベート ファイル システムの重要性について、ブラウザ ベンダーの間で合意を得る努力に尽力しています。Kiwix JS PWA では、過去にアプリを悩ませてきた UX の問題の多くが解決されており、Kiwix コンテンツのアクセシビリティを高める取り組みに役立っています。Kiwix PWA をお試しのうえ、デベロッパーに感想をお聞かせください

以下のサイトで PWA の機能に関するリソースをご覧いただけます。