Ứng dụng web lũy tiến là một cách để mang lại cảm giác ứng dụng gốc đó cho ứng dụng web truyền thống. Với PWA, chúng tôi có thể nâng cao trang web của mình bằng các tính năng ứng dụng dành cho thiết bị di động giúp tăng khả năng sử dụng và mang lại trải nghiệm tuyệt vời cho người dùng.
Trong bài viết này, chúng ta sẽ xây dựng PWA từ đầu bằng HTML, CSS và JavaScript. Dưới đây là các chủ đề chúng tôi sẽ đề cập:
Vì vậy, hãy bắt đầu với một câu hỏi quan trọng: PWA là cái quái gì?
Ứng dụng web lũy tiến là gì?
Ứng dụng web lũy tiến là một ứng dụng web mang lại trải nghiệm giống như ứng dụng cho người dùng bằng cách sử dụng các chức năng web hiện đại. Cuối cùng, đó chỉ là trang web thông thường của bạn chạy trên trình duyệt với một số cải tiến. Nó cung cấp cho bạn khả năng:
- Để cài đặt nó trên màn hình chính di động
- Để truy cập nó khi ngoại tuyến
- Để truy cập máy ảnh
- Để nhận thông báo đẩy
- Để thực hiện đồng bộ hóa nền
Và nhiều hơn nữa.
Tuy nhiên, để có thể chuyển đổi ứng dụng web truyền thống của chúng tôi thành PWA, chúng tôi phải điều chỉnh nó một chút bằng cách thêm tệp kê khai ứng dụng web và nhân viên dịch vụ.
Đừng lo lắng về những điều khoản mới này – chúng tôi sẽ đề cập đến chúng bên dưới.
Đầu tiên, chúng ta phải xây dựng ứng dụng web truyền thống của mình. Vì vậy, hãy bắt đầu với việc đánh dấu.
đánh dấu
Tệp HTML tương đối đơn giản. Chúng tôi gói tất cả mọi thứ trong main
nhãn.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<link rel="stylesheet" href="https://www.freecodecamp.org/news/build-a-pwa-from-scratch-with-html-css-and-javascript/css/style.css" />
<title>Dev'Coffee PWA</title>
</head>
<body>
<main>
<nav>
<h1>Dev'Coffee</h1>
<ul>
<li>Home</li>
<li>About</li>
<li>Blog</li>
</ul>
</nav>
<div class="container"></div>
</main>
<script src="js/app.js"></script>
</body>
</html>
Và tạo một thanh điều hướng với nav
nhãn. Sau đó, div
với lớp .container
sẽ giữ các thẻ của chúng tôi mà chúng tôi thêm sau này bằng JavaScript.
Bây giờ chúng ta đã giải quyết được vấn đề đó, hãy tạo kiểu cho nó bằng CSS.
tạo kiểu
Ở đây, như thường lệ, chúng tôi bắt đầu bằng cách nhập các phông chữ chúng tôi cần. Sau đó, chúng tôi sẽ thực hiện một số thao tác đặt lại để ngăn hành vi mặc định.
@import url("https://fonts.googleapis.com/css?family=Nunito:400,700&display=swap");
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
background: #fdfdfd;
font-family: "Nunito", sans-serif;
font-size: 1rem;
}
main {
max-width: 900px;
margin: auto;
padding: 0.5rem;
text-align: center;
}
nav {
display: flex;
justify-content: space-between;
align-items: center;
}
ul {
list-style: none;
display: flex;
}
li {
margin-right: 1rem;
}
h1 {
color: #e74c3c;
margin-bottom: 0.5rem;
}
Sau đó, chúng tôi giới hạn main
chiều rộng tối đa của phần tử để 900px
để làm cho nó trông đẹp trên màn hình lớn.
Đối với thanh điều hướng, tôi muốn logo ở bên trái và liên kết ở bên phải. Vì vậy đối với nav
thẻ, sau khi biến nó thành một thùng chứa linh hoạt, chúng tôi sử dụng justify-content: space-between;
để sắp xếp chúng.
.container {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
grid-gap: 1rem;
justify-content: center;
align-items: center;
margin: auto;
padding: 1rem 0;
}
.card {
display: flex;
align-items: center;
flex-direction: column;
width: 15rem auto;
height: 15rem;
background: #fff;
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.19), 0 6px 6px rgba(0, 0, 0, 0.23);
border-radius: 10px;
margin: auto;
overflow: hidden;
}
.card--avatar {
width: 100%;
height: 10rem;
object-fit: cover;
}
.card--title {
color: #222;
font-weight: 700;
text-transform: capitalize;
font-size: 1.1rem;
margin-top: 0.5rem;
}
.card--link {
text-decoration: none;
background: #db4938;
color: #fff;
padding: 0.3rem 1rem;
border-radius: 20px;
}
Chúng tôi sẽ có một số thẻ, vì vậy đối với phần tử vùng chứa, nó sẽ được hiển thị dưới dạng lưới. Và với grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr))
giờ đây chúng tôi có thể làm cho thẻ của mình phản hồi nhanh để chúng sử dụng ít nhất 15rem
chiều rộng nếu có đủ không gian (và 1fr
nếu không).
Và để làm cho chúng trông đẹp mắt, chúng ta nhân đôi hiệu ứng đổ bóng trên .card
lớp và sử dụng object-fit: cover
trên .card--avatar
để ngăn hình ảnh bị kéo dài.
Bây giờ có vẻ tốt hơn nhiều – nhưng chúng tôi vẫn chưa có dữ liệu để hiển thị.
Hãy khắc phục nó trong phần tiếp theo
Hiển thị dữ liệu bằng JavaScript
Lưu ý rằng tôi đã sử dụng những hình ảnh lớn sẽ mất một chút thời gian để tải. Điều này sẽ cho bạn thấy một cách tốt nhất sức mạnh của những người làm dịch vụ.
Như tôi đã nói trước đó, các .container
lớp sẽ giữ thẻ của chúng tôi. Vì vậy, chúng ta cần phải chọn nó.
const container = document.querySelector(".container")
const coffees = [
{ name: "Perspiciatis", image: "images/coffee1.jpg" },
{ name: "Voluptatem", image: "images/coffee2.jpg" },
{ name: "Explicabo", image: "images/coffee3.jpg" },
{ name: "Rchitecto", image: "images/coffee4.jpg" },
{ name: " Beatae", image: "images/coffee5.jpg" },
{ name: " Vitae", image: "images/coffee6.jpg" },
{ name: "Inventore", image: "images/coffee7.jpg" },
{ name: "Veritatis", image: "images/coffee8.jpg" },
{ name: "Accusantium", image: "images/coffee9.jpg" },
]
Sau đó, chúng tôi tạo một mảng thẻ có tên và hình ảnh.
const showCoffees = () => {
let output = ""
coffees.forEach(
({ name, image }) =>
(output += `
<div class="card">
<img class="card--avatar" src=${image} />
<h1 class="card--title">${name}</h1>
<a class="card--link" href="#">Taste</a>
</div>
`)
)
container.innerHTML = output
}
document.addEventListener("DOMContentLoaded", showCoffees)
Với đoạn mã trên, bây giờ chúng ta có thể lặp qua mảng và hiển thị chúng trên tệp HTML. Và để làm cho mọi thứ hoạt động, chúng tôi đợi cho đến khi nội dung DOM (Mô hình đối tượng tài liệu) tải xong để chạy showCoffees
phương pháp.
Chúng tôi đã làm được rất nhiều, nhưng hiện tại, chúng tôi chỉ có một ứng dụng web truyền thống. Vì vậy, hãy thay đổi điều đó trong phần tiếp theo bằng cách giới thiệu một số tính năng của PWA.

