การอ่านและเขียนไฟล์และไดเรกทอรี

เผยแพร่: 27 กรกฎาคม 2020

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

ใช้คลัง

ทั้ง 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 พร้อมปุ่มบันทึกและปุ่มบันทึกเป็นที่โฟกัส

บทสรุป

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

คำขอบคุณ

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