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ụ tập quanh chiếc máy tính xách tay, đứ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. Bối cảnh trông giống như một trường học ở một nước đang phát triển.

Nghiên cứu điển hình này tìm hiểu cách tổ chức phi lợi nhuận Kiwix 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 kho lưu trữ 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 mã xử lý Hệ thống tệp riêng của nguồn gốc (OPFS), một tính năng trình duyệt mới trong PWA Kiwix giúp tăng cường khả năng quản lý tệp, cung cấp quyền truy cập tốt hơn vào các bản 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ề các thách thức và nêu bật những phát triển tiềm năng trong tương lai trong hệ thống tệp mới này.

Giới thiệu về Kiwix

Hơn 30 năm sau khi Internet ra đời, theo Liên minh Viễn thông Quốc tế, một phần ba dân số thế giới vẫn đang chờ có quyền truy cập đáng tin cậy vào Internet. Đây có phải là nơi câu chuyện kết thúc không? Tất nhiên là không. Nhóm nghiên cứu tại Kiwix, một tổ chức phi lợi nhuận ở 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 có hạn chế 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 xuống cho bạn, tại nơi 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, chẳng hạn như Wikipedia, Project Gutenberg, Stack Exchange hoặc thậm chí là các cuộc nói chuyện trên TED, hiện có thể được chuyển đổi thành tệp lưu trữ được nén chặt gọi là tệp ZIM và được trình duyệt Kiwix đọc ngay lập tức.

Các tệp lưu trữ ZIM sử dụng phương thức nén Zstandard (ZSTD) cực kỳ hiệu quả (các phiên bản cũ sử 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 tệp ZIM cũng bao gồm một URL và một chỉ mục tiêu đề. Việc 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 thành 97 GB sau khi chuyển đổi sang định dạng ZIM. Nghe có vẻ như rất nhiều, cho đến khi bạn nhận ra rằng tổng hợp tất cả kiến thức của con người hiện 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 Wikipedia theo chủ đề, chẳng hạn như toán học, y học, v.v.

Kiwix cung cấp một loạt ứng dụng gốc nhắm mục tiêu đến mức sử dụng trên máy tính (Windows/Linux/macOS) cũng như 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 mục đích trở thành giải pháp đơn giản và phổ quát cho mọi thiết bị có trình duyệt hiện đại.

Chúng ta sẽ xem xét các thách thức trong việc phát triển một ứng dụng Web phổ biến cần cung cấp quyền truy cập nhanh vào các bản lưu trữ nội dung lớn hoàn toàn ngoại tuyến và một số API JavaScript hiện đại, đặc biệt là File System Access API (API truy cập hệ thống tệp) và Origin Private File System (Hệ thống tệp riêng của nguồn gốc), cung cấp các giải pháp sáng tạo và thú vị cho những thách thức đó.

Ứng dụng web để sử 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ó rất ít hoặc không có quyền kiểm soát đối với các thiết bị và hệ điều hành mà họ sử dụng để truy cập vào nội dung của mình. Một số thiết bị trong số này có thể chạy 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 gồm 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 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 Atwood, trong đó nêu rõ rằng Mọi ứng dụng có thể viết được bằng JavaScript, cuối cùng sẽ được viết bằng JavaScript, một số nhà phát triển của Kiwix khoảng 10 năm trước đã bắt đầu chuyển phần mềm Kiwix từ C++ sang JavaScript.

Phiên bản đầu tiên của bản chuyển đổi này, có tên là Kiwix HTML5, dành cho Firefox OS hiện không còn hoạt động và các tiện ích trình duyệt. Cốt lõi của công cụ này là (và) một 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 này được đổi tên thành Kiwix JS, các tiện ích của trình duyệt vẫn đang được phát triển tích cực.

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

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 riêng của Kiwix JS và bắt đầu thêm 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 ngoại tuyến, cài đặt, xử lý tệp và truy cập hệ thống tệp.

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

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

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

Ban đầu, yếu tố giúp Kiwix JS có thể đọc các tệp lưu trữ khổng lồ lên đến hàng trăm GB (một trong số các bản lưu trữ ZIM của chúng tôi có bộ nhớ 166 GB!) ngay cả trên các thiết bị có bộ nhớ thấp, là API Tệp. API này được hỗ trợ trên mọi trình duyệt, ngay cả các trình duyệt rất cũ, do đó, API này đóng vai trò là phương án dự phòng chung khi các API mới hơn không được hỗ trợ. Việc này cũng dễ dàng như việc xác định phần tử input trong HTML, 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 này sẽ chứa các đối tượng Tệp 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 thuần tuý phía máy khách, sẽ đọc các lát cắt nhỏ của tệp lưu trữ lớn khi cần. Nếu cần giải nén các lát đó, phần phụ trợ sẽ chuyển chúng đến bộ giải nén Wasm, 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à bạn không bao giờ phải đọc toàn bộ tệp lưu trữ lớn vào bộ nhớ.

Mặc dù phổ biến, nhưng File API có một hạn chế khiến các ứng dụng Kiwix JS trông cồng kềnh và lỗi thời so với các ứng dụng gốc: API này yêu cầu người dùng chọn các bản lưu trữ bằng công cụ chọn tệp hoặc kéo và thả một tệp vào ứng dụng, mỗi khi 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, giống như nhiều nhà phát triển khác, các nhà phát triển Kiwix JS ban đầu đã sử dụng 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ác API Node. Tuy nhiên, phương pháp này có một số hạn chế đã được biết đến:

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

Kích thước của các ứng dụng Electron, do mỗi ứng dụng có một bản sao hoàn chỉnh của Chromium, so với chỉ 5,1 MB của PWA tối thiểu và đi kèm!

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

File System Access API (API Truy cập hệ thống tệp) giúp giải quyết vấn đề

Vào khoảng năm 2019, Kiwix nhận thấy một API mới nổi đang trải qua thử nghiệm nguồn gốc trong Chrome 78, sau đó được gọi là API Hệ thống tệp gốc. API này hứa hẹn khả năng lấy một handle tệp cho một tệp hoặc thư mục và lưu trữ handle đó trong cơ sở dữ liệu IndexedDB. Quan trọng là, tên người dùng này vẫn tồn tại giữa các phiên ứng dụng, vì vậy, người dùng không bị 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 một lời nhắc cấp quyền nhanh). Vào thời điểm phát hành công khai, 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 chuẩn hoá bởi WHATWG thành File System API (FSA).

