Cải thiện dần Ứng dụng web tiến bộ của bạn

Xây dựng cho các trình duyệt hiện đại và cải tiến dần như thể đang ở năm 2003

Vào tháng 3 năm 2003, Nick FinckSteve Champeon đã gây chấn động thế giới thiết kế web bằng khái niệm tăng cường dần. Đây là chiến lược thiết kế web nhấn mạnh việc tải nội dung chính của trang web trước tiên, sau đó tăng dần các lớp trình bày và tính năng tinh tế hơn cũng như nghiêm ngặt hơn về mặt kỹ thuật lên trên nội dung. Trong khi đó, vào năm 2003, tính năng cải tiến dần dần là về việc sử dụng các tính năng CSS hiện đại, JavaScript không gây phiền toái và thậm chí chỉ là Đồ hoạ vectơ có thể mở rộng. Cải tiến tăng dần trong năm 2020 trở đi là về việc sử dụng các tính năng của trình duyệt hiện đại.

Thiết kế web toàn diện cho tương lai với tính năng cải tiến dần. Trang trình bày tiêu đề trong bản trình bày gốc của Finck và Champeon.
Trang trình bày: Thiết kế web toàn diện cho tương lai bằng tính năng cải tiến tăng dần. (Nguồn)

JavaScript hiện đại

Nói về JavaScript, tình hình hỗ trợ trình duyệt cho các tính năng JavaScript cốt lõi mới nhất của ES 2015 rất tuyệt vời. Tiêu chuẩn mới bao gồm các lời hứa, mô-đun, lớp, giá trị cố định của mẫu, hàm mũi tên, letconst, tham số mặc định, trình tạo, gán cấu trúc phân ly, phần còn lại và truyền, Map/Set, WeakMap/WeakSet và nhiều tính năng khác. Tất cả đều được hỗ trợ.

Bảng hỗ trợ CanIUse cho các tính năng ES6 cho thấy khả năng hỗ trợ trên tất cả các trình duyệt chính.
Bảng hỗ trợ trình duyệt ECMAScript 2015 (ES6). (Nguồn)

Bạn có thể sử dụng hàm không đồng bộ (một tính năng của ES 2017 và là một trong những tính năng mà tôi yêu thích) trong tất cả các trình duyệt chính. Từ khoá asyncawait cho phép bạn viết hành vi không đồng bộ, dựa trên lời hứa theo cách gọn gàng hơn, tránh phải định cấu hình rõ ràng các chuỗi lời hứa.

Bảng hỗ trợ CanIUse cho các hàm không đồng bộ cho thấy khả năng hỗ trợ trên tất cả các trình duyệt chính.
Bảng hỗ trợ trình duyệt cho các hàm Async. (Nguồn)

Thậm chí, ngay cả các tính năng bổ sung mới nhất cho ngôn ngữ ES 2020 như chuỗi tuỳ chọnhợp nhất giá trị rỗng cũng đã được hỗ trợ rất nhanh chóng. Bạn có thể xem mã mẫu bên dưới. Khi nói đến các tính năng cốt lõi của JavaScript, mọi thứ không thể tốt hơn được nữa.

const adventurer = {
  name: 'Alice',
  cat: {
    name: 'Dinah',
  },
};
console.log(adventurer.dog?.name);
// Expected output: undefined
console.log(0 ?? 42);
// Expected output: 0
Hình nền cỏ xanh đặc trưng của Windows XP.
Các tính năng cốt lõi của JavaScript vẫn hoạt động tốt. (Ảnh chụp màn hình sản phẩm của Microsoft, được sử dụng với quyền.)

Ứng dụng mẫu: Fugu Greetings

Trong bài viết này, tôi sẽ làm việc với một PWA đơn giản có tên là Fugu Greetings (GitHub). Tên của ứng dụng này là một lời tri ân đến Dự án Fugu 🐡, một nỗ lực nhằm mang đến cho web tất cả các tính năng của ứng dụng Android/iOS/máy tính. Bạn có thể đọc thêm về dự án này trên trang đích của dự án.

Fugu Greetings là một ứng dụng vẽ giúp bạn tạo thiệp chúc mừng ảo và gửi cho những người thân yêu. Ứng dụng này minh hoạ các khái niệm cốt lõi của PWA. Công cụ này đáng tin cậy và có thể hoạt động hoàn toàn khi không có mạng, vì vậy, ngay cả khi không có mạng, bạn vẫn có thể sử dụng công cụ này. Bạn cũng có thể Cài đặt ứng dụng này vào màn hình chính của thiết bị và tích hợp liền mạch với hệ điều hành dưới dạng một ứng dụng độc lập.

Ứng dụng PWA Fugu Greetings có hình vẽ giống với biểu trưng của cộng đồng PWA.
Ứng dụng mẫu Fugu Greetings.

Cải tiến tăng dần

Giờ đây, chúng ta có thể nói về tính năng nâng cao cải tiến tăng dần. Từ vựng Tài liệu web MDN xác định khái niệm này như sau:

