การอ่านและเขียนไฟล์และไดเรกทอรีด้วยไลบรารี Browser-fs-access

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

วิธีจัดการไฟล์แบบเดิม

การเปิดไฟล์

ในฐานะนักพัฒนาแอป คุณสามารถเปิดและอ่านไฟล์ผ่านองค์ประกอบ <input type="file"> ได้ การเปิดไฟล์ในรูปแบบที่ง่ายที่สุดอาจมีลักษณะคล้ายกับตัวอย่างโค้ดด้านล่าง ออบเจ็กต์ input จะให้ FileList แก่คุณ ซึ่งในกรณีด้านล่างประกอบด้วย File เพียงรายการเดียว File เป็นBlob ประเภทหนึ่ง และใช้ได้ในทุกบริบทที่ Blob ใช้ได้

const openFile = async () => {
  return new Promise((resolve) => {
    const input = document.createElement('input');
    input.type = 'file';
    input.addEventListener('change', () => {
      resolve(input.files[0]);
    });
    input.click();
  });
};

การเปิดไดเรกทอรี

หากต้องการเปิดโฟลเดอร์ (หรือไดเรกทอรี) คุณสามารถตั้งค่าแอตทริบิวต์ <input webkitdirectory> ได้ นอกเหนือจากนั้น ทุกอย่างจะทำงานเหมือนกับด้านบน แม้จะมีชื่อที่ขึ้นต้นด้วยคำนำหน้าของผู้ให้บริการ แต่ webkitdirectory ไม่ได้ใช้ได้เฉพาะในเบราว์เซอร์ Chromium และ WebKit เท่านั้น แต่ยังใช้ได้ใน Edge ที่ใช้ EdgeHTML รุ่นเดิม รวมถึงใน Firefox ด้วย

การบันทึก (หรือดาวน์โหลด) ไฟล์

โดยปกติแล้ว การบันทึกไฟล์จะจำกัดไว้ที่การดาวน์โหลดไฟล์ ซึ่งทำงานได้เนื่องจากแอตทริบิวต์ <a download> เมื่อมี Blob คุณจะตั้งค่าแอตทริบิวต์ href ของ Anchor เป็น blob: URL ที่รับได้จากเมธอด URL.createObjectURL()

const saveFile = async (blob) => {
  const a = document.createElement('a');
  a.download = 'my-file.txt';
  a.href = URL.createObjectURL(blob);
  a.addEventListener('click', (e) => {
    setTimeout(() => URL.revokeObjectURL(a.href), 30 * 1000);
  });
  a.click();
};

ปัญหา

ข้อเสียที่สำคัญของวิธีการดาวน์โหลดคือไม่มีวิธีทำให้เกิดขั้นตอนการเปิด→แก้ไข→บันทึกแบบคลาสสิก นั่นคือไม่มีวิธีเขียนทับไฟล์ต้นฉบับ แต่คุณจะได้สำเนาใหม่ของไฟล์ต้นฉบับ ในโฟลเดอร์ดาวน์โหลดเริ่มต้นของระบบปฏิบัติการทุกครั้งที่คุณ "บันทึก"

File System Access API

File System Access API ช่วยให้การดำเนินการทั้ง 2 อย่าง ได้แก่ การเปิดและการบันทึก ง่ายขึ้นมาก นอกจากนี้ยังช่วยให้บันทึกได้จริง นั่นคือคุณไม่เพียงเลือกตำแหน่งที่จะบันทึกไฟล์ได้ แต่ยังเขียนทับไฟล์ที่มีอยู่ได้ด้วย

การเปิดไฟล์

เมื่อใช้ File System Access API การเปิดไฟล์จะทำได้ด้วยการเรียกใช้เมธอด window.showOpenFilePicker() เพียงครั้งเดียว การเรียกนี้จะแสดงแฮนเดิลไฟล์ ซึ่งคุณสามารถรับ File จริงได้ผ่านเมธอด getFile()

const openFile = async () => {
  try {
    // Always returns an array.
    const [handle] = await window.showOpenFilePicker();
    return handle.getFile();
  } catch (err) {
    console.error(err.name, err.message);
  }
};

การเปิดไดเรกทอรี

เปิดไดเรกทอรีโดยเรียกใช้ window.showDirectoryPicker() ซึ่งจะทำให้เลือกไดเรกทอรีได้ในกล่องโต้ตอบไฟล์

การบันทึกไฟล์

การบันทึกไฟล์ก็ทำได้ง่ายเช่นกัน จากแฮนเดิลไฟล์ คุณจะสร้างสตรีมที่เขียนได้ผ่าน createWritable(), จากนั้นเขียนข้อมูล Blob โดยเรียกใช้เมธอด write() ของสตรีม และสุดท้ายปิดสตรีมโดยเรียกใช้เมธอด close()

const saveFile = async (blob) => {
  try {
    const handle = await window.showSaveFilePicker({
      types: [{
        accept: {
          // Omitted
        },
      }],
    });
    const writable = await handle.createWritable();
    await writable.write(blob);
    await writable.close();
    return handle;
  } catch (err) {
    console.error(err.name, err.message);
  }
};

ขอแนะนำ browser-fs-access

แม้ว่า File System Access API จะใช้งานได้ดี แต่ก็ยังไม่พร้อมใช้งานอย่างแพร่หลาย

ตารางการรองรับเบราว์เซอร์สำหรับ File System Access API เบราว์เซอร์ทั้งหมดจะมีการทำเครื่องหมายเป็น &quot;ไม่รองรับ&quot; หรือ &quot;อยู่หลังฟีเจอร์ที่ต้องเปิดใช้&quot;
ตารางการรองรับเบราว์เซอร์สำหรับ File System Access API (แหล่งที่มา)

ด้วยเหตุนี้ฉันจึงมองว่า File System Access API เป็นการเพิ่มประสิทธิภาพแบบค่อยเป็นค่อยไป ดังนั้น ฉันจึงต้องการใช้เมื่อเบราว์เซอร์รองรับ และใช้วิธีการแบบเดิมหากไม่รองรับ โดยไม่ลงโทษผู้ใช้ด้วยการดาวน์โหลดโค้ด JavaScript ที่ไม่รองรับโดยไม่จำเป็น ไลบรารี browser-fs-access คือคำตอบของฉันสำหรับความท้าทายนี้

ปรัชญาการออกแบบ

เนื่องจาก File System Access API มีแนวโน้มที่จะเปลี่ยนแปลงในอนาคต browser-fs-access API จึงไม่ได้จำลองตาม API ดังกล่าว กล่าวคือ ไลบรารีไม่ใช่ polyfill แต่เป็น ponyfill คุณสามารถนําเข้า (แบบคงที่หรือแบบไดนามิก) เฉพาะฟังก์ชันการทํางานที่จําเป็นเพื่อรักษาขนาดแอปให้เล็กที่สุด วิธีการที่ใช้ได้คือ fileOpen() directoryOpen() และ fileSave() ภายใน ไลบรารีจะตรวจหาฟีเจอร์ว่าระบบรองรับ File System Access API หรือไม่ จากนั้นจะนำเข้าเส้นทางโค้ดที่เกี่ยวข้อง

การใช้ไลบรารี browser-fs-access

ทั้ง 3 วิธีนี้ใช้งานง่าย คุณสามารถระบุmimeTypesหรือไฟล์extensionsที่แอปยอมรับ และตั้งค่าแฟล็ก multiple เพื่ออนุญาตหรือไม่อนุญาตให้เลือกหลายไฟล์หรือไดเรกทอรี ดูรายละเอียดทั้งหมดได้ที่เอกสารประกอบเกี่ยวกับ Browser-FS-Access API ตัวอย่างโค้ดด้านล่างแสดงวิธีเปิดและบันทึกไฟล์รูปภาพ

// The imported methods will use the File
// System Access API or a fallback implementation.
import {
  fileOpen,
  directoryOpen,
  fileSave,
} from 'https://unpkg.com/browser-fs-access';

(async () => {
  // Open an image file.
  const blob = await fileOpen({
    mimeTypes: ['image/*'],
  });

  // Open multiple image files.
  const blobs = await fileOpen({
    mimeTypes: ['image/*'],
    multiple: true,
  });

  // Open all files in a directory,
  // recursively including subdirectories.
  const blobsInDirectory = await directoryOpen({
    recursive: true
  });

  // Save a file.
  await fileSave(blob, {
    fileName: 'Untitled.png',
  });
})();

สาธิต

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

ไลบรารี browser-fs-access ในการใช้งานจริง

ในเวลาว่าง ผมได้มีส่วนร่วมเล็กๆ น้อยๆ ในการพัฒนา PWA ที่ติดตั้งได้ ชื่อ Excalidraw ซึ่งเป็นเครื่องมือไวท์บอร์ดที่ช่วยให้คุณร่างไดอะแกรมได้อย่างง่ายดายด้วยความรู้สึกเหมือนวาดด้วยมือ โดยจะปรับเปลี่ยนตามพื้นที่โฆษณาได้อย่างเต็มที่และทำงานได้ดีในอุปกรณ์ต่างๆ ตั้งแต่โทรศัพท์มือถือขนาดเล็กไปจนถึงคอมพิวเตอร์ที่มีหน้าจอขนาดใหญ่ ซึ่งหมายความว่าแอปต้องจัดการกับไฟล์ในแพลตฟอร์มต่างๆ ทั้งหมด ไม่ว่าจะรองรับ File System Access API หรือไม่ก็ตาม จึงเป็นตัวเลือกที่ยอดเยี่ยมสำหรับไลบรารี browser-fs-access

เช่น ฉันสามารถเริ่มวาดรูปบน iPhone บันทึกรูป (ในทางเทคนิคคือดาวน์โหลด เนื่องจาก Safari ไม่รองรับ File System Access API) ลงในโฟลเดอร์ดาวน์โหลดของ iPhone เปิดไฟล์บนเดสก์ท็อป (หลังจากโอนจากโทรศัพท์) แก้ไขไฟล์ และเขียนทับด้วยการเปลี่ยนแปลง หรือแม้แต่บันทึกเป็นไฟล์ใหม่

ภาพวาด Excalidraw บน iPhone
เริ่มวาดใน Excalidraw บน iPhone ที่ไม่รองรับ File System Access API แต่บันทึก (ดาวน์โหลด) ไฟล์ลงในโฟลเดอร์ดาวน์โหลดได้
ภาพวาด Excalidraw ที่แก้ไขแล้วใน Chrome บนเดสก์ท็อป
การเปิดและแก้ไขภาพวาด Excalidraw บนเดสก์ท็อปที่รองรับ File System Access API และเข้าถึงไฟล์ผ่าน API ได้
เขียนทับไฟล์ต้นฉบับด้วยการแก้ไข
เขียนทับไฟล์ต้นฉบับด้วยการแก้ไขไฟล์ภาพวาด Excalidraw ต้นฉบับ เบราว์เซอร์จะแสดงกล่องโต้ตอบถามว่าฉันโอเคไหม
บันทึกการแก้ไขลงในไฟล์ภาพวาด Excalidraw ใหม่
บันทึกการแก้ไขลงในไฟล์ Excalidraw ใหม่ โดยไฟล์ต้นฉบับจะยังคงเดิม

ตัวอย่างโค้ดในชีวิตจริง

ด้านล่างนี้ คุณจะเห็นตัวอย่างจริงของ browser-fs-access ที่ใช้ใน Excalidraw ข้อความที่ตัดตอนมานี้มาจาก /src/data/json.ts สิ่งที่น่าสนใจเป็นพิเศษคือวิธีที่เมธอด saveAsJSON() ส่งทั้งตัวแฮนเดิลไฟล์หรือ null ไปยังเมธอดของ browser-fs-access fileSave() ซึ่งทำให้เมธอดดังกล่าวเขียนทับเมื่อมีการระบุแฮนเดิล หรือบันทึกลงในไฟล์ใหม่หากไม่มีการระบุ

export const saveAsJSON = async (
  elements: readonly ExcalidrawElement[],
  appState: AppState,
  fileHandle: any,
) => {
  const serialized = serializeAsJSON(elements, appState);
  const blob = new Blob([serialized], {
    type: "application/json",
  });
  const name = `${appState.name}.excalidraw`;
  (window as any).handle = await fileSave(
    blob,
    {
      fileName: name,
      description: "Excalidraw file",
      extensions: ["excalidraw"],
    },
    fileHandle || null,
  );
};

export const loadFromJSON = async () => {
  const blob = await fileOpen({
    description: "Excalidraw files",
    extensions: ["json", "excalidraw"],
    mimeTypes: ["application/json"],
  });
  return loadFromBlob(blob);
};

ข้อควรพิจารณาเกี่ยวกับ UI

ไม่ว่าจะใน Excalidraw หรือแอปของคุณ UI ควรปรับให้เข้ากับสถานการณ์การรองรับของเบราว์เซอร์ หากระบบรองรับ File System Access API (if ('showOpenFilePicker' in window) {}) คุณจะแสดงปุ่มบันทึกเป็นนอกเหนือจากปุ่มบันทึกได้ ภาพหน้าจอด้านล่างแสดงความแตกต่างระหว่างแถบเครื่องมือหลักที่ตอบสนองของแอป Excalidraw บน iPhone กับบนเดสก์ท็อป Chrome โปรดสังเกตว่าปุ่มบันทึกเป็นจะไม่มีใน iPhone

แถบเครื่องมือของแอป Excalidraw บน iPhone ที่มีเพียงปุ่ม &quot;บันทึก&quot;
แถบเครื่องมือของแอป Excalidraw บน iPhone ที่มีเพียงปุ่มบันทึก
แถบเครื่องมือของแอป Excalidraw ใน Chrome บนเดสก์ท็อปพร้อมปุ่ม &quot;บันทึก&quot; และ &quot;บันทึกเป็น&quot;
แถบเครื่องมือของแอป Excalidraw ใน Chrome พร้อมปุ่มบันทึกและปุ่มบันทึกเป็นที่โฟกัส

บทสรุป

การทำงานกับไฟล์ระบบใช้ได้กับเบราว์เซอร์ที่ทันสมัยทั้งหมดในทางเทคนิค ในเบราว์เซอร์ที่รองรับ File System Access API คุณสามารถปรับปรุงประสบการณ์การใช้งานได้โดยอนุญาตให้ บันทึกและเขียนทับไฟล์ได้จริง (ไม่ใช่แค่ดาวน์โหลด) และอนุญาตให้ผู้ใช้สร้างไฟล์ใหม่ได้ทุกที่ที่ต้องการ ในขณะที่ยังคงใช้งานได้ในเบราว์เซอร์ที่ไม่รองรับ File System Access API browser-fs-access ช่วยให้ชีวิตคุณง่ายขึ้น ด้วยการจัดการความซับซ้อนของการเพิ่มประสิทธิภาพแบบค่อยเป็นค่อยไปและทำให้โค้ดของคุณเรียบง่ายที่สุด

คำขอบคุณ

บทความนี้ได้รับการตรวจสอบโดย Joe Medley และ Kayce Basques ขอขอบคุณผู้มีส่วนร่วมใน Excalidraw ที่ทำงานในโปรเจ็กต์นี้และตรวจสอบคำขอ Pull ของฉัน รูปภาพหลักโดย Ilya Pavlov บน Unsplash