Vậy phầ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 Worker).
  • Bạn phải chạy công cụ chọn tệp hoặc thư mục theo phương thức lập trình bằng cách ghi lại cử chỉ của người dùng (nhấp hoặc nhấn vào một phần tử trên giao diện người dùng).
  • Để người dùng cấp lại quyền truy cập vào một tệp đã chọn trước đó (trong một phiên mới), người dùng cũng cần thực hiện một cử chỉ. 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 được người dùng bắt đầu bằng một cử chỉ.

Mã này tương đối đơn giản, ngoại trừ việc phải sử dụng API IndexedDB cồng kềnh để lưu trữ các tay điều khiển tệp và thư mục. Tin vui là có một số thư viện giúp bạn thực hiện nhiều thao tác nặng 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 nhận rất kỹ lưỡng.

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

Mở bộ chọn tệp có dạng như sau (ở đây sử dụng Promises, nhưng nếu bạn thích async/await đường 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,
    );
  });

Xin lưu ý rằng để đơn giản, mã này chỉ xử lý tệp đã 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ả các Lời hứa 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, tốt hơn là bạn nên chọn nhiều tệp bằng cách 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 thư mục, đặc biệt là vì người dùng Kiwix có xu hướng sắp xếp tất 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ư mã ở trên, ngoại trừ việc bạn sử dụng window.showDirectoryPicker.then(function (dirHandle) { … });.

Đang 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ý để 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;
  });
}

