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