背景
咖啡酸盐可帮助用户开始阅读有关其感兴趣领域的最新发展,而不会被社交媒体分散注意力或在电子邮件,通知或什至无休止地滚动新闻应用程序中淹没自己。 Coffeemate每天早上向注册用户发送一封电子邮件,其中包含他们选择的类别的新闻文章来做到这一点。
我们将HTML,CSS和Vanilla JS用于前端和Node.js,Express.js和Firebase的后端。
指示
第1部分:后端
在这一部分中,我们将设置应用程序的后端,该应用程序涉及获取新闻数据并通过Courier发送新闻数据。
我们将使用node.js
来为后端运行我们的JavaScript代码。如果您没有安装节点,请从here安装它。您可以下载当前最新版本。
- 初始化项目
制作带有您选择的名称的项目文件夹。
打开您选择的代码编辑器中的文件夹,并在项目目录运行npm init --y
中使用终端。
您现在应该有一个package.json文件。
在终端中,运行命令git init
以启动该项目的git。如果您在本地计算机上没有安装git
,请从here
- 添加依赖项
在您的package.json文件中,添加以下字段,这是我们要为我们的项目安装的依赖项
"dependencies": {
"dotenv": "^16.0.2",
"express": "^4.18.1",
"express-validator": "^6.14.2",
"firebase-admin": "^11.0.1",
"node-fetch": "^2.6.7"
}
确保您在终端中的项目文件夹中,并运行npm i
以安装我们刚刚添加的依赖项。
命令完成运行后,我们的项目目录中将有一个node_modules
文件夹,其中包含我们安装的所有软件包的代码。我们不需要将其添加到git,因为我们可以随时使用npm i
package.json
文件安装这些软件包
要从git中删除node_modules,在项目目录中制作一个.gitignore
文件,然后在其中编写node_modules
。
- 获取新闻
在您的项目文件夹中制作一个名为.env
的文件,然后将其添加到gitignore
。
我们将将我们的秘密键存储为该文件中的变量,因此它们不会被添加到git并保持私密。这些变量称为环境变量。
我们将使用https://newsapi.org/提供的新闻API。在他们的网站上注册一个帐户,您将收到一个API密钥。
将此API键添加到您的.env文件
NEWS_API_KEY="your-api-key-here"
我们将创建一个名为apiHandler.js
的文件,以管理与API交互的函数。
在apiHandlers.js
中,添加这两行代码
const fetch = require('node-fetch') // used to make HTTP requests to our news API
require('dotenv').config() // enables us to use our private environment variables
新闻API根据我们提供的类别获取新闻。它只能一次为一个类别获取新闻。
因此,如果我们想要多个类别的新闻,我们必须对每个类别进行API调用。
我们将定义一个fetchSingleCategoryNews
函数以获取单个类别的新闻。
在apiHandlers.js
中添加以下功能
const fetchSingleCategoryNews = async () => {
const news_api = `https://newsapi.org/v2/top-headlines?country=us&category=general&apiKey=${process.env.NEWS_API_KEY}`
const res = await fetch(news_api)
if (!res.ok) throw new Error("News API is not working")
const json = await res.json()
console.log(json)
}
在apiHandlers.js
中也添加函数调用,因此我们可以检查我们的新闻API是否正常工作
fetchSingleCategoryNews()
确保您在终端中的项目目录中并运行node apiHandlers.js
您应该看到一个包含新闻文章列表的对象。
现在,我们需要获取多个类别的新闻,并将它们组合到包含5个新闻的新闻对象中。
删除函数调用fetchSingleCategoryNews()
,而是在我们的fetchSingleCategoryNews
函数之后添加此函数。
const fetchNews = async (categories = ['general', 'business']) => {
let news = []
let newsToSend = 5
try {
// fetch n number of news for each category and put it in a news list such that we have 5 news in total
newsSent = 0
for (i in categories) {
newsSent = Math.floor(newsToSend / (categories.length - i))
newsToSend = newsToSend - newsSent
news = news.concat(await fetchSingleCategoryNews(categories[i], newsSent))
}
} catch (error) {
console.log(error)
throw new Error(error)
}
// transform the news list to an object which we will be using to fill data in our email
let news_obj = {}
for (i in news) {
news_obj[i] = {}
news_obj[i]["title"] = news[i]["title"]
news_obj[i]["description"] = news[i]["description"]
news_obj[i]["link"] = news[i]["url"]
}
console.log(news_obj)
}
现在,我们将更改我们的fetchSingleCategoryNews
功能以采用类别和新闻参数并返回新闻
const fetchSingleCategoryNews = async (category, num) => {
const news_api = `https://newsapi.org/v2/top-headlines?country=us&category=${category}&apiKey=${process.env.NEWS_API_KEY}`
const res = await fetch(news_api)
if (!res.ok) throw new Error("News API is not working")
const json = await res.json()
return json.articles.slice(0, Math.min(json.articles.length, num))
}
添加fetchNews
的函数调用
fetchNews()
运行node apiHandlers.js
您现在可以在终端中看到我们5个新闻的新闻对象。
- 使用快递发送新闻
我们需要做的第一件事是使用Gmail频道设置快递帐户。
- 转到https://app.courier.com并创建一个新的秘密工作区
- 对于入职过程,请选择电子邮件频道,然后让Courier和Node.js构建并构建。从Gmail API开始,因为仅需几秒即可设置。我们需要做的只是通过Gmail登录。现在,API准备发送消息。
- 复制启动代码,这是使用卷发的基本API调用,并将其粘贴到新的终端中。它已经保存了您的API密钥,知道您要发送的电子邮件地址,并且已经内置了一条消息。
- 一旦您可以在网站上看到跳舞鸽子,就可以使用快递
在快递仪表板上,转到您在左面板上找到的设计师部分。
单击Create a Notification
。
选择Email
频道,然后单击右上角的Publish Changes
按钮。
发布更改后,单击左侧栏的电子邮件图标以创建一个新的电子邮件模板。
我们需要制作一个看起来像这样的模板
从底部工具栏中选择“ T”符号,该符号将在模板中添加文本块。
单击文本块,然后从文本块上方的按钮将文本样式更改为H1。
将此文本添加到它
Good Morning, {name}! Here's your daily dose of news.
注意:我们将使用Curly Braces {}在模板中使用变量
接下来,制作一个新的文本块并在其中复制
1. {0.title}
Description: {0.description}
Link: {0.link}
2. {1.title}
Description: {1.description}
Link: {1.link}
3. {2.title}
Description: {2.description}
Link: {2.link}
4. {3.title}
Description: {3.description}
Link: {3.link}
5. {4.title}
Description: {4.description}
Link: {4.link}
之后,制作一个新的动作块,这是新的文本块按钮之后的第二个按钮。
这将是我们的退订按钮。
单击块中的按钮两次,您将看到一个带有两个字段的模态。
在第一个字段和第二个字段中写下Unsubscribe
,添加此行https://yourdomainname.com/unsubscribe?email={email}
托管我们的应用程序后,您可以更改域名。
单击发布更改。
接下来,单击左上角的设置(齿轮图标),然后单击通知ID复制它。
将此ID保存在我们的.env
文件中,为TEMPLATE_ID
接下来,前往Courier Send API documentation并登录如果您没有登录。
从页面的右侧复制验证令牌,并将其添加到我们的.env
文件中,为COURIER_API_KEY
现在我们都设置了使用快递API!
在.env
中添加一个称为TEST_EMAIL
的变量。我们将使用此电子邮件收到测试通知。
删除fetchNews()
函数调用添加此功能到apiHandlers.js
const sendNews = async (news_obj, email = process.env.TEST_EMAIL, name = "John") => {
// options for Courier API
const options = {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
Authorization: 'Bearer ' + process.env.COURIER_API_KEY
},
body: JSON.stringify({
"message": {
"to": {
email,
"data": { ...news_obj, email, name }
},
"template": process.env.TEMPLATE_ID
}
})
}
const res = await fetch('https://api.courier.com/send', options)
if (!res.ok) throw new Error("Courier API is not working")
}
在fetchNews
函数中,添加以下行,以便我们可以将新闻对象作为返回值
return news_obj
现在,我们将结合获取新闻的两个功能,并使用函数fetchNewsAndSendEmail
发送电子邮件。将其添加到apiHandlers.js
。
const fetchNewsAndSendEmail = async (email, name, categories) => {
categories = [...new Set(categories)] // making sure there are no duplicate categories
let news_obj
// we will make 5 tries to fetch news and send news incase there is any error from api
for (let i = 0; i < 5; i++) {
try {
news_obj = await fetchNews(categories)
sendNews(news_obj, email, name.split(" ")[0])
break
} catch (err) {
console.log(err)
}
}
}
将功能调用添加到fetchNewsAndSendEmail
fetchNewsAndSendEmail(process.env.TEST_EMAIL, "John", ['general', 'health'])
运行node apiHandler.js
您应该在测试电子邮件中收到一封电子邮件,该电子邮件看起来像
现在,我们将删除fetchNewsAndSendEmail
的函数调用,而是将此代码行添加到apiHandlers.js
,以便我们可以在另一个文件中使用我们的fetchNewsAndSendEmail
函数。
module.exports = fetchNewsAndSendEmail
- 添加用户和用户信息
我们将使用firebase作为云数据库存储用户信息
设置Firebase:
- In Firebase console, click
Add Project
,然后按照屏幕上的说明创建一个firebase项目。 - 导航到Firebase Console的Cloud Firestore部分。提示您选择现有的火箱项目。关注数据库创建工作流程。
- 选择测试模式并完成设置。
获取Firebase服务帐户密钥:
- 转到Google Cloud Console,然后转到您刚刚制作的项目。
- 单击IAM&Admin
- 单击左侧栏的服务帐户
- 单击名称为firebase-adminsdk的字段
- 转到钥匙部分
- 单击添加键 - >创建新键
- 选择JSON,然后单击创建
将下载一个JSON文件。
将此文件复制到我们的项目目录,并将其重命名为google-credentials.json
将google-credentials.json
添加到.gitignore
制作一个名为db.js
的新文件
将此代码添加到其中。这是我们的Firestore数据库的配置。
const { initializeApp, cert } = require('firebase-admin/app')
const { getFirestore } = require('firebase-admin/firestore')
initializeApp({ credential: cert(require('./google-credentials.json')) })
module.exports = getFirestore()
- 设置Express
我们将使用Expressjs管理服务器和路由
在项目文件夹中创建一个名为index.js
的文件,然后将以下行添加到
const express = require('express')
const { body, validationResult } = require('express-validator')
require('dotenv').config()
const db = require('./db')
const fetchNewsAndSendEmail = require('./apiHandlers')
const port = process.env.PORT || 3000
const app = express()
app.use(express.static("public")) // this is where our html and css code will reside which we will add later
app.use(express.urlencoded({ extended: true }))
接下来,我们需要创建一个路由,该路由将处理包含用户信息的传入表单
将此代码添加到index.js
app.post('/',
// data validation
body('email').trim().rtrim().notEmpty().withMessage('Email is empty').isEmail().withMessage('Invalid email'),
body('name').trim().rtrim().notEmpty().withMessage('Name is empty').isLength({ max: 100 }),
body('checkbox-categories').isArray({ min: 1, max: 5 }).withMessage('Categories should be an array with length between 1 and 5 inclusive'),
body('checkbox-categories.*').isIn(['sports', 'technology', 'science', 'business', 'health']).withMessage('Invalid categories'),
// handling the request
async (req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) return res.status(400).json({ errors: errors.array() });
try {
const data = req.body
const doc = db.collection('users').doc(data.email);
await doc.set(data)
res.send('Record saved successfully')
// send sample email
fetchNewsAndSendEmail(data.email, data.name, data["checkbox-categories"])
} catch (error) {
res.status(400).send(error.message)
}
})
app.listen(port)
运行node index.js
要测试我们的代码是否有效,我们将使用Postman向我们的服务器提出发布请求。
点击发送
您应该收到一个回答说Record saved successfully
您可以转到Firebase控制台并查看新添加的数据。
现在,我们将添加/unsubscribe
路由处理程序,用于管理请求以退订用户。
将以下代码添加到index.js
app.get('/unsubscribe', async (req, res) => {
// we will be using the email query parameter that we pass in the request as done during setting up the email template no courier
const email = req.query.email ? req.query.email : "default"
try {
await db.collection('users').doc(email).delete()
res.status(200).send(`Unsubscribed ${email} successfully!`)
} catch (error) {
res.status(400).send(error.message)
}
})
运行node index.js
使用Postman到http://localhost:3000/unsubscribe?email=registeredemail
制作get request
您应该看到注册用户已从您的firestore数据库中删除
- cron工作发送每日电子邮件
现在,为了确保用户每天收到一封电子邮件,我们需要设置一个CRON作业来执行此过程。
我们使用onrender托管我们的网站,并且由于它不始终保持托管应用程序的活动状态,因此我们使用third party application而不是我们自己的服务器来运行CRON作业,该cron of the cron of the Server在我们的服务器上呼叫fetchNewsAndSenEmail
for All用户。
创建一个cronjob:
-
在cron-job.org上创建一个帐户,然后单击仪表板上的“创建Cronjob”按钮。
-
填写下图中显示的详细信息。填充“ URL”时,创建自己在URL后半部分中输入并在
.env
文件中设置其值的“ URL”时。 -
现在,我们在
index.js
中的CRON职位路线中添加了一个处理程序,以使数据库中的所有用户都调用fetchNewsAndSendEmail
函数以将电子邮件发送给所有用户。
app.get('/' + process.env.CRON_ROUTE, async (req, res) => { const snapshot = await db.collection('users').get() //gets list of all users snapshot.forEach(doc => fetchNewsAndSendEmail(doc.get('email'), doc.get('name'), doc.get('checkbox-categories'))) res.status(200).send("Emails sent!") })
第2部分:前端
要创建此项目的前端,您需要创建一个public
目录,该目录将包含所有代码来创建用户界面。
-
在
public
目录中,我们创建了一个assets文件夹,其中包含我们网站显示的图像。我们有两个背景图像 - 一个用于桌面网络屏幕,另一个用于移动网络屏幕 - 此文件夹中的一个徽标图像。 -
从here文件中复制
index.html
文件,并在public
目录中粘贴。
在<head>
标签中,您可以将项目名称添加到标题标签中。
现在我们的<body>
包括:
- 由标题和徽标组成的标题。
- 网站的内容,即标题和表格。
- 我们的表格包含两个输入字段,以获取用户的姓名和电子邮件,由用户可以订阅的不同新闻类别组成的复选框以及允许用户提交表单并注册每日新闻通知的注册按钮。
- 页脚
我们的index.html文件还包含代码来处理我们的表单提交。在<script>
标签中,我们创建两个函数。
我们的第一个函数handleData()
,请确保如果用户尝试注册而不选择新闻类别,则表单不会提交,并且用户的屏幕将显示一个错误,要求他们选择至少一个类别。如果用户试图适当地填写所有字段,我们通过向路由“/”提交表格,重置我们的表单以清除所有输入字段,并在屏幕上显示成功消息以告知用户用户该表格已成功提交。
function handleData(event){
const form_data = new FormData(document.querySelector("form"));
const form = document.getElementById("myForm")
//if no category has been selected
if(!form_data.has("checkbox-categories[]")){
document.getElementById("error").style.visibility = "visible"
return false
}
else {
form.submit()
form.reset()
document.getElementById("submitted").style.visibility = "visible"
return false
}
}
我们的第二个函数checkError()
确保如果用户选择类别复选框,则错误消息(如果存在)应该消失。我们通过在每个复选框上设置一个onclick侦听器来做到这一点。当用户单击复选框时,为复选框的那个实例调用了CheckError函数。如果检查了复选框,则该功能可确保未显示错误消息。
function checkError(box) {
if (box.checked == true){
document.getElementById("error").style.visibility = "hidden"
}
}
我们使用reset.css来防止不同的Broswers的默认样式影响我们的网页样式。
使用node index.js
运行服务器,您应该在https://localhost:3000
您可以使用您选择的任何托管服务部署此Web应用程序。不要忘记将
.env
和google-credentials
文件添加到您的托管项目中。
结论
该网站现在已经准备就绪,您可以按照以下步骤向用户发送每日新闻通知!
关于作者
Kirtan Desai对软件开发和开源软件充满热情,并且喜欢活跃在开发社区中。他在弗吉尼亚理工大学的CGIT担任开发商,在那里构建Fullstack GIS Web应用程序。他目前正在攻读计算机科学硕士学位,并希望从事软件开发职业。
Tanvi Khosla是Virginia Tech的CS研究生,试图从事软件工程作为职业。她目前在弗吉尼亚理工大学的IT部门网络基础架构和服务团队中担任软件开发人员/研究生助理。她喜欢通过学习新工具和技术以及创建新项目来从事前端开发和扩大技能。