Xin 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 thuận tiện nào cho việc này, trừ phi bạn sử dụng thư viện trừu tượng. Bạn có thể thấy cách triển khai của Kiwix trong tệp cache.js. Tuy nhiên, cách này có thể được đơn giản hoá đáng kể nếu chỉ dùng để lưu trữ và truy xuất một ô điều khiển 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 nhập trong thư mục đã chọn với entries.next() không đồng bộ để tìm các tệp hoặc loại tệp mà bạn muốn. Có nhiều cách để làm việc đó, nhưng dưới đây là mã được sử dụng trong PWA Kiwix, theo sơ lược:

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 nhập 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 tương đương bằng cách sử dụng const file = await entry.getFile() trong async function.

Chúng ta có thể làm gì khác không?

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

Nhưng nếu chúng ta không phải chờ đợi thì sao?! Gần đây, nhà phát triển Kiwix nhận thấy rằng có thể loại bỏ tất cả 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 nổi bật 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 gốc riêng tư.

Sử dụng hoàn toàn mã gốc: Hệ thống tệp riêng tư của Origin

Hệ thống tệp riêng của nguồn gốc (OPFS) vẫn là một tính năng thử nghiệm trong PWA Kiwix, nhưng nhóm rất vui mừng khi khuyến khích người dùng dùng thử vì tính năng này phần lớn 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à những lợi ích chính:

  • Bạn có thể truy cập vào các tệp lưu trữ trong OPFS mà không cần lời nhắc cấp quyền, ngay cả khi khởi chạy. Người dùng có thể tiếp tục đọc một bài viết và duyệt qua một mục lưu trữ, từ nơi họ đã dừng lại trong phiên trước đó mà hoàn toàn không phiền hà.
  • Công cụ này tối ưu hoá quyền truy cập vào các tệp được lưu trữ trong đó: trên Android, chúng tôi nhận thấy tốc độ cải tiế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 File API rất chậm, đặc biệt là (như 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ớ thiết bị. Tất cả những điều đó sẽ 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 (tiêu tốn bộ nhớ thiết bị, chứ không phải bộ nhớ thẻ 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 trung bình. Bạn muốn có bách khoa toàn thư y học đầy đủ nhất của WikiProject Medicine? Không vấn đề gì, với dung lượng 1,7 GB, tệp này dễ dàng vừa với OPFS! (Mẹo: tìm other (khác) → mdwiki_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. Có thể nhập các tệp vào OPFS từ hệ thống tệp mà người dùng nhìn thấy, hoặc có thể tải các tệp đó xuống trực tiếp vào OPFS (API này cũng cho phép tạo tệp trong OPFS). Khi đã ở trong OPFS, chúng sẽ được tách biệt với phần còn lại của thiết bị. Trên trình duyệt dựa trên Chromium dành cho máy tính, bạn cũng có thể xuất tệp từ OPFS trở lại 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 đó, bằng cách sử dụng navigator.storage.getDirectory() (một lần nữa, nếu bạn muốn xem mã bằng await, hãy đọc Hệ thống tệp gốc riêng tư):

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ừ thao tác này chính là loại FileSystemDirectoryHandle mà bạn nhận được từ window.showDirectoryPicker() được đề cập ở trên, tức là bạn có thể sử dụng lại mã xử lý tên người dùng đó (và thật may là bạn không cần lưu trữ tên người dùng này trong indexedDB – chỉ cần lấy tên người dùng 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() đã hiển thị trước đó, bạn có thể làm 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);
    });
});

Hãy nhớ rằng 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ý từ mảng archiveList.

Nhập tệp vào OPFS

Vậy làm cách nào để đưa tệp vào OPFS? Đừng vội! Trước tiên, bạn cần ước tính dung lượng bộ nhớ mà bạn có thể sử dụng và đảm bảo rằng người dùng không cố gắng tải tệp 97 GB nếu không vừa.

