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 quây quần bên một chiếc máy tính xách tay đặt trên một chiếc bàn đơn giản, có một chiếc ghế nhựa ở bên trái. Nền trông giống như một trường học ở một quốc gia đang phát triển.

Nghiên cứu điển hình này khám phá cách Kiwix, một tổ chức phi lợi nhuận, sử dụng công nghệ Ứng dụng web tăng tiến 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ữ Internet lớn để 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 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. Ngoài ra, còn có nhiều tài nguyên nhỏ hơn, bao gồm cả 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 đến việ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ẽ 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ừ Nguyên tắc của Atwood, theo đó Mọi ứng dụng có thể được viết bằng JavaScript, cuối cùng sẽ được viết bằng JavaScript, một số nhà phát triển Kiwix, cách đây khoảng 10 năm, đã 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à vẫn là) 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 của ASM.js, sau đó là Wasm hoặc WebAssembly, sử dụng trình biên dịch Emscripten. Sau đó đổi tên thành Kiwix JS, các tiện ích trình duyệt vẫn đượ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 dành riêng cho Kiwix JS và bắt đầu thêm các tính năng tích hợp hệ điều hành để cho phép ứng dụng cung cấp các chức năng giống như ứng dụng 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 các ngữ cảnh có Internet di động giá cao hoặc không ổn định. Công nghệ đằng sau điều này là API Trình chạy dịch vụAPI 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 ứ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ớ, bộ nhớ ở mọi nơi

Do kích thước lớn của các tệp lưu trữ ZIM, việc lưu trữ và 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 đề đ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ả các cách truy cập nội dung từ các vị trí tuỳ ý mà người dùng có thể truy cập đều cần được Kiwix JS và Kiwix PWA hỗ trợ.

Ban đầu, File API (API Tệp) là yếu tố giúp Kiwix JS có thể đọc các bản lưu trữ khổng lồ, có dung lượng hàng trăm GB (một bản 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. 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 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ơ sở 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 cắt đó, phần phụ trợ sẽ chuyển các lát cắt đó đến trình giải nén Wasm, lấy thêm các lát cắt nếu được yêu cầu cho đến khi giải nén một blob đầy đủ (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 đã đ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á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 các 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 đều có một bản sao đầy đủ của Chromium, so với chỉ 5,1 MB của PWA được rút gọn và đóng gói!

Vậy Kiwix có cách nào để 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 sẽ giải quyết vấn đề này

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 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 dưới dạng 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.

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

Việc mở trình chọn tệp sẽ có dạng như sau (ở đây sử dụng Promises, nhưng nếu bạn thích async/await, 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 hoá, 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ả các Lời hứa xử lý từng tay điều khiển trong 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 trình chọn thư mục gần giống với mã ở trên, ngoại trừ việc bạn sử dụng window.showDirectoryPicker.then(function (dirHandle) { … });.

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

Sau khi có handle, bạn cần xử lý handle đó, vì vậy 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ữ tay cầm tệp, không có phương thức tiện lợi 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ể xem cách triển khai 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 một tệp hoặc tên 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 mà bạn muốn. Có nhiều cách để thực hiện việc đó, nhưng đây là mã được dùng trong Kiwix PWA, theo bản phác thảo:

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 bắt đầu bằng một cử chỉ của người dùng trong các lần chạy ứng dụng tiếp theo sẽ gây ra một chút phiền toái khi mở lại tệp và thư mục, nhưng vẫn linh hoạt hơn nhiều so với việc buộc người dùng chọn lại tệp. Các nhà phát triển Chromium hiện đang hoàn thiện mã cho phép quyền ổn định cho các 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, các nhà phát triển Kiwix nhận thấy rằng họ có thể loại bỏ tất cả lời nhắc cấp quyền ngay lập tức bằng cách sử dụng một tính năng mới mẻ 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.

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 bài viết và duyệt xem bản lưu trữ từ nơi họ dừng lại trong phiên trước mà không gặp bất kỳ trở ngại nào.
  • API 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 nhận thấy tốc độ cải thiệ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ớ trong phạm vi ứ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 vào OPFS (API cũng cho phép tạo tệp trong OPFS). Khi ở trong OPFS, các tệp này 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 hệ thống này bằng cách sử dụng navigator.storage.getDirectory() (nhắc lại, nếu bạn muốn xem mã sử dụ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ừ 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);
    });
});

Đừng quên 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 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 tệp đó 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 được điề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ì Firefox không hỗ trợ phương thức này, 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 danh sách 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, 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.)

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 tiếp luồng tệp vào 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 ứ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 muốn làm điều 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ì quá trình tải xuống có thể khá lâu, nên Kiwix cho phép người dùng sử dụng ứng dụng một cách tự do trong quá trình này, nhưng đảm bảo biểu ngữ luôn hiển thị để nhắc người dùng không đóng ứng dụng cho đến khi quá trình tải xuống hoàn tất.

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

Tại thời điểm này, các nhà phát triển Kiwix PWA 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.

Tôi muốn giới thiệu nhanh về tiện ích OPFS Explorer tuyệt vời dành cho Chrome (tiện ích này cũng hoạt động trong Edge). Tiện ích này 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. Đây là một công cụ vô giá để kiểm tra xem mã có 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.

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 các 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, bạn không thể làm như vậy với các tệp lưu trữ có kích thước 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ấ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 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 ưu tiên lặp lại các mục nhập OPFS như đã làm ở trên để có thể kiểm tra xem tệp đã chọn có tồn tại hay không trước khi yêu cầu xác nhận, nhưng điều này có thể không cần thiết đối với 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 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ữ. Thao tác nhấn vào một trong các biểu tượng này sẽ thay đổi màu sắc của danh sách bản lưu trữ, làm gợi ý trực quan cho người dùng về việc họ sắp làm. Sau đó, người dùng nhấp hoặc nhấn vào một trong các bản 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ạ 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ờ kết thúc

OPFS là một sự đổi mới 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ẽ, góp phần làm giảm 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 đáng thương – họ không bao giờ hài lòng! OPFS gần như hoàn hảo, nhưng chưa hoàn toàn… Thật tuyệt vời khi các tính năng chính hoạt động trong cả trình duyệt Chromium và Firefox, đồng thời các tính năng này được triển khai trên Android cũng như máy tính. 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 Kiwix PWA rất cảm ơn các nhà phát triển và người ủng hộ Chromium đã đề xuất và thiết kế API Truy cập hệ thống tệp lần đầu tiên, cũng như đã 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: