เผยแพร่: 27 กรกฎาคม 2020
เบราว์เซอร์จัดการไฟล์และไดเรกทอรีได้มานานแล้ว 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 เป็น 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 หรือไม่
แล้วจึงนำเข้าเส้นทางโค้ดที่เกี่ยวข้อง
ใช้คลัง
ทั้ง 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 ของฉัน