Cách Kiwix PWA giúp người dùng lưu trữ Gigabyte dữ liệu trên Internet để sử dụng khi không có mạng

Geoffrey Kantaris
Geoffrey Kantaris
Stéphane Coillet-Matillon
Stéphane Coillet-Matillon

Mọi người tập trung quanh một chiếc máy tính xách tay đang đứng trên một chiếc bàn đơn giản với một chiếc ghế nhựa ở bên trái. Phông nền trông giống một ngôi trường ở một quốc gia đang phát triển.

Nghiên cứu điển hình này tìm hiểu cách Kiwix, một tổ chức phi lợi nhuận, sử dụng công nghệ Ứng dụng web tiến bộ và API Truy cập hệ thống tệp để cho phép người dùng tải xuống và lưu trữ các bản lưu trữ có dung lượng lớn trên Internet để sử dụng khi không có mạng. Tìm hiểu về cách triển khai kỹ thuật của việc triển khai mã xử lý hệ thống tệp riêng tư gốc (OPFS), một tính năng mới của trình duyệt trong PWA Kiwix giúp tăng cường quản lý tệp, cải thiện quyền truy cập vào tệp lưu trữ mà không cần lời nhắc cấp quyền. Bài viết này thảo luận về những thách thức và nêu bật những bước phát triển tiềm năng trong tương lai của hệ thống tệp mới này.

Giới thiệu về Kiwix

Theo Liên minh Viễn thông Quốc tế, hơn 30 năm sau khi web ra đời, một phần ba dân số thế giới vẫn đang chờ đợi khả năng truy cập Internet đáng tin cậy. Đây có phải là đoạn câu chuyện kết thúc không? Tất nhiên là không. Mọi người tại Kiwix, một tổ chức phi lợi nhuận có trụ sở tại Thuỵ Sĩ, đã phát triển một hệ sinh thái gồm các ứng dụng và nội dung nguồn mở nhằm cung cấp kiến thức cho những người bị hạn chế truy cập hoặc không có quyền truy cập Internet. Ý tưởng của họ là nếu bạn không thể dễ dàng truy cập Internet, thì ai đó có thể tải các tài nguyên chính cho bạn xuống, tại vị trí và thời điểm có kết nối, đồng thời lưu trữ các tài nguyên đó trên máy để sử dụng ngoại tuyến sau này. Nhiều trang web quan trọng, như Wikipedia, Dự án Gutenberg, Stack Exchange hoặc thậm chí các bài nói chuyện của TED, giờ đây có thể được chuyển đổi thành các tệp lưu trữ được nén cao, được gọi là tệp ZIM và được đọc nhanh bằng trình duyệt Kiwix.

Các tệp lưu trữ ZIM sử dụng tính năng nén Zstandard (ZSTD) hiệu quả cao (các phiên bản cũ dùng XZ), chủ yếu để lưu trữ HTML, JavaScript và CSS, trong khi hình ảnh thường được chuyển đổi sang định dạng WebP nén. Mỗi ZIM cũng bao gồm một URL và một chỉ mục tiêu đề. Nén là yếu tố then chốt ở đây, vì toàn bộ Wikipedia bằng tiếng Anh (6, 4 triệu bài viết, cùng với hình ảnh) được nén xuống còn 97 GB sau khi chuyển đổi sang định dạng ZIM.Điều này nghe có vẻ rất thú vị cho đến khi bạn nhận ra rằng tổng kiến thức của con người giờ đây có thể vừa với một chiếc điện thoại Android tầm trung. Nhiều tài nguyên nhỏ hơn cũng được cung cấp, bao gồm các phiên bản theo chủ đề của Wikipedia, chẳng hạn như toán học, y học, v.v.

Kiwix cung cấp nhiều ứng dụng gốc dành cho máy tính (Windows/Linux/macOS) cũng như cho mức sử dụng thiết bị di động (iOS/Android). Tuy nhiên, nghiên cứu điển hình này sẽ tập trung vào Ứng dụng web tiến bộ (PWA) nhằm trở thành một giải pháp chung và đơn giản cho mọi thiết bị có trình duyệt hiện đại.