Cải tiến tăng dần là một triết lý thiết kế cung cấp đường cơ sở về nội dung và chức năng thiết yếu cho nhiều người dùng nhất có thể, đồng thời chỉ mang lại trải nghiệm tốt nhất có thể cho người dùng các trình duyệt hiện đại nhất có thể chạy tất cả mã bắt buộc.

Phương thức phát hiện tính năng thường được dùng để xác định xem trình duyệt có thể xử lý chức năng hiện đại hơn hay không, còn polyfill thường được dùng để thêm các tính năng bị thiếu bằng JavaScript.

[…]

Cải tiến tăng dần là một kỹ thuật hữu ích cho phép nhà phát triển web tập trung vào việc phát triển các trang web tốt nhất có thể, đồng thời giúp các trang web đó hoạt động trên nhiều tác nhân người dùng không xác định. Suy giảm linh hoạt có liên quan nhưng không giống nhau và thường được xem là đi theo hướng ngược lại với việc cải tiến dần. Trong thực tế, cả hai phương pháp đều hợp lệ và thường có thể bổ sung cho nhau.

Những người đóng góp cho MDN

Việc bắt đầu mỗi thiệp chúc mừng từ đầu có thể rất rườm rà. Vậy tại sao không có một tính năng cho phép người dùng nhập hình ảnh và bắt đầu từ đó? Với phương pháp truyền thống, bạn sẽ sử dụng phần tử <input type=file> để thực hiện việc này. Trước tiên, bạn sẽ tạo phần tử, đặt type thành 'file' và thêm các loại MIME vào thuộc tính accept, sau đó "nhấp" vào phần tử đó theo phương thức lập trình và theo dõi các thay đổi. Khi bạn chọn một hình ảnh, hình ảnh đó sẽ được nhập thẳng vào canvas.

const importImage = async () => {
  return new Promise((resolve) => {
    const input = document.createElement('input');
    input.type = 'file';
    input.accept = 'image/*';
    input.addEventListener('change', () => {
      resolve(input.files[0]);
    });
    input.click();
  });
};

Khi có tính năng nhập, có thể bạn cũng nên có tính năng xuất để người dùng có thể lưu thiệp chúc mừng trên thiết bị. Cách lưu tệp truyền thống là tạo một đường liên kết neo bằng thuộc tính download và URL blob làm href. Bạn cũng sẽ "nhấp" vào nút này theo phương thức lập trình để kích hoạt quá trình tải xuống, đồng thời đừng quên thu hồi URL đối tượng blob để ngăn rò rỉ bộ nhớ.

const exportImage = async (blob) => {
  const a = document.createElement('a');
  a.download = 'fugu-greeting.png';
  a.href = URL.createObjectURL(blob);
  a.addEventListener('click', (e) => {
    setTimeout(() => URL.revokeObjectURL(a.href), 30 * 1000);
  });
  a.click();
};

Nhưng chờ đã. Trong tâm trí, bạn không "tải" thiệp chúc mừng xuống mà đã "lưu" thiệp đó. Thay vì hiển thị hộp thoại "lưu" cho phép bạn chọn vị trí đặt tệp, trình duyệt đã trực tiếp tải thiệp chúc mừng xuống mà không cần người dùng tương tác và đặt tệp đó ngay vào thư mục Tải xuống. Điều này không tốt.

Nếu có cách tốt hơn thì sao? Điều gì sẽ xảy ra nếu bạn chỉ có thể mở một tệp cục bộ, chỉnh sửa tệp đó rồi lưu nội dung sửa đổi vào một tệp mới hoặc quay lại tệp gốc mà bạn đã mở ban đầu? Hóa ra có. File System Access API (API truy cập hệ thống tệp) cho phép bạn mở và tạo các tệp và thư mục, cũng như sửa đổi và lưu các tệp và thư mục đó .

Vậy làm cách nào để phát hiện tính năng của API? API Truy cập hệ thống tệp hiển thị một phương thức mới window.chooseFileSystemEntries(). Do đó, tôi cần tải có điều kiện các mô-đun nhập và xuất khác nhau tuỳ thuộc vào việc phương thức này có sẵn hay không. Tôi đã hướng dẫn cách thực hiện việc này ở bên dưới.

const loadImportAndExport = () => {
  if ('chooseFileSystemEntries' in window) {
    Promise.all([
      import('./import_image.mjs'),
      import('./export_image.mjs'),
    ]);
  } else {
    Promise.all([
      import('./import_image_legacy.mjs'),
      import('./export_image_legacy.mjs'),
    ]);
  }
};

Tuy nhiên, trước khi đi sâu vào thông tin chi tiết về API Truy cập hệ thống tệp, hãy để tôi nhanh chóng nêu bật mẫu cải tiến tăng dần tại đây. Trên các trình duyệt hiện không hỗ trợ API Quyền truy cập vào hệ thống tệp, tôi sẽ tải các tập lệnh cũ. Bạn có thể xem các thẻ mạng của Firefox và Safari ở bên dưới.

