Đọ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. File API cung cấp các tính năng trình bày đối tượng tệp trong ứng dụng web, cũng như chọn chúng và truy cập dữ liệu của chúng theo phương thức lập trình. Tuy nhiên, khi bạn nhìn gần hơn thì tất cả những thứ 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 qua <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 bên dưới chỉ bao gồm một File. File là một loại cụ thể của Blob, và có thể dùng trong bất kỳ ngữ cảnh nào 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ở thư mục (hoặc thư mục), bạn có thể đặt <input webkitdirectory> . Ngoài ra, mọi tính năng khác đều hoạt động tương tự như trên. Mặc dù có tiền tố tên 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 (thay vì: tải xuống) tệp

Để lưu một tệp, thông thường, bạn bị giới hạn tải xuống một tệp, Quảng cáo này hoạt động nhờ vào <a download> . Với một Blob, bạn có thể đặt thuộc tính href của liên kết thành một URL blob: mà bạn có thể lấy từ 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 lớn của phương pháp tải xuống là không có cách nào để tạo tệp open→chỉnh sửa→ lưu luồng xảy ra, tức là không có cách nào để ghi đè tệp gốc. Thay vào đó, bạn sẽ tạo ra 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 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 thực, tức là bạn không chỉ có thể chọn vị trí lưu tệp, nhưng cũng ghi đè lên tệp hiện có.

Mở tệp

Với API Truy cập hệ thống tệp, mở tệp là vấn đề một lệnh 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 mà 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() 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

Cách lưu tệp cũng tương tự. Từ tên người dùng tệp, bạn tạo một luồng có thể ghi thông qua createWritable(), sau đó bạn phải 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 là đó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 tốt như API Truy cập hệ thống tệp, nên 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 &#39;phía sau một lá cờ&#39;.
Bảng hỗ trợ trình duyệt dành 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 là nâng cao tăng dần. Do đó, tôi muốn sử dụng công cụ này khi trình duyệt hỗ trợ, và nếu không thì dùng phương pháp truyền thống; và không bao giờ phạt người dùng bằng việc tải xuống mã JavaScript không được hỗ trợ một cách không cần thiết. Tham số 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, browser-fs-access API không được mô hình hóa theo nó. Tức là thư viện này không phải là một polyfill, mà là ponyfill. Bạn có thể (theo cách tĩnh hoặc động) nhập độc quyền mọi 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 có sẵn được đặt tên phù hợp 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, rồi 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 trong thực tế trong bản minh hoạ trên Glitch. Mã nguồn của thư viện 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, không thể nhúng bản minh hoạ vào bài viết này.

Thư viện browser-fs-access trong môi trường

Trong thời gian rảnh, tôi đóng góp một chút công sức cho PWA có thể cài đặt có tên là Excalidraw, một công cụ bảng trắng giúp các em dễ dàng phác thảo biểu đồ bằng tay. API này hoàn toàn phản hồi 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. Điều này nghĩa là giải pháp này cần xử lý các tệp trên mọi nền tảng chúng có hỗ trợ API Truy cập hệ thống tệp hay không. Điều này khiến thư viện này trở thành một ứng 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 của mình, lưu tệp (về mặt kỹ thuật: 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 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.

Một hình ảnh Excalidraw trên iPhone.
Bắt đầu 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 Tệp đã tải xuống.
Bản vẽ Excalidraw đã được sửa đổi trên Chrome trên máy tính.
Mở và sửa đổi bản vẽ Excalidraw trên màn hình khi API Truy cập hệ thống tệp được hỗ trợ, do đó có thể truy cập 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 các 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ách này có được hay không.
Lưu các 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 tệp Excalidraw mới. Tệp gốc vẫn giữ nguyên.

Mã mẫu thực tế

Dưới đây là một ví dụ thực tế về browser-fs-access khi được 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 là cách phương thức saveAsJSON() truyền dữ liệu xử lý tệp hoặc null đến browser-fs-access' fileSave() giúp phương thức này ghi đè khi bạn chỉ định một tên người dùng, hoặc để lưu vào 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ểm cần lưu ý 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). Các ả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 dành cho máy tính. Lưu ý rằng trên iPhone thiếu nút Save As (Lưu dưới dạng).

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

Kết luận

Về mặt kỹ thuật, làm việc với 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 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 đè (không chỉ tải xuống) tệp và bằng cách cho phép người dùng tạo tệp mới ở bất cứ nơi nào 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 cuộc sống của bạn dễ dàng hơn bằng cách xử lý các tính chất tinh vi của cải tiến 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 do Joe Medley xem xét và Kayce Basques. Nhờ những người đóng góp cho Excalidraw cho dự án của họ và để xem xét Yêu cầu kéo của tôi. Hình ảnh chính của Ilya Pavlov trên Unsplash.