Chúng tôi sẽ xem xét những thách thức đặt ra trong quá trình phát triển một ứng dụng web toàn cầu cần cung cấp quyền truy cập nhanh vào các kho lưu trữ nội dung lớn hoàn toàn ngoại tuyến, cũng như một số API JavaScript hiện đại, đặc biệt là API Truy cập hệ thống tệpHệ thống tệp riêng tư gốc, nhằm đưa ra các giải pháp sáng tạo và thú vị để giải quyết những thách thức đó.

Một ứng dụng web để dùng khi không có mạng?

Người dùng Kiwix là một nhóm đa dạng với nhiều nhu cầu khác nhau và Kiwix có ít hoặc không có quyền kiểm soát thiết bị và hệ điều hành mà họ sẽ truy cập vào nội dung của mình. Một vài thiết bị trong số này có thể chậm hoặc đã lỗi thời, đặc biệt là ở các khu vực có thu nhập thấp trên thế giới. Mặc dù Kiwix cố gắng bao quát nhiều trường hợp sử dụng nhất có thể, nhưng tổ chức này cũng nhận ra rằng họ có thể tiếp cận nhiều người dùng hơn nữa bằng cách sử dụng phần mềm phổ biến nhất trên mọi thiết bị: trình duyệt web. Vì vậy, lấy cảm hứng từ Luật của Atwood, trong đó nêu rõ rằng Mọi ứng dụng có thể viết bằng JavaScript, cuối cùng cũng sẽ được viết bằng JavaScript, khoảng 10 năm trước, một số nhà phát triển của Kiwix đã bắt đầu chuyển phần mềm Kiwix từ C++ sang JavaScript.

Phiên bản đầu tiên của cổng này có tên Kiwix HTML5 là dành cho hệ điều hành Firefox OS hiện đã ngừng hoạt động và cho các phần mở rộng của trình duyệt. Về cốt lõi, công cụ giải nén C++ (XZ và ZSTD) được biên dịch sang ngôn ngữ JavaScript trung gian là ASM.js, sau đó là Wasm hoặc WebAssembly, sử dụng trình biên dịch Emscripten. Sau đó, chúng tôi đổi tên thành Kiwix JS, các tiện ích trình duyệt vẫn đang được phát triển tích cực.

Trình duyệt ngoại tuyến Kiwix JS

Nhập Ứng dụng web tiến bộ (PWA). Nhận thấy tiềm năng của công nghệ này, các nhà phát triển Kiwix đã xây dựng một phiên bản PWA dành riêng cho Kiwix JS và thiết lập việc bổ sung tính năng tích hợp hệ điều hành để cho phép ứng dụng cung cấp các tính năng giống như gốc, đặc biệt là trong các lĩnh vực sử dụng, cài đặt, xử lý tệp và truy cập hệ thống tệp mà không cần mạng.

PWA ưu tiên ngoại tuyến cực kỳ nhẹ nên rất phù hợp với bối cảnh Internet di động gián đoạn hoặc đắt đỏ. Công nghệ đằng sau tác động này là Service Worker APIAPI Bộ nhớ đệm có liên quan, được tất cả ứng dụng dựa trên Kiwix JS sử dụng. Các API này cho phép các ứng dụng hoạt động như một máy chủ, chặn Các yêu cầu tìm nạp từ tài liệu hoặc bài viết chính đang được xem và chuyển hướng các ứng dụng đó đến phần phụ trợ (JS) để trích xuất và tạo một Phản hồi từ kho lưu trữ ZIM.

Bộ nhớ, lưu trữ ở mọi nơi

Do kích thước lớn của các tệp lưu trữ ZIM, cũng như khả năng lưu trữ và quyền truy cập vào các tệp này, đặc biệt là trên thiết bị di động, có thể là vấn đề lớn nhất đối với các nhà phát triển Kiwix. Nhiều người dùng cuối của Kiwix tải nội dung trong ứng dụng xuống (khi có Internet) để sử dụng ngoại tuyến sau này. Những người dùng khác tải xuống máy tính bằng một tệp torrent, sau đó chuyển sang thiết bị di động hoặc máy tính bảng, cũng như một số nội dung trao đổi qua thẻ USB hoặc ổ đĩa cứng di động ở những khu vực có Internet di động đắt đỏ hoặc không ổn định. Tất cả những cách truy cập nội dung từ các vị trí tuỳ ý mà người dùng có thể truy cập này cần được Kiwix JS và PWA Kiwix hỗ trợ.

