使用 Node.js 和 SQLite 构建离线优先应用程序

语言: CN / TW / HK

“离线优先”是一种应用程序开发范式,在这种范式中,开发人员确保应用程序的功能不受暂时失去网络连接的影响。渐进式 Web 应用程序(PWA)感觉像原生应用程序,但运行起来像 Web 应用程序,通常建立在这种范式之上。

本文将告诉你如何使用 Node.js 和 SQLite 数据库构建离线优先应用程序。首先,让我们从认识渐进式 Web 应用程序开始。

PWA 简介

渐进式 Web 应用程序 (PWA) 是使用服务工作者、清单和其他 Web 平台功能和渐进式增强功能为用户提供与本机应用程序相当的体验的 Web 应用程序。

PWA 在效率方面有时可以胜过原生应用程序。它们按需运行,并且始终可用,无需消耗宝贵的智能手机内存或数据。与同一应用程序的本机版本相比,用户在选择 PWA 时消耗的数据更少。他们仍然可以将 PWA 保存到他们的主屏幕,且无需完整下载即可安装。

为了展示渐进式 Web 应用程序的强大功能,我们将构建一个简单的博客应用程序。

用户将能够像其他PWA一样与之交互,例如Twitter PWA。让我们开始吧。

初始化NodeJs应用程序

首先,我们将使用以下命令创建项目文件夹:

mkdir PWA && cd PWA

然后,我们将使用以下命令初始化 Node.js 应用程序:

npm init -y

上面的命令为应用程序创建一个 package.json 文件。

接下来,在我们的项目文件夹中创建以下文件夹结构:

设置 Express 服务器

通过我们的应用程序设置,让我们安装 Express 以使用以下命令创建我们的 Node.js 服务器:

npm install express

然后,我们将在 public 文件夹中创建几个文件夹和文件:

  • css/style.css 文件
  • js/app.js 文件

接下来,在项目根目录中创建一个 index.js 文件,其中包含以下代码片段:

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"));

在代码片段中,我们导入 express 来创建我们的服务器和 路径 模块。我们将应用程序配置为使用 express.static 方法渲染静态文件,该方法采用静态文件夹(公共)的路径,我们创建了应用程序的根路由并渲染了 index.html 文件。然后我们将应用程序配置为侦听端口 8000

连接到 SQLite 数据库

为我们的应用程序设置服务器后,让我们创建并连接我们的应用程序以保存我们的博客详细信息。首先,运行以下命令来安装 sqlite3 依赖项。

npm install sqlite3

然后,在入口点 index.js 文件中,添加以下代码片段以创建应用程序并将其连接到 SQLite 数据库。

const db = new sqlite3.Database("db.sqlite", (err) => {
  if (err) {
    // Cannot open database
    console.error(err.message);
    throw err;
  } else {
    console.log("Connected to the SQLite database.");
  }
});

接下来,我们将创建一个博客列表,将其存储在我们的数据库中,稍后使用以下代码片段呈现到客户端:

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.",
  },
];

我们应用程序中的每个块帖子都有一个 id title avatar intro 字段。

现在创建一个数据库表名为 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 rows
      var insert = "INSERT INTO blogs (title, avatar, intro) VALUES (?,?,?)";
      blogs.map((blog) => {
        db.run(insert, [
          `${blog.title}`,
          `${blog.avatar}`,
          `${blog.intro}`,
        ]);
      });
    }
  }
);

在代码片段中,我们 使用 db.run 创建了一个表 blogs 。 db.run方法 接受 一个 SQL 查询作为参数,然后我们遍历我们的博客数组并将它们插入到我们刚刚使用 js map 函数创建的 blogs 表中。

查看数据库记录

现在让我们查看我们刚刚使用 Arctype 创建的记录。 要使用 Arctype 查看 SQLite 数据库中的记录,请执行以下步骤:

  • 安装 Arctype
  • 运行应用程序 node index.js 以创建数据库
  • 启动 Arctype 并单击 SQLite 选项卡

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

渲染页面

此时,我们已将应用程序连接到 SQLite 数据库,并在数据库中插入了一些记录。现在,打开 index.html 文件并在下面添加以下代码片段:

