如何使用 HTML、CSS 和 JavaScript 从头开始​​构建 PWA?

2025-05-24

如何使用 HTML、CSS 和 JavaScript 从头开始​​构建 PWA?

最初发布在我的博客上

渐进式 Web 应用是一种将原生应用体验融入普通或传统 Web 应用的方式。事实上,借助 PWA,我们现在可以利用移动应用功能来增强我们的网站,从而大幅提升可用性,并为最终用户提供卓越的用户体验。

在本文中,我们将使用 HTML、CSS 和 JavaScript 从头构建一个 PWA。
那么,让我们先来回答一个重要的问题:PWA 到底是什么?

什么是渐进式 Web 应用程序 (PWA)?

渐进式 Web 应用是一种利用现代 Web 功能为用户提供类似应用程序体验的 Web 应用。归根结底,它就是在浏览器中运行的常规网站,但具备一些增强功能,例如:

  • 将其安装在移动主屏幕上
  • 离线时访问
  • 访问相机
  • 获取推送通知
  • 执行后台同步

还有更多。

但是,为了能够将我们的传统 Web 应用程序转换为 PWA,我们必须对其进行一些调整,通过添加 Web 应用程序清单文件和服务工作线程。

不要担心这些新术语,我们稍后会介绍它们。

但首先,我们必须构建我们的 Web 应用,或者你也可以选择传统的 Web 应用。
那么,我们就从标记开始吧。

标记

HTML文件比较简单,我们将所有内容都包裹在main标签中。

  • index.html
<!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="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>
Enter fullscreen mode Exit fullscreen mode

并使用标签创建一个导航栏nav。然后,div带有该类的对象.container将用于稍后通过 JavaScript 添加的卡片。

话虽如此,让我们用 CSS 来设计它。

造型

在这里,像往常一样,我们首先导入所需的字体并进行一些重置以防止默认行为。

  • css/style.css
@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;
}
Enter fullscreen mode Exit fullscreen mode

然后,我们将main元素的最大宽度限制为900px,以使其在大屏幕上看起来不错。

对于导航栏,我希望徽标位于左侧,链接位于右侧。因此,对于nav标签,在将其设置为弹性容器后,我们使用它justify-content: space-between;来对齐它们。

  • css/style.css
.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;
}
Enter fullscreen mode Exit fullscreen mode

我们将有几张卡片,因此,对于容器元素来说,它将显示为网格。并且,有了grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr)),我们现在可以使卡片具有响应性,并允许它们15rem在空间足够时至少使用相同的宽度,1fr否则则使用相同的宽度。

为了让它们看起来漂亮,我们将.card类的阴影效果加倍,并使用object-fit: cover.card--avatar防止图像拉伸。

所以,现在看起来好多了,但我们仍然没有数据可以显示。

让我们在下一节中修复它

使用 JavaScript 显示数据

请注意,我使用了需要一些时间加载的大图像。只是为了以最佳方式向您展示 Service Worker 的强大功能。

正如我之前所说,这个.container类将掌握我们的主动权。因此,我们需要选择它。

  • js/app.js
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" },
]
Enter fullscreen mode Exit fullscreen mode

然后,我们创建一个包含名称和图像的卡片数组。

  • js/app.js
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)
Enter fullscreen mode Exit fullscreen mode

有了上面的代码,我们现在可以循环遍历数组并将其显示在 HTML 文件中。为了使一切正常工作,我们等到 DOM(文档对象模型)内容加载完成后再运行该showCoffees方法。

我们已经做了很多,但目前我们只有一个传统的 Web 应用。
所以,下一节我们将通过介绍 PWA 功能来改变这一现状。

超级兴奋

Web 应用清单

网页应用清单是一个简单的 JSON 文件,它会告知浏览器您的网页应用及其在用户移动设备或桌面设备上安装后的行为方式。要显示“添加到主屏幕”提示,网页应用清单必不可少。