Bản kê khai ứng dụng web
Tệp kê khai ứng dụng web là một tệp JSON đơn giản thông báo cho trình duyệt về ứng dụng web của bạn. Nó cho biết nó sẽ hoạt động như thế nào khi được cài đặt trên thiết bị di động hoặc máy tính để bàn của người dùng. Và để hiển thị lời nhắc Thêm vào Màn hình chính, cần có tệp kê khai ứng dụng web.
Bây giờ chúng ta đã biết bảng kê khai web là gì, hãy tạo một tệp mới có tên manifest.json
(bạn phải đặt tên như vậy) trong thư mục gốc. Sau đó, thêm khối mã này bên dưới.
{
"name": "Dev'Coffee",
"short_name": "DevCoffee",
"start_url": "index.html",
"display": "standalone",
"background_color": "#fdfdfd",
"theme_color": "#db4938",
"orientation": "portrait-primary",
"icons": [
{
"src": "/images/icons/icon-72x72.png",
"type": "image/png", "sizes": "72x72"
},
{
"src": "/images/icons/icon-96x96.png",
"type": "image/png", "sizes": "96x96"
},
{
"src": "/images/icons/icon-128x128.png",
"type": "image/png","sizes": "128x128"
},
{
"src": "/images/icons/icon-144x144.png",
"type": "image/png", "sizes": "144x144"
},
{
"src": "/images/icons/icon-152x152.png",
"type": "image/png", "sizes": "152x152"
},
{
"src": "/images/icons/icon-192x192.png",
"type": "image/png", "sizes": "192x192"
},
{
"src": "/images/icons/icon-384x384.png",
"type": "image/png", "sizes": "384x384"
},
{
"src": "/images/icons/icon-512x512.png",
"type": "image/png", "sizes": "512x512"
}
]
}
Cuối cùng, nó chỉ là một tệp JSON với một số thuộc tính bắt buộc và tùy chọn.
name: Khi trình duyệt khởi chạy màn hình giật gân, nó sẽ là tên hiển thị trên màn hình.
short_name: Nó sẽ là tên được hiển thị bên dưới lối tắt ứng dụng của bạn trên màn hình chính.
start_url: Đây sẽ là trang được hiển thị cho người dùng khi ứng dụng của bạn mở.
hiển thị: Nó cho trình duyệt biết cách hiển thị ứng dụng. Có một số chế độ như minimal-ui
, fullscreen
, browser
v.v. Ở đây, chúng tôi sử dụng standalone
chế độ ẩn mọi thứ liên quan đến trình duyệt.
background_color: Khi trình duyệt khởi chạy màn hình giật gân, nó sẽ là nền của màn hình.
theme_color: Nó sẽ là màu nền của thanh trạng thái khi chúng ta mở ứng dụng.
định hướng: Nó cho trình duyệt biết hướng cần có khi hiển thị ứng dụng.
biểu tượng: Khi trình duyệt khởi chạy màn hình giật gân, nó sẽ là biểu tượng hiển thị trên màn hình. Ở đây, tôi đã sử dụng tất cả các kích thước để phù hợp với biểu tượng ưa thích của bất kỳ thiết bị nào. Nhưng bạn chỉ có thể sử dụng một hoặc hai. Tuỳ bạn.
Bây giờ chúng ta đã có bảng kê khai ứng dụng web, hãy thêm nó vào tệp HTML.
<link rel="manifest" href="https://www.freecodecamp.org/news/build-a-pwa-from-scratch-with-html-css-and-javascript/manifest.json" />
<!-- ios support -->
<link rel="apple-touch-icon" href="images/icons/icon-72x72.png" />
<link rel="apple-touch-icon" href="images/icons/icon-96x96.png" />
<link rel="apple-touch-icon" href="images/icons/icon-128x128.png" />
<link rel="apple-touch-icon" href="images/icons/icon-144x144.png" />
<link rel="apple-touch-icon" href="images/icons/icon-152x152.png" />
<link rel="apple-touch-icon" href="images/icons/icon-192x192.png" />
<link rel="apple-touch-icon" href="images/icons/icon-384x384.png" />
<link rel="apple-touch-icon" href="images/icons/icon-512x512.png" />
<meta name="apple-mobile-web-app-status-bar" content="#db4938" />
<meta name="theme-color" content="#db4938" />
Như bạn có thể thấy, chúng tôi đã liên kết manifest.json
tập tin vào thẻ đầu. Và thêm một số liên kết khác xử lý hỗ trợ iOS để hiển thị các biểu tượng và tô màu thanh trạng thái bằng màu chủ đề của chúng tôi.
Cùng với đó, bây giờ chúng ta có thể đi sâu vào phần cuối cùng và giới thiệu nhân viên dịch vụ.
Nhân viên phục vụ là gì?
Lưu ý rằng PWA chỉ chạy trên https vì nhân viên dịch vụ có thể truy cập và xử lý yêu cầu đó. Do đó cần phải bảo mật.
Service worker là một tập lệnh mà trình duyệt của bạn chạy ẩn trong một chuỗi riêng biệt. Điều đó có nghĩa là nó chạy ở một nơi khác và hoàn toàn tách biệt với trang web của bạn. Đó là lý do tại sao nó không thể thao tác phần tử DOM của bạn.
Tuy nhiên, nó siêu mạnh. Nhân viên dịch vụ có thể chặn và xử lý các yêu cầu mạng, quản lý bộ đệm để bật hỗ trợ ngoại tuyến hoặc gửi thông báo đẩy cho người dùng của bạn.

