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

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

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

การเปิดไฟล์

ในฐานะนักพัฒนาซอฟต์แวร์ คุณสามารถเปิดและอ่านไฟล์ผ่าน <input type="file"> ในรูปแบบที่ง่ายที่สุด การเปิดไฟล์อาจมีลักษณะเหมือนตัวอย่างโค้ดด้านล่าง ออบเจ็กต์ input จะให้ FileList ในกรณีด้านล่างจะมีแท็ก 1 ประเภท 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 HTML แบบเดิมและ 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() เพียงครั้งเดียว การเรียกนี้จะแสดงแฮนเดิลไฟล์ ซึ่งคุณจะรับ 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 ที่แอปยอมรับ แล้วตั้งค่า 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 ซอร์สโค้ดของโค้ดจะพร้อมใช้งานในนั้นเช่นเดียวกัน เนื่องจากระบบไม่อนุญาตให้เฟรมย่อยแบบข้ามต้นทางแสดงเครื่องมือเลือกไฟล์ เนื่องจากเหตุผลด้านความปลอดภัย ไม่สามารถฝังการสาธิตในบทความนี้

ไลบรารีการเข้าถึงของเบราว์เซอร์ fs-access

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

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

วันที่ ภาพวาดเอ็กซ์คาลิดรอว์ใน iPhone
การเริ่มต้นการวาด Excalidraw บน iPhone ที่ไม่รองรับ File System Access API แต่สามารถบันทึกไฟล์ (ดาวน์โหลด) ลงในโฟลเดอร์ Downloads ได้
ภาพวาด 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 สำหรับงานในโครงงานและการตรวจสอบคำขอพุลของฉัน รูปภาพหลักโดย Ilya Pavlov ในรายการ Unsplash