การอ่านและเขียนไฟล์และไดเรกทอรีด้วยไลบรารี 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() เพียงครั้งเดียวก็เปิดไฟล์ได้ การเรียกนี้แสดงผลแฮนเดิลไฟล์ ซึ่งคุณจะรับ 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 ที่ยอมรับของแอป และตั้งค่า Flag 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',
  });
})();

สาธิต

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

ไลบรารี 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 ไปยังเมธอด fileSave() ของ browser-fs-access ซึ่งทําให้เขียนทับเมื่อมีการให้แฮนเดิล หรือบันทึกลงในไฟล์ใหม่หากไม่ได้ให้แฮนเดิล

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