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