Đọc và ghi tệp cũng như thư mục bằng thư viện trình duyệt-fs-access

Các trình duyệt có thể xử lý các tệp và thư mục trong một thời gian dài. API Tệp cung cấp các tính năng để biểu thị đối tượng tệp trong các ứng dụng web, cũng như lựa chọn đối tượng và truy cập dữ liệu của đối tượng theo phương thức lập trình. Tuy nhiên, khoảnh khắc bạn nhìn gần hơn, tất cả những cái lấp lánh đó không phải là vàng.

Cách truyền thống để xử lý tệp

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, bạn có thể mở tệp 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 dùng trong mọi ngữ cảnh mà một Blob có thể làm.

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ở các thư mục (hoặc thư mục), bạn có thể đặt thuộc tính <input webkitdirectory>. Ngoài ra, mọi tính năng khác cũng hoạt động tương tự như trên. Mặc dù có tên tiền tố nhà cung cấp, nhưng webkitdirectory không chỉ dùng được trong các trình duyệt Chromium và WebKit mà còn dùng được trong EdgeHTML cũ dựa trên EdgeHTML cũng như trong Firefox.

Lưu (đúng hơn là tải xuống) tệp

Để lưu tệp, thông thường, bạn chỉ được tải xuống tệp. Quá trình này hoạt động nhờ thuộc tính <a download>. Với Blob, bạn có thể đặt thuộc tính href của liên kết 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 đề

Nhược điểm rất lớn của phương pháp tải xuống là không có cách nào để mở → chỉnh sửa→lưu luồng cổ điển, nghĩa là không có cách nào để ghi đè tệp gốc. Thay vào đó, bạn sẽ nhận được một bản sao mới của tệp gốc trong thư mục Downloads (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ả thao tác, mở và lưu đơn giản hơn rất nhiều. Thao tác này cũng bật tính năng lưu đúng, tức là bạn không chỉ có thể chọn vị trí lưu tệp, mà còn 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 là một lần gọi đến phương thức window.showOpenFilePicker(). Lệnh gọi này trả về một tên người dùng tệp, từ đó bạn có thể nhận 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(). Thao tác này sẽ giúp bạn có thể chọn các thư mục trong hộp thoại tệp.

Đang lưu tệp

Việc lưu tệp cũng tương tự như vậy. Từ trình xử lý 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 và cuối cùng bạn đóng luồng bằng cách gọi phương thức 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);
  }
};

Giới thiệu browser-fs-access

Hoàn toàn ổn như API Truy cập hệ thống tệp, nhưng API này chưa được cung cấp rộng rãi.

Bảng hỗ trợ trình duyệt cho API Truy cập hệ thống tệp. Tất cả trình duyệt đều được đánh dấu là &quot;không hỗ trợ&quot; hoặc &quot;phía sau cờ&quot;.
Bảng hỗ trợ trình duyệt cho API Truy cập hệ thống tệp. (Nguồn)

Đây là lý do tại sao tôi thấy API Truy cập hệ thống tệp dưới dạng nâng cao tiến bộ. Do đó, tôi muốn sử dụng mã 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ờ phạt người dùng tải các mã JavaScript không được hỗ trợ xuống một cách không cần thiết. Thư viện browser-fs-access là câu trả lời của tôi cho thách thức 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 trình duyệt-fs-access sẽ không được mô hình hoá sau đó. Điều này nghĩa là thư viện không phải là polyfill, mà là một ponyfill. Bạn có thể (theo cách tĩnh hoặc động) độc quyền nhập bất kỳ chức năng nào mình cần để giữ cho ứng dụng của mình nhỏ nhất có thể. Các phương thức có thể sử dụng là fileOpen(), directoryOpen()fileSave(). Trong nội bộ, tính năng của 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ả 3 phương thức này đều rất trực quan để 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 lựa 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 trình duyệt-fs-access. Mã mẫu dưới đây 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 đang hoạt động trong bản minh hoạ trên Glitch. Mã nguồn của trang cũng có sẵn ở đó. Vì lý do bảo mật, các khung phụ 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ời gian rảnh, tôi đóng góp một chút vào PWA có thể cài đặt tên là Excalidraw, một công cụ bảng trắng giúp bạn dễ dàng phác thảo sơ đồ bằng tay. Nền tảng này hoàn toàn thích ứng và hoạt động tốt trên nhiều loại thiết bị, từ điện thoại di động nhỏ đến máy tính có màn hình lớn. Tức là ứng dụng này cần xử lý các tệp trên tất cả các nền tảng, bất kể các nền tảng đó có hỗ trợ API Truy cập hệ thống tệp hay không. Đây là một biến thể tuyệt vời cho thư viện browser-fs-access.

