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à ngày càng nâng cao như năm 2003

Trở lại vào tháng 3 năm 2003, Nick FinckSteve chapeon đã gây sốc cho giới thiết kế web với khái niệm nâng cao tăng dần, một chiến lược thiết kế web tập trung tải trước nội dung cốt lõi của trang web, sau đó thì các khía cạnh này ngày càng phức tạp hơn và các lớp trình bày và tính năng nghiêm ngặt về mặt kỹ thuật để bổ sung vào nội dung. Trong khi vào năm 2003, cải tiến tiến bộ được nhắm đến việc sử dụng—vào thời điểm đó—các công nghệ tiên tiến các tính năng CSS, JavaScript không phô trương và thậm chí chỉ là Đồ hoạ vectơ có thể mở rộng. Cải tiến tiến bộ trong năm 2020 và xa hơn nữa là việc 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 khả năng cải tiến tiến bộ. Trang trình bày tiêu đề trong bản trình bày ban đầu của Finck và Paneon.
Trang trình bày: Thiết kế web toàn diện cho tương lai với cải tiến tiến bộ. (Nguồn)

JavaScript hiện đại

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

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

Các chức năng không đồng bộ, một tính năng ES 2017 và một trong những chức năng mà 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. 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 cách gọn gàng hơn, tránh cần phải định cấu hình chuỗi hứa hẹn một cách rõ ràng.

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

Và thậm chí là những ngôn ngữ bổ sung mới nhất trong năm 2020 như tạo chuỗi tuỳ chọnkết hợp rỗng đã liên hệ đượ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, cây cỏ không thể xanh hơn nhiều là hôm 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 đồng cỏ xanh lục nổi tiếng của Windows XP.
Đồng cỏ có màu xanh lục khi nói đến các tính năng JavaScript chính. (Ảnh chụp màn hình sản phẩm của Microsoft, dùng với quyền.)

Ứng dụng mẫu: Fugu Greetings

Trong bài viết này, tôi làm việc với một ứng dụng web tiến bộ (PWA) đơn giản có tên là Lời chào môn Fugu (GitHub). Tên của ứng dụng này là một điều đặc biệt của Project Fugu 🐡, một nỗ lực nhằm cung cấp cho mọi người trên mạng sức mạnh của các ứng dụng Android/iOS/máy tính để bàn. Bạn có thể đọc thêm về dự án trên trang đích.

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 đến những người thân yêu. Ví dụ Các khái niệm chính của PWA. Bây giờ đáng tin cậy và được bật hoàn toàn ngoại tuyến, nên ngay cả khi bạn không có mạng, bạn vẫn có thể sử dụng mạng đó. Ứ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.

Ứng dụng web tiến bộ (PWA) chào mừng Fugu có hình vẽ giống với biểu trưng cộng đồng của PWA.
Ứng dụng mẫu Fugu Greetings.

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

Ngoài ra, đã đến lúc nói về tính năng nâng cao tăng dần. Bảng chú giải thuật ngữ Tài liệu web MDN định nghĩa khái niệm như sau:

Cải tiến tăng dần là triết lý thiết kế cung cấp đường cơ sở nội dung và chức năng 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 người dùng phiên bản hiện đại nhất các trình duyệt có thể chạy tất cả mã bắt buộ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, trong khi polyfill thường được dùng để thêm tính năng còn thiếu bằng JavaScript.

[…]

Cải tiến 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 đảm bảo các trang web đó hoạt động do nhiều tác nhân người dùng không xác định. Xuống cấp nhẹ có liên quan với nhau nhưng không giống nhau và thường được hiểu là đi theo hướng ngược lại sang cải tiến tăng dần. Trên 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 MDN

Việc tạo mới từng thiệp chúc mừng từ đầu có thể rất rườm rà. Vậy tại sao không có 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 tiếp cận truyền thống, bạn đã sử dụng một <input type=file> để làm cho điều này xảy ra. 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, rồi "nhấp" theo phương thức lập trình nó và lắng nghe những 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 nên sử dụng 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 truyền thống để lưu tệp là tạo một đường liên kết neo có download và có một URL blob là href của nó. Bạn cũng phải "nhấp" bằng cách lập trình để kích hoạt quá trình tải xuống, và để tránh rò rỉ bộ nhớ, bạn đừ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 tinh thần, bạn chưa "tải xuống" một thiệp chúc mừng, "đã lưu" nó. Thay vì hiển thị cho bạn nút "lưu" hộp thoại 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 phải tương tác và chuyển thẳng tệp đó vào thư mục Tài nguyên đã tải xuống. Điều này không tốt lắm.

Nếu có cách nào tốt hơn thì sao? Điều gì 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 các sửa đổi, sang tệp mới hay quay lại tệp gốc mà bạn đã mở ban đầu? Hoá ra có. File System Access API (API Truy cập hệ thống tệp) cho phép bạn mở và tạo tệp và các 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 tính năng của một 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 theo đ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ó dùng được hay không. Tôi đã hướng dẫn bạ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'),
    ]);
  }
};

Nhưng trước khi tôi đi sâu vào chi tiết về API Truy cập hệ thống tệp, cho phép tôi làm nổi bật nhanh mẫu nâng cao tăng dần ở đâ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 sẽ 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 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 hiển thị các tệp cũ đang được tải.
Thẻ mạng Công cụ dành cho nhà phát triển Firefox.

Tuy nhiên, trên Chrome (trình duyệt hỗ trợ API này), chỉ các tập lệnh mới mới được tải. Điều này được 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 sử dụng hỗ trợ. Như tôi đã nói, ngày nay cỏ khá xanh.

Công cụ của Chrome cho nhà phát triển hiển thị 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, khi tôi đã giải quyết vấn đề này, đã đến lúc xem xét việc 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 vào đó một thuộc tính accepts, trong đó tôi nói rằng tôi muốn tệp hình ảnh. Cả đuôi tệp và loại MIME đều được hỗ trợ. Việc này sẽ tạo ra một tên người dùng tệp mà từ đó tôi có thể lấy tệp thực 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);
  }
};

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

Mọi thứ luôn 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ể chỉ đơ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);
  }
};

Sử dụng tính năng cải tiến tăng dần với API Truy cập hệ thống tệp, Tôi có thể mở tệp như trước đây. Tệp đã nhập sẽ được vẽ ngay trên canvas. tôi có thể thực hiện các chỉnh sửa của mình và cuối cùng lưu chúng bằng hộp thoại lưu thực 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 đã sẵn sàng để được 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 với hình ảnh đã sửa đổi.
Đang lưu hình ảnh đã sửa đổi vào tệp mới.

API mục tiêu chia sẻ web và chia sẻ web

Ngoài việc lưu giữ thiệp chúc mừng 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 tôi làm điều đó. Thiết bị di động và gần đây, hệ điều hành máy tính đã tích hợp sẵn tính năng chia sẻ cơ chế cụ thể. Ví dụ: dưới đây là bảng 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 về 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 bạn bè, cho chẳng hạn như thông qua ứng dụng Tin nhắn trên macOS.

Trang tính chia sẻ của Safari dành cho máy tính trên macOS được kích hoạt bằng 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 và macOS.

Mã nguồn để thực hiện việc này khá đơn giản. Tôi gọi navigator.share() và hãy truyền cho nó một title, texturl (không bắt buộc) vào 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ẻ web chưa hỗ trợ tính năng này. Tin vui là tính năng Chia sẻ web cấp 2 đã thêm chức 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);
}

Để tôi chỉ cho bạn cách sử dụng ứng dụng Thiệp chúc mừng Fugu. Trước tiên, tôi cần chuẩn bị đối tượng data có mảng files bao gồm một blob, sau đó titletext. Tiếp theo, tôi sử dụng phương thức navigator.canShare() mới để làm phương pháp hay nhất. Phương thức này ý nghĩa của tên gọi: Nó cho tôi biết liệu đối tượng data mà tôi đang cố gắng chia sẻ có thể được trình duyệt 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 đây. Vì mọi thứ đều có thể không thành công nên tôi lại sử dụng 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' đều tồn tại trên đối tượng navigator, thì chỉ khi đó 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 đó.

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

Trong Fugu Greeting, 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, trang chia sẻ tích hợp sẽ mở ra. Ví dụ: tôi có thể chọn Gmail và tiện ích soạn email bật lên cùng với hình ảnh được đính kèm.

Trang tính 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ó đính kèm hình ảnh.
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 địa chỉ liên hệ

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