Công cụ kiểm tra web Safari cho thấy các tệp cũ đang được tải.
Thẻ mạng của Safari Web Inspector.
Công cụ dành cho nhà phát triển Firefox cho thấy các tệp cũ đang được tải.
Thẻ mạng trong Công cụ dành cho nhà phát triển Firefox.

Tuy nhiên, trên Chrome, một trình duyệt hỗ trợ API, chỉ các tập lệnh mới được tải. Điều này có thể thực hiện một cách thanh lịch nhờ import() động mà tất cả trình duyệt hiện đại đều hỗ trợ. Như tôi đã nói trước đó, cỏ dại khá xanh tươi những ngày này.

Công cụ của Chrome cho nhà phát triển hiển thị các tệp hiện đại đang tải.
Thẻ mạng trong Công cụ của Chrome cho nhà phát triển.

API Truy cập hệ thống tệp

Giờ đây, khi tôi đã giải quyết vấn đề này, đã đến lúc xem xét cách triển khai thực tế dựa trên API Truy cập hệ thống tệp. Để nhập hình ảnh, tôi gọi window.chooseFileSystemEntries() và truyền cho thuộc tính này một thuộc tính accepts mà tôi muốn có các tệp hình ảnh. Hỗ trợ cả đuôi tệp và loại MIME. Thao tác này sẽ tạo ra một handle tệp, từ đó tôi có thể lấy tệp thực tế bằng cách gọi getFile().

const importImage = async () => {
  try {
    const handle = await window.chooseFileSystemEntries({
      accepts: [
        {
          description: 'Image files',
          mimeTypes: ['image/*'],
          extensions: ['jpg', 'jpeg', 'png', 'webp', 'svg'],
        },
      ],
    });
    return handle.getFile();
  } catch (err) {
    console.error(err.name, err.message);
  }
};

Việc xuất hình ảnh cũng gần giống như vậy, nhưng lần này tôi cần truyền tham số loại 'save-file' vào phương thức chooseFileSystemEntries(). Từ đó, tôi nhận được hộp thoại lưu tệp. Khi tệp đang mở, bạn không cần làm việc này vì 'open-file' là mặc định. Tôi đặt thông số accepts tương tự như trước, nhưng lần này chỉ giới hạn ở hình ảnh PNG. Một lần nữa, tôi nhận lại một handle tệp, nhưng thay vì nhận tệp, lần này tôi tạo một luồng có thể ghi bằng cách gọi createWritable(). Tiếp theo, tôi ghi blob (là hình ảnh thiệp chúc mừng) vào tệp. Cuối cùng, tôi đóng luồng có thể ghi.

Mọi thứ đều có thể không thành công: Ổ đĩa có thể hết dung lượng, có thể xảy ra lỗi ghi hoặc đọc, hoặc có thể đơn giản là người dùng huỷ hộp thoại tệp. Đây là lý do tôi luôn gói các lệnh gọi trong câu lệnh try...catch.

const exportImage = async (blob) => {
  try {
    const handle = await window.chooseFileSystemEntries({
      type: 'save-file',
      accepts: [
        {
          description: 'Image file',
          extensions: ['png'],
          mimeTypes: ['image/png'],
        },
      ],
    });
    const writable = await handle.createWritable();
    await writable.write(blob);
    await writable.close();
  } catch (err) {
    console.error(err.name, err.message);
  }
};

Bằng cách sử dụng tính năng cải tiến dần bằng API Truy cập hệ thống tệp, tôi có thể mở tệp như trước. Tệp đã nhập được vẽ ngay trên canvas. Tôi có thể chỉnh sửa và cuối cùng lưu nội dung chỉnh sửa bằng một hộp thoại lưu thực sự, trong đó tôi có thể chọn tên và vị trí lưu trữ của tệp. Giờ đây, tệp đã sẵn sàng để lưu trữ vĩnh viễn.

Ứng dụng Fugu Greetings có hộp thoại mở tệp.
Hộp thoại mở tệp.
Ứng dụng Fugu Greetings hiện có một hình ảnh đã nhập.
Hình ảnh đã nhập.
Ứng dụng Fugu Greetings với hình ảnh đã sửa đổi.
Lưu hình ảnh đã sửa đổi vào một tệp mới.

API Chia sẻ trên web và API Mục tiêu chia sẻ trên web

Ngoài việc lưu trữ mãi mãi, có thể tôi thực sự muốn chia sẻ thiệp chúc mừng của mình. Đây là điều mà Web Share APIWeb Share Target API cho phép tôi thực hiện. Gần đây, các hệ điều hành dành cho thiết bị di động và máy tính đã tích hợp các cơ chế chia sẻ. Ví dụ: bên dưới là trang chia sẻ của Safari trên máy tính chạy macOS được kích hoạt từ một bài viết trên blog của tôi. Khi nhấp vào nút Chia sẻ bài viết, bạn có thể chia sẻ đường liên kết đến bài viết với một người bạn, ví dụ: thông qua ứng dụng Tin nhắn trên macOS.

Trang chia sẻ của Safari trên máy tính trên macOS được kích hoạt từ nút Chia sẻ của một bài viết
Web Share API trên Safari dành cho máy tính trên macOS.

Mã để thực hiện việc này khá đơn giản. Tôi gọi navigator.share() và truyền vào đó một title, texturl không bắt buộc trong một đối tượng. Nhưng nếu tôi muốn đính kèm hình ảnh thì sao? Cấp 1 của API Chia sẻ trên web chưa hỗ trợ tính năng này. Tin vui là Web Share cấp 2 đã bổ sung các tính năng chia sẻ tệp.

try {
  await navigator.share({
    title: 'Check out this article:',
    text: `"${document.title}" by @tomayac:`,
    url: document.querySelector('link[rel=canonical]').href,
  });
} catch (err) {
  console.warn(err.name, err.message);
}

Hãy để tôi hướng dẫn bạn cách thực hiện việc này với ứng dụng Thiệp chúc mừng Fugu. Trước tiên, tôi cần chuẩn bị một đối tượng data với một mảng files bao gồm một blob, sau đó là titletext. Tiếp theo, theo phương pháp hay nhất, tôi sử dụng phương thức navigator.canShare() mới. Phương thức này thực hiện đúng như tên gọi: cho tôi biết liệu trình duyệt có thể chia sẻ đối tượng data mà tôi đang cố gắng chia sẻ hay không. Nếu navigator.canShare() cho tôi biết dữ liệu có thể được chia sẻ, tôi đã sẵn sàng gọi navigator.share() như trước. Vì mọi thứ đều có thể không thành công, nên tôi sẽ sử dụng lại khối try...catch.

const share = async (title, text, blob) => {
  const data = {
    files: [
      new File([blob], 'fugu-greeting.png', {
        type: blob.type,
      }),
    ],
    title: title,
    text: text,
  };
  try {
    if (!(navigator.canShare(data))) {
      throw new Error("Can't share data.", data);
    }
    await navigator.share(data);
  } catch (err) {
    console.error(err.name, err.message);
  }
};

Như trước đây, tôi sử dụng tính năng cải tiến tăng dần. Nếu cả 'share''canShare' đều tồn tại trên đối tượng navigator, thì tôi mới tiếp tục và tải share.mjs thông qua import() động. Trên các trình duyệt như Safari dành cho thiết bị di động chỉ đáp ứng một trong hai điều kiện, tôi sẽ không tải chức năng này.

const loadShare = () => {
  if ('share' in navigator && 'canShare' in navigator) {
    import('./share.mjs');
  }
};

Trong ứng dụng Fugu Greetings, nếu tôi nhấn vào nút Chia sẻ trên một trình duyệt hỗ trợ như Chrome trên Android, thì trang chia sẻ tích hợp sẽ mở ra. Ví dụ: tôi có thể chọn Gmail và tiện ích trình soạn email sẽ bật lên cùng với hình ảnh đính kèm.

Trang chia sẻ ở cấp hệ điều hành cho thấy nhiều ứng dụng để chia sẻ hình ảnh.
Chọn một ứng dụng để chia sẻ tệp.
Tiện ích soạn email của Gmail có hình ảnh đính kèm.
Tệp được đính kèm vào một email mới trong trình soạn thư của Gmail.

API Bộ chọn danh bạ

Tiếp theo, tôi muốn nói về danh bạ, tức là sổ địa chỉ hoặc ứng dụng quản lý danh bạ của thiết bị. Khi bạn viết thiệp chúc mừng, không phải lúc nào bạn cũng có thể viết chính xác tên của một người. Ví dụ: Tôi có một người bạn tên là Sergey. Anh ấy muốn tên mình được viết bằng chữ cái Cyrillic. Tôi đang sử dụng bàn phím QWERTZ của Đức và không biết cách nhập tên của họ. Đây là vấn đề mà Contact Picker API có thể giải quyết. Vì đã lưu bạn bè của mình trong ứng dụng danh bạ trên điện thoại, nên thông qua API bộ chọn danh bạ, tôi có thể truy cập vào danh bạ của mình trên web.

Trước tiên, tôi cần chỉ định danh sách các thuộc tính mà tôi muốn truy cập. Trong trường hợp này, tôi chỉ muốn tên, nhưng đối với các trường hợp sử dụng khác, tôi có thể quan tâm đến số điện thoại, email, biểu tượng hình đại diện hoặc địa chỉ thực tế. Tiếp theo, tôi định cấu hình đối tượng options và đặt multiple thành true để có thể chọn nhiều mục nhập. Cuối cùng, tôi có thể gọi navigator.contacts.select() để trả về các thuộc tính mong muốn cho những người liên hệ do người dùng chọn.

const getContacts = async () => {
  const properties = ['name'];
  const options = { multiple: true };
  try {
    return await navigator.contacts.select(properties, options);
  } catch (err) {
    console.error(err.name, err.message);
  }
};

Và giờ đây, có thể bạn đã nắm được mẫu: Tôi chỉ tải tệp khi API thực sự được hỗ trợ.

if ('contacts' in navigator) {
  import('./contacts.mjs');
}