S0 hãy tạo nhân viên dịch vụ đầu tiên của chúng tôi trong thư mục gốc và đặt tên cho nó serviceWorker.js
(tên tùy bạn). Nhưng bạn phải đặt nó trong thư mục gốc để không giới hạn phạm vi của nó trong một thư mục.
Lưu trữ nội dung
const staticDevCoffee = "dev-coffee-site-v1"
const assets = [
"/",
"/index.html",
"/css/style.css",
"/js/app.js",
"/images/coffee1.jpg",
"/images/coffee2.jpg",
"/images/coffee3.jpg",
"/images/coffee4.jpg",
"/images/coffee5.jpg",
"/images/coffee6.jpg",
"/images/coffee7.jpg",
"/images/coffee8.jpg",
"/images/coffee9.jpg",
]
self.addEventListener("install", installEvent => {
installEvent.waitUntil(
caches.open(staticDevCoffee).then(cache => {
cache.addAll(assets)
})
)
})
Mã này ban đầu trông có vẻ đáng sợ nhưng nó chỉ là JavaScript (vì vậy đừng lo lắng).
Chúng tôi khai báo tên của bộ đệm của chúng tôi staticDevCoffee
và các tài sản để lưu trữ trong bộ đệm. Và để thực hiện hành động đó, chúng ta cần đính kèm một bộ lắng nghe vào self
.
self
chính là nhân viên phục vụ. Nó cho phép chúng ta lắng nghe các sự kiện trong vòng đời và đáp lại điều gì đó.
Service worker có nhiều vòng đời, và một trong số đó là install
biến cố. Nó chạy khi một nhân viên dịch vụ được cài đặt. Nó được kích hoạt ngay khi worker thực thi và nó chỉ được gọi một lần cho mỗi service worker.
Khi mà install
sự kiện được kích hoạt, chúng tôi chạy cuộc gọi lại cho phép chúng tôi truy cập vào event
vật.
Có thể mất một chút thời gian để hoàn thành việc lưu vào bộ nhớ đệm một thứ gì đó trên trình duyệt vì nó không đồng bộ.
Vì vậy, để xử lý nó, chúng ta cần sử dụng waitUntil()
mà, như bạn có thể đoán, đợi hành động kết thúc.
Khi API bộ đệm đã sẵn sàng, chúng ta có thể chạy open()
phương thức và tạo bộ đệm của chúng tôi bằng cách chuyển tên của nó làm đối số cho caches.open(staticDevCoffee)
.
Sau đó, nó trả về một lời hứa, giúp chúng tôi lưu trữ nội dung của mình trong bộ đệm với cache.addAll(assets)
.

Hy vọng rằng, bạn vẫn còn với tôi.

Bây giờ, chúng tôi đã lưu trữ thành công nội dung của mình trong trình duyệt. Và lần sau khi chúng tôi tải trang, nhân viên dịch vụ sẽ xử lý yêu cầu và tìm nạp bộ đệm nếu chúng tôi ngoại tuyến.
Vì vậy, hãy tìm nạp bộ đệm của chúng tôi.
Lấy tài sản
self.addEventListener("fetch", fetchEvent => {
fetchEvent.respondWith(
caches.match(fetchEvent.request).then(res => {
return res || fetch(fetchEvent.request)
})
)
})
Ở đây, chúng tôi sử dụng các fetch
sự kiện để lấy lại dữ liệu của chúng tôi. Cuộc gọi lại cho phép chúng tôi truy cập vào fetchEvent
. Sau đó, chúng tôi đính kèm respondWith()
để ngăn phản hồi mặc định của trình duyệt. Thay vào đó, nó trả về một lời hứa vì hành động tìm nạp có thể mất thời gian để hoàn thành.
Và khi bộ đệm đã sẵn sàng, chúng tôi áp dụng caches.match(fetchEvent.request)
. Nó sẽ kiểm tra xem có thứ gì trong bộ đệm khớp không fetchEvent.request
. Nhân tiện, fetchEvent.request
chỉ là mảng tài sản của chúng tôi.
Sau đó, nó trả lại một lời hứa. Và cuối cùng, chúng ta có thể trả về kết quả nếu nó tồn tại hoặc tìm nạp ban đầu nếu không.
Giờ đây, nội dung của chúng tôi có thể được lưu vào bộ đệm và tìm nạp bởi nhân viên dịch vụ, điều này làm tăng thời gian tải hình ảnh của chúng tôi lên một chút.
Và quan trọng nhất, nó làm cho ứng dụng của chúng tôi khả dụng ở chế độ ngoại tuyến.
Nhưng một nhân viên dịch vụ một mình không thể thực hiện công việc. Chúng tôi cần phải đăng ký nó trong dự án của chúng tôi.

