Trình duyệt đã có thể xử lý các tệp và thư mục trong một thời gian dài. File API (API Tệp) cung cấp các tính năng để biểu thị đối tượng tệp trong ứng dụng web, cũng như chọn và truy cập dữ liệu của các đối tượng đó theo phương thức lập trình. Tuy nhiên, khi bạn nhìn kỹ hơn, bạn sẽ thấy không phải tất cả những gì lấp lánh đều là vàng.
Cách xử lý tệp truyền thống
Mở tệp
Là nhà phát triển, bạn có thể mở và đọc tệp thông qua phần tử <input type="file">
.
Ở dạng đơn giản nhất, việc mở tệp có thể trông giống như mã mẫu dưới đây.
Đối tượng input
cung cấp cho bạn một FileList
, trong trường hợp dưới đây chỉ bao gồm một File
.
File
là một loại Blob
cụ thể và có thể được sử dụng trong bất kỳ ngữ cảnh nào mà Blob có thể sử dụng.
const openFile = async () => {
return new Promise((resolve) => {
const input = document.createElement('input');
input.type = 'file';
input.addEventListener('change', () => {
resolve(input.files[0]);
});
input.click();
});
};
Mở thư mục
Để mở thư mục (hoặc thư mục), bạn có thể đặt thuộc tính <input webkitdirectory>
.
Ngoài ra, mọi thứ khác đều hoạt động giống như trên.
Mặc dù có tên có tiền tố của nhà cung cấp, nhưng webkitdirectory
không chỉ có thể sử dụng trong trình duyệt Chromium và WebKit, mà còn có thể sử dụng trong Edge dựa trên EdgeHTML cũ cũng như trong Firefox.
Lưu (thay vì tải xuống) tệp
Theo truyền thống, để lưu tệp, bạn chỉ có thể tải tệp xuống. Thao tác này hoạt động nhờ thuộc tính <a download>
.
Với một Blob, bạn có thể đặt thuộc tính href
của neo thành URL blob:
mà bạn có thể lấy từ phương thức 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();
};
Vấn đề
Một nhược điểm lớn của phương pháp tải xuống là không có cách nào để thực hiện quy trình mở→chỉnh sửa→lưu cổ điển, tức là không có cách nào để ghi đè tệp gốc. Thay vào đó, bạn sẽ có một bản sao mới của tệp gốc trong thư mục Tệp đã tải xuống mặc định của hệ điều hành mỗi khi bạn "lưu".
API Truy cập hệ thống tệp
API Truy cập hệ thống tệp giúp cả hai thao tác, mở và lưu, trở nên đơn giản hơn rất nhiều. Phương thức này cũng cho phép lưu thực sự, tức là bạn không chỉ có thể chọn nơi lưu tệp mà còn có thể ghi đè tệp hiện có.
Mở tệp
Với File System Access API (API Truy cập hệ thống tệp), việc mở tệp chỉ là một lệnh gọi đến phương thức window.showOpenFilePicker()
.
Lệnh gọi này trả về một handle tệp, từ đó bạn có thể lấy File
thực tế thông qua phương thức 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);
}
};
Mở thư mục
Mở một thư mục bằng cách gọi window.showDirectoryPicker()
để có thể chọn thư mục trong hộp thoại tệp.
Lưu tệp
Việc lưu tệp cũng đơn giản như vậy.
Từ một tay cầm tệp, bạn tạo một luồng có thể ghi qua createWritable()
, sau đó ghi dữ liệu Blob bằng cách gọi phương thức write()
của luồng, cuối cùng, bạn đóng luồng bằng cách gọi phương thức close()
của luồng.
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);
}
};
Giới thiệu browser-fs-access
Mặc dù API Truy cập hệ thống tệp hoạt động hoàn hảo, nhưng API này chưa được cung cấp rộng rãi.
Đó là lý do tôi coi API Truy cập hệ thống tệp là một tính năng nâng cao dần. Do đó, tôi muốn sử dụng phương thức này khi trình duyệt hỗ trợ, và sử dụng phương pháp truyền thống nếu không; đồng thời không bao giờ trừng phạt người dùng bằng việc tải xuống không cần thiết mã JavaScript không được hỗ trợ. Thư viện browser-fs-access là câu trả lời của tôi cho thử thách này.
Triết lý thiết kế
Vì API Truy cập hệ thống tệp vẫn có thể thay đổi trong tương lai, nên API browser-fs-access không được mô hình hoá theo API đó.
Tức là thư viện này không phải là polyfill mà là ponyfill.
Bạn có thể (tĩnh hoặc động) chỉ nhập chức năng mà bạn cần để giữ cho ứng dụng của mình nhỏ nhất có thể.
Các phương thức hiện có được đặt tên phù hợp là fileOpen()
, directoryOpen()
và fileSave()
.
Trong nội bộ, tính năng thư viện sẽ phát hiện xem API truy cập hệ thống tệp có được hỗ trợ hay không, sau đó nhập đường dẫn mã tương ứng.
Sử dụng thư viện browser-fs-access
Cả ba phương thức này đều dễ sử dụng.
Bạn có thể chỉ định mimeTypes
hoặc tệp extensions
được chấp nhận của ứng dụng và đặt cờ multiple
để cho phép hoặc không cho phép chọn nhiều tệp hoặc thư mục.
Để biết toàn bộ thông tin chi tiết, hãy xem tài liệu về API browser-fs-access.
Mã mẫu bên dưới cho biết cách bạn có thể mở và lưu tệp hình ảnh.
// 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',
});
})();
Bản minh hoạ
Bạn có thể xem mã trên hoạt động trong một bản minh hoạ trên Glitch. Mã nguồn của ứng dụng cũng có sẵn ở đó. Vì lý do bảo mật, các khung con trên nhiều nguồn gốc không được phép hiển thị bộ chọn tệp, nên không thể nhúng bản minh hoạ vào bài viết này.
Thư viện browser-fs-access trong thực tế
Trong thời gian rảnh, tôi đóng góp một chút cho một ứng dụng web tiến bộ có thể cài đặt có tên là Excalidraw. Đây là một công cụ bảng trắng giúp bạn dễ dàng phác thảo sơ đồ với cảm giác như được vẽ tay. Giao diện này có khả năng thích ứng hoàn toàn và hoạt động tốt trên nhiều thiết bị, từ điện thoại di động nhỏ đến máy tính có màn hình lớn. Điều này có nghĩa là ứng dụng cần xử lý các tệp trên tất cả các nền tảng, cho dù các nền tảng đó có hỗ trợ API truy cập hệ thống tệp hay không. Điều này khiến nó trở thành một ứng cử viên tuyệt vời cho thư viện browser-fs-access.
Ví dụ: tôi có thể bắt đầu vẽ trên iPhone, lưu bản vẽ (về mặt kỹ thuật: tải bản vẽ xuống, vì Safari không hỗ trợ File System Access API) vào thư mục Tải xuống trên iPhone, mở tệp trên máy tính (sau khi chuyển tệp từ điện thoại), sửa đổi tệp và ghi đè tệp bằng các thay đổi của tôi, hoặc thậm chí lưu tệp dưới dạng tệp mới.
Mã mẫu trong thực tế
Dưới đây là ví dụ thực tế về browser-fs-access được sử dụng trong Excalidraw.
Đoạn trích này được lấy từ /src/data/json.ts
.
Điều đặc biệt quan tâm là cách phương thức saveAsJSON()
truyền một handle tệp hoặc null
đến phương thức fileSave()
của browser-fs-access, khiến phương thức này ghi đè khi được cung cấp một handle hoặc lưu vào một tệp mới nếu không.
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);
};
Những điều cần cân nhắc về giao diện người dùng
Cho dù trong Excalidraw hay ứng dụng của bạn, giao diện người dùng phải thích ứng với tình huống hỗ trợ của trình duyệt.
Nếu API Truy cập hệ thống tệp được hỗ trợ (if ('showOpenFilePicker' in window) {}
), bạn có thể hiển thị nút Save As (Lưu dưới dạng) cùng với nút Save (Lưu).
Ảnh chụp màn hình bên dưới cho thấy sự khác biệt giữa thanh công cụ ứng dụng chính thích ứng của Excalidraw trên iPhone và trên Chrome dành cho máy tính.
Lưu ý cách trên iPhone, nút Save As (Lưu dưới dạng) bị thiếu.
Kết luận
Về mặt kỹ thuật, việc xử lý tệp hệ thống hoạt động trên tất cả trình duyệt hiện đại. Trên các trình duyệt hỗ trợ API Truy cập hệ thống tệp, bạn có thể cải thiện trải nghiệm bằng cách cho phép lưu và ghi đè thực sự (không chỉ tải xuống) các tệp và cho phép người dùng tạo tệp mới ở bất cứ đâu họ muốn, đồng thời vẫn hoạt động trên các trình duyệt không hỗ trợ API Truy cập hệ thống tệp. browser-fs-access giúp bạn dễ dàng hơn bằng cách xử lý các chi tiết của tính năng cải tiến dần và giúp mã của bạn đơn giản nhất có thể.
Lời cảm ơn
Bài viết này đã được Joe Medley và Kayce Basques xem xét. Cảm ơn những người đóng góp cho Excalidraw vì những đóng góp của họ cho dự án và đã xem xét các Yêu cầu thay đổi của tôi. Hình ảnh chính của Ilya Pavlov trên Unsplash.