在本文中,您将学习如何构建一个 GitHub 跟踪器,当跟踪的存储库中有新的问题/PR 时,它会通过发送推送通知来通知用户。

如果您选择加入,GitHub 已经通过电子邮件发送通知,但许多研究表明,推送通知比电子邮件更能吸引用户。按照本教程构建 GitHub 跟踪器后,您将学会如何:

  • 添加服务工作者并将跟踪器转换为PWA
  • 订阅推送通知
  • 使用GitHub API
  • 通过Vercel云函数发送推送事件
  • 使用EasyCron定期获取新问题

先决条件

阅读本文需要一些技能和服务:

  • 安装了Node.js和 npm
  • 先前的Svelte知识
  • 一个免费的GitHub帐户,因为我们使用的是GitHub API
  • 一个免费的MongoDB Atlas帐户,用于在云中使用 MongoDB
  • 用于部署应用程序和云功能的免费Vercel帐户

什么是推送通知?

让我们来看看这些所谓的“推送通知”是什么。

您必须熟悉定期通知。这些是出现在屏幕上的小气泡文本,用于通知您某些事情。推送通知类似,只是它们不是按需生成的,而是在接收推送事件时生成的。推送通知在应用程序关闭时起作用,而常规通知要求您打开应用程序。

现代 Web 浏览器(如 Chrome)通过使用称为服务工作者的东西支持推送通知。Service Worker 是独立于浏览器主线程运行的一小段 JavaScript,因此,如果您的应用程序安装为 PWA(渐进式 Web 应用程序),则可以离线运行。

推送通知用于聊天应用程序中以在用户有未读消息时通知用户,在游戏中,通知用户游戏事件,在新闻站点中,通知用户突发文章,以及用于许多其他目的。

在您的应用程序中显示推送通知有四个步骤:

  1. 请求许可window.Notification.requestPermission()
  2. 将您的应用程序转换为游戏 PWA 并安装它
  3. 订阅推送事件
  4. 收到推送事件后,发送通知

第 1 步:创建跟踪器

让我们在本文中使用 Svelte 和Vite.js,而不是 Rollup。顾名思义,Vite 比 Rollup 更快,并且还提供了对环境变量的内置支持。要使用 Svelte 和 Vite 创建新项目,请运行以下命令:

npm init vite

选择要成为的框架svelte。如果你愿意,你可以使用 TypeScript。我将使用常规的 JavaScript。

接下来,cd进入项目文件夹,您可以将TailwindCSS添加到您的应用程序并使用以下命令安装所有依赖项:

npx svelte-add tailwindcss# Install packages
yarn install # or npm install

最后,在您喜欢的代码编辑器中打开项目并在http://localhost:3000npm run dev上运行或yarn dev启动应用程序。

跟踪器将如何工作

我们将使用 GitHub API 获取用户跟踪的存储库的问题列表和拉取请求。用户跟踪的存储库及其用户名将存储在 MongoDB 数据库中。

第一步是提示用户输入他们的用户名。Create 
src/lib/UsernamePrompt.svelte,它将是执行此操作的组件。这是我的表单 UI,但您可以根据需要进行设计:

