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