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

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

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

เกี่ยวกับ 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) ซึ่งมีเป้าหมายเพื่อเป็นโซลูชันที่ใช้งานง่ายและใช้ได้กับทุกอุปกรณ์ที่มีเบราว์เซอร์สมัยใหม่

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

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

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

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

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

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

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

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

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

File API คือสิ่งที่ทำให้ Kiwix JS อ่านที่เก็บถาวรขนาดใหญ่ได้ตั้งแต่แรก ซึ่งมีขนาดหลายร้อย GB (ที่เก็บถาวร ZIMรายการหนึ่งของเรามีขนาด 166 GB) แม้ในอุปกรณ์ที่มีหน่วยความจำต่ำ 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 นั้นใหญ่กว่า PWA ที่รวมไว้และบีบอัดไว้เพียง5.1 MB มาก เนื่องจากมีสำเนา Chromium ฉบับสมบูรณ์รวมอยู่ด้วยในทุกแอป

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

โปรดทราบว่าคุณต้องระบุฟังก์ชันเพื่อจัดเก็บตัวแฮนเดิลไฟล์ ไม่มีเมธอดที่สะดวกสําหรับการดำเนินการนี้ เว้นแต่คุณจะใช้ไลบรารีการแยกความคิด การใช้งานของ 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 Private File System

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

ระบบไฟล์ส่วนตัวของ 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 โปรดอ่านระบบไฟล์ส่วนตัวของ Origin)

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 หรือไม่

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

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