Điều ban đầu giúp Kiwix JS có thể đọc các tệp lưu trữ khổng lồ, trong số hàng trăm GB (một trong số các tệp lưu trữ ZIM của chúng tôi có dung lượng 166 GB!) ngay cả trên các thiết bị có bộ nhớ thấp, chính là File API. API này được hỗ trợ phổ biến trong mọi trình duyệt, ngay cả trình duyệt rất cũ. Vì vậy, API này đóng vai trò như một phương án dự phòng chung khi các API mới không được hỗ trợ. Bạn có thể dễ dàng xác định phần tử input trong HTML, như trong trường hợp của Kiwix:

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

Sau khi được chọn, phần tử đầu vào sẽ chứa các đối tượng File (Tệp) mà về cơ bản là siêu dữ liệu tham chiếu đến dữ liệu cơ bản trong bộ nhớ. Về mặt kỹ thuật, phần phụ trợ hướng đối tượng của Kiwix (được viết bằng JavaScript phía máy khách) sẽ đọc các phần nhỏ của kho lưu trữ lớn khi cần. Nếu cần giải nén các lát cắt đó, phần phụ trợ sẽ truyền các lát cắt đó đến bộ giải nén Wasm và nhận thêm các lát cắt nếu được yêu cầu, cho đến khi một blob đầy đủ được giải nén (thường là một bài viết hoặc một tài sản). Điều này có nghĩa là tệp lưu trữ lớn không bao giờ phải được đọc hoàn toàn trong bộ nhớ.

Nhìn chung, File API có một hạn chế khiến các ứng dụng Kiwix JS có vẻ rối rắm và lỗi thời so với ứng dụng gốc: đòi hỏi người dùng phải chọn tệp lưu trữ bằng bộ chọn tệp hoặc kéo và thả tệp vào ứng dụng, mỗi lần khởi chạy ứng dụng, vì với API này, không có cách nào để duy trì quyền truy cập từ phiên này sang phiên khác.

Để giảm thiểu trải nghiệm người dùng kém này, như nhiều nhà phát triển, ban đầu các nhà phát triển Kiwix JS đã đi theo lộ trình electron. ElectronJS là một khung tuyệt vời cung cấp các tính năng mạnh mẽ, bao gồm cả quyền truy cập đầy đủ vào hệ thống tệp bằng cách sử dụng các API Nút. Tuy nhiên, phương pháp này có một số hạn chế phổ biến:

  • Ứng dụng này chỉ chạy trên hệ điều hành máy tính để bàn.
  • Tệp này lớn và nặng (70 MB – 100 MB).

Kích thước của các ứng dụng electron, do thực tế là một bản sao hoàn chỉnh của Chromium đi kèm với mọi ứng dụng, sẽ rất bất lợi với chỉ 5,1 MB đối với PWA thu nhỏ và đi kèm!

Vậy có cách nào Kiwix có thể cải thiện tình hình cho người dùng PWA không?

API Truy cập hệ thống tệp để giải cứu

Khoảng năm 2019, Kiwix phát hiện ra một API mới nổi đang trải qua quá trình dùng thử theo nguyên gốc trong Chrome 78, sau đó được gọi là Native File System API (API Hệ thống tệp gốc). Phiên bản này hứa hẹn khả năng lấy tên người dùng xử lý tệp cho một tệp hoặc thư mục và lưu trữ tệp đó trong cơ sở dữ liệu IndexedDB. Điều quan trọng là tên người dùng này vẫn tồn tại giữa các phiên hoạt động của ứng dụng. Vì vậy, người dùng không buộc phải chọn lại tệp hoặc thư mục khi khởi chạy lại ứng dụng (mặc dù họ phải trả lời lời nhắc cấp quyền nhanh). Vào thời điểm phát hành chính thức, API này được đổi tên thành File System Access API (API Truy cập hệ thống tệp) và các phần cốt lõi được whatWG chuẩn hoá là File System API (FSA).

Vậy phần Quyền truy cập hệ thống tệp của API hoạt động như thế nào? Một số điểm quan trọng cần lưu ý:

  • Đây là một API không đồng bộ (ngoại trừ các hàm chuyên biệt trong Web Workers).
  • Bộ chọn tệp hoặc thư mục phải được khởi chạy theo phương thức lập trình bằng cách chụp ảnh cử chỉ của người dùng (nhấp hoặc nhấn vào một thành phần trên giao diện người dùng).
  • Để người dùng cấp lại quyền truy cập vào tệp đã chọn trước đó (trong phiên mới), cũng cần có cử chỉ của người dùng. Trên thực tế, trình duyệt sẽ từ chối hiển thị lời nhắc cấp quyền nếu không phải do cử chỉ của người dùng khởi tạo.

Mã này tương đối đơn giản, ngoài việc phải sử dụng API IndexedDB khó hiểu để lưu trữ các tệp và thư mục xử lý. Tin vui là có một số thư viện đảm nhiệm nhiều công việc cho bạn, chẳng hạn như browser-fs-access. Tại Kiwix JS, chúng tôi đã quyết định làm việc trực tiếp với các API được ghi chép rất đầy đủ.

Mở bộ chọn tệp và thư mục

Mở bộ chọn tệp có dạng như sau (ở đây là Promise, nhưng nếu bạn thích đường async/await hơn, hãy xem hướng dẫn về Chrome dành cho nhà phát triển):

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,
    );
  });

Lưu ý để đơn giản, mã này chỉ xử lý tệp được chọn đầu tiên (và cấm chọn nhiều tệp). Trong trường hợp muốn cho phép chọn nhiều tệp bằng { multiple: true }, bạn chỉ cần gói tất cả Hứa hẹn sẽ xử lý từng tên người dùng trong một câu lệnh Promise.all().then(...), ví dụ:

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);
)};

Tuy nhiên, việc chọn nhiều tệp có lẽ tốt hơn là nên yêu cầu người dùng chọn thư mục chứa các tệp đó thay vì các tệp riêng lẻ trong đó, đặc biệt là vì người dùng Kiwix có xu hướng sắp xếp tất cả các tệp ZIM của họ trong cùng một thư mục. Mã để chạy bộ chọn thư mục gần giống như trên, ngoại trừ việc bạn sử dụng window.showDirectoryPicker.then(function (dirHandle) { … });.

Xử lý tệp hoặc thư mục xử lý

Sau khi đã có tên người dùng, bạn cần xử lý tên đó để hàm processFileHandle có thể có dạng như sau:

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;
  });
}

Lưu ý rằng bạn phải cung cấp hàm để lưu trữ tên người dùng tệp, không có phương thức nào thuận tiện cho việc này, trừ phi bạn sử dụng thư viện trừu tượng. Bạn có thể xem cách triển khai chức năng này của Kiwix trong tệp cache.js, nhưng có thể đơn giản hoá đáng kể nếu chỉ dùng để lưu trữ và truy xuất tên xử lý tệp hoặc thư mục.

Xử lý thư mục phức tạp hơn một chút vì bạn phải lặp lại các mục trong thư mục đã chọn bằng entries.next() không đồng bộ để tìm các tệp hoặc loại tệp bạn muốn. Có nhiều cách để làm điều đó, nhưng đây là mã được dùng trong PWA Kiwix như sau:

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);
    });
}

Lưu ý rằng đối với mỗi mục trong entryList, sau này bạn sẽ cần lấy tệp bằng entry.getFile().then(function (file) { … }) khi cần sử dụng hoặc mục tương đương bằng cách sử dụng const file = await entry.getFile() trong async function.

Chúng ta có thể tiến xa hơn không?

Việc yêu cầu người dùng cấp quyền bắt đầu bằng cử chỉ của người dùng trong các lần khởi chạy ứng dụng tiếp theo sẽ gây thêm chút phiền toái khi mở tệp và thư mục (lại), nhưng vẫn linh hoạt hơn nhiều so với việc buộc chọn lại một tệp. Nhà phát triển Chromium hiện đang hoàn thiện mã để cho phép các quyền lâu dài đối với các PWA đã cài đặt. Đây là điều mà rất nhiều nhà phát triển PWA đã mong đợi và đã mong đợi.

