เบราว์เซอร์จัดการไฟล์และไดเรกทอรีได้มานานแล้ว 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 เป็น blob:
URL ที่รับได้จากเมธอด
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 เป็นการเพิ่มประสิทธิภาพแบบค่อยเป็นค่อยไป ดังนั้น ฉันจึงต้องการใช้เมื่อเบราว์เซอร์รองรับ และใช้วิธีการแบบเดิมหากไม่รองรับ โดยไม่ลงโทษผู้ใช้ด้วยการดาวน์โหลดโค้ด 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
ที่แอปยอมรับ และตั้งค่าแฟล็ก 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 เปิดไฟล์บนเดสก์ท็อป (หลังจากโอนจากโทรศัพท์) แก้ไขไฟล์ และเขียนทับด้วยการเปลี่ยนแปลง หรือแม้แต่บันทึกเป็นไฟล์ใหม่




ตัวอย่างโค้ดในชีวิตจริง
ด้านล่างนี้ คุณจะเห็นตัวอย่างจริงของ 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


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