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 dần như năm 2003

Ngày xuất bản: 29 tháng 6 năm 2020

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

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

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 là rất tốt. Tiêu chuẩn mới này bao gồm các promise, mô-đun, lớp, chuỗi mẫu, hàm mũi tên, letconst, các tham số mặc định, trình tạo, phép gán phân tách, phần còn lại và trải rộng, Map/Set, WeakMap/WeakSet và nhiều thành phần 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)

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 tôi yêu thích, có thể được dùng trong tất cả các trình duyệt chính. Các từ khoá asyncawait cho phép viết hành vi dựa trên lời hứa không đồng bộ theo kiểu rõ ràng hơn, tránh nhu cầu đị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 không đồng bộ. (Nguồn)

Ngay cả những tính năng mới nhất của 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. Khi nói đến các tính năng cốt lõi của JavaScript, thì không có gì có thể tốt hơn nữa.

Ví dụ:

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 mang tính biểu tượng của Windows XP.
Cỏ xanh tươi khi nói đến các tính năng cốt lõi của JavaScript. (Ảnh chụp màn hình sản phẩm của Microsoft, được sử dụng khi có sự cho phép.)

Ứng dụng mẫu: Fugu Greetings

Đối với tài liệu này, tôi làm việc với một PWA 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ả sức mạnh của các ứng dụng Android, iOS và máy tính. 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 cho người thân yêu. Đây là ví dụ minh hoạ về các khái niệm cốt lõi của PWA. Ứng dụng này đáng tin cậy và hoàn toàn có thể sử dụng 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 ứng dụng này. Đây cũng là một ứng dụ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.

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

Sau khi giải quyết xong vấn đề này, giờ là lúc nói về tính năng nâng cao cải tiến tăng dần. Thuật ngữ trong Tài liệu web MDN định nghĩa khái niệm này như sau:

Nâng cao từng bước là một triết lý thiết kế cung cấp cơ sở cho 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ủa các trình duyệt hiện đại nhất có thể chạy tất cả mã cần thiết.

Tính năng phát hiện 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 các tính năng còn thiếu bằng JavaScript.

[…]

Nâng cao từng bước 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. Giảm cấp một cách uyển chuyển có liên quan nhưng không giống nhau và thường được coi là đi ngược lại với việc cải tiến từng bước. Trên thực tế, cả hai phương pháp này đề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 tạo từng tấm thiệp từ đầu có thể rất tốn thời gian. 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ẽ dùng một 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 đó lập trình để "nhấp" vào phần tử đó 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ó lẽ bạn nên có tính năng xuất để người dùng có thể lưu thiệp chúc mừng vào thiết bị của họ. Cách truyền thống để lưu tệp là tạo một đường liên kết cố định bằng thuộc tính download và có URL blob làm href. Bạn cũng sẽ lập trình để "nhấp" vào nút này nhằm kích hoạt quá trình tải xuống và hy vọng không quên thu hồi URL đối tượng blob để ngăn chặn tình trạng 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 khoan đã. Về mặt tinh thần, bạn không "tải" thiệp chúc mừng xuống mà là "lưu" thiệp đó. Thay vì cho bạn thấy hộp thoại "lưu" để bạn chọn vị trí lưu 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à đưa thẳng vào thư mục Tải xuống. Điều này không ổn.

Nếu có cách hay hơn thì sao? Điều gì sẽ xảy ra nếu bạn chỉ cần mở một tệp cục bộ, chỉnh sửa tệp đó rồi lưu các 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? Hoá ra là có. File System Access API (API Quyền truy cập vào hệ thống tệp) cho phép bạn mở và tạo tệp cũng như 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 cung cấp 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ó dùng được hay không.

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 đi sâu vào chi tiết về File System Access API, tôi sẽ nhanh chóng nêu bật mẫu cải tiến tăng dần tại đây. Trên những trình duyệt không hỗ trợ File System Access API, tôi tải các tập lệnh cũ.

Safari Web Inspector cho thấy các tệp cũ đang được tải.
Công cụ cho nhà phát triển của Firefox cho thấy các tệp cũ đang được tải.

Tuy nhiên, trên Chrome (một trình duyệt hỗ trợ API này), 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 dễ dàng nhờ import() động mà tất cả các trình duyệt hiện đại đều hỗ trợ. Như tôi đã nói trước đó, tình hình hiện tại khá tốt.

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

File System Access API

Vậy là tôi đã giải quyết xong vấn đề này, giờ là lúc xem xét việc triển khai thực tế dựa trên File System Access API. Để nhập một hình ảnh, tôi gọi window.chooseFileSystemEntries() và truyền cho nó một thuộc tính accepts, trong đó tôi cho biết tôi muốn tệp hình ảnh. Cả đuôi tệp và loại MIME đều được hỗ trợ. Thao tác này sẽ tạo ra một trình xử lý tệp. Từ trình xử lý này, 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 'save-file' đến phương thức chooseFileSystemEntries(). Từ đó, tôi nhận được một hộp thoại lưu tệp. Khi mở tệp, điều này là không cần thiết vì 'open-file' là giá trị 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. Lần này, tôi nhận lại một mã nhận dạng tệp, nhưng thay vì nhận tệp, 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 (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ể thất bại: Ổ đĩ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. Đó là lý do tại sao tôi luôn bao bọc 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);
  }
};

Khi sử dụng tính năng cải tiến tăng dần với File System Access API, tôi có thể mở một tệp như trước. Tệp đã nhập sẽ được vẽ ngay trên canvas. Tôi có thể chỉnh sửa và cuối cùng lưu các chỉnh sửa đó 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. Giờ đây, tệp đã sẵn sàng để được lưu giữ mãi mãi.

Ứ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 (Lời chào Fugu) hiện có hình ảnh được nhập.
Hình ảnh đã nhập.
Ứng dụng Fugu Greetings có hình ảnh đã sửa đổi.
Lưu hình ảnh đã chỉnh sửa vào một tệp mới.

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

attempt-right

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. Các hệ điều hành di động và gần đây là hệ điều hành máy tính đã có sẵn các cơ chế chia sẻ.

Ví dụ: bảng chia sẻ của Safari trên máy tính sẽ xuất hiện trên macOS khi người dùng nhấp vào Chia sẻ bài viết trên blog của tôi. Bạn có thể chia sẻ đường liên kết đến bài viết đó với bạn bè bằng ứng dụng Tin nhắn trên macOS.

Để thực hiện việc này, tôi gọi navigator.share() và truyền vào đó 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 Web Share API hiện chưa hỗ trợ tính năng này. Tin vui là Web Share Level 2 đã bổ sung các 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);
}

Hãy để tôi chỉ cho 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 có 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 những gì tên của nó gợi ý: Phương thức này 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 phép chia sẻ dữ liệu, tôi có thể gọi navigator.share() như trước đây. Vì mọi thứ đều có thể gặp lỗi, 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 chiến lược cải tiến tăng dần. Nếu cả 'share''canShare' đều có trên đối tượng navigator, thì tôi mới tiếp tục và tải share.mjs bằng cách sử dụng 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 không tải chức năng này.

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

Trong Fugu Greetings, nếu tôi nhấn vào nút Chia sẻ trên một trình duyệt được 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 soạn email sẽ xuất hiện cùng với hình ảnh được đí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 được đính kèm.
Tệp sẽ được đính kèm vào một email mới trong trình soạn thảo của Gmail.

Contact Picker API

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 thiệp chúc mừng, bạn có thể không phải lúc nào cũng dễ dàng viết đúng tên của một người. Ví dụ: Tôi có một người bạn tên là Sergey và anh ấy muốn tên của mình được viết bằng chữ cái Cyrillic. Tôi đang 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ì tôi đã lưu bạn bè trong ứng dụng danh bạ trên điện thoại, nên tôi có thể khai thác danh bạ của mình trên web bằng cách sử dụng Contacts Picker API.

Trước tiên, tôi cần chỉ định danh sách các tài sản 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 đại diện hoặc địa chỉ thực. 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 nhiều mục. Cuối cùng, tôi có thể gọi navigator.contacts.select(), thao tác này sẽ trả về các thuộc tính lý tưởng cho những người liên hệ mà 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à đến giờ, có lẽ bạn đã biết được mẫu này: 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 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ọ chứ 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ọ sẽ được vẽ lên tấm thiệp của tôi.

Bộ chọn người liên hệ cho thấy tên của hai người liên hệ trong sổ địa chỉ.
Chọn hai tên bằng trình 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.
Sau đó, hai 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à 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à nhà phát triển phần mềm là sao chép và dán. Là một tác giả viết 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 một tấm thiệp mà tôi đang làm hoặc sao chép tấm thiệp đó để có thể tiếp tục chỉnh sửa ở nơi khác. Async Clipboard API 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 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 nội dung đó 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 tham số. Về cơ bản, mỗi mục trên bảng nhớ tạm là một đối tượng có một blob làm giá trị và 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(). Lý do là có thể có nhiều mục trên bảng nhớ tạm ở các biểu thị khác nhau. Mỗi mục trên bảng nhớ tạm đều 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 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);
  }
};

Và giờ thì 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 được hỗ trợ.

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

Vậy quy trình này diễn ra như thế nào? Tôi có một hình ảnh đang mở 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 xem tôi có muốn cho phép ứng dụng này 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 truy cập vào bảng nhớ tạm.
Lời nhắc về quyền truy cập vào bảng nhớ tạm.

Cuối cùng, sau khi bạn chấp nhận quyền, hình ảnh sẽ được dán vào ứng dụng. Cách khác cũng được. Hãy để tôi sao chép một tấm thiệp vào bảng nhớ tạm. Sau đó, khi tôi mở ứng dụng Xem trước, nhấp vào Tệp rồi nhấp vào Mới từ bảng nhớ tạm, tấm thiệp 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.
Một hình ảnh được dán vào ứng dụng Xem trước của macOS.

Badging API

Một API hữu ích khác là Badging API. Là một PWA có thể cài đặt, tất nhiên Fugu Greetings có biểu tượng ứng dụng mà người dùng có thể đặt trên thanh công cụ của ứng dụng hoặc màn hình chính. Một cách thú vị để minh hoạ API này là sử dụng nó trong Fugu Greetings, dưới dạng một 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, 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ẽ được đặ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 vẫn 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 là 7.

Các số từ 1 đến 7 được vẽ trê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 cho thấy số 7.
Bộ đếm nét bút dưới 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 bắt đầu mỗi ngày với những nội dung mới mẻ? Một tính năng hay 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 buổ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 này sử dụng Periodic Background Sync API để đạ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ụ. Nó 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, vì vậy người dùng có thể nhận được một 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à lắng 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 đó), thì hình ảnh trong ngày sẽ được truy xuất bằng hàm getImageOfTheDay() và kết quả được truyền đến tất cả các ứng dụng để chúng 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,
          });
        });
      })()
    );
  }
});

Đây thực sự là một tính năng nâng cao từng bước, vì vậy, mã chỉ được tải khi trình duyệt hỗ trợ API này. Điều này áp dụng cho cả mã ứng dụng và mã trình chạy 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 ý rằng trong trình chạy dịch vụ, thay vì import() động (hiện chưa được hỗ trợ trong ngữ cảnh trình chạy dịch vụ ), 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 Fugu Greetings, khi bạn nhấn vào nút Wallpaper (Hình nền), hình ảnh thiệp chúc mừng trong ngày sẽ xuất hiện. Hình ảnh này được cập nhật hằng ngày bằng Periodic Background Sync API.

Khi bạn nhấn vào nút Hình nền, hình ảnh trong ngày 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 lời nhắc để hoàn thành tấm thiệp chúc mừng mà bạn đã bắt đầu. Đây là một tính năng được bật bằng Notification Triggers API. 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 tấm thiệp. Khi đó, tôi sẽ nhận được thông báo rằng thiệp của tôi đang chờ được gửi.

Sau khi nhắc về 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 với ngày mục tiêu đã chọn trước đó. Thông báo nhắc nhở 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 Lời nhắc trong 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 thành thiệp chúc mừng.
Lập lịch thông báo cục bộ để được nhắc hoàn tất một tấm thiệp chúc mừng.

Khi một thông báo đã lên lịch kích hoạt trong Fugu Greetings, 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 đó, thông báo này không yêu cầu kết nối mạng.

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.

Wake Lock API

Tôi cũng muốn thêm Wake Lock API. Đôi khi, bạn chỉ cần nhìn chằm chằm vào màn hình đủ lâu cho đến khi cảm hứng đến với bạn. Điều tồi tệ nhất có thể xảy ra là màn hình sẽ tắt. Wake Lock API có thể ngăn chặn điều này.

Bước đầu tiên là lấy khoá đánh thức bằng navigator.wakelock.request method(). Tôi truyền chuỗi 'screen' để lấy khoá chế độ thức của 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á đánh thức được giải phóng. Đ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á đánh 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 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 Fugu Greetings, có một hộp đánh dấu Insomnia (Mất ngủ). Khi được đánh dấu, hộp này sẽ giữ cho màn hình luôn bật.

Nếu bạn đánh dấu vào hộp kiểm mất ngủ, màn hình sẽ luôn sáng.
Hộp đánh dấu Insomnia (Mất ngủ) giúp ứng dụng luôn hoạt động.

Idle Detection API

Đôi khi, ngay cả khi bạn nhìn chằm chằm vào màn hình hàng giờ, bạn vẫn không thể nghĩ ra dù chỉ là ý tưởng nhỏ nhất về việc làm gì với tấm thiệp của mình. Idle Detection API 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 được bảo vệ bằng quyền gửi thông báo, vì nhiều trường hợp sử dụng thực tế của tính năng phát hiện trạng thái rảnh là liên quan đến thông báo, chẳng hạn như 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 thông báo đã được cấp, tôi sẽ khởi tạo 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 không hoạt động, bao gồm cả trạng thái người dùng và 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ể đang mở khoá hoặc khoá. Nếu người dùng không hoạt động, canvas sẽ xoá. Tôi đặt ngưỡng cho trình phát hiện trạng thái không hoạt động 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ợ 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 (Tạm thời) được đánh dấu và người dùng không hoạt động quá lâu.

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

Closing (Đang đóng)

Phù, thật là một chuyến đi đáng nhớ. 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 chiến lược cải tiến tăng dần, tôi đảm bảo chỉ mã có liên quan mới được tải. 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 hiệu quả cho nhiều ứng dụng, mặc dù bạn có thể muốn cân nhắc sử dụng một trình đóng gói cho các ứng dụng thực sự lớn.

Thẻ Network (Mạng) của Công cụ cho nhà phát triển của Chrome chỉ hiển thị các yêu cầu đối với những tệp có mã mà trình duyệt 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ả các tính năng, nhưng chức năng cốt lõi luôn có ở đó – được cải tiến dần theo khả năng của trình duyệt cụ thể. 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 một thẻ trình duyệt.

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

Bạn có thể phân nhánh Fugu 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 tăng dần khi tạo ứ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à ổn định, nhưng những người 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 thấy những gì bạn làm với tính năng nâng cao tăng dần trong các ứng dụng của mình.

Lời cảm ơn

Tôi rất biết ơn Christian LiebelHemanth HM vì cả hai đều đã đóng góp cho Fugu Greetings. Tài liệu này được Joe MedleyKayce Basques xem xét. Jake Archibald đã giúp tôi tìm hiểu tình hình với import() động trong bối cảnh trình chạy dịch vụ.