<script>let username = "";async function submit() {// TODO}
</script><formon:submit|preventDefault="{submit}"class="mx-auto min-w-[350px] max-w-[1100px] w-[50%] border border-gray-500 rounded my-4 px-6 py-4"
><h1 class="text-center text-3xl m-4">Enter a username</h1><p class="text-center text-xl m-4">Enter a username to use this tracker</p><inputtype="text"class="rounded px-4 py-2 border border-gray-300 w-full outline-none"placeholder="Username"aria-label="Username"bind:value="{username}"/><buttonclass="mt-4 border border-transparent bg-blue-500 text-white rounded px-4 py-2 w-full">Submit</button>
</form>

像这样添加这个组件App.svelte:

<script>import UsernamePrompt from "./lib/UsernamePrompt.svelte";
</script><UsernamePrompt />

接下来,让我们添加主动跟踪器 UI。创建文件src/lib/Tracker.svelte并在其中添加以下代码:

<script>let repo = "";function track() {// TODO}function untrack(repo) {// TODO}
</script><formon:submit|preventDefault={track}class="mx-auto min-w-[350px] max-w-[1100px] w-[50%] border border-gray-500 rounded my-4 px-6 py-4"
><h1 class="text-center text-3xl m-4">GitHub tracker</h1><inputtype="text"class="rounded px-4 py-2 border border-gray-300 w-full outline-none"placeholder="Enter the repository's URL"aria-label="Repository URL"bind:value={repo}/><buttonclass="mt-2 border border-transparent bg-blue-500 text-white rounded px-4 py-2 w-full">Track repository</button><h2 class="mt-4 text-2xl">Tracked repositories</h2><ul class="m-2 list-decimal"><!-- We'll use a loop to automatically add repositories here later on. --><li class="py-1 flex items-center justify-between"><a class="text-gray-500 hover:underline" href="https://github.com/test/test">https://github.com/test/test</a><button class="text-red-500 cursor-pointer" on:click={() => untrack("")}>Untrack</button></li></ul>
</form>

要测试您的组件,请暂时将组件换成UsernamePrompt新Tracker组件App.svelte:

<script>// import UsernamePrompt from "./lib/UsernamePrompt.svelte";import Tracker from "./lib/Tracker.svelte";
</script><!-- <UsernamePrompt /> -->
<Tracker />

您的屏幕现在应该如下所示:

注意:记得恢复App.svelte到之前的代码!

第 2 步:设置云功能

我们需要有一个后端服务器来向我们的应用程序发送推送事件。这意味着您需要创建一个新的(可能)ExpressJS 项目,然后单独部署它。对于刚刚尝试推送通知的人来说,这将是一件令人头疼的事情。

Vercel 云函数来救援!云功能就像 Express 路由。他们可以运行代码并在您获取其 URL 时给您响应。Vercel 支持云功能;您只需要在api文件夹中创建文件。您将使用云功能与 MongoDB 进行交互,因为在客户端公开秘密从来都不是一件好事。

首先,确保您在MongoDB Atlas中有一个集群。MongoDB 有一个免费计划 ( M0),所以如果您还没有,请务必创建一个。现在,转到Atlas 仪表板侧栏中的Database Access选项卡。通过单击右侧的绿色按钮添加新的数据库用户。输入用户的详细信息(不要忘记密码),然后创建用户。

要连接到数据库,您需要连接字符串。将新用户和密码保存在某处,然后前往集群概览。单击右侧的连接按钮,然后选择连接您的应用程序作为连接方法。您应该看到一个类似于下面的连接字符串。

现在您有了连接字符串,您可以连接到数据库,但首先,您需要将当前应用程序部署到Vercel。最简单的方法是使用GitHub。

创建一个新的GitHub 存储库并将您的代码推送到其中。接下来,前往您的Vercel 仪表板并单击“新建项目”按钮。导入您的 GitHub 存储库,确保框架为Vite,并添加一个名为MONGODB_URL. 将其值设置为 MongoDB 数据库的连接字符串。

部署网站后,您需要将本地开发命令从 更改yarn dev为vercel dev. 运行命令后,如果系统要求您链接到现有项目,请单击yes

npm i -g vercel注意:如果您还没有安装 Vercel CLI,请确保安装。

vite像我一样,如果您在使用with时遇到问题vercel dev,请务必将项目的Development Command更改为Vercel Dashboardvite --port $PORT中的from 。vite

这将允许我们在本地使用具有正确环境变量的云函数。

让我们添加一个帮助文件,它允许我们在不打开太多连接的情况下访问 MongoDB。创建文件api/_mongo.js并将以下代码放入其中。api目录中以 a 为前缀的文件_不会视为云函数。这允许我们在单独的文件中添加助手和其他逻辑:

const { MongoClient } = require("mongodb");const mongo = new MongoClient(process.env.MONGODB_URL);// Export the connection promise
export default mongo.connect();

导出连接承诺而不是主客户端本身将防止我们拥有冗余连接,因为我们在无服务器平台中工作。

使用 CommonJS 而不是 ESModules

注意我是如何使用require而不是import? 这是因为,在撰写本文时,Vercel Cloud Functions支持 JavaScript 文件中的 ESModuleimport语句。相反,您需要使用 CommonJSrequire语句。

这里有一个问题。如果您看到package.json我们应用程序的 ,您会注意到它有一行"type": "module"。这意味着项目中的每个 JavaScript 文件都是一个 EsModule。这不是我们想要的,所以要将api目录中的所有文件都标记为 CommonJS 文件,这样我们就可以使用require语句,在其中创建api/package.json并添加这一行:

{"type": "commonjs"
}

这将允许我们在目录中使用require语句。api使用以下命令安装 MongoDB 连接驱动程序:

# Don't forget to CD!
cd api
npm i mongodb # or use yarn

第 3 步:添加功能

到目前为止,跟踪器并没有真正起作用,所以让我们解决这个问题。

验证

对于身份验证,我们需要将用户输入的用户名存储在 MongoDB 数据库中。

创建一个文件/api/storeusername.js。这将是一个云功能,并将映射到
http://localhost:3000/api/storeusername. 将以下代码放入其中:

const mongoPromise = require("../src/lib/mongo");
// All cloud functions must export a function that takes a req and res object.
// These objects are similar to their express counterparts.
module.exports = async (req, res) => {// TODO
};

接下来,像这样获取 MongoDB 客户端:

module.exports = async (req, res) =>// Wait for the client to connectconst mongo = await mongoPromise;
}

username从请求的正文中提取:

// ...
const { username } = req.body;// Check if the username is valid
if (typeof username !== "string" || !username.trim()) {res.status(400).json({ message: "Please send the username" });return;
}

接下来,您需要将此用户名存储在数据库中:

// Get the collection
const usersCol = mongo.db().collection("users");
// Check if the username already exists in the database
if (await usersCol.findOne({ _id: username })) {res.status(400).json({ message: "User already exists!" });return;
}
// We want the username to be the identifier of the user
await usersCol.insertOne({ _id: username });// Everything went well :)
res.status(200).json({ message: "Username recorded" });

最后,这是api/storeusername.js文件的外观:

const mongoPromise = require("./_mongo");module.exports = async (req, res) => {const mongo = await mongoPromise;const { username } = req.body;if (typeof username !== "string" || !username.trim()) {res.status(400).json({ message: "Please send the username" });return;}// Get the collectionconst usersCol = mongo.db().collection("users");// Check if the username already exists in the databaseif (await usersCol.findOne({ _id: username })) {res.status(400).json({ message: "User already exists!" });return;}// We want the username to be the identifier of the userawait usersCol.insertOne({ _id: username });// Everything went well :)res.status(200).json({ message: "Username recorded" });
};

使用 或通过推送到 GitHub 将您的应用程序部署到 Vercel vercel .,您的无服务器功能应该是实时的!您可以通过以下命令使用 cURL 对其进行测试:

curl -X POST -H "Content-Type: application/json" -d '{"username": "test"}' https://your-app.vercel.app/api/storeusername

users这应该在集合中创建一个新文档,其_id字段是我们刚刚提供的用户名。

现在剩下的就是在前端获取这个函数。在
src/lib/UsernamePrompt.svelte,在submit函数中,首先需要向云函数发送请求,然后将用户名放入localStorage,这样我们就知道用户通过了身份验证。您可以使用以下功能发送请求fetch:

async function submit() {const res = await fetch("/api/storeusername", {body: JSON.stringify({ username }),headers: {"Content-Type": "application/json",},method: "POST",});const data = await res.json();if (!res.ok) alert(data.message);else {// Store the username in localStoragelocalStorage.setItem("username", username);// Reload the pagewindow.location.reload();}
}

我们正在重新加载页面,因为在 中App.svelte,当页面加载时,我们需要检查 中是否有用户名localStorage。如果有,我们可以跳过UsernamePrompt屏幕。为此,请将此代码添加到的script标记中App.svelte:

<script>import { onMount } from "svelte";import UsernamePrompt from "./lib/UsernamePrompt.svelte";import Tracker from "./lib/Tracker.svelte";let isLoggedIn = false;onMount(() => {// If there is a username in the localStorage, set isLoggedIn to trueisLoggedIn = !!localStorage.getItem("username");});
</script>

上面的代码将检查localStorage用户名并设置isLoggedIn为true是否存在。接下来,我们要做的就是更新 DOM。在 的script标记下App.svelte,添加以下内容:

{#if !isLoggedIn}
<UsernamePrompt />
{:else}
<Tracker />
{/if}

跟踪和取消跟踪存储库

现在让我们为跟踪器的实际跟踪功能添加功能。如果您打开Tracker.svelte,您会注意到有两个功能 -track()和untrack(). 这些函数应该通过将存储库添加到数据库来分别跟踪和取消跟踪存储库。

但在此之前,您需要添加更多的云功能。一个用于跟踪存储库,另一个用于取消跟踪,最后一个用于获取用户跟踪的存储库。

让我们一一研究它们。

跟踪存储库

创建文件api/trackrepo.js. 这将映射到/api/trackrepo:

const mongoPromise = require("./_mongo");module.exports = async (req, res) => {const mongo = await mongoPromise;// TODO
};

当用户想要跟踪存储库时,他们将向POST此函数发送一个请求,其中包含存储库的名称和他们在正文中的用户名。该函数将在集合的trackedRepos字段中添加存储库的名称。users添加一些代码以从正文中获取这些字段:

const { username, repo } = req.body;
if (typeof username !== "string" || typeof repo !== "string") {res.status(400).json({ message: "Invalid body" });return;
}

最后,通过将存储库添加到数据库来添加代码以跟踪存储库:

// Get the users collection
const usersCol = mongo.db().collection("users");
let user = await usersCol.findOne({ _id: username });
if (!user) {res.status(400).json({ message: "User not found" });
}
// Add repository to user's tracked repositories
user.trackedRepos = !user.trackedRepos ? [repo] : [...user.trackedRepos, repo];
// Helps avoid duplicates
user.trackedRepos = [...new Set(user.trackedRepos)];
// Save changes to DB
user = await usersCol.updateOne({ _id: username }, { $set: user });
res.status(200).json({ user });

这api/trackrepo.js应该是这样的:

const mongoPromise = require("./_mongo");module.exports = async (req, res) => {const mongo = await mongoPromise;const { username, repo } = req.body;if (typeof username !== "string" || typeof repo !== "string") {res.status(400).json({ message: "Invalid body" });return;}const usersCol = mongo.db().collection("users");const user = await usersCol.findOne({ _id: username });if (!user) {res.status(400).json({ message: "User not found" });}user.trackedRepos = !user.trackedRepos? [repo]: [...user.trackedRepos, repo];user.trackedRepos = [...new Set(user.trackedRepos)];await usersCol.updateOne({ _id: username }, { $set: user });res.status(200).json({ user });
};

现在是时候在跟踪器中使用此功能了。打开src/lib/Tracker.svelte并将track()函数更改为:

function track() {// If there is no username, reload the page and end the functionif (!localStorage.getItem("username")) return window.location.reload();fetch("/api/trackrepo", {body: JSON.stringify({ username: localStorage.getItem("username"), repo }),headers: { "Content-Type": "application/json" },method: "POST",}).then(async (r) => {// Return the data and the response itselfreturn { r, data: await r.json() };}).then(({ r, data }) => {if (!r.ok) alert(data.message);else console.log("Repository tracked");});trackedRepos = [...trackedRepos, repo];repo = "";
}

现在,当您在输入中输入存储库并单击Track时,它应该会保存在数据库中。

取消跟踪存储库

让我们添加一个云函数来取消跟踪存储库。创建文件api/untrackrepo.js. 这将映射到/api/untrackrepo:

const mongoPromise = require("./_mongo");module.exports = async (req, res) => {const mongo = await mongoPromise;// TODO
};

这个云函数的请求体将与函数的请求体相同trackrepo——用户username和repo:

const { username, repo } = req.body;
if (typeof username !== "string" || typeof repo !== "string") {res.status(400).json({ message: "Invalid body" });return;
}

接下来,这里是从用户删除存储库的代码trackedRepos:

const usersCol = mongo.db().collection("users");
const user = await usersCol.findOne({ _id: username });
if (!user) {res.status(400).json({ message: "User not found" });
}
if (!Array.isArray(user.trackedRepos)) {user.trackedRepos = [];
} else {// Remove the repo from the user's list of tracked repos.user.trackedRepos = user.trackedRepos.filter((r) => r !== repo);
}
// Save changes
await usersCol.updateOne({ _id: username }, { $set: user });
res.status(200).json({ user });

这api/untrackrepo.js应该是这样的:

const mongoPromise = require("./_mongo");module.exports = async (req, res) => {const mongo = await mongoPromise;const { username, repo } = req.body;if (typeof username !== "string" || typeof repo !== "string") {res.status(400).json({ message: "Invalid body" });return;}const usersCol = mongo.db().collection("users");const user = await usersCol.findOne({ _id: username });if (!user) {res.status(400).json({ message: "User not found" });}if (!Array.isArray(user.trackedRepos)) {user.trackedRepos = [];} else {user.trackedRepos = user.trackedRepos.filter((r) => r !== repo);}await usersCol.updateOne({ _id: username }, { $set: user });res.status(200).json({ user });
};

现在是时候在前端使用这个云功能了。在 的untrack()函数中src/lib/Tracker.svelte,添加以下代码:

function untrack(repo) {// If there is no username, reload the page and end the functionif (!localStorage.getItem("username")) return window.location.reload();fetch("/api/untrackrepo", {body: JSON.stringify({ username: localStorage.getItem("username"), repo }),headers: { "Content-Type": "application/json" },method: "POST",}).then(async (r) => {// Return the data and the response itselfreturn { r, data: await r.json() };}).then(({ r, data }) => {if (!r.ok) alert(data.message);else console.log("Repository untracked");});trackedRepos = trackedRepos.filter((r) => r !== repo);
}

您会注意到它与track()函数非常相似,因为它实际上是相同的;只是网址已更新。你现在还不能真正测试它,因为我们没有显示跟踪存储库的列表,所以让我们修复它。

列出跟踪的存储库

这部分非常简单。您只需要从数据库中获取用户跟踪的存储库并将其显示在前端。创建一个云函数api/listrepos.js并向其中添加以下代码:

const mongoPromise = require("./_mongo");module.exports = async (req, res) => {const mongo = await mongoPromise;const username = req.query.username;if (typeof username !== "string" || !username.trim()) {res.status(401).json({ message: "Please send `username` in the querystring." });return;}const usersCol = mongo.db().collection("users");const user = await usersCol.findOne({ _id: username });if (!user) {res.status(400).json({ message: "User not found" });}let repositories = [];if (Array.isArray(user.trackedRepos)) {repositories = user.trackedRepos;}res.status(200).json({ repositories });
};

由于将使用 HTTPGET请求调用云函数,因此您不能在其中放入正文,因此我们使用查询字符串来传递用户名;由于user.trackedReposcan be null,我们确保返回一个数组。接下来,是时候在前端使用这个云功能了!创建一个在文件中async调用的函数。该函数将负责使用我们刚刚创建的云函数从数据库中获取用户跟踪的存储库:
fetchRepossrc/lib/Tracker.svelte

async function fetchRepos() {// If there is no username, reload the page and end the functionif (!localStorage.getItem("username")) return window.location.reload();const res = await fetch("/api/listrepos?username=" + localStorage.getItem("username"));const data = await res.json();if (!res.ok) alert(data.message);else return data.repositories;
}

当组件被挂载时,我们需要获取这个函数。这可以使用onMountSvelte 中的钩子来完成。当组件挂载时,我想将上述函数的返回值设置为一个名为 的变量trackedRepos,这样我们就可以在 DOM 中使用它了:

import { onMount } from "svelte";let trackedRepos = [];
onMount(async () => {trackedRepos = await fetchRepos();
});

现在我们可以访问用户的跟踪存储库,让我们更新 HTML 模板Tracker.svelte以显示跟踪存储库的准确列表:

<!-- ... --><ul class="m-2 list-decimal">{#each trackedRepos as repo}<li class="py-1 flex items-center justify-between"><a class="text-gray-500 hover:underline" href="https://github.com/{repo}">https://github.com/{repo}</a><button class="text-red-500 cursor-pointer" on:click={() => untrack(repo)}>Untrack</button></li>{/each}</ul>
<!-- ... -->

我们仍然需要重新加载页面才能看到任何更改。让我们通过每次单击track或untrack按钮时更新 DOM 来解决这个问题:

function track() {// ...trackedRepos = [...trackedRepos, repo];repo = "";
}function untrack(repo) {// ...trackedRepos = trackedRepos.filter((r) => r !== repo);
}

这是Tracker.svelte应该的样子:

<script>import { onMount } from "svelte";let trackedRepos = [];onMount(async () => {trackedRepos = await fetchRepos();});async function fetchRepos() {if (!localStorage.getItem("username")) return window.location.reload();const res = await fetch("/api/listrepos?username=" + localStorage.getItem("username"));const data = await res.json();if (!res.ok) alert(data.message);else return data.repositories;}let repo = "";function track() {if (!localStorage.getItem("username")) return window.location.reload();fetch("/api/trackrepo", {body: JSON.stringify({ username: localStorage.getItem("username"), repo }),headers: { "Content-Type": "application/json" },method: "POST"}).then(async r => {// Return the data and the response itselfreturn { r, data: await r.json() };}).then(({ r, data }) => {if (!r.ok) alert(data.message);else console.log("Repository tracked");});trackedRepos = [...trackedRepos, repo];repo = "";}function untrack(/** @type string*/ repo) {// If there is no username, reload the page and end the functionif (!localStorage.getItem("username")) return window.location.reload();fetch("/api/untrackrepo", {body: JSON.stringify({ username: localStorage.getItem("username"), repo }),headers: { "Content-Type": "application/json" },method: "POST"}).then(async r => {// Return the data and the response itselfreturn { r, data: await r.json() };}).then(({ r, data }) => {if (!r.ok) alert(data.message);else console.log("Repository untracked");});trackedRepos = trackedRepos.filter(r => r !== repo);}
</script><formon:submit|preventDefault={track}class="mx-auto min-w-[350px] max-w-[1100px] w-[50%] border border-gray-500 rounded my-4 px-6 py-4"
><h1 class="text-center text-3xl m-4">GitHub tracker</h1><inputtype="text"class="rounded px-4 py-2 border border-gray-300 w-full outline-none"placeholder="Enter the repository's URL"aria-label="Repository URL"bind:value={repo}/><buttonclass="mt-2 border border-transparent bg-blue-500 text-white rounded px-4 py-2 w-full">Track repository</button><h2 class="mt-4 text-2xl">Tracked repositories</h2><ul class="m-2 list-decimal">{#each trackedRepos as repo}<li class="py-1 flex items-center justify-between"><a class="text-gray-500 hover:underline" href="https://github.com/{repo}">https://github.com/{repo}</a><button class="text-red-500 cursor-pointer" on:click={() => untrack(repo)}>Untrack</button></li>{/each}</ul>
</form>

这是应用程序现在应该如何显示的屏幕截图。

第 4 步:使应用程序可安装

推送通知仅在已安装的应用程序上受支持。是的,您可以使用支持的浏览器(即 Chrome 和其他基于 Chromium 的浏览器)将 Web 应用程序安装为常规应用程序。

要使应用程序可安装,您需要将其转换为渐进式网络应用程序。这是一个三步过程:

  1. 添加服务人员
  2. 让您的应用离线工作
  3. 添加manifest.json文件

如果所有三个步骤都完成,当您访问应用程序时,地址栏上会出现一个安装按钮。

添加服务工作者

服务工作者是可以在后台运行的 JavaScript 文件,脱离浏览器的主线程。这允许他们执行离线运行、在后台运行和下载大文件等操作。它们主要用于缓存请求和监听事件,我们将做这两件事。

要添加服务工作者,您需要添加一个公开可用的 JavaScript 文件,就像任何 CSS 文件一样。名称并不重要,但通常命名为service-worker.jsor sw.js。这个文件应该像你的 CSS 一样公开提供,所以把它放在public目录中。

服务工作者通过监听事件来工作。对于缓存文件,以便您的应用程序离线工作,您将监听install,activate和fetch事件。install安装服务工作者时调用该事件。activate当服务工作者运行时调用该事件,并且每当fetch发出网络请求时调用该事件。可以使用添加事件侦听器self.addEventListener()。让我们创建一个public/service-worker.js文件并向其中添加以下代码:

self.addEventListener("install", (event) => {console.log("Installed SW");
});self.addEventListener("activate", (event) => {console.log("Activated SW");
});self.addEventListener("fetch", (event) => {console.log("Fetch:", event.request);
});

剩下的就是注册这个 service worker。我们将在 的onMount函数中做到这一点App.svelte。在内部回调的末尾添加此代码onMount:

if ("serviceWorker" in navigator) {// Service worker supportednavigator.serviceWorker.register("/service-worker.js");
}

上面的代码首先在浏览器中检查 Service Worker 的支持,然后注册我们的 Service Worker。需要注意的是,register()函数中的路径是相对于您的域的路径,而不是相对于项目文件夹的路径——这意味着服务工作者应该可以在 访问
http://localhost:3000/service-worker.js,因为它在public目录中。

现在,如果您重新加载页面并打开控制台,您应该会看到上述消息。

让我们的应用离线工作

要使应用离线工作,您需要使用服务工作者缓存其内容。由于我们的应用程序向云功能发出请求,因此在没有网络的情况下,它实际上并不能做太多事情。因此,让我们显示一个指示我们处于离线状态的页面,而不是显示应用程序的缓存、无功能版本。创建一个public/offline.html文件并将以下代码放入其中:

<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8" /><meta http-equiv="X-UA-Compatible" content="IE=edge" /><meta name="viewport" content="width=device-width, initial-scale=1.0" /><title>You're offline</title></head><body><h1>You're offline</h1><p>This app doesn't work while offline. Please check your network</p></body>
</html>

您可以随意自定义此页面。您现在需要缓存此页面。缓存也是一个三步过程,它使用了我们监听的上述三个 service worker 事件。以下是它的工作原理:

  1. 打开缓存并使用 将所需路由添加到缓存中cache.add。这发生在install.
  2. 旧的缓存被删除,因此只有最新的缓存被保存到用户的计算机上。这使用了更少的存储空间。这发生在activate.
  3. 我们拦截任何网络请求并检查这些请求是否是页面导航——即更改路由。如果请求成功,一切都很好,但如果请求失败,我们将offline.html要显示的页面交付给用户。这发生在fetch.

让我们实现第一步。打开 service worker 文件并更改install事件的处理程序,如下所示:

let CACHE_NAME = "cache-" + Date.now();self.addEventListener("install", event => {console.log("Installed SW");event.waitUntil(caches.open(CACHE_NAME).then(cache => {return cache.add("/offline.html");}););self.skipWaiting();
});

event.waitUntil()是一个类似于await关键字的函数。的回调addEventListener不能是异步的,因此要实现该功能,我们应该使用event.waitUntil()并向其传递一个 Promise,以便等待该 Promise。

self.skipWaiting()告诉浏览器我们已经完成了这个install过程,所以激活 service worker。说到activate,现在让我们添加代码以删除所有旧缓存:

self.addEventListener("activate", (event) => {console.log("Activated SW");event.waitUntil(// Loop through the cachecaches.keys().then((keys) => {// We must return a promise so it gets awaitedreturn Promise.all(keys.map((k) => {// If the key doesn't match the name of the current cache, delete itif (k !== CACHE_NAME) return caches.delete(k);}));}));
});

有了这个,offline.html页面应该被缓存。要仔细检查,请通过按下F12并选择应用程序选项卡打开开发人员工具。在侧边栏上,应该有一个缓存存储选项卡。点击它,你应该注意到/offline.html。

现在剩下要做的就是在没有网络时提供这个文件:

self.addEventListener("fetch", (event) => {console.log("Fetch:", event.request);// We only want to send /offline.html when the user is navigating pages,// not when they're requesting something else like CSS files or API requests.if (event.request.mode !== "navigate") return;event.respondWith(fetch(event.request).catch(() => {return caches.open(CACHE_NAME).then((cache) => {return cache.match("offline.html");});}));
});

该函数将使用传递给它event.respondWith()的任何对象响应网络获取请求。Response在这种情况下,我们首先获取请求,如果请求失败,这很可能是由于互联网问题,我们将发送offline.html页面,该页面由服务工作者缓存。

现在刷新页面并关闭 Wi-Fi 或以太网。刷新时,您现在应该会看到我们的离线页面,而不是默认的 chrome “无网络”页面。不幸的是,这个离线页面没有恐龙游戏,但它确实使我们能够将应用程序安装为 PWA。

以下是 service worker 的外观:

let CACHE_NAME = "cache-" + Date.now();self.addEventListener("install", (event) => {console.log("Installed SW");event.waitUntil(caches.open(CACHE_NAME).then((cache) => {return cache.add("/offline.html");}));self.skipWaiting();
});self.addEventListener("activate", (event) => {console.log("Activated SW");event.waitUntil(// Loop through the cachecaches.keys().then((keys) => {// We must return a promise so it gets awaitedreturn Promise.all(keys.map((k) => {// If the key doesn't match the name of the current cache, delete itif (k !== CACHE_NAME) return caches.delete(k);}));}));
});self.addEventListener("fetch", (event) => {console.log("Fetch:", event.request);// We only want to send /offline.html when the user is navigating pages,// not when they're requesting something else like CSS files or API requests.if (event.request.mode !== "navigate") return;event.respondWith(fetch(event.request).catch(() => {return caches.open(CACHE_NAME).then((cache) => {return cache.match("offline.html");});}));
});

添加manifest.json文件

或 Web 清单包含有关您的应用程序的manifest.json一些有用信息——例如应用程序的名称、主题颜色、描述、图标等等。该文件通常被调用manifest.json,并且必须使用 HTML 中的标签链接到您的网站<link>,就像链接 CSS 文件的方式一样。让我们为我们的应用程序添加一个清单。随意为此使用生成器:

{"background_color": "#ffffff","description": "Notifies you of new issues and PRs in GitHub repositories","display": "standalone","icons": [{"src": "/icons/icon-128x128.png","sizes": "128x128","type": "image/png"},{"src": "/icons/icon-144x144.png","sizes": "144x144","type": "image/png"},{"src": "/icons/icon-152x152.png","sizes": "152x152","type": "image/png"},{"src": "/icons/icon-192x192.png","sizes": "192x192","type": "image/png"},{"src": "/icons/icon-256x256.png","sizes": "256x256","type": "image/png"},{"src": "/icons/icon-512x512.png","sizes": "512x512","type": "image/png"},{"src": "/icons/maskable_icon.png","sizes": "640x640","type": "image/png","purpose": "any maskable"}],"name": "GitHub Tracker","orientation": "portrait","short_name": "GH Tracker","start_url": "/","theme_color": "#000000"
}

您需要为该应用程序下载一堆图标。这些图标大小不同,被不同的操作系统使用。您可以从源代码存储库或使用此链接下载它们。请务必将 ZIP 文件解压缩为public/icons.

接下来,您需要将清单和图标添加到index.html文件中。您可以通过将以下代码放入其中来做到这一点:

<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8" /><link rel="icon" href="/favicon.ico" /><meta name="viewport" content="width=device-width, initial-scale=1.0" /><title>GitHub Tracker</title><metaname="description"content="Tracks GitHub repositories for new Issues/PRs and send you notifications"/><link rel="manifest" href="/manifest.json" /><meta name="description" content="Svelte PWA starter template" /><meta name="theme-color" content="#333333" /><meta name="apple-mobile-web-app-capable" content="yes" /><meta name="apple-mobile-web-app-status-bar-style" content="black" /><meta name="apple-mobile-web-app-title" content="Svelte PWA" /><link rel="apple-touch-icon" href="/icons/icon-152x152.png" /></head><body><div id="app"></div><script type="module" src="/src/main.js"></script></body>
</html>

F12通过按下并前往Lighthouse选项卡打开 Chrome 的开发人员工具并创建一个新的审计。您现在应该在 PWA 部分获得“可安装”分数。这意味着您已经成功地将您的网站转换为 web 应用程序,您现在可以通过单击地址栏上的按钮来安装它。

第 5 步:订阅推送通知

在我们发送推送通知之前,我们需要获得用户的许可。您可以使用该
Notification.requestPermission()方法来执行此操作。此方法是异步的,并返回一个可以等于,和的字符串。当用户分别在通知提示上按下X、按下Deny或按下Allow时,将返回这些信息。我们将使用hook in来调用这个函数:defaultdeniedgrantedonMountApp.svelte

onMount(async () => {isLoggedIn = !!localStorage.getItem("username");if ("serviceWorker" in navigator) {navigator.serviceWorker.register("/service-worker.js");}const status = await Notification.requestPermission();if (status !== "granted")alert("Please allow notifications to make sure that the application works.");
});

您现在应该会看到一个弹出窗口,要求您在应用程序中允许通知。现在我们有了发送通知的权限,让我们使用 service worker 订阅推送事件。这可以使用pushManager.subscribe()服务工作者的功能来完成。您可以在 service worker 本身中执行此操作,也可以在将 service worker 注册到App.svelte. 我会选择后者,所以如果你想这样做,只需用下面的代码替换
navigator.serviceWorker.register函数:onMount

navigator.serviceWorker.register("/service-worker.js");
const reg = await navigator.serviceWorker.ready;
reg.pushManager.subscribe({ userVisibleOnly: true });

如果您打开控制台,您会注意到一个错误,指出applicationServerKey缺少 。推送通知需要服务器向它们发送推送消息,并且这些服务器使用 VAPID 密钥进行身份验证。这些密钥标识服务器并让浏览器知道推送消息是有效的。我们将使用 Vercel Cloud Functions 发送推送消息,因此我们需要对其进行设置。

创建推送消息服务器

我们将使用web-push npm 包来帮助我们生成密钥并发送推送事件。要安装它,cd到api文件夹并运行以下命令:

npm i web-push

请记住到cd该api文件夹​,否则该web-push软件包将安装在 Svelte 应用程序中。

要发送推送通知,您需要生成一个公共和私有 VAPID 密钥对。为此,请使用node命令打开 Node REPL 并运行以下命令:

$ node
> const webPush = require("web-push");
> webPush.generateVAPIDKeys()
{publicKey: "XXXXXXX",privateKey: "XXXXXX"
}

复制这两个密钥并将它们作为环境变量存储在 Vercel 上。一定要称它们为令人难忘的东西,例如VAPID_PRIVATE_KEY和VAPID_PUBLIC_KEY。

现在,我们可以开始使用云功能了。创建文件api/vapidkeys.js. 该文件将负责向客户端发送公共VAPID 密钥。您永远不应该共享私有 VAPID 密钥。在api/vapidkeys.js中,首先我们需要初始化web-push:

const webPush = require("web-push");webPush.setVapidDetails("YOUR_VERCEL_DOMAIN",process.env.VAPID_PUBLIC_KEY,process.env.VAPID_PRIVATE_KEY
);

请务必替换YOUR_VERCEL_DOMAIN为您的 Vercel 应用程序的域。接下来,让我们导出一个函数,将公共 VAPID 密钥返回给请求者:

module.exports = (_, res) => {res.send(process.env.VAPID_PUBLIC_KEY);
};

完成后,您现在可以更新onMount函数App.svelte以首先获取云函数以获取公钥,然后在subscribe函数中使用公钥:

let sub;
onMount(async () => {// ...if ("serviceWorker" in navigator) {// Service worker supportednavigator.serviceWorker.register("/service-worker.js");const reg = await navigator.serviceWorker.ready;sub = await reg.pushManager.getSubscription();if (!sub) {// Fetch VAPID public keyconst res = await fetch("/api/vapidkeys");const data = await res.text();sub = await reg.pushManager.subscribe({userVisibleOnly: true,applicationServerKey: data,});}console.log(sub);}// ...
});

请注意,如果我们没有订阅推送通知,我们只会获取 VAPID 密钥。如果您打开控制台,您应该会看到订阅记录到控制台。

提供的端点对我们来说非常重要。该端点将允许我们使用 通知该用户web-push。让我们创建一个云函数来将此端点存储在数据库中。创建文件api/storeendpoint.js:

const mongoPromise = require("./_mongo");module.exports = async (req, res) => {const mongo = await mongoPromise;const usersCol = mongo.db().collection("users");// TODO
};

让我们subscription从正文中获取和用户名:

const { username, subscription } = req.body;
if (typeof username !== "string" || typeof subscription !== "object") {res.status(400).json({ message: "Invalid body" });return;
}

让我们将它添加到数据库中:

await usersCol.updateOne({ _id: username }, { $set: { subscription } });

以下是最终云函数的外观:

const mongoPromise = require("./_mongo");module.exports = async (req, res) => {const mongo = await mongoPromise;const usersCol = mongo.db().collection("users");const { username, subscription } = req.body;if (typeof username !== "string" || typeof subscription !== "string") {res.status(400).json({ message: "Invalid body" });return;}await usersCol.updateOne({ _id: username },{$set: {subsciption,},});res.status(204).end();
};

每次我们订阅推送通知时都应该调用这个函数。每当变量有值变量为真时,让我们使用Svelte 反应块调用此云函数。在标签结束之前添加此代码:subisLoggedIn<script>App.svelte

// ...
$: if (sub && isLoggedIn) {// Push notifs have been subscribed to, and there's a username in localStorageconst username = localStorage.getItem("username");fetch("/api/storeendpoint", {body: JSON.stringify({ username, subscription: sub.toJSON() }),headers: {"Content-Type": "application/json",},method: "POST",});
}

刷新页面,您应该会看到当前浏览器的推送端点和密钥都存储在subscription对象中的 MongoDB 数据库中。

您所要做的就是push在 service worker 中处理事件并创建一个云函数来检查 GitHub 是否有新问题和 PR。

让我们先做后者。创建一个新的云功能api/fetchgh.js。该函数将负责检查 GitHub 并发送推送通知:

const mongoPromise = require("./_mongo");
const webPush = require("web-push");webPush.setVapidDetails("YOUR_VERCEL_DOMAIN",process.env.VAPID_PUBLIC_KEY,process.env.VAPID_PRIVATE_KEY
);module.exports = async (req, res) => {const mongo = await mongoPromise;const usersCol = mongo.db().collection("users");
};

让我们从数据库中获取所有用户,这样我们就知道要获取哪些存储库:

const users = await usersCol.find().toArray();

接下来,创建两个变量来存储当前获取的存储库以及包含任何新问题或 PR 的存储库:

const alreadyFetchedRepos = [];
const reposWithIssues = [];

对于每个用户,让我们检查他们跟踪的存储库是否有任何新问题。为了确保一个存储库只被检查一次,我们将把存储库添加到alreadyFetchedRepos,我们会将任何有新问题的存储库添加到reposWithIssues. 为此,我们需要遍历users数组中的每个用户并获取要获取的存储库列表。这将通过检查它们trackedRepos是否有任何重复来完成。完成后,我们将为fetchRepo每个存储库调用该函数。fetchRepo将返回一个布尔值——true如果有新问题,false否则:

for await (let user of users) {// Make sure to fetch each repo ONCE.const reposToFetch = user.trackedRepos.filter((i) => !alreadyFetchedRepos.includes(i));await Promise.all(reposToFetch.map(async (repo) => {const hasNewIssues = await fetchRepo(repo, user._id);alreadyFetchedRepos.push(repo);if (hasNewIssues) reposWithIssues.push(repo);}));
}

由于fetchRepo将是异步的,因此我习惯于map每次都返回 Promise 并使用Promise.all. 这是有效的,因为for循环是异步的。如果没有等待承诺,变量可以是undefined,所以一定要等待承诺!

现在为fetchRepo功能。该函数将获取我们上次从数据库中检查 GitHub API 的时间。这是为了只从 GitHub 获取最新的问题。然后它会为任何新问题获取 GitHub API,如果有任何问题,则返回一个布尔值:

async function fetchRepo(repo) {const mongo = await mongoPromise;const fetchedCol = mongo.db().collection("fetched");const lastFetchedDoc = await fetchedCol.findOne({},{ sort: { createdAt: -1 } });const timestamp = lastFetchedDoc ? lastFetchedDoc.createdAt : null;const { data: issues } = await axios.get(`https://api.github.com/repos/${repo}/issues?state=open${timestamp ? "&since=" + timestamp : ""}`);if (Array.isArray(issues)) {await fetchedCol.insertOne({ createdAt: new Date() });}if (Array.isArray(issues) && issues.length > 0) return true;return false;
}

完成后,我们需要向任何跟踪有任何新问题的存储库的用户发送推送通知。这可以使用web-push. 将这些代码行添加到导出函数的末尾:

for await (let user of users) {// TODO
}

首先,我们需要检查用户跟踪的任何回购是否有新问题。这可以通过Array.some方法来完成。Array.some()确定指定的回调函数是否true为数组的任何元素返回,因此我们可以很容易地使用它来检查:

if (user.trackedRepos.some((i) => reposWithIssues.includes(i))) {// TODO
}

最后,我们发送通知:

await webPush.sendNotification(user.subscription);

以下是云功能的外观:

const mongoPromise = require("./_mongo");
const webPush = require("web-push");
const axios = require("axios");webPush.setVapidDetails("https://github-tracker-arnu515.vercel.com",process.env.VAPID_PUBLIC_KEY,process.env.VAPID_PRIVATE_KEY
);async function fetchRepo(repo) {const mongo = await mongoPromise;const fetchedCol = mongo.db().collection("fetched");const lastFetchedDoc = await fetchedCol.findOne({},{ sort: { createdAt: -1 } });const timestamp = lastFetchedDoc ? lastFetchedDoc.createdAt : null;const { data: issues } = await axios.get(`https://api.github.com/repos/${repo}/issues?state=open${timestamp ? "&since=" + timestamp : ""}`);if (Array.isArray(issues)) {await fetchedCol.insertOne({ createdAt: new Date() });}if (Array.isArray(issues) && issues.length > 0) return true;return false;
}module.exports = async (req, res) => {const mongo = await mongoPromise;const usersCol = mongo.db().collection("users");const users = await usersCol.find().toArray();const alreadyFetchedRepos = [];const reposWithIssues = [];for await (let user of users) {// Make sure to fetch each repo ONCE.const reposToFetch = user.trackedRepos.filter((i) => !alreadyFetchedRepos.includes(i));await Promise.all(reposToFetch.map(async (repo) => {const hasNewIssues = await fetchRepo(repo, user._id);alreadyFetchedRepos.push(repo);if (hasNewIssues) reposWithIssues.push(repo);}));}for await (let user of users) {// Send push notificationsif (user.trackedRepos.some((i) => reposWithIssues.includes(i))) {await webPush.sendNotification(user.subscription, "new-issues");}}// And we're done!res.status(200).json({ reposWithIssues, fetched: alreadyFetchedRepos });
};

监听push事件

剩下要做的就是监听push服务工作者中的事件。打开 service worker 并添加以下代码:

self.addEventListener("push", (event) => {console.log(event.data.text());
});

当您调用云函数时,可能使用 cURL,您应该new-issue会在浏览器控制台中看到已登录。这并不是很有帮助,所以让我们让它发送一个通知:

self.addEventListener("push", (event) => {// Double check the push eventif (event.data.text() === "new-issue") {event.waitUntil(self.registration.showNotification("New issues", {body: "One or more tracked repositories have new issues or pull requests.",}));}
});

从 MongoDB 中删除fetched集合并再次调用云函数。您现在应该会收到来自 Web 浏览器的通知。

使用或通过推送到 GitHub 部署应用程序vercel .,将应用程序安装为 PWA,然后通过转到运行云功能
https://YOUR_VERCEL_APP/api/fetchgh,即使您尚未打开应用程序,您也会收到通知!

如果您没有收到通知,或者您收到410网络推送错误,请确保forever在收到提示时允许提示中的通知。

第 6 步:创建 CRON 作业

如果我们必须手动调用云函数,那么跟踪器就不是真正的跟踪器,对吧?让我们使用EasyCron每小时自动调用一次云函数。

前往您的 EasyCron 仪表板并创建一个新的 CRON 作业。对于 URL,输入
https://YOUR_VERCEL_DOMAIN/api/fetchgh,然后选择一个时间间隔。我会每隔一小时进行一次,但可以随意自定义它。

结论

这样一来,每当您跟踪的任何存储库中出现新问题/公关时,您都应该收到通知。如果您遇到任何问题,请随时查看源代码或实时版本。

如果本文对你有帮助,别忘记给我个3连 ,点赞,转发,评论,,咱们下期见。

收藏 等于白嫖,点赞才是真情。

亲爱的小伙伴们,有需要JAVA面试文档资料请点赞+转发,关注我后,私信我333就可以领取免费资料哦

怎么构建有推送通知的 GitHub 跟踪器,你知道吗相关推荐

  1. go gorilla_使用gorilla websocket构建浏览器推送通知服务的低级设计

    go gorilla Singhania AdityaSinghania Aditya Follow跟随 Aug 31 8月31 gopher leaving everyone awestruck w ...

  2. swift通知栏推送_如何使用Swift使用推送通知构建食品交付应用

    swift通知栏推送 by Neo Ighodaro 由新Ighodaro 如何使用Swift使用推送通知构建食品交付应用 (How to build a food delivery app with ...

  3. swift通知栏推送_如何使用Swift和Laravel使用推送通知创建iOS加密跟踪应用

    swift通知栏推送 by Neo Ighodaro 由新Ighodaro 如何使用Swift和Laravel使用推送通知创建iOS加密跟踪应用 (How to create an iOS crypt ...

  4. firebase 推送_如何使用Firebase向Web应用程序添加推送通知?

    firebase 推送 by Leonardo Cardoso 由莱昂纳多·卡多佐(Leonardo Cardoso) 如何使用Firebase向Web应用程序添加推送通知? (How to add ...

  5. (转)iOS开发资源:推送通知相关开源项目--PushSharp、APNS-PHP以及Pyapns等

    PushSharp  (github) PushSharp是一个实现了由服务器端向移动客户端推送消息的开源C#库,支持 iOS (iPhone/iPad APNS). Android (C2DM/GC ...

  6. IOS开发之----远程推送通知

    原文地址:IOS开发之----远程推送通知作者:倒計時 玩了一年的iPhone了各种App的远程通知接收了不少,每次接收到的时候,就在反思,这丫的怎么实现的! 由于工作方面一直没有接触的机会,所以只好 ...

  7. android自定义push通知_20个海外Web和App推送通知服务工具

    在App和网站中使用推送通知有不同的原因,并且在提高流量和与客户互动方面有很多好处.推送通知是一种交互式可点击消息,可将访问者直接引导至你的网站.它们可以帮助你以指数方式增加流量和参与率.因此,营销人 ...

  8. Android推送通知指南(转)

    在开发Android和iPhone应用程序时,我们往往需要从服务器不定的向手机客户端即时推送各种通知消息,iPhone上已经有了比较简单的和完美的推送通知解决方案,可是Android平台上实现起来却相 ...

  9. android 推送的小图标,android - 推送通知中没有声音并且没有自定义的小图标 - 堆栈内存溢出...

    我已经使用Firebase Cloud Messaging实现了推送通知,除了几个自定义问题之外,其他所有功能都正常运行: 当我从Notification Composer工具发送测试通知时,清单中设 ...

最新文章

  1. 应用流量管理,新网络管理必修课
  2. 核显也能玩游戏,OS X Yosemite优化指南
  3. Swift2.0语言教程之函数的返回值与函数类型
  4. Dubbo负载均衡配置
  5. easyui 1.4.2 Tab刷新图标重复问题
  6. C#.NET学习笔记 -类,接口,对象
  7. 日期时间选择器-jeDate日期控件
  8. java 马士兵 io 代码分析_学习笔记-马士兵java- (IO初步)流
  9. Mysql数据库查询当前操作的数据库名
  10. DP为王——动态规划法学习笔记
  11. C++primer习题--第3章
  12. JS只能输入数字,数字和字母等的正则表达式
  13. dj鲜生-36-商品应用-其它模型类的创建-完善goods应用的数据表
  14. Linux设备驱动程序学习-Linux设备模型(总线、设备、驱动程序和类)
  15. 解决“远程会话已断开连接,因为访问被拒绝导致许可证存储的创建失败,请使用提升的权限运行远程桌面客户端”问题
  16. Qt linux双屏,qt5 多屏显示
  17. Python快速生成注释文档
  18. 浏览器URL中 encodeURIComponent()加密和decodeURIComponent()解码
  19. python视频网站项目_Python项目04 视频网站数据清洗整理和结论研究
  20. c语言如何注释一段代码,如何在C语言注释一段代码?【C++培训】

热门文章

  1. CSharp再爱我一次,你真的懂泛型吗?
  2. 给我教小学的女友写了个自动批改作业的脚本。她好像更爱我了。
  3. 【Stream—7】NetworkStream相关知识分享
  4. 如何快速通过微软PL-100考试
  5. 公文识别开发包软件简介
  6. 四磺基铝酞菁(AlS4 Pc),酞青铜相对分子质量,齐岳生物供应
  7. 命题和命题的否定可以同时成立吗?
  8. define AR_DLL_API __declspec(dllexport) 问题
  9. 已备案未注册老域名挖掘工具
  10. 【PAT】Python 1006 换个格式输出整数