بإمكان المتصفحات التعامل مع الملفات والأدلة منذ فترة طويلة. توفّر 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 عمليتَي الفتح والحفظ بشكل كبير. يتيح لك هذا الوضع أيضًا الحفظ الفعلي، أي أنّه يمكنك اختيار مكان حفظ الملف، بالإضافة إلى إمكانية الكتابة فوق ملف حالي.
فتح الملفات
باستخدام 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 استنادًا إليها.
وهذا يعني أنّ المكتبة ليست polyfill،
بل هي ponyfill.
يمكنك استيراد أي وظائف تحتاج إليها بشكل حصري (ثابت أو ديناميكي) للحفاظ على حجم تطبيقك صغيرًا قدر الإمكان.
الطرق المتاحة هي
fileOpen()
وdirectoryOpen()
وfileSave()
.
تتحقّق المكتبة داخليًا من توفّر واجهة برمجة التطبيقات File System Access API، ثم تستورد مسار الرمز البرمجي المناسب.
استخدام مكتبة browser-fs-access
الطرق الثلاث سهلة الاستخدام.
يمكنك تحديد 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 في العالم الحقيقي
في وقت فراغي، أساهم بشكل بسيط في تطبيق ويب تقدّمي قابل للتثبيت يُسمى 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);
};
اعتبارات واجهة المستخدم
سواء في Excalidraw أو في تطبيقك، يجب أن تتكيّف واجهة المستخدم مع حالة التوافق مع المتصفّح.
في حال توفّر واجهة File System Access API (if ('showOpenFilePicker' in window) {}
)،
يمكنك عرض زر الحفظ باسم (Save As) بالإضافة إلى زر حفظ (Save).
توضّح لقطات الشاشة أدناه الفرق بين شريط أدوات تطبيق Excalidraw الرئيسي المتجاوب على iPhone وعلى متصفّح Chrome على الكمبيوتر.
لاحظ كيف أنّ زر الحفظ باسم غير متوفّر على iPhone.


الاستنتاجات
يمكن من الناحية الفنية استخدام ملفات النظام على جميع المتصفّحات الحديثة. على المتصفحات التي تتوافق مع File System Access API، يمكنك تحسين تجربة المستخدم من خلال السماح بحفظ الملفات واستبدالها بشكل كامل (وليس تنزيلها فقط)، والسماح للمستخدمين بإنشاء ملفات جديدة في أي مكان يريدونه، مع الحفاظ على الوظائف على المتصفحات التي لا تتوافق مع File System Access API. تسهّل browser-fs-access حياتك من خلال التعامل مع التفاصيل الدقيقة للتحسين التدريجي وجعل الرمز البرمجي بسيطًا قدر الإمكان.
الإقرارات
راجَع هذه المقالة جو ميدلي وكايس باسكس. أودّ أن أشكر المساهمين في Excalidraw على عملهم في المشروع وعلى مراجعة طلبات الدمج التي أرسلتها. الصورة الرئيسية من إيليا بافلوف على Unsplash.