Đăng ký nhân viên dịch vụ
if ("serviceWorker" in navigator) {
window.addEventListener("load", function() {
navigator.serviceWorker
.register("/serviceWorker.js")
.then(res => console.log("service worker registered"))
.catch(err => console.log("service worker not registered", err))
})
}
Ở đây, chúng tôi bắt đầu bằng cách kiểm tra xem serviceWorker
được trình duyệt hiện tại hỗ trợ (vì nó vẫn chưa được hỗ trợ bởi tất cả các trình duyệt).
Sau đó, chúng tôi lắng nghe sự kiện tải trang để đăng ký nhân viên dịch vụ của chúng tôi bằng cách chuyển tên tệp của chúng tôi serviceWorker.js
đến navigator.serviceWorker.register()
như một tham số để đăng ký nhân viên của chúng tôi.
Với bản cập nhật này, giờ đây chúng tôi đã chuyển đổi ứng dụng web thông thường của mình thành PWA.

suy nghĩ cuối cùng
Trong suốt bài viết này, chúng ta đã thấy PWA có thể tuyệt vời như thế nào. Bằng cách thêm tệp kê khai ứng dụng web và nhân viên dịch vụ, nó thực sự cải thiện trải nghiệm người dùng của ứng dụng web truyền thống của chúng tôi. Điều này là do PWA nhanh, an toàn, đáng tin cậy và – quan trọng nhất – chúng hỗ trợ chế độ ngoại tuyến.
Nhiều khung hiện có đi kèm với tệp nhân viên dịch vụ đã được thiết lập sẵn cho chúng tôi. Nhưng biết cách triển khai nó với Vanilla JavaScript có thể giúp bạn hiểu PWAs.
Và bạn có thể tiến xa hơn nữa với nhân viên dịch vụ bằng cách tự động lưu nội dung vào bộ đệm hoặc giới hạn kích thước bộ đệm của bạn, v.v.
Cảm ơn đã đọc bài viết này.
Bạn có thể xem trực tiếp tại đây và mã nguồn tại đây.
Đọc thêm các bài viết của tôi trên blog của tôi
Bước tiếp theo
Tài liệu kê khai web
Tài liệu nhân viên dịch vụ
Trình tạo tệp kê khai web
Hỗ trợ trình duyệt