现在我们知道了什么是 Web 清单,让我们manifest.json在根目录中创建一个名为(您必须这样命名)的新文件,并在下面添加此代码块。

  • manifest.json
{
  "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"
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

最后,它只是一个具有一些强制和可选属性的 JSON 文件。

  • name:当浏览器启动闪屏时,它将是屏幕上显示的名称。

  • short_name:它将是主屏幕上应用程序快捷方式下方显示的名称。

  • start_url:当您的应用程序打开时,它将是向用户显示的页面。

  • display:它告诉浏览器如何显示应用。它有几种模式,例如minimal-uifullscreen等等browser
    在这里,我们使用该standalone模式来隐藏与浏览器相关的所有内容。

  • background_color:当浏览器启动启动画面时,它将是屏幕的背景。

  • theme_color:当我们打开应用程序时,它将是状态栏的背景颜色。

  • 方向:它告诉浏览器显示应用程序时的方向。

  • 图标:浏览器启动启动画面时,图标会显示在屏幕上。这里,我使用了各种尺寸,以适应不同设备的图标。但您也可以只使用一个或两个图标,由您决定。

现在,我们有一个 Web 应用程序清单,让我们将其添加到 HTML 文件中。

  • index.html(head 标签)中
<link rel="manifest" href="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" />
Enter fullscreen mode Exit fullscreen mode

如你所见,我们将manifest.json文件链接到了 head 标签。此外,还添加了一些其他链接,用于处理 iOS 支持,以显示图标并使用主题颜色为状态栏着色。

有了这些,我们现在可以深入到最后一部分并介绍服务人员。

什么是 Service Worker?

请注意,PWA 仅在 https 上运行,因为 Service Worker 可以访问并处理请求。因此,安全性是必需的。

Service Worker 是浏​​览器在后台单独线程中运行的脚本。这意味着它运行在不同的地方,与你的网页完全独立。这就是为什么它无法操作你的 DOM 元素的原因。

然而,它非常强大。Service Worker 可以拦截和处理网络请求,管理缓存以启用离线支持,或向用户发送推送通知。

哇

话虽如此,让我们在根文件夹中创建第一个 Service Worker 并为其命名serviceWorker.js(名称由您决定)。但您必须将其放在根目录中,以免将其范围限制在一个文件夹中。

缓存资产

  • serviceWorker.js
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)
    })
  )
})
Enter fullscreen mode Exit fullscreen mode

这里,它乍一看很吓人,但它只是 JavaScript(别担心)。

我们声明了缓存的名称staticDevCoffee以及要存储在缓存中的资源。
为了执行该操作,我们需要为 附加一个监听器self

self是 Service Worker 本身。它使我们能够监听生命周期事件并执行相应的操作。

Service Worker 有多个生命周期,其中之一就是install事件。它在 Service Worker 安装时运行。
它在 Service Worker 执行时立即触发,并且每个 Service Worker 只会调用一次。

install事件被触发时,我们运行回调,以便我们访问该event对象。

由于浏览器缓存某些内容是异步的,因此可能需要一些时间才能完成。

因此,为了处理它,我们需要使用waitUntil()您可能猜到的方法,等待操作完成。

一旦缓存 API 准备就绪,我们现在就可以运行该open()方法并通过将其名称作为参数传递给来创建我们的缓存caches.open(staticDevCoffee)

然后,它返回一个承诺,帮助我们将资产存储在缓存中cache.addAll(assets)

图像缓存

希望你仍与我同在。

绝望

现在,我们成功地将资源缓存到了浏览器上。下次加载页面时,如果处于离线状态,Service Worker 会处理请求并获取缓存。

那么,让我们获取我们的缓存。

获取资产

  • serviceWorker.js
self.addEventListener("fetch", fetchEvent => {
  fetchEvent.respondWith(
    caches.match(fetchEvent.request).then(res => {
      return res || fetch(fetchEvent.request)
    })
  )
})
Enter fullscreen mode Exit fullscreen mode

这里,我们使用fetch事件来获取数据。回调函数允许我们访问fetchEvent,然后我们附加它respondWith()来阻止浏览器的默认响应,而是返回一个 Promise。因为获取操作可能需要一些时间才能完成。

一旦缓存准备好,我们就会应用caches.match(fetchEvent.request)。它会检查缓存中是否有匹配的内容fetchEvent.request。顺便说一下,fetchEvent.request就是我们的资源数组。

然后,它返回一个承诺,最后,如果存在,我们可以返回结果,如果不存在,我们可以返回初始提取。

现在,我们的资产可以被服务工作者缓存和获取,这大大增加了图像的加载时间。

最重要的是,它使我们的应用程序可以在离线模式下使用。

但是服务工作者不能单独完成这项工作,我们需要在我们的项目中注册它。

我们开始做吧

注册 Service Worker

  • js/app.js
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))
  })
}
Enter fullscreen mode Exit fullscreen mode

这里,我们首先检查serviceWorker当前浏览器是否支持。因为并非所有浏览器都支持它。

然后,我们监听页面加载事件,通过将文件的名称作为参数传递来注册我们的服务serviceWorker.js工作者navigator.serviceWorker.register()

通过此更新,我们现在已将常规 Web 应用程序转变为 PWA。

我们做到了

最后的想法

在本文中,我们见识了 PWA 的强大之处。通过添加 Web 应用清单文件和 Service Worker,它极大地提升了传统 Web 应用的用户体验。PWA 快速、安全、可靠,而且最重要的是,它支持离线模式。

现在很多框架都已经为我们设置好了 Service Worker 文件,但是,了解如何使用原生 JavaScript 实现它可以帮助你理解 PWA。
你还可以通过动态缓存资源或限制缓存大小等方式进一步利用 Service Worker。

话虽如此,感谢您阅读这篇文章。

您可以在这里实时查看

源代码在这里

后续步骤Next steps

Web 清单文档

服务工作者文档

Web 清单生成器

浏览器支持

文章来源:https://dev.to/ibrahima92/how-to-build-a-pwa-from-scratch-with-html-css-and-javascript-4bg5
PREV
React TypeScript - 如何在 Hooks 上设置类型(+ 备忘单)
NEXT
高级 TypeScript 类型速查表(含示例)