Trước tiên, tôi cần xác định danh sách các cơ sở lưu trú 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, hình đại diện hoặc địa chỉ thực tế. Tiếp theo, tôi định cấu hình một đối tượng options và đặt multiple thành true để có thể chọn thêm 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 đây, có lẽ bạn đã tìm hiểu về 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 Fugu Greeting, khi tôi nhấn vào nút Contacts (Danh bạ) và chọn hai người bạn thân thiết nhất của mình, bạn có thể thấy cách bộ chọn địa chỉ liên hệ chỉ được hiển thị tên của họ, nhưng không phải địa chỉ email hay các thông tin khác như số điện thoại của họ. 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ạ cho thấy tên của hai người liên hệ trong sổ địa chỉ.
Chọn 2 tên bằng bộ chọn người liên hệ trong sổ địa chỉ.
Tên của 2 người liên hệ đã chọn trước đó được vẽ trên thiệp chúc mừng.
Sau đó, 2 tên này được vẽ trên 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. Sao chép và dán là một trong những hoạt động yêu thích của chúng tôi với vai trò là nhà phát triển phần mềm. Là một tác giả thiệp chúc mừng, đôi khi tôi có thể 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ủa tôi để tôi có thể tiếp tục chỉnh sửa thiệp ở 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 hướng dẫn bạn cách thêm tính năng hỗ trợ sao chép và dán vào Fugu Ứng dụng lời chào.

Để 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 trong bảng nhớ tạm làm một . Về cơ bản, mỗi mục trong bảng nhớ tạm là một đối tượng có giá trị blob và thuộc loại 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 trong bảng nhớ tạm mà tôi nhận được bằng cách gọi navigator.clipboard.read(). Nguyên nhân là do nhiều mục trong bảng nhớ tạm có thể nằm trong bảng nhớ tạm ở các cách biểu thị khác nhau. Mỗi mục trong bảng nhớ tạm có một trường types cho tôi biết các loại MIME của mục có sẵn của chúng tôi. Tôi gọi phương thức getType() của mục bảng nhớ tạm, truyền phương thức Loại MIME tôi đã lấy được trước đây.

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, gần như không cần nói gì thêm. 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 tính năng 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 Bản xem trước của macOS và sao chép vào bảng nhớ tạm. Khi tôi nhấp vào Dán, ứng dụng Fugu Greetings sau đó hỏi tôi liệu 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 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 sự cho phép, hình ảnh sẽ được dán vào ứng dụng. Mặt khác, vòng tròn cũng mang lại hiệu quả. Để 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 Folder (Mới từ bảng nhớ tạm), thiệp chúc mừng được dán vào một hình ảnh mới không có tiêu đề.

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

API Huy hiệu

Một API hữu ích khác là API Huy hiệu. Tất nhiên, là một ứng dụng web tiến bộ (PWA) có thể cài đặt, Fugu Greetings tất nhiên có 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à (ab) sử dụng API đó trong Fugu Greetings như một công cụ đếm nét bút. Tôi thêm một trình nghe sự kiện để tăng bộ đếm nét vẽ mỗi khi sự kiện pointerdown xảy ra rồi đặ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 tăng 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 bằ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 số chỉ bằng một nét vẽ.
Vẽ các số từ 1 đến 7, sử dụng bảy 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ẽ ở dạng huy hiệu biểu tượng ứng dụng.

API Định kỳ đồng bộ hoá trong nền

Bạn muốn khởi đầu ngày mới 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à ứng dụng này có thể truyền cảm hứng cho bạn mỗi sáng với hình nền mới để bắt đầu thiệp chúc mừng. Ứng dụng sử dụng API Định kỳ đồng bộ hoá trong nền để đạt được mục tiêu này.

Bước đầu tiên là đăng ký 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 một 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 hình nền mới sau 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 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 được truy xuất thông qua hàm getImageOfTheDay(), và kết quả được lan truyền đến tất cả khách hàng để họ 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,
          });
        });
      })()
    );
  }
});

Một lần nữa, đây thực sự là một cải tiến tăng dần, vì vậy mã chỉ được tải khi API được trình duyệt hỗ trợ. Đ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ợ, không có trình duyệt nào được tải. Hãy lưu ý cách thực hiện trong trình chạy dịch vụ, thay vì import() động (không được hỗ trợ trong ngữ cảnh trình chạy dịch vụ chưa), Tôi sử dụng phiên bản cổ điển importScripts().

// 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 Fugu Greetings, 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 đó được cập nhật hằng ngày thông qua Periodic nền Sync API (API Đồng bộ hoá trong 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 trong ngày.

API Notification Triggers

Đô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 lời chào đã bắt đầu . Đây là tính năng được bật bởi API kích hoạt thông báo. Là người dùng, tôi có thể nhập thời điểm 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 lên lịch thông báo bằng showTrigger. Đây có thể là một TimestampTrigger có ngày mục tiêu đã chọn trước đó. Thông báo nhắc nhở sẽ được kích hoạt trên thiết 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 nội dung khác mà tôi đã trình bày từ trước đến nay, đây là một cải tiến tiến bộ, 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 Prompt (Lời nhắc) trong Fugu Greetings, một lời nhắc sẽ hỏi tôi khi tôi muốn được nhắc hoàn thành thiệp chúc mừng.

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

Khi một thông báo theo lịch kích hoạt trong Fugu Greeting, thông báo đó được hiển thị giống như mọi thông báo khác, nhưng như tôi đã viết trước đây, thiết bị không cần 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 đủ lâu vào màn hình cho đến khi cảm hứng hôn bạn. Điều tồi tệ nhất có thể xảy ra khi đó 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à tạo khoá chế độ thức bằng navigator.wakelock.request method(). Tôi truyền vào chuỗi 'screen' để có được khoá chế độ thức màn hình. Sau đó, tôi thêm một trình nghe sự kiện để nhận thông báo khi khoá chế độ thức được huỷ bỏ. Điều này có thể xảy ra, chẳng hạn như khi chế độ hiển thị của 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ẻ xuất hiện 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);

Đúng, đây là một cải tiến tiến bộ, vì vậy, tôi chỉ cần tải nó 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 In mất ngủ mà khi được chọn, sẽ giữ cho bật màn hình.

Hộp đánh dấu trạng thái mất ngủ (nếu được đánh dấu) sẽ giúp màn hình luôn bật.
Hộp đánh dấu Mất ngủ 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 nhìn màn hình hàng giờ, điều đó thật vô dụng và bạn không thể đưa ra ý tưởng dù nhỏ nhất về 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 người dùng không hoạt độ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 theo quyền gửi thông báo, vì rất nhiều trường hợp sử dụng phiên bản chính thức của việc phát hiện trạng thái rảnh đều liên quan đến thông báo, ví dụ: để chỉ gửi thông báo đến thiết bị mà người dùng đang sử dụng.

Sau khi đảm bảo 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 có chức năng 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 không hoạt động, và màn hình có thể mở khoá hoặc khoá được. Nếu người dùng không hoạt động, canvas sẽ bị xoá. Tôi đặt mộ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ư thường lệ, 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ẽ bị xoá khi hộp đánh dấu Tạm thời được được chọn và người dùng không hoạt động quá lâu.

Ứng dụng Fugu Greetings có canvas bị 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, canvas sẽ bị xoá.

Closing (Đang đóng)

Chà, một chuyến đi thật thú vị. 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ả 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ỉ tải mã có liên quan. Và vì với HTTP/2, yêu cầu có chi phí thấp, nên mẫu này sẽ hoạt động tốt đối với nhiều ứng dụng, mặc dù bạn có thể muốn xem xét trình tạo gói cho các ứng dụng thực sự lớn.

Bảng điều khiển Mạng Chrome Công cụ 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ô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ợ.

Giao diện ứng dụng có thể hơi khác nhau trên mỗi trình duyệt vì không phải tất cả cá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 vẫn luôn có — đượ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 khả 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 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 hiện có.
Fugu Greetings chạy trên Android Chrome.
Lời chào Fugu chạy trên trình duyệt Safari dành cho máy tính và hiển thị ít tính năng hơn.
Chào mừng Fuugu chạy trên trình duyệt Safari dành cho máy tính.
Fugu Greetings chạy trên Chrome dành cho máy tính để bàn, hiển thị nhiều tính năng hiện có.
Lời chúc mừng Fugu 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à phát triển nhánh 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 sử dụng 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ơ bản tốt và vững chắc, mà 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ó đượ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 sẽ làm với tính năng cải tiến tiến bộ trong ứng dụng của mình.

Xác nhận

Tôi biết ơn Christian LiebelHemanth hm cả hai đều đã đóng góp cho Fugu Greetings. Bài viết này do Joe Medley xem xét và Kayce Basques. Jake Archibald đã giúp tôi tìm hiểu tình hình với import() động trong ngữ cảnh trình chạy dịch vụ.