使用 Node.js 构建 URL 缩短器
由 Mux 赞助的 DEV 全球展示挑战赛:展示你的项目!
作者:Subha Chanda ✏️
像Bitly和Cuttly这样的网址缩短服务非常流行。在本文中,我们将创建一个类似的工具,通过构建一个 API 服务来缩短用户提供的网址。
本项目将使用MongoDB和Node.js,因此您应该具备它们的基本知识才能学习本教程。
在Node.js中规划URL缩短器构建流程
我们先来规划一下构建流程,这非常简单。对于传递给我们 API 的每个 URL,我们会生成一个唯一的 ID,并以此创建一个短 URL。然后,长 URL、短 URL 和唯一 ID 将被存储在数据库中。
当用户GET向短链接发送请求时,系统会在数据库中查找该链接,并将用户重定向到对应的原始链接。听起来很复杂?别担心,我们会讲解您需要了解的一切。
初始化应用程序并安装 MongoDB 依赖项
首先,我们需要一个数据库。因为我们将使用 MongoDB,所以需要一个 MongoDB SRV URI。您可以点击此链接创建数据库。下一步是使用 NPM 初始化项目文件夹。
让我们使用项目目录中的命令npm init进行初始化。项目初始化完成后,我们将安装所需的依赖项。我们需要的依赖项有:
.envdotenv:此软件包从名为to 的文件中加载环境变量。process.env- Express.js:这是一个基于 Node.js 的简洁而灵活的 Web 应用程序框架。
- Mongoose:这是一个用于 Node.js 的 MongoDB 对象建模工具。
- ShortId:此软件包允许我们为 URL 生成短 ID。
我们唯一需要的开发者依赖项是 nodemon。nodemon 是一个简单的工具,它会在文件发生更改时自动重启 Node.js 服务器。
现在,我们来安装依赖项。要安装应用程序所需的依赖项,我们将使用以下命令:
npm i dotenv express mongoose shortid
安装完所有依赖项后,我们将安装开发者依赖项:
npm i -D nodemon
让我们使用 Express 在文件中创建服务器app.js。要设置 Express 服务器,我们需要将 Express 包导入到文件中app.js。导入包后,对其进行初始化并将其存储在一个名为 `<variable_name>` 的变量中app。
现在,使用现有listen函数创建服务器。以下是一个示例。
const Express = require('Express');
const app = Express();
// Server Setup
const PORT = 3333;
app.listen(PORT, () => {
console.log(`Server is running at PORT ${PORT}`);
});
我使用端口3333来运行服务器。Expresslisten中的该方法会启动一个 UNIX 套接字,并监听指定端口上的连接。
现在,.env在该文件夹内创建一个文件config,用于存储 MongoDB SRV URI 和基本 URL。目前,基本 URL 将是您的本地主机服务器位置。以下是我的.env文件代码:
MONGO_URI=mongodb+srv://nemo:YourPasswordHere@cluster0.mkws3.mongodb.net/myFirstDatabase?retryWrites=true&w=majority
BASE=http://localhost:3333
请记住将<password>MongoDB URI 中的字段替换为您的数据库密码。
将数据库连接到应用程序
现在,我们将数据库连接到应用程序。为此,请将 Mongoose 和 dotenv 依赖项导入到db.js位于该config文件夹内的文件中。
const mongoose = require('mongoose');
require('dotenv').config({ path: './.env' });
由于该文件不在根目录下,因此需要将对象键传递到 dotenv 配置中。我们通过这种方式传递文件path的位置。.env.env
connectDB现在,在名为 `<filename>` 的文件夹中db.js创建一个名为 `<unsynchronized_function>` 的异步函数config。本文将使用 async/await。
const connectDB = async () => {
try {
await mongoose.connect(process.env.MONGO_URI, {
useNewUrlParser: true,
useUnifiedTopology: true,
});
console.log('Database Connected');
} catch (err) {
console.error(err.message);
process.exit(1);
}
};
module.exports = connectDB;
在这个try代码块中,我们等待 Mongoose 连接到给定的 MongoDB URI。该mongoose.connect方法的第一个参数是 MongoDB SRV URI。请注意,第二个参数中传递了两个键值对,用于消除控制台警告。让我们来了解一下这两个键值对参数的含义。
useNewUrlParser: true底层 MongoDB 驱动程序已弃用当前的连接字符串解析器。因此,它添加了一个新标志。如果连接在使用新的字符串解析器时遇到任何问题,它可以回退到旧的解析器。useUnifiedTopology: true默认情况下,此项已启用false。此处将其设置为启用true,以便可以使用 MongoDB 驱动程序的新连接管理引擎。
如果语句中出现任何错误catch,我们将向控制台输出错误信息并退出process.exit(1)。最后,我们使用 `.` 导出函数module.exports。
现在,将db.js文件导入到app.js文件中const connectDB = require('./config/db');,并调用该connectDB函数connectDB()。
在 MongoDB 中创建 Mongoose schema
我们将使用 Mongoose schema 来确定数据在 MongoDB 中的存储方式。本质上,Mongoose schema 就是一个数据模型。让我们Url.js在一个models文件夹内创建一个名为 `.mongoose.schema` 的文件。在这里导入 Mongoose,然后使用mongoose.Schema构造函数创建 schema。
const mongoose = require('mongoose');
const UrlSchema = new mongoose.Schema({
urlId: {
type: String,
required: true,
},
origUrl: {
type: String,
required: true,
},
shortUrl: {
type: String,
required: true,
},
clicks: {
type: Number,
required: true,
default: 0,
},
date: {
type: String,
default: Date.now,
},
});
module.exports = mongoose.model('Url', UrlSchema);
父对象键是将要存储在数据库中的键。我们定义每个数据键。请注意,某些键是必填字段,而其他键则有默认值。
最后,我们使用导出模式module.exports = mongoose.model('Url', UrlSchema);。其中第一个参数mongoose.model是要存储的数据的单数形式,第二个参数是模式本身。
构建 URL 和索引路由
URL路由会根据原始URL生成一个短URL并将其存储在数据库中。routes在根目录下创建一个名为`<folder_name>`的文件夹,并urls.js在其中创建一个名为`<file_name>`的文件。这里我们将使用Express路由。首先,导入所有必要的包,如下所示。
const Express = require('express');
const router = Express.Router();
const shortid = require('shortid');
const Url = require('../models/Url');
const utils = require('../utils/utils');
require('dotenv').config({ path: '../config/.env' });
utils.js文件夹内的文件包含utils一个函数,用于检查传入的 URL 是否有效。以下是该utils.js文件的代码。
function validateUrl(value) {
return /^(?:(?:(?:https?|ftp):)?\\/\\/)(?:\\S+(?::\\S*)?@)?(?:(?!(?:10|127)(?:\\.\\d{1,3}){3})(?!(?:169\\.254|192\\.168)(?:\\.\\d{1,3}){2})(?!172\\.(?:1[6-9]|2\\d|3[0-1])(?:\\.\\d{1,3}){2})(?:[1-9]\\d?|1\\d\\d|2[01]\\d|22[0-3])(?:\\.(?:1?\\d{1,2}|2[0-4]\\d|25[0-5])){2}(?:\\.(?:[1-9]\\d?|1\\d\\d|2[0-4]\\d|25[0-4]))|(?:(?:[a-z\\u00a1-\\uffff0-9]-*)*[a-z\\u00a1-\\uffff0-9]+)(?:\\.(?:[a-z\\u00a1-\\uffff0-9]-*)*[a-z\\u00a1-\\uffff0-9]+)*(?:\\.(?:[a-z\\u00a1-\\uffff]{2,})))(?::\\d{2,5})?(?:[/?#]\\S*)?$/i.test(
value
);
}
module.exports = { validateUrl };
我们将使用文件中的 HTTP POST 请求urls.js来生成详细信息并将其发布到数据库。
const Express = require('express');
const router = Express.Router();
const shortid = require('shortid');
const Url = require('../models/Url');
const utils = require('../utils/utils');
require('dotenv').config({ path: '../config/.env' });
// Short URL Generator
router.post('/short', async (req, res) => {
const { origUrl } = req.body;
const base = process.env.BASE;
const urlId = shortid.generate();
if (utils.validateUrl(origUrl)) {
try {
let url = await Url.findOne({ origUrl });
if (url) {
res.json(url);
} else {
const shortUrl = `${base}/${urlId}`;
url = new Url({
origUrl,
shortUrl,
urlId,
date: new Date(),
});
await url.save();
res.json(url);
}
} catch (err) {
console.log(err);
res.status(500).json('Server Error');
}
} else {
res.status(400).json('Invalid Original Url');
}
});
module.exports = router;
它将从 HTTP 请求体中const { origUrl } = req.body;提取origUrl值。然后,我们将基本 URL 存储到一个变量中。const urlId = shortid.generate();它还会生成一个短 ID 并将其存储到一个变量中。
生成 URL 后,我们使用utils目录中的函数检查原始 URL 是否有效。如果 URL 有效,则进入相应的try代码块。
首先,我们使用Url.findOne({ origUrl });Mongoose 方法在数据库中查找原始 URL 是否已存在。如果存在,则以 JSON 格式返回数据;否则,我们将原始 URL 和短 ID 组合成一个短 URL。
然后,我们使用 Mongoose 模型,将字段传递给模型构造函数,并通过相应url.save();方法将其保存到数据库。保存完成后,我们以 JSON 格式返回响应。
try该代码块会处理意外错误,并且如果函数catch返回无效 URL,则会返回一条消息,提示该 URL 无效。最后,我们导出路由。falsevalidateUrl
之前我们需要安装 body-parser 包,但现在它已经集成到 Express 中了。所以回到app.js文件并添加以下两行代码即可使用 body-parser:
// Body Parser
app.use(Express.urlencoded({ extended: true }));
app.use(Express.json());
这两行代码帮助我们读取传入的请求。在这两行代码之后,导入 URL 路由。
app.use('/api', require('./routes/urls'));
因为我们使用了/api端点,所以完整的端点变为http://localhost:3333/api/short。以下是一个示例。
index.js现在,在该文件夹内创建另一个文件routes来处理重定向过程。在该文件中,导入必要的依赖项。
首先,我们会搜索数据库中传入的短链接ID。如果找到该链接,我们会将其重定向到原始链接。
const Express = require('express');
const router = Express.Router();
const Url = require('../models/Url');
router.get('/:urlId', async (req, res) => {
try {
const url = await Url.findOne({ urlId: req.params.urlId });
if (url) {
url.clicks++;
url.save();
return res.redirect(url.origUrl);
} else res.status(404).json('Not found');
} catch (err) {
console.log(err);
res.status(500).json('Server Error');
}
});
module.exports = router;
HTTPGET请求借助 . 获取 URL ID :urlId。然后,在try代码块中,我们使用 . 方法查找 URL Url.findOne,类似于我们在urls.js路由中所做的。
如果找到该 URL,我们增加该 URL 的点击次数并保存点击量。最后,我们使用重定向将用户重定向到原始 URL return res.redirect(url.origUrl);。
如果找不到 URL,我们会发送一条 JSON 消息,提示 URL 未找到。任何未捕获的异常都会在该catch代码块中处理。我们会将错误记录到控制台,并发送一条“服务器错误”的 JSON 消息。最后,我们导出路由。
将路由导入app.js文件后,我们的网址缩短器即可使用。导入后,最终app.js文件内容将如下所示:
const Express = require('Express');
const app = Express();
const connectDB = require('./config/db');
require('dotenv').config({ path: './config/.env' });
connectDB();
// Body Parser
app.use(Express.urlencoded({ extended: true }));
app.use(Express.json());
app.use('/', require('./routes/index'));
app.use('/api', require('./routes/urls'));
// Server Setup
const PORT = 3333;
app.listen(PORT, () => {
console.log(`Server is running at PORT ${PORT}`);
});
结论
在本文中,我们学习了如何从零开始构建一个网址缩短服务 API。您可以将其与任何前端集成,甚至可以构建一个全栈网址缩短服务。希望您喜欢阅读本文,并从中有所收获。您可以在我的GitHub 代码库中找到完整的源代码。
仅需 200 毫秒 ✔️ 监控生产环境中失败和缓慢的网络请求
部署基于 Node 的 Web 应用或网站并不难,难的是如何确保 Node 实例持续为应用提供资源。如果您希望确保对后端或第三方服务的请求都能成功,不妨试试 LogRocket。
LogRocket就像 Web 应用的 DVR,它会记录网站上发生的一切。您无需猜测问题原因,即可汇总并报告有问题的网络请求,从而快速了解根本原因。
LogRocket 可以检测您的应用,记录页面加载时间、首字节到达时间、慢网络请求等基准性能指标,并记录 Redux、NgRx 和 Vuex 的操作/状态。立即开始免费监控。
文章来源:https://dev.to/logrocket/building-a-url-shortener-with-node-js-52oe


