วิธีที่ Kiwix PWA ช่วยให้ผู้ใช้จัดเก็บข้อมูลกิกะไบต์จากอินเทอร์เน็ตเพื่อการใช้งานแบบออฟไลน์

ผู้คนรวมตัวกันรอบแล็ปท็อปโดยยืนอยู่บนโต๊ะง่ายๆ โดยมีเก้าอี้พลาสติกทางด้านซ้าย พื้นหลังดูคล้ายโรงเรียนในประเทศกำลังพัฒนา

กรณีศึกษานี้สำรวจว่า Kiwix ซึ่งเป็นองค์กรไม่แสวงผลกำไร ใช้เทคโนโลยี Progressive Web App และ File System Access API เพื่อให้ผู้ใช้ดาวน์โหลดและจัดเก็บอินเทอร์เน็ตที่เก็บถาวรขนาดใหญ่สำหรับการใช้งานแบบออฟไลน์ได้อย่างไร เรียนรู้เกี่ยวกับการติดตั้งใช้งานโค้ดทางเทคนิคในการจัดการกับ Origin Private File System (OPFS) ซึ่งเป็นฟีเจอร์ใหม่ของเบราว์เซอร์ภายใน Kiwix PWA ที่ช่วยเพิ่มประสิทธิภาพในการจัดการไฟล์ มอบการเข้าถึงที่เก็บถาวรที่ดีขึ้นโดยไม่ต้องขอสิทธิ์ บทความนี้พูดถึงความท้าทายและไฮไลต์การพัฒนาที่เป็นไปได้ในอนาคตในระบบไฟล์ใหม่นี้

เกี่ยวกับ Kiwix

กว่า 30 ปีหลังจากมีการกำเนิดเว็บขึ้น ประชากร 1 ใน 3 ของโลกยังคงรอการเข้าถึงอินเทอร์เน็ตที่เชื่อถือได้ตามสหภาพโทรคมนาคมระหว่างประเทศ เรื่องราวจะสิ้นสุดตรงไหนใช่ไหม แน่นอนว่าคำตอบคือไม่ ทีมงาน Kiwix ซึ่งเป็นองค์กรการกุศลในสวิตเซอร์แลนด์ได้พัฒนาระบบนิเวศของแอปและเนื้อหาแบบโอเพนซอร์ส ที่มีเป้าหมายเพื่อให้ผู้คนเข้าถึงอินเทอร์เน็ตได้อย่างจำกัดหรือไม่สามารถเข้าถึงได้ โดยมีแนวคิดว่าหากคุณเข้าถึงอินเทอร์เน็ตได้ยาก จะมีผู้อื่นดาวน์โหลดทรัพยากรหลักให้คุณ รวมถึงตำแหน่งและเวลาการเชื่อมต่อที่พร้อมใช้งาน แล้วจัดเก็บไว้ในเครื่องเพื่อใช้งานแบบออฟไลน์ในภายหลัง ตอนนี้เว็บไซต์สำคัญๆ หลายแห่ง เช่น Wikipedia, Project Gutenberg, Stack Exchange หรือแม้แต่การพูดคุย TED สามารถแปลงเป็นไฟล์ที่เก็บถาวรที่มีการบีบอัดสูง ที่เรียกว่าไฟล์ ZIM และอ่านได้ทันทีโดยเบราว์เซอร์ Kiwix

ที่เก็บถาวรของ ZIM ใช้การบีบอัด Zstandard(ZSTD) ที่มีประสิทธิภาพสูง (เวอร์ชันเก่าใช้ XZ) โดยส่วนใหญ่ใช้เพื่อจัดเก็บ HTML, JavaScript และ CSS ในขณะที่รูปภาพมักจะแปลงเป็นรูปแบบ WebP ที่บีบอัด ZIM แต่ละรายการจะมี URL และดัชนีชื่อด้วย การบีบอัดคือกุญแจสำคัญในเรื่องนี้ เนื่องจากข้อมูลทั้งหมดของ Wikipedia ในภาษาอังกฤษ (6.4 ล้านบทความและรูปภาพ) ถูกบีบอัดเป็น 97 GB หลังจากแปลงเป็นรูปแบบ ZIM ซึ่งฟังดูมีประโยชน์มากจนกว่าคุณจะรู้ว่าตอนนี้สามารถรวบรวมความรู้ทั้งหมดของมนุษย์ได้ในโทรศัพท์ Android ระดับกลางแล้ว นอกจากนี้ยังมีแหล่งข้อมูลเล็กๆ จำนวนมาก รวมถึง Wikipedia ที่มีธีม เช่น คณิตศาสตร์ การแพทย์ และอื่นๆ

Kiwix มีแอปที่มาพร้อมเครื่องหลากหลาย ซึ่งกำหนดเป้าหมายไปที่เดสก์ท็อป (Windows/Linux/macOS) และการใช้งานอุปกรณ์เคลื่อนที่ (iOS/Android) อย่างไรก็ตาม กรณีศึกษานี้จะมุ่งเน้นไปที่ Progressive Web App (PWA) โดยมีเป้าหมายที่จะเป็นโซลูชันสากลที่ง่ายดายสำหรับอุปกรณ์ทุกประเภทที่มีเบราว์เซอร์ที่ทันสมัย

เราจะมาดูความท้าทายในการพัฒนาเว็บแอปสากลซึ่งจำเป็นต้องให้คุณเข้าถึงที่เก็บเนื้อหาขนาดใหญ่แบบออฟไลน์ได้อย่างรวดเร็ว และ JavaScript API สมัยใหม่บางตัว โดยเฉพาะอย่างยิ่ง File System Access API และOrigin Private File System ที่ให้โซลูชันที่สร้างสรรค์และน่าตื่นเต้นสำหรับความท้าทายเหล่านั้น

เว็บแอปสำหรับใช้งานแบบออฟไลน์ใช่ไหม

ผู้ใช้ Kiwix เป็นคนกลุ่มเล็กที่มีความต้องการหลากหลาย และ Kiwix มีความสามารถในการควบคุมอุปกรณ์และระบบปฏิบัติการที่ผู้ใช้จะเข้าถึงเนื้อหาได้น้อยมากหรือไม่มีเลย อุปกรณ์เหล่านี้บางอย่างอาจทำงานช้าหรือล้าสมัย โดยเฉพาะในพื้นที่ที่มีรายได้ต่ำทั่วโลก แม้ว่า Kiwix พยายามที่จะครอบคลุมกรณีการใช้งานต่างๆ ให้มากที่สุด แต่องค์กรก็ตระหนักว่าเบราว์เซอร์นี้สามารถเข้าถึงผู้ใช้ได้มากขึ้นไปอีกโดยการใช้ซอฟต์แวร์ที่เป็นสากลมากที่สุดบนอุปกรณ์ใดก็ได้ ซึ่งก็คือเว็บเบราว์เซอร์ เราจึงได้รับแรงบันดาลใจจากกฎของ Atwood ซึ่งระบุว่าแอปพลิเคชันใดก็ตามที่เขียนได้ด้วย JavaScript จะเขียนด้วย JavaScript ได้ในท้ายที่สุด นักพัฒนาซอฟต์แวร์ Kiwix บางรายเมื่อประมาณ 10 ปีก่อนได้เริ่มการดำเนินการพอร์ตซอฟต์แวร์ Kiwix จาก C++ ไปยัง JavaScript

พอร์ตเวอร์ชันแรกของ Kiwix HTML5 เป็นระบบปฏิบัติการ Firefox ที่เลิกใช้งานแล้วสำหรับส่วนขยายเบราว์เซอร์ หลักๆ แล้วคือ (และเป็น) เครื่องมือคลายการบีบอัด C++ (XZ และ ZSTD) ที่คอมไพล์เป็นภาษา JavaScript ระดับกลางของ ASM.js และเวอร์ชันต่อมาคือ Wasm หรือ WebAssembly โดยใช้คอมไพเลอร์ Emscripten ต่อมาได้เปลี่ยนชื่อเป็น Kiwix JS และส่วนขยายของเบราว์เซอร์ก็ยังคงได้รับการพัฒนาอย่างต่อเนื่อง

เบราว์เซอร์แบบออฟไลน์ของ Kiwix JS

ป้อน Progressive Web App (PWA) นักพัฒนาซอฟต์แวร์ Kiwix ตระหนักถึงศักยภาพของเทคโนโลยี จึงได้สร้าง Kiwix JS สร้างเวอร์ชัน PWA โดยเฉพาะ และตั้งค่าการผสานรวมระบบปฏิบัติการซึ่งจะช่วยให้แอปสามารถนำเสนอความสามารถที่เหมือนกับโฆษณาเนทีฟ โดยเฉพาะอย่างยิ่งในด้านการใช้งานแบบออฟไลน์ การติดตั้ง การจัดการไฟล์ และการเข้าถึงระบบไฟล์