Nhưng nếu chúng ta không phải chờ thì sao?! Các nhà phát triển của Kiwix gần đây nhận thấy có thể loại bỏ mọi lời nhắc cấp quyền ngay bây giờ, bằng cách sử dụng một tính năng mới của API Truy cập tệp được cả trình duyệt Chromium và Firefox hỗ trợ (và được Safari hỗ trợ một phần, nhưng vẫn thiếu FileSystemWritableFileStream). Tính năng mới này là Hệ thống tệp riêng tư gốc.

Chuyển sang định dạng gốc hoàn toàn: Hệ thống tệp riêng tư gốc

Hệ thống tệp riêng tư gốc (OPFS) vẫn là một tính năng thử nghiệm trong PWA Kiwix, nhưng nhóm thực sự hào hứng khuyến khích người dùng dùng thử vì tính năng này giúp thu hẹp khoảng cách giữa ứng dụng gốc và ứng dụng web. Dưới đây là các lợi ích chính:

  • Người dùng có thể truy cập vào các bản lưu trữ trong OPFS mà không cần nhắc cấp quyền, ngay cả khi khởi chạy. Người dùng có thể tiếp tục đọc bài viết và duyệt qua bản lưu trữ từ nơi họ đã dừng lại trong phiên trước đó mà hoàn toàn không gặp khó khăn.
  • Ứng dụng này cung cấp quyền truy cập được tối ưu hoá cao vào các tệp được lưu trữ trong đó: trên Android, chúng tôi thấy tốc độ được cải thiện nhanh hơn từ 5 đến 10 lần.

Quyền truy cập tệp tiêu chuẩn trong Android bằng API Tệp sẽ rất chậm, đặc biệt là (thường xảy ra với người dùng Kiwix) nếu các tệp lưu trữ lớn được lưu trữ trên thẻ microSD thay vì trong bộ nhớ của thiết bị. Mọi thứ đã thay đổi với API mới này. Mặc dù hầu hết người dùng sẽ không thể lưu trữ tệp 97 GB trong OPFS (tệp này chiếm bộ nhớ của thiết bị chứ không phải bộ nhớ của thẻ nhớ microSD), nhưng đây là lựa chọn hoàn hảo để lưu trữ các tệp lưu trữ có kích thước từ nhỏ đến vừa. Bạn muốn có bách khoa toàn thư y tế hoàn chỉnh nhất từ WikiProject Y học phải không? Không sao cả, với dung lượng 1,7 GB, nó dễ dàng phù hợp với OPFS! (Mẹo: tìm othermdwiki_en_all_maxi trong thư viện trong ứng dụng.)

Cách hoạt động của OPFS

OPFS là một hệ thống tệp do trình duyệt cung cấp, riêng biệt cho từng nguồn gốc, có thể được coi là tương tự như bộ nhớ có giới hạn trong ứng dụng trên Android. Bạn có thể nhập tệp vào OPFS từ hệ thống tệp hiển thị cho người dùng hoặc tải tệp xuống trực tiếp (API cũng cho phép tạo tệp trong OPFS). Sau khi nằm trong OPFS, các thiết bị này sẽ được tách riêng với phần còn lại của thiết bị. Trên các trình duyệt dựa trên Chromium dành cho máy tính, bạn cũng có thể xuất lại các tệp từ OPFS sang hệ thống tệp hiển thị cho người dùng.

Để sử dụng OPFS, bước đầu tiên là yêu cầu quyền truy cập vào tệp bằng navigator.storage.getDirectory() (xin nhắc lại, nếu bạn muốn xem mã bằng await, hãy đọc Hệ thống tệp riêng tư gốc):

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

Tên người dùng mà bạn nhận được từ đây cũng giống như loại FileSystemDirectoryHandle mà bạn nhận được từ window.showDirectoryPicker() đã đề cập ở trên, có nghĩa là bạn có thể sử dụng lại mã để xử lý việc đó (và rất may là bạn không cần phải lưu trữ mã này trong indexedDB – chỉ cần nhận khi cần). Giả sử bạn đã có một số tệp trong OPFS và muốn sử dụng các tệp đó, sau đó sử dụng hàm iterateAsyncDirEntries() đã thấy trước đó, bạn có thể làm những việc như sau:

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);
    });
});

Đừng quên là bạn vẫn cần sử dụng getFile() trên mọi mục nhập mà bạn muốn xử lý trong mảng archiveList.

Nhập tệp vào OPFS

Vậy bạn làm cách nào để tải các tệp vào OPFS trước tiên? Không nhanh lắm! Trước tiên, bạn cần ước tính dung lượng lưu trữ mà bạn phải xử lý và đảm bảo rằng người dùng không cố gắng chuyển tệp 97 GB vào nếu tệp đó không phù hợp.

Bạn có thể dễ dàng tải hạn mức ước tính: navigator.storage.estimate().then(function (estimate) { … });. Hơi khó hơn là tìm cách hiển thị thông tin này cho người dùng. Trong ứng dụng Kiwix, chúng tôi đã chọn một bảng điều khiển nhỏ trong ứng dụng hiển thị ngay bên cạnh hộp đánh dấu cho phép người dùng dùng thử OPFS:

Bảng điều khiển hiển thị dung lượng đã sử dụng tính theo phần trăm và dung lượng lưu trữ còn lại tính bằng Gigabyte.

Bảng điều khiển này được điền sẵn bằng cách sử dụng estimate.quotaestimate.usage, ví dụ:

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>';
});

Như bạn có thể thấy, cũng có một nút cho phép người dùng thêm tệp vào OPFS từ hệ thống tệp mà người dùng thấy được. Tin vui ở đây là bạn chỉ cần sử dụng API Tệp để lấy đối tượng (hoặc các đối tượng) Tệp cần thiết sẽ được nhập. Trên thực tế, bạn không nên sử dụng window.showOpenFilePicker() vì phương thức này không được Firefox hỗ trợ, trong khi OPFS được hỗ trợ nhiều nhất.

Nút Add file(s) (Thêm tệp) mà bạn thấy trong ảnh chụp màn hình ở trên không phải là bộ chọn tệp cũ, mà có click() một bộ chọn cũ ẩn (phần tử <input type="file" multiple … />) khi người dùng nhấp hoặc nhấn vào nút này. Sau đó, ứng dụng chỉ ghi lại sự kiện change của dữ liệu đầu vào của tệp ẩn, kiểm tra kích thước của các tệp và từ chối các tệp nếu các tệp đó quá lớn so với hạn mức. Nếu mọi thứ đều ổn, hãy hỏi người dùng xem họ có muốn thêm họ không:

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!)
      });
  }
});

Hộp thoại hỏi người dùng xem họ có muốn thêm danh sách các tệp .zim vào hệ thống tệp riêng tư gốc hay không.

Vì trên một số hệ điều hành, như Android, việc nhập tệp lưu trữ không phải là thao tác nhanh nhất, nên Kiwix cũng hiển thị một biểu ngữ và một vòng quay nhỏ trong khi nhập các tệp lưu trữ. Nhóm đã không tìm ra cách thêm chỉ báo tiến trình cho thao tác này: nếu bạn đã tìm ra, vui lòng trả lời trên bưu thiếp!

Vậy Kiwix đã triển khai hàm importOPFSEntries() như thế nào? Việc này bao gồm việc sử dụng phương thức fileHandle.createWriteable(). Phương thức này cho phép truyền trực tuyến từng tệp đến OPFS một cách hiệu quả. Tất cả công việc khó khăn đều do trình duyệt xử lý. (Kiwix sử dụng Promises tại đây vì lý do liên quan đến cơ sở mã cũ của chúng tôi, nhưng phải nói rằng trong trường hợp này, await sẽ tạo ra một cú pháp đơn giản hơn và tránh được kim tự tháp của hiệu ứng chết chóc.)

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);
    });
}

Tải luồng tệp trực tiếp xuống OPFS

Một biến thể khác là khả năng truyền trực tuyến tệp từ Internet trực tiếp vào OPFS hoặc vào bất kỳ thư mục nào mà bạn có xử lý thư mục (tức là các thư mục được chọn bằng window.showDirectoryPicker()). API này sử dụng các nguyên tắc tương tự như mã trên nhưng tạo Response bao gồm ReadableStream và một bộ điều khiển xếp hàng các byte đã đọc từ tệp từ xa. Sau đó, Response.body thu được sẽ được chuyển đến người ghi của tệp mới trong OPFS.

Trong trường hợp này, Kiwix có thể đếm các byte truyền qua ReadableStream, do đó, cung cấp chỉ báo tiến trình cho người dùng, đồng thời cảnh báo họ không nên thoát khỏi ứng dụng trong quá trình tải xuống. Mã hơi phức tạp nên không thể hiển thị ở đây, nhưng vì ứng dụng của chúng tôi là một ứng dụng FOSS, nên bạn có thể xem nguồn nếu muốn thực hiện việc tương tự. Đây là giao diện người dùng Kiwix (các giá trị tiến trình khác nhau được hiển thị bên dưới là vì giao diện này chỉ cập nhật biểu ngữ khi tỷ lệ phần trăm thay đổi, nhưng cập nhật bảng Tiến trình tải xuống thường xuyên hơn):

Giao diện người dùng của Kiwix có thanh ở dưới cùng để cảnh báo người dùng không thoát khỏi ứng dụng và cho thấy tiến trình tải tệp lưu trữ .zim xuống.

Vì việc tải xuống có thể mất nhiều thời gian, nên Kiwix cho phép người dùng thoải mái sử dụng ứng dụng trong quá trình này, nhưng vẫn đảm bảo biểu ngữ luôn hiển thị để người dùng được nhắc không đóng ứng dụng cho đến khi thao tác tải xuống hoàn tất.

Triển khai trình quản lý tệp mini trong ứng dụng

Tại thời điểm này, các nhà phát triển PWA Kiwix nhận ra rằng việc thêm tệp vào OPFS là chưa đủ. Ứng dụng cũng cần cung cấp cho người dùng cách xoá các tệp họ không cần nữa khỏi khu vực lưu trữ này, và tốt nhất là xuất mọi tệp bị khoá trong OPFS sang hệ thống tệp mà người dùng nhìn thấy. Để thực sự hiệu quả, việc triển khai hệ thống quản lý tệp nhỏ bên trong ứng dụng là điều cần thiết.

Cảm ơn bạn đã sử dụng tiện ích tuyệt vời OPFS Explorer dành cho Chrome (cũng hoạt động trong Edge). Thư viện này thêm một thẻ trong các công cụ cho nhà phát triển để cho phép bạn xem chính xác nội dung trong OPFS, đồng thời xoá các tệp lừa đảo hoặc không thành công. Bạn nên kiểm tra xem mã có đang hoạt động hay không, theo dõi hành vi tải xuống và thường xuyên dọn dẹp các thử nghiệm phát triển của chúng tôi.

Xuất tệp phụ thuộc vào khả năng lấy tên người dùng của tệp trên một tệp hoặc thư mục đã chọn mà Kiwix sẽ lưu tệp đã xuất, vì vậy, tính năng này chỉ hoạt động trong các ngữ cảnh có thể sử dụng phương thức window.showSaveFilePicker(). Nếu tệp Kiwix nhỏ hơn vài GB, chúng ta có thể tạo một blob trong bộ nhớ, cung cấp cho tệp URL đó, sau đó tải tệp này xuống hệ thống tệp mà người dùng hiển thị. Rất tiếc, bạn không thể làm điều đó với những tệp lưu trữ lớn như vậy. Nếu được hỗ trợ, việc xuất khá đơn giản: gần như tương tự như khi lưu một tệp vào OPFS (nhận xử lý tệp cần lưu, yêu cầu người dùng chọn một vị trí để lưu bằng window.showSaveFilePicker(), sau đó sử dụng createWriteable() trên saveHandle). Bạn có thể xem mã trong kho lưu trữ.

Tất cả trình duyệt đều hỗ trợ tính năng xoá tệp và bạn có thể thực hiện được việc này bằng một dirHandle.removeEntry('filename') đơn giản. Trong trường hợp của Kiwix, chúng tôi muốn lặp lại các mục OPFS như đã làm ở trên để có thể kiểm tra xem tệp đã chọn có tồn tại trước hay không và yêu cầu xác nhận, nhưng có thể không cần thiết cho mọi người. Xin nhắc lại, bạn có thể kiểm tra mã của chúng tôi nếu quan tâm.