Bạn có thể dễ dàng xem hạn mức ước tính: navigator.storage.estimate().then(function (estimate) { … });. Khó hơn một chút là việc tìm ra 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 cho biết dung lượng bộ nhớ đã sử dụng theo tỷ lệ phần trăm và dung lượng bộ nhớ còn trống theo GB.

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 hiển thị cho người dùng. Tin vui là bạn chỉ cần sử dụng File API (API Tệp) để lấy đối tượng Tệp (hoặc các đối tượng) 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ợ chắc chắn nhất.

Nút Add file(s) (Thêm (các) tệp) hiển thị trong ảnh chụp màn hình ở trên không phải là bộ chọn tệp cũ, nhưng nút này sẽ click() một bộ chọn tệp cũ ẩn (phần tử <input type="file" multiple … />) khi được nhấp hoặc nhấn vào. Sau đó, ứng dụng chỉ ghi lại sự kiện change của dữ liệu đầu vào tệp ẩn, kiểm tra kích thước của các tệp và từ chối 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 các thông tin đó 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 một 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, chẳng hạn 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 tệp lưu trữ. Nhóm chúng tôi chưa 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 cách, 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 liên quan đến việc sử dụng phương thức fileHandle.createWriteable(), cho phép truyền trực tuyến từng tệp vào 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 đang sử dụng Promises ở đâ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 tạo ra cú pháp đơn giản hơn và tránh được hiệu ứng kim tự tháp của sự diệt vong.)

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 trực tuyến tệp xuống OPFS

Một biến thể của phương thức này 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ó một tay điều khiển thư mục (tức là các thư mục được chọn bằng window.showDirectoryPicker()). Phương thức này sử dụng các nguyên tắc giống như mã ở trên, nhưng tạo một Response bao gồm một ReadableStream và một bộ điều khiển đưa các byte đọc được từ tệp từ xa vào hàng đợi. Sau đó, Response.body thu được sẽ được chuyển vào trình ghi của tệp mới bên trong OPFS.

Trong trường hợp này, Kiwix có thể đếm số byte đi qua ReadableStream, do đó cung cấp chỉ báo tiến trình cho người dùng và cũng cảnh báo họ không được thoát khỏi ứng dụng trong khi tải xuống. Mã này hơi phức tạp để hiển thị tại đây, nhưng vì ứng dụng của chúng tôi là ứng dụng FOSS, nên bạn có thể xem nguồn nếu quan tâm đến việc làm tương tự. Giao diện người dùng Kiwix có dạng như sau (các giá trị tiến trình khác nhau hiển thị bên dưới là do 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 điều khiển Tiến trình tải xuống thường xuyên hơn):

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

Vì tải xuống có thể là một hoạt động khá dài, nên Kiwix cho phép người dùng thoải mái sử dụng ứng dụng trong khi thực hiện thao tác, nhưng đả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 thu nhỏ trong ứng dụng

Hiện tại, các nhà phát triển PWA của 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 mà họ không còn cần đến khỏi khu vực lưu trữ này, đồng thời, lý tưởng nhất là xuất mọi tệp bị khoá trong OPFS trở lại hệ thống tệp mà người dùng nhìn thấy. Do đó, bạn cần triển khai một hệ thống quản lý tệp thu nhỏ bên trong ứng dụng.

Xin thông báo nhanh về tiện ích OPFS Explorer tuyệt vời cho Chrome (tiện ích này cũng hoạt động trong Edge). Tiện ích này sẽ thêm một thẻ trong công cụ dành 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 độc hại hoặc không thành công. Việc kiểm tra xem mã có đang hoạt động hay không, theo dõi hành vi tải xuống và nói chung dọn dẹp các thử nghiệm phát triển của chúng tôi là vô ích.