PWA แบบออฟไลน์มีน้ำหนักเบามาก จึงเหมาะอย่างยิ่งสำหรับบริบทที่มีอินเทอร์เน็ตบนอุปกรณ์เคลื่อนที่เป็นระยะๆ หรือมีราคาแพง เทคโนโลยีที่อยู่เบื้องหลังนี้คือ Service Worker API และ Cache API ที่เกี่ยวข้อง ซึ่งแอปทั้งหมดใช้ Kiwix JS API เหล่านี้อนุญาตให้แอปทำหน้าที่เป็นเซิร์ฟเวอร์ ดักจับคำขอดึงข้อมูลจากเอกสารหรือบทความหลักที่มีคนดู และเปลี่ยนเส้นทางไปยังแบ็กเอนด์ (JS) เพื่อดึงข้อมูลและสร้างการตอบกลับจากที่เก็บถาวรของ ZIM

พื้นที่เก็บข้อมูล พื้นที่เก็บข้อมูลในทุกที่

เนื่องจากที่เก็บถาวร พื้นที่เก็บข้อมูล และการเข้าถึงที่เก็บถาวรของ ZIM มีขนาดใหญ่ โดยเฉพาะอย่างยิ่งบนอุปกรณ์เคลื่อนที่ คงเป็นเรื่องน่าปวดหัวที่สุดสำหรับนักพัฒนาซอฟต์แวร์ Kiwix ผู้ใช้ปลายทางของ Kiwix หลายคนดาวน์โหลดเนื้อหาในแอปเมื่อมีอินเทอร์เน็ตเพื่อใช้แบบออฟไลน์ในภายหลัง ส่วนผู้ใช้คนอื่นๆ จะดาวน์โหลดลงในพีซีโดยใช้ Torrent จากนั้นโอนไปยังโทรศัพท์มือถือหรือแท็บเล็ต บางคนแลกเปลี่ยนเนื้อหาบนแท่ง USB หรือฮาร์ดไดรฟ์แบบพกพาในพื้นที่ที่มีอินเทอร์เน็ตมือถือซึ่งมีปริมาณไม่สูงหรือราคาแพง Kiwix JS และ Kiwix PWA จะต้องรองรับวิธีการทั้งหมดในการเข้าถึงเนื้อหาจากตำแหน่งที่ผู้ใช้เข้าถึงได้แบบกำหนดเองทั้งหมด

ในตอนแรก Kiwix JS สามารถอ่านที่เก็บถาวรขนาดใหญ่หลายร้อย GB (1 ที่เก็บถาวรของ ZIM ของเราได้ขนาด 166 GB!) แม้ในอุปกรณ์ที่มีหน่วยความจำต่ำก็คือ File API API นี้รองรับในทุกๆ เบราว์เซอร์ แม้แต่เบราว์เซอร์รุ่นเก่า ดังนั้นจึงทำหน้าที่เป็น API สำรองสากลสำหรับกรณีที่ระบบไม่รองรับ API ใหม่ ซึ่งง่ายพอๆ กับการกำหนดองค์ประกอบ input ใน HTML ในกรณีของ Kiwix มีดังนี้

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

เมื่อเลือกแล้ว องค์ประกอบอินพุตจะเก็บออบเจ็กต์ไฟล์ซึ่งเป็นข้อมูลเมตาที่อ้างอิงถึงข้อมูลพื้นฐานในพื้นที่เก็บข้อมูล ในทางเทคนิค แบ็กเอนด์ตามออบเจ็กต์ของ Kiwix ที่เขียนด้วย JavaScript ฝั่งไคลเอ็นต์เพียงอย่างเดียว จะอ่านที่เก็บถาวรขนาดเล็กตามที่จำเป็น หากต้องแยกส่วนเหล่านั้น แบ็กเอนด์จะส่งส่วนที่บีบอัดไปยังตัวแยกส่วน Wasm และจะได้รับสไลซ์เพิ่มเติมหากขอ จนกว่าจะมีการแยกส่วนของ Blob ทั้งหมด (โดยปกติจะเป็นบทความหรือเนื้อหา) ซึ่งหมายความว่าไม่จำเป็นต้องอ่านที่เก็บถาวรขนาดใหญ่ทั้งหมดลงในหน่วยความจำ

อย่างไรก็ดี File API มีข้อด้อยที่ทำให้แอป Kiwix JS ดูไม่เป็นระเบียบและล้าสมัยเมื่อเทียบกับแอปที่มาพร้อมเครื่อง โดยกำหนดให้ผู้ใช้ต้องเลือกไฟล์ที่เก็บถาวรโดยใช้เครื่องมือเลือกไฟล์หรือลากและวางไฟล์ลงในแอปทุกครั้งที่เปิดแอป เนื่องจาก API นี้ไม่สามารถคงสิทธิ์เข้าถึงจากเซสชันหนึ่งไปยังเซสชันถัดไปได้

ในช่วงแรก นักพัฒนาซอฟต์แวร์ Kiwix JS ได้ลด UX ที่ไม่ดีนี้ลง เหมือนกับนักพัฒนาซอฟต์แวร์จำนวนมาก เริ่มเดินตามเส้นทาง Electron ElectronJS เป็นเฟรมเวิร์กที่ยอดเยี่ยมซึ่งมอบฟีเจอร์ที่มีประสิทธิภาพ ซึ่งรวมถึงการเข้าถึงระบบไฟล์อย่างเต็มรูปแบบโดยใช้ Node API อย่างไรก็ตาม จะมีข้อด้อยที่ทราบกันดีดังนี้

  • ทำงานบนระบบปฏิบัติการเดสก์ท็อปเท่านั้น
  • ไฟล์มีขนาดใหญ่และหนัก (70 MB-100 MB)

เนื่องจากขนาดแอป Electron เนื่องจากสำเนาที่สมบูรณ์ของ Chromium รวมอยู่ในทุกแอป จึงเปรียบเทียบได้ไม่ดีกับ เพียง 5.1 MB สำหรับ PWA แบบย่อและรวมมา

ในกรณีนี้ Kiwix จะปรับปรุงสถานการณ์ให้กับผู้ใช้ PWA ได้ไหม

File System Access API ช่วยคุณได้

ประมาณปี 2019 Kiwix ได้ตระหนักถึง API ฉุกเฉินที่อยู่ระหว่างการทดลองจากต้นทางใน Chrome 78 ซึ่งมีชื่อว่า Native File System API ระบบสัญญาว่าจะสามารถรับแฮนเดิลไฟล์สำหรับไฟล์หรือโฟลเดอร์และจัดเก็บไว้ในฐานข้อมูล IndexedDB ที่สำคัญ แฮนเดิลนี้จะยังคงอยู่ระหว่างเซสชันของแอป เพื่อไม่ต้องให้ผู้ใช้เลือกไฟล์หรือโฟลเดอร์อีกครั้งเมื่อเปิดแอปอีกครั้ง (แต่ต้องตอบข้อความแจ้งสิทธิ์แบบรวดเร็ว) ก่อนที่จะถึงเวอร์ชันที่ใช้งานจริง เราได้เปลี่ยนชื่อเป็น File System Access API และส่วนหลักที่ได้มาตรฐานจาก WHATWG เป็น File System API (FSA)

แล้วส่วน การเข้าถึงระบบไฟล์ ของ API ทำงานอย่างไร สิ่งสำคัญบางประการที่ควรทราบมีดังนี้

  • ซึ่งเป็น API แบบอะซิงโครนัส (ยกเว้นฟังก์ชันเฉพาะทางใน Web Workers)
  • เครื่องมือเลือกไฟล์หรือไดเรกทอรีต้องเปิดใช้แบบเป็นโปรแกรมโดยการบันทึกท่าทางสัมผัสของผู้ใช้ (คลิกหรือแตะองค์ประกอบ UI)
  • นอกจากนี้ ยังต้องมีท่าทางสัมผัสของผู้ใช้เพื่อให้ผู้ใช้ให้สิทธิ์อีกครั้งเพื่อเข้าถึงไฟล์ที่เลือกก่อนหน้านี้ (ในเซสชันใหม่) ซึ่งในความเป็นจริงเบราว์เซอร์จะปฏิเสธที่จะแสดงข้อความแจ้งสิทธิ์หากไม่ได้เริ่มด้วยท่าทางสัมผัสของผู้ใช้

โค้ดค่อนข้างตรงไปตรงมา นอกเหนือจากการใช้ IndexedDB API ที่ซับซ้อนเพื่อจัดเก็บไฟล์และแฮนเดิลไดเรกทอรี ข่าวดีคือมีไลบรารีอีก 2 แห่งที่ช่วยคุณทำงานหนักได้ เช่น browser-fs-access ทาง Kiwix JS เราตัดสินใจที่จะทำงานกับ API โดยตรงซึ่งมีการบันทึกข้อมูลไว้เป็นอย่างดี

การเปิดไฟล์และเครื่องมือเลือกไดเรกทอรี

การเปิดเครื่องมือเลือกไฟล์จะมีลักษณะดังนี้ (ที่นี่โดยใช้ Promises แต่หากคุณต้องการใส่ async/await Sugar โปรดดูบทแนะนำของ Chrome สำหรับนักพัฒนาซอฟต์แวร์)

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

โปรดทราบว่าโค้ดนี้จะประมวลผลเฉพาะไฟล์แรกที่เลือกเท่านั้น (และไม่อนุญาตให้เลือกมากกว่า 1 ไฟล์) เพื่อความง่าย ในกรณีที่คุณต้องการอนุญาตให้เลือกหลายไฟล์ด้วย { multiple: true } เพียงรวมคำสัญญาทั้งหมดที่ประมวลผลแต่ละแฮนเดิลในคำสั่ง 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;
  });
}

โปรดทราบว่าคุณต้องให้ฟังก์ชันเพื่อจัดเก็บแฮนเดิลไฟล์ ไม่มีวิธีการที่สะดวกสำหรับการดำเนินการนี้ เว้นแต่ว่าคุณจะใช้ไลบรารี Abstraction การใช้งานของ 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) { … }) ในภายหลังเมื่อต้องการใช้ หรือในไฟล์ที่เทียบเท่าโดยใช้ const file = await entry.getFile() ใน async function

เราไปไกลกว่านั้นไหม

ความต้องการสำหรับผู้ใช้ต้องให้สิทธิ์ซึ่งเริ่มต้นจากท่าทางสัมผัสของผู้ใช้ในการเปิดแอปครั้งต่อๆ มาจะเพิ่มความยุ่งยากในการเปิดไฟล์และโฟลเดอร์ (เปิด) เล็กน้อย แต่ก็ยังทำงานได้ลื่นไหลมากกว่าการบังคับให้เลือกไฟล์ใหม่มาก ขณะนี้นักพัฒนาซอฟต์แวร์ Chromium กำลังสรุปโค้ดซึ่งจะอนุญาตสิทธิ์ถาวรสำหรับ PWA ที่ติดตั้ง นี่เป็นสิ่งที่นักพัฒนาซอฟต์แวร์ PWA จำนวนมากเรียกร้องและเราตั้งตารอ

แต่ถ้าเราไม่ต้องรอล่ะ เมื่อเร็วๆ นี้ นักพัฒนาซอฟต์แวร์ Kiwix พบว่าคุณสามารถกำจัดข้อความแจ้งเกี่ยวกับสิทธิ์ทั้งหมดได้ในขณะนี้ โดยใช้ฟีเจอร์ใหม่เอี่ยมของ File Access API ที่ทั้งเบราว์เซอร์ Chromium และ Firefox รองรับ (และรองรับโดย Safari เพียงบางส่วน แต่ยังไม่มี FileSystemWritableFileStream) ฟีเจอร์ใหม่นี้คือระบบไฟล์ส่วนตัวดั้งเดิม

พัฒนาอย่างเต็มรูปแบบ: ระบบไฟล์ส่วนตัวต้นทาง

ระบบไฟล์ส่วนตัวต้นทาง (OPFS) ยังคงเป็นฟีเจอร์ทดลองใน Kiwix PWA แต่ทีมเองก็ตื่นเต้นที่จะกระตุ้นให้ผู้ใช้ทดลองใช้ เนื่องจากฟีเจอร์นี้จะเป็นตัวเชื่อมช่องว่างระหว่างแอปที่มาพร้อมเครื่องกับเว็บแอปเป็นอย่างมาก ประโยชน์หลักๆ มีดังนี้

  • ที่เก็บถาวรใน OPFS สามารถเข้าถึงได้โดยไม่มีข้อความแจ้งเกี่ยวกับสิทธิ์แม้ว่าจะเปิดใช้งานอยู่ก็ตาม ผู้ใช้สามารถอ่านบทความและเรียกดูที่เก็บถาวรต่อจากที่อ่านค้างไว้ในเซสชันก่อนหน้าได้อย่างราบรื่น
  • โดยมอบการเข้าถึงไฟล์ได้อย่างมีประสิทธิภาพสูงสุดที่จัดเก็บไว้ใน Android โดยเราเห็นการปรับปรุงความเร็วระหว่าง 5 ถึง 10 เท่าใน Android

การเข้าถึงไฟล์มาตรฐานใน 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() (ขอย้ำอีกครั้งว่าหากต้องการดูโค้ดโดยใช้await โปรดอ่านระบบไฟล์ส่วนตัวต้นทาง)

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

แฮนเดิลที่คุณจะได้รับจากนี่คือประเภทเดียวกันกับ FileSystemDirectoryHandleที่คุณได้รับจากwindow.showDirectoryPicker() ที่กล่าวไปข้างต้น ซึ่งหมายความว่าคุณสามารถใช้โค้ดที่จัดการเรื่องนี้ซ้ำได้ (และไม่จำเป็นต้องเก็บข้อมูลนี้ไว้ใน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);
    });
});

อย่าลืมว่าคุณยังต้องใช้ getFile() กับทุกรายการที่ต้องการใช้งานจากอาร์เรย์ archiveList

การนำเข้าไฟล์ไปยัง OPFS

แล้วคุณจะนำไฟล์เข้าสู่ OPFS ตั้งแต่แรกได้อย่างไร ยังไม่ค่อยเร็วนัก! ก่อนอื่น คุณต้องประมาณปริมาณพื้นที่เก็บข้อมูลที่ต้องใช้ และตรวจสอบว่าผู้ใช้ไม่พยายามใส่ไฟล์ขนาด 97 GB ไว้ถ้าจำนวนไม่พอดี

คุณดูโควต้าโดยประมาณได้ง่ายๆ โดยทำดังนี้ navigator.storage.estimate().then(function (estimate) { … }); อาจยากขึ้นเล็กน้อย คือต้องหาวิธีแสดงให้ผู้ใช้เห็น ในแอป Kiwix เราเลือกใช้แผงในแอปขนาดเล็กที่มองเห็นได้ข้างช่องทำเครื่องหมายเพื่อให้ผู้ใช้ลองใช้ OPFS ได้

แผงแสดงพื้นที่เก็บข้อมูลที่ใช้แล้วเป็นเปอร์เซ็นต์และพื้นที่เก็บข้อมูลที่เหลืออยู่ในหน่วยกิกะไบต์

แผงระบบจะสร้างโดยใช้ estimate.quota และ estimate.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 รองรับวิธีนี้มากที่สุด

ปุ่มเพิ่มไฟล์ที่มองเห็นได้ในภาพหน้าจอด้านบนไม่ใช่เครื่องมือเลือกไฟล์แบบเดิม แต่click()เป็นเครื่องมือเลือกเดิมที่ซ่อนไว้ (องค์ประกอบ <input type="file" multiple … /> รายการ) เมื่อมีการคลิกหรือแตะที่ปุ่ม จากนั้นแอปจะบันทึกเหตุการณ์ 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 ใช้ Promises ที่นี่ด้วยเหตุผลที่เกี่ยวข้องกับฐานของโค้ดเดิมของเรา แต่จำเป็นต้องระบุว่าในกรณีนี้ 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()) โดยใช้หลักการเดียวกันกับโค้ดข้างต้น แต่จะสร้าง Response ที่ประกอบด้วย ReadableStream และตัวควบคุมที่จะจัดคิวจำนวนไบต์ที่อ่านจากไฟล์ระยะไกล จากนั้น Response.body ที่ได้จะนำไปเชื่อมต่อกับผู้เขียนไฟล์ใหม่ใน OPFS

ในกรณีนี้ Kiwix สามารถนับไบต์ที่ผ่าน ReadableStream และให้สัญญาณบอกสถานะความคืบหน้าแก่ผู้ใช้และเตือนผู้ใช้ไม่ให้ออกจากแอปในระหว่างการดาวน์โหลด โค้ดดูซับซ้อนเกินไปที่จะแสดงที่นี่ แต่เนื่องจากแอปของเราเป็นแอป FOSS คุณสามารถ ดูซอร์สโค้ด หากสนใจทำอย่างอื่นที่คล้ายกัน หน้าตา UI ของ Kiwix มีลักษณะดังนี้ (ค่าความคืบหน้าต่างๆ ที่แสดงด้านล่างเป็นเพราะระบบจะอัปเดตแบนเนอร์เฉพาะเมื่อเปอร์เซ็นต์เปลี่ยนแปลง แต่จะอัปเดตแผงความคืบหน้าในการดาวน์โหลดบ่อยขึ้น)

อินเทอร์เฟซผู้ใช้ Kiwix ที่มีแถบด้านล่างเตือนผู้ใช้ไม่ให้ออกจากแอปและแสดงความคืบหน้าในการดาวน์โหลดที่เก็บถาวร .zim

เนื่องจากการดาวน์โหลดเป็นเรื่องที่ค่อนข้างนาน Kiwix จึงอนุญาตให้ผู้ใช้ใช้แอปได้อย่างอิสระในระหว่างดำเนินการ แต่ก็จะทำให้มีแบนเนอร์ปรากฏขึ้นตลอดเวลาเพื่อเตือนผู้ใช้ไม่ให้ปิดแอปจนกว่าการดาวน์โหลดจะเสร็จสมบูรณ์

การใช้โปรแกรมจัดการไฟล์ขนาดเล็กในแอป

ณ จุดนี้ นักพัฒนาซอฟต์แวร์ Kiwix PWA ตระหนักว่าการเพิ่มไฟล์ไปยัง OPFS ยังไม่เพียงพอ แอปยังต้องมอบวิธีลบไฟล์ที่ผู้ใช้ไม่จำเป็นต้องใช้จากพื้นที่เก็บข้อมูลนี้อีกต่อไป และควรจะส่งออกไฟล์ที่ล็อกใน OPFS กลับไปยังระบบไฟล์ที่ผู้ใช้มองเห็น ด้วยเหตุนี้คุณจึงต้องนำระบบการจัดการไฟล์ขนาดเล็กไปใช้ภายในแอปอย่างมีประสิทธิภาพ

ขอขอบคุณส่วนขยาย OPFS Explorer ที่ยอดเยี่ยมสำหรับ Chrome (ใช้ได้กับ Edge ด้วยเช่นกัน) และเพิ่มแท็บในเครื่องมือสำหรับนักพัฒนาซอฟต์แวร์เพื่อให้คุณดู สิ่งที่อยู่ใน OPFS ได้อย่างชัดเจน และลบไฟล์ที่หลอกลวงหรือล้มเหลวได้ด้วย ซึ่งมีประโยชน์มากสำหรับการตรวจสอบว่าโค้ดทำงานได้หรือไม่ การตรวจสอบพฤติกรรมการดาวน์โหลด และการจัดระเบียบการทดสอบการพัฒนาของเราโดยทั่วไป

export ไฟล์ขึ้นอยู่กับความสามารถในการจัดการไฟล์ในไฟล์หรือไดเรกทอรีที่เลือกซึ่ง Kiwix จะบันทึกไฟล์ที่ส่งออก ดังนั้น จึงใช้งานได้เฉพาะในบริบทที่ใช้เมธอด window.showSaveFilePicker() ได้ หากไฟล์ Kiwix มีขนาดเล็กกว่าหลาย GB เราจะสามารถสร้าง BLOB ในหน่วยความจำ ระบุ URL แล้วดาวน์โหลดลงในระบบไฟล์ที่ผู้ใช้มองเห็น ต้องขออภัยด้วย เป็นไปไม่ได้หากคลังข้อมูลขนาดใหญ่เช่นนี้ หากมีการรองรับ การส่งออกจะค่อนข้างตรงไปตรงมา คล้ายกับการบันทึกไฟล์ใน OPFS (ดูแฮนเดิลในไฟล์ที่จะบันทึก ขอให้ผู้ใช้เลือกตำแหน่งที่จะบันทึกไฟล์ด้วย window.showSaveFilePicker() แล้วใช้ createWriteable() ใน saveHandle) คุณดูโค้ดได้ในที่เก็บ

ทุกเบราว์เซอร์รองรับการลบไฟล์ และสามารถทำได้ง่ายๆ dirHandle.removeEntry('filename') ในกรณีของ Kiwix เราอยากจะทำซ้ำรายการ OPFS ดังเช่นที่เคยทำด้านบน เพื่อจะได้ตรวจสอบว่ามีไฟล์ที่เลือกอยู่ก่อนแล้วและขอการยืนยัน แต่นั่นอาจไม่จำเป็นสำหรับทุกคน คุณตรวจสอบโค้ดของเราได้ถ้าสนใจ

เราตัดสินใจไม่ทำให้ UI ของ Kiwix รกตาด้วยปุ่มต่างๆ ที่ให้ตัวเลือกเหล่านี้ แต่วางไอคอนเล็กๆ ไว้ใต้รายการที่เก็บถาวรโดยตรง การแตะไอคอนใดไอคอนหนึ่งเหล่านี้จะเปลี่ยนสีของรายการที่เก็บถาวร ซึ่งเป็นสัญญาณที่มองเห็นได้ให้ผู้ใช้เห็นถึงการดำเนินการที่ตนกำลังจะทำ จากนั้นผู้ใช้คลิกหรือแตะที่ที่เก็บถาวรรายการใดรายการหนึ่ง และจะดำเนินการที่เกี่ยวข้อง (ส่งออกหรือลบ) (หลังจากยืนยัน)

กล่องโต้ตอบที่ถามผู้ใช้ว่าต้องการลบไฟล์ .zim หรือไม่

สุดท้ายนี้ ระบบจะแสดงการสาธิต Screencast ของฟีเจอร์การจัดการไฟล์ทั้งหมดที่กล่าวถึงข้างต้น ซึ่งได้แก่ การเพิ่มไฟล์ไปยัง OPFS, ดาวน์โหลดไฟล์ลงในไฟล์โดยตรง, ลบไฟล์ และส่งออกไปยังระบบไฟล์ที่ผู้ใช้มองเห็น

งานของนักพัฒนาแอปไม่มีวันจบสิ้น

OPFS เป็นนวัตกรรมที่ยอดเยี่ยมสำหรับนักพัฒนาแอป PWA เพราะมีฟีเจอร์การจัดการไฟล์ที่มีประสิทธิภาพ ช่วยขจัดช่องว่างระหว่างแอปที่มาพร้อมเครื่องกับเว็บแอปเป็นอย่างมาก แต่นักพัฒนาซอฟต์แวร์ก็ต้องหวั่นไหวไปกับ OPFS เกือบจะสมบูรณ์แบบแล้ว แต่ยังไม่ใช่... เป็นเรื่องดีที่ฟีเจอร์หลัก ทำงานได้ทั้งในเบราว์เซอร์ Chromium และ Firefox และมีการนำไปใช้ใน Android และเดสก์ท็อป เราหวังว่าชุดฟีเจอร์ทั้งหมด จะใช้ได้ใน Safari และ iOS เร็วๆ นี้ ปัญหาต่อไปนี้ยังคงอยู่

  • ปัจจุบัน Firefox จำกัดความจุ 10 GB ในโควต้า OPFS ไม่ว่าจะมีพื้นที่ในดิสก์มากแค่ไหนก็ตาม แม้ว่าผู้เขียน PWA ส่วนใหญ่อาจมีพื้นที่เพียงพอ แต่สำหรับ Kiwix จะเป็นข้อจำกัดที่ค่อนข้างจำกัด แต่โชคดีที่เบราว์เซอร์ Chromium มีใจกว้างกว่ามาก
  • ปัจจุบันคุณยังส่งออกไฟล์ขนาดใหญ่จาก OPFS ไปยังระบบไฟล์ที่ผู้ใช้มองเห็นในเบราว์เซอร์ในอุปกรณ์เคลื่อนที่ไม่ได้หรือ Firefox ในเดสก์ท็อปไม่ได้เนื่องจากไม่ได้นำ window.showSaveFilePicker() ไปใช้ ในเบราว์เซอร์เหล่านี้ ไฟล์ขนาดใหญ่จะติดอยู่ใน OPFS ได้อย่างมีประสิทธิภาพ ซึ่งขัดกับแนวคิดของ Kiwix ในด้านการเข้าถึงเนื้อหาอย่างอิสระ และความสามารถในการแชร์ที่เก็บถาวรระหว่างผู้ใช้ โดยเฉพาะในพื้นที่ที่มีการเชื่อมต่ออินเทอร์เน็ตที่ไม่เสถียรหรือมีค่าใช้จ่ายสูง
  • ผู้ใช้ไม่สามารถควบคุมได้ว่าระบบไฟล์เสมือน OPFS จะใช้พื้นที่เก็บข้อมูลใด กรณีนี้เป็นปัญหาโดยเฉพาะบนอุปกรณ์เคลื่อนที่ ซึ่งผู้ใช้อาจมีพื้นที่ขนาดใหญ่ในการ์ด microSD แต่เป็นพื้นที่เก็บข้อมูลของอุปกรณ์น้อยมาก

แต่โดยรวมแล้ว ทั้งหมดนี้เป็นเรื่องน่าหวาดเสียวเล็กน้อยซึ่งถือเป็นก้าวสำคัญในการเข้าถึงไฟล์ใน PWA ทีม Kiwix PWA ขอขอบคุณนักพัฒนาซอฟต์แวร์และผู้สนับสนุน Chromium ที่ได้เสนอและออกแบบ File System Access API ขึ้นเป็นครั้งแรก รวมถึงการทำงานอย่างหนักเพื่อให้ได้ความเห็นพ้องในหมู่ผู้ให้บริการเบราว์เซอร์เกี่ยวกับความสำคัญของระบบไฟล์ส่วนตัวดั้งเดิม สำหรับ Kiwix JS PWA พบว่า เครื่องมือนี้แก้ปัญหาด้าน UX มากมายที่เคยรบกวนการทำงานของแอปในอดีต และช่วยให้เราปรับปรุงการเข้าถึงเนื้อหา Kiwix ให้กับทุกคนได้ โปรดลองใช้ PWA ของ Kiwix และบอกนักพัฒนาแอปว่าคุณคิดอย่างไร

สำหรับแหล่งข้อมูลดีๆ เกี่ยวกับความสามารถของ PWA ให้ดูที่เว็บไซต์ต่อไปนี้