Chúng tôi đã quyết định không làm lộn xộn giao diện người dùng Kiwix với các nút cung cấp các tuỳ chọn này mà thay vào đó, hãy đặt các biểu tượng nhỏ ngay bên dưới danh sách lưu trữ. Thao tác nhấn vào một trong các biểu tượng này sẽ thay đổi màu của danh sách lưu trữ, như một gợi ý trực quan cho người dùng về những gì họ sẽ thực hiện. Sau đó, người dùng nhấp hoặc nhấn vào một trong các nội dung lưu trữ và thao tác tương ứng (xuất hoặc xoá) sẽ được thực hiện (sau khi xác nhận).

Hộp thoại hỏi người dùng xem họ có muốn xoá tệp .zim hay không.

Cuối cùng, đây là bản minh hoạ bản ghi màn hình về tất cả các tính năng quản lý tệp được thảo luận ở trên – thêm một tệp vào OPFS, trực tiếp tải tệp xuống, xoá tệp và xuất tệp sang hệ thống tệp mà người dùng thấy được.

Công việc của nhà phát triển không bao giờ được hoàn tất

OPFS là một cải tiến tuyệt vời dành cho các nhà phát triển PWA, cung cấp các tính năng quản lý tệp thực sự mạnh mẽ, giúp thu hẹp khoảng cách giữa ứng dụng gốc và ứng dụng web. Nhưng nhà phát triển thật sự là một nhóm tồi tệ, họ chẳng bao giờ hài lòng lắm! OPFS gần như hoàn hảo nhưng chưa hoàn toàn chính xác... Thật tuyệt vời khi các tính năng chính hoạt động được trên cả trình duyệt Chromium và Firefox và các tính năng đó được triển khai trên Android cũng như máy tính để bàn. Chúng tôi hy vọng bộ tính năng đầy đủ cũng sẽ sớm được triển khai trong Safari và iOS. Các vấn đề sau vẫn còn:

  • Firefox hiện đặt giới hạn 10 GB cho hạn mức OPFS, bất kể dung lượng ổ đĩa cơ bản là bao nhiêu. May mắn là trình duyệt Chromium được sử dụng rộng rãi hơn nhiều.
  • Hiện tại, bạn không thể xuất các tệp lớn từ OPFS sang hệ thống tệp hiển thị cho người dùng trên các trình duyệt dành cho thiết bị di động hoặc Firefox dành cho máy tính vì window.showSaveFilePicker() chưa được triển khai. Trong các trình duyệt này, các tệp lớn được giữ lại hiệu quả trong OPFS. Điều này đi ngược lại với nguyên tắc Kiwix về việc truy cập mở vào nội dung và khả năng chia sẻ các bản lưu trữ giữa những người dùng, đặc biệt là trong những khu vực kết nối Internet gián đoạn hoặc đắt đỏ.
  • Người dùng không thể kiểm soát dung lượng lưu trữ mà hệ thống tệp ảo OPFS sẽ sử dụng. Điều này đặc biệt rắc rối trên các thiết bị di động, khi người dùng có thể có một lượng lớn dung lượng trên thẻ nhớ microSD, nhưng lại có một lượng rất nhỏ trên bộ nhớ của thiết bị.

Nhưng nhìn chung, đây chỉ là những vấn đề nhỏ trong bước tiến lớn về quyền truy cập vào tệp trong PWA. Nhóm PWA Kiwix rất biết ơn các nhà phát triển và những người ủng hộ Chromium, những người lần đầu tiên đề xuất và thiết kế API Truy cập hệ thống tệp, cũng như nỗ lực hết mình để đạt được sự đồng thuận giữa các nhà cung cấp trình duyệt về tầm quan trọng của Hệ thống tệp riêng tư gốc. Đối với PWA Kiwix JS, nền tảng này đã giải quyết rất nhiều vấn đề về trải nghiệm người dùng ảnh hưởng đến ứng dụng trong quá khứ, đồng thời giúp chúng tôi nâng cao khả năng tiếp cận nội dung trên Kiwix cho mọi người. Vui lòng làm quen với PWA Kiwixcho các nhà phát triển biết suy nghĩ của bạn!

Để tham khảo một số tài nguyên hữu ích về các chức năng của PWA, hãy tham khảo các trang web sau: