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à tăng cường dần như phiên bản 2003

Tháng 3 năm 2003, Nick FinckSteve Cheeon đã làm cho thế giới thiết kế web sửng sốt khi đề cập đến khái niệm nâng cao tiến bộ, một chiến lược thiết kế web tập trung vào việc tải nội dung trang web cốt lõi trước, sau đó dần dần thêm các lớp trình bày và tính năng phức tạp hơn vào nội dung. Vào năm 2003, cải tiến dần dần xoay quanh việc sử dụng các tính năng CSS hiện đại, JavaScript không phô trương và thậm chí chỉ là Đồ hoạ vectơ có thể mở rộng. Trong năm 2020 và trong tương lai, chúng tôi sẽ nâng cao dần bằng cách sử dụng các tính năng hiện đại của trình duyệt.

Thiết kế web toàn diện cho tương lai với tính năng nâng cao tăng dần. Trang trình bày tiêu đề trong bản trình bày ban đầu của Finck và sâm ban đầu của Đừng bỏ lỡ.
Trang trình bày: Thiết kế web hoà nhập cho tương lai với các cải tiến tăng dần. (Nguồn)

JavaScript hiện đại

Nói về JavaScript, khả năng hỗ trợ của trình duyệt cho các tính năng JavaScript chính mới nhất của ES 2015 là rất tốt. 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, chỉ định giải cấu trúc, nghỉ và trải rộng, Map/Set, WeakMap/WeakSet và nhiều nội dung 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)

Các hàm không đồng bộ, một tính năng ES 2017 và là một trong những tính năng cá nhân tôi yêu thích, có thể dùng trong tất cả các trình duyệt chính. Các từ khoá asyncawait cho phép hành vi không đồng bộ, dựa trên lời hứa được viết theo kiểu rõ ràng hơn, tránh nhu cầu định cấu hình rõ ràng chuỗi lời hứa.

Bảng hỗ trợ CanIUse cho các hàm không đồng bộ thể hiện 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 các hàm không đồng bộ. (Nguồn)

Ngoài ra, thậm chí các ngôn ngữ bổ sung mới nhất cho ngôn ngữ ES 2020 như chuỗi tuỳ chọnhợp nhất 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, bãi cỏ không thể xanh hơn nhiều như hiện nay.

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 lục mang tính biểu tượng của Windows XP.
Những tính năng cốt lõi của JavaScript có màu xanh lục. (Ả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 mẹo hay của Project Fugu 🐡, một nỗ lực mang lại cho web tất cả sức mạnh của các ứng dụng dành cho Android/iOS/máy tính để bàn. Bạn có thể đọc thêm về dự án trên trang đích của dự án.

Fugu Greetings là một ứng dụng vẽ cho phép bạn tạo thiệp chúc mừng ảo và gửi chúng cho những người thân yêu. Tệp này minh hoạ các khái niệm chính của PWA. Tính năng này đáng tin cậy và được bật hoàn toàn ngoại tuyến, vì vậy, ngay cả khi không có mạng, bạn vẫn có thể sử dụng ứng dụng này. Ứng dụng này cũng Có thể cài đặt 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.

Fugu Greetings PWA có hình vẽ giống 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

Sau đây, đã đến lúc thảo luận về tính năng nâng cao dần dần. Bảng chú giải thuật ngữ về Tài liệu web của MDN xác định khái niệm như sau:

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

Phát hiện tính năng thường 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, trong khi polyfill thường được dùng để thêm các tính năng còn thiếu bằng JavaScript.

[…]

Nâng cao tiến bộ là một kỹ thuật hữu ích cho phép các 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ể trong khi vẫn khiến những trang web đó hoạt động trên nhiều tác nhân người dùng không xác định. Tình trạng xuống cấp nhẹ có liên quan, nhưng không giống nhau và thường được coi là đi theo hướng ngược lại với tính năng nâng cao 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.

Người đóng góp cho MMDN

Việc tạo từng thiệp chúc mừng từ đầu có thể thực sự 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 nên 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 của phần tử đó thành 'file' rồi thêm loại MIME vào thuộc tính accept, sau đó lập trình "nhấp" vào phần tử đó và lắng nghe các thay đổi. Khi bạn chọn, 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 import, bạn nên dùng tính năng import để người dùng có thể lưu thiệp chúc mừng trên máy. Cách truyền thống để lưu tệp là tạo một đường liên kết neo với thuộc tính download và với URL của blob là 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, và để tránh rò rỉ bộ nhớ, hy vọng là đừng quên thu hồi URL của đối tượng blob.

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ờ một chút. Về mặt trí tuệ, bạn chưa "tải xuống" thiệp chúc mừng thì nghĩa là bạn đã "lưu" thiệp. Thay vì cho bạn thấy hộp thoại "lưu" cho phép bạn chọn nơi đặt tệp, trình duyệt đã tải trực tiếp thiệp chúc mừng xuống mà không cần người dùng tương tác và chuyển thẳng thiệp đó vào thư mục Downloads (Tệp đã tải xuống). Không ổn lắm.

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

Vậy làm cách nào để phát hiện API? API Truy cập hệ thống tệp cho thấy 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 có phương pháp này 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 tìm hiểu chi tiết về API Truy cập hệ thống tệp, vui lòng cho phép tôi làm nổi bật nhanh mẫu tính năng nâng cao tăng dần tại đây. Trên các trình duyệt hiện không hỗ trợ API Truy cập hệ thống tệp, tôi tải các tập lệnh cũ. Bạn có thể thấy các thẻ mạng của Firefox và Safari bên dưới.

Trình kiểm tra web trong Safari hiển thị các tệp cũ đang được tải.
Thẻ mạng trong trình kiểm tra web Safari.
Công cụ dành cho nhà phát triển Firefox hiển thị các tệp cũ đang được tải.
Thẻ mạng Công cụ cho nhà phát triển Firefox.

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

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

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

Giờ đây, sau khi 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à chuyển thuộc tính đó vào thuộc tính accepts, trong đó tôi nói tôi muốn các tệp hình ảnh. Cả hai đuôi tệp và loại MIME đều được hỗ trợ. Kết quả là một trình xử lý 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 gần giống nhau, nhưng lần này, tôi cần truyền một tham số loại của 'save-file' sang phương thức chooseFileSystemEntries(). Từ đó, tôi nhận được một hộp thoại lưu tệp. Khi tệp mở, bạn không cần làm điều này vì 'open-file' là chế độ mặc định. Tôi đặt tham 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 sẽ quay lại trình xử lý 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 viết 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ể luôn 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 sao tôi luôn gói các lệnh gọi trong một 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);
  }
};

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

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

Web Share Target API (API Mục tiêu chia sẻ web) và Web Share Target API

Ngoài việc lưu trữ để lưu giữ vĩnh viễn, có lẽ tôi thực sự muốn chia sẻ thiệp chúc mừng của mình. Đây là điều mà API Chia sẻ webAPI Mục tiêu chia sẻ web cho phép. Các hệ điều hành dành cho thiết bị di động và gần đây là máy tính đã có cơ chế chia sẻ tích hợp sẵn. Ví dụ: dưới đây là trang chia sẻ của Safari dành cho máy tính trên 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 Share Article (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, chẳng hạn như qua ứng dụng Tin nhắn của macOS.

Trang chia sẻ của Safari dành cho máy tính trên macOS được kích hoạt qua nút Chia sẻ của một bài viết
API Chia sẻ web 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 một hình ảnh thì sao? Cấp 1 của API Chia sẻ web chưa hỗ trợ tính năng này. Tin vui là tính năng Chia sẻ web cấp 2 đã bổ sung khả 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 chỉ cho bạn cách làm cho ứng dụng này hoạt động 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 có một mảng files bao gồm một blob, sau đó là titletext. Tiếp theo, như một phương pháp hay nhất, tôi sử dụng phương thức navigator.canShare() mới đúng như tên gọi của nó: Phương thức này cho tôi biết liệu trình duyệt có thể chia sẻ đối tượng data mà tôi đang cố chia sẻ về mặt kỹ thuật 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 nâng cao tăng dần. Nếu cả 'share''canShare' tồn tại trên đối tượng navigator, thì chỉ khi đó tôi sẽ tiếp tục và tải share.mjs thông qua import() động. Trên các trình duyệt như Safari trên thiết bị di động chỉ đáp ứng một trong hai điều kiện này, tôi sẽ không tải chức năng.

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

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

Trang tính chia sẻ ở cấp hệ điều hành cho thấy nhiều ứng dụng mà bạn có thể 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ảo của Gmail.

API Bộ chọn danh bạ

Tiếp theo, tôi muốn nói về danh bạ, nghĩa là sổ địa chỉ trên thiết bị hoặc ứng dụng trình quản lý danh bạ. Khi viết thiệp chúc mừng, không phải lúc nào bạn cũng dễ dàng viết chính xác tên của ai đó. Ví dụ: Tôi có một người bạn Sergey thích tên của mình được viết bằng chữ cái Kirin. Tôi đang dùng bàn phím QWERTZ bằng tiếng Đức và không biết cách nhập tên. Đây là vấn đề mà API Bộ chọn liên hệ có thể giải quyết. Do bạn tôi đã lưu trữ bạn bè trong ứng dụng danh bạ trên điện thoại của mình nên thông qua API Bộ chọn danh bạ, tôi có thể nhấn 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 tôi muốn truy cập. Trong trường hợp này, tôi chỉ cầ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 sẽ đị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 các địa chỉ 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);
  }
};

Đến nay, có lẽ bạn đã học đượ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 Lời chào Fugu, khi tôi nhấn vào nút Danh bạ và chọn hai người bạn thân nhất của mình, tiếng Anh Sau đó, tên của họ sẽ được vẽ trên thiệp chúc mừng của tôi.

Bộ chọn danh bạ 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 tên này sẽ được vẽ vào thiệp chúc mừng.

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

Tiếp theo là sao chép và dán. Một trong những thao tác mà chúng tôi yêu thích với vai trò là nhà phát triển phần mềm là sao chép và dán. Là tác giả thiệp chúc mừng, đôi khi, tôi có thể cũng muốn làm như vậy. Tôi muốn dán hình ảnh vào thiệp chúc mừng 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 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. Tôi sẽ 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 nào đó vào bảng nhớ tạm của hệ thống, tôi cần ghi vào đó. Phương thức navigator.clipboard.write() lấy một mảng các mục trong bảng nhớ tạm làm thông số. Về cơ bản, mỗi mục trong bảng nhớ tạm là một đối tượng với một blob là giá trị và loại của blob là 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 trong 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 trong bảng nhớ tạm có thể nằm trên bảng nhớ tạm theo các cách biểu diễn khác nhau. Mỗi mục trong bảng nhớ tạm có một trường types cho tôi biết loại MIME của các tài nguyên hiện có. Tôi gọi phương thức getType() của mục trong 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);
  }
};

Đến nay, bạn gần như không cần phải nói nữa. 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 quy trình này hoạt động như thế nào trong thực tế? Tôi có một hình ảnh đang mở trong ứng dụng macOS Preview và sao chép hình ảnh đó vào bảng nhớ tạm. Khi tôi nhấp vào Paste (Dán), ứng dụng Fugu Greetings sau đó 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 cho thấy lời nhắc cấp quyền vào bảng nhớ tạm.
Lời nhắc cấp quyền 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òn một cách khác nữa. Để tôi sao chép 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), sau đó 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 không có tiêu đề.

Ứng dụng macOS Preview (Bản xem trước macOS) có một hình ảnh vừa có tiêu đề và chưa được dán.
Một hình ảnh được dán vào ứng dụng macOS Preview.

API Huy hiệu

Một API hữu ích khác là API Huy hiệu. Tất nhiên là 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 để chứng minh API là (ab) sử dụng API trong Lời chào Fugu dưới dạng 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 bất cứ khi nào sự kiện pointerdown xảy ra và 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 theo mức độ tăng dần, vì vậy, logic tải như thường lệ.

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ẽ trên thiệp chúc mừng, mỗi chữ chỉ bằng 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 số nét chữ của 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 ngày mới thật mới mẻ? Một tính năng thú vị của ứng dụng Fugu Greetings là ứng dụng này 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 thiệp chúc mừng. Ứng dụng 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ý trình chạy dịch vụ. Phương thức này sẽ 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à 1 ngày để người dùng có thể nhận được hình nền mới 24 giờ một lần.

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

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, đây thực sự là một tính năng nâng cao tăng 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 khách và mã trình chạy dịch vụ. Trên các trình duyệt không hỗ trợ, cả hai trình duyệt này đều không được tải. Hãy 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ụ), tôi sử dụng importScripts() cũ.

// 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 Lời chào Fugu, việc nhấn nút Hình nền sẽ hiển thị hình ảnh thiệp chúc mừng của ngày hôm đó đượ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 với hình ảnh thiệp chúc mừng mới của ngày.
Nhấn nút Hình nền sẽ hiển thị hình ảnh của ngày trong ngày.

API Kích hoạt thông báo

Đôi khi, ngay cả khi có rất nhiều cảm hứng, bạn cần một lời nhắc để hoàn thành thiệp chúc mừng đã bắt đầu. Đây là tính năng do API Kích hoạt thông báo bật. Là người dùng, tôi có thể nhập thời gian mà tôi muốn được nhắc hoàn thành 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ờ.

Sau khi nhắc về thời gian mục tiêu, ứng dụng sẽ lên lịch gửi thông báo bằng một showTrigger. Đây có thể là TimestampTrigger với ngày đích đã chọn trước đó. Thông báo nhắc nhở sẽ được kích hoạt cục bộ mà không cần phía mạng hoặc 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 nội dung khác mà tôi đã trình bày cho đến nay, đây là một tính năng nâng cao tăng dần, vì vậy, mã chỉ được tải theo đ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 Lời nhắc trong Lời chào Fugu, một lời nhắc sẽ hỏi tôi khi nào tôi muốn được nhắc hoàn thành 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 thành thiệp chúc mừng.
Lên lịch gửi thông báo cục bộ để nhắc hoàn thành thiệp chúc mừng.

