Đọ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.

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.

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;bị gắn 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 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()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ột bản 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à sửa đổi bản vẽ Excalidraw trên máy tính nơi API Truy cập hệ thống tệp được hỗ trợ và do đó, bạn có thể truy cập vào tệp thông qua API.
Ghi đè tệp gốc bằng nội dung sửa đổi.
Ghi đè tệp gốc bằng nội dung sửa đổi đối với tệp bản vẽ Excalidraw gốc. 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ể 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.

Thanh công cụ của ứng dụng Excalidraw trên iPhone chỉ có nút &quot;Lưu&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áy tính chạy Chrome, có nút &quot;Save&quot; (Lưu) và &quot;Save As&quot; (Lưu dưới dạng).
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 tiêu đ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 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 MedleyKayce 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.