การอ่านและเขียนไฟล์และไดเรกทอรีด้วยไลบรารี 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 ของแอนคอร์เป็น URL blob: ที่คุณได้รับจากเมธอด 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() เพียง 1 ครั้ง การเรียกนี้แสดงผลแฮนเดิลไฟล์ ซึ่งคุณจะรับ 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 ยังมีแนวโน้มว่าจะมีการเปลี่ยนแปลงในอนาคต จึงจะไม่มีการสร้างแบบจำลอง API สำหรับเบราว์เซอร์-fs-access API ในอนาคต กล่าวคือ ไลบรารีไม่ใช่ polyfill แต่คือ ponyfill คุณสามารถนําเข้าฟังก์ชันการทำงานใดก็ได้ (แบบคงที่หรือแบบไดนามิก) เฉพาะที่คุณต้องการเพื่อให้แอปมีขนาดเล็กที่สุด วิธีการที่ใช้ได้มีชื่ออย่างเหมาะสมว่า fileOpen(), directoryOpen() และ fileSave() ฟีเจอร์ของไลบรารีจะตรวจหาภายในว่าระบบรองรับ File System Access API หรือไม่ จากนั้นจึงนําเข้าเส้นทางโค้ดที่เกี่ยวข้อง

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

ทั้ง 3 วิธีนี้ใช้งานง่าย คุณสามารถระบุ mimeTypes หรือไฟล์ extensions ที่ยอมรับของแอป และตั้งค่า Flag multiple เพื่ออนุญาตหรือไม่อนุญาตให้เลือกไฟล์หรือไดเรกทอรีหลายรายการ ดูรายละเอียดทั้งหมดได้ที่ เอกสารประกอบของเบราว์เซอร์-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',
  });
})();

สาธิต

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

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

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

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

ภาพวาดเอ็กซ์คาลิดรอว์ใน 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

UI ควรปรับให้เข้ากับสถานการณ์การสนับสนุนของเบราว์เซอร์ ไม่ว่าจะเป็นใน Excalidraw หรือแอปของคุณ หากระบบรองรับ 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 ที่ทํางานในโปรเจ็กต์และตรวจสอบคําขอดึงข้อมูลของฉัน รูปภาพหลักโดย Ilya Pavlov จาก Unsplash