渐进式Web应用程序(PWA)可以使用Web平台功能和渐进式增强功能为用户提供与本机应用程序相当的体验的应用程序。
“离线优先”是一种应用程序开发范式,在这种范式中,开发人员确保应用程序的功能不受暂时失去网络连接的影响。渐进式 Web 应用程序(PWA)感觉像原生应用程序,但运行起来像 Web 应用程序,通常建立在这种范式之上。
本文将告诉你如何使用 Node.js 和 SQLite 数据库构建离线优先应用程序。首先,让我们从认识渐进式 Web 应用程序开始。
PWA 简介
渐进式 Web 应用程序 (PWA) 是使用服务工作者、清单和其他 Web 平台功能和渐进式增强功能为用户提供与本机应用程序相当的体验的 Web 应用程序。
PWA 在效率方面有时可以胜过原生应用程序。它们按需运行,并且始终可用,无需消耗宝贵的智能手机内存或数据。与同一应用程序的本机版本相比,用户在选择 PWA 时消耗的数据更少。他们仍然可以将 PWA 保存到他们的主屏幕,且无需完整下载即可安装。
为了展示渐进式 Web 应用程序的强大功能,我们将构建一个简单的博客应用程序。

初始化NodeJs应用程序
首先,我们将使用以下命令创建项目文件夹:然后,我们将使用以下命令初始化 Node.js 应用程序:
mkdir PWA && cd PWA
上面的命令为应用程序创建一个package.json文件。 接下来,在我们的项目文件夹中创建以下文件夹结构:
npm init -y

设置 Express 服务器
通过我们的应用程序设置,让我们安装 Express 以使用以下命令创建我们的 Node.js 服务器:
npm install express
然后,我们将在 public 文件夹中创建几个文件夹和文件:
- css/style.css 文件
 - js/app.js 文件
 
在代码片段中,我们导入express来创建我们的服务器和路径模块。我们将应用程序配置为使用express.static方法渲染静态文件,该方法采用静态文件夹(公共)的路径,我们创建了应用程序的根路由并渲染了index.html文件。然后我们将应用程序配置为侦听端口8000。
const express = require("express");const path = require("path");const app = express();app.use(express.static(path.join(__dirname, "public")));app.get("/", function (req, res) {res.sendFile(path.join(__dirname, "public/index.html"));});app.listen(8000, () => console.log("Server is running on Port 8000"));
连接到 SQLite 数据库
为我们的应用程序设置服务器后,让我们创建并连接我们的应用程序以保存我们的博客详细信息。首先,运行以下命令来安装 sqlite3 依赖项。然后,在入口点index.js文件中,添加以下代码片段以创建应用程序并将其连接到 SQLite 数据库。
npm install sqlite3
接下来,我们将创建一个博客列表,将其存储在我们的数据库中,稍后使用以下代码片段呈现到客户端:
const db = new sqlite3.Database("db.sqlite", (err) => {if (err) {// Cannot open databaseconsole.error(err.message);throw err;} else {console.log("Connected to the SQLite database.");}});
我们应用程序中的每个块帖子都有一个id、title、 avatar和intro字段。 现在创建一个数据库表名为blogs并使用下面的代码片段保存我们刚刚在上面创建的博客详细信息:
let blogs = [{id: "1",title: "How To Build A RESTAPI With Javascript",avatar: "images/coffee2.jpg",intro: "iste odio beatae voluptas dolor praesentium illo facere optio nobis magni, aspernatur quas.",},{id: "2",title: "How to Build an Offline-First Application with Node.js,"avatar: "images/coffee2.jpg","iste odio beatae voluptas dolor praesentium illo facere optio nobis magni, aspernatur quas.",},{id: "3",title: "Building a Trello Clone with React DnD",avatar: "images/coffee2.jpg",intro: "iste odio beatae voluptas dolor praesentium illo facere optio nobis magni, aspernatur quas.",},];
在代码片段中,我们使用db.run 创建了一个表**blogs 。db.run方法接受**一个 SQL 查询作为参数,然后我们遍历我们的博客数组并将它们插入到我们刚刚使用 js map 函数创建的 blogs 表中。
db.run(`CREATE TABLE blog (id INTEGER PRIMARY KEY AUTOINCREMENT, title text,avatar text,intro text)`,(err) => {if (err) {// console.log(err)// Table already created} else {// Table just created, creating some rowsvar insert = "INSERT INTO blogs (title, avatar, intro) VALUES (?,?,?)";blogs.map((blog) => {db.run(insert, [`${blog.title}`,`${blog.avatar}`,`${blog.intro}`,]);});}});
查看数据库记录
现在让我们查看我们刚刚使用Arctype创建的记录。要使用 Arctype 查看 SQLite 数据库中的记录,请执行以下步骤:- 安装 Arctype
 - 运行应用程序node index.js以创建数据库
 - 启动 Arctype 并单击 SQLite 选项卡
 

- 单击 Select SQLite file按钮,找到运行服务器时生成的db.sqlite文件。
 - 您应该看到 blogs 表和我们创建的记录,如下面的屏幕截图所示:
 

渲染页面
此时,我们已将应用程序连接到 SQLite 数据库,并在数据库中插入了一些记录。现在,打开index.html文件并在下面添加以下代码片段:我们在上面的文件中创建了一个简单的标记,其中包含指向我们清单的链接,我们将在下一部分、styles和app.js文件中创建它。 然后,我们将在index.js 文件中创建一个blogs路由,以将博客返回到客户端。
<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8" /><meta name="viewport" content="width=device-width, initial-scale=1.0" /><meta data-fr-http-equiv="X-UA-Compatible" content="ie=edge" /><link rel="stylesheet" href="css/style.css" /><title>Blogger</title><link rel="manifest" href="manifest" /></head><body><section><nav><h1>Blogger</h1><ul><li>Home</li><li class="active">Blog</li></ul></nav><div class="container"></div></section><script src="js/app.js"></script></body></html>
在我们的public/js/app.js文件中,我们将向博客端点发送一个获取请求,以从我们的后端获取博客。然后我们遍历博客,定位容器类并显示它们。
...app.get("/blogs", (req, res) => {res.status(200).json({blogs,});});...
我们还将使用以下代码片段在public/css/style.css 中为我们的应用程序添加一些样式:
let result = "";fetch("http://localhost:8000/blogs").then((res) => res.json()).then(({ rows } = data) => {rows.forEach(({ title, avatar, intro } = rows) => {result += `<div class="card"><img class="card-avatar" src="/${avatar}"/><h1 class="card-title">${title}</h1><p class="intro">${intro}</p><a class="card-link" href="#">Read</a></div>`;});document.querySelector(".container").innerHTML = result;}).catch((e) => {console.log(e);});
css
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}
body {
  background: #fdfdfd;
  font-size: 1rem;
}
section {
  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: #0e9c95;
  margin-bottom: 0.5rem;
}
.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;
  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: #16a0d6e7;
  color: #fff;
  padding: 0.3rem 1rem;
  border-radius: 20px;
  margin: 10px;
}
.intro {
  color: #c2c5c5;
  padding: 10px;
}
.active {
  color: #16a0d6e7;
}
现在打开package.json文件并添加启动脚本。
至此,我们已经设置了我们的应用程序。但是当服务器没有运行或者没有网络连接用于生产时,我们无法运行我们的应用程序。让我们在下一节中进行设置。
"start": "node index.js"
优化应用
我们需要使我们的应用程序与所有屏幕尺寸兼容。我们还将通过在index.html 文件的 head 部分添加下面的标记来添加主题颜色。
<meta name="viewport" content="width=device-width, initial-scale=1"><meta name="theme-color" content="#16a0d6e7"/>
创建清单
我们需要描述我们的应用程序以及它在安装在用户设备上时的行为方式。我们可以通过创建清单来做到这一点。 在项目根目录下创建manifest文件,添加如下配置:json
{
    "name": "Blogger"
    "short_name": "Blogger"
    "start_url": "/",
    "display": "standalone",
    "background_color": "#0e9c95",
    "theme_color": "#16a0d6e7",
    "orientation": "portrait",
    "icons": []
}
在我们的清单中,我们定义了以下配置:
- name:这定义了应用程序的显示名称。
 - short_name:这定义了安装时将在应用程序图标下显示的名称。
 - start_url:这告诉浏览器应用程序的根 URL。
 - display:这告诉浏览器如何显示应用程序。
 - background_color: 这定义了安装时应用程序的背景颜色。
 - theme_color: 这定义了状态栏的颜色。
 - 方向: 这定义了在应用显示期间使用的方向。
 - 图标: 这定义了不同大小的图标或图像用作我们的应用程序主页图标。
 
上面的命令将在 public 文件夹中创建一个图标 文件夹,其中包含我们应用程序的许多图标,以及终端上的一些JSON,我们将粘贴到清单中的图标数组中。
#change directory to the public foldercd public#generate iconsnpx pwa-asset-generator logo.png icons

此外,该命令生成了指向生成的图标的标记链接。
"icons": [{"src": "public/icons/manifest-icon-192.maskable.png","sizes": "192x192","type": "image/png","purpose": "any"},{"src": "public/icons/manifest-icon-192.maskable.png","sizes": "192x192","type": "image/png","purpose": "maskable"},{"src": "public/icons/manifest-icon-512.maskable.png","sizes": "512x512","type": "image/png","purpose": "any"},{"src": "public/icons/manifest-icon-512.maskable.png","sizes": "512x512","type": "image/png","purpose": "maskable"}]

将标记复制并粘贴到public/index.html 文件中标记的 head 部分。
设置服务工作者
创建清单后,让我们设置服务工作者。Service Worker 是一段 JavaScript 代码,您的浏览器在后台运行在一个单独的线程中,以处理您为将来的请求保存的资产和数据的缓存,从而为您的应用程序启用离线支持。 所以在public 文件夹中创建一个blogger.serviceWorker.js 文件。对于 service worker,有很多事件(push、activate、install、fetch、message、sync),但对于本教程的演示,我们将介绍install、activate 和fetch 事件。在此之前,我们需要创建一个数组来存储我们在应用程序中使用的所有资产。然后,我们将监听install 事件来注册并将我们的静态文件保存到浏览器的缓存中。此过程需要一些时间才能完成。要跳过等待,我们将使用skipWaiting()。
const assets = ["/","css/style.css","js/app.js","/images/blog1.jpg","/images/blog2.jpg","/images/blog3.jpg,"];
javascript
const BLOGGER_ASSETS = "blogger-assets";
self.addEventListener("install", (installEvt) => {
  installEvt.waitUntil(
    caches
      .open(BLOGGER_ASSETS)
      .then((cache) => {
        cache.addAll(assets);
      })
      .then(self.skipWaiting())
      .catch((e) => {
        console.log(e);
      })
  );
});
...
然后,我们需要在 service worker 更新时清除缓存以删除旧资产。为此,我们将收听下面的激活 代码片段:
在上面的代码片段中,我们在 service worker 上使用了waitUntil方法。此方法等待操作完成,然后在删除之前检查我们尝试清除的资产是否是我们当前应用程序的资产。 接下来,我们需要存储在缓存中的文件才能使用它们。
...self.addEventListener("activate", function (evt) {evt.waitUntil(caches.keys().then((keysList) => {return Promise.all(keysList.map((key) => {if (key === BLOGGER_ASSETS) {console.log(`Removed old cache from ${key}`);return caches.delete(key);}}));}).then(() => self.clients.claim()));});
当在页面上发出请求时,PWA 将检查我们的缓存并从缓存中读取数据,而不是去网络。然后,使用respondWith方法,我们覆盖浏览器的默认值并让我们的事件返回一个 Promise。缓存完成后,我们可以返回evt.request对应的缓存。当缓存准备好后,我们可以返回匹配 evt.request 的缓存。 我们已经成功设置了我们的 service worker。现在让我们让它在我们的应用程序中可用。
self.addEventListener("fetch", function (evt) {evt.respondWith(fetch(evt.request).catch(() => {return caches.open(BLOGGER_ASSETS).then((cache) => {return cache.match(evt.request);});}));})
注册 Service Worker
现在让我们在public/js/app.js 文件中注册我们的 service worker,代码片段如下:
在这里,我们检查我们的应用程序的浏览器是否支持Service Worker(当然不是所有的浏览器都支持Service Worker),然后注册我们的Service Worker 文件。 现在使用以下命令运行应用程序:
...if ("serviceWorker" in navigator) {window.addEventListener("load", function () {navigator.serviceWorker.register("/blogger.serviceWorker.js").then((res) => console.log("service worker registered")).catch((err) => console.log("service worker not registered", err));});}
在浏览器中转到 localhost:8000 以访问该应用程序。
npm start
谷歌灯塔检查
现在让我们检查一下我们是否使用Google Lighthouse检查正确设置了 PWA 。右键单击浏览器并选择“检查”。在检查选项卡上,选择灯塔并单击生成报告。如果您的应用程序一切顺利,您应该会看到如下屏幕截图中的输出:

结论
渐进式 Web 应用 (PWA) 使用现代 API 通过单个代码库提供增强的功能、可靠性和可安装性。它们允许您的最终用户使用您的应用程序,无论他们是否有互联网连接。您应该随时 fork存储库并向项目添加其他功能。原文标题:Building Offline-First Apps With Node.js and SQLite
原文作者:Clara Ekekenta
