Đọ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 cung cấp các tính năng để biểu thị các đối tượng tệp trong các ứng dụng web, cũng như chọn các đối tượng đó theo phương pháp lập trình và truy cập vào dữ liệu của chúng. Tuy nhiên, khi bạn nhìn kỹ hơn, bạn sẽ thấy không phải thứ gì lấp lánh cũng là vàng.

Cách xử lý tệp theo cách 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ở một tệp có thể trông giống như đoạn mã mẫu bên dưới. Đối tượng input cung cấp cho bạn một FileList, trong trường hợp dưới đây, đối tượng nà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à Blob có thể.

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, webkitdirectory không chỉ dùng được trong các trình duyệt Chromium và WebKit, mà còn dùng được trong Edge dựa trên EdgeHTML cũ cũng như trong Firefox.

Lưu (chính xác hơn là tải) tệp xuống

Để lưu một tệp, theo cách truyền thống, bạn chỉ có thể tải một tệp xuống. Cách 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 một 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 theo cách truyền thống, 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ải xuống mặc định của hệ điều hành bất cứ khi nào bạn "lưu".

File System Access API

File System Access API giúp cả hai thao tác mở và lưu trở nên đơn giản hơn nhiều. Tính năng 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 đè một tệp hiện có.

Mở tệp

Với File System Access API, việc mở một tệp chỉ cần một lệnh gọi đến phương thức window.showOpenFilePicker(). Lệnh gọi này trả về một mã nhận dạng tệp. Từ đó, bạn có thể nhận được 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 tương tự như vậy. Từ một mã nhận dạng tệp, bạn tạo một luồng có thể ghi thông qua createWritable(), sau đó bạn 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() 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 về browser-fs-access

Mặc dù File System Access API hoạt động hoàn toàn bình thường, nhưng hiện 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;đằng sau một cờ&quot;.
Bảng hỗ trợ trình duyệt cho File System Access API. (Nguồn)

Đó là lý do tôi xem File System Access API là một phương pháp cải tiến tăng dần. Do đó, tôi muốn sử dụng tính năng 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ờ gây khó chịu cho người dùng bằng cách tải xuống mã JavaScript không được hỗ trợ một cách không cần thiết. Thư viện browser-fs-access là giải pháp 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ó khả năng thay đổi trong tương lai, nên API browser-fs-access không được mô hình hoá theo API này. Tức là thư viện này không phải là một polyfill mà là một ponyfill. Bạn có thể nhập (tĩnh hoặc động) riêng mọi chức năng cần thiết để giữ cho ứng dụng của bạn có kích thước nhỏ nhất có thể. Các phương thức có sẵn là fileOpen(), directoryOpen()fileSave(). Về nội bộ, thư viện sẽ phát hiện tính năng nếu API Quyền truy cập vào hệ thống tệp được hỗ trợ, sau đó nhập đường dẫn mã tương ứng.

Sử dụng thư viện browser-fs-access

Ba phương thức này rất 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 thông tin chi tiết đầy đủ, hãy xem tài liệu về browser-fs-access API. Mẫu mã 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 đoạn mã trên hoạt động trong một bản minh hoạ trên GitHub. Mã nguồn của ứng dụng này 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 bản minh hoạ không thể được nhúng trong bài viết này.

Thư viện browser-fs-access trong tự nhiên

Vào thời gian rảnh, tôi đóng góp một chút cho PWA có thể cài đặt có tên là Excalidraw, một công cụ bảng trắng cho phép bạn dễ dàng phác thảo sơ đồ theo cách vẽ tay. Ứng dụng này hoàn toàn thích ứng 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, bất kể nền tảng đó có hỗ trợ File System Access API hay không. Điều này khiến thư viện browser-fs-access trở thành lựa chọn lý tưởng.

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 đè bằng các thay đổi của mình, hoặc thậm chí lưu tệp đó dưới dạng một tệp mới.

Một bản vẽ Excalidraw trên iPhone.
Bắt đầu vẽ bằng Excalidraw trên iPhone (nơi không hỗ trợ File System Access API) nhưng có thể lưu (tải) tệp xuống thư mục Tải xuống.
Bản vẽ đã sửa đổi trong Excalidraw 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ợ File System Access API (API Truy cập hệ thống tệp) và do đó, bạn có thể truy cập vào tệp thông qua API này.
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 sẽ hiện một hộp thoại hỏi tôi xem có ổn không.
Lưu nội dung sửa đổi vào một 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 được giữ nguyên.

Đoạn mã mẫu trong thực tế

Dưới đây là một ví dụ thực tế về browser-fs-access khi được dùng trong Excalidraw. Đoạn trích này được lấy từ /src/data/json.ts. Điều đặc biệt thú vị là cách phương thức saveAsJSON() truyền một mã nhận dạng 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ó mã nhận dạng 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 đều phải thích ứng với tình trạ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 thấy nút Lưu dưới dạng ngoài nút 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ụ chính thích ứng của ứng dụng Excalidraw trên iPhone và trên Chrome dành cho máy tính. Lưu ý rằng nút Lưu dưới dạng không xuất hiện trên iPhone.

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ó nút Lưu.
Thanh công cụ của ứng dụng Excalidraw trên Chrome dành cho máy tính có nút &quot;Lưu&quot; và &quot;Lưu dưới dạng&quot;.
Thanh công cụ của ứng dụng Excalidraw trên Chrome có nút Lưu và nút Lưu dưới dạng được làm nổi bật.

Kết luận

Về mặt kỹ thuật, việc sử dụng các tệp hệ thống hoạt động trên tất cả các trình duyệt hiện đại. Trên những trình duyệt hỗ trợ File System Access API, bạn có thể cải thiện trải nghiệm bằng cách cho phép lưu và ghi đè (không chỉ tải xuống) các tệp thực sự, đồng thời cho phép người dùng tạo tệp mới ở bất cứ nơi nào họ muốn, trong khi vẫn duy trì chức năng trên những trình duyệt không hỗ trợ File System Access API. browser-fs-access giúp cuộc sống của bạn dễ dàng hơn bằng cách xử lý những điểm tinh tế của tính năng cải tiến tăng 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à vì đã xem xét các Yêu cầu kéo của tôi. Hình ảnh chính của Ilya Pavlov trên Unsplash.