Ví dụ: tôi có thể bắt đầu một bản vẽ trên iPhone, lưu nó (về mặt kỹ thuật là: tải xuống, vì Safari không hỗ trợ API Truy cập hệ thống tệp) vào thư mục Downloads (Tệp đã tải xuống) trên iPhone, mở tệp trên máy tính để bàn (sau khi chuyển từ điện thoại của tô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 nó dưới dạng tệp mới.

Một hình vẽ Excalidraw trên iPhone.
Bắt đầu một bản vẽ Excalidraw trên iPhone khi API Truy cập hệ thống tệp không được hỗ trợ, nhưng có thể lưu (tải xuống) tệp vào thư mục Downloads (Tệp đã tải xuống).
Bản vẽ Excalidraw đã sửa đổi trên Chrome trên máy tính.
Mở và sửa đổi bản vẽ Excalidraw trên máy tính có hỗ trợ API Truy cập hệ thống tệp, do đó, có thể truy cập tệp thông qua API.
Ghi đè tệp gốc có nội dung sửa đổi.
Ghi đè tệp gốc bằng cách sửa đổi tệp bản vẽ Excalidraw ban đầu. Trình duyệt hiển thị một hộp thoại hỏi tôi xem như vậy có ổn không.
Đang lưu nội dung sửa đổi vào tệp vẽ mới của Excalidraw.
Đang lưu nội dung sửa đổi vào tệp Excalidraw mới. Tệp gốc vẫn giữ nguyên.

Mã mẫu thực tế

Bạn có thể xem ví dụ thực tế về browser-fs-access khi sử dụng trong Excalidraw. Phần trích dẫn này được lấy từ /src/data/json.ts. Điều đặc biệt cần quan tâm là phương thức saveAsJSON() chuyển tên người dùng tệp hoặc null sang phương thức fileSave() của browser-fs-access, khiến phương thức này ghi đè khi một tên người dùng được cung cấp hoặc lưu vào một tệp mới nếu không có.

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ểm cần lưu ý về giao diện người dùng

Cho dù là trong Excalidraw hay trong ứ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) ngoài nút Save (Lưu). Ảnh chụp màn hình dưới đây cho thấy sự khác biệt giữa thanh công cụ thích ứng của ứng dụng chính của Excalidraw trên iPhone và trên Chrome trên máy tính để bàn. Lưu ý rằng trên iPhone, nút Save As (Lưu dưới dạng) bị thiếu.

Thanh công cụ của ứng dụng Excalidraw trên iPhone chỉ với một nút &quot;Save&quot;.
Thanh công cụ của ứng dụng Excalidraw trên iPhone chỉ với nút Save (Lưu).
Thanh công cụ của ứng dụng Excalidraw trên trình duyệt Chrome trên máy tính có nút &quot;Lưu&quot; và nút &quot;Lưu dưới dạng&quot;.
Thanh công cụ của ứng dụng Excalidraw trên Chrome có nút Save (Lưu) và nút Save As (Lưu dưới dạng) được làm nổi bật.

Kết luận

Làm việc với các tệp hệ thống về mặt kỹ thuật hoạt động trên tất cả cá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 đè tệp (không chỉ tải xuống) tệp, đồng thời cho phép người dùng tạo tệp mới ở bất cứ đâu họ muốn, trong khi 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. Quyền truy cập browser-fs-access giúp bạn làm việc dễ dàng hơn bằng cách xử lý các tinh vi của tính năng nâng cao theo mức độ tăng dần và làm cho mã của bạn trở nên đơn giản nhất có thể.

Xác nhận

Bài viết này đã được Joe MedleyKayce Basques đánh giá. Cảm ơn những người đóng góp cho Excalidraw vì đã hoàn thành dự án và xem xét các Yêu cầu lấy dữ liệu. Hình ảnh chính của Ilya Pavlov trên Unsplash.