Trong ứng dụng Fugu Greeting, khi tôi nhấn vào nút Contacts (Danh bạ) và chọn hai người bạn thân nhất của mình là Сергей Михайлович Брин劳伦斯·爱德华·"拉里"·佩奇, bạn có thể thấy bộ chọn danh bạ chỉ hiển thị tên của họ mà không hiển thị địa chỉ email hoặc thông tin khác như số điện thoại. Sau đó, tên của họ được vẽ lên thiệp chúc mừng của tôi.

Bộ chọn người liên hệ hiển thị tên của hai người liên hệ trong sổ địa chỉ.
Chọn hai tên bằng bộ chọn người liên hệ trong sổ địa chỉ.
Tên của hai người liên hệ đã chọn trước đó được vẽ trên thiệp chúc mừng.
Sau đó, hai cái tên này sẽ được vẽ lên thiệp chúc mừng.

API Bảng nhớ tạm không đồng bộ

Tiếp theo là thao tác sao chép và dán. Một trong những thao tác mà chúng tôi yêu thích khi làm nhà phát triển phần mềm là sao chép và dán. Là một tác giả thiệp chúc mừng, đôi khi tôi cũng muốn làm như vậy. Tôi có thể muốn dán hình ảnh vào thiệp chúc mừng mà tôi đang làm hoặc sao chép thiệp chúc mừng để có thể tiếp tục chỉnh sửa thiệp từ một nơi khác. API Bảng nhớ tạm không đồng bộ hỗ trợ cả văn bản và hình ảnh. Hãy để tôi hướng dẫn bạn cách thêm tính năng hỗ trợ sao chép và dán vào ứng dụng Fugu Greetings.

Để sao chép nội dung vào bảng nhớ tạm của hệ thống, tôi cần ghi vào bảng nhớ tạm đó. Phương thức navigator.clipboard.write() lấy một mảng các mục trên bảng nhớ tạm làm tham số. Về cơ bản, mỗi mục trên bảng nhớ tạm là một đối tượng có blob làm giá trị và loại của blob làm khoá.

const copy = async (blob) => {
  try {
    await navigator.clipboard.write([
      new ClipboardItem({
        [blob.type]: blob,
      }),
    ]);
  } catch (err) {
    console.error(err.name, err.message);
  }
};

Để dán, tôi cần lặp lại các mục trên bảng nhớ tạm mà tôi nhận được bằng cách gọi navigator.clipboard.read(). Lý do là nhiều mục trên bảng nhớ tạm có thể nằm trên bảng nhớ tạm ở nhiều dạng. Mỗi mục trên bảng nhớ tạm có một trường types cho tôi biết các loại MIME của tài nguyên có sẵn. Tôi gọi phương thức getType() của mục bảng nhớ tạm, truyền loại MIME mà tôi đã nhận được trước đó.

const paste = async () => {
  try {
    const clipboardItems = await navigator.clipboard.read();
    for (const clipboardItem of clipboardItems) {
      try {
        for (const type of clipboardItem.types) {
          const blob = await clipboardItem.getType(type);
          return blob;
        }
      } catch (err) {
        console.error(err.name, err.message);
      }
    }
  } catch (err) {
    console.error(err.name, err.message);
  }
};

Và giờ đây, bạn gần như không cần phải nói điều này. Tôi chỉ làm việc này trên các trình duyệt hỗ trợ.

if ('clipboard' in navigator && 'write' in navigator.clipboard) {
  import('./clipboard.mjs');
}

Vậy trong thực tế, cách này hoạt động như thế nào? Tôi mở một hình ảnh trong ứng dụng Xem trước của macOS và sao chép hình ảnh đó vào bảng nhớ tạm. Khi tôi nhấp vào Dán, ứng dụng Fugu Greetings sẽ hỏi tôi xem tôi có muốn cho phép ứng dụng xem văn bản và hình ảnh trên bảng nhớ tạm hay không.

Ứng dụng Fugu Greetings hiển thị lời nhắc cấp quyền truy cập vào bảng nhớ tạm.
Lời nhắc cấp quyền truy cập vào bảng nhớ tạm.

Cuối cùng, sau khi chấp nhận quyền, hình ảnh sẽ được dán vào ứng dụng. Cách ngược lại cũng hoạt động. Hãy để tôi sao chép một thiệp chúc mừng vào bảng nhớ tạm. Sau đó, khi tôi mở Preview (Xem trước) và nhấp vào File (Tệp) rồi nhấp vào New from Clipboard (Mới từ bảng nhớ tạm), thiệp chúc mừng sẽ được dán vào một hình ảnh mới chưa có tiêu đề.

Ứng dụng Xem trước trên macOS có một hình ảnh chưa có tiêu đề, vừa được dán.
Hình ảnh được dán vào ứng dụng Xem trước trên macOS.

API Gán huy hiệu

Một API hữu ích khác là API gắn huy hiệu. Là một PWA có thể cài đặt, Fugu Greetings tất nhiên có một biểu tượng ứng dụng mà người dùng có thể đặt trên thanh dock ứng dụng hoặc màn hình chính. Một cách thú vị và dễ dàng để minh hoạ API là (lạm) dụng API đó trong ứng dụng Fugu Greetings làm bộ đếm nét bút. Tôi đã thêm một trình nghe sự kiện để tăng bộ đếm nét bút mỗi khi sự kiện pointerdown xảy ra, sau đó đặt huy hiệu biểu tượng đã cập nhật. Bất cứ khi nào canvas bị xoá, bộ đếm sẽ đặt lại và huy hiệu sẽ bị xoá.

let strokes = 0;

canvas.addEventListener('pointerdown', () => {
  navigator.setAppBadge(++strokes);
});

clearButton.addEventListener('click', () => {
  strokes = 0;
  navigator.setAppBadge(strokes);
});

Tính năng này là một tính năng nâng cao dần, vì vậy, logic tải sẽ như bình thường.

if ('setAppBadge' in navigator) {
  import('./badge.mjs');
}

Trong ví dụ này, tôi đã vẽ các số từ 1 đến 7, sử dụng một nét bút cho mỗi số. Bộ đếm huy hiệu trên biểu tượng hiện ở mức 7.

Các số từ 1 đến 7 được vẽ lên thiệp chúc mừng, mỗi số chỉ có một nét bút.
Vẽ các số từ 1 đến 7 bằng 7 nét bút.
Biểu tượng huy hiệu trên ứng dụng Fugu Greetings hiển thị số 7.
Bộ đếm nét vẽ bằng bút ở dạng huy hiệu biểu tượng ứng dụng.

API Đồng bộ hoá định kỳ ở chế độ nền

Bạn muốn bắt đầu mỗi ngày bằng những điều mới mẻ? Một tính năng thú vị của ứng dụng Fugu Greetings là có thể truyền cảm hứng cho bạn mỗi sáng bằng một hình nền mới để bắt đầu tạo thiệp chúc mừng. Ứng dụng sử dụng API Đồng bộ hoá định kỳ ở chế độ nền để đạt được điều này.

Bước đầu tiên là register một sự kiện đồng bộ hoá định kỳ trong quá trình đăng ký worker dịch vụ. Trình nghe này theo dõi thẻ đồng bộ hoá có tên là 'image-of-the-day' và có khoảng thời gian tối thiểu là một ngày để người dùng có thể nhận được hình nền mới mỗi 24 giờ.

const registerPeriodicBackgroundSync = async () => {
  const registration = await navigator.serviceWorker.ready;
  try {
    registration.periodicSync.register('image-of-the-day-sync', {
      // An interval of one day.
      minInterval: 24 * 60 * 60 * 1000,
    });
  } catch (err) {
    console.error(err.name, err.message);
  }
};

Bước thứ hai là nghe sự kiện periodicsync trong worker dịch vụ. Nếu thẻ sự kiện là 'image-of-the-day', tức là thẻ đã được đăng ký trước đó, thì hình ảnh của ngày đó sẽ được truy xuất thông qua hàm getImageOfTheDay() và kết quả sẽ được truyền đến tất cả ứng dụng để các ứng dụng này có thể cập nhật canvas và bộ nhớ đệm.

self.addEventListener('periodicsync', (syncEvent) => {
  if (syncEvent.tag === 'image-of-the-day-sync') {
    syncEvent.waitUntil(
      (async () => {
        const blob = await getImageOfTheDay();
        const clients = await self.clients.matchAll();
        clients.forEach((client) => {
          client.postMessage({
            image: blob,
          });
        });
      })()
    );
  }
});

Xin nhắc lại rằng đây thực sự là một tính năng nâng cao dần dần, vì vậy, mã chỉ được tải khi trình duyệt hỗ trợ API. Điều này áp dụng cho cả mã ứng dụng và mã worker dịch vụ. Trên các trình duyệt không hỗ trợ, cả hai đều không được tải. Lưu ý cách trong trình chạy dịch vụ, thay vì import() động (chưa được hỗ trợ trong ngữ cảnh trình chạy dịch vụ vẫn), tôi sử dụng importScripts() cổ điển.

// In the client:
const registration = await navigator.serviceWorker.ready;
if (registration && 'periodicSync' in registration) {
  import('./periodic_background_sync.mjs');
}
// In the service worker:
if ('periodicSync' in self.registration) {
  importScripts('./image_of_the_day.mjs');
}

Trong ứng dụng Fugu Greetings, khi nhấn nút Wallpaper (Hình nền), bạn sẽ thấy hình ảnh thiệp chúc mừng trong ngày. Hình ảnh này được cập nhật hằng ngày thông qua API Đồng bộ hoá định kỳ ở chế độ nền.

Ứng dụng Fugu Greetings có hình ảnh thiệp chúc mừng mới trong ngày.
Khi nhấn vào nút Hình nền, hình ảnh của ngày hôm đó sẽ xuất hiện.

Notification Triggers API

