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

การเปิดไฟล์

เมื่อใช้ 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 ยังคงมีแนวโน้มที่จะเปลี่ยนแปลงในอนาคต ระบบจึงไม่ได้สร้างโมเดลของ browser-fs-access 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',
  });
})();

ข้อมูลประชากร

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

ไลบรารีการเข้าถึงเบราว์เซอร์ fs ในธรรมชาติ

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

ตัวอย่างเช่น ฉันสามารถเริ่มภาพวาดใน 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 ใหม่ ส่วนไฟล์ต้นฉบับจะยังคงเดิม

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

ด้านล่างนี้เป็นตัวอย่างจริงของการเข้าถึงเบราว์เซอร์ fs ดังที่ใช้ใน 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 หายไป

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

บทสรุป

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

ข้อความแสดงการยอมรับ

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