<!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>

我们在上面的文件中创建了一个简单的标记,其中包含指向我们清单的链接,我们将在下一部分、 styles app.js 文件中创建它。

然后,我们将在 index.js 文件中创建一个blogs 路由,以将博客返回到客户端。

...
app.get("/blogs", (req, res) => {
  res.status(200).json({
    blogs,
  });
});
...

在我们的 public/js/app.js 文件中,我们将向博客端点发送一个获取请求,以从我们的后端获取博客。然后我们遍历博客,定位 容器 类并显示它们。

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);
  });

我们还将使用以下代码片段在 public/css/style.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文件,添加如下配置:

{
    "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: 这定义了状态栏的颜色。
  • 方向: 这定义了在应用显示期间使用的方向。
  • 图标: 这定义了不同大小的图标或图像用作我们的应用程序主页图标。

手动创建我们的主屏幕图标可能是一项非常复杂的任务,但不用担心。 我们将利用名为pwa-asset-generator 的第三方模块,使用以下命令从公共目录中的主应用程序图标生成不同大小的图标:

#change directory to the public folder
cd public
#generate icons
npx pwa-asset-generator logo.png icons

上面的命令将在 public 文件夹中创建一个 图标 文件夹,其中包含我们应用程序的许多图标,以及终端上的一些JSON ,我们将粘贴到清单中的图标数组中。

我们清单中的图标数组应如下所示:

"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 事件。在此之前,我们需要创建一个数组来存储我们在应用程序中使用的所有资产。

const assets = [
  "/",
  "css/style.css",
  "js/app.js",
  "/images/blog1.jpg",
  "/images/blog2.jpg",
  "/images/blog3.jpg,"
];

然后,我们将监听 install 事件来注册并将我们的静态文件保存到浏览器的缓存中。此过程需要一些时间才能完成。要跳过等待,我们将使用 skipWaiting()。

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 更新时清除缓存以删除旧资产。为此,我们将收听下面的 激活 代码片段:

...
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())
  );
});

在上面的代码片段中,我们在 service worker 上使用了 waitUntil 方法。此方法等待操作完成,然后在删除之前检查我们尝试清除的资产是否是我们当前应用程序的资产。

接下来,我们需要存储在缓存中的文件才能使用它们。

self.addEventListener("fetch", function (evt) {
  evt.respondWith(
    fetch(evt.request).catch(() => {
      return caches.open(BLOGGER_ASSETS).then((cache) => {
        return cache.match(evt.request);
      });
    })
  );
})

当在页面上发出请求时,PWA 将检查我们的缓存并从缓存中读取数据,而不是去网络。 然后,使用 respondWith 方法,我们覆盖浏览器的默认值并让我们的事件返回一个 Promise。 缓存完成后,我们可以返回evt.request对应的缓存。 当缓存准备好后,我们可以返回匹配 evt.request 的缓存。

我们已经成功设置了我们的 service worker。 现在让我们让它在我们的应用程序中可用。

注册 Service Worker

现在让我们在public/js/app.js 文件中注册我们的 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));
  });
}

在这里,我们检查我们的应用程序的浏览器是否支持Service Worker(当然不是所有的浏览器都支持Service Worker),然后注册我们的Service Worker 文件。

现在使用以下命令运行应用程序:

npm start

在浏览器中转到 localhost:8000 以访问该应用程序。

谷歌灯塔检查

现在让我们检查一下我们是否使用Google Lighthouse 检查正确设置了 PWA 。右键单击浏览器并选择“检查”。在检查选项卡上,选择灯塔并单击生成报告。如果您的应用程序一切顺利,您应该会看到如下屏幕截图中的输出:

我们已经成功地创建了我们的第一个应用程序。你还可以停止服务器以在离线模式下测试应用程序。

结论

渐进式 Web 应用 (PWA) 使用现代 API 通过单个代码库提供增强的功能、可靠性和可安装性。它们允许您的最终用户使用您的应用程序,无论他们是否有互联网连接。您应该随时 fork 存储库 并向项目添加其他功能。

原文标题:Building Offline-First Apps With Node.js and SQLite

原文作者: Clara Ekekenta