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

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.

Mở tệp

Là nhà phát triển, bạn có thể mở và đọc tệp qua phần tử <input type="file">. Ở dạng đơn giản nhất, việc mở một tệp có thể có dạ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

Cách lưu tệp cũng tương tự 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.

Bảng hỗ trợ trình duyệt dành cho API Truy cập hệ thống tệp. Tất cả trình duyệt đượ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)

Đó 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 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ử 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 bất kỳ chức năng nào bạn cần để giữ cho ứng dụng của mình nhỏ nhất có thể. Các phương thức có sẵn được đặt tên phù hợp 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

Đây là 3 phương thức 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 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 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 một bản vẽ trên iPhone, lưu bản vẽ (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ải xuống) trên iPhone, mở tệp trên máy tính của tôi (sau khi chuyển tệp từ điện thoại của tôi), sửa đổi tệp và ghi đè 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.

Hình vẽ Excalidraw trên iPhone.
Bắt đầu một bản vẽ Excalidraw trên iPhone không hỗ trợ API Truy cập hệ thống tệp, nhưng có thể lưu (tải xuống) tệp vào thư mục Tải xuống.
Bản vẽ Excalidraw đã sửa đổi trên Chrome trên máy tính.
Mở và chỉnh sửa bản vẽ Excalidraw trên màn hình mà API Truy cập hệ thống tệp được hỗ trợ. Nhờ đó, bạn có thể truy cập vào tệp này thông qua API.
Ghi đè tệp gốc bằng nội dung sửa đổi.
Ghi đè tệp gốc bằng các nội dung sửa đổi đối vớ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 có được không.
Lưu nội dung sửa đổi vào tệp bản vẽ Excalidraw mới.
Lưu nội dung sửa đổi vào một tệp Excalidraw mới. Tệp gốc vẫn giữ nguyên.

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ể cho hiện 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 nút Save As (Lưu dưới dạng) bị thiếu trên iPhone.

Thanh công cụ của ứng dụng Excalidraw trên iPhone chỉ bằng nút &quot;Save&quot;.
Thanh công cụ của ứng dụng Excalidraw trên iPhone chỉ có một nút Lưu.
Thanh công cụ của ứng dụng Excalidraw trên màn hình Chrome với 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à Save As (Lưu dưới dạng) đang được lấy làm tâm điểm.

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, đồng thời cho phép người dùng tạo tệp mới ở bất cứ đâu họ muốn, tất cả 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. 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 MedleyKayce Basques xem xét. Cảm ơn những người đóng góp cho Excalidraw vì đã làm việc trên 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.