Đôi khi, ngay cả khi có nhiều cảm hứng, bạn vẫn cần một cú hích để hoàn thành một thiệp chúc mừng đã bắt đầu. Đây là tính năng được bật bằng Notification Triggers API (API Trình kích hoạt thông báo). Là người dùng, tôi có thể nhập thời điểm tôi muốn được nhắc hoàn tất thiệp chúc mừng. Khi đến thời điểm đó, tôi sẽ nhận được thông báo rằng thiệp chúc mừng của tôi đang chờ xử lý.

Sau khi nhắc thời gian mục tiêu, ứng dụng sẽ lên lịch thông báo bằng showTrigger. Đây có thể là TimestampTrigger có ngày mục tiêu đã chọn trước đó. Thông báo nhắc sẽ được kích hoạt cục bộ, không cần mạng hoặc phía máy chủ.

const targetDate = promptTargetDate();
if (targetDate) {
  const registration = await navigator.serviceWorker.ready;
  registration.showNotification('Reminder', {
    tag: 'reminder',
    body: "It's time to finish your greeting card!",
    showTrigger: new TimestampTrigger(targetDate),
  });
}

Giống như mọi thứ khác mà tôi đã trình bày cho đến nay, đây là một tính năng nâng cao dần, vì vậy mã chỉ được tải có điều kiện.

if ('Notification' in window && 'showTrigger' in Notification.prototype) {
  import('./notification_triggers.mjs');
}

Khi tôi đánh dấu vào hộp đánh dấu Lời nhắc trong ứng dụng Fugu Greetings, một lời nhắc sẽ hỏi tôi thời điểm tôi muốn được nhắc hoàn tất thiệp chúc mừng.

Ứng dụng Fugu Greetings có lời nhắc hỏi người dùng thời điểm họ muốn được nhắc hoàn tất thiệp chúc mừng.
Lên lịch thông báo cục bộ để nhắc hoàn tất thiệp chúc mừng.

Khi một thông báo được lên lịch kích hoạt trong ứng dụng Fugu Greetings, thông báo đó sẽ hiển thị giống như mọi thông báo khác, nhưng như tôi đã viết trước đó, thông báo đó không yêu cầu kết nối mạng.

Trung tâm thông báo của macOS hiển thị một thông báo được kích hoạt từ ứng dụng Fugu Greetings.
Thông báo được kích hoạt sẽ xuất hiện trong Trung tâm thông báo của macOS.

API Khoá chế độ thức

Tôi cũng muốn thêm API Khoá chế độ thức. Đôi khi, bạn chỉ cần nhìn chằm chằm vào màn hình cho đến khi nguồn cảm hứng đến với bạn. Điều tồi tệ nhất có thể xảy ra sau đó là màn hình tắt. API khoá chế độ thức có thể ngăn điều này xảy ra.

Bước đầu tiên là lấy khoá chế độ thức bằng navigator.wakelock.request method(). Tôi truyền chuỗi 'screen' vào để lấy khoá chế độ thức của màn hình. Sau đó, tôi thêm trình nghe sự kiện để được thông báo khi khoá chế độ thức được nhả. Điều này có thể xảy ra, chẳng hạn như khi chế độ hiển thị thẻ thay đổi. Nếu điều này xảy ra, tôi có thể lấy lại khoá chế độ thức khi thẻ hiển thị trở lại.

let wakeLock = null;
const requestWakeLock = async () => {
  wakeLock = await navigator.wakeLock.request('screen');
  wakeLock.addEventListener('release', () => {
    console.log('Wake Lock was released');
  });
  console.log('Wake Lock is active');
};

const handleVisibilityChange = () => {
  if (wakeLock !== null && document.visibilityState === 'visible') {
    requestWakeLock();
  }
};

document.addEventListener('visibilitychange', handleVisibilityChange);
document.addEventListener('fullscreenchange', handleVisibilityChange);

Có, đây là một tính năng nâng cao dần dần, vì vậy, tôi chỉ cần tải tính năng này khi trình duyệt hỗ trợ API.

if ('wakeLock' in navigator && 'request' in navigator.wakeLock) {
  import('./wake_lock.mjs');
}

Trong ứng dụng Fugu Greetings, có một hộp đánh dấu Insomnia (Mất ngủ). Khi bạn chọn hộp này, màn hình sẽ luôn bật.

Hộp đánh dấu chứng mất ngủ (nếu được đánh dấu) sẽ giữ cho màn hình luôn bật.
Hộp đánh dấu Insomnia (Mất ngủ) giúp ứng dụng luôn thức.

API Phát hiện trạng thái rảnh

Đôi khi, ngay cả khi bạn nhìn chằm chằm vào màn hình hàng giờ, việc này cũng chẳng có ích gì và bạn không thể nghĩ ra ý tưởng nào cho thiệp chúc mừng. Idle Detection API (API phát hiện trạng thái rảnh) cho phép ứng dụng phát hiện thời gian người dùng rảnh. Nếu người dùng không hoạt động quá lâu, ứng dụng sẽ đặt lại về trạng thái ban đầu và xoá canvas. API này hiện được kiểm soát bằng quyền thông báo, vì nhiều trường hợp sử dụng chính thức của tính năng phát hiện trạng thái rảnh liên quan đến thông báo, ví dụ: chỉ gửi thông báo đến một thiết bị mà người dùng hiện đang chủ động sử dụng.

Sau khi đảm bảo rằng quyền thông báo đã được cấp, tôi sẽ tạo bản sao cho trình phát hiện trạng thái rảnh. Tôi đăng ký một trình nghe sự kiện để theo dõi các thay đổi về trạng thái rảnh, bao gồm cả người dùng và trạng thái màn hình. Người dùng có thể đang hoạt động hoặc đang rảnh, đồng thời màn hình có thể đang mở khoá hoặc đang khoá. Nếu người dùng không hoạt động, canvas sẽ bị xoá. Tôi đặt ngưỡng cho trình phát hiện trạng thái rảnh là 60 giây.

const idleDetector = new IdleDetector();
idleDetector.addEventListener('change', () => {
  const userState = idleDetector.userState;
  const screenState = idleDetector.screenState;
  console.log(`Idle change: ${userState}, ${screenState}.`);
  if (userState === 'idle') {
    clearCanvas();
  }
});

await idleDetector.start({
  threshold: 60000,
  signal,
});

Và như mọi khi, tôi chỉ tải mã này khi trình duyệt hỗ trợ.

if ('IdleDetector' in window) {
  import('./idle_detection.mjs');
}

Trong ứng dụng Fugu Greetings, canvas sẽ xoá khi hộp đánh dấu Ephemeral (Tạm thời) được đánh dấu và người dùng ở trạng thái rảnh quá lâu.

Ứng dụng Fugu Greetings có canvas đã xoá sau khi người dùng ở trạng thái rảnh quá lâu.
Khi bạn đánh dấu vào hộp đánh dấu Tạm thời và người dùng đã ở trạng thái rảnh quá lâu, canvas sẽ bị xoá.

Closing (Đang đóng)

Phù, thật là một chuyến đi. Có rất nhiều API chỉ trong một ứng dụng mẫu. Và hãy nhớ rằng tôi không bao giờ bắt người dùng trả phí tải xuống cho một tính năng mà trình duyệt của họ không hỗ trợ. Bằng cách sử dụng tính năng cải tiến tăng dần, tôi đảm bảo chỉ tải mã có liên quan. Và vì với HTTP/2, các yêu cầu có chi phí thấp, nên mẫu này sẽ hoạt động tốt cho nhiều ứng dụng, mặc dù bạn có thể cân nhắc sử dụng trình đóng gói cho các ứng dụng thực sự lớn.

Bảng điều khiển Mạng trong Công cụ của Chrome cho nhà phát triển chỉ hiển thị các yêu cầu về tệp có mã mà trình duyệt hiện tại hỗ trợ.
Thẻ Mạng trong Công cụ dành cho nhà phát triển của Chrome chỉ hiển thị các yêu cầu về tệp có mã mà trình duyệt hiện tại hỗ trợ.

Ứng dụng có thể trông hơi khác trên mỗi trình duyệt vì không phải nền tảng nào cũng hỗ trợ tất cả tính năng, nhưng chức năng cốt lõi luôn có sẵn và được nâng cao dần theo khả năng của trình duyệt cụ thể. Xin lưu ý rằng các chức năng này có thể thay đổi ngay cả trong cùng một trình duyệt, tuỳ thuộc vào việc ứng dụng đang chạy dưới dạng ứng dụng đã cài đặt hay trong thẻ trình duyệt.

Fugu Greetings chạy trên Android Chrome, hiển thị nhiều tính năng có sẵn.
Fugu Greetings chạy trên Android Chrome.
Fugu Greetings chạy trên Safari dành cho máy tính, hiển thị ít tính năng hơn.
Fugu Greetings chạy trên Safari dành cho máy tính.
Fugu Greetings chạy trên Chrome dành cho máy tính, hiển thị nhiều tính năng có sẵn.
Fugu Greetings chạy trên Chrome dành cho máy tính.

Nếu bạn quan tâm đến ứng dụng Fugu Greetings, hãy tìm và tạo nhánh ứng dụng đó trên GitHub.

Kho lưu trữ Fugu Greetings trên GitHub.
Ứng dụng Fugu Greetings trên GitHub.

Nhóm Chromium đang nỗ lực cải thiện các API Fugu nâng cao. Bằng cách áp dụng tính năng cải tiến dần trong quá trình phát triển ứng dụng, tôi đảm bảo rằng mọi người đều có trải nghiệm cơ sở tốt và vững chắc, nhưng những người sử dụng trình duyệt hỗ trợ nhiều API nền tảng Web hơn sẽ có trải nghiệm tốt hơn nữa. Tôi rất mong được xem những gì bạn làm với tính năng cải tiến dần trong ứng dụng của mình.

Lời cảm ơn

Tôi rất cảm ơn Christian LiebelHemanth HM, cả hai đều đã đóng góp cho Fugu Greetings. Bài viết này đã được Joe MedleyKayce Basques xem xét. Jake Archibald đã giúp tôi tìm ra tình huống với import() động trong ngữ cảnh của worker dịch vụ.