Khi một thông báo đã lên lịch kích hoạt trong Lời chào Fugu, thông báo đó sẽ xuất hiện giống như mọi thông báo khác, nhưng như tôi đã viết trước đây, thông báo này 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ừ 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ú vào màn hình đủ lâu cho đến khi nguồn cảm hứng đột ngột xuất hiện. Điều tồi tệ nhất có thể xảy ra khi đó là màn hình tắt. API Khóa 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 cho nó chuỗi 'screen' để lấy khoá chế độ thức màn hình. Sau đó, tôi thêm trình nghe sự kiện để nhận thông báo khi khoá chế độ thức được nhả ra. Đ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ể mở 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 tăng dần, vì vậy, tôi chỉ cần tải bản nâng cao này khi trình duyệt hỗ trợ API.

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

Trong Fugu Greetings có một hộp đánh dấu Insomnia (Chứng mất ngủ) giúp giữ cho màn hình luôn bật.

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

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

Đôi khi, ngay cả khi bạn chăm chú vào màn hình hàng giờ, điều đó chỉ vô ích và bạn không thể nghĩ ra sơ lược việc cần làm với thiệp chúc mừng. API Phát hiện trạng thái rảnh cho phép ứng dụng phát hiện thời gian không hoạt động của người dùng. 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 sau quyền gửi thông báo, vì nhiều trường hợp sử dụng tính năng phát hiện trạng thái rảnh trong phiên bản chính thức liên quan đến thông báo, chẳng hạn như để chỉ gửi thông báo đến một thiết bị mà người dùng hiện đang sử dụng.

Sau khi chắc chắn rằng quyền gửi thông báo đã được cấp, tôi sẽ tạo thực thể 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 ở 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 ở trạng thái rảnh, và màn hình có thể được mở khoá hoặc khoá. Nếu người dùng ở trạng thái rảnh, canvas sẽ bị xoá. Tôi đặt ngưỡng là 60 giây cho trình phát hiện trạng thái rảnh.

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ợ mã.

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

Trong ứng dụng Fugu Greetings, canvas sẽ xoá khi hộp đánh dấu Ephemeral được đánh dấu và người dùng không hoạt động quá lâu.

Ứng dụng Fugu Greetings có canvas được xoá sau khi người dùng không hoạt động quá lâu.
Khi bạn đánh dấu vào hộp Tạm thời và người dùng không hoạt động quá lâu, thì canvas sẽ bị xoá.

Closing (Đang đóng)

Chà, 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 phải trả chi 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 nâng cao tăng dần, tôi đảm bảo chỉ có mã có liên quan được tải. Và vì với HTTP/2, các yêu cầu khá rẻ 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ể muốn xem xét một trình đóng gói cho các ứng dụng thực sự lớn.

Bảng điều khiển Mạng của Công cụ của Chrome cho nhà phát triển chỉ hiển thị các yêu cầu đối với tệp có mã mà trình duyệt hiện tại hỗ trợ.
Thẻ Mạng của Công cụ của Chrome cho nhà phát triển chỉ hiển thị các yêu cầu đối với các 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 tất cả nền tảng đều hỗ trợ tất cả các tính năng, nhưng chức năng cốt lõi thì luôn có sẵn — được cải tiến 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 một và 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.

Lời chào Fugu chạy trên Chrome dành cho Android, hiển thị nhiều tính năng có sẵn.
Fugu Greetings chạy trên Android Chrome.
Lời chào Fugu chạy trên Safari dành cho máy tính, cho thấy ít tính năng hiện có hơn.
Fugu Greetings chạy trên trình duyệt Safari dành cho máy tính.
Lời chào Fugu 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 trình duyệt 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à phát triển ứng dụng này 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 làm cho cỏ xanh hơn khi nói đến API Fugu nâng cao. Bằng cách áp dụng tính năng nâng cao tăng 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ó đượ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 nữa sẽ có trải nghiệm tốt hơn nữa. Tôi rất mong được thấy những việc bạn làm được với tính năng nâng cao dần dần trong ứng dụng của bạn.

Xác nhận

Tôi rất biết ơn Christian LiebelHemanth HM vì cả hai đã đóng góp cho lời chúc Fugu. Bài viết này đã được Joe MedleyKayce Basques đánh giá. Jake Archibald đã giúp tôi tìm hiểu tình huống với import() động trong ngữ cảnh trình chạy dịch vụ.