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

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

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

เกี่ยวกับ Kiwix

กว่า 30 ปีหลังจากที่เว็บถือกำเนิดขึ้น ประชากรโลก 1 ใน 3 ยังคงรอการเข้าถึงอินเทอร์เน็ตที่เชื่อถือได้ ตามที่สหภาพโทรคมนาคมระหว่างประเทศ (International Telecommunication Union) ระบุ หนังเรื่องนี้จบลงแล้วใช่ไหม ไม่ได้ ทีมจาก Kiwix ซึ่งเป็นองค์กรการกุศลในสวิตเซอร์แลนด์ได้พัฒนาระบบนิเวศของแอปและเนื้อหาโอเพนซอร์สที่มีจุดมุ่งหมายเพื่อทำให้ความรู้พร้อมใช้งานสำหรับผู้ที่เข้าถึงอินเทอร์เน็ตได้จำกัดหรือไม่มีสิทธิ์เข้าถึงอินเทอร์เน็ต แนวคิดคือหากคุณเข้าถึงอินเทอร์เน็ตได้ยาก ผู้อื่นจะดาวน์โหลดแหล่งข้อมูลสำคัญให้คุณได้เมื่อใดก็ตามที่การเชื่อมต่อพร้อมใช้งาน และจัดเก็บไว้ในเครื่องเพื่อใช้งานแบบออฟไลน์ในภายหลัง ตอนนี้เว็บไซต์ที่สำคัญหลายแห่ง เช่น วิกิพีเดีย, Project Gutenberg, Stack Exchange หรือแม้แต่ TED Talks สามารถแปลงเป็นไฟล์เก็บถาวรที่บีบอัดสูงซึ่งเรียกว่าไฟล์ 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) ซึ่งมีเป้าหมายเพื่อเป็นโซลูชันที่ใช้งานง่ายและใช้ได้กับทุกอุปกรณ์ที่มีเบราว์เซอร์สมัยใหม่

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

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

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

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

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

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

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

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

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

ตั้งแต่แรกเริ่มที่ทำให้ Kiwix JS อ่านไฟล์ที่เก็บถาวรขนาดใหญ่ได้หลายร้อย GB (1 ในที่เก็บถาวรของ ZIM คือ 166 GB!) แม้แต่ในอุปกรณ์ที่มีหน่วยความจำน้อยก็คือ File 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 ทั้งหมดออกได้ (โดยปกติคือบทความหรือชิ้นงาน) ซึ่งหมายความว่าไม่ต้องอ่านที่เก็บถาวรขนาดใหญ่ทั้งหมดลงในหน่วยความจำ

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

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

  • โดยจะทำงานได้บนระบบปฏิบัติการเดสก์ท็อปเท่านั้น
  • ไฟล์มีขนาดใหญ่และหนัก (70-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 Worker)
  • เครื่องมือเลือกไฟล์หรือไดเรกทอรีต้องเปิดใช้งานแบบเป็นโปรแกรมโดยจับภาพท่าทางสัมผัสของผู้ใช้ (คลิกหรือแตะองค์ประกอบ UI)
  • หากต้องการให้ผู้ใช้ให้สิทธิ์เข้าถึงไฟล์ที่เลือกไว้ก่อนหน้านี้อีกครั้ง (ในเซสชันใหม่) ผู้ใช้จะต้องใช้ท่าทางสัมผัสด้วย ความจริงแล้วเบราว์เซอร์จะปฏิเสธที่จะแสดงข้อความแจ้งสิทธิ์หากไม่ได้เริ่มต้นโดยท่าทางสัมผัสของผู้ใช้

โค้ดนี้ค่อนข้างตรงไปตรงมานอกเหนือจากการใช้ IndexedDB API ที่ไม่ซับซ้อนเพื่อจัดเก็บไฟล์และแฮนเดิลไดเรกทอรี ข่าวดีคือมีไลบรารี 2-3 รายการที่จะช่วยคุณทำงานหนักๆ มากมาย เช่น 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 ทั้งหมดที่ประมวลผลแต่ละแฮนเดิลไว้ในคำสั่ง 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) ฟีเจอร์ใหม่นี้คือระบบไฟล์ส่วนตัวต้นทาง

ใช้รูปแบบเนทีฟอย่างสมบูรณ์: ระบบไฟล์ส่วนตัวต้นทาง

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

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

การเข้าถึงไฟล์มาตรฐานใน Android โดยใช้ File API จะช้ามาก โดยเฉพาะ (ซึ่งมักเป็นกรณีของผู้ใช้ Kiwix) หากเก็บไฟล์ขนาดใหญ่ไว้ในการ์ด microSD แทนที่จะเป็นพื้นที่เก็บข้อมูลของอุปกรณ์ ทุกอย่างจะเปลี่ยนไปด้วย API ใหม่นี้ แม้ว่าผู้ใช้ส่วนใหญ่จะจัดเก็บไฟล์ขนาด 97 GB ใน OPFS ไม่ได้ (ซึ่งจะใช้พื้นที่เก็บข้อมูลของอุปกรณ์ ไม่ใช่พื้นที่เก็บข้อมูลของการ์ด microSD) แต่ OPFS ก็เหมาะอย่างยิ่งสำหรับการจัดเก็บไฟล์เก็บถาวรขนาดเล็กถึงขนาดกลาง คุณต้องการสารานุกรมทางการแพทย์ที่สมบูรณ์ที่สุดจาก WikiProject Medicine ไหม ไม่มีปัญหา เพราะขนาด 1.7 GB จะใส่ใน OPFS ก็ใส่ได้ไม่ยาก (เคล็ดลับ: มองหา othermdwiki_en_all_maxi ในคลังในแอป)

วิธีการทำงานของ OPFS

OPFS คือระบบไฟล์ที่เบราว์เซอร์จัดเตรียมไว้แยกกันสำหรับต้นทางแต่ละแห่ง ซึ่งอาจกล่าวได้ว่าคล้ายกับพื้นที่เก็บข้อมูลระดับแอปใน Android คุณสามารถนําเข้าไฟล์ไปยัง OPFS จากระบบไฟล์ที่ผู้ใช้มองเห็น หรือดาวน์โหลดไฟล์ไปยัง 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 จึงอนุญาตให้ผู้ใช้ใช้แอปได้อย่างอิสระในระหว่างการดำเนินการ แต่แบนเนอร์จะแสดงอยู่เสมอเพื่อเตือนผู้ใช้ว่าอย่าปิดแอปจนกว่าการดาวน์โหลดจะเสร็จสมบูรณ์

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

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

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

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

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

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