Tính năng xuất tệp phụ thuộc vào khả năng lấy tay cầm 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ào đó, vì vậy, tính năng này chỉ hoạt động trong ngữ cảnh có thể sử dụng phương thức window.showSaveFilePicker(). Nếu các 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 blob đó một URL, sau đó tải blob đó xuống hệ thống tệp hiển thị cho người dùng. Rất tiếc, điều đó là không thể với các kho lưu trữ lớn như vậy. Nếu được hỗ trợ, việc xuất khá đơn giản: gần như giống hệt, ngược lại với việc lưu tệp vào OPFS (lấy một handle trên tệp cần lưu, yêu cầu người dùng chọn vị trí để lưu tệp bằng window.showSaveFilePicker(), sau đó sử dụng createWriteable() trên saveHandle). Bạn có thể xem mã trong kho lưu trữ.

Tính năng xoá tệp được tất cả trình duyệt hỗ trợ và có thể thực hiện được 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 nhập OPFS như đã làm ở trên để có thể kiểm tra nhằm đảm bảo rằng tệp đã chọn tồn tại trước tiên và yêu cầu xác nhận, nhưng việc này có thể không cần thiết đối với mọi người. Bạn có thể kiểm tra mã của chúng tôi nếu muốn.

Chúng tôi quyết định không làm giao diện người dùng Kiwix trở nên lộn xộn bằng các nút cung cấp các tuỳ chọn này, mà thay vào đó, đặt các biểu tượng nhỏ ngay bên dưới danh sách lưu trữ. Việ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 để người dùng biết về những việc họ sẽ làm. Sau đó, người dùng nhấp hoặc nhấn vào một trong các tệp lưu trữ và thao tác tương ứng (xuất hoặc xoá) đượ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ạ ghi lại màn hình về tất cả các tính năng quản lý tệp đã thảo luận ở trên – thêm tệp vào OPFS, trực tiếp tải tệp vào đó, xoá tệp và xuất sang hệ thống tệp hiển thị cho người dùng.

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

OPFS là một sự đổi mới tuyệt vời dành cho các nhà phát triển ứng dụng 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 là một nhóm người khốn nạn — chẳng bao giờ họ hoàn toàn hài lòng! OPFS gần như hoàn hảo, nhưng chưa hoàn hảo... Thật tuyệt khi các tính năng chính hoạt động được trong cả trình duyệt Chromium và Firefox và chú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. Vẫn còn các vấn đề sau:

  • Firefox hiện đặt hạn mức 10 GB cho hạn mức OPFS, bất kể dung lượng ổ đĩa cơ bản là bao nhiêu. Mặc dù đối với hầu hết các tác giả PWA, điều này có thể là đủ, nhưng đối với Kiwix, điều đó khá hạn chế. May mắn thay, các trình duyệt Chromium lại hào phóng 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 trình duyệt 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 lưu trữ hiệu quả trong OPFS. Điều này trái với tinh thần của Kiwix về quyền truy cập công khai vào nội dung và khả năng chia sẻ bản lưu trữ giữa người dùng, đặc biệt là ở những khu vực có kết nối Internet không ổn định hoặc đắt đỏ.
  • Người dùng không thể kiểm soát bộ nhớ mà hệ thống tệp ảo OPFS sẽ sử dụng. Điều này đặc biệt gây ra vấn đề trên các thiết bị di động, nơi người dùng có thể có nhiều dung lượng trên thẻ microSD nhưng rất ít dung lượng trên bộ nhớ thiết bị.

Nhưng nhìn chung, đây là những vấn đề nhỏ trong một bước tiến lớn về quyền truy cập vào tệp trong PWA. Nhóm PWA của Kiwix rất biết ơn các nhà phát triển Chromium và những người ủng hộ, những người đầu tiên đề xuất và thiết kế API Truy cập hệ thống tệp, đồng thời nỗ lực đạ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 Kiwix JS PWA, công nghệ này đã giải quyết rất nhiều vấn đề về trải nghiệm người dùng đã làm tê liệt ứng dụng trong quá khứ, đồng thời giúp chúng tôi trong hành trình nâng cao khả năng hỗ trợ tiếp cận nội dung Kiwix cho mọi người. Vui lòng trải nghiệm Kiwix PWAcho nhà phát triển biết suy nghĩ của bạn!

Để biết một số tài nguyên hữu ích về các tính năng của PWA, hãy xem các trang web sau: